Imported from minad/unit (
AGENTS.md). Install upstream withnpx skills add minad/unit. Copyright stays with the author.
AGENTS.md
Guidance for working effectively in the unit gem. See README.markdown for
user-facing usage.
What this is
unit adds scientific and computational units to Ruby: unit-aware arithmetic,
conversions, and expression parsing (Unit('1 m/s^2')), plus an optional DSL
(1.meter.in_kilometer). It is pure Ruby with no runtime dependencies.
Setup
- Ruby >= 3.3 (see
required_ruby_versioninunit.gemspec). bundle install
Running tests
bundle exec rakeruns the full suite. It deliberately runs the specs twice:rake spec:no_dslexcludes:dsl-tagged examples and does not loadlib/unit/dsl.rb, verifying the core works without the DSL.rake spec:dslloads the DSL and runs everything.
- This split exists because the DSL monkey-patches
Numericand is not loaded byrequire 'unit'; it must be required explicitly (require 'unit/dsl'). Seespec/spec_helper.rb. - Single file or example:
bundle exec rspec spec/unit_spec.rb -e "supports addition".
Layout
lib/unit.rb— entry point. Requiresversion,class,system,constructor. Does not load the DSL.lib/unit/class.rb— theUnitclass (aNumericsubclass): arithmetic,normalize,in/conversions, comparison, and formatting.lib/unit/system.rb—Unit::System: loads unit and factor definitions from YAML, parses unit expressions, and defines the defaultSIsystem.lib/unit/constructor.rb— the top-levelUnit(...)constructor.lib/unit/dsl.rb— optional DSL adding methods toNumericandUnit(1.meter,.in_kilometer, ...). Opt in withrequire 'unit/dsl'.lib/unit/systems/*.yml— unit definitions. The default system loadssi,binary,degree, andtime;scientific,imperial, andmiscare available on demand.
Design goals
- Complex-valued units are a supported use case. Impedance in electrical
engineering is a complex-valued quantity (ohms), phasors in signal processing
are complex-valued, and so on.
Unit(Complex(3, 4), 'ohm')is valid. Methods that distinguish real from complex (real?,real,imag,conj,rect) must delegate to the value rather than unconditionally returning theNumericdefaults. Keep this in mind when adding new numeric methods.
Things to know before editing
Unitis aNumericsubclass, and modern Ruby treats numerics as immutable value objects —dup/clonereturnselfby default.Unitoverrides#dupso copy-based methods like#normalizedon't mutate the receiver;#freezeis overridden for the complementary reason — it pre-populates@normalizedwhile the object is still mutable, because the lazy@normalized ||=innormalizewould raiseFrozenErroron the first comparison otherwise. Don't reintroduce reliance on the defaultdup, and don't add new lazy ivars that bypass thefreezepre-population.- Comparison semantics:
==returnsfalsefor objects that are neitherNumericnor coerceable (never raises).<=>raisesArgumentErrorfor compatible-type but incompatible-dimension operands (e.g. metres vs seconds); it returnsnilfor 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 asMeV(scientific) need their system loaded first, e.g.Unit.default_system.load(:scientific). - Adding a unit system: drop a YAML file under
lib/unit/systems/and load it viaUnit::System#load. - Packaging:
unit.gemspecships only runtime files (Dir['lib/**/*.{rb,yml}']plusREADME.markdownandLICENSE); specs, CI config, and docs are intentionally excluded from the built gem.
CI
GitHub Actions (.github/workflows/ci.yml) runs bundle exec rake against Ruby
3.3, 3.4, 4.0, and head on every push and pull request.
Releasing
- Bump
Unit::VERSIONinlib/unit/version.rb. - Add a
CHANGELOG.mdentry for the new version. - Tag
vX.Y.Z, thengem build unit.gemspec && gem push unit-X.Y.Z.gem.