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

View File

@ -1,6 +1,9 @@
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 = {
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

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