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