From 6b12583fef2bf07a828082a8808765a3dda95dd6 Mon Sep 17 00:00:00 2001 From: Grant Hutchins Date: Tue, 23 Jun 2026 16:42:06 -0500 Subject: [PATCH] Treat empty unit string as dimensionless in conversions Unit#in!("") (and in!(" "), and the same via #in) crashed with NoMethodError: undefined method 'each' for nil instead of behaving like the dimensionless unit. Construction already treats an empty unit string as dimensionless (Unit(1, "") => Unit("1")), so conversion was inconsistent with construction for the same input. System#parse_unit returned result.last, which is nil for an empty or whitespace-only expression (the tokenizer yields no tokens). validate_unit then called nil.each. Make parse_unit total by returning [] -- the dimensionless unit shape every consumer (to_unit, the constructor, validate_unit) already accepts -- instead of nil. --- lib/unit/system.rb | 26 +++++++++++++++++++++++++- spec/error_spec.rb | 23 +++++++++++++++++++++++ spec/system_spec.rb | 12 ++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/lib/unit/system.rb b/lib/unit/system.rb index ccd57e7..3783b39 100644 --- a/lib/unit/system.rb +++ b/lib/unit/system.rb @@ -60,6 +60,30 @@ def validate_unit(units) end end + # Parse a unit expression into a unit definition: an array of + # [factor, unit, exponent] triples suitable for +Unit.new+ and + # +validate_unit+. + # + # The expression is tokenized (see +TOKENIZER+) and evaluated with a + # shunting-yard pass over +OPERATOR+, supporting multiplication + # (*, ·, or juxtaposition), division (/), + # exponentiation (^, **), grouping parentheses, and + # numeric literals. + # + # system.parse_unit("m/s^2") # => [[:one, 1, 1], [:meter, ..., 1], [:second, ..., -2]] + # + # An empty or whitespace-only expression has no tokens and yields the + # *dimensionless* unit [] — the same definition produced by + # Unit(1, "") — so conversion stays consistent with construction + # rather than returning +nil+. + # + # system.parse_unit("") # => [] + # system.parse_unit(" ") # => [] + # + # Unrecognized glyphs survive lexing (see +SYMBOL+) and fail loudly as an + # "Undefined unit" +TypeError+ during validation rather than being dropped. + # + # Raises +SyntaxError+ on unbalanced parentheses. def parse_unit(expr) stack, result, implicit_mul = [], [], false expr.to_s.scan(TOKENIZER).each do |tok| @@ -87,7 +111,7 @@ def parse_unit(expr) end end compute(result, stack.pop) while !stack.empty? - result.last + result.last || [] end private diff --git a/spec/error_spec.rb b/spec/error_spec.rb index 71747e7..d0e55a1 100644 --- a/spec/error_spec.rb +++ b/spec/error_spec.rb @@ -26,4 +26,27 @@ end end + describe "converting to an empty (dimensionless) unit string" do + it "treats #in(\"\") as the dimensionless unit" do + expect(Unit(1, "").in("")).to eq(Unit(1, "")) + end + + it "treats #in(\" \") (whitespace-only) as the dimensionless unit" do + expect(Unit(1, "").in(" ")).to eq(Unit(1, "")) + end + + it "treats #in!(\"\") as the dimensionless unit" do + expect(Unit(1, "").in!("")).to eq(Unit(1, "")) + end + + it "treats #in!(\" \") (whitespace-only) as the dimensionless unit" do + expect(Unit(1, "").in!(" ")).to eq(Unit(1, "")) + end + + it "raises a clean TypeError (not NoMethodError) from #in! for an incompatible unit" do + unit = Unit(5, "m/s") + expect { unit.in!("") }.to raise_error(TypeError) + end + end + end diff --git a/spec/system_spec.rb b/spec/system_spec.rb index c5bcd2b..32ed18b 100644 --- a/spec/system_spec.rb +++ b/spec/system_spec.rb @@ -172,4 +172,16 @@ expect(Unit(1, "MeV", system).unit).to eq(Unit(1, "megaelectronvolt", system).unit) end end + + describe "#parse_unit with an empty or whitespace-only expression" do + before { system.load(:si) } + + it "returns [] (the dimensionless unit) for an empty string" do + expect(system.parse_unit("")).to eq([]) + end + + it "returns [] (the dimensionless unit) for a whitespace-only string" do + expect(system.parse_unit(" ")).to eq([]) + end + end end