Skip to content
Merged
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
18 changes: 6 additions & 12 deletions lib/unitsml/compose/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
32 changes: 21 additions & 11 deletions lib/unitsml/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions lib/unitsml/power_numerator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 0 additions & 8 deletions lib/unitsml/unit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 28 additions & 6 deletions spec/unitsml/composite_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Loading