Enhanced unit conversion

This commit is contained in:
Dan Elbert 2016-01-20 18:37:28 -06:00
parent 1150af5143
commit d909347a7e
7 changed files with 143 additions and 58 deletions

4
.gitignore vendored
View File

@ -17,4 +17,6 @@
/tmp /tmp
/.idea /.idea
.byebug_history .byebug_history
/public/assets

View File

@ -17,7 +17,7 @@ class RecipesController < ApplicationController
# GET /recipes/1 # GET /recipes/1
def scale def scale
@scale = params[:factor] @scale = params[:factor]
@recipe.scale(@scale) @recipe.scale(@scale, true)
render :show render :show
end end

View File

@ -24,9 +24,9 @@ class Recipe < ActiveRecord::Base
end end
end end
def scale(factor) def scale(factor, auto_unit = false)
recipe_ingredients.each do |ri| recipe_ingredients.each do |ri|
ri.scale(factor) ri.scale(factor, auto_unit)
end end
end end

View File

@ -22,9 +22,13 @@ class RecipeIngredient < ActiveRecord::Base
end end
end end
def scale(factor) def scale(factor, auto_unit = false)
if factor.present? && self.quantity.present? && factor != '1' if factor.present? && self.quantity.present? && factor != '1'
self.quantity = UnitConversion.convert(self.quantity, factor, nil, nil) self.quantity = UnitConversion.convert(self.quantity, factor, nil, nil)
if auto_unit
self.quantity, self.units = UnitConversion.auto_unit(self.quantity, self.units)
end
end end
end end

View File

