dimensional 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG ADDED
File without changes
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008-2009 Christopher Cyrus Hapgood
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,11 @@
1
+ # This library aims to provide effective parsing of user-supplied measures/metrics, intuitive scaling of those
2
+ # measures into a standard base scale unit and intuitive presentation of values. It does not attempt to mixin
3
+ # unit-oriented methods into Ruby standard classes -to perform operations, particularly across dimensions, you
4
+ # will need to use basic conversion methods provided by this library combined with standard Ruby numerical oper-
5
+ # ations. There is no method_missing magic or large mixin of spiffy unit-like methods here.
6
+
7
+ # The long-term objective for this library is to achieve compliance with the UCUM standard, with additional
8
+ # facilities for simple parsing and formatting of measures according to localized standards, and conversion
9
+ # of measures between units. References:
10
+ # UCUM Website: http://unitsofmeasure.org/
11
+ # UCUM Standard: http://aurora.regenstrief.org/~ucum/ucum.html
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+ require 'rubygems'
6
+
7
+ task :default => [:test]
8
+
9
+ Rake::TestTask.new do |t|
10
+ t.libs << 'test'
11
+ t.test_files = FileList['test/*_test.rb']
12
+ t.verbose = true
13
+ end
14
+ Rake::Task['test'].comment = "Run all tests in test/*_test.rb"
15
+
16
+ spec = Gem::Specification.new do |s|
17
+ s.platform = Gem::Platform::RUBY
18
+ s.name = %q{dimensional}
19
+ s.version = "0.0.2"
20
+ s.required_ruby_version = '>= 1.6.8'
21
+ s.date = %q{2009-10-09}
22
+ s.authors = ["Chris Hapgood"]
23
+ s.email = %q{cch1@hapgoods.com}
24
+ s.summary = %q{Dimensional provides handling for numbers with units.}
25
+ s.homepage = %q{http://cho.hapgoods.com/dimensional}
26
+ s.description = <<-EOF
27
+ Dimensional provides handling for dimensional values (numbers with units). Dimensional values
28
+ can be parsed, stored, converted and formatted for output.
29
+ EOF
30
+ s.files = Dir['lib/**/*.rb'] + Dir['test/**/*.rb']
31
+ s.files += ["README", "CHANGELOG", "LICENSE", "Rakefile"]
32
+ s.test_files = Dir['test/**/*.rb']
33
+ end
34
+
35
+ Rake::GemPackageTask.new(spec) do |pkg|
36
+ pkg.need_zip = true
37
+ pkg.need_tar = false
38
+ end
@@ -0,0 +1,8 @@
1
+ require 'dimensional/dimension'
2
+ require 'dimensional/system'
3
+ require 'dimensional/unit'
4
+ require 'dimensional/measure'
5
+
6
+ module Dimensional
7
+ VERSION = "0.0.2"
8
+ end
@@ -0,0 +1,118 @@
1
+ require 'dimensional/dimension'
2
+ require 'dimensional/system'
3
+ require 'dimensional/unit'
4
+ require 'dimensional/metric'
5
+
6
+ module Dimensional
7
+ # A little DSL for defining units. Beware of Ruby 1.8 binding a block variable
8
+ # to a local variable if they have the same name -it can subtly goof things up.
9
+ class Configurator
10
+ # A simple container for holding a context for definition, parsing and formatting of dimensional data
11
+ Context = Struct.new(:system, :dimension, :unit) do
12
+ def valid?
13
+ true
14
+ end
15
+ end
16
+
17
+ attr_reader :context
18
+
19
+ # Start the configurator with the given context hash and evaluate the supplied block
20
+ def self.start(c_hash = {}, &block)
21
+ new.change_context(c_hash, block)
22
+ end
23
+
24
+ def self.dimension_default_metric_name(d)
25
+ d && d.symbol
26
+ end
27
+
28
+ def initialize(c = Context.new)
29
+ @context = c
30
+ raise "Invalid context" unless context.valid?
31
+ end
32
+
33
+ # Change the context, either for the scope of the supplied block or for this instance
34
+ # NB: The scope in which constants (and classes) are evaluated in instance_eval is surprising in some
35
+ # versions of Ruby. Reference: http://groups.google.com/group/ruby-talk-google/browse_thread/thread/186ac9e618a7312d/a8c5dafa7fcfa3dd?lnk=raot
36
+ def change_context(c_hash, block = nil)
37
+ new_context = context.dup
38
+ c_hash.each{|k, v| new_context[k] = v}
39
+ if block
40
+ self.class.new(new_context).instance_eval &block
41
+ else
42
+ @context = new_context
43
+ self # Allow chaining
44
+ end
45
+ end
46
+
47
+ # Change dimension of the context to the given dimension (or its symbol)
48
+ def dimension(d = nil, &block)
49
+ d = Dimension[d] unless d.kind_of?(Dimension)
50
+ change_context({:dimension => d}, block)
51
+ end
52
+
53
+ # Change system of the context to the given system (or its abbreviation)
54
+ def system(s = nil, &block)
55
+ s = System[s] unless s.kind_of?(System)
56
+ change_context({:system => s}, block)
57
+ end
58
+
59
+ # Register a new base unit
60
+ def base(name, options = {}, &block)
61
+ u = register_unit(name, options)
62
+ change_context({:unit => u}, block)
63
+ end
64
+
65
+ # Register a new derived unit
66
+ def derive(name, factor, options = {}, &block)
67
+ options[:reference_factor] = factor
68
+ options[:reference_unit] = context.unit
69
+ u = register_unit(name, options)
70
+ change_context({:unit => u}, block)
71
+ end
72
+
73
+ # Register an alias for the unit in context
74
+ def alias(name, options = {}, &block)
75
+ derive(name, 1, options, &block)
76
+ end
77
+
78
+ # Register a new unit in the current context that references an arbitrary unit
79
+ def reference(name, reference, factor, options = {}, &block)
80
+ options.merge!(:reference_factor => factor, :reference_unit => reference)
81
+ u = register_unit(name, options)
82
+ change_context({:unit => u}, block)
83
+ end
84
+
85
+ # Register a new unit in the current context that is composed of multiple units
86
+ def combine(name, components, options = {}, &block)
87
+ options.merge!(:reference_factor => 1, :reference_unit => components)
88
+ u = register_unit(name, options)
89
+ change_context({:unit => u}, block)
90
+ end
91
+
92
+ # Register preferred options for the current unit in the named metric
93
+ def prefer(name, options = {})
94
+ m = find_or_register_metric(name, dimension_metric)
95
+ m.prefer(context.unit, options)
96
+ end
97
+
98
+ def to_s
99
+ context.to_s
100
+ end
101
+
102
+ private
103
+ def register_unit(name, options)
104
+ u = Unit.register(name, context.system, context.dimension, options)
105
+ dimension_metric.prefer(u, options)
106
+ u
107
+ end
108
+
109
+ def dimension_metric
110
+ find_or_register_metric(self.class.dimension_default_metric_name(context.dimension), nil)
111
+ end
112
+
113
+ # Register the metric defined by the given key and the current context if it does not already exist.
114
+ def find_or_register_metric(name, parent = nil)
115
+ Metric[name] || Metric.register(name, context.dimension, parent)
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,56 @@
1
+ module Dimensional
2
+ # Represents a dimension that can be used to characterize a physical quantity. With the exception of certain
3
+ # fundamental dimensions, all dimensions are expressed as a set of exponents relative to the fundamentals.
4
+ # Reference: http://en.wikipedia.org/wiki/Dimensional_analysis
5
+ class Dimension
6
+ @registry = Hash.new
7
+ @symbol_registry = Hash.new # Not a Ruby Symbol but a (typically one-character) natural language symbol
8
+
9
+ def self.register(*args)
10
+ d = new(*args)
11
+ raise "Dimension #{d.name} already exists" if @registry[d.name]
12
+ raise "Dimension #{d.name}'s symbol already exists" if @symbol_registry[d.symbol]
13
+ @registry[d.name.to_sym] = d
14
+ @symbol_registry[d.symbol.to_sym] = d
15
+ const_set(d.symbol.to_s, d) rescue nil # Not all symbols strings are valid constant names
16
+ d
17
+ end
18
+
19
+ # Lookup the dimension by name or symbol
20
+ def self.[](sym)
21
+ return nil unless sym = sym && sym.to_sym
22
+ @registry[sym] || @symbol_registry[sym]
23
+ end
24
+
25
+ # Purge all dimensions from storage.
26
+ def self.reset!
27
+ constants.each {|d| remove_const(d)}
28
+ @registry.clear
29
+ @symbol_registry.clear
30
+ end
31
+
32
+ attr_reader :exponents, :name, :symbol
33
+
34
+ def initialize(name, symbol = nil, exponents = {})
35
+ exponents.each_pair do |k,v|
36
+ raise "Invalid fundamental dimension #{k}" unless k.fundamental?
37
+ raise "Invalid exponent #{v}" unless v.kind_of?(Integer) # Can't this really be any Rational?
38
+ end
39
+ @exponents = Hash.new(0).merge(exponents)
40
+ @name = name.to_s
41
+ @symbol = symbol.nil? ? name.to_s.slice(0, 1).upcase : symbol.to_s
42
+ end
43
+
44
+ def fundamental?
45
+ exponents.empty?
46
+ end
47
+
48
+ def to_s
49
+ name rescue super
50
+ end
51
+
52
+ def to_sym
53
+ symbol.to_sym
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,98 @@
1
+ require 'dimensional/unit'
2
+ require 'dimensional/metric'
3
+ require 'delegate'
4
+
5
+ module Dimensional
6
+ # A numeric-like class used to represent the measure of a physical quantity. An instance of this
7
+ # class represents both the unit and the numerical value of a measure. In turn, the (scale)
8
+ # unit implies a dimension of the measure. Instances of this class are immutable (value objects)
9
+ # Reference: http://en.wikipedia.org/wiki/Physical_quantity
10
+ class Measure < DelegateClass(Numeric)
11
+ # A Measure string is composed of a number followed by a unit separated by optional whitespace.
12
+ # A unit (optional) is composed of a non-digit character followed by zero or more word characters and terminated by some stuff.
13
+ # Scientific notation is not currently supported.
14
+ NUMERIC_REGEXP = /((?=\d|\.\d)\d*(?:\.\d*)?)\s*(\D\w*?)?(?=\b|\d|\W|$)/
15
+
16
+ # Parse a string into a Measure instance. The metric and system parameters may be keys for looking up the associated values.
17
+ # Unrecognized strings return nil.
18
+ def self.parse(str, metric, system = nil)
19
+ metric = Metric[metric] unless metric.kind_of?(Metric)
20
+ system = System[system] unless system.kind_of?(System)
21
+ raise "Metric not specified" unless metric
22
+ units = metric.units
23
+ elements = str.to_s.scan(NUMERIC_REGEXP).map do |(v, us)|
24
+ units = units.select{|u| system == u.system} if system
25
+ unit = us.nil? ? units.first : units.detect{|u| u.match(us.to_s)}
26
+ raise ArgumentError, "Unit cannot be determined (#{us})" unless unit
27
+ system = unit.system
28
+ value = unit.dimension.nil? ? v.to_i : v.to_f
29
+ new(value, unit, metric)
30
+ end
31
+ # Coalesce the elements into a single Measure instance in "expression base" units.
32
+ # The expression base is the first provided unit in an expression like "1 mile 200 feet"
33
+ elements.inject do |t, e|
34
+ converted_value = e.convert(t.unit)
35
+ new(t + converted_value, t.unit, metric)
36
+ end
37
+ end
38
+
39
+ attr_reader :unit, :metric
40
+
41
+ def initialize(value, unit, metric = nil)
42
+ @unit = unit
43
+ metric = Metric[metric] if metric.kind_of?(Symbol)
44
+ @metric = metric || Metric[unit.dimension]
45
+ super(value)
46
+ end
47
+
48
+ # Convert this dimensional value to a different unit
49
+ def convert(new_unit)
50
+ new_value = self * unit.convert(new_unit)
51
+ self.class.new(new_value, new_unit, metric)
52
+ end
53
+
54
+ # Return a new dimensional value expressed in the base unit
55
+ # DEPRECATE: this method has dubious semantics for composed units as there may be no defined unit with
56
+ # a matching dimension vector.
57
+ def base
58
+ raise "Composed units cannot be converted to a base unit" if unit.reference_unit.kind_of?(Enumerable)
59
+ convert(unit.base)
60
+ end
61
+
62
+ def native
63
+ metric.dimension ? to_f : to_i
64
+ end
65
+
66
+ def to_s
67
+ strfmeasure(metric.preferences(unit)[:format]) rescue super
68
+ end
69
+
70
+ # Like Date, Time and DateTime, Measure represents both a value and a context. Like those built-in classes,
71
+ # Measure needs this output method to control the context. The format string is identical to that used by
72
+ # Kernel.sprintf with the addition of support for the U specifier:
73
+ # %U replace with unit. This specifier supports the '#' flag to use the unit's name instead of abbreviation
74
+ # In addition, this specifier supports the same width and precision modfiers as the '%s' specifier.
75
+ # For example: %#10.10U
76
+ # All other specifiers are applied to the numeric value of the measure.
77
+ # TODO: Support positional arguments (n$).
78
+ # TODO: Support modulo subordinate units with format hash -> {1 => "'", 12 => :inch} or {1 => "%d#", 16 => "%doz."}
79
+ def strfmeasure(format = nil, *args)
80
+ v = if precision = metric.preferences(unit)[:precision]
81
+ pfactor = 10**(-precision)
82
+ ((self * pfactor).round / pfactor.to_f).to_s
83
+ else
84
+ native
85
+ end
86
+ format = format || unit.format
87
+ format.gsub!(/%(#)?([\d.\-\*]*)U/) do |s|
88
+ arg = ($1) ? unit.name : unit.abbreviation
89
+ Kernel.sprintf("%#{$2}s", arg)
90
+ end
91
+ Kernel.sprintf(format, v, *args)
92
+ end
93
+
94
+ def inspect
95
+ "#{super} : #{unit.inspect}"
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,59 @@
1
+ require 'dimensional/dimension'
2
+
3
+ module Dimensional
4
+ # A specific physical entity that can be measured.
5
+ # TODO: Add a hierarchy that allows metrics to be grouped by domain, like shipping, carpentry or sports
6
+ class Metric
7
+ include Enumerable
8
+
9
+ @registry = Hash.new
10
+
11
+ attr_reader :name, :dimension, :parent
12
+
13
+ def self.register(*args)
14
+ m = new(*args)
15
+ raise "Metric #{m} has already been registered." if @registry[m.name]
16
+ @registry[m.name && m.name.to_sym] = m
17
+ m
18
+ end
19
+
20
+ # Lookup the metric by key. The default metric for a dimension is keyed by the dimension's symbol
21
+ def self.[](sym)
22
+ @registry[sym && sym.to_sym]
23
+ end
24
+
25
+ def self.reset!
26
+ @registry.clear
27
+ end
28
+
29
+ def initialize(name, dimension, parent = nil)
30
+ @name = name && name.to_s
31
+ @dimension = dimension
32
+ @units = Hash.new{|h,k| h[k] = {}}
33
+ @parent = parent
34
+ end
35
+
36
+ def prefer(unit, options = {})
37
+ raise "Unit #{unit} is not compatible with dimension #{dimension || '<nil>'}." unless unit.dimension == dimension
38
+ @units[unit] = options
39
+ end
40
+
41
+ def units
42
+ baseline = parent ? parent.units : @units.keys
43
+ baseline.sort{|a,b| (@units.has_key?(b) ? 1 : 0) <=> (@units.has_key?(a) ? 1 : 0)}
44
+ end
45
+
46
+ def preferences(u)
47
+ baseline = parent ? parent.preferences(u) : {}
48
+ baseline.merge(@units[u])
49
+ end
50
+
51
+ def each
52
+ units.each
53
+ end
54
+
55
+ def to_s
56
+ name || super
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,51 @@
1
+ module Dimensional
2
+ # Represents a set of units for comprehensive measurement of physical quantities.
3
+ class System
4
+ @registry = Hash.new
5
+ @abbreviation_registry = Hash.new
6
+
7
+ def self.register(*args)
8
+ s = new(*args)
9
+ raise "System #{s.name} already exists" if @registry[s.name]
10
+ raise "System #{s.name}'s abbreviation already exists" if @abbreviation_registry[s.abbreviation]
11
+ @registry[s.name.to_sym] = s
12
+ @abbreviation_registry[s.abbreviation.to_sym] = s if s.abbreviation
13
+ const_set(s.abbreviation, s) rescue nil # Not all symbols strings are valid constant names
14
+ s
15
+ end
16
+
17
+ # Lookup the system by name or abbreviation
18
+ def self.[](sym)
19
+ return nil unless sym = sym && sym.to_sym
20
+ @abbreviation_registry[sym] || @registry[sym]
21
+ end
22
+
23
+ # Systems are expected to be declared 'universally' and always be in context so we only dump the name
24
+ def self._load(str)
25
+ @registry[str.to_sym]
26
+ end
27
+
28
+ # Purge all systems from storage.
29
+ def self.reset!
30
+ constants.each {|d| remove_const(d)}
31
+ @registry.clear
32
+ @abbreviation_registry.clear
33
+ end
34
+
35
+ attr_reader :name, :abbreviation
36
+
37
+ def initialize(name, abbreviation = nil)
38
+ @name = name.to_s.freeze
39
+ @abbreviation = abbreviation && abbreviation.to_s
40
+ end
41
+
42
+ # Systems are expected to be declared 'universally' and always be in context so we only dump the name
43
+ def _dump(depth)
44
+ name
45
+ end
46
+
47
+ def to_s
48
+ name rescue super
49
+ end
50
+ end
51
+ end