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 = /^(?#{DECIMAL_REGEX}|#{RATIONAL_REGEX})\s+(?#{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 (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.present? && output_unit.present? && 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.01) 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