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