Unit conversion and parsing

This commit is contained in:
Dan Elbert 2016-01-15 13:49:08 -06:00
parent ec099a0077
commit 433eecbc69
3 changed files with 216 additions and 6 deletions

View File

@ -66,6 +66,13 @@ class IngredientsController < ApplicationController
@ingredients = Ingredient.where("name LIKE ?", query).order(:name) @ingredients = Ingredient.where("name LIKE ?", query).order(:name)
end end
def convert
quantity = params[:quantity]
unit = params[:unit]
factor = params[:factor]
output_unit = params[:output_unit]
end
private private
# Use callbacks to share common setup or constraints between actions. # Use callbacks to share common setup or constraints between actions.
def set_ingredient def set_ingredient

View File

@ -1,6 +1,9 @@
module UnitConversion module UnitConversion
UNIT_PARSING_REGEX = /^(?<value>(?:-?[0-9]+)?(?:\.?[0-9]*)?)\s*(?<unit>[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 = /^(?<value>#{DECIMAL_REGEX}|#{RATIONAL_REGEX})\s+(?<unit>#{UNIT_REGEX})$/
UNIT_ALIASES = { UNIT_ALIASES = {
cup: %w(cups c), cup: %w(cups c),
@ -31,7 +34,8 @@ module UnitConversion
if match && match[:value].present? && match[:unit].present? if match && match[:value].present? && match[:unit].present?
begin begin
Unitwise(match[:value].to_f, match[:unit]) value = get_value(match[:value])
Unitwise(value, normalize_unit_names(match[:unit]))
rescue Unitwise::ExpressionError => err rescue Unitwise::ExpressionError => err
raise UnknownUnitError, err.message raise UnknownUnitError, err.message
end end
@ -45,13 +49,55 @@ module UnitConversion
end end
def normalize_unit_names(unit_description) def normalize_unit_names(unit_description)
result = unit_description.dup unit_description.downcase.gsub(/[a-z]+/) do |match|
result = result.downcase.gsub(/[a-z]+/) do |match|
UNIT_ALIAS_MAP[match] || match UNIT_ALIAS_MAP[match] || match
end 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
end end

View File

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