David J. Anderson
March 2008
Numbers are everywhere. They surround us in every direction we turn - the price of gas, the distance to the beach, the temperature outside, the weight we want to lose. Sometimes we mention the units - $3.29/gallon, 46 miles, 75 F, 15 lbs - but often they're understood intuitively, as if they were part of the local dialect. Though we may mention them in conversation, there's little penalty for leaving them unspoken: "three twenty-nine", "forty-six", "seventy-five", and "fifteen" are perfectly good conversational numbers. Because we're embedded in society and share common contexts, being explicit about units isn't strictly necessary.
It's only this common context that allows us to consider specifying units a formality. Things only get confusing when we switch contexts and converting between unit systems is necessary - 1.02 euros/liter, 74 kilometers, 24 C, 7 kgs. We stumble and fumble as we do our best to quickly compute estimates for these values in our heads until we've gotten a feel for the new units. When unit-intuition is lost, numbers are meaningless to us.
Whether or not they're mentioned, units always accompany numbers in the real world. Numbers by themselves are just a reckoning system - it's only when they're attached to units in context that they have meaning. Though mathematics has abstracted the concept of number and allowed us to count independently of units, the units aren't unimportant; they're still there lurking behing the scenes.
Programmers must work between the two perspectives depending on the kind of programs they're writing - either treating numbers as unitless, or being forced to consider units more formally. Because they aren't supported directly, in most situations units are an afterthought. Numeric values are calculated by programs and output onto screens, plugged into reports and spreadsheets - units are simply hardcoded on the screens and forms. When units are considered to be implicit in the problem domain, numbers can just be treated as integers, floats and doubles without unit. So units typically aren't carried through computations. They are a pain to deal with and they just slow programmers down, getting in their way for no good reason. Most programs today count and calculate without a real concern for units.
A cavalier attitude like this can cause serious problems, however. Incorrect conversion factors and forgotten scalings have led to the ruin of many projects - bridges have fallen down, planes have flipped over, buildings have toppled, boats have sunk. There needs to be some way to avoid this. It should be easy to associate units with numbers and convert them reliably within and between different systems of measurement. It's not a new idea, and many valiant attempts have been made.
The Ruby programming language does not have explicit support for units. Ruby developers will typically treat units as an afterthought in the same way as they would in other programming languages, converting unit-less numbers using conversion factors and simple transformations. However, there are a few frameworks for units that have been developed.
The three prevalent systems that are used to convert units are the hard-coded unit converters in Rails, the Units portion of the Facets Rubygem, and Carlson and Buttler's units Rubygem.
Rails ActiveSupport provides a simple unit conversion paradigm for several unit conversions, allowing you to say things like:
3.weeks + 2.days | ==> | 1382400 (seconds) |
20.minutes.ago | ==> | 1176491007 (seconds since the Epoch) |
However, this is driven by hard code, not conversion data. And as such, it can fail if incorrectly:
1.minute | ==> | 60 (seconds) |
1.hour | ==> | 3600 (seconds) |
1.hour.minutes | ==> | 216000 (wait - I expected 60...) |
Rails gives you a handy way to do conversions, but they have to be simple and there's nothing but numbers supporting them from underneath - they're not complete and aren't easily extensible. They do many of the things web developers need, though, and they come for free in today's most popular Ruby-based web development framework.
Since 2003, the Ruby Facets library has been growing, filled with new classes and extensions to existing classes. It's a truly phenomenal piece of work.
Part of this package is a units system that is quite full-featured.
1.minute | ==> | 1 (minutes) |
1.hour | ==> | 1 (hour) |
1.hours.to(minute) | ==> | 60 (minutes) |
A complete SI system is even available. The system promotes units as an object, and provides arithemtic methods to handle unit type compatability and conversion. The data files are formatted in YAML, and are extensive. However, it's missing a tie between the numbers and the units - the two are still separated conceptually.
In 2005, Carlson and Butler put together units.gem, which started down a different path, also loading their conversions from data and using method_missing to do conversions:
1.minute | ==> | 1 (minutes) |
1.hour | ==> | 1 (hour) |
1.hours.to_minutes | ==> | 60 (minutes) |
Very good stuff. But their definition strategy was not self referent, as the Facets system is; and their implementation did not promote units to first class objects.
I also wanted to say some things like:
Units.length.systems | ==> | [ 'english', 'metric' ] |
Units.second.unit_type | ==> | 'time' |
a = Time.minutes_per_day | ==> | 1440 (minutes) |
a.to_s | ==> | '1440 minutes' |
a.hr.to_s | ==> | '24 hours' |
Basically units are a Domain Specific Language. They define things relative to each other, and the meaning of unit expressions is intuitive and flexible based on the types of the arguments. I wanted to define the units much like the way database migrations are defined in Rails, and allow forward reference to take place during definition, coding units and conversions in Ruby. I wanted the code readability to be at the same level as Ruby, using the method_missing to decompose messages and do the work, and optionally be able to reliably create methods - not have everything be pre-created. I also didn't want the package to be a heavyweight. I wanted it lean and mean and fast.
I felt that something comprehensive could be done and packed into a Rubygem that would give me a nice extensible framework for units in Ruby. So I started on a programming expedition...
An afterword for before we start writing code In this document I've described the process of the development of an actual chunk of Ruby infrastructure. What you'll read about is the thoughts that I had as I wrote the code, complete with dreaming up usage, trying things, and refactoring. When programming, my thoughts were bouncing between a full spectrum of concerns: from accumulated philosophy to wild creativity to pragmatic intention, and even sometimes to pedantic banality. What I wrote included the false starts and revisits to previously written code - that is the way real development works. Sometimes you just get it wrong and have to change what you've done to get things working right. No apologies, this is what has truly gone on during development - I really did write this document as I wrote the code. By the way, this document is littered with both euphemisms and pats on the back. At times it may even seem like I'm talking to myself. Hmmm. Well, yes, I guess I am. For me there is a constant inner dialogue that happens when I'm writing code. And any congratulations or deprications I may make are also part of that dialog: excitement and passion are integral parts of the romance of programming! The description of the development I've done is very close to the stream of consciousness that I was actually experiencing, complete with the thrills of victory and the agonies of defeat. The thing is, I'm also including you, the reader, in this document, having my internal dialogue with you instead of just me. Scary, huh? I suppose that programming (and reading) is not for the faint of heart! One thing that I skirt over in the discussion that follows are the three pillars of any good software development effort - version control, unit testing and project automation. Rest assured, I am doing them. When I'm developing I've got Subversion backing everything, I'm writing tests before I write code, and I've automated my processes appropriately. It's not that I don't want to talk about it - I'm just not going to do that here. They're all part of my software development landscape and here I'm focussed on the foreground of units as my subject matter. If you're not doing version control, unit testing or automation, get with it and start today. It doesn't take much to effort to get it all going and the payback is huge. Sometimes I may go off on a slight tangent, talking about some arcane development practice or rambling about something I've learned in the past and am dredging up to the surface. I hope you will indulge these episodes; skip over them if you'd like, but be aware that there may be some small nugget of wisdom behind them. Also, when I start out, I'm going to expain the Ruby code more fully than I will be later in the document. I'm going to assume that you'll start getting it as we go along. If you feel like you're missing something, pick up a copy of the Pickaxe (that's Programming Ruby by Dave Thomas, Chad Fowler and Andy Hunt.) In any case, know that you can download the finished units infrastructure as a Rubygem complete with API documentation and unit tests - I haven't been slacking through this process, even though I'm calling it a pipe dream. Though I may not be entirely disciplined (just ask my wife) I do try to make sure that the stuff I create works as intended. |
I began with some pondering. What are units, really? And how are they organized so that we can assume them without thinking about them?
Units are the quality of quantities that provide the context in which they can be compared to each other. This comparison is done in a measurement context - only values in the same context can be compared, such as by length, weight, or duration. A measurement context is expressed systematically, that is in the context of one or more system of units. Finally, the unit values themselves are defined within a system context.
Context is everything when it comes to our fundamental understanding of units. This taxonomy of Measures / Systems / Units provides what we need to put everything in perspective. For instance, we probably include the following subset:
Measure | System | Unit |
Length | English | Yard |
Foot | ||
Inch | ||
Metric | Meter | |
Centimeter | ||
Mass | English | Pound |
Ounce | ||
Metric | Kilogram | |
Gram | ||
Time | Base | Hour |
Minute | ||
Second |
We can only compare values with the same Measure, we can convert values between Systems, and Units within those Systems can be sorted by scale. Contextually, knowing a Unit (either explicitly or implicitly) also means you know its Measure and System. It now also makes sense to convert between Units and Systems.
Interestingly, notice that we could have rooted the taxonomy with Systems instead of Measures. I chose Measure as more fundamental since it has its roots in discovery rather than invention, and Measure bounds the conversions we can do at the broadest level. In the chart, though Systems may occur in different Measures, they may not exist in all Measures which would leave us with gaps. The organization in the chart is not without it's own gaps (Time is labeled as Base, for example) but at least there are not gaps at the top level. And besides this, there is also a sense of cause and effect to this hierarchy: Measure came first by observing of the world around us, while systems were invented subsequent to the need to measure.
Also, we may have different names for Measures that mean the same thing formally, but have different informal contexts. For instance, Length and Distance are formally equivalent, but Length has a static connotation while Distance connotes movement. Time and Duration have are also equivalent but have different connotations (again, interestingly, static and dynamic.) The simple rule is that if you can use the same Units, the formal equivalence holds even though we may talk about the Measures differently.
Setting Ruby code to this, we have:
units.rb a units pipe dream | |
class Units @@measures = {} end class UnitsMeasure < MethodicHash end class UnitsSystem < MethodicHash end class UnitsUnit < MethodicHash end |
The
Units
class is at the top level to provide a global context for defining and accessing the
Units
system, and managing a
Hash
of
UnitsMeasures.
UnitMeasures
is a
MethodicHash
that contains
UnitsSystems
, and
UnitSystems
is a
MethodicHash
that contains
UnitsUnits
. A
UnitsUnit
is a
MethodicHash
that contains its properties.
Note the use of the
MethodicHash
. This class is just a
Hash
that defines its
method_missing
method to use its method argument as the value of the key used to store or retrieve its values. It's a convenient shorthand that makes accessing the hash look like accessing instance variables. The mechanism will be replicated at the class level in
the
Units
class because it is not used to create instances, but only to root the sofware.
So, let's take the next step and detail the code skeleton.
Units
is used at the class level to manage a
Hash
of
UnitsMeasure
instances, so all of the methods are class methods.
units.rb a units pipe dream | |
class Units def Units.units_measures @@measures.keys end def Units.create(name, &block) measure = (@@measures[name.to_s] ||= UnitsMeasure.new) block.call measure if block_given? measure end def Units.derive(name, target, &block) measure = (@@measures[name.to_s] = (target.kind_of? UnitsMeasure) ? target : Units.create(target.to_s)) block.call measure if block_given? measure end def Units.delete(name) @@measures.delete name.to_s end def Units.clear @@measures.clear self end def Units.size @@measures.size end def Units.[](name) @@measures[name.to_s] end def Units.method_missing(method,*args) measure = self[method] raise UnitsException.new("UnitsMeasure '#{method}' undefined") if !measure measure end def Units.names_of(units_measure) @@measures.keys.select { |name| @@measures[name].equal? units_measure } end end |
The methods are all fairly simple and provide guarded access to the
Hash
. The
units_measures
method returns an
Array
of the names of the currently defined
UnitsMeasure
s. The
create
method returns a new
UnitsMeasure
with the given, or the existing one with the given name if present. If a block is passed in, the
UnitsMeasure
is yielded to it. The
derive
method is much like
create
, associating a
UnitsMeasure
to the given name, except it is derived from existing
UnitMeasures
. If the target of the derivation is a
UnitsMeasure
, the name is associated with it. Otherwise a new
UnitMeasure
is created with the value of target as its name, and the name is associated with it. If a block is passed in, the
UnitsMeasure
is yielded to it. The
delete
method disassociates the name with a
UnitsMeasure,
and returns the name. The
clear
method empties the
Hash
of
UnitMeasure
s and returns
Units
. The
size
method returns the number of
UnitMeasure
s in the
Hash
. The
[]
method returns the
UnitsMeasure
with the given name from the
Hash
. The
method_missing
method tries to get the
UnitsMeasure
using the name of the method as the associated name, and returns it. If no value was found, a
UnitsException
is raised. The
names_of
method returns an
Array
of names that are associated with the given
UnitsMeasure
.
The key feature of the class is that it allows a
UnitsMeasure
to have multiple names associated with it, preserving formal equality while admitting connotative difference.
A
UnitsMeasure
instance associates names to
UnitsSystem
s.
units.rb a units pipe dream | |
class UnitsMeasure def system(name,&block) system = (self[name] ||= UnitsSystem.new(self,name)) block.call system if block_given? system end def names Units.names_of self end end |
There's not a lot to
UnitsMeasure
at this level, since most of the mechanism is managed by the
MethodicHash
from which it inherits. The
system
method creates a new
UnitsSystem
in the context of this
UnitsMeasure
or gets the existing one if found, and yields it to the block if it was passed in. The
UnitsSystem
is returned. The
names
method returns the names by which this
UnitsMeasure
is known at the top level.
A
UnitsSystem
instance associates names to
UnitsUnit
s.
units.rb a units pipe dream | |
class UnitsSystem attr_reader :units_measure, :name def initialize(units_measure,name) @units_measure = units_measure @name = name end def unit(attributes) unit = UnitsUnit.new self, attributes self[unit.name] = unit end end |
There's not a lot to the
UnitsSystem
class either, since most of the mechanism is managed by the
MethodicHash
from which it inherits. However, it does know to which
UnitsMeasure
it belongs. The
initialize
method sets up a new
UnitsSystem
, remembering its name and the given
UnitsMeasure
. The
UnitsSystem
is returned. The
unit
method associates a new
UnitsUnit
in the
UnitsSystem
with its name from the given
Hash
of options. The
UnitsUnit
is returned.
A
UnitsUnit
is a unit in a
UnitsSystem
, defined by its attributes.
units.rb a units pipe dream | |
class UnitsUnit attr_reader :units_system def initialize(units_system,attributes) @units_system = units_system merge! attributes end end |
The
UnitsUnit
is trivial, containing only a simple initializer. The
initialize
method remembering the given
UnitsSystem
and merging the attributes into itself. The
UnitsUnit
is returned.
Of course, when unexpected things happen related to
Units
, a
UnitsException
is raised.
units.rb a units pipe dream | |
class UnitsException < Exception end |
This is just a cover for
Exception
in the
Units
context.
At this skeletal level, the classes are simple. The fun begins when they are fleshed out. Our next step is taking a closer look at Measures.
Measures come in two flavors: those that exist independently and stand up on their own, and those that are derived from other Measures. For instance, if length is an independent Measure, then area can be derived as length*length.
Since when you're working with measures the only thing that makes sense is multiplying them together, you only need to maintain three operations: multiplication, division and exponentiation. When units are derived, a combination of other units are combined to produce a new type of unit. For instance, consider kilograms divided by cubic meters. This formulation yields a density in kg/m3. But from the Measures point of view, you aren't interested in the kilograms and meters - it's mass and volume that are formulated to get density. The Density Measure is what is derived and is what drives the specific unit conversions.
Way back in an early mathematics class I was taught the concept of Dimension Analysis using what they called the fencepost method, used to figure out the units when factors are multiplied. The image is this: you have a series of fenceposts with a single rail between them. You line up the unit "dimensions" of a each value between the fenceposts such that the unit numerators are above the rail and the denomenators are below the rail. Then you run the rails, cancelling out any units that are both above and below. What's left are the units of the result. At the time it was a revelation, but of course it was only a simple crutch until a little later when negative exponents were introduced and it could all be done by addition and subtraction of exponents.
Only a few methods are needed to facilitate deriving Measures.
units.rb a units pipe dream | |
class UnitsMeasure attr_reader :derived def **(exponent) UnitsMeasure.new.merge_derivation(self => exponent) end def /(divisor) UnitsMeasure.new.merge_derivation(self).merge_derivation(divisor,-1) end def *(factor) UnitsMeasure.new.merge_derivation(self).merge_derivation(factor) end def merge_derivation derivation, multiplier=1 current = (self.derived ||= {}) if derivation.kind_of? UnitsMeasure if derivation.derived derivation.derived.each {|key,value| current[key] = (current[key] || 0) + multiplier*value } else current[derivation] = (current[derivation] || 0) + multiplier end elsif derivation.kind_of? Hash derivation.each {|key,value| current[key] = ((current[key] || 0 ) + multiplier*value) } else raise UnitsException.new( "Cannot add #{derivation.self_name} to derivation") end self end end |
The Measures derivation is simple when viewed this way. The
**
method creates a new
UnitsMeasure
and merges itself into the derivation raised to the given power. The
/
method creates a new
UnitsMeasure
, merges itself to the derivation, and merges the divisior to the derivation with its exponents inverted. The
*
method creates a new
UnitsMeasure
and merges itself and the factor to the derivation. For each method, a new
UnitsMeasure
is returned.
The
merge_derivation
method is the real workhorse. If the given derivation is a derived
UnitsMeasure
or a
Hash
, each element of the derivation is merged. If the given derivation is an underived
UnitsMeasure
, it is merged. Otherwise, we can't deal with it and a
UnitsException
is thrown.
The derivation is a
Hash
that is kept in the
derived
instance variable in the
UnitsMeasure
that associates
UnitsMeasure
s to exponents. When the derivation is merged into a
UnitsMeasure
using
merge_derivation
, the exponents of existing elements are modified, or new elements are added if missing. What results is a new
UnitsMeasure
with the value of its derived set to the Measures that define it.
Given the ability to create derived Measures, we must still be able to add them to the set of measures in the
Units
class.
This brings up another twist, which we handle in the same way we did for the
UnitsMeasure
creation. Namely, if two derived
UnitsMeasure
s have the same derivation, we consider them to be formally equivalent. This makes sense: we consider all Measures to be unified. It goes right back to the previous statements about formalism and connotation. Two derived
Measures can be formally equilvalent, but called by different names to preserve difference in connotation.
units.rb a units pipe dream | |
class Units def Units.derive(name, target, &block) measure = ( @@measures[name.to_s] = if target.kind_of? UnitsMeasure if target.derived && (derived = find_by_derivation target.derived) derived else target end else Units.create(target.to_s) end ) block.call(measure) if block_given? measure end def Units.find_by_derivation(derivation) matches = @@measures.values.uniq.select { |measure| measure.derived ? measure == derivation : false } case matches.size when 0 then nil when 1 then matches[0] else raise UnitsException.new( "Multiple UnitsMeasures with same derivation found") end end end |
The
derive
method is still the analogue of the
create
method, but it's been beefed up to handle derived
UnitsMeasure
s. Given a name and target, if the target is a
UnitsMeasure
it checks to see if it is derived. If so, then it so then it sets it to a known
UnitsMeasure
with the same derivation, or adds it anew. If it isn't a
UnitsMeasure
, then name is a new connotation of the target, and the name is associated with the target
UnitsMeasure
. Note that
create
is called with target - which either finds its
UnitsMeasure
or creates a new one. Finally, the resultant
UnitsMeasure
is yielded to the block if it was given, and returned to the caller.
The
find_by_derivation
method finds a known
UnitsMeasure
matching the given derivation if it exists. Each of the known derived
UnitsMeasure
s' derivations is tested for equality with the given derivation. If there are no matches,
nil
is returned. If one was matched, the match is returned. If more than one was found, then a unification problem occurred and it is time for us to raise a
UnitsException
.
We'll use the
UnitsMeasure
s to do our conversions later. But before that, we need to detail the definition of Unit values.
As people establish what sorts of things are going to be measured, Unit values are designated so that comparisons could be made. Whether it was sticks laid end to end measuring distance, rocks used in a balance to measure weight, or the position of the sun and moon in the sky to measure time, unit values provided a new sense of order to the world. We all could finally relate different objects to each other with common scales.
In human language there are three common constructs for speaking of units - by name, by the plural of the name, and by abbreviation. Since most of us capture the subtleties of this before we're grown, we hardly pay attention unless we encounter ambiguity or something we've not yet experienced. Fortunately for us, unit names, plurals and abbreviations are common and we have no problem with them.
The same cannot be said of computer software, of course. Computers have no real world experience to draw upon. We'll need to explicitly state the names, plurals and abbreviations we use to reference the units we define.
units.rb a units pipe dream | |
class UnitsUnit def initialize(units_system,attributes) @units_system = units_system merge! attributes normalize end def normalize raise UnitsException.new("UnitUnits must have a name attribute") unless self.name self.name = self.name.to_s add_plural add_abbrevs self end def add_plural self.plural = (plural = self.plural) ? plural.to_s : self.name+'s' end def add_abbrevs(attribute = nil) if attribute == nil self.abbrevs = add_abbrevs(:abbrev)+add_abbrevs(:abbrevs) elsif (value = self[attribute]) self.remove(attribute) (value.kind_of? Array) ? value.collect {|e| e.to_s } : [ value.to_s ] else [ ] end end end |
When a
UnitsUnit
is created, a short dance is done to normalize its name, plural and abbreviations. The
initialize
method is extended to call the
normalize
method after the incoming attributes have been merged into the instance and the instance is returned. The
normalize
method makes sure the name of the instance is a
String
and adds plurals and abbreviations to the instance. If no name is given as an attribute, a
UnitsException
is raised. The instance is returned. The
add_plural
method asigns the
plural
attribute and makes sure that it is a
String
if it exists. If it doesn't exist, it is created by adding an 's' to the value of the name.
The
add_abbrevs
method make sure that the
abbrevs
attribute is an
Array
of
Strings
. For flexibility, the values may be passed using the
abbrev
or
abbrevs
attribute, either as an
Array
or a single abbreviation. The method is intersting in that on entry from
normalize,
no arguments are given and the attribute is
nil
. This causes the
abbrevs
attribute to be set to the sum of the value of
add_abbrevs
called with the
abbrev
and the
abbrevs
attributes. When the method is entered with a non-nil
attribute, the value is remembered, the attribute is removed and an
Array
is returned. If the value was an
Array,
it's elements are converted to
Strings
; if not an
Array,
then an
Array
is constructed from the value converted to a
String
. Finally if the value is
nil
, the attribute not being present, an empty
Array
is returned. And now, going back to the beginning, we can see the sum is a sum of
Arrays
, resulting in a new
Array
stored into the
abbrevs
attribute.
So now we have nice little mechanism that sets up named unit values with plurals and abbreviations. But to get at them, we need to build a quick-access mechanism.
units.rb a units pipe dream | |
class UnitsSystem def unit(attributes) unit = UnitsUnit.new self, attributes self[unit.name] = unit Units.add_unit unit end end class Units @@unit_names = {} @@unit_plurals = {} @@unit_abbrevs = {} def Units.add_unit(unit) @@unit_names[unit.name] = unit @@unit_plurals[unit.plural] = unit unit.abbrevs.each { |abbrev| @@unit_abbrevs[abbrev] = unit } end def Units.clear @@measures.clear @@unit_names.clear @@unit_plurals.clear @@unit_abbrevs.clear self end end |
We create the high speed mechanism by rooting it at the top level, through
Hash
es of the unit's names, plurals and abbreviations. In
UnitsSystem
s, the
unit
method is expanded to make a call to the
add_unit
method in
Unit
s. The
Units.add_unit
method associates the unit's name, unit's plural and each of the unit's abbreviations to the unit. The
Units.clear
method is extended to do a last spot of bookkeeping, emptying the three new
Hash
es.
Since it can look upwards for its
UnitsSystem
and
UnitsMeasure
, all of the relevant parts of the
Units
infrastructre can be retrieved once a
UnitsUnit
has been identified. Using these three hashes, identifying a
UnitsUnit
should very fast.
But what using these structures? There still isn't a way to equate units and look them up in context. That just happens to be our next step. But take care. It turns out this one is a doozy!
When the relationship between units is being defined, we need to effectively say things like:
1.foot = 12.inches
1.hour = 60.minutes
1.ounce = (1/16.0).pound
In the scheme we've set up so far, we make our unit definitions in code that reads a lot like the Rails DB migrations. We'll go ahead and add an
equals
attribute to our incoming unit
Hash
handling:
definitions.rb a units pipe dream | |
Units.create :length do |m| m.system :english do |s| s.unit :name => :inch, :plural => :inches, :abbrev => :in s.unit :name => :foot, :plural => :feet, :abbrev => :ft, :equals => 12.inches s.unit :name => :yard, :abbrev => :yd, :equals => 3.feet end end |
Now we've done it - the can of worms is opened. Up to this point we've just been building interconnected data structures. But with this simple inclusion, we have a whole set of additional issues to contend with. How the heck can we make this work?
Let's consider the problem in the context of this simple inch-foot-yard example. The first thing to think about is that the inch unit has no equals attribute. Therefore, we'll consider it to be the basis for the Measure - all un-equaled units will be bases, and other Unit values will be relative to the bases. The next thing to notice is that the unit equality does not need to be defined in terms of the base. This is important since, for instance, people may define a mile as 5280 feet, but it is not likely they'll define it as 63360 inches. That is a constraint to which we do not want to be subject - we shouldn't have to define equality in terms of a base; computers should be figuring out all that for us. Of course, people may units in whatever way they wish - we don't want to preclude anyone from doing things in any way that makes sense to them.
So given this, how should we record the equality?
We'll focus on the
equals
attribute of the foot unit first. As given, the statement
12.inches
will trigger a method_missing on the
Fixnum
object. Great - we'll just add a method_missing on
Fixnum
, do a conversion, attach the
UnitsUnit
, and away we go... Except, the
Fixnum
is a singleton... shared across all usage. And we want the same activity for other types of numbers. No, just adding units to a
Numeric
isn't quite good enough.
So we'll still put a
method_missing
on
Numeric
, but instead of converting the value and adding a
UnitsUnit
just, we'll create and return an instance of a new object,
NumericWithUnits
. Besides the advantage of not crufting up
Numeric
, we'll be able to add other methods to the new object that can handle conversions leaving us open to do all the crazy things we can dream up.
One might wonder why we're stoping with
Numeric
s. Why not an
ObjectWithUnits
? This is a good question and we'll address it shortly. For now though, let's take a look at the consequences of our design choice and see how things shake out.
units.rb a units pipe dream | |
class NumericWithUnits attr_reader :numeric, :unit def initialize(numeric,unit) @numeric, @unit = numeric, unit end def to_s "#{numeric} #{(numeric == 1)? unit.name : unit.plural}" end end class Units def Units.lookup(unit_identifier) unit_identifier = unit_identifier.to_s @@unit_names[unit_identifier] || @@unit_plurals[unit_identifier] || @@unit_abbrevs[unit_identifier] || nil end def Units.convert(numeric,unit_identifier) unit = lookup(unit_identifier) raise UnitException.new("#{unit_identifier} missing") unless unit NumericWithUnits.new(numeric*unit.equals.numeric,unit.equals.unit) end end class UnitsUnit def normalize raise UnitsException.new("UnitUnits must have a name attribute") unless self.name self.name = self.name.to_s add_plural add_abbrevs add_equals self end def add_equals self.equals = NumericWithUnits.new(1,self) unless self.equals end end class Numeric alias :method_missing :method_missing_before_units def method_missing(method,*args) begin unit = Units.convert self, method rescue method_missing_before_units(method,args) end end end |
The changes required are not too complicated. The
NumericWithUnits
'
initialize
method assigns the incoming
Numeric
and
UnitsUnit
. The
NumericWithUnits
'
to_s
method returns a
String
containing its numeric part followed by its unit name or plural. The
Units.lookup
method returns the unit associated with a name, plural or abbreviation if it exists, or
nil
if it can't be found. The
Units.convert
method takes a numeric value and a unit name, plural or abbreviation and returns a new
NumericWithUnits
instance. The numeric part of the instance is converted relative to the identified unit, and the identified unit is assigned to the instance. If the name, plural or abbreviation could not be found, a
UnitsException
is raised. The
UnitsUnit
's normalize method is extended to call the
add_equals
method to add its
equals
attribute if missing. The
UnitsUnit
's
add_equals
method makes this unit a base - that is, if it has no
equals
attribute, it makes it a new
NumericWithUnits
instance with a quantity of 1 and a unit of itself. Finally, the
Numeric
's
method_missing
method is extended to try to convert to the given units if possible, or otherwise call the prior
method_missing
.
Great! Now we can load in units and have them all defined in our data structures in terms of base units. With these equalities in place, conversion is trivial!
The
Units.convert
method does conversion quite nicely - but thinking ahead, we only want it to convert to the base units during
definition! When we're not defining the units, we want to keep the units that are given:
5.feet | ==> | NumericWithUnits: numeric = 5, unit = feet |
1.5.hours | ==> | NumericWithUnits: numeric = 1.5, unit = hour |
1.ounce | ==> | NumericWithUnits: numeric = 1, unit = ounce |
What we need is a switch that only does the conversion based on whether or not we're in a defining context. We'll take that to mean: when calling the
system
method of a
UnitsMeasure
. Inside of these calls the system is in a
defining
context... but outside, is a
using
context. Note that the unit method of a
UnitSystem
is also a defining context, but units are not ever accessed within it except for in a defining context - nothing special is needed.
units.rb a units pipe dream | |
class Units @@defining = nil def Units.defining? @@defining end def Units.defining(defining) @@defining = defining end def Units.convert(numeric,unit_identifier) unit = lookup(unit_identifier) raise UnitException.new("#{unit_identifier} missing") unless unit if defining? NumericWithUnits.new(numeric*unit.equals.numeric,unit.equals.unit) else NumericWithUnits.new(numeric,unit) end end end class UnitsMeasure def system(name,&block) Units.defining true system = (self[name] ||= UnitsSystem.new(self,name)) block.call system if block_given? Units.defining nil system end end |
Definition declaration sort of crosscuts through this stuff; besides the creation of a
defining
class variable to hold the definition state in Units, additional changes must be made. The
Units.defining?
method returns
true
if a measure is being defined, indicating the defining context. The
Units.defining
method sets the measure being defined. The
Units.convert
method is extended to only convert to the base when in the defining context. Otherwise the initialization of the
NumericWithUnits
is done directly with the numeric and the unit that was found. The
UnitsMeasure
's
system
method is extended to indicate the defining context by setting and clearing itself.
By regulating all of the pathways into the
system
method, we ensure that the system will continue to work reasonably - below
system
, there's no place to establish a contextual foothold. If control goes through the method, we'll certainly get the context right unless another developer down the road starts changing our code. But since we're also writing unit
tests as we go along here, I'm not too worred.
But we're not done yet.
In the last example, we used the unit "ounce". But did we mean a measure of volume, or were we perhaps really talking about weight? We don't know! It's ambiguous!
There are two ways to circumvent ambiguity The first is by avoiding it: simply work in a perfect world where it never appears. The second is to deal with it. Since we live in the real world, we must use or extend the context we've established to try to figure out something reasonable. But it may get a little hairy. Context is like that.
What ambiguity implies is choice. If there's just one thing, there's no ambiguity - it's only when more that one thing that can be chosen that ambiguity becomes an issue. When we're choosing from unit names, or plurals, or abbreviations, we've been expecting to see that we'll only get one unit back. But when we lookup ounces, we would potentially get more than one unit. We need to change out code a bit to turn this potential into reality.
This actually sheds some light on the the entire lookup process. Why do we need three
Hash
es when one would do? At the time, it seemed like a good idea to keep the name, plural and abbreviation different
Hash
es; but since we're just going to use them for lookup at the same time, let's reduce the code a bit while we're adjusting things.
Additionally, we need to take a closer look the defining state. While setting a
true
/false
flag only conveys whether we're defining or not, we're going to need some more information to get us a further. We'll make a slight change to the mechanism to record the measure being defined - that way we can compare the unit being defined against
it. If we encounter two units with the same name when defining, we'll prefer the one with the measure that matches the one we're defining. In fact, we'll also look at the derivation and if the requested unit is in a measure that's part of a
derivation, we'll choose it over one that's not.
units.rb a units pipe dream | |
class UnitsMeasure def system(name,&block) Units.defining self system = (self[name] ||= UnitsSystem.new(self,name)) block.call system if block_given? Units.defining nil system end end class Units def Units.add_unit(unit,unit_identifier=nil) if unit_identifier if (element = @@units[unit_identifier]) @@units[unit_identifier] += [ unit ] unless element.index(unit) else @@units[unit_identifier] = [ unit ] end else add_unit unit, unit.name add_unit unit, unit.plural unit.abbrevs.each { |abbrev| add_unit unit, abbrev } end end def Units.lookup(unit_identifier) @@units[unit_identifier.to_s] || [ ] end def Units.convert(numeric,unit_identifier) if (candidates = lookup(unit_identifier)).size == 0 raise UnitsException.new("#{unit_identifier} missing") elsif !defining? if candidates.size > 1 raise UnitsException.new("#{unit_identifier} ambiguous") else unit = candidates[0] NumericWithUnits.new(numeric,unit) end else units = candidates.select { |candidate| @@defining == candidate.units_system.units_measure } case units.size when 0 then raise UnitsException.new("#{unit_identifier} missing") when 1 then unit = units[0] NumericWithUnits.new(numeric*unit.equals.numeric,unit.equals.unit) else raise UnitsException.new("#{unit_identifier} ambiguous") end end end def Units.clear @@measures.clear @@units.clear self end end class UnitsUnit def ==(unit) self.equal?(unit) && self.units_system.equal?(unit.units_system) end end class NumericWithUnits def ==(value) (value.numeric == numeric) && (value.unit.equal? unit) end end |
The change to the indicate the defining state is slight. The change to handle the conversion in the defining versus using state is much more dramatic. The
UnitMeasure
's system method now sets the defining state to the measure of the unit being defined rather than just a true value. The
Units.add_unit
method is modified to add associations from the unit's name, plural and each abbreviation to the unit - but the associations are now to
Array
s of units. Initially, we don't supply a
unit_identifier
. This triggers the code that adds the name, then the plural, then each abbreviation by re-calling the method. This time around a
unit_identifier
is supplied which adds the association to the
Hash
if not already present. We need this test-of-presence because if we redefine a unit, we need to make sure it only gets into the
Array
once.
The
Units.convert
method is significantly changed. First, if there are no units defined that match the
unit_identifier
, an exception is raised. If there are units, then if the system is in the defining state, then if a single unit matches, we return a new
NumericWithUnits
- otherwise, we have more than one matching unit and we have ambiguity. We'll figure out how to deal with using state ambiguity later; for now we'll just raise a
UnitsException
. If we're in the defining state, then we make sure the unit being used in the definition is in the right context. Once we've eliminated the units that don't match this tests, we should have gotten rid of our potential
ambiguity. A single match indicates a non-ambiguous choice and we create a new
NumericWithUnits
; zero matches indicates a missing unit and more than one match means we still have ambiguity, so we throw a
UnitException
. The
Units.clear
now just clears out the one
Hash
that has replaced the three.
The
UnitsUnit
's
==
method is replaced and tests that the underlying
MethodicHash
is the same given unit. This is done so the index call on the
Array
in the
Units.add_unit
method will add the unit if it's in a different measure/system context - otherwise, we wouldn't add units with the same attributes. We also have to define
NumericWithUnit
's
==
method: equivalent numeric, the same unit.
Note that we do not go down to the system level for our ambiguity checks. Why not? Because we're just not going to find two different units in the same measure context with the same identifier. That would just be too unreasonable - while we aren't writing code for the perfect world, we have to assume everything is at least reasonable.
There are two types of
UnitsException
s we generated to deal with ambiguity: one for a
missing
unit - not found in the lookup, and the other for
multiple
units - too many found. While schematic problems could cause both errors, the first could also be a
forward referencing
issue - occuring when we are referencing a unit ahead of its definition. Happily, this problem can be overcome to a large extent by embedding a forward reference solution into the system.
The first thing we need to do is sort out our
UnitsExceptions
:
units.rb a units pipe dream | |
class MissingUnitsException < UnitsException end class AmbiguousUnitsException < UnitsException end |
When we're defining units, if we get a
MissingUnitsException
we want to remember it and revisit it later when more information is present in the system - say, when more units have been defined. The rationale is this: if unit X is defined in terms of unit Y, but unit Y is encountered after unit X, then unit X
will be missing unit Y and an exception will be raised. So, we remember the exception, keep accepting units, and once more units have been defined (possibly containing the definition of unit Y) we can go back and try unit X again. If unit Y
has
now been defined, the definition of unit X will succeed.
Yikes! How do we code such an animal? The happy answer is, we don't have to! I built a general-purpose forward referencing solution a while back that can do all the heavy lifting for us. To use this work, just a few minor changes and code insertions are needed:
units.rb a units pipe dream | |
require 'forward_referencing' class Numeric def method_missing(method,*args) if Units.defining? reference = Units.make_forward_reference(method,Units.defining?) begin value = Units.convert self, method Units.release_forward_reference reference value rescue MissingUnitsException Units.hold_forward_reference rescue UnitsException => exception units_problem("definition",exception,method,args) rescue Exception method_missing_before_units(method,args) end else begin Units.convert self, method rescue UnitsException => exception units_problem("usage",exception,method,args) rescue Exception method_missing_before_units(method,args) end end end def units_problem(state,exception,method,*args) raise exception end end class Units extend ForwardReferencing start_forward_referencing def Units.convert(numeric,unit_identifier) if (candidates = lookup(unit_identifier)).size == 0 raise MissingUnitsException.new(unit_identifier.to_s) elsif !defining? if candidates.size > 1 raise AmbiguousUnitsException.new(unit_identifier.to_s) else unit = candidates[0] NumericWithUnits.new(numeric,unit) end else units = candidates.select { |candidate| @@defining == candidate.units_system.units_measure } case units.size when 0 then raise MissingUnitsException.new(unit_identifier.to_s) when 1 then unit = units[0] NumericWithUnits.new(numeric*unit.equals.numeric,unit.equals.unit) else raise AmbiguousUnitsException.new(unit_identifier.to_s) end end end @@holding = nil def Units.hold_forward_reference(hold = true) @@holding = hold end def Units.holding_forward_reference? @@holding end def Units.make_forward_reference(method,context) @@holding ? nil : create_forward_reference(method,context) end def Units.release_forward_reference(reference = nil) remove_forward_reference(reference) if reference != nil end def Units.establish_forward_reference_context(context) Units.defining context end def Units.clear @@measures.clear @@units.clear forward_references_clear self end end class UnitsMeasure def system(name,&block) Units.defining self system = (self[name] ||= UnitsSystem.new(self,name)) block.call system if block_given? Units.resolve_forward_references Units.defining nil system end end class UnitsSystem def unit(attributes) unit = nil if !Units.holding_forward_reference? unit = UnitsUnit.new self, attributes self[unit.name] = unit Units.add_unit unit else Units.hold_forward_reference nil end Units.continue_forward_reference_resolution unit end end |
The changes are hardly even invasive! We just require the
forward_referencing
feature, and away we go.
Numeric
's
method_missing
method is reorganized with respect to the defining context. If defining, we create a forward reference, try to convert, remove the forward reference if succesful and return the value. If the unit we needed was missing, we hold off on the unit
definition. If some other
Units
-related problem occurred, we report it. Otherwise, we defer to the previous
method_missing
. If not defining, we try to convert and return the value if successul. If a
Units
-related problem occurred, we report it. Otherwise, we defer to the previous
method_missing
.
Numeric
's
units_problem
method simply re-raises the
UnitsException
that occurred. This would likely be overriden for domain-based handling.
Units
is made to extend forward referencing, and starts it up. (The working details of the mechanism are described below.) The
Units.convert
method is adjusted to raise the
MissingUnitsException
and the
AmbiguousUnitException
that are rescued in
Numeric
's
method_missing
method.
The
holding
class variable is added to
Units
to indicate whether a definition should be added and associated or not based on the status of a forward reference. The
Units.hold_forward_reference
method sets the value of the variable, while the
holding_forward_reference?
method reports its value. What's important is that is we've gotten stopped during the definition, we don't want to add any other forward references until we get past it - otherwise we may save forward references that don't have a viable context for
resolution later - that's why we have the
make_forward_reference
and
release_forward_reference
as holding-sensitive covers for the mixed-in
create_forward_reference
and
remove_forward_reference
methods. The
Units.establish_forward_reference_context
method asserts the given context as the defining context.
UnitMeasure
's system method adds a call to
Units.resolve_forward_references
when the system block is finished to try to resolve any unresolved forward references that may have occurred.
UnitSystems
's unit method is adjusted to be sensitive to forward reference holding, and adds a call to
Units.continue_forward_reference_resolution
to jump back into resolution when trying to resolve forward references.
What's happening is this: when we try to define a unit, the
equals
method depends on the resolution of the equality. The equality is in the form of
number.unit_identifier
. The
unit_identifier
is not a instance method in the
Numeric
class, so this fires the
method_missing
method. Since we're defining units, we create a forward reference with the expectation that there may be no unit associated with the method (that is, the
unit_identifier
) that was given. We then try to convert.
The
Units.convert
method looks up the given unit by identifier. If it couldn't be found, a
MissingUnitsException
is raised. Since we're defining, we qualify the units returned by the lookup against the measure being defined. If none of those match, then a
MissingUnitsException
is raised; if more than one was found, then an
AmbiguousUnitsException
is reased. If just one qualified, then a
NumericWithUnits
is created and returned.
Back in
Numeric,
if we didn't get a raise, we remove the forward reference because everything went fine, and we return the value. Now the
unit
method in
UnitsSystem
is called and the
UnitsUnit
is created and appropriately added. But if a
MissingUnitsException
occurred,
Numeric
places a hold on the system and proceeds. If some other type of
UnitsException
occurred (including an
AmbiguousUnitsException
) we're at a loss. We can't recover and we have to handle the problem outside of the system, so the
units_problem
method is called and the
UnitsException
is propagated.
The hold is interesting - the processing can't stop, and since the return type of calling hold is true, that's what gets returned from
method_missing
. It's not a
Numeric
, but that's okay - the
unit
method in
UnitsSystem
is called since we got through the
Numeric
with success. When the
UnitsSystem
's
unit
method is entered, it checks to see if we're holding - which we are. It doesn't create the unit - it just turns off the hold. Since we bypassed all of the effort to get the unit in, we want to turn off the hold so we can try the next unit definition.
In either case, we call the
continue_forward_reference_resolution
method, which at this point is a no-op. We then return the unit that was created.
Note that in this flow, we never removed the forward reference.
Units
, which has been extended by
ForwardReferencing
is accumulating all of these forward references for us. We keep defining and accumulating until finally, the block that's doing the unit definitions returns in
UnitsMeasure
. Now we call the
resolve_forward_references
method - this is where the magic happens! Inside this method, we go back and try to resolve each forward reference, one at a time.
When we get ready to try, the context we established when we created the forward reference (remember - way back at the beginning of
Numeric
) is re-asserted through the
establish_forward_reference_context
method in
Units
. Following this, the resolver jumps back to the spot where we created the forward reference, as if we were trying to handle the
equals
method again! If the unit we need is still undefined, we go through the same mechanism we went through. But if the unit we are looking for has been defined (remember, we didn't stop - we kept going and the definition of the unit we needed may have
since happenned) now
Numeric
will successfully get the converted value, the forward reference will be removed, no hold will be made, and the unit will be created in
UnitSystem
and properly associated. When we now hit the
continue_forward_reference_resolution,
it's no longer a no-op, but instead jumps back into the
resolve_forward_references
method and tries the next forward reference.
Once we've run through all of the forward references, if we did resolve something, we try to resolve again - the resolutions we made may have created the units other forward references depended on. But if nothing got resolved, we've done all we
can and we return. If there are any unresolved forward references left over, we can try them again the next time units are created in the
UnitsMeasure
.
While this may seem complicated, all the dirty work is going on behind the scenes. All we really had to do was add calls to create and remove forward references, start and continue forward reference resolution, and implement a hold mechanism to keep a unit from being defined if it had a forward reference. Nice and clean.
One of the big benefits of the metric units system is the greek prefix. The prefix coming before a base unit multiplies it by a power of ten, making it easy for us to abstract scale away from our numbers. For instance, it's much easier for us to think in centimeters or kilometers than meters for certain scales, the way we would with inches or miles in the english system. However, the conversions are simple in metric (100 cm = 1 m, 1000m = 1 km, 100,000 cm = 1 km) while more difficult in english (12 in = 1 ft, 5280 ft = 1 mi, 63360 in = 1 mi).
Because the greek prefixes can be applied to any unit, we can make them easy to create:
units.rb a units pipe dream | |
class UnitsSystem def unit(attributes) unit = nil if !Units.holding_forward_reference? unit = UnitsUnit.new self, attributes self[unit.name] = unit Units.add_unit unit case unit.greek when :ten then greek_ten unit when :two then greek_two unit end else Units.hold_forward_reference nil end Units.continue_forward_reference_resolution unit end def greek_ten(base) greek_unit base, "yocto", "y", 0.000000000000000000000001 greek_unit base, "zepto", "z", 0.000000000000000000001 greek_unit base, "atto", "a", 0.000000000000000001 greek_unit base, "femto", "f", 0.000000000000001 greek_unit base, "pico", "p", 0.000000000001 greek_unit base, "nano", "n", 0.000000001 greek_unit base, "micro", "u", 0.000001 greek_unit base, "milli", "m", 0.001 greek_unit base, "centi", "c", 0.01 greek_unit base, "deci", "d", 0.1 greek_unit base, "deca", "da", 10.0 greek_unit base, "hecto", "h", 100.0 greek_unit base, "kilo", "k", 1000.0 greek_unit base, "mega", "M", 1000000.0 greek_unit base, "giga", "G", 1000000000.0 greek_unit base, "tera", "T", 1000000000000.0 greek_unit base, "peta", "P", 1000000000000000.0 greek_unit base, "exa", "E", 1000000000000000000.0 greek_unit base, "zetta", "Z", 1000000000000000000000.0 greek_unit base, "yotta", "Y", 1000000000000000000000000.0 end def greek_two(base) greek_unit base, "kilo", "k", 2**10 greek_unit base, "mega", "m", 2**20 greek_unit base, "giga", "g", 2**30 greek_unit base, "tera", "t", 2**40 greek_unit base, "peta", "p", 2**50 greek_unit base, "exa", "e", 2**60 greek_unit base, "zetta", "z", 2**70 greek_unit base, "yotta", "y", 2**80 end def greek_unit(base,prefix,abbrev_prefix,scalar) unit :name => prefix+base.name, :plural => prefix+base.plural, :abbrevs => base.abbrevs.collect { |abbrev| abbrev_prefix+abbrev }, :equals => base.equals * scalar end end |
Hey - what's that
greek_two
method doing there? Well, there's also a common use of greek for powers of two because they're close to powers of ten (103
=~ 210). In fact with the advent of computer technology, the binary-based powers of two use of the prefixes is much a part of out lives as its decimal-based powers of ten precursor.
UnitSystem
's
unit
method is extended to greek the unit if the
greek
attribute is set.
UnitSystem
's
greek_ten
method defines and creates the power-of-ten prefixes and scalar for a unit.
UnitSystem
's
greek_two
method defines and creates the power-of-two prefixes and scalar for a unit.
UnitSystem
's
greek_unit
method applies the given prefixes and scalar to a unit to create a new unit.
The reason that we define the powers of ten the way we do is so the roundoff in Ruby doesn't bite us. Take yocto and yotta for instance:
10**24 - 10.0**24 | ==> | 0.0 |
10**-24 - 10.0**-24 | ==> | 1.83670992315982e-040 |
This all despite the fact that
10**-1 - 10.0**-1 | ==> | 0.0 |
A quick experiment
(1..100).select { |i| 10**-i - 10.0**-i != 0.0 } |
reveals that 23, 24, 25, 26, 28, 29, 32, 34, 39, 42, 45, 49, 50, 54, 56, 60, 63, 66, 72, 75, 81, 82, 84, 88, 91, 97 all have roundoff - that's over one quarter of just the first hundred powers of ten. A bug? Perhaps. But it is quite reproducible. We could delve deeper, but suffice to say that roundoff exists and expressing the number in greek_ten as the full sequence of digits tames the error.
The one thing that's still missing to make this work is scalar multiplication. In the
greek_unit
method, we have to be able to multiply a
NumericWithUnits
by a
Numeric
to get a new
NumericWithUnits
. While we're at it, we'll also throw in division. As we can see, when we multiply by a scalar, we're just multiplying the numeric part and leaving the units alone; we'll do the same with division.
units.rb a units pipe dream | |
class NumericWithUnits def *(value) if value.kind_of? Numeric NumericWithUnits.new(numeric*value,unit) else raise UnitsException.new("units mismatch") end end def /(value) if value.kind_of? Numeric NumericWithUnits.new(numeric/value,unit) else raise UnitsException.new("units mismatch") end end end |
For both methods, if it's a
Numeric
we're applying, we create a new instance with its numeric part modified. At this time, we raise a
UnitsException
if the value isn't
Numeric
- we'll adjust this shortly.
Okay! There's one more stop in this part of our journey: derived units! How do we represent units whose measures are derived?
Well, it just so happens that we have most of the pieces in place - we just need to use the derivation we tucked away in the
UnitsMeasure
to rationalize the process. But before we do, we need to make it a little easier on ourselves and do some refactoring.
Right now, our
NumericWithUnits
holds a
Numeric
and a
UnitsUnit
. Derivations are based on a
collection
of units that are raised to powers and multiplied together. We can do this in a
Hash
that is analogous to the work we did previously to accomodate derived measures. In this case the keys are
UnitsUnit
instances and the values are powers. Since this envelopes our previous representation as a
Hash
with a single element whose key is a
UnitsUnit
associated with a value of 1, there's no loss of functionality.
Because we know that we're tucking away
UnitUnit
s and powers in this
Hash
, we'll specialize it to include some appropriate behaviors.
units.rb a units pipe dream | |
class UnitsHash < Hash def initialize(unit = nil,power = 1) if unit if unit.kind_of? UnitsUnit self[unit] = power elsif unit.kind_of? UnitsHash unit.each { |k,v| self[k] = v*power } else raise UnitsException.new("invalid unit: #{unit}") end end end def to_s(numeric = 1,use_abbrevs = false) if size == 1 && (su = select {|k,v| v == 1}).size == 1 && !use_abbrevs su = su.to_a[0][0] (numeric == 1)? su.name : su.plural else abbrevs_to_s end end def abbrevs_to_s p = select {|k,v| v > 0}.sort.collect {|k,v| "#{k.abbrevs[0]}#{(v == 1) ? "" : "**#{v}"}"} n = select {|k,v| v < 0}.sort.collect {|k,v| "#{k.abbrevs[0]}#{(v == -1) ? "" : "**#{-v}"}"} numerator = (p.size > 0)? p.join(" ") : (n.size > 0)? "1" : "" denominator = (n.size > 0)? ((p.size > 0)? " / " : "/")+n.join(" ") : "" "#{numerator}#{denominator}" end def power!(power) self.each { |k,v| self[k] = v*power } end def **(power) clone.power!(power) end def merge!(value,power=1) value.unit.each { |k,v| self[k] = (self[k] || 0) + v*power delete k if self[k] == 0 } self end def merge(value,power=1) clone.merge!(value,power) end alias :merge :* end |
We've captured the laws of exponents in here, as well as hijacked and expanded the code for printing unit names. The
initialize
method returns a new instance built from a
UnitsUnit
or another
UnitsHash
, optionally raised to a power. If no arguments are given, an empty instance is created. The
to_s
method returns a string representation of the instance. The caller can optionally provide a
Numeric
to have pluralization considered, and also indicate whether unit abbreviations will be used - but only in the case of a non-derived unit with a power of 1; otherwise, abbreviations will be used. The
abbrevs_to_s
method returns a string representation of the instance using abbreviations, formed as a numerator of units with positive powers over a denominator of units with positive powers. The
power!
method extends the power of each unit by the given power. The
exponentiation
operator returns a copy of the
UnitsHash
with the power of each unit extended by the given power - this is effectively unit exponentiation. The
merge!
method coallesces a given
UnitsHash
into the instance, adding in members that don't exist, extending members that do exist, and removing members whose powers become zero. Of course, we could keep the zero powers around, but since anything raised to the zeroth power is 1, we really
don't need them anymore. The
merge
method coallesces a given
UnitsHash
into a copy of the instance. We alias the multiplication operation to this method - this is effectively unit multiplication.
By pushing the unit string representation code into
UnitsHash
we can simplify
NumericWithUnits
a little.
units.rb a units pipe dream | |
class NumericWithUnits def initialize(numeric,unit) @numeric, @unit = numeric, units_hash(unit) end def units_hash(unit) (unit.kind_of? UnitsHash)? unit : UnitsHash.new(unit) end def to_s "#{numeric} #{unit.to_s(numeric)}" end end |
The
initialize
method sets the numeric as before, but calls the
units_hash
method to rationalize the unit argument. The
units_hash
method returns the argument if it is a
UnitsHash,
or otherwise creates a new
UnitsHash
built using it. The
to_s
method is simplified to return a string representation of the instance as the numeric and the String returned by the
UnitHash
's
to_s
method.
We now have everything set to create derived units. Amazingly enough, this simply amounts to extending and defining methods in
NumericWithUnits
.
units.rb a units pipe dream | |
class NumericWithUnits def *(value) if value.kind_of? Numeric NumericWithUnits.new(numeric*value,unit) elsif value.kind_of? NumericWithUnits extend_units(value,1) else raise UnitsException.new("units mismatch") end end def /(value) if value.kind_of? Numeric NumericWithUnits.new(numeric/value,unit) elsif value.kind_of? NumericWithUnits extend_units(value,-1) else raise UnitsException.new("units mismatch") end end def **(value) if value.kind_of? Numeric extend_units(nil,value) else raise UnitsException.new("units mismatch") end end def align(target) factor = 1 target_unit = UnitsHash.new target.unit.each do |tu,tv| su = target.unit.keys.select {|u| u.units_measure == tu.units_measure} if su.size == 1 factor *= (tu.equals.numeric/su[0].equals.numeric)**tv target_unit[su[0]] = tv else target_unit[tu] = tv end end NumericWithUnits.new(numeric*factor,target_unit) end def extend(units,power) if !units NumericWithUnits.new(numeric**power,unit**power) else value = align(units) NumericWithUnits.new(value.numeric*(units.numeric**power), value.unit.merge(units,power)) end end end |
We're basically expanding the multiplication and division operators to handle
NumericWithUnits
, adding an exponetiation operator, and throwing in a few methods to do the real work. The multiplication operator extends the instance by multiplying by the given
NumericWithUnits
value with a power of 1. The division operator extends the instance by multiplying by the given
NumericWithUnits
value with a power of -1. The exponentiation operator extends the instance by raising the instance to the given power. Note that we only raise to numeric powers - raising to a value with units is just a bit too exotic. The align method returns a new
NumericWithUnits
that represents the target with it's units aligned to the instance. What this means is that for each of the instance's constituent units, a unit in the target with the same measure is promoted to that unit and it's numeric part is appropriately
scaled. This effectively pulls all units in the same measure together. The extend method does double duty, either raising the instance to a power by rasing the numeric and unit part of the instance each to a power (if the units argument is nil) or
multiplying the instance by the given numeric with units raised to the given power. The units are aligned prior to multiplication to make sure we can eliminate any units that end up with powers of 0.
So there we are. We can now define derived units appropriately. That covers all of our bases for unit definition and we'll move on to using units.
Using numbers with units should be as simple as using numbers. They should do all of the same things as regular Ruby Numerics as transparently as possible.
Before we start using
Units
, we'll need a starting set of unit definitions to draw from. While I'm keen to drop in a whole load of definitions, I'll keep things simple here, and add definitions in later sections as needed while we move forward.
We'll use the length measure in the english system that we set up earlier. We already built inches, feet and yards so we'll just go with those as our base.
First we'll try a few simple identities:
puts 72.inches | ==> | '72 inches' |
puts 6.feet | ==> | '6 feet' |
puts 2.yards | ==> | '2 yards' |
This is just the sort of output we were looking for.
Note that I'll be using a representation here with Ruby code to the left of the arrow and output to the right in a schematized form (numbers are plain, hashes are surronded by curly braces, arrays by square brackets, string output by quotes, etc.) I'm just trying to be clear - if you know even a little Ruby, you'll get the idea.
Lets see if our multiplication, division and exponentiation do what they should too.
puts 72.inches/10 | ==> | '7 inches' |
puts 6.feet*5 | ==> | '30 feet' |
puts 2.yards**2 | ==> | '4 yd**2' |
Interesting. While the second two statements seemed to work fine, the result was truncated in the first. Shouldn't we have gotten
7.2 inches
? Well, yes and no. In the human world we expect the fraction, but Ruby is only doing what we tell it to do according to well-established rules. When we use integer arithmetic (FixNum
instances in Ruby) we get truncation. To do what we want, we need to make our
NumericWithUnits
result use real numbers.
Fortunately, this is easy to do.
puts 72/10 | ==> | '7' |
puts 72.inches/10 | ==> | '7 inches' |
puts 72.0/10 | ==> | '7.2' |
puts 72.0.inches/10 | ==> | '7.2 inches' |
Take a look - we're consistent with Ruby arithmetic without units. This is precisely the kind of behavior we want! Let's try a few more things.
puts 72.inches/2.feet | ==> | '3.0' |
puts 6.feet/2.feet | ==> | '3.0' |
puts 2.yards/2.feet | ==> | '3.0' |
This makes sense - the result is the ratio between the two values - a unitless number. Also notice that here - where two
NumericWithUnits
are involved, we automatically get conversions to a real number. However, even though we can't see it, the value is a
NumericWithUnits
.
puts (72.inches/2.feet).class.name | ==> | 'NumericWithUnits' |
While this works - and it does need to continue to work - we probably should do a quick repair to
NumericWithUnits
's extend method and get back a
Numeric
in these cases.
units.rb a units pipe dream | |
class NumericWithUnits def extend(units,power) if !units NumericWithUnits.new(numeric**power,unit**power) else value = align(units) extended_numeric = value.numeric*(units.numeric**power) extended_unit = value.unit.merge(units,power) (extended_unit.size == 0)? extended_numeric : NumericWithUnits.new(extended_numeric,extended_unit) end end end |
This is a route back to
Numeric
instances from
NumericWithUnits
intstances - a number without units really should be a
Numeric
. Appropriately, evaluating the expression now gives
puts (72.0.inches/2.feet).class.name | ==> | 'Float' |
Here's another thing:
puts 12.inches*12.inches | ==> | '144.0 in**2' |
puts 12.inches*1.foot | ==> | '1.0 ft**2' |
puts 1.foot*12.inches | ==> | '144.0 in**2' |
puts 1.foot*1.foot | ==> | '1.0 ft**2' |
The way things are set up, when multiplying (or dividing) the resulting units are aligned to those of the second value. The values are equalvalent, of course: 144 square inches is equal to 1 square foot since 12 inches is a foot. Take a look:
puts (1.0.ft == 12.0.in) | ==> | 'false' |
Huh? Wait a second. That's not right. We missed something - we have to align units so we aren't comparing apples and oranges. We need to clean up the
==
operator.
units.rb a units pipe dream | |
class NumericWithUnits def ==(value) value = align(value) (value.numeric == numeric) && (value.unit.equal? unit) end end |
Ok. Let's try again.
puts (1.0.ft == 12.0.in) | ==> | 'true' |
That's better. So, like we said, 144 square inches is equal to 1 square foot.
puts (1.0.ft**2 == 144.0.in**2) | ==> | 'false' |
What the heck? Let's take a closer look:
puts 1.0.ft**2 | ==> | '1.0 ft**2' |
puts 144.0.in**2 | ==> | '20736.0 in**2' |
Damn. See what's happening?
144**2 = 20736
. The
NumericWithUnits
144.inches
is being created prior to raising it to the second power - which squares both the numeric and unit parts. Friends,
this
is an ugly one. In fact, there's no good way to deal with it - it's one of those shorthands humans use all the time that defy the rules of mathematical logic. We could handle it by applying the
**
operator to only the units part of the
NumericWithUnits
, but that doesn't work because we'd lose the ability to raise the whole instance to a power, and we don't want that to happen. What do we do?
Well, the first thing we'll do is make a longhanded version of our initializer.
units.rb a units pipe dream | |
class NumericWithUnits def initialize(numeric,unit,power=1) @numeric, @unit = numeric, units_hash(unit)**power end end |
This will at least let us get the right result during creation if we're fastidious enough.
puts NumericWithUnits.new(144.0,Unit.length.english.inch,2) | ==> | '144.0 in**2' |
Unfortunately, this is impossibly painful for regular use. If we look a little more closely at our original problem, we can see that what we get is
(144.0.inches)**2
instead of what we want
144.0*(inches**2)
because of the way Ruby handles the operator precedence. In fact, there aren't any operators we can define that get evaluated ahead of the dereference ('dot') operator. We need to resort to extreme measures and do something depraved and shameful. We need to define a specialized unit-exponentiation operator.
units.rb a units pipe dream | |
class NumericWithUnits def ^(value) if value.kind_of? Numeric NumericWithUnit.new(numeric,unit,value) else raise UnitsException.new("units mismatch") end end end |
Yeah, this is ugly - we're re-using the exclusive-or ('hat') operator as the power operator. But you know what? Tough. The exclusive-or is a logical, set-theoretical operator - it only has a loose interpretation in the numeric domain. In fact, we'll adopt a convention for units - we'll make them always use the 'hat' notation for unit powers.
units.rb a units pipe dream | |
class UnitsHash def abbrevs_to_s p = select {|k,v| v > 0}.sort.collect {|k,v| "#{k.abbrevs[0]}#{(v == 1) ? "" : "^#{v}"}"} n = select {|k,v| v < 0}.sort.collect {|k,v| "#{k.abbrevs[0]}#{(v == -1) ? "" : "^#{-v}"}"} numerator = (p.size > 0)? p.join(" ") : (n.size > 0)? "1" : "" denominator = (n.size > 0)? ((p.size > 0)? " / " : "/")+n.join(" ") : "" "#{numerator}#{denominator}" end end |
Finally, we have something that we can do the job for us.
puts 1.0.ft^2 | ==> | '1.0 ft^2' |
puts 144.0.in^2 | ==> | '144.0 in^2' |
puts (1.0.ft^2 == 144.0.in^2) | ==> | 'true' |
Note that we didn't replace exponentiation - we still need it! We just have a different symbol to use to just exponentiate units. The biggest problem we have by doing this is that the operator precedence gets messed up - the 'hat' has a precedence below the rest of arithmetic. This is really ugly - it means that something like
1.0.ft^2 * 1.0.ft
that would be a perfectly good expression to use to extrude a volume from an area would be evaluated as
1.0.ft^(2 * 1.0.ft)
since multiplication has higher precedence than exclusive-or. Unfortunately, because the precedence of Ruby operators is not configurable, there is no way to recover from this one without parenthesis. We need to do it like
(1.0.ft^2) * 1.0.ft
using parenthesis to enforce the correct evaluation order. Since many of the unit uses will be defined as variable expressions
a = 1.0.ft^2
l = 1.0.ft
v = a*l
this shouldn't be a problem most of the time. But in all honesty, I feel a little dirty after doing this.
There is another way to look at the problem that we need to consider:
144.0 * 1.inch**2
This is a valid expression that unfortunately wont't work. Why? Because, when the * operator gets called, it's getting called on the
Numeric
, not the
NumericWithUnits
. Let's go fix that up.
units.rb a units pipe dream | |
class NumericWithUnits def NumericWithUnits.create_commutative_operators(klass) code = <<operatorsEND alias_method :old_multiply, :* def *(value) (value.kind_of? NumericWithUnits) ? value*self : old_multiply(value) end alias :old_divide :/ def /(value) (value.kind_of? NumericWithUnits) ? (value**-1)*self : old_divide(value) end operatorsEND klass.class_eval code end end NumericWithUnits.create_commutative_operators Fixnum NumericWithUnits.create_commutative_operators Bignum NumericWithUnits.create_commutative_operators Float |
Here we're using a little bit of metaprogramming - we've written some code that writes code, and when evaluated adds method to a class. What the code does is modifies the computation when the argument is a
NumericWithUnits
, appropriately reversing the order of the operation; otherwise, everything is left alone. Since multiplication (and by using the reciprocal, division) is commutative, the result is independent of the order of the values.
We create the operations for Ruby's fixed, big fixed and real number classes.
puts 144.0 * 1.in^2 | ==> | '144.0 in^2' |
puts 2.0 / 3.0.ft^3 | ==> | '0.666666666666667 1/ft^3' |
Sweet. It looks like everything's going fine! Multiplication and division work great, but we'll need parenthesis a little more often, when we use the 'hat' operation in an out-of-precedence position.
You might have thought we should have done addition before multiplication, but not so. The twist in addition (and subtraction) is that you can't add two numbers with units unless they have the same units - the result doesn't make sense
mathematically. What does
3.feet + 2.seconds
mean? You just don't get a reasonable answer. Sure, we might be able to come up with something meaningful, but it'd be a stretch. We want to keep things making sense or this effort will be for naught.
Fortunately, we built most of the mechanicism we need to do this when we were working on multiplication. We define addition and subtraction and modify our align method to do just what we need. First we'll adjust the
align
method.
units.rb a units pipe dream | |
class NumericWithUnits def align(target,all=true) factor = 1 target_unit = UnitsHash.new target.unit.each do |tu,tv| su = target.unit.keys.select {|u| u.units_measure.equal? tu.units_measure } if su.size == 1 factor *= ((1.0*tu.equals.numeric)/su[0].equals.numeric)**tv target_unit[su[0]] = tv else target_unit[tu] = tv end end raise UnitsException.new("units mismatch") if all && (unit != target_unit) NumericWithUnits.new(numeric*factor,target_unit) end end |
We'll add an argument to the method:
all
will indicate that the resulting units of the target must equal the instance's units. The code is the same except for the inclusion of raising a
UnitsException
if this condition is
false
. While we're at it, we'll also make sure we don't lose fractional parts when the numeric parts coming in are integers.
Now we can add the addition and subtraction operations, but we have to adjust our call to
align
in the
extend
method. We'll also throw in a unary plus and minus for compatability and to let us implement subtraction as addition.
units.rb a units pipe dream | |
class NumericWithUnits def +@ clone end def -@ value = clone value.numeric = -(value.numeric) value end def +(value) if value.kind_of? NumericWithUnits aligned_value = align(value) aligned_value.numeric += value.numeric aligned_value else raise UnitsException.new("units mismatch") end end def -(value) self + (-value) end def extend(units,power) if !units NumericWithUnits.new(numeric**power,unit**power) else value = align(units,false) extended_numeric = value.numeric*(units.numeric**power) extended_unit = value.unit.merge(units,power) (extended_unit.size == 0)? extended_numeric : NumericWithUnits.new(extended_numeric,extended_unit) end end end |
Now when we try to add one
NumericWithUnits
to another, the units are aligned prior to performing the addition, to make sure the numeric part we're adding to has been scaled appropriately. If the units don't match, the
align
method throws a
UnitsException
.
puts (2.0.ft^2) + (3.0.in^2) | ==> | '291.0 in^2' |
puts (1.0.yd^2) - (1.0.ft^2) | ==> | '8.0 ft^2' |
Continuing on this path, does it make any sense to add a unitless value to a value with units? Technically no, but practically yes. In the real world, "5 inches plus 2" is 7 inches; the 2 is assumed to be in inches because people are smart and can
make that connection. Of course programmers are smart people too and we want to make their lives easier, so we'll also mimic the human interpretation in our Ruby code. We just need a slight adustment in our
add
method.
units.rb a units pipe dream | |
class NumericWithUnits def +(value) if value.kind_of? Numeric NumericWithUnits.new(numeric+value,unit) elsif value.kind_of? NumericWithUnits aligned_value = align(value) aligned_value.numeric += value.numeric aligned_value else raise UnitsException.new("units mismatch") end end end |
This gives us our desired behavior.
puts 5.0.inches + 2 | ==> | '7.0 inches' |
puts 5.0.inches - 2 | ==> | '3.0 inches' |
Of course, we should also make sure to add the commutative operations
units.rb a units pipe dream | |
class NumericWithUnits def NumericWithUnits.create_commutative_operators(klass) code = <<operatorsEND alias_method :old_multiply, :* def *(value) (value.kind_of? NumericWithUnits) ? value*self : old_multiply(value) end alias :old_divide :/ def /(value) (value.kind_of? NumericWithUnits) ? (value**-1)*self : old_divide(value) end alias_method :old_add, :+ def +(value) (value.kind_of? NumericWithUnits) ? value+self : old_add(value) end alias :old_subtract :- def -(value) (value.kind_of? NumericWithUnits) ? (-value)+self : old_subtract(value) end operatorsEND klass.class_eval code end end |
So that
puts 2 + 5.0.inches | ==> | '7.0 inches' |
puts 2 - 5.0.inches | ==> | '-3.0 inches' |
works too, which despite being a little more tenuous in human usage should really be there for mathematical completeness. Otherwise, of course, someone will try it, get an error, and wonder why.
A
NumericWithUnits
should, like its
Numeric
breathren, be comparable. We should be able to determine whether one is less than, greater than, less than or equal to, greater than or equal to, equal to or not equal to another. Ruby has a nice clean shorthand for this, the
compare
operator and companion
Comparable
module.
What we need to do is define the
<=>
operator and mix in
Comparable
. The really neat thing is, since
Comparable
gives us the
==
method, we can remove it from the class and leverage the one that's provided by the module.
units.rb a units pipe dream | |
class NumericWithUnits understands Comparable def <=>(value) if value.kind_of? Numeric numeric <=> value elsif value.kind_of? NumericWithUnits align(value).numeric <=> value.numeric else raise UnitsException.new("units mismatch") end end def NumericWithUnits.create_commutative_operators(klass) code = <<operatorsEND alias_method :old_multiply, :* def *(value) (value.kind_of? NumericWithUnits) ? value*self : old_multiply(value) end alias :old_divide :/ def /(value) (value.kind_of? NumericWithUnits) ? (value**-1)*self : old_divide(value) end alias_method :old_add, :+ def +(value) (value.kind_of? NumericWithUnits) ? value+self : old_add(value) end alias :old_subtract :- def -(value) (value.kind_of? NumericWithUnits) ? (-value)+self : old_subtract(value) end alias :old_compare :<=> def <=>(value) (value.kind_of? NumericWithUnits) ? (value <=> self) : old_compare(value) end alias :old_gt :> def >(value) (value.kind_of? NumericWithUnits) ? (value < self) : old_gt(value) end alias :old_lt :< def <(value) (value.kind_of? NumericWithUnits) ? (value > self) : old_lt(value) end alias :old_gteq :>= def >=(value) (value.kind_of? NumericWithUnits) ? (value <= self) : old_gteq(value) end alias :old_lteq :<= def <=(value) (value.kind_of? NumericWithUnits) ? (value >= self) : old_lteq(value) end alias :old_eq :== def ==(value) (value.kind_of? NumericWithUnits) ? (value == self) : old_eq(value) end operatorsEND klass.class_eval code end end |
Notice that we included the unitless variants as well and expanded the commutative operations to include comparison. The question is why did we need to include the >, <, >=, <= and == in the commutative operations instead of just <=>? The
answer is that for
Fixnum
,
Bignum
and
Float
instances, the individual operations are defined separately for performance reasons. We must override them to alter the behavior for
NumericWithUnits
instances - changing
<=>
isn't enough.
puts 13.inches <=> 1.foot | ==> | '1' |
puts 12.inches <=> 1.foot | ==> | '0' |
puts 11.inches <=> 1.foot | ==> | '-1' |
puts 13.inches > 1.foot | ==> | 'true' |
puts 12.inches > 1.foot | ==> | 'false' |
puts 11.inches > 1.foot | ==> | 'false' |
puts 13.inches < 1.foot | ==> | 'false' |
puts 12.inches < 1.foot | ==> | 'false' |
puts 11.inches < 1.foot | ==> | 'true' |
puts 13.inches >= 1.foot | ==> | 'true' |
puts 12.inches >= 1.foot | ==> | 'true' |
puts 11.inches >= 1.foot | ==> | 'false' |
puts 13.inches <= 1.foot | ==> | 'false' |
puts 12.inches <= 1.foot | ==> | 'true' |
puts 11.inches <= 1.foot | ==> | 'true' |
puts 13.inches == 1.foot | ==> | 'false' |
puts 12.inches == 1.foot | ==> | 'true' |
puts 11.inches == 1.foot | ==> | 'false' |
Voila! We now implement comparison, with all the appropriate variants. You can try the unitless variants yourself if you'd like; the results are the same.
Since the code for that section is really smelling funny because it's geting so long, we'll do a quick refactoring.
units.rb a units pipe dream | |
class NumericWithUnits def NumericWithUnits.commutative_operator(op,old,calc) ([] << "alias :old_#{old} :#{op}" << "def #{op}(value)" << " (value.kind_of? NumericWithUnits) ? #{calc} : old_#{old}(value)" << "end"). join("\r\n") end def NumericWithUnits.create_commutative_operators(klasses) commutative_operators = ([] << commutative_operator( "*", "multiply", "value * self" ) << commutative_operator( "/", "divide", "(value**-1) * self" ) << commutative_operator( "+", "add", "value + self" ) << commutative_operator( "-", "subtract", "(-value) + self" ) << commutative_operator( "<=>", "compare", "-(value <=> self)" ) << commutative_operator( ">", "gt", "(value < self)" ) << commutative_operator( "<", "lt", "(value > self)" ) << commutative_operator( ">=", "gteq", "(value <= self)" ) << commutative_operator( "<=", "lteq", "(value >= self)" ) << commutative_operator( "==", "eq", "(value == self)" )). join("\r\n") klasses.each { |klass| klass.class_eval commutative_operators } end end NumericWithUnits.create_commutative_operators [ Fixnum, Bignum, Float ] |
That's much cleaner now.
There's one other mathematical operator we need to include: the modulo operation, which returns the remainder of a division. This one isn't too bad, but it's too bizarre a calculation to do simply in commutative form - so we'll use a helper.
units.rb a units pipe dream | |
class NumericWithUnits def %(value) if value.kind_of? Numeric NumericWithUnits.new(numeric % value,unit) elsif value.kind_of? NumericWithUnits aligned_value = align(value) aligned_value.numeric = aligned_value.numeric % value.numeric aligned_value else raise UnitsException.new("units mismatch") end end def inv_mod(value) if value.kind_of? Numeric NumericWithUnits.new(value % numeric,unit) elsif value.kind_of? NumericWithUnits aligned_value = align(value) aligned_value.numeric = value.numeric % aligned_value.numeric aligned_value else raise UnitsException.new("units mismatch") end end def NumericWithUnits.create_commutative_operators(klasses) commutative_operators = ([] << commutative_operator( "*", "multiply", "value * self" ) << commutative_operator( "/", "divide", "(value**-1) * self" ) << commutative_operator( "+", "add", "value + self" ) << commutative_operator( "-", "subtract", "(-value) + self" ) << commutative_operator( "%", "modulo", "value.inv_mod(self)" ) << commutative_operator( "<=>", "compare", "-(value <=> self)" ) << commutative_operator( ">", "gt", "(value < self)" ) << commutative_operator( "<", "lt", "(value > self)" ) << commutative_operator( ">=", "gteq", "(value <= self)" ) << commutative_operator( "<=", "lteq", "(value >= self)" ) << commutative_operator( "==", "eq", "(value == self)" )). join("\r\n") klasses.each { |klass| klass.class_eval commutative_operators } end end |
This gives us
puts 5.feet % 7.inches | ==> | '4.0 inches' |
puts 27.inches % 2.feet | ==> | '0.25 feet' |
puts 12.inches % 5 | ==> | '2 inches' |
puts 12 % 9.inches | ==> | '3 inches' |
which is just what we expected.
A comparison operation that doesn't come as part of the Ruby core, but one that I've personally found to be extremely useful is comparing to see if two numbers are approximately equal to each other. Approximate equality is tested with the
approximately_equals?
method and the
=~
operator that are defined in my eymiha_math rubygem.
The problem occurs when we consider two values that are close to each other, but not exact. Consider the length of the diagonal of a two-foot square:
a = (2.feet^2)**0.5 b = 16.9705627476.inches |
||
puts a == b | ==> | 'false' |
Yeah, right. The two values are equal for all practical purposes. The difference is miniscule.
puts a - b | ==> | '8.77143691013771e-10 inches' |
The idea behind approximate equality is that if the distance between two values is less than some "measurement error" threshhold (called
epsilon
in Mathematics) then for all practical purposes the two values are equal. If we download the eymiha_math gem and
require 'approximately_equals'
in our code, we can check the relationship with
Numeric
s. This can be easily extended to
NumericWithUnits
.
units.rb a units pipe dream | |
require 'approximately_equals' class NumericWithUnits def approximately_equals?(value,epsilon=Numeric.epsilon) if value.kind_of? Numeric numeric.approximately_equals?(value,epsilon) elsif value.kind_of? NumericWithUnits align(value).numeric.approximately_equals?(value.numeric,epsilon) else raise UnitsException.new("units mismatch") end end alias :=~ :approximately_equals? def NumericWithUnits.create_commutative_operators(klasses) commutative_operators ||= ([] << commutative_operator( "*", "multiply", "value * self" ) << commutative_operator( "/", "divide", "(value**-1) * self" ) << commutative_operator( "+", "add", "value + self" ) << commutative_operator( "-", "subtract", "(-value) + self" ) << commutative_operator( "%", "modulo", "value.inv_mod(self)" ) << commutative_operator( "<=>", "compare", "-(value <=> self)" ) << commutative_operator( ">", "gt", "(value < self)" ) << commutative_operator( "<", "lt", "(value > self)" ) << commutative_operator( ">=", "gteq", "(value <= self)" ) << commutative_operator( "<=", "lteq", "(value >= self)" ) << commutative_operator( "==", "eq", "(value == self)" ) << commutative_operator( "=~", "approxeq", "(value =~ self)" )). join("\r\n") klasses.each { |klass| klass.class_eval commutative_operators } end end |
The operation works just like equality, with a little fudge thrown in:
puts a =~ b | ==> | 'true' |
The epsilon value can be changed to adjust the approximate equality threshhold.
While addition, subtraction, multiplication, division, exponentiation, modulo and comparison are all core mathematical operations for dealing with units, there are some more methods we need if we'd like them to act like regular
Numeric
types.
Happily we don't have to write them. Instead we'll get them by piggybacking on the numeric part of a
NumericWithUnits
, forwarding the call using
method_missing
.
units.rb a units pipe dream | |
class NumericWithUnits def method_missing(method,*args) NumericWithUnits.new(numeric.send(method,*args),unit) end end |
Using a smattering of calls we know are in
Numeric
,
puts(10.4.inches).ceil | ==> | '11 inches' |
puts(-10.4.inches).ceil | ==> | '-10 inches' |
puts(10.4.inches).floor | ==> | '10 inches' |
puts(-10.4.inches).floor | ==> | '-11 inches' |
puts(10.4.inches).abs | ==> | '10.4 inches' |
puts(-10.4.inches).abs | ==> | '10.4 inches' |
puts(10.4.inches).round | ==> | '10 inches' |
puts(-10.4.inches).round | ==> | '-10 inches' |
We're going to use the
method_missing
method a lot more, but we'll keep this as the last step to make sure we fall through to the underlying
Numeric
's methods when we don't recognize the intended method.
In our code we frequently need to check to see if we have the right kind of object before execute some code; we use the
kind_of?
method to do this. This discrimination violates the dynamic spirit of Ruby in a way - but it's needed because objects don't all behave the same way and we sometime really need to know what we're dealing with before we do something. Ruby advocates
"duck typing" in which methods are called on objects and any fallout that happens is handled by the receiver. The terminology comes from the old expression, "if it walks like a duck, and quacks like a duck, it must be a duck."
In our case, if an object can respond to a method, it must be something that could handle the method. Doing this gives the receiver the opportunity to figure out how to perform the function is it's so inclined, either by calling other methods,
delegating the call to another object, or creating new code to do the work. We want
NumericWithUnits
to act like a
Numeric
, and we've done that. For all practical purposes,
NumericWithUnits
is a
Numeric
duck.
The problem is that
NumericWithUnits
does not
inherit
from
Numeric
. This means that if some code interrogates an instance of
NumericWithUnits
to see if its a
Numeric
10.inches.kind_of? Numeric
the value returned would be
false
. We'd like this to be
true
despite the inheritence. Fortunately, we can.
units.rb a units pipe dream | |
class NumericWithUnits alias :old_kind_of? :kind_of? def kind_of?(klass) (numeric.kind_of? klass)? true : old_kind_of?(klass) end end |
We redirect the request to the numeric because that's what makes the instance compatible with the
Numeric
. So now,
puts 10.inches.kind_of? Numeric | ==> | 'true' |
puts 10.inches.kind_of? Fixnum | ==> | 'true' |
puts 10.0.inches.kind_of? Float | ==> | 'true' |
puts 10.0.inches.kind_of? String | ==> | 'false' |
You have to love Ruby. Faking out the inheritence tree is just too darned cool.
Of course, this means that we need to rearrange some of our methods - whenever we discriminate between a
NumericWithUnits
and a
Numeric
, we need to check for the
NumericWithUnits
first since both of the
kind_of?
tests on a
NumeriWithUnits
will now return true.
units.rb a units pipe dream | |
class NumericWithUnits def <=>(value) if value.kind_of? NumericWithUnits align(value).numeric <=> value.numeric elsif value.kind_of? Numeric numeric <=> value else raise UnitsException.new("units mismatch") end end def approximately_equals?(value,epsilon=Numeric.epsilon) if value.kind_of? NumericWithUnits align(value).numericapproximately_equals?(value.numeric,epsilon) elsif value.kind_of? Numeric numeric.approximately_equals?(value,epsilon) else raise UnitsException.new("units mismatch") end end def +(value) if value.kind_of? NumericWithUnits aligned_value = align(value) aligned_value.numeric += value.numeric aligned_value elsif value.kind_of? Numeric NumericWithUnits.new(numeric+value,unit) else raise UnitsException.new("units mismatch") end end def *(value) if value.kind_of? NumericWithUnits extend(value,1) elsif value.kind_of? Numeric NumericWithUnits.new(numeric*value,unit) else raise UnitsException.new("units mismatch") end end def /(value) if value.kind_of? NumericWithUnits extend(value,-1) elsif value.kind_of? Numeric NumericWithUnits.new(numeric/value,unit) else raise UnitsException.new("units mismatch") end end def **(value) if value.kind_of? Numeric && !(value.kind_of? NumericWithUnits) extend(nil,value) else raise UnitsException.new("units mismatch") end end def ^(value) if value.kind_of? Numeric && !(value.kind_of? NumericWithUnits) NumericWithUnits.new(numeric,unit,value) else raise UnitsException.new("units mismatch") end end def %(value) if value.kind_of? NumericWithUnits aligned_value = align(value) aligned_value.numeric = aligned_value.numeric % value.numeric aligned_value elsif value.kind_of? Numeric NumericWithUnits.new(numeric % value,unit) else raise UnitsException.new("units mismatch") end end def inv_mod(value) if value.kind_of? NumericWithUnits aligned_value = align(value) aligned_value.numeric = value.numeric % aligned_value.numeric aligned_value elsif value.kind_of? Numeric NumericWithUnits.new(value % numeric,unit) else raise UnitsException.new("units mismatch") end end end |
This rearrangement preserves the correct behavior for the operations.
The last simple-use detail we need to deal with is making it easier to construct values with units in a generic method - basically, a way to deal with units as a variable that can be applied to numbers.
What we'll do is add one more method to
Numeric
, that will give us back a
NumericWithUnits
.
units.rb a units pipe dream | |
class Numeric def unite(unit=nil,power=1,measure=nil) if !unit self else if (unit.kind_of? String)||(unit.kind_of? Symbol) units = Units.lookup(unit) units = units.select {|u| u.units_measure == measure} if units.size > 1 if units.size == 0 units_problem("usage",MissingUnitsException.new(unit),:unit=,unit) elsif units.size == 1 unit = units[0] else units_problem("usage",AmbiguousUnitsException.new(unit),:unit=,unit) end elsif unit.kind_of? NumericWithUnit unit = unit.unit end NumericWithUnits.new(self,unit,power) end end end |
The
unite
method is named for
unit-extend, but could also have the meaning of
unite
as in uniting a
Numeric
with a unit. It's versatile, being able to handle a string or symbol to indicate a unit name, or the unit of a given
NumericWithUnit
, or through the
NumericWithUnits
initializer, a
UnitsUnit
or a
UnitsHash
. This method tries to give you a meaningful value if you give it anything that has almost any relation to a unit. And if the unit being passed in is
nil
, it returns the
Numeric
's value.
inch = Units.length.english.inch | ||
n = 10 | ||
puts n.unite inch | ==> | '10 inches' |
puts n.unite "inch" | ==> | '10 inches' |
puts n.unite :inch | ==> | '10 inches' |
puts n.unite 1.inch | ==> | '10 inches' |
puts n.unite 1.inch.unit | ==> | '10 inches' |
puts n.unite nil | ==> | '10' |
Note that when a
String
or
Symbol
is given, the unit is looked up - which can result in a missing or ambiguous exception. If it's ambiguous, we've designed it to try to find the intended unit if a measure is provided as context.
There is one thing though - calling
unite
on a
NumericWithUnits
will fire its
method_missing
method and use the
unite
method in
Numeric
. Because the
unite
method is already sensative to units, we need to avoid this delegation and define a
unite
method in
NumericWithUnits
; otherwise given the way
unite
is defined in
Numeric,
we'll get an invalid result. The question is, what should the correct result be?
units.rb a units pipe dream | |
class NumericWithUnits def unite(target_unit=nil,power=1,measure=nil) numeric.unite(target_unit,power,measure) end end |
Since
unite
in
Numeric
is effectively "assigning" the given unit to it's value, we'll make
unite
do the same thing with a
NumericWithUnits
instance - it'll return a
NumericWithUnits
with the existing numeric with the given unit substituted - even returning just the numeric part if the target unit is nil.
n = 10.feet | ||
puts n.unite inch | ==> | '10 inches' |
puts n.unite "inch" | ==> | '10 inches' |
puts n.unite :inch | ==> | '10 inches' |
puts n.unite 1.inch | ==> | '10 inches' |
puts n.unite 1.inch.unit | ==> | '10 inches' |
puts n.unite nil | ==> | '10' |
Our unit-uniter can be used to quickly add and adjust units as needed.
Now that we have the mechanisms in place to define and operate on unit values, it's time to figure out how to convert them. The goal is that given a
NumericWithUnits
instance, we need to be able to convert its units as painlessly as possible. We'll take two tacks here. In the first, we want explicit conversion we want to provide a generic method to do our work, and in the second we want a mechanism to strip down
a method name that will call the generic mechanism. This way conversions can be done explictly or generically depending on need.
It's here that we'll start adding to
NumericWithUnits
'
method_missing
method in earnest.
The first thing we need to do is add a
convert
method. It should take units in much the same way as our
unite
method does and convert any existing units in a measure to the ones that are indicated. We'll build convert to return a new
NumericWithUnits
instance, and we'll also provide a
convert!
method to perform the conversion on the existing instance. While we're at it, we'll include the capability to pass in an
Array
of units - this makes it nice and easy to do conversions when we've built up complex unit structures.
units.rb a units pipe dream | |
class NumericWithUnits def convert(target_units=nil) if target_units units_hash = UnitsHash.new if !(target_units.kind_of? Array) units_hash.merge!(1.unite(target_units)) else target_units.each { |target_unit| units_hash.merge!(1.unite(target_unit)) } end align(1.unite(units_hash),false) else clone end end def convert!(target_units=nil) result = convert(target_units) self.numeric, self.unit = result.numeric, result.unit self end end |
Conversion is trivial since we've already written it into the
align
method. In order to perform many of the operations within a
NumericWithUnits
, we've had to
align
units first, which is exactly what a conversion does - just with a slightly different interface.
The only interesting thing we're doing here is building up a value to align with, since our
unite
method can now do the work of verifying our units for us and setting them up into a
UnitsHash
. Once we have this, we can just unite the the result and align the instance to it to get our converted value.
As an example, let's calculate a velocity from a distance and duration. First we'll add some more unit definitions.
definitions.rb a units pipe dream | |
Units.create :length do |m| m.system :english do |s| s.unit :name => :mile, :abbrev => :mi, :equals => 1760.yards end end Units.create :time do |m| m.system :base do |s| s.unit :name => :second, :abbrev => :sec s.unit :name => :minute, :abbrev => :min, :equals => 60.seconds s.unit :name => :hour, :abbrev => :hr, :equals => 60.minutes end end |
With these in place, we'll use
Units
to solve a simple conversion problem: if we went 140 miles in 2 hours and 35 minutes, what was our average speed in feet per second?
distance = 140.miles | ||
duration = 2.hours + 35.minutes | ||
velocity = distance/duration | ||
puts velocity.convert [1.foot, 1.second] | ==> | '79.48387090677419 ft / sec' |
Nice. We didn't need to muck around with conversion factors or anything, we just expressed the quantities in Ruby and did our calculation. What's more, if we want to use some stock conversions or see other values and make other calculations, we can do all it quite easily.
feet_per_second = [1.foot, 1.second] | ||
miles_per_hour = [1.hour, 1.mile] | ||
puts velocity.convert feet_per_second | ==> | '79.48387090677419 ft / sec' |
puts velocity.convert miles_per_hour | ==> | '54.1935483870968 mi / hr |
puts distance.convert 1.yard | ==> | '246400.0 yards' |
puts 5.days * velocity | ==> | '6503.22580645161 miles' |
Taking another look at this, the thought occurs that passing an
Array
into
unite
would be good to add to the interface - it'd just create a
NumericWithUnits
with its resulting units as the ones identified by calling
unite
recursively for each element in the
Array
. Though this might not be critical for external unit users, it isn't without some additional utility. And when used internally, the
convert
method can be simplified dramatically.
A little judicious refactoring, and
units.rb a units pipe dream | |
class Numeric def unite(unit=nil,power=1,measure=nil) if !unit self else if (unit.kind_of? Array) units = UnitsHash.new unit.each {|u| units.merge! 1.unite(u)} unit = units elsif (unit.kind_of? String)||(unit.kind_of? Symbol) units = Units.lookup(unit) units = units.select {|u| u.units_measure == measure} if units.size > 1 if units.size == 0 units_problem("usage",MissingUnitsException.new(unit),:unit=,unit) elsif units.size == 1 unit = units[0] else units_problem("usage",AmbiguousUnitsException.new(unit),:unit=,unit) end elsif unit.kind_of? NumericWithUnits unit = unit.unit end NumericWithUnits.new(self,unit,power) end end end class NumericWithUnits def convert(target_units=nil) target_units ? align(1.unite(target_units),false) : clone end end |
the
convert
method becomes just a simple a one-liner.
Now that we have our generic
convert
method out of the way, we can build an explicit handler through the method_missing method. We'll focus on a nice
human
way to describe conversions.
If you've used Rails, you've seen the ease with which data can be accessed through a human-oriented method naming strategy. For simple queries, methods indicate the columns that are being used to select records making code more readable and freeing the developers from the underlying details of database access. While units are typically less complex to deal with, we'd still like to give unit users with the same conveniences. We've done much of that already for unit definition, declaration and usage. We now need to go the same distance for conversion.
When dealing with conversions, there are three types of verbs we need to distinguish:
to - used to change the units of the existing instance.
in - used to return a new instance with the new units.
per - used to return a new instance with a modified form.
Let's look at each alternative in turn and build up code to support them.
Converting a value with units
to
a value with new units is analogous to what's done by our generic
convert!
method. What we need to do is figure out what units to convert to, tuck those into an
Array
, and pass it all off to
convert!
to do the work.
We'll make a unilateral decision here - unit names shouldn't contain underscores. While this may seem constraining, in practice it really isn't; unit names are typically short single words. That's the way they've evolved over time, and even when
they start out with longer or compound names, they typically get shortened - people just don't like long names for units. What this lets us do is build method names that can be simply parsed in a
method_missing
method.
When we want to change the units of a value, let's say a velocity to miles and hours, the method
velocity.to_miles_and_hours
should do this for us. To make this happen, we need to turn this into
velocity.convert ['miles', 'hours']
in
method_missing
.
units.rb a units pipe dream | |
class NumericWithUnits def method_missing(method,*args) s = method.to_s.split '_' if s[0] == 'to' s.shift convert! s.select{|e| e != 'and'} else NumericWithUnits.new(numeric.send(method,*args),unit) end end end |
It's easy - we just convert the method name to a
String,
split it into an
Array
on the underscores, and if the first element is 'to', we shift it off, get rid of any split parts that were 'and', and convert the instance's units.
velocity = 55.feet / 1.second | ||
puts velocity | ==> | '55 ft / sec' |
velocity.to_miles_and_hours | ||
puts velocity | ==> | '37.5 mi / hr' |
The order of the unit names in the method doesn't matter and we don't need to specify the 'and' - it's optional. The
to_miles_and_hours
,
to_hours_and_miles
,
to_miles_hours
, and
to_hours_miles
methods are all equivalent.
The
in
conversion is just like the
to
conversion except that the old instance is left untouched - a new
NumericWithUnits
is returned.
units.rb a units pipe dream | |
class NumericWithUnits def method_missing(method,*args) s = method.to_s.split '_' if s[0] == 'to' s.shift convert! s.select{|e| e != 'and'} elsif s[0] == 'in' s.shift convert s.select{|e| e != 'and'} else NumericWithUnits.new(numeric.send(method,*args),unit) end end end |
This gives us
velocity1 = 55.feet / 1.second | ||
puts velocity1 | ==> | '55 ft / sec' |
velocity2 = velocity1.in_miles_and_hours | ||
puts velocity1 | ==> | '55 ft / sec' |
puts velocity2 | ==> | '37.5 mi / hr' |
As you can see in this example,
velocity1
is unaffected by the call to the conversion. And also, as before,
in_miles_and_hours
,
in_hours_and_miles
,
in_miles_hours
, and
in_hours_miles
are all equivalent. Oh, and for that matter, we can use any of a unit's identifiers in the method names to get the same result:
in_mi_hr
,
in_mile_hour
,
in_mile_hours
, etc. It all just works.
The
per
conversion is a little less trivial. This one, like
in
returns a new
NumericWithUnits
, but the conversion must be completely aligned with the given units - and also admits the possibility of inversion. Inversion? Yes.
By inversion we mean that we can use
per
to flip the conversion upside down. Using our example, if we have a value in length/time we'd want to convert the units using
miles_per_hour
or
feet_per_second
methods. But we also want to be able to use
per
to get time/length by specifying
seconds_per_mile
, for instance. Units should know that we've completely specified the units and inverted the relationship. For this reason, a
per
conversion is a bit more complicated than a simple
in.
units.rb a units pipe dream | |
class NumericWithUnits def method_missing(method,*args) s = method.to_s.split '_' if s[0] == 'to' s.shift convert! s.select{|e| e != 'and'} elsif s[0] == 'in' s.shift convert s.select{|e| e != 'and'} elsif s.select{|e| e == 'per'}.size > 0 convert_per method else NumericWithUnits.new(numeric.send(method,*args),unit) end end def convert_per method ps = method.to_s.select{|e| e != 'and'}.join('_').split '_per_' raise UnitsException.new('invalid per method') if ps.size != 2 numerator = 1.unite(ps[0].split('_')) numerator_units = Set.new numerator.unit.keys denominator = 1.unite(ps[1].split('_')) denominator_units = Set.new denominator.unit.keys positives = unit.keys.collect{|k| unit[k] > 0 ? k : nil}.compact! negatives = unit.keys.collect{|k| unit[k] < 0 ? k : nil}.compact! numerator_positives = Set.new 1.unite(positives).align(numerator,false).unit.keys numerator_negatives = Set.new 1.unite(negatives).align(numerator,false).unit.keys denominator_positives = Set.new 1.unite(positives).align(denominator,false).unit.keys denominator_negatives = Set.new 1.unite(negatives).align(denominator,false).unit.keys if (numerator_units == numerator_positives) && (denominator_units == denominator_negatives) convert((ps[0]+'_'+ps[1]).split('_')) elsif (numerator_units == numerator_negatives) && (denominator_units == denominator_positives) convert((ps[0]+'_'+ps[1]).split('_'))**-1 else raise UnitsException.new('invalid per units') end end end |
This may seem a little daunting, but it's really not so bad. First we make
method_missing
call the
convert_per
method if 'per' is found in the method name. When we enter
compare_per
, we make sure 'per' is between two sets of units. We pull out the first set and make the numerator, and the second we make the denominator. Next we separate out the sets of units in this instance that have positive or
negative exponents. We then align our positives and negatives to the numerator and denominator - and then we compare. If the numerator and denominator are aligned to the positives and negatives respectively, we're a normal conversion. If the
numerator and denominator are aligned to the negatives and positives respectively, we're an inverted conversion. Otherwise, we've got units measures in the instance that don't match the ones implied by the method, so we raise a
UnitsException
.
It's not trivial only because we have a couple layers of matching going on. But it does give us what we want.
velocity = 55.feet / 1.second | ||
puts velocity.miles_per_hour | ==> | '37.5 mi / hr' |
puts velocity.minutes_per_mile | ==> | '1.6 min / mi' |
We have to. We have in. We have per. But it's still got one small bit of clunckiness. If we were to say something like
102.inches.feet
it's pretty clear that what we want to do is to get back a new
NumericWithUnits
with a value of 8.5 feet - exactly what
in
would give us. Putting the in_ at the beginning of the method name shouldn't be strictly necessary. But how should we do this? We should create a new convert method that decodes the method name, matches it up with the units we have to make sure
everything's valid and does the conversion. And then how do we funnel methods that aren't meant to be conversions to the numeric part? Easy. We just reorganize using a
begin-rescue
to encapsulate our conversions.
units.rb a units pipe dream | |
class NumericWithUnits def method_missing(method,*args) begin s = method.to_s.split '_' if s[0] == 'to' s.shift convert! s.select{|e| e != 'and'} elsif s[0] == 'in' s.shift convert s.select{|e| e != 'and'} elsif s.select{|e| e == 'per'}.size > 0 convert_per method else convert s.select{|e| e != 'and'} end rescue NumericWithUnits.new(numeric.send(method,*args),unit) end end end |
Sneaky, huh? We just assume it's a conversion and we try it! If it's not, that's okay because we'll raise an exception, be rescued and give the instance's numeric a try at the end.
You know though, we did uncover a small bit of ugliness when we were trying thing out here. If you look back you'll see that to get
55 feet per second
, we had to express it as
55.feet / 1.second
It'd be much more natural to be able to express this as
55.feet/second
Well, this is really quite simple now that we're so familiar with
method_missing
. We just have to push it onto
Object
's
method_missing
method.
units.rb a units pipe dream | |
class Object alias :method_missing_before_units :method_missing def method_missing(method,*args) begin 1.unite(method.to_s.split('_').select{|e| e!= 'and'}) rescue Exception method_missing_before_units(method,*args) end end end |
And if we slip in another flourish, we can use the per form here quite naturally.
55.feet_per_second
We get this by pushing on
Numeric
's method_missing code.
units.rb a units pipe dream | |
class Numeric def method_missing(method,*args) if Units.defining? reference = Units.create_forward_reference(method,Units.defining?) begin value = Units.convert self, method Units.remove_forward_reference reference value rescue MissingUnitsException Units.hold_forward_reference rescue UnitsException => exception units_problem("definition",exception,method,args) rescue Exception method_missing_before_units(method,args) end else begin s = method.to_s.split('_').select{|e| e != 'and'} if s.select{|e| e == 'per'}.size > 0 ps = s.join('_').split '_per_' raise UnitsException('invalid per method') if ps.size != 2 unite(ps[0].split('_'))/(1.unite(ps[1].split('_'))) else unite s end rescue UnitsException => exception units_problem("usage",exception,method,args) rescue Exception method_missing_before_units(method,*args) end end end end |
We're dividing the first part of the
per
expression by the second part if it's well formed, but notice that we took the liberty of simplifying the non-defining case considerably, just calling the
unite
method rather than going through
Units
'
convert
method as we did previously.
There's one more place where a per conversion is useful - when we want to obtain a pure conversion factor between two units in the same Measure. For instance, if we say
feet_per_mile
we want to get back a value of 5280. For this, we slip a little more code into
Object
's
method_missing
method.
units.rb a units pipe dream | |
class Object def method_missing(method,*args) begin s = method.to_s.split('_') if s.select{|e| e == 'per'}.size > 0 per_ratio method else 1.unite(s.select{|e| e!= 'and'}) end rescue Exception => exception method_missing_before_units(method,*args) end end def per_ratio method ps = method.to_s.select{|e| e != 'and'}.join('_').split '_per_' raise UnitsException('invalid per method') if ps.size != 2 value = (1.unite ps[1].split('_')) / (1.unite(ps[0].split('_'))) raise UnitsException.new("per ratio has units") if value.kind_of? NumericWithUnits value end end |
If the method has a
per
in it, we split it and do a division and return the division. Two things to notice though. First, we raise an
UnitsException
if the result of the division has units - that is, if it's a type of
NumericWithUnits
. Remember, that when the result of the division of two values with units is unitless (as in this case when the Measures cancel out) that the result is demoted to a pure
Numeric
. And second, look closely at the division. Why are we dividing the second value by the first? Well, because when we say something like "feet per mile", what we really mean is mile.in_feet/feet. It doesn't make pure mathematical
sense - it's just the way people say it! Remember that for people, it's the context that matters: when the measures are the same on both sides of the
per
, the result of the division is inverted.
Per
changes its meaning in different contexts.
Alas, our
to_s
method in
NumericWithUnits
is a bit lacking. Often, values in a measure are expressed not just in a single unit, but broken down into related pieces. For instance, while
puts 155.25.lbs | ==> | '155.25 pounds' |
is perfectly reasonable, we'd also like the ability to have it display as
==> | '155 pounds 4 ounces' |
or
==> | '155 lb 4 oz' |
This really isn't so hard to do - what's hard is figuring out a good way to express it. What we'll do is add the definition of formats to our infrastructure, and allow to_s to take a format as an argument. This strategy will let us define as many formats as we want, and they'll be carried right along with the unit definitions.
Since we only do this sort of formating within a given system of units within a measure, it make sense to define the functionality there. Let's consider the length measure and a way to get feet, inches, and thirty-seconds of an inch. What would be nice is something like
units.rb a units pipe dream | |
Units.create :length do |m| m.system :english do |s| s.format :name => :feet_inches_and_32s, :format => "#{whole_feet} #{remaining_inches_with_fraction 32}" end end |
and then do
puts 13.2890625.ft.format("feet_inches_and_32s") | ==> | '13 ft 3-15/32 in' |
Yeah, that would be really nice. Unfortunately, it does have a few problems, not the least of which the
String
itself would be evaluated during definition - even if we could come up with good definitions for getting whole and remaining parts (and whatever other interesting types of slicing and dicing we might need) of a
NumericWithUnits
in the right order. No, unfortunately an abstraction like this is just a little too abstract. What should we do?
Well, since we're in Ruby, why not use Ruby? Let's see. Instead of defining a
String
that gets evaluated when the system is being defined, let's use a closure - basically a nice Ruby way to define a function - and see where that gets us. The
lambda
method takes a block and converts it to a callable proc; the proc is Ruby's mechanism for closures.
units.rb a units pipe dream | |
Units.create :length do |m| m.system :english do |s| s.format :name => :feet_inches_and_32s, :format => lambda { |u| "#{u.whole_feet} #{u.remaining_inches_with_fraction 32}" } end end |
Ok, so far so good. But how about those other pesky functions in there? There could be lots of them! How many of those are we going to have to write?
Well, the short answer is none, unless there's a good reason. What we'll do is just add processing to the block.
units.rb a units pipe dream | |
Units.create :length do |m| m.system :english do |s| s.format :name => :feet_inches_and_32s, :format => lambda { |u| ru = u.round_to_nearest(1.inch,32) feet = ru.feet.floor inches = (ru-feet).inches "#{feet} #{inches.with_fraction 32}" } end end |
Hey - that's pretty close! We get to use most of what we already had. It looks like we need to just add a specialy method that will render the numeric part of the unit a little differently. The code will be cake!
Let's start writing...
units.rb a units pipe dream | |
class UnitsSystem attr_reader :formats def initialize(units_measure,name) @units_measure = units_measure @name = name @formats = MethodicHash.new end def format(options) @formats[options[:name]] = options[:proc] end end class NumericWithUnits def format(name=nil) if name == nil to_s else raise UnitsException.new("system not explicit") if (system == nil) format = system.formats[name] raise UnitsException.new("missing format") if format == nil format.call(self) end end |
Oops. Our definition of the format in
UnitsSystem
looks fine, but our call to
system
qualifying the first raise is a big problem. Consider - a measure is independent of systems! For example, the same length measured in the English system is equivalent to that length measured in the Metric system - this is just a way of rendering!
While it's fine to define formats at the system level, they should be accessed through the measure level. Let's do a little quick change.
units.rb a units pipe dream | |
class UnitsMeasure attr_reader :formats def initialize @formats = MethodicHash.new end def format(options) @formats[options[:name]] = options[:format] end end class UnitsSystem def format(options) @units_measure.format(options) end end class NumericWithUnits def format(name=nil) if name == nil to_s else raise UnitsException.new("measure not explicit") if (measure == nil) format = measure.formats[name] raise UnitsException.new("missing format") if format == nil format.call(self) end end |
This is interesting. By coding it this way, storing the formats in the
UnitsMeasure
and defining a
format
method there, and calling that method from a
UnitsSystem
using a
format
method that takes the same arguments, we can potentially define formats at the
UnitsMeasure
level as well as within a
UnitsSystem
. Of course, we need to define the
measure
method in
NumericWithUnits
, but this is as easy as combining the
UnitMeasure
s of the
Units
in the
UnitsHash
. We'll build up a measure derivation and, since we need to make sure the "artificial" derivation matches a measure, we must enhance the
find_by_derivation
method in
Units
.
units.rb a units pipe dream | |
class UnitsHash def measure measure = UnitsMeasure.new each { |unit,power| measure.merge_derivation unit.units_system.units_measure => power } Units.find_by_derivation(measure.derived) end end class Units def Units.find_by_derivation(derivation) matches = @@measures.values.uniq.select { |measure| if measure.derived measure == derivation elsif derivation.size == 1 a = derivation.to_a[0] a[0] == measure && a[1] == 1 else false end } case matches.size when 0 then nil when 1 then matches[0] else raise UnitsException.new( "Multiple UnitsMeasures with same derivation found") end end end |
The remainder of work is specifying any additional renderers for the numeric part of a
NumericWithUnits
. But there is some great fallout from this. When we think about it, whatever we'd like to do to the numeric part, we'd like to be able to do to a
Numeric
. This part of the work on
Numeric
actually transcends units and we'll take a general approach to the solution. Lets code our
round_to_nearest
and
with_fraction
methods.
We'll first throw together our fraction printer and specialized rounder.
units.rb a units pipe dream | |
class Numeric def with_fraction(denominator=1,separator='-',show_zero=false) sign = self <=> 0 unsigned = self*sign whole = (((unsigned*denominator).round)/denominator).floor numerator = ((unsigned-whole)*denominator).round "#{sign*whole}#{(numerator != 0)||show_zero ? "#{separator}#{numerator}/#{denominator}" : ""}" end def round_to_nearest(denominator=1) ((self*denominator).round)/(1.0*denominator) end end |
Aside from some generality and the fact that because we're using the
floor
function we have to pay attention to the sign of the numeric, our
with_fraction
prints whole and fractional parts of numbers pretty nicely.
puts 9.1.with_fraction(50)
|
==>
|
'9-5/50' |
puts 9.0.with_fraction(25)
|
==>
|
'9' |
puts 9.0.with_fraction(25.5,' ',true) | ==> | '9 0/25.5' |
The whole reason we need a
round_to_nearest
method is to ensure we get the right values all the way up to the highest unit. Hierarchical units are like the odometer on your car: rounding may cascade up to push you up to the next unit. If you don't take care, and just render the pieces
separately before you rationalize the whole numeric, you may not do rounding correctly.
puts 1.999.ft.inch.round_to_nearest(32) | ==> | '24.0 inches' |
puts 1.996.ft.inch.round_to_nearest(32) | ==> | '23.9375 inches' |
See? If we just did a straight rounding on the feet or the inches, we'd lose the 32nds. It's just these sort of methods that, while we want to use them for formatting
NumericWithUnit
values, they make good sense to just add to the
Numeric
From here it's just a short hop to get what we need - but like any other method in
Numeric
we want to use from a
NumericWithUnits
, we'll call the method on the numeric part from the
method_missing
. The difference is, we'll take a look at what the call returns: if it's a kind of
String
value, we'll tack on the units.
units.rb a units pipe dream | |
class NumericWithUnits def method_missing(method,*args) begin s = method.to_s.split '_' if s[0] == 'to' s.shift convert! s.select{|e| e != 'and'} elsif s[0] == 'in' s.shift convert s.select{|e| e != 'and'} elsif s.select{|e| e == 'per'}.size > 0 convert_per method else convert s.select{|e| e!= 'and'} end rescue Exception value = numeric.send(method,*args) if (value.kind_of? String) "#{value} #{unit.to_s(numeric)}" else NumericWithUnits.new(value,unit) end end end end |
So, let's give it a shot.
length = 13.28090635.ft | ||
puts length.format :feet_inches_and_32s | ==> | '13 feet 2-12/32 inches' |
Now before you start casting aspersions, I already know there are easy holes to poke into this. But look, with the power of Ruby behind you, you can write your own complex-but-generalized rendering methods and keep them associated with the unit
definitions - not buried in custom code. Rendering is just as much a part of a
Units
package as all of the arithemtic and conversions, and now everything is brought together in such a way that we don't have to sweat it as much.
One of the things that has been overlooked until now is that we don't handle derived units correctly. In our headlong rush to add functionality, this got overlooked. While we can do:
puts 225.ml | ==> | '225 milliliters' |
puts 225.ml/5.cm | ==> | '45 ml / cm' |
we don't get the reduction from
ml/cm
to
cm^2
that we should have expected. What did we miss? Where did we go wrong?
Well, in truth, we didn't go wrong anywhere and we didn't miss anything - we just didn't write tests for derived unit capabilities. If we were starting from behavioral specifications, we probably would have captured this. But we weren't starting from specs. Remember, this is a pipe dream. Should we have written specs first? Perhaps. But this ignores one of the fundamental truisms of human endeavor: You almost never get it right. Translated to the problem at hand: if we had spec'ed it first, we would have missed something. If not this behavior, we would have missed something else. Why do anything in the first place then, you may ask, if we're never going to get anything right? Because two other fundamental truisms come into play, namely: Nothing gets done if you give up and Perfectionism is too expensive
If we look closely at the problem, the answer is fine - it's just that we really want an alternative that is reduced for calculating and the option to create an unreduced version for output. So, what do we do to fix this? We'll dive in and approach the issues we find one at a time. First, we'll look at the conversion problem above.
We like the declarative statement just fine. When we set the unit to be milliliters, we want to get milliliters back out. It's the conversion that's troublesome. When we divided by centimeters, we wanted the milliliters to be treated as a volume from which we could factor out a length. The problem seems to be that even though we're dealing with a unit derived from length, we don't take that into account during calculation. What we need to do is expand the units we're calculating with whenever we've got a derived unit.
units.rb a units pipe dream | |
class UnitsHash def derived? keys.select{|unit| unit.units_system.units_measure.derived != nil}.size > 0 end def reduce factor = 1.0 new = UnitsHash.new each do |unit,power| if unit.units_system.units_measure.derived != nil factor *= unit.equals.numeric new.merge!(unit.equals,power) else new.merge! unit end end factor.unite new end end class NumericWithUnits def derived? unit.derived? end def reduce numeric*unit.reduce end def <=>(value) if derived? reduce <=> value elsif value.kind_of? NumericWithUnits if value.derived? self <=> value.reduce else align(value).numeric <=> value.numeric end elsif value.kind_of? Numeric numeric <=> value else raise UnitsException.new("units mismatch") end end def approximately_equals?(value,epsilon=Numeric.epsilon) if derived? reduce.approximately_equals?(value,epsilon) elsif value.kind_of? NumericWithUnits if value.derived? self.approximately_equals?(value.reduce,epsilon) else align(value).numeric.approximately_equals?(value.numeric,epsilon) end elsif value.kind_of? Numeric numeric.approximately_equals?(value,epsilon) else raise UnitsException.new("units mismatch") end end def +(value) if derived? reduce+value elsif value.kind_of? NumericWithUnits if value.derived? self+value.reduce else aligned_value = align(value) aligned_value.numeric += value.numeric aligned_value end elsif value.kind_of? Numeric NumericWithUnits.new(numeric+value,unit) else raise UnitsException.new("units mismatch") end end def *(value) if derived? reduce*value elsif value.kind_of? NumericWithUnits if value.derived? self*value.reduce else extend(value,1) end elsif value.kind_of? Numeric NumericWithUnits.new(numeric*value,unit) else raise UnitsException.new("units mismatch") end end def /(value) if derived? reduce/value elsif value.kind_of? NumericWithUnits if value.derived? self/value.reduce else extend(value,-1) end elsif value.kind_of? Numeric NumericWithUnits.new(numeric/value,unit) else raise UnitsException.new("units mismatch") end end def %(value) if derived? reduce%value elsif value.kind_of? NumericWithUnits if value.derived? self%value.reduce else aligned_value = align(value) aligned_value.numeric = aligned_value.numeric % value.numeric aligned_value end elsif value.kind_of? Numeric NumericWithUnits.new(numeric % value,unit) else raise UnitsException.new("units mismatch") end end def inv_mod(value) if derived? reduce.inv_mod value elsif value.kind_of? NumericWithUnits if value.derived? self.inv_mod value.reduce else aligned_value = align(value) aligned_value.numeric = value.numeric % aligned_value.numeric aligned_value end elsif value.kind_of? Numeric NumericWithUnits.new(value % numeric,unit) else raise UnitsException.new("units mismatch") end end end |
We need to check the operands of any numeric operation where another
NumericWithUnits
is involved, reducing each prior to applying the operation if they are derived. For other operations, like exponentiation or ones that just operate on a
Numeric
, we can leave things alone since they don't require a reduction. Doing reduction this way means we'll adjust our values to underived units whenever we do calculations.
Let's see how we're doing so far:
a = 225.ml b = 5.cm c = 15.cm^3 |
||
puts "#{a+c}, #{c+a} | ==> | '240.0 cm^3, 0.00024 m^3' |
puts "#{a-c}, #{c-a} | ==> | '210.0 cm^3, -0.00021 m^3' |
puts "#{a*b}, #{b*a} | ==> | '1125.0 cm^4, 1.125e-05 m^4' |
puts "#{a/b}, #{b/a} | ==> | '45.0 cm^2, 222.222222222222 1/m^2' |
The numbers are right and the units match the rules we laid down about what unit takes precedence in an operation - everything looks good. So, now we just need to switch to the intended units during conversion. Let's work on a value in cubic centimeters and try to convert it milliliters.
puts c.in_ml | ==> | '15 cm^3' |
puts c.to_ml | ==> | '15 cm^3' |
No luck, but we expected that. Our problem is that the code doesn't look at the units we're derived from - we need to use what they're equal to and "back-convert". To do this we must create a more sophisticated alignment when we're working with derived units, keeping our non-derived-unit version around for fast performance.
units.rb a units pipe dream | |
class NumericWithUnits def self.derived_align_type=(type) @@derived_align_type = type end def self.derived_align_type @@derived_align_type end @@derived_align_type = :whole_powers def align(target,all=true,type=@@derived_align_type) if !derived? && !target.derived? simple_align(target,all) else power_align(target,all,type) end end def simple_align(target,all=true) factor = 1 target_unit = UnitsHash.new unit.each do |tu,tv| su = target.unit.keys.select {|u| u.units_measure.equal? tu.units_measure } if su.size == 1 factor *= ((1.0*tu.equals.numeric)/su[0].equals.numeric)**tv target_unit[su[0]] = tv else target_unit[tu] = tv end end raise UnitsException.new("units mismatch") if all && (target.unit != target_unit) NumericWithUnits.new(numeric*factor,target_unit) end def can_align(target,exact=true) ru, tru = reduce.unit, target.reduce.unit !exact || (ru == tru) end def reduce_power(p,f,type) if type == :whole_powers pa = p.abs (p == 0) ? [p,f,0] : (pa < f) ? [p, f, pa/p] : [p, f, p/f] else (p == 0) ? [p,f,0] : [p, f, (1.0*p)/f] end end def common_power(ps) ps.values.collect {|a| a[2]}.inject {|p,e| p.abs < e.abs ? p : e} end def revise_power(rp,p) [rp[0], rp[1], p, rp[0]-p*rp[1]] end def power_align(target,all=true,type=@@derived_align_type) raise UnitsException.new("units mismatch") unless can_align(target,all) factor = 1 target_unit = UnitsHash.new unit_reduce = reduce target.unit.each do |tu,tv| t_u = {} tt_u = {} tu_reduce = tu.equals.reduce found = tu_reduce.unit.each do |ttu,ttv| su = unit_reduce.unit.keys.select {|u| u.units_measure.equal? ttu.units_measure } break false if su.size == 0 tt_u[ttu] = reduce_power(unit_reduce.unit[ttu],ttv,type) end if found cp = common_power(tt_u) tt_u.each {|k,v| tt_u[k] = revise_power(v,cp)} t_u[tu] = tv**cp t_factor = tu_reduce.numeric**cp target_unit[tu] = cp factor *= t_factor/(tu.equals.numeric**cp) tt_u.each {|k,v| unit_reduce.numeric /= t_factor unit_reduce.unit[k] = v[3]} end end unit_redux = unit_reduce.simple_align(self,false) result_unit = target_unit.merge(unit_redux) raise UnitsException.new("units mismatch") if all && (target.unit != result_unit) NumericWithUnits.new(factor*unit_redux.numeric,result_unit) end end |
Now when we
align
, we check to see if either the aligner or alignee is derived. If not, we just do a
simple_align
; otherwise, we do a
power_align
.
The
power_align
method uses reduced units to perform an alignment. First we check to make sure that we can do an alignment - if not, we fail abruptly. Then we walk through the set of target units; we reduce each in turn and look to if there are sufficient reduced
units left to convert in the instance we're aligning to do a conversion of that unit. If so, we add the target, and reduce the instance's reduced units appropriately. At the end, we merge in the remaining reduced units after they've been re-aligned
with the instance's original units. Of course, if we had to align all the units - because we're adding things together, for instance - we'll fail if things didn't come out right.
Other than this, there is one variation we wish to maintain when we power align - to use whole or fractional units. The choice really is arbitrary for calculation, but when rendering some would consider whole-number-dimensioned units to be prettier than fractionally-dimensioned units. We allow the method that will be used to be set at the class level for convenience.
puts c.in_ml | ==> | '15.0 milliliters' |
puts c.to_ml | ==> | '15.0 milliliters' |
And what's more,
puts 225.ml/5.cm | ==> | '45.0 cm^2' |
puts (225.ml/5.cm).in_ml | ==> | '45.0 ml / cm' |
Triumph! But there is a small consideration that we still need to take care of. Consider doing a conversion where you don't want to exhaust a measure too early - if you did, you'd have nothing left to draw from when aligning to subsequent targets. This may not happen very often, but it still could nonetheless. The trick is that we need to take the power of the resultant units into consideration during the alignment.
What we need is a way to specify the precise units and powers we'd like the resulting
NumericWithUnits
to have. For this, we'll call up the
align
method directly and give it an
Array
of the pieces to which we want to be aligned. For example, let's say we want milliliters over meter. We would want to do
puts (225.ml/5.cm).align([1.ml,1/m]) |
We just need to take an
Array
of
NumericWithUnits
as a target and use each one (and it's power) to form the result.
units.rb a units pipe dream | |
class NumericWithUnits def align(target,all=true,type=@@derived_align_type) if target.kind_of? Array piece_align(target) elsif !derived? && !target.derived? simple_align(target,all) else power_align(target,all,type) end end def piece_align(pieces,type=@@derived_align_type) factor = 1 target_unit = UnitsHash.new unit_reduce = reduce pieces.each do |p| p.unit.each do |tu,tv| t_u = {} tt_u = {} tu_reduce = tu.equals.reduce found = tu_reduce.unit.each do |ttu,ttv| su = unit_reduce.unit.keys.select {|u| u.units_measure.equal? ttu.units_measure } break false if su.size == 0 tt_u[ttu] = reduce_power(unit_reduce.unit[ttu],ttv,type) end if found tt_u.each {|k,v| tt_u[k] = revise_power(v,tv)} t_u[tu] = tv t_factor = tu_reduce.numeric**tv target_unit[tu] = tv factor *= t_factor/(tu.equals.numeric**tv) tt_u.each {|k,v| unit_reduce.numeric /= t_factor unit_reduce.unit[k] = v[3]} end end end raise UnitsException.new("units mismatch") if unit_reduce.unit.has_units? NumericWithUnits.new(factor*unit_reduce.numeric,target_unit) end end class UnitsHash def has_units? (self.select {|k,v| v != 0.0}).size > 0 end end |
Now we run the power reduction for each piece, and instead of asking for the common power, we just use the power that was passed in. At the end, if there are any units left over, we throw an exception. For this, we want an exact match.
puts (225.ml/5.cm).align [1.ml,1/m] | ==> | 4500.0 ml / m |
It's arguable that we should try to use the
convert
method instead of exposing the
align
method directly. However, there's something more subtle going on here - specifically, we don't want the elements of the array combined before we align, which is what
convert
does. We want each applied in turn, incrementally aligning the object to each element of the
Array
. We may not do it often, but when we want to output in particular units that don't come out of the calculations naturally, this will do the job.
Now we have all we need to render derived units correctly.
When I've used units in a program's output up to now, most of the time they've been specified to me by someone else, or in a particular problem. "Answer the problem in meters per second," or "express the weight in pounds", or "how many tablespoons in a cup?" Sometimes, the choice is based on convention, sometimes on preference, and sometimes it's ad hoc. When we're given the choice, which units do we choose?
This is a good question - but because choice is involved, getting a answer can be tricky. The best we can really do is allow the user to give us some options and constraints and try to make a reasonable determination.
Since all the alternatives to choose from are effectively equivalent - all equal to each other as far as measurement is concerned - the constraints are based on weighting the units and the value of the numerics. For instance, if a units user were
to present us with four unit choices each weighted by preference, and a preferred range for the numeric part of our
NumericWithUnits
, we should be able to return the units that provide the best fit.
What we'll do is set up a mechanism that takes constraints, weights and an
Array
of sample values that will return the ranked alternatives.
units.rb a units pipe dream | |
class Units def Units.rank(unit_choices = {}, samples = [], &numeric_ranker) if block_given? scores = {} samples.each {|s| unit_choices.each {|uc,w| puts s.convert(uc) scores[uc] = (scores[uc] || 0) + w*(yield s.convert(uc).numeric) } } scores.sort {|e1,e2| -(e1[1] <=> e2[1])} scores.collect {|s| s[0]} else rank(unit_choices,samples) { |n| e = ((((n.abs)-5.5).abs-4.5).at_most(0)) (e == 0) ? 0 : (e == 1) ? 0 : -(e + 1/(1-e)) } end end end |
Alright. A little explanation is in order. We pass in a
Hash
of unit choices and a weight multiplier for each choice. For the sake of illustration, we'll pick
choices = { 1.meter/min => 2, 1.mile/hour => 1, 1.km/hr => 1 } |
What you see here is that scores for meter/minute are weighted twice as heavily as scores for miles/hour or kilometers/hour. The position we've taken is that the highest score wins, so scores should be more negative the further their values are from the ideal scheme - I've set it up so everything non-zero will be a negative, errors are the only values that will have weight. However, the algorithm is completely general - you can make it do whatever you'd like it to do.
Next we throw together a few samples:
samples = [ 7.35.in/sec 1.18.in/sec, 0.92.in/sec ] |
The idea is that these are values are representative for what we expect to get when we want "just the right units". We may have a sample set containing one element or a thousand - it doesn't matter. The unit that scores highest overall will be ranked first.
We've set up the algorithm to take the choices and samples, and a block that will give us a score based on the numeric part of a sample value when it's units are converted to one of the choices. As you can see, if we don't provide a block, the
method will be re-sent with the curious-looking block provided. What this block does is scores zero if a numeric is between one and ten, and a value of
-(e + 1/(1-e))
when it's outside that range. The idea is that values with a
single
leading digit are preferred - what this does is makes the error value from one down to zero look like the range from 10 up to infinity - the farther you are from the range
logarithmically
speaking, the greater the error, and the weight magnifies the resulting error. We also exclude a plain zero (when
e == 1
) since it's a single digit.
So, we give it a try and
puts Units.rank(choices,samples) | ==> |
'1 m / min' '1 km / hr' '1 mi / hr' |
So meters/minute is the best choice for this particular set of samples - despite the fact that we weighted the error twice as high! Given our samples, meters/minute is just the right unit to use.
Oh, by the way, you may not be familiar with
at_most
(or
at_least
.) No problem. They were schemed up to provide the max and min functionality available in most numerical libraries -
at_most
limits the a value on the upside,
at_least
limits it on the downside.
units.rb a units pipe dream | |
class Numeric def at_most(m=0) m < self ? m : self end def at_least(m=0) m > self ? m : self end end |
Kudos to Ruby for at least being extensible enough to add functionality that's missing. It'd be nice to have everything you could ever want already baked into the language, but at least you aren't locked out from adding the parts you need.
I'll be the first to admit we've added a lot of goo to numbers in Ruby. While the goo is good for making values with units actually work, it's going to slow computations down at least a little. I think we all need the unit-awareness, and it's incredible useful to have it at our fingertips, but how gooey is Ruby now? There's all the goo for units themselves, and method_missing, and... hmmm... let's take a look at how big the hit the units goo really is.
The best way to look at this is, I suppose, is to consider what happens when we're working with unitless values. We should be satisfied with the system when the values we're working with do have units - but we need to make sure that when they don't that we didn't handcuff ourselves.
So lets take stock here. We'll look at the methods we've added or changed that would get called if we're operating on unitless values in unitless contexts.
Ruby Object | Method | Effect |
Numeric |
method_missing
|
the case we're not defining units, we look for methods with '_per_' in their name and failing that, we try to use the name of the method to convert the object to a value with units. Otherwise the processing goes to the prior, overriden
method_missing
processing.
|
Object |
method_missing
|
We look for methods with '_per_' in their name and failing that, we try to use the name of the method to create a value with units. Otherwise the the processing goes to the prior, overriden
method_missing
processing.
|
Wait just a second. That's it?
If we're not using units we just take a hit when the methods we're using aren't defined and we have to drop into
method_missing
- and these sections fail quickly because we've assumed we're not working with units so the missing methods will have non-units sorts of names, right? That's not so bad. After all, if the control flow of the program goes through
method_missing
, then we should expect it may take some extra time to get to the real guts of the computations. What we're seeing is that units put a nearly insignificant load on the related objects when we're not using them. That's very good news.
Well, there is a little more, but it's a startup issue. We still may be feeding a big set of units to the system to "prime the units pump" so to speak, establishing the baseline units into our Ruby program. But of course, we haven't defined this yet, and we can even make the baseline unit loading be optional.
No, it really looks like the hit is small and that most of the time, working with unitless values will happily bypass our units system.
As I alluded to in the beginning of this excursion into the creation of a units infrastructure, down through the years programmers have avoided explicitly using units in their code. Why? Well, besides the fact that they've perhaps only been explicitly available in a handful of languages (and after a quick survey, I still can't find anything like this in the mainstream) the explicit use of units is slower than just using numbers directly. To this I concede, absolutely! Look at all the additional definitions and lookups and alignments and extensions we've had to work our way through! Making units relatively transparent has not been easy, and there certainly is a performance penalty when using them.
But software developers are sharp thinkers. Disregarding units until values are ready to be output is still a good idea in many situations. By doing this you keep the numeric calculations running fast. Sure, errors can creep in - insidious ones that are hard to track down - but once everything's tested and certified, writing code this way should work fine.
The units package we've developed doesn't keep us from doing this, of course. We can unite numerics and units at any time during the process - and at any time we can pull out the numeric part of a NumericWithUnits and use it in any way we'd like. Given our ability to directly use scaling conversion factors like
centimeters_per_kilometer
seconds_per_day
or apply transforming ones like
x.miles_per_gallon
y.kV_per_meter
we can apply units late in the process and keep our numeric processing clean and fast. Just keep in mind that the debugging of complex unitless code isn't necessarily easy. I've been there.
I've transposed digits or slipped powers-of-ten and been off on a conversion factor more times than I can count. While this has (thankfully) always been caught during testing, it still has slowed down project deliveries. If you've ever been up late wondering why the heck the values aren't coming out right only to find you slipped up doing a conversion and it's been carried through a few dozen different transformations that have magnified the error to the point where the result bears no resemblance to what the expected, you know exactly what I'm talking about. And if you haven't, consider yourself lucky. Your only recourse is to become very good at copying 9-digit numbers from the back pages of reference books.
The bottom line is that if you're going to be doing a lot of mathematical calculations, sometimes it makes sense to strip the units off your values before and re-unite them afterwards. For instance, if you're going to multiply a bunch of big matrices together, you know the units of the values you're putting in and you know what the resulting units will be, strip the units first and reapply them when you finish - it'll run faster. But just be careful - one unitless number looks like any other. If you're sloppy, you'll be in trouble.
Okay. Before we go further, I need to come up for air to try a few things and see where we are so far.
We defined a lot of framework and wrote a lot of unit tests along the way. Everything we've built passes muster, but is everything we need there? Do we need a course correction? One way to find out... let's try to define some units for keeps, the kind of thing we'll want to come along for free when we write unit-sensitive Ruby code.
We'll start with length:
definitions.rb a units pipe dream | |
Units.create :length do |m| m.system :english do |s| s.unit :name => :inch, :plural => :inches, :abbrev => :in s.unit :name => :foot, :plural => :feet, :equals => 12.inches, :abbrev => :ft s.unit :name => :yard, :equals => 3.feet, :abbrev => :yd s.unit :name => :mile, :equals => 5280.feet, :abbrev => :mi s.unit :name => :nautical_mile, :equals => 1852.meters, :abbrev => :nmi s.format :name => :feet_inches_and_32s, :format => lambda { |u| ru = u.round_to_nearest(1.inch,32) feet = ru.feet.floor inches = (ru-feet).inches "#{feet} #{inches.with_fraction 32}" } end m.system :metric do |s| s.unit :name => :meter, :equals =>39.37.inches, :abbrev => :m, :greek => :ten s.unit :name => :angstrom, :equals => 0.1.nanometers, :abbrev => :A end m.system :old_english do |s| s.unit :name => :fathom, :equals => 2.yards s.unit :name => :chain, :equals => 22.yards s.unit :name => :furlong, :equals => 660.feet s.unit :name => :league, :equals => 3.nmi end m.system :astronomical do |s| s.unit :name => :astronomical_unit, :equals => 149598000.kilometers, :abbrev => :AU s.unit :name => :light_year, :equals => speed_of_light*seconds_per_year, :abbrev => :ly s.unit :name => :parsec, :equals => 3.262.light_years, :abbrev => :pc end end |
This is a decent set to go with, english system units, metric units with the greek prefixes and angstroms, some old archaic english units for fun, and some huge-distance astronomical units. Let's wire it in, run a unit test and... damn.
We blew up because we referenced the speed of light and subsquently light year before they were defined. When we added our forward reference mechanism to the defining state, we did Numeric - not an arbitrary Object. Apparently we need to discriminate between Numerics and other Objects, since we may have objects coming down the pipe. We need to expand on how Object handles missing units.
units.rb a units pipe dream | |
class Object def method_missing(method,*args) begin s = method.to_s.split('_') if s.select{|e| e == 'per'}.size > 0 per_ratio method, args else convert_unit_value method, args end rescue Exception => exception method_missing_before_units method, args end end def convert_unit_value(method,*args) if Units.defining? reference = Units.make_forward_reference(method,Units.defining?) begin value = Units.convert 1, method Units.release_forward_reference reference value rescue MissingUnitsException Units.hold_forward_reference rescue UnitsException => exception units_problem("definition",exception,method,args) rescue Exception method_missing_before_units(method,args) end else begin Units.convert 1, method rescue UnitsException => exception units_problem("use",exception,method,args) rescue Exception method_missing_before_units(method,args) end end end def per_ratio(method,*args) if Units.defining? reference = Units.make_forward_reference(method,Units.defining?) puts "trying #{method} in #{Units.defining?}" begin value = convert_per_ratio method Units.release_forward_reference reference value rescue MissingUnitsException Units.hold_forward_reference rescue UnitsException => exception units_problem("definition",exception,method,args) rescue Exception method_missing_before_units(method,args) end else convert_per_ratio method end end def convert_per_ratio(method) ps = method.to_s.select{|e| e != 'and'}.join('_').split '_per_' raise UnitsException('invalid per method') if ps.size != 2 value = (1.unite ps[1].split('_')) / (1.unite(ps[0].split('_'))) raise UnitsException.new("per ratio has units") if value.kind_of? NumericWithUnits value end def units_problem(state,exception,method,args) raise exception end end |
That takes care of that. We'll save forward references when we're defining units that resolve to Object's method_missing. So now, let's keep going. Let's take care of the time-based entries.
definitions.rb a units pipe dream | |
Units.create :time do |m| m.system :base do |s| s.unit :name => :nanosecond, :equals => 0.000000001.seconds, :abbrev => [:ns, :nsec] s.unit :name => :microsecond, :equals => 0.000001.seconds, :abbrev => [:us, :usec] s.unit :name => :millisecond, :equals => 0.001.seconds, :abbrev => [:ms, :msec] s.unit :name => :second, :abbrev => [:s, :sec] s.unit :name => :minute, :equals => 60.seconds, :abbrev => [:m, :min] s.unit :name => :hour, :equals => 60.minutes, :abbrev => [:h, :hr] s.unit :name => :day, :equals => 24.hours, :abbrev => [:d, :dy] s.unit :name => :week, :equals => 7.days, :abbrev => [:w, :wk] end m.system :common do |s| s.unit :name => :month, :equals => [30.days, 4.weeks], :abbrev => [:m, :mo] s.unit :name => :year, :equals => [365.days, 12.months, 52.weeks], :abbrev => [:y, :yr] end m.system :long do |s| s.unit :name => :decade, :equals => 10.years s.unit :name => :century, :plural => :centuries, :equals => 100.years s.unit :name => :millenium, :plural => :millenia, :equals => 1000.years end m.system :old_english do |s| s.unit :name => :fortnight, :equals => 2.weeks end end |
We once again wire, run, and... damn. Why did we blow up this time?
It's because of the way we defined the month and year. Look at how funky the equals element are - they're Arrays, which are definitely not handled correctly anywhere in the code we've written so far! What we want to say here is that a month is 30 days, or alternately 4 weeks and that a year is 12 months, or 365 days, or 52 weeks. We have to define both equalities in multiple ways since they can be valid in different contexts. Time conversion, so near and dear to us, has some human-based inexactness baked in, and we must handle it correctly.
Okay then, how do we reconcile this split-personality equality? We handle the inexactness by processing equality Arrays specially and throw in a bit of convention enacted through our simple_align method - but we have to establish some new framework first.
units.rb a units pipe dream | |
class NumericWithUnits attr_accessor :numeric, :unit, :original def initialize(numeric,unit,power=1,original=nil) @numeric, @unit, @original = numeric, units_hash(unit)**power, original end end class Units def Units.convert(numeric,unit_identifier) if (candidates = lookup(unit_identifier)).size == 0 raise MissingUnitsException.new(unit_identifier.to_s) elsif !defining? if candidates.size > 1 raise AmbiguousUnitsException.new(unit_identifier.to_s) else unit = candidates[0] NumericWithUnits.new(numeric,unit) end else if candidates.size == 1 units = candidates else units = candidates.select { |candidate| @@defining == candidate.units_system.units_measure } units = candidates.select { |candidate| @@defining.derived[candidate.units_measure] } if units.size == 0 end case units.size when 0 then raise MissingUnitsException.new(unit_identifier.to_s) when 1 then unit = units[0] if unit.equals.kind_of? Array element = unit.equals[0] NumericWithUnits.new(numeric*element.numeric,element.unit, 1,numeric.unite(unit)) else NumericWithUnits.new(numeric*unit.equals.numeric,unit.equals.unit, 1,numeric.unite(unit)) end else raise AmbiguousUnitsException.new(unit_identifier.to_s) end end end end class NumericWithUnits def promote_original @numeric, @unit = original.numeric, original.unit end end class UnitsUnit def normalize raise UnitsException.new("UnitUnits must have a name attribute") unless self.name self.name = self.name.to_s add_plural add_abbrevs add_equals equals.each { |n| n.promote_original } if equals.kind_of? Array self end end class NumericWithUnits def simple_align(target,all=true) factor = 1 target_unit = UnitsHash.new unit.each do |tu,tv| su = target.unit.keys.select {|u| u.units_measure.equal? tu.units_measure } if tu.equals.kind_of? Array m = tu.equals.select{|u| u.unit[su[0]]} m = tu.equals.collect{|u| u.align(1.unite(su[0]))}.compact if m.size == 0 factor *= (m[0].numeric)**tv target_unit[su[0]] = tv else if su.size == 1 e = su[0].equals if e.kind_of? Array m = e.select{|u| u.unit[tu]} m = e.equals.collect{|u| u.align(1.unite(su[0]))}.compact if m.size == 0 factor *= (1.0/m[0].numeric)**tv else factor *= (1.0*tu.equals.numeric/e.numeric)**tv end target_unit[su[0]] = tv else target_unit[tu] = tv end end end raise UnitsException.new("units mismatch") if all && (target.unit != target_unit) NumericWithUnits.new(numeric*factor,target_unit) end end |
The first realization is that because we have an inexactness, we need to retain it. If we work through it and leave it behind, we'll never know what the original intention was. So, we add a placeholder for the original intentions in a NumericWithUnits, and we tuck away the original values as we're defining units. Once we've defined the unit and are normalizing it, we add a step that says "if we have multiple interpretations, use the original intentions rather than the computed value" so we can figure out which one to use later when we have more context. We'll set everything up to make use of the intentions during conversion.
The heart of the mechanism is buried in alignment. What we do here is make it sensitive to units with arrays embeded into their notion of equality. When we have to align that involves a unit with inexact equality, we first check to see if a member of the equality contains a NumericWithUnits containing the unit we're after; if so we use it. If the unit isn't there, we then take each element of the equality and try to convert to the target unit - the first one we find wins and is rolled up into the conversion.
Here's where the subtle use of convention comes in: elements in the equality Array are tried in order. The first elements are preferred over the latter elements. This tightens up the inexactness and makes conversions predictable. Of course, there's nothing preventing us from using later elements in our conversions - we just have to convert through the Array explicitly.
But perhaps we've been to generic. Usually inexactness like this is caused by trying too hard to model everything with artificial rules rather than include the outlying richness of the real world. For instance, in business planning a generic month is typically considered to be 30 days long - but it's perfectly reasonable to ask how many days there are in August and use that as a basis for conversion. It's the facts about the real world that aren't reflected in our units system.
We can push on our units system slightly and include some of the real-world exceptions to the rules. For instance:
definitions.rb a units pipe dream | |
Units.create :time do |m| m.system :months do |s| s.unit :name => :january, :equals => 31.days s.unit :name => :february, :equals => [28.days, 29.days] s.unit :name => :march, :equals => 31.days s.unit :name => :april, :equals => 30.days s.unit :name => :may, :equals => 31.days s.unit :name => :june, :equals => 30.days s.unit :name => :july, :equals => 31.days s.unit :name => :august, :equals => 31.days s.unit :name => :september, :equals => 30.days s.unit :name => :october, :equals => 31.days s.unit :name => :november, :equals => 30.days s.unit :name => :december, :equals => 31.days end end |
And spin a little bit of code into Object's method_missing method:
units.rb a units pipe dream | |
class Object def method_missing(method,*args) begin s = method.to_s.split('_') if s.select{|e| e == 'per'}.size > 0 per_ratio method, args elsif s.select{|e| e == 'in'}.size > 0 per_ratio method.to_s.sub(/_in_/,'_per_').to_sym, args else convert_unit_value method, args end rescue Exception => exception method_missing_before_units method, args end end end |
This lets us do wonderful things like
puts hours_in_january | ==> | '744.0' |
puts july.weeks | ==> | '4.42857142857143 weeks' |
Yeah, that's just the way I'd expect it to read. Except that fractional weeks aren't natural - when I ask for weeks, I want weeks and days. Let's add one of our formatters and tame this a little.
definitions.rb a units pipe dream | |
Units.create :time do |m| m.system :base do |s| s.format :name => :weeks_and_days, :format => lambda { |u| weeks = u.weeks.floor days = (u-weeks).days.round weeks == 0 ? (days == 0 ? "#{weeks}" : "#{days}") : "#{weeks} #{days}" } end end |
Now we can do
puts july.format :weeks_and_days | ==> | '4 weeks 3 days' |
Beautiful! Except, darn it, using that format method just seems to get in our way. Sure, it's specific and we really do need to say what format we should use, but couldn't we just call it from the to_s method? Of course!
units.rb a units pipe dream | |
class NumericWithUnits def to_s(format = nil) format == nil ? "#{numeric} #{unit.to_s(numeric)}" : self.format(format) end end |
That should do it.
puts july.to_s :weeks_and_days | ==> | '4 weeks 3 days' |
I just love Ruby!
Okay. So now, let's get back to the speed of light.
definitions.rb a units pipe dream | |
Units.derive :velocity, Units.length/Units.time do |m| m.system :metric do |s| s.unit :constant => true, :name => :speed_of_light, :no_plural => true, :equals => 299792458.meters/second, :abbrev => :c end end |
We see a few interesting things here, namely the use of :constant and :no_plural, but let's ignore those for a second. The big thing is that this definition will work, and should resolve our issue with the undefined forward references. Let's make sure.
puts 4.5.ly.in_mi | ==> | '26435654658917.8 miles' |
puts 4.5.ly.AU | ==> | '284389.813364457 astronomical_units' |
puts speed_of_light.miles | ==> | '186282.024486427 mi / s' |
puts speed_of_light.feet_nanosecond | ==> | '0.983569089299333 ft / ns' |
Yep, Grace Hopper was right - about a foot per nanosecond.
One thing troubles me though. We've used underscores in the names of light years and astronomical units. I want to clean up this output.
units.rb a units pipe dream | |
class UnitsHash def to_s(numeric = 1,use_abbrevs = false) if size == 1 && (su = select {|k,v| v == 1}).size == 1 && !use_abbrevs su = su.to_a[0][0] (numeric == 1 ? su.name : su.plural).gsub(/_/,' ') else abbrevs_to_s end end end |
Now let's go back and try that printing again.
puts 4.5.ly.AU | ==> | '284389.813364457 astronomical units' |
No underscore, much nicer. Picky, perhaps. But it's the details that make it smooth. Looking at it symetrically however, what about underscores on the input side?
puts 4.5.light_years.AU | ==> |
Error. Damn, it tried to parse light_years into converting light and years. This is a tough problem, one based in the ambiguity of human utterance. We want things both ways - in some cases the units should be separate, in the other we want the units divided. If we don't do it right, our units-users will get very confused. And what if it occurs during definition versus happening when units are being used? We need a plan.
Well, it looks like we have it right already during definition. If we look back to the definition of parsec in the length measure, that's defined as 3.262 light years, and it works just fine. The key there is that we don't decompose the units on underscores - we just take them verbatim as a unit identifier. Great and whew. Half our work is already working right and we don't have to try to handle some really hairy forward referencing issues. So let's look at the half not done.
puts 4.5.unite("light_years").AU | ==> | '284389.813364457 astronomical units' |
puts 4.5.unite(["light_years"]).AU | ==> | '284389.813364457 astronomical units' |
Ah ha! There is hope! If we use unite, we can get around our issue by avoiding the parsing! And we could give unite an Array of units if we have multiples! Therefore, our plan is to convert these underscored units into the elements of an array during the parse before we hand them to the unite. Hmmm. But not just unite. We need to be underscore-sensitive anywhere we parse a method as units. It's time to pick a convention and do a little software dance.
Here's what we'll do. We'll look for occurences of the string
'_and_'
. If we see any, we'll automatically assume we have units with embeded underscores between them. If we don't see any, we'll try the string by itself. If we get back a MissingUnitsException, we'll assume the underscores are
separating units and we'll parse out each of them. We'll be careful to only do our decomposition during unit
use
- we'll leave unit
definition
alone.
units.rb a units pipe dream | |
class Object def convert_per_ratio(method) ps = method.to_s.split '_per_' raise UnitsException('invalid per method') if ps.size != 2 value = (1.unite ps[1])/(1.unite ps[0]) raise UnitsException.new("per ratio has units") if value.kind_of? NumericWithUnits value end end class Numeric def method_missing(method,*args) if Units.defining? reference = Units.make_forward_reference(method,Units.defining?) begin value = Units.convert self, method Units.release_forward_reference reference value rescue MissingUnitsException Units.hold_forward_reference rescue UnitsException => exception units_problem("definition",exception,method,args) rescue Exception => exception method_missing_before_units(method,args) end else begin ps = method.to_s.split '_per_' case ps.size when 1 then unite method when 2 then unite(ps[0])/(1.unite(ps[1])) else raise UnitsException('invalid per method') end rescue UnitsException => exception units_problem("usage",exception,method,args) rescue Exception method_missing_before_units(method,*args) end end end def unite(unit=nil,power=1,measure=nil) if !unit self else if (unit.kind_of? Array) units = UnitsHash.new unit.each {|u| units.merge! 1.unite(u)} unit = units elsif (unit.kind_of? String)||(unit.kind_of? Symbol) s = unit.to_s as = s.split('_and_') if as.size == 1 units = Units.lookup(as[0]) case units.size when 1 then unit = units[0] when 0 as = as[0].split('_') units_problem("usage", AmbiguousUnitsException.new(unit), :unit=,unit) if as.size == 0 unite(as,power,measure).unit else units_problem("usage", AmbiguousUnitsException.new(unit), :unit=,unit) end else unite(as,power,measure).unit end elsif unit.kind_of? NumericWithUnits unit = unit.unit end NumericWithUnits.new(self,unit,power) end end end class NumericWithUnits def method_missing(method,*args) begin s = method.to_s ms = s.split '_' if ms[0] == 'to' convert! s.gsub(/^to_/,"") elsif ms[0] == 'in' convert s.gsub(/^in_/,"") elsif ms.select{|e| e == 'per'}.size > 0 convert_per method else convert s end rescue Exception value = numeric.send(method,*args) if (value.kind_of? String) "#{value} #{unit.to_s(numeric)}" else NumericWithUnits.new(value,unit) end end end def convert_per method ps = method.to_s.split '_per_' raise UnitsException.new('invalid per method') if ps.size != 2 numerator = 1.unite(ps[0]) numerator_units = Set.new numerator.unit.keys denominator = 1.unite(ps[1]) denominator_units = Set.new denominator.unit.keys positives = unit.keys.collect{|k| unit[k] > 0 ? k : nil}.compact! negatives = unit.keys.collect{|k| unit[k] < 0 ? k : nil}.compact! numerator_positives = Set.new 1.unite(positives).align(numerator,false).unit.keys numerator_negatives = Set.new 1.unite(negatives).align(numerator,false).unit.keys denominator_positives = Set.new 1.unite(positives).align(denominator,false).unit.keys denominator_negatives = Set.new 1.unite(negatives).align(denominator,false).unit.keys if (numerator_units == numerator_positives) && (denominator_units == denominator_negatives) convert(ps[0]+'_and_'+ps[1]) elsif (numerator_units == numerator_negatives) && (denominator_units == denominator_positives) convert(ps[0]+'_and_'+ps[1])**-1 else raise UnitsException.new('invalid per units') end end end |
Apart from determining if we need special treatment for the per conversion, we've pulled all of the heavy lifting into the unite method. Now we can say things like
puts 4.5.light_years.astronomical_units | ==> | '284389.813364457 astronomical units' |
and they just work.
Okay, let's quick jump back to the :constant and :no_plural we used to define the speed of light. We skipped over them because they really don't mean much - the :constant is just for information purposes, and the :no_plural is just a cue for default formating. We're going to leave the :constant out of everything - remember, since a UnitsUnit is a kind of MethodicHash, it's just getting added as a hashed value with no other overhead. We'll add just a little bit of code to take care of the :no_plural case.
units.rb a units pipe dream | |
class UnitsUnit def add_plural self.plural = (self.no_plural == true) ? self.name : (plural = self.plural) ? plural.to_s : self.name+'s' end end |
That's it. If it's set to no plural, we make the plural the same as the singular name. This allows fictitios statements to be made, like
puts 4.5.speed_of_light | ==> | '4.5 speed of light' |
and keep the unit name singular even when a plural would have been used. The trick is that we are using the plural, but it's the same as the singular form.
A good friend of mine recently mentioned the need for a Units Framework - and I couldn't help but committing the gem to the waking hours before I had a chance to complete the work. However, since the scalar are complete, now's a good time. More is to come, but for now I've been rousted.
I will be back to bed shortly, hopefully to pick up where I've left off...