diff --git a/lib/unitsml/compose/builder.rb b/lib/unitsml/compose/builder.rb index 335a03d..b600205 100644 --- a/lib/unitsml/compose/builder.rb +++ b/lib/unitsml/compose/builder.rb @@ -16,13 +16,10 @@ class << self # "/" and inverts b's power; `a * b` keeps "*"). An explicit trailing # extender on the left (from #extender) already is the join, so no glyph # is inserted after it — but `/` still inverts the right-hand side. - # An empty side inserts no glyph. invert_dimensions: false keeps - # Dimension leaves un-inverted on division (Quantity compatibility). - def build_product(left_terms, right_terms, operator, - invert_dimensions: true) + # An empty side inserts no glyph. + def build_product(left_terms, right_terms, operator) left = build_terms(left_terms, invert: false) - right = build_terms(right_terms, invert: operator == "/", - invert_dimensions: invert_dimensions) + right = build_terms(right_terms, invert: operator == "/") joiner = join_glyph(left, right, operator) build_root_formula(left + joiner + right) end @@ -33,7 +30,7 @@ def quantity_product(left, right, operator) return nil unless left || right build_product(left ? left.value : [], right ? right.value : [], - operator, invert_dimensions: false) + operator) end # Independent copy of a Formula (tree, powers, prefixes, text and @@ -99,11 +96,8 @@ def build_root_formula(terms, metadata = nil) # Rebuild every operand: map_leaves reconstructs branch wrappers and # each Unit/Dimension leaf is copied with a freshly built final power # (and a duplicated prefix). Non-leaf terms (Extender/Number) plain-dup. - def build_terms(terms, invert:, invert_dimensions: true) - TermTree.map_leaves(terms) do |leaf| - rebuild_leaf(leaf, invert && (invert_dimensions || - !leaf.is_a?(Dimension))) - end + def build_terms(terms, invert:) + TermTree.map_leaves(terms) { |leaf| rebuild_leaf(leaf, invert) } end def join_glyph(left, right, operator) diff --git a/lib/unitsml/parser.rb b/lib/unitsml/parser.rb index ca464fa..6b669d5 100644 --- a/lib/unitsml/parser.rb +++ b/lib/unitsml/parser.rb @@ -73,18 +73,28 @@ def update_units_exponents(array, inverse, sqrt = false) end end - case object - when Unit - next object.power_numerator = Number.new("0.5") if sqrt - next unless inverse - - inverse ? object.inverse_power_numerator : object.power_numerator - when Dimension then object.power_numerator = Number.new("0.5") if sqrt - when Extender then inverse = !inverse if ["/", "//"].any?(object.symbol) - when Formula then update_units_exponents(object.value, inverse) - when Fenced then update_units_exponents([object.value], inverse, sqrt) - end + inverse = update_term_exponent(object, inverse, sqrt) + end + end + + # Apply the sqrt/inverse state to one term; returns the inverse state for + # the next term (a "/" or "//" separator flips it). + def update_term_exponent(object, inverse, sqrt) + case object + when Unit, Dimension then update_leaf_exponent(object, inverse, sqrt) + when Extender then return !inverse if ["/", "//"].any?(object.symbol) + when Formula then update_units_exponents(object.value, inverse) + when Fenced then update_units_exponents([object.value], inverse, sqrt) end + inverse + end + + # Units and dimensions follow the same rule: a sqrt sets the 0.5 exponent, + # otherwise a term after "/" has its exponent inverted. + def update_leaf_exponent(object, inverse, sqrt) + return object.power_numerator = Number.new("0.5") if sqrt + + object.inverse_power_numerator if inverse end def post_extras diff --git a/lib/unitsml/power_numerator.rb b/lib/unitsml/power_numerator.rb index f75ee8b..be358df 100644 --- a/lib/unitsml/power_numerator.rb +++ b/lib/unitsml/power_numerator.rb @@ -17,6 +17,17 @@ def power_numerator=(power) @power_numerator = coerce_power_type(power) end + # Flip the exponent's sign in place for a term after "/" in a parsed + # string; no exponent becomes -1. Used by the parser for both units and + # dimensions. + def inverse_power_numerator + if power_numerator + power_numerator.update_negative_sign + else + @power_numerator = Number.new("-1") + end + end + private # The exponent lexemes the library itself stores: an integer, an n/m or diff --git a/lib/unitsml/unit.rb b/lib/unitsml/unit.rb index 1289ee8..5cb4848 100644 --- a/lib/unitsml/unit.rb +++ b/lib/unitsml/unit.rb @@ -114,14 +114,6 @@ def downcase_system_type Lutaml::Model::Utils.snake_case(system_type) end - def inverse_power_numerator - if power_numerator - power_numerator.update_negative_sign - else - @power_numerator = Number.new("-1") - end - end - private # Resolve a unit reference to a canonical symbol id, exactly like the parser diff --git a/spec/unitsml/composite_spec.rb b/spec/unitsml/composite_spec.rb index b1d8b95..f869f66 100644 --- a/spec/unitsml/composite_spec.rb +++ b/spec/unitsml/composite_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require "spec_helper" -require "digest" # Composite-unit builder (issue #2): the operator DSL (Unit#* / Unit#/) and the # Unitsml.compose keyword form, both producing a root Formula whose output @@ -678,8 +677,8 @@ def expect_parity(built, string) expect_parity(built, "W/m/s") end - it "keeps dimensions glyph-only, like the parser" do - built = Unitsml::Dimension.new("dim_M").ext("/").dimension("dim_L") + it "states dimension exponents explicitly after an extender, like units" do + built = Unitsml::Dimension.new("dim_M").ext("/").dimension("dim_L", -1) expect_parity(built, "dim_M/dim_L") end @@ -1116,13 +1115,13 @@ def nil_side_results(formula) expect(copied.prefix).not_to be(leaf.prefix) end - it "keeps main's un-inverted dimension division" do + it "inverts dimension division like the parser" do result = builder.quantity_product(Unitsml.parse("dim_M"), Unitsml.parse("dim_L"), "/") xml = result.to_xml expect(xml).to eq(Unitsml.parse("dim_M/dim_L").to_xml) - expect(Digest::SHA256.hexdigest(xml)[0, 16]).to eq("fadb9a9811d67de4") - expect(result.value.last.power_numerator).to be_nil + expect(xml).to include('xml:id="D_L-1M"') + expect(result.value.last.power_numerator.raw_value).to eq("-1") end it "keeps the default DSL dimension inversion" do @@ -1321,4 +1320,27 @@ def mutable_parts(formula) expect(formula.norm_text).to eq("s*m^(0)") end end + + describe "parsed dimension division" do + { + "dim_L/dim_T" => "D_LT-1", + "dim_L/dim_T^2" => "D_LT-2", + "dim_M/dim_L^-3" => "D_L3M", + }.each do |text, id| + it "inverts the dimension after / in #{text.inspect}" do + expect(Unitsml.parse(text).to_xml).to include(%(xml:id="#{id}")) + end + end + + it "matches the explicit negative exponent form" do + expect(Unitsml.parse("dim_L/dim_T").to_xml) + .to eq(Unitsml.parse("dim_L*dim_T^-1").to_xml) + end + + it "gives dimension quantities the inverted text" do + length = Unitsml::Quantity.new(value: 2, unit: "dim_L") + time = Unitsml::Quantity.new(value: 1, unit: "dim_T") + expect((length / time).norm_text).to eq("dim_L*dim_T^(-1)") + end + end end