Class: Dimensional::Metric

Inherits:
Numeric
  • Object
show all
Defined in:
lib/dimensional/metric.rb

Overview

A specific physical entity that can be measured. TODO: Add a hierarchy that allows metrics to be built into a taxonomy by domain, like shipping, carpentry or sports

Constant Summary collapse

NUMERIC_REGEXP =

A Measure string is composed of a number followed by a unit separated by optional whitespace. A unit (optional) is composed of a non-digit character followed by zero or more word characters and terminated by some stuff. Scientific notation is not currently supported. TODO: Move this to a locale

/((?=\d|\.\d)\d*(?:\.\d*)?)\s*(\D.*?)?\s*(?=\d|$)/

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(value, unit = self.class.default || self.class.base) ⇒ Metric

Returns a new instance of Metric.

Raises:

  • (ArgumentError)


85
86
87
88
89
# File 'lib/dimensional/metric.rb', line 85

def initialize(value, unit = self.class.default || self.class.base)
  raise ArgumentError, "No default unit set" unless unit
  @unit = unit
  super(value)
end

Class Attribute Details

.baseObject

Returns the value of attribute base.



16
17
18
# File 'lib/dimensional/metric.rb', line 16

def base
  @base
end

.defaultObject

Returns the value of attribute default.



16
17
18
# File 'lib/dimensional/metric.rb', line 16

def default
  @default
end

.dimensionObject

Returns the value of attribute dimension.



16
17
18
# File 'lib/dimensional/metric.rb', line 16

def dimension
  @dimension
end

.universal_systemsObject

Returns the value of attribute universal_systems.



16
17
18
# File 'lib/dimensional/metric.rb', line 16

def universal_systems
  @universal_systems
end

Instance Attribute Details

#unitObject (readonly)

Returns the value of attribute unit.



84
85
86
# File 'lib/dimensional/metric.rb', line 84

def unit
  @unit
end

Class Method Details

.best_fit(target_oom, system) ⇒ Object

Sort units by “best” fit for the desired order of magnitude. Preference values offset OOM differences. There is a bias in favor of negative OOM differences (humans like 6“ more than 0.5ft).



67
68
69
70
71
72
73
74
75
# File 'lib/dimensional/metric.rb', line 67

def best_fit(target_oom, system)
  us = units[system]
  us = us.sort_by do |u|
    oom_delta = Math.log10(u.factor) - target_oom
    avoid_fraction_bonus = (oom_delta > 0.0 ? 0 : 1.5)
    (configuration[u][:preference] - oom_delta.abs) + avoid_fraction_bonus 
  end
  us.last
end

.configurationObject



33
34
35
36
37
# File 'lib/dimensional/metric.rb', line 33

def configuration
  @configuration ||= Hash.new do |h,u|
    h[u] = {:detector => u.detector, :format => u.format, :preference => u.preference}
  end
end

.configure(unit, options = {}) ⇒ Object



39
40
41
42
43
44
45
# File 'lib/dimensional/metric.rb', line 39

def configure(unit, options = {})
  @dimension ||= unit.dimension
  @base ||= unit
  @default ||= unit
  raise "Unit #{unit} is not compatible with dimension #{dimension || '<nil>'}." unless unit.dimension == dimension
  configuration[unit] = {:detector => unit.detector, :format => unit.format, :preference => unit.preference * 1.01}.merge(options)
end

.find_unit(str, locale = Locale.default) ⇒ Object

Find the unit matching the given string, preferring units in the given locale



28
29
30
31
# File 'lib/dimensional/metric.rb', line 28

def find_unit(str, locale = Locale.default)
  us = systems(locale).inject([]){|us, system| us + units[system].sort_by{|u| configuration[u][:preference]}.reverse}
  us.detect{|u| configuration[u][:detector].match(str.to_s)}
end

.load(v) ⇒ Object

Create a new instance with the given value (assumed to be in the base unit) and convert it to the preferred unit.



78
79
80
81
# File 'lib/dimensional/metric.rb', line 78

def load(v)
  raise "No base unit defined" unless base
  new(v, base).preferred
end

.parse(str, locale = Locale.default) ⇒ Object

Parse a string into a Metric instance. Providing a locale will help resolve ambiguities. Unrecognized strings return nil.



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/dimensional/metric.rb', line 49

