diff --git a/Gemfile b/Gemfile index 55bcf16..e0e6749 100644 --- a/Gemfile +++ b/Gemfile @@ -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 diff --git a/lib/unitwise/expression.rb b/lib/unitwise/expression.rb index a7064ea..47fce6f 100644 --- a/lib/unitwise/expression.rb +++ b/lib/unitwise/expression.rb @@ -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 diff --git a/lib/unitwise/expression/atomic_parser.rb b/lib/unitwise/expression/atomic_parser.rb new file mode 100644 index 0000000..aa1b3f7 --- /dev/null +++ b/lib/unitwise/expression/atomic_parser.rb @@ -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 diff --git a/lib/unitwise/expression/decomposer.rb b/lib/unitwise/expression/decomposer.rb index ec93857..44edd3b 100644 --- a/lib/unitwise/expression/decomposer.rb +++ b/lib/unitwise/expression/decomposer.rb @@ -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 @@ -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 diff --git a/lib/unitwise/expression/parser.rb b/lib/unitwise/expression/parser.rb index 82226a2..cc23096 100644 --- a/lib/unitwise/expression/parser.rb +++ b/lib/unitwise/expression/parser.rb @@ -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) @@ -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 diff --git a/lib/unitwise/unit.rb b/lib/unitwise/unit.rb index d8ad8be..c395443 100644 --- a/lib/unitwise/unit.rb +++ b/lib/unitwise/unit.rb @@ -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 diff --git a/test/unitwise/expression/atomic_parser_test.rb b/test/unitwise/expression/atomic_parser_test.rb new file mode 100644 index 0000000..b297047 --- /dev/null +++ b/test/unitwise/expression/atomic_parser_test.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true + +require "test_helper" + +describe Unitwise::Expression::AtomicParser do + describe "when parser is initialized with :primary_code as key" do + subject { Unitwise::Expression::AtomicParser.new } + + describe "#metric_atom" do + it "must match 'N'" do + _(subject.metric_atom.parse("N")[:atom_code]).must_equal("N") + end + + it "must match 't'" do + _(subject.atom.parse("t")[:atom_code]).must_equal("t") + end + end + + describe "#atom" do + it "must match '[in_i]'" do + _(subject.atom.parse("[in_i]")[:atom_code]).must_equal("[in_i]") + end + + it "must match '[ft_i]'" do + _(subject.atom.parse("[ft_i]")[:atom_code]).must_equal("[ft_i]") + end + end + + describe "#annotation" do + it "must match '{foobar}'" do + _(subject.annotation.parse("{foobar}")[:annotation]).must_equal("foobar") + end + end + + describe "#factor" do + it "must match positives and fixnums" do + _(subject.factor.parse("3.2")[:factor]).must_equal(fixnum: "3.2") + end + + it "must match negatives and integers" do + _(subject.factor.parse("-5")[:factor]).must_equal(integer: "-5") + end + end + + describe "#exponent" do + it "must match positives integers" do + _(subject.exponent.parse("4")[:exponent]).must_equal(integer: "4") + end + + it "must match negative integers" do + _(subject.exponent.parse("-5")[:exponent]).must_equal(integer: "-5") + end + end + + describe "term" do + it "must match basic atoms" do + _(subject.term.parse("[in_i]")[:term][:atom][:atom_code]).must_equal("[in_i]") + end + + it "must not match prefixed terms" do + assert_raises Parslet::ParseFailed do + subject.term.parse("cm3")[:term] + end + end + + it "must match exponential atoms" do + match = subject.term.parse("m3")[:term] + _(match[:atom][:atom_code]).must_equal "m" + _(match[:exponent][:integer]).must_equal "3" + end + + it "must match factors" do + _(subject.term.parse("3.2")[:term][:factor][:fixnum]).must_equal "3.2" + end + + it "must match annotations" do + match = subject.term.parse("N{Normal}")[:term] + _(match[:atom][:atom_code]).must_equal "N" + _(match[:annotation]).must_equal "Normal" + end + end + + describe "#group" do + it "must not match prefixed term within parentheses" do + assert_raises Parslet::ParseFailed do + subject.term.parse("(kg)") + end + end + + it "must match parentheses with a term" do + match = subject.group.parse("(s2)")[:group][:nested][:left][:term] + _(match[:atom][:atom_code]).must_equal "s" + _(match[:exponent][:integer]).must_equal "2" + end + + it "must not match nested groups with prefix" do + assert_raises Parslet::ParseFailed do + subject.term.parse("((kg))") + end + end + + it "must match nested groups" do + match = subject.group.parse("((g))")[:group][:nested][:left][:group][:nested][:left][:term] + _(match[:atom][:atom_code]).must_equal "g" + end + + it "must pass exponents down" do + match = subject.group.parse("([in_i])3")[:group] + _(match[:exponent][:integer]).must_equal "3" + _(match[:nested][:left][:term][:atom][:atom_code]).must_equal "[in_i]" + end + + it "must not match prefixed terms with exponents" do + assert_raises Parslet::ParseFailed do + subject.term.parse("kg2") + end + end + end + + describe "#expression" do + it "must not match prefixed expressions" do + assert_raises Parslet::ParseFailed do + subject.term.parse("ft/s") + end + + assert_raises Parslet::ParseFailed do + subject.term.parse("km/s") + end + end + + it "must match left only" do + match = subject.expression.parse("m") + _(match[:left][:term][:atom][:atom_code]).must_equal("m") + end + + it "must match left + right + operator" do + match = subject.expression.parse("m.s") + _(match[:left][:term][:atom][:atom_code]).must_equal("m") + _(match[:operator]).must_equal(".") + _(match[:right][:left][:term][:atom][:atom_code]).must_equal("s") + end + + it "must match operator + right" do + match = subject.expression.parse("/s") + _(match[:operator]).must_equal("/") + _(match[:right][:left][:term][:atom][:atom_code]).must_equal("s") + end + end + end + + describe "when parser is initialized with :symbol as key" do + subject { Unitwise::Expression::AtomicParser.new(:symbol) } + + describe "#metric_atom" do + it "must match 'N'" do + _(subject.metric_atom.parse("N")[:atom_code]).must_equal("N") + end + end + + describe "#atom" do + it "must match 'in'" do + _(subject.atom.parse("in")[:atom_code]).must_equal("in") + end + + it "must match 'ft'" do + _(subject.atom.parse("ft")[:atom_code]).must_equal("ft") + end + end + + describe "#expression" do + it "must match left only" do + match = subject.expression.parse("ft") + _(match[:left][:term][:atom][:atom_code]).must_equal("ft") + end + + it "must match left + right + operator" do + match = subject.expression.parse("ft.s") + _(match[:left][:term][:atom][:atom_code]).must_equal("ft") + _(match[:operator]).must_equal(".") + _(match[:right][:left][:term][:atom][:atom_code]).must_equal("s") + end + + it "must match operator + right" do + match = subject.expression.parse("/s") + _(match[:operator]).must_equal("/") + _(match[:right][:left][:term][:atom][:atom_code]).must_equal("s") + end + end + end +end diff --git a/test/unitwise/expression/decomposer_test.rb b/test/unitwise/expression/decomposer_test.rb index 489ecf6..2586143 100644 --- a/test/unitwise/expression/decomposer_test.rb +++ b/test/unitwise/expression/decomposer_test.rb @@ -1,4 +1,6 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" describe Unitwise::Expression::Decomposer do subject { Unitwise::Expression::Decomposer } @@ -24,10 +26,25 @@ saff = subject.new("gn").terms _(saff.count).must_equal 1 end + it "should accept exact symbol match over prefixed matches" do + ft = subject.new("ft").terms + _(ft.count).must_equal 1 + _(ft.first.atom.primary_code).must_equal "[ft_i]" + end + it "should accept prefixed units" do + kg = subject.new("kg").terms + _(kg.count).must_equal 1 + _(kg.first.prefix.primary_code).must_equal "k" + end it "should accept complex units" do complex = subject.new("(mg.(km/s)3/J)2.Pa").terms _(complex.count).must_equal 5 end + it "should accept complex units with symbol match over prefixed match" do + complex = subject.new("((ft/s)3/J)2.Pa").terms + _(complex.count).must_equal 4 + _(complex.first.atom.primary_code).must_equal "[ft_i]" + end it "should accept more complex units" do complex = subject.new("4.1(mm/2s3)4.7.3J-2").terms _(complex.count).must_equal 3 @@ -36,10 +53,14 @@ frequency = subject.new("/s").terms _(frequency.count).must_equal 1 end + it "should accept weird exact symbol match units over prefixed match" do + per_foot = subject.new("/ft").terms + _(per_foot.count).must_equal 1 + _(per_foot.first.atom.primary_code).must_equal "[ft_i]" + end it "should accept units with a factor and unit" do oddity = subject.new("2ms2").terms _(oddity.count).must_equal 1 end end - end diff --git a/test/unitwise/expression/parser_test.rb b/test/unitwise/expression/parser_test.rb index 33d7f26..eca7417 100644 --- a/test/unitwise/expression/parser_test.rb +++ b/test/unitwise/expression/parser_test.rb @@ -1,109 +1,174 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" describe Unitwise::Expression::Parser do - subject { Unitwise::Expression::Parser.new} - describe '#metric_atom' do - it "must match 'N'" do - _(subject.metric_atom.parse('N')[:atom_code]).must_equal('N') - end - end + describe "when parser is initialized with :primary_code as key" do + subject { Unitwise::Expression::Parser.new } - describe '#atom' do - it "must match '[in_i]'" do - _(subject.atom.parse('[in_i]')[:atom_code]).must_equal('[in_i]') - end - end + describe "#metric_atom" do + it "must match 'N'" do + _(subject.metric_atom.parse("N")[:atom_code]).must_equal("N") + end - describe '#prefix' do - it "must match 'k'" do - _(subject.prefix.parse('k')[:prefix_code]).must_equal('k') + it "must match 't'" do + _(subject.atom.parse("t")[:atom_code]).must_equal("t") + end end - end - describe '#annotation' do - it "must match '{foobar}'" do - _(subject.annotation.parse('{foobar}')[:annotation]).must_equal('foobar') - end - end + describe "#atom" do + it "must match '[in_i]'" do + _(subject.atom.parse("[in_i]")[:atom_code]).must_equal("[in_i]") + end - describe "#factor" do - it "must match positives and fixnums" do - _(subject.factor.parse('3.2')[:factor]).must_equal(:fixnum => '3.2') + it "must match '[ft_i]'" do + _(subject.atom.parse("[ft_i]")[:atom_code]).must_equal("[ft_i]") + end end - it "must match negatives and integers" do - _(subject.factor.parse('-5')[:factor]).must_equal(:integer => '-5') - end - end - describe "#exponent" do - it "must match positives integers" do - _(subject.exponent.parse('4')[:exponent]).must_equal(:integer => '4') + describe "#prefix" do + it "must match 'k'" do + _(subject.prefix.parse("k")[:prefix_code]).must_equal("k") + end + + it "must match 'f'" do + _(subject.prefix.parse("f")[:prefix_code]).must_equal("f") + end end - it "must match negative integers" do - _(subject.exponent.parse('-5')[:exponent]).must_equal(:integer => '-5') + + describe "#annotation" do + it "must match '{foobar}'" do + _(subject.annotation.parse("{foobar}")[:annotation]).must_equal("foobar") + end end - end - describe "term" do - it "must match basic atoms" do - _(subject.term.parse('[in_i]')[:term][:atom][:atom_code]).must_equal('[in_i]') + describe "#factor" do + it "must match positives and fixnums" do + _(subject.factor.parse("3.2")[:factor]).must_equal(fixnum: "3.2") + end + + it "must match negatives and integers" do + _(subject.factor.parse("-5")[:factor]).must_equal(integer: "-5") + end end - it "must match prefixed atoms" do - match = subject.term.parse('ks')[:term] - _(match[:atom][:atom_code]).must_equal('s') - _(match[:prefix][:prefix_code]).must_equal('k') + + describe "#exponent" do + it "must match positives integers" do + _(subject.exponent.parse("4")[:exponent]).must_equal(integer: "4") + end + + it "must match negative integers" do + _(subject.exponent.parse("-5")[:exponent]).must_equal(integer: "-5") + end end - it "must match exponential atoms" do - match = subject.term.parse('cm3')[:term] - _(match[:atom][:atom_code]).must_equal 'm' - _(match[:prefix][:prefix_code]).must_equal 'c' - _(match[:exponent][:integer]).must_equal '3' + + describe "term" do + it "must match basic atoms" do + _(subject.term.parse("[in_i]")[:term][:atom][:atom_code]).must_equal("[in_i]") + end + + it "must match prefixed atoms" do + match = subject.term.parse("ks")[:term] + _(match[:atom][:atom_code]).must_equal("s") + _(match[:prefix][:prefix_code]).must_equal("k") + end + + it "must match exponential atoms" do + match = subject.term.parse("cm3")[:term] + _(match[:atom][:atom_code]).must_equal "m" + _(match[:prefix][:prefix_code]).must_equal "c" + _(match[:exponent][:integer]).must_equal "3" + end + + it "must match factors" do + _(subject.term.parse("3.2")[:term][:factor][:fixnum]).must_equal "3.2" + end + + it "must match annotations" do + match = subject.term.parse("N{Normal}")[:term] + _(match[:atom][:atom_code]).must_equal "N" + _(match[:annotation]).must_equal "Normal" + end end - it "must match factors" do - _(subject.term.parse('3.2')[:term][:factor][:fixnum]).must_equal '3.2' + + describe "#group" do + it "must match parentheses with a term" do + match = subject.group.parse("(s2)")[:group][:nested][:left][:term] + _(match[:atom][:atom_code]).must_equal "s" + _(match[:exponent][:integer]).must_equal "2" + end + + it "must match nested groups" do + match = subject.group.parse("((kg))")[:group][:nested][:left][:group][:nested][:left][:term] + _(match[:atom][:atom_code]).must_equal "g" + _(match[:prefix][:prefix_code]).must_equal "k" + end + + it "must pass exponents down" do + match = subject.group.parse("([in_i])3")[:group] + _(match[:exponent][:integer]).must_equal "3" + _(match[:nested][:left][:term][:atom][:atom_code]).must_equal "[in_i]" + end end - it "must match annotations" do - match = subject.term.parse('N{Normal}')[:term] - _(match[:atom][:atom_code]).must_equal 'N' - _(match[:annotation]).must_equal 'Normal' + + describe "#expression" do + it "must match left only" do + match = subject.expression.parse("m") + _(match[:left][:term][:atom][:atom_code]).must_equal("m") + end + + it "must match left + right + operator" do + match = subject.expression.parse("m.s") + _(match[:left][:term][:atom][:atom_code]).must_equal("m") + _(match[:operator]).must_equal(".") + _(match[:right][:left][:term][:atom][:atom_code]).must_equal("s") + end + + it "must match operator + right" do + match = subject.expression.parse("/s") + _(match[:operator]).must_equal("/") + _(match[:right][:left][:term][:atom][:atom_code]).must_equal("s") + end end end - describe '#group' do - it "must match parentheses with a term" do - match = subject.group.parse('(s2)')[:group][:nested][:left][:term] - _(match[:atom][:atom_code]).must_equal 's' - _(match[:exponent][:integer]).must_equal '2' - end - it "must match nested groups" do - match = subject.group.parse('((kg))')[:group][:nested][:left][:group][:nested][:left][:term] - _(match[:atom][:atom_code]).must_equal 'g' - _(match[:prefix][:prefix_code]).must_equal 'k' + describe "when parser is initialized with :names as key" do + subject { Unitwise::Expression::Parser.new(:names) } + + describe "#metric_atom" do + it "must match 'newton'" do + _(subject.metric_atom.parse("newton")[:atom_code]).must_equal("newton") + end end - it "must pass exponents down" do - match = subject.group.parse('([in_i])3')[:group] - _(match[:exponent][:integer]).must_equal '3' - _(match[:nested][:left][:term][:atom][:atom_code]).must_equal '[in_i]' + + describe "#atom" do + it "must match 'inch'" do + _(subject.atom.parse("inch")[:atom_code]).must_equal("inch") + end + + it "must match 'foot'" do + _(subject.atom.parse("foot")[:atom_code]).must_equal("foot") + end end end - describe "#expression" do - it "must match left only" do - match = subject.expression.parse('m') - _(match[:left][:term][:atom][:atom_code]).must_equal("m") - end - it "must match left + right + operator" do - match = subject.expression.parse('m.s') - _(match[:left][:term][:atom][:atom_code]).must_equal("m") - _(match[:operator]).must_equal('.') - _(match[:right][:left][:term][:atom][:atom_code]).must_equal('s') - end - it "must match operator + right" do - match = subject.expression.parse("/s") - _(match[:operator]).must_equal('/') - _(match[:right][:left][:term][:atom][:atom_code]).must_equal('s') + describe "when parser is initialized with :symbol as key" do + subject { Unitwise::Expression::Parser.new(:symbol) } + + describe "#metric_atom" do + it "must match 'N'" do + _(subject.metric_atom.parse("N")[:atom_code]).must_equal("N") + end end - end + describe "#atom" do + it "must match 'in'" do + _(subject.atom.parse("in")[:atom_code]).must_equal("in") + end + it "must match 'ft'" do + _(subject.atom.parse("ft")[:atom_code]).must_equal("ft") + end + end + end end