Better fractinoal display

This commit is contained in:
Dan Elbert 2016-01-20 15:07:37 -06:00
parent bc65256776
commit 1150af5143
2 changed files with 120 additions and 9 deletions

View File

@ -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 = /^(?<value>#{DECIMAL_REGEX}|#{RATIONAL_REGEX})\s+(?<unit>#{UNIT_REGEX})$/
UNIT_PARSING_REGEX = /^(?<value>#{INTEGER_REGEX}|#{DECIMAL_REGEX}|#{RATIONAL_REGEX})\s+(?<unit>#{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

View File

@ -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