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

2
.gitignore vendored
View File

@ -18,3 +18,5 @@
/.idea
.byebug_history
/public/assets

View File

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

View File

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

View File

@ -22,9 +22,13 @@ class RecipeIngredient < ActiveRecord::Base
end
end
def scale(factor)
def scale(factor, auto_unit = false)
if factor.present? && self.quantity.present? && factor != '1'
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

View File

@ -7,63 +7,63 @@ module UnitConversion
UNIT_PARSING_REGEX = /^(?<value>#{INTEGER_REGEX}|#{DECIMAL_REGEX}|#{RATIONAL_REGEX})\s+(?<unit>#{UNIT_REGEX})$/
UNIT_ALIASES = {
cup: %w(cups c),
tablespoon: %w(tbsp tbs tablespoons),
teaspoon: %w(tsp teaspoons),
ounce: %w(oz ounces),
pound: %w(pounds lb lbs),
pint: %w(pints),
quart: %w(quarts qt),
gallon: %w(gallons ga),
'[cup_us]': %w(cup cups c),
'[tbs_us]': %w(tablespoon tablespoons tbsp tbs),
'[tsp_us]': %w(teaspoon teaspoons tsp),
'[oz_av]': %w(ounce ounces oz),
'[lb_av]': %w(pound pounds lb lbs),
'[pt_us]': %w(pint pints),
'[qt_us]': %w(quart quarts qt),
'[gal_us]': %w(gallon gallons ga),
gram: %w(grams g),
kilogram: %w(kilograms kg),
milliliter: %w(ml milliliters),
liter: %w(l liters)
g: %w(gram grams),
kg: %w(kilograms kilogram),
ml: %w(milliliter milliliters),
l: %w(liter liters)
}
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
UNIT_RANGES = {
teaspoon: (0.125...3.0),
tablespoon: (0.5...4.0),
cup: (0.25...4.0),
quart: (1.0...4.0),
gallon: (0.5..9999),
'[tsp_us]': (0.125...3.0),
'[tbs_us]': (0.5...4.0),
'[cup_us]': (0.25...4.0),
'[qt_us]': (1.0...4.0),
'[gal_us]': (0.5..9999),
ounce: (0..16),
pound: (0.5..9999),
'[oz_av]': (0..16),
'[lb_av]': (1.0..9999),
gram: (1.0..750),
kilogram: (0.5..9999),
g: (1.0..750),
kg: (0.5..9999),
milliliter: (1.0..750),
liter: (0.5..9999)
ml: (1.0..750),
l: (0.5..9999)
}
UNIT_ORDERS = {
standard_volume: [
:teaspoon,
:tablespoon,
:cup,
:quart,
:gallon
:'[tsp_us]',
:'[tbs_us]',
:'[cup_us]',
:'[qt_us]',
:'[gal_us]'
],
metric_volume: [
:milliliter,
:liter
:ml,
:l
],
standard_mass: [
:ounce,
:pound
:'[oz_av]',
:'[lb_av]'
],
metric_mass: [
:gram,
:kilogram
:g,
:kg
]
}
@ -192,7 +192,6 @@ module UnitConversion
end
def rationalize(value)
if value.is_a? Integer
return value
end
@ -203,6 +202,10 @@ module UnitConversion
useful_denominators = [2, 3, 4, 8, 16]
if value.is_a?(Rational) && useful_denominators.include?(value.denominator)
return value
end
whole = value.floor
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
# 19 oz, 3 Tbsp, 1 1/2 Tbsp
def auto_unit(quantity, units)
# First scale the unit if it seems to use the wrong unit
normalized_unit = normalize_unit_names(units)
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
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
unit_orders = unit_orders.reverse
@ -241,13 +243,19 @@ module UnitConversion
while !unit_range.include?(value) && idx < (unit_orders.length - 1)
idx += 1
next_unit = unit_orders[idx]
value = Unitwise(value, new_unit).convert_to(next_unit).value
new_unit = next_unit
unit_range = UNIT_RANGE[new_unit.to_sym]
value = Unitwise(value, new_unit).convert_to(next_unit).value.round(4)
new_unit = next_unit.to_s
unit_range = UNIT_RANGES[new_unit.to_sym]
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

View File

@ -23,6 +23,6 @@ module Parsley
# Do not swallow errors in after_commit/after_rollback callbacks.
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

View File

@ -2,6 +2,35 @@ require 'rails_helper'
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
it 'returns rationals' do
expect(UnitConversion.get_value('1/2')).to eq Rational(1, 2)
@ -146,17 +175,59 @@ RSpec.describe UnitConversion do
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
it 'converts simple units' do
data = {
'c' => 'cup',
'cups' => 'cup',
'pints' => 'pint',
'g' => 'gram',
'grams' => 'gram',
'Grams' => 'gram',
'Tbsp' => 'tablespoon'
'c' => '[cup_us]',
'cups' => '[cup_us]',
'pints' => '[pt_us]',
'gram' => 'g',
'grams' => 'g',
'Grams' => 'g',
'Tbsp' => '[tbs_us]'
}
data.each do |input, output|
@ -166,9 +237,9 @@ RSpec.describe UnitConversion do
it 'converts mixed units' do
data = {
'oz/c' => 'ounce/cup',
'kg/cups' => 'kilogram/cup',
'pints/junk' => 'pint/junk'
'oz/c' => '[oz_av]/[cup_us]',
'kilograms/cups' => 'kg/[cup_us]',
'pints/junk' => '[pt_us]/junk'
}
data.each do |input, output|