In Progress
Unit 1, Lesson 21
In Progress

Unitwise

Video transcript & code

Our long adventure in building classes that represent physical measurements is nearly at an end. Along the way we've learned about immutability, value objects, conversion functions, and many other useful tidbits. What started out seeming like a trivial exercise turned out to have surprising depth.

And yet, many episodes later, our classes for representing Feet and Meters are still disappointingly rudimentary. Most glaringly, those are the only two units we support. But there are other issues. For instance, intuitively when we multiply one foot measurement by another, the result should be in a square feet type. But that is not the case.

require "./quantities"

Feet(20) * Feet(30)             # => #<Feet:600.0>

And that's just a simple example. What about more complex compound units, such as foot-pounds?

We know enough now that we could continue extending our code to support numerous units of measure as well as compound units. But the truth is, this is a solved problem. In fact, there are at least a dozen libraries tackling this task in Ruby alone.

Today let's take a look at one of these libraries. Unitwise, by Josh W. Lewis, aims to be a complete solution for dealing with physical quantities and units of measure in Ruby.

Let's require the library and play around with it.

To represent a measurement, we use the Unitwise conversion function, and give it a magnitude and a unit type. The unit type is a string, which Unitwise will parse and try to recognize from its extensive database of units.

Given a measurement, we can examine its unit. There's a lot of information here, which we won't dig unto right now.

Naturally, we can also get the measurement's raw value, using either the #value method or conventional explicit conversion methods.

Let's try multiplying our measurement by a raw number. This works no matter which side of the operator we put the units on.

Now let's try to subtract a raw number from our measurement. As we discussed back in Episode 200, this is the kind of operation which could hide a latent mixed-units bug in a program. When we evaluate this expression, it fails with a TypeError, preventing us from introducing such a bug.

require "unitwise"

m = Unitwise(23, "foot") # => #<Unitwise::Measurement value=23 unit=foot>
m.unit
# => #<Unitwise::Unit expression="foot", terms=[#<Unitwise::Term atom=#<Unitwise::Atom names=["foot"], primary_code="[ft_i]", secondary_code="[FT_I]", symbol="ft", scale=#<Unitwise::Scale value=0.12E2 unit=[in_i]>, classification="intcust", property="length", metric=false, special=false, arbitrary=false, dim="L">, prefix=nil, factor=1, exponent=1, annotation=nil>]>
m.value
# => 23

m.to_i                          # => 23
m.to_f                          # => 23.0

m * 10                          # => #<Unitwise::Measurement value=230 unit=foot> 

10 * m                          # => #<Unitwise::Measurement value=230 unit=[ft_i]>

m - 5                           # => 
# ~> /home/avdi/.rvm/gems/ruby-2.1.0/gems/unitwise-0.7.1/lib/unitwise/measurement.rb:72:in `-': Can't subtract 5 from 23 foot. (TypeError)
# ~>    from -:12:in `<main>'

Of course, we can add and subtract to our heart's content so long as our units are specified.

We can even mix units.

require 'unitwise'

Unitwise(50, "foot") + Unitwise(30, "foot")
# => #<Unitwise::Measurement value=80 unit=foot>

Unitwise(50, "foot") - Unitwise(30, "foot")
# => #<Unitwise::Measurement value=20 unit=foot>

Unitwise(20, "foot") + Unitwise(10, "yard")
# => #<Unitwise::Measurement value=50 unit=foot>

Speaking of mixing units, we can also explicitly request conversions from one unit to another.

require "unitwise"

Unitwise(1, "yard").convert_to("meter")
# => #<Unitwise::Measurement value=0.9144E0 unit=meter>

As you might expect, we can also compare measurements of disparate units to each other.

require "unitwise"

Unitwise(1, "yard") > Unitwise(1, "meter")
# => false

…but when we try to compare units from different domains, we get an argument error.

require "unitwise"

Unitwise(1, "yard") > Unitwise(1, "volt")
# => 
# ~> -:3:in `>': comparison of Unitwise::Measurement with Unitwise::Measurement failed (ArgumentError)
# ~>    from -:3:in `<main>'

Now to tackle one of the questions that we kicked off this episode with. Can Unitwise handle compound units? Let's find out.