def parse(str, locale = Locale.default)
  elements = str.to_s.scan(NUMERIC_REGEXP).map do |(v, us)|
    unit = us.nil? ? default : find_unit(us, locale)
    raise ArgumentError, "Unit cannot be determined (#{us})" unless unit
    value = Integer(v) rescue Float(v)
    new(value, unit)
  end
  # Coalesce the elements into a single Measure instance in "expression base" units.
  # The expression base is the first provided unit in an expression like "1 mile 200 feet"
  elements.inject do |t, e|
    raise ArgumentError, "Inconsistent units in compound metric" unless t.unit.system == e.unit.system
    converted_value = e.convert(t.unit)
    new(t + converted_value, t.unit)
  end
end

.systems(locale) ⇒ Object



23
24
25
# File 'lib/dimensional/metric.rb', line 23

def systems(locale)
  locale.systems.dup.unshift(*(universal_systems || [])).uniq
end

.unitsObject

The units of this metric, grouped by system.



19
20
21
# File 'lib/dimensional/metric.rb', line 19

def units
  @units ||= Unit.select{|u| u.dimension == dimension}.inject(Hash.new{|h, s| h[s] = []}){|h, u| h[u.system] << u;h}
end

Instance Method Details

#baseObject

Return a new metric expressed in the base unit



116
117
118
119
# File 'lib/dimensional/metric.rb', line 116

def base
  raise "No base unit defined" unless self.class.base
  convert(self.class.base)
end

#change_system(system) ⇒ Object

Convert into the “most appropriate” unit in the given system. A similar order-of-magnitude for the result is preferred.



98
99
100
101
102
103
# File 'lib/dimensional/metric.rb', line 98

def change_system(system)
  system = System[system] unless system.kind_of?(System)
  target_oom = Math.log10(self.unit.factor)
  bu = self.class.best_fit(target_oom, system)
  convert(bu)
end

#convert(new_unit) ⇒ Object

Convert this dimensional value to a different unit



92
93
94
95
# File 'lib/dimensional/metric.rb', line 92

def convert(new_unit)
  new_value = self * unit.convert(new_unit)
  self.class.new(new_value, new_unit)
end

#inspectObject



149
150
151
# File 'lib/dimensional/metric.rb', line 149

def inspect
  strfmeasure("<%p <%#U>>")
end

#localize(locale = Locale.default) ⇒ Object Also known as: preferred

Convert into the best unit for the given Locale. The first system of the given locale with units is elected the preferred system, and within the preferred system, preference is given to units yielding a metric whose order of magnitude is close to zero.



107
108
109
110
111
112
# File 'lib/dimensional/metric.rb', line 107

def localize(locale = Locale.default)
  preferred_system = self.class.systems(locale).detect{ |s| self.class.units[s].any? }
  target_oom = Math.log10(self.abs) + Math.log10(self.unit.factor) rescue -(1.0/0.0) # Ruby 1.8.6 raises an exception on log10(0) instead of -Infinity
  bu = self.class.best_fit(target_oom, preferred_system)
  convert(bu)
end

#strfmeasure(format) ⇒ Object

Like Date, Time and DateTime, Metric represents both a value and a context. Like those built-in classes, Metric needs this output method to control the context. The format string is identical to that used by Kernel.sprintf with the addition of support for the U specifier:

%U  replace with unit.  This specifier supports the '#' flag to use the unit's name instead of abbreviation
    In addition, this specifier supports the same width and precision modfiers as the '%s' specifier.
    For example: %#10.10U

All other specifiers are applied to the numeric value of the measure. TODO: Support modulo subordinate units with format hash -> => “‘”, 12 => :inch or => “%d#”, 16 => “%doz.”



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/dimensional/metric.rb', line 133

def strfmeasure(format)
  v = if (precision = self.class.configuration[unit][:precision])
    # TODO: This precision could more usefully be converted to "signifigant digits"
    pfactor = 10**(-precision)
    ((self * pfactor).round / pfactor.to_f)
  else
    __getobj__ # We need the native value to prevent infinite recursion if the user specifies the %s specifier.
  end
  format = format.gsub(/%(#)?([\d.\-\*]*)U/) do |s|
    us = ($1) ? unit.name : (unit.abbreviation || unit.name)
    Kernel.sprintf("%#{$2}s", us)
  end
  count = format.scan(/(?:\A|[^%])(%[^% ]*[A-Za-z])/).size
  Kernel.sprintf(format, *Array.new(count, v))
end

#to_sObject



121
122
123
# File 'lib/dimensional/metric.rb', line 121

def to_s
  strfmeasure(self.class.configuration[unit][:format])
end