quantify 1.0.0

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.
@@ -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
+