quantify 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,217 @@
1
+ #! usr/bin/ruby
2
+
3
+ module Quantify
4
+ module Unit
5
+
6
+ extend ExtendedMethods
7
+
8
+ # The Unit module contains functionality for defining and handling
9
+ # representations of physical units.
10
+ #
11
+ # All units are defined using the Unit::SI and Unit::NonSI classes, both of
12
+ # which inherit from Unit::Base.
13
+ #
14
+ # New units can be defined to represent whatever is required. However a
15
+ # system of known units is stored in the Unit module instance variable
16
+ # @units, accessible using Unit.units. These known units can be configured
17
+ # to represent which ever units are required. The Unit module will handle
18
+ # any combinations of units and prefixes according to the known units and
19
+ # prefixes specified in config.rb. New units can be defined (with or without
20
+ # prefixes) at any time and either used in place or loaded into the known
21
+ # system.
22
+
23
+ # Make the @units instance array readable
24
+ class << self
25
+ attr_reader :units
26
+ end
27
+
28
+ def self.configure &block
29
+ self.class_eval &block if block
30
+ end
31
+
32
+ # Instance variable containing system of known units
33
+ @units = []
34
+
35
+ # Load a new unit into they system of known units
36
+ def self.load(unit)
37
+ @units << unit if unit.is_a? Unit::Base
38
+ end
39
+
40
+ # Remove a unit from the system of known units
41
+ def self.unload(*unloaded_units)
42
+ [unloaded_units].flatten.each do |unloaded_unit|
43
+ unloaded_unit = Unit.for(unloaded_unit)
44
+ @units.delete_if { |unit| unit.label == unloaded_unit.label }
45
+ end
46
+ end
47
+
48
+ # Returns an instance of the class Quantity which represents the ratio of two
49
+ # units. For example, the ratio of miles to kilometers is 1.609355, or there
50
+ # are 1.609355 km in 1 mile.
51
+ #
52
+ # ratio = Unit.ratio :km, :mi #=> <Quantify::Quantity:0xj9ab878a7>
53
+ #
54
+ # ratio.to_s :name #=> "1.609344 kilometres per mile"
55
+ #
56
+ # In other words the quantity represents the definition of one unit in terms
57
+ # of the other.
58
+ #
59
+ def self.ratio(unit,other_unit)
60
+ unit = Unit.for unit
61
+ other_unit = Unit.for other_unit
62
+ unless unit.is_alternative_for? other_unit
63
+ raise InvalidUnitError, "Units do not represent the same physical quantity"
64
+ end
65
+ new_unit = (unit / other_unit)
66
+ value = 1/new_unit.factor
67
+ Quantity.new(value, new_unit)
68
+ end
69
+
70
+ # Retrieve an object representing the specified unit.
71
+ #
72
+ # Argument can be the unit name, symbol or JScience label and provided as
73
+ # a string or a symbol, e.g.
74
+ #
75
+ # Unit.for :metre
76
+ #
77
+ # Unit.for 'kilogram'
78
+ #
79
+ # This can be shortened to, for example, Unit.metre by virtue of the
80
+ # #method_missing method (see below)
81
+ #
82
+ # This method will recognise valid combinations of known units and prefixes,
83
+ # irrespective of whether the prefixed unit has been initialized into the
84
+ # system of known units in it's own right. For example,
85
+ #
86
+ # Unit.centimetre ... or, alternatively ... Unit.cm
87
+ #
88
+ # will return a Unit::SI object with attributes representing a centimetre
89
+ # based on the initialized Unit for :metre and Prefix :centi.
90
+ #
91
+ def self.for(name_symbol_label_or_object)
92
+ return name_symbol_label_or_object.clone if name_symbol_label_or_object.is_a? Unit::Base
93
+ return nil if name_symbol_label_or_object.nil? or
94
+ ( name_symbol_label_or_object.is_a?(String) and name_symbol_label_or_object.empty? )
95
+ name_symbol_or_label = name_symbol_label_or_object
96
+ unless name_symbol_or_label.is_a? String or name_symbol_or_label.is_a? Symbol
97
+ raise InvalidArgumentError, "Argument must be a Symbol or String"
98
+ end
99
+ if unit = Unit.match(name_symbol_or_label)
100
+ return unit
101
+ end
102
+ if unit = Unit.parse(name_symbol_or_label)
103
+ return unit
104
+ end
105
+ rescue InvalidUnitError
106
+ return nil
107
+ end
108
+
109
+ # Parse complex strings into unit.
110
+ #
111
+ def self.parse(string)
112
+ string = string.standardize
113
+ if string.scan(/(\/|per)/).size > 1
114
+ raise InvalidArgumentError, "Malformed unit: multiple uses of '/' or 'per'"
115
+ end
116
+
117
+ units = []
118
+ numerator, per, denominator = string.split(/(\/|per)/)
119
+ units += Unit.parse_numerator_units(numerator)
120
+ units += Unit.parse_denominator_units(denominator) unless denominator.nil?
121
+ if units.size == 1 and units.first.index == 1
122
+ return units.first.unit
123
+ else
124
+ return Unit::Compound.new(*units)
125
+ end
126
+ end
127
+
128
+ def self.match(name_symbol_or_label)
129
+ return name_symbol_or_label.clone if name_symbol_or_label.is_a? Unit::Base
130
+ Unit.match_known_unit_or_prefixed_variant(:label, name_symbol_or_label) or
131
+ Unit.match_known_unit_or_prefixed_variant(:name, name_symbol_or_label) or
132
+ Unit.match_known_unit_or_prefixed_variant(:symbol, name_symbol_or_label)
133
+ end
134
+
135
+ protected
136
+
137
+ def self.match_known_unit_or_prefixed_variant(attribute, string_or_symbol)
138
+ Unit.match_known_unit(attribute, string_or_symbol) or
139
+ Unit.match_prefixed_variant(attribute, string_or_symbol)
140
+ end
141
+
142
+ def self.match_known_unit(attribute, string_or_symbol)
143
+ string_or_symbol = Unit.format_unit_attribute(attribute, string_or_symbol)
144
+ unit = @units.find { |unit| unit.send(attribute) == string_or_symbol }
145
+ return unit.clone rescue nil
146
+ end
147
+
148
+ def self.match_prefixed_variant(attribute, string_or_symbol)
149
+ string_or_symbol = Unit.format_unit_attribute(attribute, string_or_symbol)
150
+ if string_or_symbol =~ /\A(#{Unit::Prefix.si_prefixes.map(&attribute).join("|")})(#{Unit.si_non_prefixed_units.map(&attribute).join("|")})\z/ or
151
+ string_or_symbol =~ /\A(#{Unit::Prefix.non_si_prefixes.map(&attribute).join("|")})(#{Unit.non_si_non_prefixed_units.map(&attribute).join("|")})\z/
152
+ return Unit.for($2).with_prefix($1).clone
153
+ end
154
+ return nil
155
+ end
156
+
157
+ # Standardizes query strings or symbols into canonical form for unit names,
158
+ # symbols and labels
159
+ #
160
+ def self.format_unit_attribute(attribute, string_or_symbol)
161
+ string_or_symbol = case attribute
162
+ when :symbol then string_or_symbol.standardize
163
+ when :name then string_or_symbol.standardize.singularize.downcase
164
+ else string_or_symbol.to_s
165
+ end
166
+ end
167
+
168
+ def self.parse_unit_and_index(string)
169
+ string.scan(/([^0-9\^]+)\^?([\d\.-]*)?/i)
170
+ index = ($2.nil? or $2.empty? ? 1 : $2.to_i)
171
+ CompoundBaseUnit.new($1.to_s, index)
172
+ end
173
+
174
+ def self.parse_numerator_units(string)
175
+ # If no middot then names parsed by whitespace
176
+ # Need to consider multi word unit names
177
+ num_units = ( string =~ /·/ ? string.split("·") : string.split(" ") )
178
+ num_units.map! do |substring|
179
+ Unit.parse_unit_and_index(substring)
180
+ end
181
+ end
182
+
183
+ def self.parse_denominator_units(string)
184
+ Unit.parse_numerator_units(string).map do |unit|
185
+ unit.index *= -1
186
+ unit
187
+ end
188
+ end
189
+
190
+ # This can be replicated by method missing approach, but explicit method provided
191
+ # given importance in #match (and #for) methods regexen
192
+ #
193
+ def self.si_non_prefixed_units
194
+ @units.select {|unit| unit.is_si_unit? and not unit.is_prefixed_unit? }
195
+ end
196
+
197
+ # This can be replicated by method missing approach, but explicit method provided
198
+ # given importance in #match (and #for) methods regexen
199
+ #
200
+ def self.non_si_non_prefixed_units
201
+ @units.select {|unit| unit.is_non_si_unit? and not unit.is_prefixed_unit? }
202
+ end
203
+
204
+ def self.multi_word_unit_names
205
+ @units.map(&:name).compact.select {|name| name.word_count > 1 }
206
+ end
207
+
208
+ def self.multi_word_unit_pluralized_names
209
+ multi_word_unit_names.map(&:pluralize)
210
+ end
211
+
212
+ def self.multi_word_unit_symbols
213
+ @units.map(&:symbol).compact.select {|symbol| symbol.word_count > 1 }
214
+ end
215
+
216
+ end
217
+ end
data/lib/quantify.rb ADDED
@@ -0,0 +1,26 @@
1
+
2
+ # The Quantify library provides a system for handling physical quantities. Physical
3
+ # quantities are represented by a value and a unit. Quantities can be added,
4
+ # subtracted, multiplied, etc. or converted into alternative units
5
+ #
6
+
7
+ require 'pp'
8
+ require 'rubygems'
9
+ require 'active_support/inflector'
10
+ require 'quantify/quantify'
11
+ require 'quantify/core_extensions'
12
+ require 'quantify/inflections'
13
+ require 'quantify/exception'
14
+ require 'quantify/dimensions'
15
+ require 'quantify/unit/prefix/prefix'
16
+ require 'quantify/unit/prefix/base_prefix'
17
+ require 'quantify/unit/prefix/si_prefix'
18
+ require 'quantify/unit/prefix/non_si_prefix'
19
+ require 'quantify/unit/unit'
20
+ require 'quantify/unit/base_unit'
21
+ require 'quantify/unit/si_unit'
22
+ require 'quantify/unit/non_si_unit'
23
+ require 'quantify/unit/compound_base_unit'
24
+ require 'quantify/unit/compound_unit'
25
+ require 'quantify/quantity'
26
+ require 'quantify/config'
@@ -0,0 +1,294 @@
1
+ require 'quantify'
2
+ include Quantify
3
+
4
+ describe Dimensions do
5
+
6
+ it "should hold array of recignised base quantities" do
7
+ Dimensions::BASE_QUANTITIES. should == [
8
+ :mass, :length, :time, :electric_current, :temperature,
9
+ :luminous_intensity, :amount_of_substance, :information,
10
+ :currency, :item ]
11
+ end
12
+
13
+ it "should return an array class variable" do
14
+ Dimensions.dimensions.class.should == Array
15
+ end
16
+
17
+ it "class variable should contain dimension objects" do
18
+ Dimensions.dimensions[0].class.should == Dimensions
19
+ Dimensions.dimensions[1].class.should == Dimensions
20
+ end
21
+
22
+ it "should list all physical quantities as strings" do
23
+ list = Dimensions.physical_quantities
24
+ list.should include "acceleration"
25
+ list.should include "force"
26
+ list.should_not include "effort"
27
+ end
28
+
29
+ it "should return correct dimension object with symbol :length" do
30
+ dimensions = Dimensions.for(:length)
31
+ dimensions.class.should == Dimensions
32
+ dimensions.length.should == 1
33
+ dimensions.mass.should == nil
34
+ end
35
+
36
+ it "should return correct dimension object with symbol :energy" do
37
+ dimensions = Dimensions.for(:energy)
38
+ dimensions.class.should == Dimensions
39
+ dimensions.length.should == 2
40
+ dimensions.mass.should == 1
41
+ dimensions.time.should == -2
42
+ dimensions.luminous_intensity.should == nil
43
+ end
44
+
45
+ it "should return correct dimension object with string 'energy'" do
46
+ dimensions = Dimensions.for('ENERGY')
47
+ dimensions.class.should == Dimensions
48
+ dimensions.length.should == 2
49
+ dimensions.mass.should == 1
50
+ dimensions.time.should == -2
51
+ dimensions.luminous_intensity.should == nil
52
+ end
53
+
54
+ it "should return correct dimension object with string 'length'" do
55
+ dimensions = Dimensions.for('length')
56
+ dimensions.class.should == Dimensions
57
+ dimensions.length.should == 1
58
+ dimensions.mass.should == nil
59
+ end
60
+
61
+ it "should raise error with invalid integer argument" do
62
+ lambda{dimensions = Dimensions.for(1)}.should raise_error
63
+ end
64
+
65
+ it "should raise error with unknown dimension" do
66
+ lambda{dimensions = Dimensions.for(:effort)}.should raise_error
67
+ end
68
+
69
+ it "should initialize a new object in @@dimensions class variable" do
70
+ Dimensions.load :physical_quantity => :some_dimensions, :length => 12
71
+ dimensions = Dimensions.for :some_dimensions
72
+ dimensions.class.should == Dimensions
73
+ dimensions.length.should == 12
74
+ end
75
+
76
+ it "should refuse to load new object in class array if no physical quantity" do
77
+ lambda{Dimensions.load :mass => 1}.should raise_error
78
+ end
79
+
80
+ it "should refuse to load new object in class array if no physical quantity" do
81
+ lambda{Dimensions.load :physical_quantity => nil, :mass => 1}.should raise_error
82
+ end
83
+
84
+ it "should create a new object with valid arguments" do
85
+ dimensions = Dimensions.new :mass => 1, :length => -2
86
+ dimensions.class.should == Dimensions
87
+ dimensions.mass.should == 1
88
+ dimensions.length.should == -2
89
+ dimensions.time.should == nil
90
+ end
91
+
92
+ it "should raise error with invalid arguments" do
93
+ lambda{dimension = Dimensions.new :acceleration => 1}.should raise_error
94
+ end
95
+
96
+ it "should identify the physical quantity and set ivar if known" do
97
+ dimensions = Dimensions.new :mass => 1
98
+ dimensions.describe.should == 'mass'
99
+ dimensions.physical_quantity.should == 'mass'
100
+ end
101
+
102
+ it "should identify the physical quantity if known" do
103
+ dimensions = Dimensions.new :mass => 1, :length => 2, :time => -2
104
+ dimensions.describe.should == 'energy'
105
+ dimensions.is_known?.should == true
106
+ end
107
+
108
+ it "should return nil if physical quantity not known" do
109
+ dimensions = Dimensions.new :mass => 81, :length => 2, :time => -2
110
+ dimensions.describe.should == nil
111
+ dimensions.is_known?.should == false
112
+ end
113
+
114
+ it "should return the correct dimensions representation on multiplying" do
115
+ dimensions_1 = Dimensions.for :length
116
+ dimensions_2 = Dimensions.for :length
117
+ dimensions_3 = dimensions_1 * dimensions_2
118
+ dimensions_3.class.should == Dimensions
119
+ dimensions_3.length.should == 2
120
+ dimensions_3.physical_quantity.should == 'area'
121
+ end
122
+
123
+ it "should return the correct dimensions representation on dividing" do
124
+ dimensions_1 = Dimensions.for :area
125
+ dimensions_2 = Dimensions.for :length
126
+ dimensions_3 = dimensions_1 / dimensions_2
127
+ dimensions_3.class.should == Dimensions
128
+ dimensions_3.length.should == 1
129
+ dimensions_3.physical_quantity.should == 'length'
130
+ end
131
+
132
+ it "should return the correct dimensions representation on raising to power" do
133
+ dimensions_1 = Dimensions.for :length
134
+ dimensions_2 = dimensions_1 ** 2
135
+ dimensions_2.class.should == Dimensions
136
+ dimensions_2.length.should == 2
137
+ dimensions_2.physical_quantity.should == 'area'
138
+ end
139
+
140
+ it "should return the correct dimensions representation on reciprocalize" do
141
+ dimensions_1 = Dimensions.for :time
142
+ dimensions_2 = dimensions_1.reciprocalize
143
+ dimensions_2.class.should == Dimensions
144
+ dimensions_2.time.should == -1
145
+ dimensions_2.physical_quantity.should == 'frequency'
146
+ end
147
+
148
+ it "should retrieve the correct dimension with dynamic method" do
149
+ Dimensions.acceleration.physical_quantity.should == 'acceleration'
150
+ end
151
+
152
+ it "should recognise length as a base quantity" do
153
+ dimension = Dimensions.length
154
+ dimension.is_base?.should == true
155
+ end
156
+
157
+ it "should recognise mass as a base quantity" do
158
+ dimension = Dimensions.mass
159
+ dimension.is_base?.should == true
160
+ end
161
+
162
+ it "should not recognise force as a base quantity" do
163
+ dimension = Dimensions.force
164
+ dimension.is_base?.should == false
165
+ end
166
+
167
+ it "should recognise plane angle as dimensionless" do
168
+ dimension = Dimensions.plane_angle
169
+ dimension.is_dimensionless?.should == true
170
+ end
171
+
172
+ it "should not recognise power as dimensionless" do
173
+ dimension = Dimensions.power
174
+ dimension.is_dimensionless?.should == false
175
+ end
176
+
177
+ it "should make an instance dimensionless" do
178
+ dimension = Dimensions.power
179
+ dimension.is_dimensionless?.should == false
180
+ dimension
181
+ end
182
+
183
+ it "should return appropriate associated units" do
184
+ dimension = Dimensions.power
185
+ units = dimension.units :name
186
+ units.class.should == Array
187
+ units.include?('watt').should == true
188
+ end
189
+
190
+ it "should return appropriate associated units" do
191
+ dimension = Dimensions.length
192
+ units = dimension.units :name
193
+ units.class.should == Array
194
+ units.include?('yard').should == true
195
+ units.include?('siemens').should == false
196
+ end
197
+
198
+ it "should return appropriate associated units" do
199
+ dimension = Dimensions.energy
200
+ units = dimension.units :symbol
201
+ units.class.should == Array
202
+ units.include?('J').should == true
203
+ units.include?('ft').should == false
204
+ units.include?('kWh').should == true
205
+ end
206
+
207
+ it "should return appropriate associated units" do
208
+ dimension = Dimensions.temperature
209
+ units = dimension.units :symbol
210
+ units.class.should == Array
211
+ units.include?('K').should == true
212
+ units.include?('°C').should == true
213
+ units.include?('L').should == false
214
+ end
215
+
216
+ it "should return correct SI unit" do
217
+ dimension = Dimensions.temperature
218
+ dimension.si_unit.symbol.should == 'K'
219
+ end
220
+
221
+ it "should return correct SI unit" do
222
+ dimension = Dimensions.power
223
+ dimension.si_unit.symbol.should == 'W'
224
+ end
225
+
226
+ it "should return correct SI unit" do
227
+ dimension = Dimensions.kinematic_viscosity
228
+ dimension.si_unit.name.should == 'square metre per second'
229
+ end
230
+
231
+ it "should return correct SI unit" do
232
+ dimension = Dimensions.mass
233
+ dimension.si_unit.symbol.should == 'kg'
234
+ dimension.si_unit.symbol.should_not == 'd'
235
+ end
236
+
237
+ it "should return correct SI unit" do
238
+ dimension = Dimensions.plane_angle
239
+ dimension.si_unit.symbol.should == 'rad'
240
+ dimension.si_unit.symbol.should_not == 'W'
241
+ end
242
+
243
+ it "should return the correct SI base units" do
244
+ units = Dimensions.mass.si_base_units :name
245
+ units.class.should == Array
246
+ units.include?('kilogram').should == true
247
+ units.include?('gram').should == false
248
+ end
249
+
250
+ it "should return the correct SI base units" do
251
+ units = Dimensions.energy.si_base_units :name
252
+ units.class.should == Array
253
+ units.include?('kilogram').should == true
254
+ units.include?('square metre').should == true
255
+ units.include?('per square second').should == true
256
+ units.include?('kelvin').should == false
257
+ units.size.should == 3
258
+ end
259
+
260
+ it "should return the correct SI base units" do
261
+ units = Dimensions.force.si_base_units :name
262
+ units.class.should == Array
263
+ units.include?('kilogram').should == true
264
+ units.include?('metre').should == true
265
+ units.include?('second').should == false
266
+ units.size.should == 3
267
+ end
268
+
269
+ it "should return the correct SI base units" do
270
+ units = Dimensions.mass.si_base_units :symbol
271
+ units.class.should == Array
272
+ units.include?('kg').should == true
273
+ units.size.should == 1
274
+ end
275
+
276
+ it "should return the correct SI base units" do
277
+ units = Dimensions.area.si_base_units :symbol
278
+ units.class.should == Array
279
+ units.include?('m^2').should == true
280
+ end
281
+
282
+ it "should recognise molar quantity" do
283
+ dimension = Dimensions.mass/Dimensions.amount_of_substance
284
+ dimension.is_molar_quantity?.should == true
285
+ dimension.is_specific_quantity?.should == false
286
+ end
287
+
288
+ it "should recognise specific quantity" do
289
+ dimension = Dimensions.volume/Dimensions.mass
290
+ dimension.is_molar_quantity?.should == false
291
+ dimension.is_specific_quantity?.should == true
292
+ end
293
+ end
294
+