parsley/app/models/unit_conversion.rb
2016-01-15 16:15:50 -06:00

136 lines
3.8 KiB
Ruby

module UnitConversion
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),
tablespoon: %w(tbsp tbs tablespoons),
teaspoon: %w(tsp teaspoons),
ounce: %w(oz ounces),
pound: %w(pounds lb lbs),
pint: %w(pints),
quart: %w(quarts),
gallon: %w(gallons ga),
gram: %w(grams g),
kilogram: %w(kilograms kg)
}
UNIT_ALIAS_MAP = Hash[UNIT_ALIASES.map { |unit, values| values.map { |v| [v, unit] } }.flatten(1)]
class UnparseableUnitError < StandardError
end
class UnknownUnitError < UnparseableUnitError
end
class << self
def parse(unit_string)
match = UNIT_PARSING_REGEX.match(unit_string.to_s.strip)
if match && match[:value].present? && match[:unit].present?
begin
value = get_value(match[:value])
Unitwise(value, normalize_unit_names(match[:unit]))
rescue Unitwise::ExpressionError => err
raise UnknownUnitError, err.message
end
else
raise UnparseableUnitError, "'#{unit_string}' does not appear to be a valid measurement of the form <value> <units> (ie '5 cup' or '223 gram/cup')"
end
end
def density?(unit)
unit.compatible_with? Unitwise(1, 'g/ml')
end
def volume?(unit)
unit.compatible_with? Unitwise(1, 'ml')
end
def mass?(unit)
unit.compatible_with? Unitwise(1, 'g')
end
def get_density(str)
unit = parse(str)
raise UnknownUnitError, "#{str} expected to be a density" unless density?(unit)
unit
end
def normalize_unit_names(unit_description)
unit_description.downcase.gsub(/[a-z]+/) do |match|
UNIT_ALIAS_MAP[match] || match
end
end
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, density = nil)
value = get_value(quantity)
factor = get_value(factor)
if value.is_a?(BigDecimal) && factor.is_a?(Rational)
factor = factor.to_d(10)
end
converted = value * factor
input_unit = normalize_unit_names(input_unit) unless input_unit.nil?
output_unit = normalize_unit_names(output_unit) unless output_unit.nil?
if input_unit && output_unit && input_unit != output_unit
in_unit = Unitwise(1, input_unit)
out_unit = Unitwise(1, output_unit)
unit = Unitwise(converted, input_unit)
if volume?(in_unit) && mass?(out_unit)
unit_density = get_density(density)
unit = unit * unit_density
elsif mass?(in_unit) && volume?(out_unit)
unit_density = get_density(density)
unit = unit / unit_density
end
converted = unit.convert_to(output_unit).value
end
if value.is_a? Rational
rational_val = converted.to_r.rationalize(0.001)
if rational_val.denominator == 1
rational_val.to_i.to_s
elsif rational_val.denominator < rational_val.numerator.abs
whole = rational_val.floor
fraction = rational_val - whole
"#{whole} #{fraction}"
else
rational_val.to_s
end
else
"%g" % ("%.3f" % converted)
end
end
end
end