UnitSoup
Unit Soup is a DRY approach to unit conversion. Specify a set of conversion rules and you can start converting from one unit to another.
Analogy
- 
Mixis a ready-made set of units and conversion rules.
- 
Soupis made from (soup) mixes.
- 
Measurementis amount of items of a particular type (unit). (e.g. 3 cars, 4 miles)
- 
Soupcan convert onemeasurementto another if possible.
Usage
- Add as many mixestosoup.
- Make the soup.
- Start converting measurementsfrom one unit to another.
Examples
Metric Units
require "unit_soup/mix"
require "unit_soup/soup"
metric_units_mix = Mix.new("Metric Units") do |m|
  m << "1 cm = 10 mm"
  m << "1 dm = 10 cm"
  m << "1 m = 100 cm"
  m << "1 dcm = 10 m"
  m << "1 hcm = 100 m"
  m << "1 km = 1000 m"
end
soup = Soup.new("Metric Soup (mm to km)")
soup << metric_units_mix
soup.make
# convert 550 centimeters to meters
soup.convert(550, :cm, :m) #=> 5.5
# convert 50000 centimeters to kilometers
soup.convert(50000, :cm, :km) #=> 0.5Simple expenses calculator
require "unit_soup/mix"
require "unit_soup/soup"
expenses_per_time_period_mix = Mix.new("Expenses/TimePeriod") do |mix|
  mix << "1 per_day = 7 per_week"
  mix << "1 per_week = 4 per_month"
  mix << "1 per_month = 12 per_year"
  mix << "1 per_weekend = 1 per_week"
  mix << "1 per_weekday = 5 per_week"
end
soup = Soup.new("Expense calculator")
soup << expenses_per_time_period_mix
soup.make
parking_per_week = Measurement.new(50, "per_week")
parking_per_month = soup.convert(parking_per_week, :per_month) #=> 200
parking_per_year = soup.convert(parking_per_week, :per_year) #=> 2400Details
Unit
- Has nameandsymbol
- 
Unit.new("Kilometer", :km)orUnit.new(:km)
- Usually inferred from the rules
Measurement
- Describes things like "3 cars", "4 miles"
- Has amountandunit
- m = Measurement.new("3 car")- 
m.unit=>:car
- 
m.amount=>3/1 (rational)
 
- 
- Other ways to initialize
- Measurement.from("4 mile")
- Measurement.new(4, :mile)
- Measurement.new(4, Unit.new(:mile))
- Measurement.new(2.5, :mile)
- Measurement.new("2.5", :mile)
- Measurement.new(3/2r, :mile)
- Measurement.new("3/2", :mile)
- Measurement.from(another_measurement)
 
Rule
- Describes conversion rule from one unit to another.
- Can be specified as equality of two measurements.
- 
1 km = 1000 m- 
this_measurement=1 km,that_measurement=1000 m
 
- 
- r = Rule.new("1 km = 1000 m")orRule.new("1000 m = 1 km")
- 
valid?can be used to check if a string can be parsed into aRule
- Can be initialized as
- Rule.new "1 km = 1000 m"
- Rule.from "1 km = 1000 m"
- Rule.from another_rule
- Rule.new measurement_a, measurement_b
- Rule.new Measurement.new("1 km"), Measurement.new(1000, :m)
 
Mix
- 
Represents a set of rules 
- 
Can be used to group related units together. e.g. Metric Units, time units, etc. 
- 
Has a name and a Set of rules 
- 
Initializing: time_units = Mix.new("Time Units")orMix.define("Time Units")
- 
Adding Rules: - time_units << "1 minute = 60 second"
- time_units << Rule.new("1 minute = 60 second")
- time_units.add "1 minute = 60 second"
- time_units.add Rule.new("1 minute = 60 second")
- From Rule strings list: distance << ["1cm = 10mm", "1km = 1.60934mile"]
- From Rule list: distance << [Rule.new("1cm = 10mm"), Rule.new("1km = 1.60934mile")]
- From mixed list: distance << ["1cm = 10mm", Rule.new("1km = 1.60934mile")]
- From other mixes:
- my_mix << time_units
- my_mix << distance_units
 
 
- 
Initialize via block: 
time_units = Mix.define("Time units") do |m|  
  m << "1 minute = 60 second"
  m << ["1 hour = 60 minute", "1 millisecond  = 1000 second"]
end
Soup
- Soup allows conversion from one unit to another
- Has an optional name
- Supports adding rules from strings, lists and mixes exactly like Mix- Soup.new("time_soup") << time_units_mix
 
- Add as many related/unrelated rules and mixes as you like to the soup
- 
make: Make the soup (internally creates a graph of all rules for conversion lookups)
- 
convert(value, from, to)=> rational representing converted value in target unit- 
convert(2, :minute, :second)=> 120/1
- 
convert(2, Unit.new(:minute), :second)=> 120/1
- 
convert(50, :foo, :bar)=>nil#if conversion was not possible
 
- 
- Return value is a rational to keep precision
- 
makeneeds to be called beforeconverts can work
Conversion
- 
soup.makeCreates a graph from the rules. So,soup = Soup.new("my_distance_converter") soup << Mix.new("my_distance_mix") do |m| m << "1 centimeter = 10 millimeter" m << "1 decimeter = 10 centimeter" m << "1 centimeter = 100 meter" m << "1 centimeter = 0.39 inch" m << "1 foot = 12 inch" m << "1 kilometer = 1000 meter" m << "1 kilometer = 0.62 mile" end soup.make becomes +------------+ | millimeter | +------------+ | | | +-----------+ +------------+ +------+ +------+ | decimeter | --- | centimeter | --- | inch | --- | foot | +-----------+ +------------+ +------+ +------+ | | | +-----------+ +------------+ | kilometer | --- | meter | +-----------+ +------------+ | | | +-----------+ | mile | +-----------+
- 
Any rules added after soup.makewill not be a part of the graph until another call tosoup.makeis made.
- 
A lookupis initialized internally with all direct conversions inferred from the set of rules. e.g. From "1 foot = 12 inch" we can inferlookup[:foot][:inch]=12andlookup[:inch][:foot]=1/12
- 
soup.convert(2, :foot, :inch)-lookuphas information to convert fromfoottoinch, soconvertreturns2 * (lookup[:foot][:inch]=12) = 24/1
- 
soup.convert(200, :foot, :meter)-lookupdoes not have this information, so abfsgraph traversal starting fromfootis performed. It gives us the following chain that can be traversed to get the right fraction for multiplication to perform conversion:+-------+ +------------+ +------+ +------+ | meter | <--- | centimeter | <--- | inch | <--- | foot | +-------+ +------------+ +------+ +------+ 200 * lookup[:foot][:inch] * lookup[:inch][:centimeter] * lookup[:centimeter][:meter]This conversion fraction is also saved in lookup[:foot][:meter]andlookup[:meter][:foot]so the next conversion fromfoottometerdoes not perform a graph traversal.
Installation
Add this line to your application's Gemfile:
gem 'unit_soup'And then execute:
$ bundle
Or install it yourself as:
$ gem install unit_soup
Development
After checking out the repo, run bin/setup to install dependencies. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome.
License
The gem is available as open source under the terms of the MIT License.