Enhanced unit conversion
This commit is contained in:
parent
1150af5143
commit
d909347a7e
2
.gitignore
vendored
2
.gitignore
vendored
@ -18,3 +18,5 @@
|
||||
|
||||
/.idea
|
||||
.byebug_history
|
||||
|
||||
/public/assets
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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|
|
||||
|
Loading…
Reference in New Issue
Block a user