diff --git a/app/controllers/ingredients_controller.rb b/app/controllers/ingredients_controller.rb index dec2aef..788deeb 100644 --- a/app/controllers/ingredients_controller.rb +++ b/app/controllers/ingredients_controller.rb @@ -66,6 +66,13 @@ class IngredientsController < ApplicationController @ingredients = Ingredient.where("name LIKE ?", query).order(:name) end + def convert + quantity = params[:quantity] + unit = params[:unit] + factor = params[:factor] + output_unit = params[:output_unit] + end + private # Use callbacks to share common setup or constraints between actions. def set_ingredient diff --git a/app/models/unit_conversion.rb b/app/models/unit_conversion.rb index c11ca45..4768863 100644 --- a/app/models/unit_conversion.rb +++ b/app/models/unit_conversion.rb @@ -1,6 +1,9 @@ module UnitConversion - UNIT_PARSING_REGEX = /^(?(?:-?[0-9]+)?(?:\.?[0-9]*)?)\s*(?[a-z\/.\-()0-9]+)$/ + DECIMAL_REGEX = /(?:-?[0-9]+)?\.[0-9]+/ + RATIONAL_REGEX = /-?(?:[0-9]+|(?:[0-9] )?[0-9]+\/[0-9]+)/ + UNIT_REGEX = /[\[\]a-zA-Z][\[\]a-zA-Z\/.\-()0-9]*/ + UNIT_PARSING_REGEX = /^(?#{DECIMAL_REGEX}|#{RATIONAL_REGEX})\s+(?#{UNIT_REGEX})$/ UNIT_ALIASES = { cup: %w(cups c), @@ -31,7 +34,8 @@ module UnitConversion if match && match[:value].present? && match[:unit].present? begin - Unitwise(match[:value].to_f, match[:unit]) + value = get_value(match[:value]) + Unitwise(value, normalize_unit_names(match[:unit])) rescue Unitwise::ExpressionError => err raise UnknownUnitError, err.message end @@ -45,13 +49,55 @@ module UnitConversion end def normalize_unit_names(unit_description) - result = unit_description.dup - - result = result.downcase.gsub(/[a-z]+/) do |match| + unit_description.downcase.gsub(/[a-z]+/) do |match| UNIT_ALIAS_MAP[match] || match end + end - result + def get_value(str) + if str =~ /^#{RATIONAL_REGEX}$/ + parts = str.split(' ') + if parts.length == 2 + whole = parts.first.to_r + fractional = parts.last.to_r + (whole.abs + fractional) * (whole < 0 ? -1.to_r : 1.to_r) + else + str.to_r + end + elsif str =~ /^#{DECIMAL_REGEX}$/ + str.to_d + else + raise UnparseableUnitError, "str (#{str}) is not a valid number" + end + end + + def convert(quantity, factor, input_unit, output_unit) + + value = get_value(quantity) + factor = get_value(factor) + + if value.is_a?(BigDecimal) && factor.is_a?(Rational) + factor = factor.to_d(10) + end + + input_unit = normalize_unit_names(input_unit) + output_unit = normalize_unit_names(output_unit) + + original = Unitwise(value, input_unit) + scaled = original * factor + + converted = scaled.convert_to output_unit + + if value.is_a? Rational + rational_val = converted.value.to_r.rationalize(0.001) + if rational_val.denominator == 1 + rational_val.to_i.to_s + else + rational_val.to_s + end + else + "%g" % ("%.3f" % converted.value) + end end end diff --git a/spec/models/unit_conversion_spec.rb b/spec/models/unit_conversion_spec.rb new file mode 100644 index 0000000..977e577 --- /dev/null +++ b/spec/models/unit_conversion_spec.rb @@ -0,0 +1,157 @@ +require 'rails_helper' + +RSpec.describe UnitConversion do + + describe '.get_value' do + it 'returns rationals' do + expect(UnitConversion.get_value('1/2')).to eq Rational(1, 2) + expect(UnitConversion.get_value('1 1/2')).to eq Rational(3, 2) + expect(UnitConversion.get_value('-1/2')).to eq Rational(-1, 2) + expect(UnitConversion.get_value('-1 1/2')).to eq Rational(-3, 2) + end + + it 'returns decimals' do + expect(UnitConversion.get_value('-1')).to eq -1.to_d + expect(UnitConversion.get_value('1.0')).to eq 1.to_d + expect(UnitConversion.get_value('5.56')).to eq "5.56".to_d + end + end + + describe '.convert' do + it 'scales decimal numbers' do + expect(UnitConversion.convert('1', '2', 'cup', 'cup')).to eq '2' + expect(UnitConversion.convert('1.5', '2', 'cup', 'cup')).to eq '3' + expect(UnitConversion.convert('4', '.5', 'cup', 'cup')).to eq '2' + expect(UnitConversion.convert('4', '1/2', 'cup', 'cup')).to eq '2' + end + + it 'scales rational numbers' do + expect(UnitConversion.convert('1/2', '2', 'cup', 'cup')).to eq '1' + expect(UnitConversion.convert('1 1/2', '2', 'cup', 'cup')).to eq '3' + expect(UnitConversion.convert('4', '.5', 'cup', 'cup')).to eq '2' + expect(UnitConversion.convert('4', '1/2', 'cup', 'cup')).to eq '2' + expect(UnitConversion.convert('2', '1/3', 'cup', 'cup')).to eq '2/3' + end + + it 'converts units' do + expect(UnitConversion.convert('1/2', '1', 'cup', 'tbsp')).to eq '8' + expect(UnitConversion.convert('8', '1', 'tablespoon', 'cup')).to eq '1/2' + expect(UnitConversion.convert('1', '1', 'tablespoon', 'cup')).to eq '1/16' + expect(UnitConversion.convert('2.0', '1', 'tablespoon', 'cup')).to eq '0.125' + end + + it 'converts and scales' do + expect(UnitConversion.convert('1/2', '2', 'cup', 'tbsp')).to eq '16' + expect(UnitConversion.convert('2.0', '1 1/2', 'tablespoon', 'cup')).to eq '0.188' + expect(UnitConversion.convert('2', '1 1/2', 'tablespoon', 'cup')).to eq '3/16' + end + end + + describe '.parse' do + it 'correctly parses strings to Units' do + data = { + '4 c' => Unitwise(4, 'cup'), + '5.5 oz' => Unitwise(5.5, 'ounce'), + '-4 tbsp' => Unitwise(-4, 'tablespoon'), + '223 g/c' => Unitwise(223, 'gram/cup'), + '1/3 c' => Unitwise("1/3".to_r, 'cup'), + '-5/16 g' => Unitwise("-5/16".to_r, 'gram') + } + + data.each do |input, output| + expect(UnitConversion.parse(input)).to eq output + end + end + + it 'raises UnknownUnitError with bad units' do + data = [ + '5 cats', + '33 gulps/thinking', + '9.2 meeters/s2', + '5/8 floor' + ] + + data.each do |input| + expect { UnitConversion.parse(input) }.to raise_error(UnitConversion::UnknownUnitError), "'#{input}' didn't raise" + end + end + + it 'raises UnparseableUnitError on malformed string' do + data = [ + '55', + '55.5', + '-55', + '-55.55', + '5.5/2 cups', + '2/3.0 cups', + 'ounce', + '-.4 cups', + 'gallons 6', + '5,5 tsp' + ] + + data.each do |input| + expect { UnitConversion.parse(input) }.to raise_error(UnitConversion::UnparseableUnitError), "'#{input}' didn't raise" + end + end + end + + describe '.density?' do + it 'returns true for any mass over volume unit' do + data = [ + Unitwise(1, 'gram/cup'), + Unitwise(1, 'pound/gallon'), + Unitwise(1, 'ounce/tablespoon'), + Unitwise(1, 'ounce/centimeter3') + ] + + data.each do |input| + expect(UnitConversion.density?(input)).to be_truthy + end + end + + it 'returns false for any non density unit' do + data = [ + Unitwise(1, 'cup'), + Unitwise(1, 'gram'), + Unitwise(1, 'gram/hour'), + Unitwise(1, 'centimeter3/ounce') + ] + + data.each do |input| + expect(UnitConversion.density?(input)).to be_falsey + end + end + end + + describe '.normalize_unit_name' do + + it 'converts simple units' do + data = { + 'c' => 'cup', + 'cups' => 'cup', + 'pints' => 'pint', + 'g' => 'gram', + 'grams' => 'gram' + } + + data.each do |input, output| + expect(UnitConversion.normalize_unit_names(input)).to eq output + end + end + + it 'converts mixed units' do + data = { + 'oz/c' => 'ounce/cup', + 'kg/cups' => 'kilogram/cup', + 'pints/junk' => 'pint/junk' + } + + data.each do |input, output| + expect(UnitConversion.normalize_unit_names(input)).to eq output + end + end + + end + +end