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
14 changes: 12 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,18 @@ conversions, and expression parsing (`Unit('1 m/s^2')`), plus an optional DSL

- **`Unit` is a `Numeric` subclass, and modern Ruby treats numerics as immutable
value objects** — `dup`/`clone` return `self` by default. `Unit` overrides
`#dup` precisely so copy-based methods like `#normalize` don't mutate the
receiver. Don't reintroduce reliance on the default `dup`.
`#dup` so copy-based methods like `#normalize` don't mutate the receiver;
`#freeze` is overridden for the complementary reason — it pre-populates
`@normalized` while the object is still mutable, because the lazy
`@normalized ||=` in `normalize` would raise `FrozenError` on the first
comparison otherwise. Don't reintroduce reliance on the default `dup`, and
don't add new lazy ivars that bypass the `freeze` pre-population.
- **Comparison semantics:** `==` returns `false` for objects that are neither
`Numeric` nor coerceable (never raises). `<=>` raises `ArgumentError` for
compatible-type but incompatible-dimension operands (e.g. metres vs seconds);
it returns `nil` for non-`Numeric`, non-coerceable objects, honouring Ruby's
`<=>` contract. Keep this split: incompatible *types* → `nil`; incompatible
*dimensions* → `ArgumentError`.
- **Not every unit is loaded by `require 'unit'`.** Only SI + binary + degree +
time load by default; units such as `MeV` (scientific) need their system
loaded first, e.g. `Unit.default_system.load(:scientific)`.
Expand Down
19 changes: 19 additions & 0 deletions lib/unit/class.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ def initialize_copy(other)
@normalized = other.normalized
end

# Pre-populate +@normalized+ before locking the object.
#
# +Unit+ is a +Numeric+ subclass, so Ruby treats it as an immutable value
# object. The lazy +@normalized ||=+ in +normalize+ would raise +FrozenError+
# on the first comparison call if the instance were frozen first; calling
# +normalize+ here ensures the ivar is already set when +super+ seals it.
def freeze
normalize
super
end

# Converts to base units
def normalize
@normalized ||= dup.normalize!
Expand Down Expand Up @@ -113,6 +124,9 @@ def zero?
value.zero?
end

# Returns +false+ for any object that is neither +Numeric+ nor coerceable,
# rather than raising. This keeps mixed-type equality checks safe (e.g.
# comparing against +nil+, strings, or arbitrary objects).
def ==(other)
if Numeric === other
other = coerce_numeric(other)
Expand All @@ -129,6 +143,11 @@ def eql?(other)
Unit === other && value.eql?(other.value) && unit == other.unit
end

# Raises +ArgumentError+ when both operands are +Numeric+ but have
# incompatible dimensions (e.g. metres vs seconds); returns +nil+ for
# non-+Numeric+, non-coerceable objects. This split honours Ruby's +<=>+
# contract (return +nil+ for incomparable types) while still surfacing
# dimension mismatches loudly through every comparison operator.
def <=>(other)
if Numeric === other
coerced = coerce_numeric(other)
Expand Down
14 changes: 14 additions & 0 deletions spec/unit_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,20 @@
end
end

describe "#freeze" do
it "allows >= comparison without raising FrozenError" do
expect(Unit(1, "m").freeze >= Unit("0.9 m")).to be true
end

it "allows < comparison without raising FrozenError" do
expect(Unit(60, "Hz").freeze < Unit("61 Hz")).to be true
end

it "allows == comparison without raising FrozenError" do
expect(Unit(1, "kg").freeze == Unit("1 kg")).to be true
end
end

it 'should convert units' do
expect(Unit(1, "MeV").in("joule")).to eq(Unit(1.602176487e-13, 'joule'))
expect(Unit(1, "kilometer").in("meter")).to eq(Unit(1000, 'meter'))
Expand Down
Loading