We'll start by multiplying two measurements in feet. Let's take a closer look at the unit of the result. The important portion is here, in the value of "unit". The somewhat obscure term "fti" is the designation for imperial feet from the Unified Code for Units of Measure. The trailing "2" indicates that the unit is feet squared. If we want we can also break the unit down into its individual terms.

require "unitwise"

result = Unitwise(5, "foot") * Unitwise(5, "foot") 
# => #<Unitwise::Measurement value=0.25E2 unit=[ft_i]2>

result.unit
# => #<Unitwise::Unit expression="[ft_i]2", terms=[#<Unitwise::Term atom=#<Unitwise::Atom names=["foot"], primary_code="[ft_i]", secondary_code="[FT_I]", symbol="ft", scale=#<Unitwise::Scale value=0.12E2 unit=[in_i]>, classification="intcust", property="length", metric=false, special=false, arbitrary=false, dim="L">, prefix=nil, factor=1, exponent=1, annotation=nil>, #<Unitwise::Term atom=#<Unitwise::Atom names=["foot"], primary_code="[ft_i]", secondary_code="[FT_I]", symbol="ft", scale=#<Unitwise::Scale value=0.12E2 unit=[in_i]>, classification="intcust", property="length", metric=false, special=false, arbitrary=false, dim="L">, prefix=nil, factor=1, exponent=1, annotation=nil>]>

result.unit.terms
# => [#<Unitwise::Term atom=#<Unitwise::Atom names=["foot"], primary_code="[ft_i]", secondary_code="[FT_I]", symbol="ft", scale=#<Unitwise::Scale value=0.12E2 unit=[in_i]>, classification="intcust", property="length", metric=false, special=false, arbitrary=false, dim="L">, prefix=nil, factor=1, exponent=1, annotation=nil>,
#     #<Unitwise::Term atom=#<Unitwise::Atom names=["foot"], primary_code="[ft_i]", secondary_code="[FT_I]", symbol="ft", scale=#<Unitwise::Scale value=0.12E2 unit=[in_i]>, classification="intcust", property="length", metric=false, special=false, arbitrary=false, dim="L">, prefix=nil, factor=1, exponent=1, annotation=nil>]

Let's try something a little more complex. We'll multiply feet by pounds. Unitwise understands this, and generates a measurement of foot-pounds.

require "unitwise"

Unitwise(10, "foot") * Unitwise(20, "pound")
# => #<Unitwise::Measurement value=200 unit=[ft_i].[lb_av]>

As a matter of fact, Unitwise can deal with arbitrarily complex compound units. In the gem's README you can find an impressive example involving distance, acceleration, horsepower, and miles per hour.

distance = 0.25.mile # => #<Unitwise::Measurement value=0.25 unit=mile>
time = 10.second # => #<Unitwise::Measurement value=10 unit=second>
mass = 2800.pound # => #<Unitwise::Measurement value=2800 unit=pound>

acceleration = 2 * distance / time ** 2
# => #<Unitwise::Measurement value=0.005 unit=[mi_us]/s2>

force = (mass * acceleration).to_lbf
# => #<Unitwise::Measurement value=2297.5084316991147 unit=lbf>

power = (force * distance / time).to_horsepower
# => #<Unitwise::Measurement value=551.4031264140402 unit=horsepower>

speed = ((2 * acceleration * distance) ** 0.5).convert_to("mile/hour")
# => #<Unitwise::Measurement value=180.0 unit=mile/hour>

One last thing. If we require the library "unitwise/ext", we can get a syntactic shortcut for creating unit values. This library extends Ruby's built-in numeric types so that we can type things like 23.foot and get a Unitwise object.

require "unitwise/ext"

23.foot                         # => #<Unitwise::Measurement value=23 unit=foot>

Personally, I find these sorts of core extensions a little too clever for my taste. And if you do use this feature, it should only be within your own applications, never in a public gem. One of the things that I like about Unitwise is that, while many physical quantities libraries offer this kind of shorthand, Unitwise makes it strictly optional.

Well, this overview should get you up and running with representing quantities using Unitwise. Happy hacking!

Responses