Add functional conversions for non-proportional units#188
Open
Conversation
…fety Extract ConversionTableBuilderBase module to DRY up cycle detection, graph validation, and conversion caching shared by both builders. Add convert_to:/forward:/backward:/description: kwargs to UnitSystemBuilder#unit as a cleaner alternative to the value: hash format. Fix IDENTITY lambda to preserve input types, coerce convert inputs to Rational for exact arithmetic, and use Rational(1) in comparable_amount. Add UnitSystem#functional? for introspection.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add functional conversions for non-proportional units
Adds support for arbitrary conversion functions between units, enabling non-proportional relationships like temperature. Addresses #59, taking a different approach from #151.
Problem
Every conversion in the gem today is a simple ratio: multiply by some constant to go between units. That works for length, weight, and volume, but not for relationships with offsets. Converting Celsius to Fahrenheit (
F = C * 9/5 + 32) can't be expressed as a single multiplicative factor.Approach
Units can now define conversions as proc pairs, one to convert toward the base unit, one to convert away from it:
forwardmaps a value in the defined unit into the base unit.backwarddoes the reverse. For indirect paths (e.g. K to F), the builder composes the procs automatically.What changed
Measured::Unit-parse_valuenow recognizes[{forward:, backward:}, "base_unit"]alongside the existing string and[number, unit]formats. Addsfunctional?and a publicdescriptionreader.Measured::UnitSystemBuilder-unit()acceptsconvert_to:,forward:,backward:, anddescription:keyword arguments as a cleaner alternative to thevalue:hash format.Measured::ConversionTableBuilderBase- shared module extracted from both builders containing cycle detection, acyclic graph validation, and direct conversion caching.Measured::FunctionalConversionTableBuilder- builds a conversion table of procs instead of Rationals. The identity lambda preserves input types. Handles mixed systems where some units are static and others are functional.Measured::UnitSystem- selects the builder based on whether any unit is functional. Addsfunctional?for introspection.convertcoerces inputs to Rational before calling procs for exact arithmetic, and dispatches through eitherproc.call(value)orvalue * rationaldepending on the table entry.Measured::CacheError- raised at construction time if a functional system attempts to use a cache (procs aren't serializable).Backward compatibility
Static unit systems are untouched: same builder, same Rational arithmetic, same JSON caching. All existing tests pass without modification. The
value: [{...}, "unit"]format still works internally.How this differs from #151
#151 introduced
StaticUnitConversion/DynamicUnitConversionwrapper classes and a subclass ofConversionTableBuilderthat mutated units in place viato_dynamic. This PR takes a simpler path:conversion_amountis either a Rational or a Proc directly, no intermediary objectsFunctionalConversionTableBuilderis a standalone classconvert_to:/forward:/backward:kwargs instead of overloadingvalue:with hash syntaxLimitations