Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
source 'https://rubygems.org'
source "https://rubygems.org"

gem 'covered'
gem 'bigdecimal', '>= 2', :platform => :mri
gem "covered"
gem "bigdecimal", ">= 2", platform: :mri
gem "standard"

gemspec
7 changes: 5 additions & 2 deletions lib/unitwise/expression.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# frozen_string_literal: true

require 'unitwise/expression/atomic_parser'
require 'unitwise/expression/composer'
require 'unitwise/expression/decomposer'
require 'unitwise/expression/matcher'
require 'unitwise/expression/parser'
require 'unitwise/expression/transformer'
require 'unitwise/expression/composer'
require 'unitwise/expression/decomposer'

module Unitwise
# The Expression module encompases all functions around encoding and decoding
Expand Down
102 changes: 102 additions & 0 deletions lib/unitwise/expression/atomic_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# frozen_string_literal: true

module Unitwise
module Expression
# Special parser to prioritise parsing of expressions without prefixes.
#
# This is added as prefixed matches can take precedence over non-prefixed
# :symbol matches. An example of this would be the expression "ft".
#
# # Unitwise::Expression::Parser.new.parse("ft")
# # => {
# # :left => {
# # :term => {
# # :prefix => {
# # :prefix_code => "f"@0
# # },
# # :atom => {
# # :atom_code => "t"@1
# # }
# # }
# # }
# # }
#
# Where in most common use cases, it is probably more intuitive to have "ft"
# be matched to the atom "foot":
#
# # Unitwise::Expression::AtomicParser.new(:symbol).parse("ft")
# # => {
# # :left => {
# # :term => {
# # :atom => {
# # :atom_code => "ft"@0
# # }
# # }
# # }
# # }
#
# AtomicParser should only be needed for :primary_code, :secondary_code and
# :symbol type matches
#
class AtomicParser < Parslet::Parser
attr_reader :key

def initialize(key = :primary_code)
@key = key
@atom_matcher = Matcher.atom(key)
@metric_atom_matcher = Matcher.metric_atom(key)
end

private

attr_reader :atom_matcher, :metric_atom_matcher

root :expression

rule(:atom) { atom_matcher.as(:atom_code) }
rule(:metric_atom) { metric_atom_matcher.as(:atom_code) }

rule(:basic_unit) do
metric_atom.as(:atom) | atom.as(:atom)
end

rule(:annotation) do
str('{') >> match['^}'].repeat.as(:annotation) >> str('}')
end

rule(:digits) { match['0-9'].repeat(1) }

rule(:integer) { (str('-').maybe >> digits).as(:integer) }

rule(:fixnum) do
(str('-').maybe >> digits >> str('.') >> digits).as(:fixnum)
end

rule(:number) { fixnum | integer }

rule(:exponent) { integer.as(:exponent) }

rule(:factor) { number.as(:factor) }

rule(:operator) { (str('.') | str('/')).as(:operator) }

rule(:term) do
(
((factor >> basic_unit) | basic_unit | factor) >>
exponent.maybe >>
annotation.maybe
).as(:term)
end

rule(:group) do
(factor.maybe >> str('(') >> expression.as(:nested) >> str(')') >>
exponent.maybe).as(:group)
end

rule(:expression) do
(group | term).as(:left).maybe >>
(operator >> expression.as(:right)).maybe
end
end
end
end
46 changes: 30 additions & 16 deletions lib/unitwise/expression/decomposer.rb
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
# frozen_string_literal: true

module Unitwise
module Expression
# The decomposer is used to turn string expressions into collections of
# terms. It is responsible for executing the parsing and transformation
# of a string, as well as caching the results.
class Decomposer

MODES = [:primary_code, :secondary_code, :names, :slugs, :symbol].freeze
ATOMIC_MODES = %i[primary_code secondary_code symbol].freeze
MODES = %i[primary_code secondary_code names slugs symbol].freeze

class << self

# Parse an expression to an array of terms and cache the results
def parse(expression)
expression = expression.to_s

if cache.key?(expression)
cache[expression]
elsif decomposer = new(expression)
elsif (decomposer = new(expression))
cache[expression] = decomposer
end
end

def parsers
@parsers ||= MODES.reduce({}) do |hash, mode|
hash[mode] = Parser.new(mode); hash
return @parsers if !@parsers.nil? && @parsers.any?

@parsers = ATOMIC_MODES.each_with_object({}) do |mode, hash|
hash[AtomicParser.new(mode)] = mode
end

MODES.each { |mode| @parsers[Parser.new(mode)] = mode }

