diff --git a/.gitignore b/.gitignore index 1ea0960..2463e2a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,6 @@ /tmp /.idea -.byebug_history \ No newline at end of file +.byebug_history + +/public/assets \ No newline at end of file diff --git a/app/controllers/recipes_controller.rb b/app/controllers/recipes_controller.rb index 88c1fd8..8eabbf5 100644 --- a/app/controllers/recipes_controller.rb +++ b/app/controllers/recipes_controller.rb @@ -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 diff --git a/app/models/recipe.rb b/app/models/recipe.rb index 7faf4ca..225005b 100644 --- a/app/models/recipe.rb +++ b/app/models/recipe.rb @@ -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 diff --git a/app/models/recipe_ingredient.rb b/app/models/recipe_ingredient.rb index 542a06f..26290be 100644 --- a/app/models/recipe_ingredient.rb +++ b/app/models/recipe_ingredient.rb @@ -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 diff --git a/app/models/unit_conversion.rb b/app/models/unit_conversion.rb index 02b0f87..97f5cab 100644 --- a/app/models/unit_conversion.rb +++ b/app/models/unit_conversion.rb @@ -7,63 +7,63 @@ module UnitConversion UNIT_PARSING_REGEX = /^(?#{INTEGER_REGEX}|#{DECIMAL_REGEX}|#{RATIONAL_REGEX})\s+(?#{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 diff --git a/config/application.rb b/config/application.rb index 275f52c..79888e1 100644 --- a/config/application.rb +++ b/config/application.rb @@ -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 diff --git a/spec/models/unit_conversion_spec.rb b/spec/models/unit_conversion_spec.rb index 0afe0fd..78d92f6 100644 --- a/spec/models/unit_conversion_spec.rb +++ b/spec/models/unit_conversion_spec.rb @@ -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|