dimensional 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +0 -0
- data/LICENSE +20 -0
- data/README +11 -0
- data/Rakefile +38 -0
- data/lib/dimensional.rb +8 -0
- data/lib/dimensional/configurator.rb +118 -0
- data/lib/dimensional/dimension.rb +56 -0
- data/lib/dimensional/measure.rb +98 -0
- data/lib/dimensional/metric.rb +59 -0
- data/lib/dimensional/system.rb +51 -0
- data/lib/dimensional/unit.rb +90 -0
- data/test/configurator_test.rb +144 -0
- data/test/demo.rb +140 -0
- data/test/dimension_test.rb +55 -0
- data/test/dimensional_test.rb +27 -0
- data/test/measure_test.rb +185 -0
- data/test/metric_test.rb +90 -0
- data/test/system_test.rb +40 -0
- data/test/unit_test.rb +102 -0
- metadata +80 -0
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
|
data/lib/dimensional.rb
ADDED
@@ -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
|