dimensional 0.0.6 → 0.1.1
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 +2 -0
- data/Rakefile +1 -1
- data/lib/dimensional.rb +1 -1
- data/lib/dimensional/configuration.rb +90 -17
- data/lib/dimensional/configurator.rb +7 -34
- data/lib/dimensional/dimension.rb +24 -8
- data/lib/dimensional/metric.rb +115 -41
- data/lib/dimensional/system.rb +3 -2
- data/lib/dimensional/unit.rb +55 -17
- data/lib/dimensional/version.rb +1 -1
- data/test/configuration_test.rb +217 -0
- data/test/configurator_test.rb +25 -30
- data/test/demo.rb +240 -51
- data/test/dimension_test.rb +28 -3
- data/test/dimensional_test.rb +47 -2
- data/test/metric_test.rb +250 -78
- data/test/unit_test.rb +83 -19
- metadata +4 -5
- data/lib/dimensional/measure.rb +0 -122
- data/test/measure_test.rb +0 -226
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dimensional
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Hapgood
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date:
|
12
|
+
date: 2010-02-05 00:00:00 -05:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -25,17 +25,16 @@ files:
|
|
25
25
|
- lib/dimensional/configuration.rb
|
26
26
|
- lib/dimensional/configurator.rb
|
27
27
|
- lib/dimensional/dimension.rb
|
28
|
-
- lib/dimensional/measure.rb
|
29
28
|
- lib/dimensional/metric.rb
|
30
29
|
- lib/dimensional/system.rb
|
31
30
|
- lib/dimensional/unit.rb
|
32
31
|
- lib/dimensional/version.rb
|
33
32
|
- lib/dimensional.rb
|
33
|
+
- test/configuration_test.rb
|
34
34
|
- test/configurator_test.rb
|
35
35
|
- test/demo.rb
|
36
36
|
- test/dimension_test.rb
|
37
37
|
- test/dimensional_test.rb
|
38
|
-
- test/measure_test.rb
|
39
38
|
- test/metric_test.rb
|
40
39
|
- test/system_test.rb
|
41
40
|
- test/unit_test.rb
|
@@ -72,11 +71,11 @@ signing_key:
|
|
72
71
|
specification_version: 3
|
73
72
|
summary: Dimensional provides handling for numbers with units.
|
74
73
|
test_files:
|
74
|
+
- test/configuration_test.rb
|
75
75
|
- test/configurator_test.rb
|
76
76
|
- test/demo.rb
|
77
77
|
- test/dimension_test.rb
|
78
78
|
- test/dimensional_test.rb
|
79
|
-
- test/measure_test.rb
|
80
79
|
- test/metric_test.rb
|
81
80
|
- test/system_test.rb
|
82
81
|
- test/unit_test.rb
|
data/lib/dimensional/measure.rb
DELETED
@@ -1,122 +0,0 @@
|
|
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| metric.preferences(u)[:detector].match(us)}
|
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
|
-
# Convert this measure to the most appropriate unit in the given system
|
55
|
-
# A heuristic approach is used that considers the resulting measure's order-of-magnitude (similar
|
56
|
-
# is good) and membership in the preferred units of the measure's metric (membership is good).
|
57
|
-
def change_system(system, fallback = false)
|
58
|
-
system = System[system] unless system.kind_of?(System)
|
59
|
-
units = metric.units.select{|u| system == u.system}
|
60
|
-
if units.empty?
|
61
|
-
if fallback
|
62
|
-
units = metric.units
|
63
|
-
else
|
64
|
-
raise "No suitable units available in #{system}"
|
65
|
-
end
|
66
|
-
end
|
67
|
-
target_oom = Math.log10(self.unit.factor)
|
68
|
-
units = units.sort_by do |u|
|
69
|
-
oom_delta = (Math.log10(u.factor) - target_oom).abs # == Math.log10(self.unit.factor / u.factor)
|
70
|
-
magnitude_fit = Math.exp(-0.20 * oom_delta) # decay function
|
71
|
-
0.75 * magnitude_fit + 0.25 * metric.preference(u)
|
72
|
-
end
|
73
|
-
u = units.last
|
74
|
-
convert(u)
|
75
|
-
end
|
76
|
-
|
77
|
-
# Return a new dimensional value expressed in the base unit
|
78
|
-
# DEPRECATE: this method has dubious semantics for composed units as there may be no defined unit with
|
79
|
-
# a matching dimension vector.
|
80
|
-
def base
|
81
|
-
raise "Composed units cannot be converted to a base unit" if unit.reference_unit.kind_of?(Enumerable)
|
82
|
-
convert(unit.base)
|
83
|
-
end
|
84
|
-
|
85
|
-
def to_s
|
86
|
-
strfmeasure(metric.preferences(unit)[:format] || "%s%U")
|
87
|
-
end
|
88
|
-
|
89
|
-
# Like Date, Time and DateTime, Measure represents both a value and a context. Like those built-in classes,
|
90
|
-
# Measure needs this output method to control the context. The format string is identical to that used by
|
91
|
-
# Kernel.sprintf with the addition of support for the U specifier:
|
92
|
-
# %U replace with unit. This specifier supports the '#' flag to use the unit's name instead of abbreviation
|
93
|
-
# In addition, this specifier supports the same width and precision modfiers as the '%s' specifier.
|
94
|
-
# For example: %#10.10U
|
95
|
-
# All other specifiers are applied to the numeric value of the measure.
|
96
|
-
# TODO: Support modulo subordinate units with format hash -> {1 => "'", 12 => :inch} or {1 => "%d#", 16 => "%doz."}
|
97
|
-
def strfmeasure(format)
|
98
|
-
# We need the native value to prevent infinite recursion if the user specifies the %s specifier.
|
99
|
-
v = if precision = metric.preferences(unit)[:precision]
|
100
|
-
pfactor = 10**(-precision)
|
101
|
-
((self * pfactor).round / pfactor.to_f).to_s
|
102
|
-
else
|
103
|
-
native
|
104
|
-
end
|
105
|
-
format = format.gsub(/%(#)?([\d.\-\*]*)U/) do |s|
|
106
|
-
us = ($1) ? unit.name : (unit.abbreviation || unit.name)
|
107
|
-
Kernel.sprintf("%#{$2}s", us)
|
108
|
-
end
|
109
|
-
count = format.scan(/(?:\A|[^%])(%[^% ]*[A-Za-z])/).size
|
110
|
-
Kernel.sprintf(format, *Array.new(count, v))
|
111
|
-
end
|
112
|
-
|
113
|
-
def inspect
|
114
|
-
strfmeasure("<%p <%#U>>")
|
115
|
-
end
|
116
|
-
|
117
|
-
private
|
118
|
-
def native
|
119
|
-
metric.dimension ? to_f : to_i
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
data/test/measure_test.rb
DELETED
@@ -1,226 +0,0 @@
|
|
1
|
-
require 'test/unit'
|
2
|
-
require 'dimensional/measure'
|
3
|
-
require 'dimensional/configurator'
|
4
|
-
require 'rational'
|
5
|
-
|
6
|
-
class MeasureTest < Test::Unit::TestCase
|
7
|
-
include Dimensional
|
8
|
-
|
9
|
-
def setup
|
10
|
-
System.register('International System of Units', 'SI')
|
11
|
-
System.register('United States Customary Units', 'US')
|
12
|
-
System.register('British Admiralty', 'BA')
|
13
|
-
|
14
|
-
Dimension.register('Length')
|
15
|
-
Dimension.register('Mass')
|
16
|
-
|
17
|
-
Metric.register('length', :L)
|
18
|
-
|
19
|
-
Configurator.start do
|
20
|
-
dimension(:L) do
|
21
|
-
system(:SI) do
|
22
|
-
base('meter', 'm', :detector => /\A(meters?|m)\Z/) do
|
23
|
-
prefer(:length_over_all, :precision => 0.01)
|
24
|
-
derive('centimeter', 'cm', 1e-2, :detector => /\A(centimeters?|cm)\Z/)
|
25
|
-
derive('kilometer', 'km', 1e3, :detector => /\A(kilometers?|km)\Z/)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
system(:US) do # As of 1 July 1959 (http://en.wikipedia.org/wiki/United_States_customary_units#Units_of_length)
|
29
|
-
reference('yard', 'yd', Unit[:L, :SI, 'meter'], 0.9144, :detector => /\A(yards?|yds?)\Z/) do
|
30
|
-
derive('foot', 'ft', Rational(1,3), :detector => /\A(foot|feet|ft|')\Z/, :format => "%p'") do
|
31
|
-
prefer(:length_over_all, :precision => Rational(1, 12))
|
32
|
-
derive('inch', 'in', Rational(1,12), :detector => /\A(inch|inches|in|")\Z/, :format => "%p\"")
|
33
|
-
end
|
34
|
-
derive('furlong', nil, 220, :detector => /\A(furlongs?)\Z/) do
|
35
|
-
derive('mile', 'mi', 8, :detector => /\Amiles?\Z/)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
system(:BA) do
|
40
|
-
base('mile', 'nm', :detector => /\A(miles?|nm|nmi)\Z/) do
|
41
|
-
prefer(:distance, :precision => -2)
|
42
|
-
derive('cable', nil, Rational(1,10), :detector => /\A(cables?|cbls?)\Z/) do
|
43
|
-
derive('fathom', 'fm', Rational(1,10), :detector => /\A(fathoms?|fms?)\Z/) do
|
44
|
-
derive('yard', 'yd', Rational(1,6), :detector => /\A(yards?|yds?)\Z/) do
|
45
|
-
derive('foot', 'ft', Rational(1,3), :detector => /\A(foot|feet|ft|')\Z/) do
|
46
|
-
prefer(:length_over_all, :precision => Rational(1, 12))
|
47
|
-
derive('inch', 'in', Rational(1,12), :detector => /\A(inch|inches|in|")\Z/)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
dimension(:M) do
|
56
|
-
system(:SI) do
|
57
|
-
base('kilogram', 'kg', :detector => /\A(kilograms?|kg)\Z/) do
|
58
|
-
derive('tonne', 't', 1000, :detector => /\A(tonnes?)\Z/) # metric ton
|
59
|
-
derive('gram', 'g', Rational(1, 1000), :detector => /\A(grams?|g)\Z/)
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
# Dimensionless Units
|
64
|
-
base('each', 'ea') do
|
65
|
-
derive('dozen', 'dz', 12)
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def teardown
|
71
|
-
Dimension.reset!
|
72
|
-
System.reset!
|
73
|
-
Unit.reset!
|
74
|
-
Metric.reset!
|
75
|
-
end
|
76
|
-
|
77
|
-
def test_create_new_measure
|
78
|
-
u = Unit[:L, :BA, 'mile']
|
79
|
-
assert m = Measure.new(3000, u, :length)
|
80
|
-
assert_equal 3000, m
|
81
|
-
assert_equal u, m.unit
|
82
|
-
assert_same Metric[:length], m.metric
|
83
|
-
end
|
84
|
-
|
85
|
-
def test_to_f
|
86
|
-
d = Measure.parse("1.85m", :L, :SI)
|
87
|
-
assert_instance_of Float, d.to_f
|
88
|
-
end
|
89
|
-
|
90
|
-
def test_to_i
|
91
|
-
d = Measure.parse("1 each", Metric[nil])
|
92
|
-
assert_instance_of Fixnum, d.to_i
|
93
|
-
end
|
94
|
-
|
95
|
-
def test_convert
|
96
|
-
old_unit = Unit[:L, :BA, 'cable']
|
97
|
-
new_unit = Unit[:L, :BA, 'fathom']
|
98
|
-
new = Measure.new(1, old_unit).convert(new_unit)
|
99
|
-
assert_in_delta(10, new, 0.000001)
|
100
|
-
assert_same new_unit, new.unit
|
101
|
-
end
|
102
|
-
|
103
|
-
def test_do_identity_conversion
|
104
|
-
old_unit = Unit[:L, :BA, 'cable']
|
105
|
-
new_unit = old_unit
|
106
|
-
old_value = Measure.new(12, old_unit)
|
107
|
-
new_value = old_value.convert(new_unit)
|
108
|
-
assert_equal old_value, new_value
|
109
|
-
end
|
110
|
-
|
111
|
-
# These system-conversion tests rely on very specific constants in the heuristics of #change_system
|
112
|
-
def test_change_system_yd
|
113
|
-
u0 = Unit[:L, :US, 'yd']
|
114
|
-
m0 = Measure.new(1, u0, :length_over_all)
|
115
|
-
u1 = Unit[:L, :SI, 'm']
|
116
|
-
assert m1 = m0.change_system(:SI)
|
117
|
-
assert_same u1, m1.unit
|
118
|
-
end
|
119
|
-
|
120
|
-
def test_change_system_with_metric_override
|
121
|
-
u0 = Unit[:L, :US, 'in']
|
122
|
-
m0 = Measure.new(1, u0, :length_over_all)
|
123
|
-
u1 = Unit[:L, :SI, 'm']
|
124
|
-
assert m1 = m0.change_system(:SI)
|
125
|
-
assert_same u1, m1.unit
|
126
|
-
end
|
127
|
-
|
128
|
-
def test_change_system_with_oom_dominance
|
129
|
-
u0 = Unit[:L, :US, 'in']
|
130
|
-
m0 = Measure.new(1, u0, :L)
|
131
|
-
u1 = Unit[:L, :SI, 'cm']
|
132
|
-
assert m1 = m0.change_system(:SI)
|
133
|
-
assert_same u1, m1.unit
|
134
|
-
end
|
135
|
-
|
136
|
-
def test_change_system_ft
|
137
|
-
u0 = Unit[:L, :US, 'ft']
|
138
|
-
m0 = Measure.new(1, u0, :length_over_all)
|
139
|
-
u1 = Unit[:L, :SI, 'm']
|
140
|
-
assert m1 = m0.change_system(:SI)
|
141
|
-
assert_same u1, m1.unit
|
142
|
-
end
|
143
|
-
|
144
|
-
def test_change_system_mile
|
145
|
-
u0 = Unit[:L, :US, 'mile']
|
146
|
-
m0 = Measure.new(1, u0, :length_over_all)
|
147
|
-
u1 = Unit[:L, :SI, 'km']
|
148
|
-
assert m1 = m0.change_system(:SI)
|
149
|
-
assert_same u1, m1.unit
|
150
|
-
end
|
151
|
-
|
152
|
-
def test_return_base
|
153
|
-
u = Unit[:L, :BA, 'fathom']
|
154
|
-
b = Measure.new(1, u).base
|
155
|
-
assert_in_delta(1e-2, b, 0.000001)
|
156
|
-
assert_same u.base, b.unit
|
157
|
-
end
|
158
|
-
|
159
|
-
def test_parse
|
160
|
-
assert m = Measure.parse("15'", :L, :BA)
|
161
|
-
assert_same Unit[:L, :BA, 'foot'], m.unit
|
162
|
-
assert_equal 15, m
|
163
|
-
end
|
164
|
-
|
165
|
-
def test_parse_with_whitespace
|
166
|
-
m = Measure.parse("15 feet", :L, :BA)
|
167
|
-
assert_same Unit[:L, :BA, 'foot'], m.unit
|
168
|
-
assert_equal 15, m
|
169
|
-
end
|
170
|
-
|
171
|
-
def test_parse_compound
|
172
|
-
d = Measure.parse("15'11\"", :L, :US)
|
173
|
-
assert_in_delta(15 + Rational(11, 12), d, 0.000001)
|
174
|
-
end
|
175
|
-
|
176
|
-
def test_parse_compound_with_whitespace
|
177
|
-
d = Measure.parse("1 foot 11 inches", :L, :US)
|
178
|
-
assert_same d.unit, Unit[:L, :US, 'foot']
|
179
|
-
assert_in_delta(1 + Rational(11, 12).to_f, d, 0.000001)
|
180
|
-
end
|
181
|
-
|
182
|
-
def test_raise_on_parse_of_mixed_compound
|
183
|
-
assert_raises ArgumentError do
|
184
|
-
Measure.parse("1 foot 11cm", :L)
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
def test_parse_with_default_unit
|
189
|
-
metric = Metric[:L]
|
190
|
-
du = metric.units.first
|
191
|
-
assert_instance_of Measure, m = Measure.parse("10", :L)
|
192
|
-
assert_equal du, m.unit
|
193
|
-
end
|
194
|
-
|
195
|
-
def test_parse_dimensionless_units
|
196
|
-
assert m = Measure.parse('2 dozen', nil)
|
197
|
-
assert_instance_of Measure, m
|
198
|
-
assert_equal 2, m
|
199
|
-
assert_equal Unit[nil, nil, 'dozen'], m.unit
|
200
|
-
assert_equal 12, m.unit.factor
|
201
|
-
assert_nil m.unit.dimension
|
202
|
-
end
|
203
|
-
|
204
|
-
def test_stringify_with_abbreviation
|
205
|
-
assert_equal "1.85nm", Measure.parse('1.85 miles', :L, :BA).to_s
|
206
|
-
end
|
207
|
-
|
208
|
-
def test_parse_gibberish_as_nil
|
209
|
-
assert_nil Measure.parse("gibberish", :L)
|
210
|
-
end
|
211
|
-
|
212
|
-
def test_format_output
|
213
|
-
m = Measure.parse("15'3\"", :L, :BA)
|
214
|
-
assert_equal "15.25 (ft)", m.strfmeasure("%4.2f (%U)")
|
215
|
-
end
|
216
|
-
|
217
|
-
def test_format_output_with_multiple_substitutions
|
218
|
-
m = Measure.parse("15'4\"", :L, :BA)
|
219
|
-
assert_equal "15.33 (ft)\t%\t<15.3333333ft>", m.strfmeasure("%4.2f (%U)\t%%\t<%.10s%U>")
|
220
|
-
end
|
221
|
-
|
222
|
-
def test_precision_recognition
|
223
|
-
assert_equal "1.8600nm", Measure.parse('1.8565454 miles', :distance, :BA).strfmeasure("%.4f%U")
|
224
|
-
assert_equal "1.86", Measure.parse('1.8565454 miles', :distance, :BA).strfmeasure("%s")
|
225
|
-
end
|
226
|
-
end
|