@parsers
end

def transformer
Expand Down Expand Up @@ -50,31 +58,37 @@ def reset

def initialize(expression)
@expression = expression.to_s

if expression.empty? || terms.nil? || terms.empty?
fail(ExpressionError, "Could not evaluate '#{ expression }'.")
fail(ExpressionError, "Could not evaluate '#{expression}'.")
end
end

def parse
self.class.parsers.reduce(nil) do |_, (mode, parser)|
parsed = parser.parse(expression) rescue next
self.class.parsers.reduce(nil) do |_, (parser, mode)|
begin
parsed = parser.parse(expression)
rescue Parslet::ParseFailed
next nil
end

@mode = mode
break parsed
end
end

def transform
@transform ||= self.class.transformer.apply(parse, :mode => mode)
@transform ||= self.class.transformer.apply(parse, mode: mode)
end

def terms
@terms ||= if transform.respond_to?(:terms)
transform.terms
else
Array(transform)
end
@terms ||=
if transform.respond_to?(:terms)
transform.terms
else
Array(transform)
end
end

end
end
end
46 changes: 24 additions & 22 deletions lib/unitwise/expression/parser.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# frozen_string_literal: true

module Unitwise
module Expression
# Parses a string expression into a hash tree representing the
# expression's terms, prefixes, and atoms.
class Parser < Parslet::Parser
attr_reader :key

def initialize(key = :primary_code)
@key = key
@atom_matcher = Matcher.atom(key)
Expand All @@ -17,49 +20,48 @@ def initialize(key = :primary_code)

root :expression

rule (:atom) { atom_matcher.as(:atom_code) }
rule (:metric_atom) { metric_atom_matcher.as(:atom_code) }
rule (:prefix) { prefix_matcher.as(:prefix_code) }
rule(:atom) { atom_matcher.as(:atom_code) }
rule(:metric_atom) { metric_atom_matcher.as(:atom_code) }
rule(:prefix) { prefix_matcher.as(:prefix_code) }

rule (:simpleton) do
(prefix.as(:prefix) >> metric_atom.as(:atom) | atom.as(:atom))
rule(:basic_unit) do
prefix.as(:prefix) >> metric_atom.as(:atom) | atom.as(:atom)
end

rule (:annotation) do
str('{') >> match['^}'].repeat.as(:annotation) >> str('}')
rule(:annotation) do
str("{") >> match["^}"].repeat.as(:annotation) >> str("}")
end

rule (:digits) { match['0-9'].repeat(1) }
rule(:digits) { match["0-9"].repeat(1) }

rule (:integer) { (str('-').maybe >> digits).as(:integer) }
rule(:integer) { (str("-").maybe >> digits).as(:integer) }

rule (:fixnum) do
(str('-').maybe >> digits >> str('.') >> digits).as(:fixnum)
rule(:fixnum) do
(str("-").maybe >> digits >> str(".") >> digits).as(:fixnum)
end

rule (:number) { fixnum | integer }
rule(:number) { fixnum | integer }

rule (:exponent) { integer.as(:exponent) }
rule(:exponent) { integer.as(:exponent) }

rule (:factor) { number.as(:factor) }
rule(:factor) { number.as(:factor) }

rule (:operator) { (str('.') | str('/')).as(:operator) }
rule(:operator) { (str(".") | str("/")).as(:operator) }

rule (:term) do
((factor >> simpleton | simpleton | factor) >>
rule(:term) do
((factor >> basic_unit | basic_unit | factor) >>
exponent.maybe >> annotation.maybe).as(:term)
end

rule (:group) do
(factor.maybe >> str('(') >> expression.as(:nested) >> str(')') >>
rule(:group) do
(factor.maybe >> str("(") >> expression.as(:nested) >> str(")") >>
exponent.maybe).as(:group)
end

rule (:expression) do
((group | term).as(:left)).maybe >>
rule(:expression) do
(group | term).as(:left).maybe >>
(operator >> expression.as(:right)).maybe
end

end
end
end
16 changes: 8 additions & 8 deletions lib/unitwise/unit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ def initialize(input)
# @return [Array]
# @api public
def terms
unless frozen?
unless @terms
decomposer = Expression.decompose(@expression)
@mode = decomposer.mode
@terms = decomposer.terms
end
freeze
end
return @terms if frozen?
return @terms if !@terms.nil? && @terms.any?

decomposer = Expression.decompose(@expression)
@mode = decomposer.mode
@terms = decomposer.terms
freeze

@terms
end

Expand Down
Loading