dimensional 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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