FIX: issues with convertion and unit prefix #1

Merged
merged 3 commits into from Sep 19, 2011
View
139 lib/alchemist.rb
@@ -11,7 +11,7 @@ module Alchemist
class << self
attr_accessor :use_si
end
-
+
@@si_units = %w[m meter metre meters metres liter litre litres liters l L farad farads F coulombs C gray grays Gy siemen siemens S mhos mho ohm ohms volt volts V ]
@@si_units += %w[joule joules J newton newtons N lux lx henry henrys H b B bits bytes bit byte lumen lumens lm candela candelas cd]
@@si_units += %w[tesla teslas T gauss Gs G gram gramme grams grammes g watt watts W pascal pascals Pa]
@@ -25,15 +25,17 @@ class << self
:electromotive_force => :volt,
:power => :watt,
:illuminance => :lux,
- :pressure => :pascal
+ :pressure => :pascal,
+ :distance => :meter
}
@@common_imperial_units = {
:time => :second,
:temperature => :fahrenheit,
:electromotive_force => :volt,
:power => :watt,
:illuminance => :lux,
- :power => :psi
+ :power => :psi,
+ :distance => :feet
}
@@operator_actions = {}
@@ -57,7 +59,7 @@ class << self
},
:area => {
:square_meter => 1.0, :square_meters => 1.0, :square_metre => 1.0, :square_metres => 1.0,
- :acre => 4046.85642, :acres => 4046.85642,
+ :acre => 4046.85642, :acres => 4046.85642,
:are => 1.0e+2, :ares => 1.0e+2, :a => 1.0e+2,
:barn => 1.0e-28, :barns => 1.0e-28, :b => 1.0e-28,
:circular_mil => 5.067075e-10, :circular_mils => 5.067075e-10,
@@ -279,15 +281,15 @@ class << self
},
:temperature => {
:kelvin => 1.0, :K => 1.0,
-
+
:celsius => [Proc.new{ |t| t + 273.15 }, Proc.new{ |t| t - 273.15 }], :centrigrade => [Proc.new{ |t| t + 273.15 }, Proc.new{ |t| t - 273.15 }],
:degree_celsius => [Proc.new{ |t| t + 273.15 }, Proc.new{ |t| t - 273.15 }], :degree_centrigrade => [Proc.new{ |t| t + 273.15 }, Proc.new{ |t| t - 273.15 }],
:degrees_celsius => [Proc.new{ |t| t + 273.15 }, Proc.new{ |t| t - 273.15 }], :degrees_centrigrade => [Proc.new{ |t| t + 273.15 }, Proc.new{ |t| t - 273.15 }],
:fahrenheit => [Proc.new{ |t| (t + 459.67) * (5.0/9.0) }, Proc.new{ |t| t * (9.0/5.0) - 459.67 }],
:degree_fahrenheit => [Proc.new{ |t| (t + 459.67) * (5.0/9.0) }, Proc.new{ |t| t * (9.0/5.0) - 459.67 }],
:degrees_fahrenheit => [Proc.new{ |t| (t + 459.67) * (5.0/9.0) }, Proc.new{ |t| t * (9.0/5.0) - 459.67 }],
:rankine => 1.8, :rankines => 1.8
- },
+ },
:time => {
:second => 1.0, :seconds => 1.0, :s => 1.0,
:minute => 60.0, :minutes => 60.0, :min => 60.0,
@@ -350,9 +352,9 @@ class << self
:atto => 1e-18, :a => 1e-18,
:zepto => 1e-21, :z => 1e-21,
:yocto => 1e-24, :y => 1e-24,
-
+
# binary prefixes
-
+
:kibi => 2.0**10.0, :Ki => 2.0**10.0,
:mebi => 2.0**20.0, :Mi => 2.0**20.0,
:gibi => 2.0**30.0, :Gi => 2.0**30.0,
@@ -362,19 +364,19 @@ class << self
:zebi => 2.0**70.0, :Zi => 2.0**70.0,
:yobi => 2.0**80.0, :Yi => 2.0**80.0
}
-
+
def from(unit_name)
send(unit_name)
end
-
+
def self.unit_prefixes
@@unit_prefixes
end
-
+
def self.conversion_table
@@conversion_table
end
-
+
def self.operator_actions
@@operator_actions
end
@@ -386,7 +388,7 @@ def self.common_metric_units
def self.common_imperial_units
@@common_imperial_units
end
-
+
class CompoundNumericConversion
attr_accessor :numerators, :denominators
def initialize(numerator)
@@ -396,15 +398,15 @@ def initialize(numerator)
end
def *(value)
case value
- when Numeric
+ when Numeric
@coefficient *= value
self
when Alchemist::NumericConversion
@numerators << value
return consolidate
end
end
-
+
def consolidate
@numerators.each_with_index do |numerator, n|
@denominators.each_with_index do |denominator, d|
@@ -420,71 +422,75 @@ def consolidate
end
if @denominators.length == 0 && @numerators.length == 1
@numerators[0] * @coefficient
- elsif @denominators.length == 0 && @numerators.length == 0
+ elsif @denominators.length == 0 && @numerators.length == 0
@coefficient
else
self
end
end
-
+
def to_s
-
+
end
-
+
def method_missing(method, *attrs, &block)
if Conversions[method]
@denominators << 1.send(method)
consolidate
end
end
end
-
+
class NumericConversion
include Comparable
-
+
def per
Alchemist::CompoundNumericConversion.new(self)
end
-
+
def p
per
end
-
+
def to(type = nil)
unless type
- self
+ self
else
send(type)
end
end
alias_method :as, :to
-
+
def base(unit_type)
if(Alchemist.conversion_table[unit_type][@unit_name].is_a?(Array))
@exponent * Alchemist.conversion_table[unit_type][@unit_name][0].call(@value)
else
@exponent * @value * Alchemist.conversion_table[unit_type][@unit_name]
end
end
-
+
def unit_name
@unit_name
end
+ def prefix_name
+ @prefix_name
+ end
+
def unit_name_t # returns the translated unit name
if @value == 1
- return I18n.t("units.#{Conversions[@unit_name]}.#{@unit_name}.name.one")
+ return I18n.t("units.#{Conversions[@unit_name]}.#{@prefix_name}#{@unit_name}.name.one")
else
- return I18n.t("units.#{Conversions[@unit_name]}.#{@unit_name}.name.many")
+ return I18n.t("units.#{Conversions[@unit_name]}.#{@prefix_name}#{@unit_name}.name.many")
end
end
def unit_symbol_t # returns the translated unit symbol
- return I18n.t("units.#{Conversions[@unit_name]}.#{@unit_name}.symbol")
+ return I18n.t("units.#{Conversions[@unit_name]}.#{@prefix_name}#{@unit_name}.symbol")
end
-
+
def to_s
- (@exponent*@value).to_s + " " + self.unit_symbol_t
+ (@exponent * @value).to_s + " " + self.unit_symbol_t
end
def get_regional_unit(country)
@@ -510,44 +516,61 @@ def get_regional_unit(country)
def to_regional_unit(country)
return self.to(get_regional_unit(country))
end
-
+
def value
@value
end
-
+
def to_f
@value
end
-
+
def ==(other)
self <=> other
end
-
+
def <=>(other)
- (self.to_f * @exponent).to_f <=> other.to(@unit_name).to_f
+ (self.to_f * @exponent).to_f <=> other.to(@prefix_name.to_s + @unit_name.to_s).to_f
end
-
- private
- def initialize value, unit_name, exponent = 1.0
+
+ private
+ def initialize value, unit_name, exponent = 1.0, prefix_name = nil
@value = value.to_f
+ @prefix_name = prefix_name
@unit_name = unit_name
@exponent = exponent
end
-
+
def method_missing unit_name, *args, &block
- exponent, unit_name = Alchemist.parse_prefix(unit_name)
- if Conversions[ unit_name ]
+ unit_name, prefix_name = Alchemist.parse_prefix(unit_name)
+
+ @value = @value / Alchemist.unit_prefixes[prefix_name] if not prefix_name.nil?
+ @value = @value * Alchemist.unit_prefixes[@prefix_name] if not @prefix_name.nil?
+
+
+ if !(Conversions[ @unit_name ] & [ :information_storage ]).empty? && !Alchemist.use_si && @value >= 1000.0 && @value.to_i & -@value.to_i != @value
+ @value = @value * Alchemist.unit_prefixes[prefix_name] if not prefix_name.nil?
+ @value = @value / Alchemist.unit_prefixes[@prefix_name] if not @prefix_name.nil?
+ @value = @value / (2 ** (10 * (Math.log(Alchemist.unit_prefixes[prefix_name]) / Math.log(10)) / 3)) if not prefix_name.nil?
+ @value = @value * (2 ** (10 * (Math.log(Alchemist.unit_prefixes[@prefix_name]) / Math.log(10)) / 3)) if not @prefix_name.nil?
+ end
+
+
+ if Conversions[ unit_name ]
types = Conversions[ @unit_name] & Conversions[ unit_name]
if types[0] # assume first type
if(Alchemist.conversion_table[types[0]][unit_name].is_a?(Array))
- NumericConversion.new(Alchemist.conversion_table[types[0]][unit_name][1].call(base(types[0])), unit_name)
+ NumericConversion.new(Alchemist.conversion_table[types[0]][unit_name][1].call(base(types[0])), unit_name, 1.0, prefix_name)
else
- NumericConversion.new(base(types[0]) / (exponent * Alchemist.conversion_table[types[0]][unit_name]), unit_name)
+ NumericConversion.new(base(types[0]) / (Alchemist.conversion_table[types[0]][unit_name]), unit_name, 1.0, prefix_name)
end
else
raise Exception, "Incompatible Types"
end
else
+ # Operations convert to default unit (therefore remove prefix)
+ @prefix_name = nil
+
if args[0] && args[0].is_a?(NumericConversion) && Alchemist.operator_actions[unit_name]
t1 = Conversions[ @unit_name ][0]
t2 = Conversions[ args[0].unit_name ][0]
@@ -566,24 +589,24 @@ def method_missing unit_name, *args, &block
end
end
if unit_name == :/ && args[0].is_a?(NumericConversion)
- raise Exception, "Incompatible Types" unless (Conversions[@unit_name] & Conversions[args[0].unit_name]).length > 0
+ raise Exception, "Incompatible Types" unless (Conversions[@unit_name] & Conversions[args[0].unit_name]).length > 0
end
args.map!{|a| a.is_a?(NumericConversion) ? a.send(@unit_name).to_f / @exponent : a }
@value = @value.send( unit_name, *args, &block )
-
-
+
+
unit_name == :/ ? @value : self
end
end
end
-
+
Conversions = {}
def method_missing unit_name, *args, &block
- exponent, unit_name = Alchemist.parse_prefix(unit_name)
+ unit_name, prefix_name = Alchemist.parse_prefix(unit_name)
Conversions[ unit_name ] || super( unit_name, *args, &block )
- NumericConversion.new self, unit_name, exponent
+ NumericConversion.new self, unit_name, 1.0, prefix_name
end
-
+
def self.register(type, names, value)
names = [names] unless names.is_a?(Array)
value = value.is_a?(NumericConversion) ? value.base(type) : value
@@ -598,23 +621,20 @@ def self.register_operation_conversions type, other_type, operation, converted_t
@@operator_actions[operation] ||= []
@@operator_actions[operation] << [type, other_type, converted_type]
end
-
+
def self.parse_prefix(unit)
@@unit_prefixes.each do |prefix, value|
- if unit.to_s =~ /^#{prefix}.+/ && @@si_units.include?(unit.to_s.gsub(/^#{prefix}/,''))
- if !(Conversions[ unit.to_s.gsub(/^#{prefix}/,'').to_sym ] & [ :information_storage ]).empty? && !@use_si && value >= 1000.0 && value.to_i & -value.to_i != value
- value = 2 ** (10 * (Math.log(value) / Math.log(10)) / 3)
- end
- return [value, unit.to_s.gsub(/^#{prefix}/,'').to_sym]
+ if unit.to_s =~ /^#{prefix}.+/ && @@si_units.include?(unit.to_s.gsub(/^#{prefix}/,''))
+ return [unit.to_s.gsub(/^#{prefix}/,'').to_sym, prefix]
end
end
- [1.0, unit]
+ [unit, nil]
end
def self.is_si_unit?(unit)
return @@si_units.include?(unit.to_s)
end
-
+
@@conversion_table.each do |type, conversions|
conversions.each do |name, value|
Conversions[name] ||= []
@@ -628,3 +648,4 @@ class Numeric
end
require 'alchemist/compound'
+
View
38 locales/de.yml
@@ -2,7 +2,12 @@ de:
units:
time:
second:
- name:
+ name:
+ one: "Sekunde"
+ many: "Sekunden"
+ symbol: "s"
+ s:
+ name:
one: "Sekunde"
many: "Sekunden"
symbol: "s"
@@ -19,11 +24,11 @@ de:
symbol: "V"
temperature:
kelvin:
- name:
+ name:
one: "Kelvin"
many: "Kelvin"
symbol: "K"
- celsius:
+ celsius:
name:
one: "Grad Celsius"
many: "Grad Celsius"
@@ -45,3 +50,30 @@ de:
one: "Pascal"
many: "Pascal"
symbol: "Pa"
+ distance:
+ meter:
+ name:
+ one: "Meter"
+ many: "Meter"
+ symbol: "m"
+ m:
+ name:
+ one: "Meter"
+ many: "Meter"
+ symbol: "m"
+ cm:
+ name:
+ one: "Zentimeter"
+ many: "Zentimeter"
+ symbol: "cm"
+ feet:
+ name:
+ one: "Fuss"
+ many: "Füsse"
+ symbol: "ft"
+ inch:
+ name:
+ one: "Zoll"
+ many: "Zoll"
+ symbol: "in"
+
View
38 locales/en.yml
@@ -2,10 +2,15 @@ en:
units:
time:
second:
- name:
+ name:
one: "second"
many: "seconds"
symbol: "s"
+ s:
+ name:
+ one: "Sekunde"
+ many: "Sekunden"
+ symbol: "s"
power:
watt:
name:
@@ -19,11 +24,11 @@ en:
symbol: "V"
temperature:
kelvin:
- name:
+ name:
one: "kelvin"
many: "kelvins"
symbol: "K"
- celsius:
+ celsius:
name:
one: "degree celsius"
many: "degrees celsius"
@@ -45,3 +50,30 @@ en:
one: "pascal"
many: "pascals"
symbol: "Pa"
+ distance:
+ meter:
+ name:
+ one: "meter"
+ many: "meters"
+ symbol: "m"
+ m:
+ name:
+ one: "meter"
+ many: "meters"
+ symbol: "m"
+ cm:
+ name:
+ one: "centimeter"
+ many: "centimeters"
+ symbol: "cm"
+ feet:
+ name:
+ one: "foot"
+ many: "feet"
+ symbol: "ft"
+ inch:
+ name:
+ one: "inch"
+ many: "inches"
+ symbol: "in"
+
View
74 test/alchemist_test.rb
@@ -12,11 +12,11 @@ def truncate(dec = 0)
end
class AlchemistTest < Test::Unit::TestCase
-
+
def test_equivalence
assert_equal(1.m, 1.meter)
end
-
+
def test_bit_and_bytes
assert_equal( 65.bit.to_f, (1.bit + 8.bytes).to_f )
assert_equal( 0.125.bytes.to_f, 1.bit.to.bytes.to_f )
@@ -31,55 +31,55 @@ def test_bit_and_bytes
assert_in_delta(1.MB.to.b.to_f, 8000000.0, 1e-5)
assert_in_delta(1.MB.to.kB.to_f, 1000.0, 1e-5)
end
-
+
def test_feet_to_miles
assert_equal( 5280.feet, 1.mile.to.feet )
end
-
+
def test_acre_to_yards_squared
assert_in_delta( 4840.square_yards.to_f, 1.acre.to.square_yards.to_f, 1e-5)
end
-
+
def test_gallon_to_liter
assert_in_delta( 3.785411784.L.to_f, 1.gallon.to.L.to_f, 1e-5 )
end
-
+
def test_lb_to_kg
assert_equal( 0.45359237.kg.to_f, 1.lb.to.kg.to_f )
end
-
+
def test_comparison
assert_equal( 5.grams, 0.005.kilograms )
end
-
+
def test_register
Alchemist.register(:distance, [:beard_second, :beard_seconds], 5.angstroms)
- assert_equal( 1.beard_second, 5.angstroms)
+ assert_equal( 1.beard_second, 5.angstroms)
Alchemist.register(:temperature, :yeti, [Proc.new{|t| t + 1}, Proc.new{|t| t - 1}])
- assert_equal( 0.yeti, 1.kelvin)
+ assert_equal( 0.yeti, 1.kelvin)
end
-
+
def test_meters_times_meters
assert_equal(1.meter * 1.meter, 1.square_meter)
end
-
+
def test_meters_times_meters_times_meters
assert_equal(1.meter * 2.meter * 3.meter, 6.cubic_meters)
assert_equal(2.square_meters * 3.meters, 6.cubic_meters)
end
-
+
def test_division
assert_equal(2.meters / 1.meters, 2.0)
end
-
+
def test_temperature
assert_equal(1.fahrenheit, 1.fahrenheit)
assert_in_delta(1.fahrenheit, 1.fahrenheit.to.fahrenheit, 1e-5)
end
-
+
def test_density
assert_equal(25.brix.to_f, 1.1058.sg.to.brix.value.truncate(1))
- assert_equal(25.brix, 13.87.baume.truncate(1))
+ assert_equal(25.brix, 13.87.baume.truncate(1))
assert_equal(25.plato, 25.125.brix)
end
@@ -115,15 +115,43 @@ def test_to_s
end
def test_to_regional_unit
- assert 10.kelvin.to_regional_unit("us").unit_name.to_s == "fahrenheit"
- assert 10.kelvin.to_regional_unit("ch").unit_name.to_s == "celsius"
+ assert 10.kelvin.to_regional_unit("us").unit_name.to_s == "fahrenheit"
+ assert 10.kelvin.to_regional_unit("ch").unit_name.to_s == "celsius"
# also works with locales:
I18n.locale = "en"
- assert 10.kelvin.to_regional_unit("us").unit_name_t == "degrees fahrenheit"
- assert 10.kelvin.to_regional_unit("ch").unit_name_t == "degrees celsius"
+ assert 10.kelvin.to_regional_unit("us").unit_name_t == "degrees fahrenheit"
+ assert 10.kelvin.to_regional_unit("ch").unit_name_t == "degrees celsius"
I18n.locale = "de"
- assert 10.kelvin.to_regional_unit("us").unit_name_t == "Grad Fahrenheit"
- assert 10.kelvin.to_regional_unit("ch").unit_name_t == "Grad Celsius"
+ assert 10.kelvin.to_regional_unit("us").unit_name_t == "Grad Fahrenheit"
+ assert 10.kelvin.to_regional_unit("ch").unit_name_t == "Grad Celsius"
end
-
+
+ def test_units_respect_prefix
+ I18n.locale = "en"
+ assert 1.cm.unit_name_t.to_s == "centimeter"
+ assert 10.cm.unit_name_t.to_s == "centimeters"
+ I18n.locale = "de"
+ assert 1.cm.unit_name_t.to_s == "Zentimeter"
+ assert 10.cm.unit_name_t.to_s == "Zentimeter"
+ end
+
+ def test_prefix_convertion_works
+ I18n.locale = "en"
+ assert 1.m.to.cm.unit_name_t.to_s == "centimeters"
+ assert 1.m.to.cm.to_f == 100.0
+ assert 1.m.to.cm.to.m.to_f == 1.0
+ assert 1.km.to.cm.to.m.to_f == 1000.0
+ end
+
+ def test_prefix_operations_convert_to_default_type
+ I18n.locale = "en"
+ assert (1.m + 1.cm).unit_name_t.to_s == "meters"
+ assert (1.m + 1.cm).to_f == 1.01
+ assert (1.cm + 1.m).unit_name_t.to_s == "meters"
+ assert (1.cm + 1.m).to_f == 1.01
+ assert (1.cm + 1.km).unit_name_t.to_s == "meters"
+ assert (1.cm + 1.km).to_f == 1000.01
+ end
+
end
+