@ -7,63 +7,63 @@ module UnitConversion
UNIT_PARSING_REGEX = /^(?<value>#{INTEGER_REGEX}|#{DECIMAL_REGEX}|#{RATIONAL_REGEX})\s+(?<unit>#{UNIT_REGEX})$/ UNIT_PARSING_REGEX = /^(?<value>#{INTEGER_REGEX}|#{DECIMAL_REGEX}|#{RATIONAL_REGEX})\s+(?<unit>#{UNIT_REGEX})$/
UNIT_ALIASES = { UNIT_ALIASES = {
cup: %w(cups c), '[cup_us]': %w(cup cups c),
tablespoon: %w(tbsp tbs tablespoons), '[tbs_us]': %w(tablespoon tablespoons tbsp tbs),
teaspoon: %w(tsp teaspoons), '[tsp_us]': %w(teaspoon teaspoons tsp),
ounce: %w(oz ounces), '[oz_av]': %w(ounce ounces oz),
pound: %w(pounds lb lbs), '[lb_av]': %w(pound pounds lb lbs),
pint: %w(pints), '[pt_us]': %w(pint pints),
quart: %w(quarts qt), '[qt_us]': %w(quart quarts qt),
gallon: %w(gallons ga), '[gal_us]': %w(gallon gallons ga),
gram: %w(grams g), g: %w(gram grams),
kilogram: %w(kilograms kg), kg: %w(kilograms kilogram),
milliliter: %w(ml milliliters), ml: %w(milliliter milliliters),
liter: %w(l liters) l: %w(liter liters)
} }
UNIT_ALIAS_MAP = Hash[UNIT_ALIASES.map { |unit, values| values.map { |v| [v, unit] } }.flatten(1)] UNIT_ALIAS_MAP = Hash[UNIT_ALIASES.map { |unit, values| values.map { |v| [v, unit] } }.flatten(1)]
# map that gives comfortable ranges for a given set of units # map that gives comfortable ranges for a given set of units
UNIT_RANGES = { UNIT_RANGES = {
teaspoon: (0.125...3.0), '[tsp_us]': (0.125...3.0),
tablespoon: (0.5...4.0), '[tbs_us]': (0.5...4.0),
cup: (0.25...4.0), '[cup_us]': (0.25...4.0),
quart: (1.0...4.0), '[qt_us]': (1.0...4.0),
gallon: (0.5..9999), '[gal_us]': (0.5..9999),
ounce: (0..16), '[oz_av]': (0..16),
pound: (0.5..9999), '[lb_av]': (1.0..9999),
gram: (1.0..750), g: (1.0..750),
kilogram: (0.5..9999), kg: (0.5..9999),
milliliter: (1.0..750), ml: (1.0..750),
liter: (0.5..9999) l: (0.5..9999)
} }
UNIT_ORDERS = { UNIT_ORDERS = {
standard_volume: [ standard_volume: [
:teaspoon, :'[tsp_us]',
:tablespoon, :'[tbs_us]',
:cup, :'[cup_us]',
:quart, :'[qt_us]',
:gallon :'[gal_us]'
], ],
metric_volume: [ metric_volume: [
:milliliter, :ml,
:liter :l
], ],
standard_mass: [ standard_mass: [
:ounce, :'[oz_av]',
:pound :'[lb_av]'
], ],
metric_mass: [ metric_mass: [
:gram, :g,
:kilogram :kg
] ]
} }
@ -192,7 +192,6 @@ module UnitConversion
end end
def rationalize(value) def rationalize(value)
if value.is_a? Integer if value.is_a? Integer
return value return value
end end
@ -203,6 +202,10 @@ module UnitConversion
useful_denominators = [2, 3, 4, 8, 16] useful_denominators = [2, 3, 4, 8, 16]
if value.is_a?(Rational) && useful_denominators.include?(value.denominator)
return value
end
whole = value.floor whole = value.floor
fraction = value - whole fraction = value - whole
@ -224,13 +227,12 @@ module UnitConversion
# it will round the rational and scale the unit to give a more reasonable, useful measurement, ie # it will round the rational and scale the unit to give a more reasonable, useful measurement, ie
# 19 oz, 3 Tbsp, 1 1/2 Tbsp # 19 oz, 3 Tbsp, 1 1/2 Tbsp
def auto_unit(quantity, units) def auto_unit(quantity, units)
# First scale the unit if it seems to use the wrong unit
normalized_unit = normalize_unit_names(units) normalized_unit = normalize_unit_names(units)
value = initial_value = get_value(quantity) value = initial_value = get_value(quantity)
type_key, unit_orders = UNIT_ORDERS.detect { |key, orders| orders.include(normalized_unit.to_sym) } type_key, unit_orders = UNIT_ORDERS.detect { |key, orders| orders.include?(normalized_unit.to_sym) }
new_unit = normalized_unit new_unit = normalized_unit
if unit_orders && (unit_range = UNIT_RANGE[normalized_unit.to_sym]) if unit_orders && (unit_range = UNIT_RANGES[normalized_unit.to_sym])
if value < unit_range.first if value < unit_range.first
unit_orders = unit_orders.reverse unit_orders = unit_orders.reverse
@ -241,13 +243,19 @@ module UnitConversion
while !unit_range.include?(value) && idx < (unit_orders.length - 1) while !unit_range.include?(value) && idx < (unit_orders.length - 1)
idx += 1 idx += 1
next_unit = unit_orders[idx] next_unit = unit_orders[idx]
value = Unitwise(value, new_unit).convert_to(next_unit).value value = Unitwise(value, new_unit).convert_to(next_unit).value.round(4)
new_unit = next_unit new_unit = next_unit.to_s
unit_range = UNIT_RANGE[new_unit.to_sym] unit_range = UNIT_RANGES[new_unit.to_sym]
end end
end end
quantity_format(value, initial_value) if normalized_unit == new_unit
new_unit = units
else
new_unit = (UNIT_ALIASES[new_unit.to_sym] || []).first || new_unit
end
return quantity_format(value, initial_value), new_unit
end end
end end

View File

@ -23,6 +23,6 @@ module Parsley
# Do not swallow errors in after_commit/after_rollback callbacks. # Do not swallow errors in after_commit/after_rollback callbacks.
config.active_record.raise_in_transactional_callbacks = true config.active_record.raise_in_transactional_callbacks = true
config.assets.precompile << Proc.new { |filename, path| puts "#{filename}, #{path}"; yea =%w(.eot .svg .tff .woff .woff2).include?(File.extname(filename)); puts yea; yea } config.assets.precompile << Proc.new { |filename, path| %w(.eot .svg .tff .woff .woff2).include?(File.extname(filename)) }
end end
end end

View File

@ -2,6 +2,35 @@ require 'rails_helper'
RSpec.describe UnitConversion do RSpec.describe UnitConversion do
describe '.auto_unit' do
it 'leaves units alone if reasonable' do
expect(UnitConversion.auto_unit('1/2', 'tbsp')).to eq ['1/2', 'tbsp']
expect(UnitConversion.auto_unit('2', 'cups')).to eq ['2', 'cups']
expect(UnitConversion.auto_unit('1', 'c')).to eq ['1', 'c']
end
it 'leaves units alone if unknown' do
expect(UnitConversion.auto_unit('1/16', 'splat')).to eq ['1/16', 'splat']
expect(UnitConversion.auto_unit('4.5', 'dogs')).to eq ['4.5', 'dogs']
expect(UnitConversion.auto_unit('0', '')).to eq ['0', '']
end
it 'converts standard volume units correctly' do
expect(UnitConversion.auto_unit('6', 'tsp')).to eq ['2', 'tablespoon']
expect(UnitConversion.auto_unit('24', 'tsp')).to eq ['1/2', 'cup']
expect(UnitConversion.auto_unit('1/16', 'cup')).to eq ['1', 'tablespoon']
expect(UnitConversion.auto_unit('1/48', 'cup')).to eq ['1', 'teaspoon']
expect(UnitConversion.auto_unit('768', 'tsp')).to eq ['1', 'gallon']
end
it 'converts standard mass units correctly' do
expect(UnitConversion.auto_unit('32', 'oz')).to eq ['2', 'pound']
expect(UnitConversion.auto_unit('40', 'oz')).to eq ['2 1/2', 'pound']
expect(UnitConversion.auto_unit('3/4', 'lb')).to eq ['12', 'ounce']
expect(UnitConversion.auto_unit('0.0625', 'lb')).to eq ['1', 'ounce']
end
end
describe '.get_value' do describe '.get_value' do
it 'returns rationals' do it 'returns rationals' do
expect(UnitConversion.get_value('1/2')).to eq Rational(1, 2) expect(UnitConversion.get_value('1/2')).to eq Rational(1, 2)
@ -146,17 +175,59 @@ RSpec.describe UnitConversion do
end end
end end
describe '.rationalize' do
it 'leaves integers alone' do
expect(UnitConversion.rationalize(1)).to eq 1
expect(UnitConversion.rationalize(15)).to eq 15
expect(UnitConversion.rationalize(-1)).to eq -1
expect(UnitConversion.rationalize(0)).to eq 0
end
it 'leaves non-fractional numbers alone' do
expect(UnitConversion.rationalize(1.0)).to eq 1.0
expect(UnitConversion.rationalize(-1.0)).to eq -1.0
expect(UnitConversion.rationalize(0.0)).to eq 0.0
expect(UnitConversion.rationalize(35.0)).to eq 35.0
end
it 'leaves already nice rationals alone' do
expect(UnitConversion.rationalize(Rational(1,2))).to eq Rational(1,2)
expect(UnitConversion.rationalize(Rational(5,2))).to eq Rational(5,2)
expect(UnitConversion.rationalize(Rational(3,16))).to eq Rational(3,16)
expect(UnitConversion.rationalize(Rational(3,4))).to eq Rational(3,4)
end
it 'converts neat decimals to rationals' do
expect(UnitConversion.rationalize(1.5)).to eq Rational(3,2)
expect(UnitConversion.rationalize(0.125)).to eq Rational(1,8)
expect(UnitConversion.rationalize(5.0625)).to eq Rational(81, 16)
expect(UnitConversion.rationalize(0.75)).to eq Rational(3,4)
end
it 'rounds weird rationals to nice rationals' do
expect(UnitConversion.rationalize(Rational(3,7))).to eq Rational(7,16)
expect(UnitConversion.rationalize(Rational(2,5))).to eq Rational(3,8)
expect(UnitConversion.rationalize(Rational(2,5))).to eq Rational(3,8)
end
it 'rounds weird decimals to nice rationals' do
expect(UnitConversion.rationalize(0.24)).to eq Rational(1,4)
expect(UnitConversion.rationalize(1.24)).to eq Rational(5,4)
expect(UnitConversion.rationalize(1.13)).to eq Rational(9,8)
end
end
describe '.normalize_unit_name' do describe '.normalize_unit_name' do
it 'converts simple units' do it 'converts simple units' do
data = { data = {
'c' => 'cup', 'c' => '[cup_us]',
'cups' => 'cup', 'cups' => '[cup_us]',
'pints' => 'pint', 'pints' => '[pt_us]',
'g' => 'gram', 'gram' => 'g',
'grams' => 'gram', 'grams' => 'g',
'Grams' => 'gram', 'Grams' => 'g',
'Tbsp' => 'tablespoon' 'Tbsp' => '[tbs_us]'
} }
data.each do |input, output| data.each do |input, output|
@ -166,9 +237,9 @@ RSpec.describe UnitConversion do
it 'converts mixed units' do it 'converts mixed units' do
data = { data = {
'oz/c' => 'ounce/cup', 'oz/c' => '[oz_av]/[cup_us]',
'kg/cups' => 'kilogram/cup', 'kilograms/cups' => 'kg/[cup_us]',
'pints/junk' => 'pint/junk' 'pints/junk' => '[pt_us]/junk'
} }
data.each do |input, output| data.each do |input, output|