From 1150af514341d9ed492b1abfa0c327e1a3ede77e Mon Sep 17 00:00:00 2001 From: Dan Elbert Date: Wed, 20 Jan 2016 15:07:37 -0600 Subject: [PATCH] Better fractinoal display --- app/models/unit_conversion.rb | 126 ++++++++++++++++++++++++++-- spec/models/unit_conversion_spec.rb | 3 +- 2 files changed, 120 insertions(+), 9 deletions(-) diff --git a/app/models/unit_conversion.rb b/app/models/unit_conversion.rb index 4528003..02b0f87 100644 --- a/app/models/unit_conversion.rb +++ b/app/models/unit_conversion.rb @@ -1,9 +1,10 @@ module UnitConversion + INTEGER_REGEX = /-?[0-9]+/ DECIMAL_REGEX = /(?:-?[0-9]+)?\.[0-9]+/ - RATIONAL_REGEX = /-?(?:[0-9]+|(?:[0-9] )?[0-9]+\/[0-9]+)/ + RATIONAL_REGEX = /-?(?:[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_PARSING_REGEX = /^(?#{INTEGER_REGEX}|#{DECIMAL_REGEX}|#{RATIONAL_REGEX})\s+(?#{UNIT_REGEX})$/ UNIT_ALIASES = { cup: %w(cups c), @@ -12,16 +13,60 @@ module UnitConversion ounce: %w(oz ounces), pound: %w(pounds lb lbs), pint: %w(pints), - quart: %w(quarts), + quart: %w(quarts qt), gallon: %w(gallons ga), gram: %w(grams g), kilogram: %w(kilograms kg), - milliliter: %w(ml milliliters) + milliliter: %w(ml milliliters), + liter: %w(l 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), + + ounce: (0..16), + pound: (0.5..9999), + + gram: (1.0..750), + kilogram: (0.5..9999), + + milliliter: (1.0..750), + liter: (0.5..9999) + } + + UNIT_ORDERS = { + standard_volume: [ + :teaspoon, + :tablespoon, + :cup, + :quart, + :gallon + ], + + metric_volume: [ + :milliliter, + :liter + ], + + standard_mass: [ + :ounce, + :pound + ], + + metric_mass: [ + :gram, + :kilogram + ] + } + class UnparseableUnitError < StandardError end @@ -78,7 +123,9 @@ module UnitConversion end def get_value(str) - if str =~ /^#{RATIONAL_REGEX}$/ + if str =~ /^#{INTEGER_REGEX}$/ + str.to_i + elsif str =~ /^#{RATIONAL_REGEX}$/ parts = str.split(' ') if parts.length == 2 whole = parts.first.to_r @@ -124,8 +171,12 @@ module UnitConversion converted = unit.convert_to(output_unit).value end - if value.is_a? Rational - rational_val = converted.to_r.rationalize(0.01) + quantity_format(converted, value) + end + + def quantity_format(quantity, original_quantity) + if original_quantity.is_a?(Rational) || original_quantity.is_a?(Integer) + rational_val = rationalize(quantity) if rational_val.denominator == 1 rational_val.to_i.to_s elsif rational_val.denominator < rational_val.numerator.abs @@ -136,9 +187,68 @@ module UnitConversion rational_val.to_s end else - "%g" % ("%.3f" % converted) + "%g" % ("%.3f" % quantity) end end + def rationalize(value) + + if value.is_a? Integer + return value + end + + if value.floor == value + return value + end + + useful_denominators = [2, 3, 4, 8, 16] + + whole = value.floor + fraction = value - whole + + approximations = useful_denominators.map do |d| + best_n = (1...d).each.map { |n| {delta: (fraction - Rational(n, d)).abs, numerator: n} }.sort { |a, b| a[:delta] <=> b[:delta] }.first + {denominator: d, numerator: best_n[:numerator], delta: best_n[:delta]} + end + + # Add 0 and 1 + approximations << { denominator: 1, numerator: 0, delta: (fraction - Rational(0,1)).abs } + approximations << { denominator: 1, numerator: 1, delta: (fraction - Rational(1,1)).abs } + + best = approximations.sort { |a, b| [a[:delta], a[:denominator]] <=> [b[:delta], b[:denominator]] }.first + + Rational((whole * best[:denominator]) + best[:numerator], best[:denominator]) + end + + # Given an awkward measurement such as '18 9/10 oz' or '5/24 cup' or '0.09434 cup', + # 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) } + new_unit = normalized_unit + + if unit_orders && (unit_range = UNIT_RANGE[normalized_unit.to_sym]) + + if value < unit_range.first + unit_orders = unit_orders.reverse + end + + idx = unit_orders.index(new_unit.to_sym) + + 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] + end + end + + quantity_format(value, initial_value) + end + end end diff --git a/spec/models/unit_conversion_spec.rb b/spec/models/unit_conversion_spec.rb index ba601a7..0afe0fd 100644 --- a/spec/models/unit_conversion_spec.rb +++ b/spec/models/unit_conversion_spec.rb @@ -8,6 +8,7 @@ RSpec.describe UnitConversion do 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) + expect(UnitConversion.get_value('18 9/10')).to eq Rational(189, 10) end it 'returns decimals' do @@ -39,7 +40,7 @@ RSpec.describe UnitConversion do expect(UnitConversion.convert('1', '1', 'tablespoon', 'cup')).to eq '1/16' expect(UnitConversion.convert('2.0', '1', 'tablespoon', 'cup')).to eq '0.125' expect(UnitConversion.convert('2/3', '1', 'tablespoon', 'teaspoons')).to eq '2' - expect(UnitConversion.convert('2', '4', 'teaspoons', 'tablespoons')).to eq '2 2 /3' + expect(UnitConversion.convert('2', '4', 'teaspoons', 'tablespoons')).to eq '2 2/3' end it 'scales odd units without conversion' do