Unit conversion and parsing
This commit is contained in:
parent
ec099a0077
commit
433eecbc69
@ -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
|
||||
|
@ -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
|
||||
|
157
spec/models/unit_conversion_spec.rb
Normal file
157
spec/models/unit_conversion_spec.rb
Normal 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
|
Loading…
Reference in New Issue
Block a user