Josh W Lewis
Josh W Lewis
/
Essays
/
Rails Unit Measurement Persistence

Rails Unit Measurement Persistence


By on March 9, 2014


I’ve worked on a few apps in the past that had some cumbersome constraints in regards to the units of measurement. Models needed to store heat transfer or fluid dynamics properties in either english or SI units. This is can be a bit of a pain, so I thought I’d share my methods and save someone else the trouble.

My problem scenario was in an engineering realm, but there are plenty of other mainstream domains that exhibit the problem. Consider foot races – they are commonly defined in either mileage (like 26.1 miles for a marathon) or kilometers (like a 5K). Or consider a mechanic’s wrenches – they have a metric set with sizes like 10 mm, but also have an english set with sizes like 5/8 inch.

To demonstrate the problem and solution, say you are creating an app for cooks. Maybe it would let cooks share recipes, perhaps it might be used to help them decide what to offer on tonight’s menu, or perhaps it just tracks ingredient inventory. In any of those cases, you probably need a model for a recipe and its ingredients.

# create_table :recipes do |t|
#   t.string  :name, null: false
# end
class Recipe < ActiveRecord::Base
  has_many :ingredients
end
# create_table :ingredients do |t|
#   t.string  :substance,     null: false
#   t.decimal :quantity,      null: false,  default: 0
#   t.integer :recipe_id
# end
class Ingredient < ActiveRecord::Base
  belongs_to :recipe
end

So, these are some pretty typical models. Sure, they are overly simplified. In any case, we can setup a new ingredient pretty easily.

Ingredient.create(substance: 'Olive Oil', quantity: 1)

Now we can see the problem. The obvious question here is how much olive oil is ‘1’? Is that one teaspoon, tablespoon, ounce, or maybe even pint or gallon?

In a lot of apps, you might just have some tribal knowledge where everyone understands that we store quantities in tablespoons. That might be fine in a lot of cases. But, say we need to know how much olive oil to buy for next week. Can we buy olive oil by the tablespoon? My olive oil usually comes in 750 ml bottles. How many tablespoons is that?

It just so happens that I’ve built a tool for that. Unitwise is a Ruby library for converting and performing math on all kinds of units, volumes included.

Say our grand total of estimated olive oil usage for the week was 1000 tablespoons. With Unitwise, we could calculate how many bottles of olive oil is required.

(1000.tablespoon / 750.ml).to_f # => 19.715686375000004

Helpful, but there is a little more we can do here. Instead of relying on tribal knowledge about storing the quantity in tablespoons, we could make it explicit.

class Ingredient < ActiveRecord::Base
  def quantity
    read_attribute(:quantity).to_tablespoon
  end
end

With this modification, each time we retrieve the quantity, we’ll get a measurement back with a value and a unit.

Ingredient.last.quantity # => #<Unitwise::Measurement value=1 unit=tablespoon>

Unitwise comes with a handy to_s method that should get invoked by your views, which means you can display this value to your users as '1 tablespoon'.

Cool, but we can make this a little better yet.

class Ingredient < ActiveRecord::Base
  def quantity
    # unchanged from above
  end

  def quantity=(value)
    write_attribute(:quantity, value.to_tablespoon)
  end
end

Now we have the advantage of setting the quantity with any unit compatible with tablespoons – that is, any volumetric unit.

tomatoes = Ingredient.create(name: 'Crushed tomatoes', quantity: 8.fluid_ounce)
tomatoes.quantity # => #<Unitwise::Measurement value=16 unit=tablespoon>

With this change, we can now set the quantity with any compatible unit. When we retrieve the value, it comes back in tablespoons.

We are definitely making progress here. All the values stored and retrieved are based on tablespoons, no matter what unit you set it with. But, who buys tomatoes by the tablespoon? When the amount of tomatoes is displayed to the user, they probably just want to know how many cans (8 fluid ounces) or cups to use.

Lets make one more enhancement. This time, we are going to add a string column to the ingredients table so we can display an appropriate unit for any ingredient.

# create_table :ingredients do |t|
#   t.string  :substance,     null: false
#   t.decimal :quantity,      null: false,  default: 0
#   t.string  :quantity_unit, null: false,  default: 'tablespoon'
#   t.integer :recipe_id,     null: false
# end

class Ingredient < ActiveRecord::Base

  def quantity
    read_attribute(:quantity).convert_to(self.quantity_unit)
  end

  def quantity=(value)
    write_attribute :quantity, value.to_tablespoon
    if value.respond_to?(:unit)
      self.quantity_unit = value.unit.to_s
    end
  end

end

Now we’ve got something solid. Now all database entries are stored in the same unit (tablespoon), which is nice, in case some other program or query wants to do something with that data. And now whatever unit we set the quantity with is what gets read into the model. For example:

water = Ingredient.create(substance: 'Water', quantity: 1.cup)
water.quantity # => #<Unitwise::Measurement value=1 unit=cup>

beer = Ingredient.create(wine: 'Beer', quantity: 1.pint)
beer.quantity # => #<Unitwise::Measurmeent value=1 unit=pint>

This example is fairly simple, but it should give you an idea of how to do something similar in your own domain. Unitwise supports a ton of units, and has some other really nice features – Check it out.

This article was categorized as ruby, rails, gem, math, and science