sy 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/lib/sy.rb +274 -0
- data/lib/sy/absolute_magnitude.rb +96 -0
- data/lib/sy/abstract_algebra.rb +102 -0
- data/lib/sy/composition.rb +219 -0
- data/lib/sy/dimension.rb +182 -0
- data/lib/sy/expressible_in_units.rb +135 -0
- data/lib/sy/fixed_assets_of_the_module.rb +287 -0
- data/lib/sy/magnitude.rb +515 -0
- data/lib/sy/mapping.rb +84 -0
- data/lib/sy/matrix.rb +69 -0
- data/lib/sy/quantity.rb +455 -0
- data/lib/sy/signed_magnitude.rb +51 -0
- data/lib/sy/unit.rb +288 -0
- data/lib/sy/version.rb +4 -0
- data/lib/sy/wildcard_zero.rb +29 -0
- data/sy.gemspec +19 -0
- data/test/sy_test.rb +385 -0
- metadata +80 -0
@@ -0,0 +1,135 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
|
3
|
+
# This mixin endows a class with the capacity to respond to method
|
4
|
+
# symbols corresponding to metrological units defined in SY.
|
5
|
+
#
|
6
|
+
module SY::ExpressibleInUnits
|
7
|
+
class RecursionError < StandardError; end
|
8
|
+
|
9
|
+
def method_missing ß, *args, &block
|
10
|
+
# hack #0: working around a bug in a 3rd party library
|
11
|
+
return self if ß.to_s.include?( 'begin' ) || ß.to_s.include?( 'end' )
|
12
|
+
# hack #1: get rid of missing methods 'to_something', esp. #to_ary
|
13
|
+
super if ß == :to_ary || ß.to_s.starts_with?( 'to_' )
|
14
|
+
begin # prevent recurrent method_missing for the same symbol
|
15
|
+
anti_recursion_exec_with_token ß, :@SY_Units_mmiss do
|
16
|
+
puts "Method missing: '#{ß}'" if SY::DEBUG
|
17
|
+
# Parse the unit symbol.
|
18
|
+
prefixes, units, exps = parse_unit_symbol( ß )
|
19
|
+
# Define the unit method.
|
20
|
+
self.class.module_eval write_unit_method( ß, prefixes, units, exps )
|
21
|
+
end
|
22
|
+
rescue NameError => m
|
23
|
+
puts "NameError raised: #{m}" if SY::DEBUG
|
24
|
+
super # give up
|
25
|
+
rescue SY::ExpressibleInUnits::RecursionError
|
26
|
+
super # give up
|
27
|
+
else # invoke the defined method that we just defined
|
28
|
+
send ß, *args, &block
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def respond_to_missing? ß, *args, &block
|
33
|
+
str = ß.to_s
|
34
|
+
return false if str.start_with?( 'to_' ) || # speedup hack
|
35
|
+
str == 'begin' || str == 'end' # bugs in 3rd party library
|
36
|
+
begin
|
37
|
+
anti_recursion_exec_with_token ß, :@SY_Units_rmiss do
|
38
|
+
parse_unit_symbol( ß )
|
39
|
+
end
|
40
|
+
rescue NameError, SY::ExpressibleInUnits::RecursionError
|
41
|
+
false
|
42
|
+
else
|
43
|
+
true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Looking at the method symbol, delivered to #method_missing, this method
|
50
|
+
# figures out which SY units it represents, along with prefixes and exponents.
|
51
|
+
#
|
52
|
+
def parse_unit_symbol ß
|
53
|
+
SY::Unit.parse_sps_using_all_prefixes( ß ) # rely on SY::Unit
|
54
|
+
end
|
55
|
+
|
56
|
+
# Taking method name symbol as the first argument, and three more arguments
|
57
|
+
# representing equal-length arrays of prefixes, unit symbols and exponents,
|
58
|
+
# appropriate method string is written.
|
59
|
+
#
|
60
|
+
def write_unit_method ß, prefixes, units, exponents
|
61
|
+
known_units = SY::Unit.instances
|
62
|
+
# A procedure to find unit based on name or abbreviation:
|
63
|
+
find_unit = lambda do |ς|
|
64
|
+
known_units.find { |u| u.name.to_s == ς || u.short.to_s == ς }
|
65
|
+
end
|
66
|
+
# Return prefix method or empty ς if not necessary.
|
67
|
+
prefix_method_ς = lambda do |prefix|
|
68
|
+
puts "About to call PREFIX TABLE.to_full with #{prefix}" if SY::DEBUG
|
69
|
+
full_prefix = SY::PREFIX_TABLE.to_full( prefix )
|
70
|
+
full_prefix == '' ? '' : ".#{full_prefix}"
|
71
|
+
end
|
72
|
+
# Return exponentiation string (suffix) or empty ς if not necessary.
|
73
|
+
exponentiation_ς = lambda do |exponent|
|
74
|
+
exponent == 1 ? '' : " ** #{exponent}"
|
75
|
+
end
|
76
|
+
# Prepare prefix / unit / exponent triples for making factor strings:
|
77
|
+
triples = [ prefixes, units, exponents ].transpose
|
78
|
+
# A procedure for triple processing before use:
|
79
|
+
process_triple = lambda do |prefix, unit_ς, exponent|
|
80
|
+
[ find_unit.( unit_ς ).name.to_s.upcase,
|
81
|
+
prefix_method_ς.( prefix ),
|
82
|
+
exponentiation_ς.( exponent ) ]
|
83
|
+
end
|
84
|
+
# Method skeleton:
|
85
|
+
if triples.size == 1 && triples.first[-1] == 1 then
|
86
|
+
method_skeleton = "def #{ß}( exp=1 )\n" +
|
87
|
+
" %s\n" +
|
88
|
+
"end"
|
89
|
+
method_body = "if exp == 1 then\n" +
|
90
|
+
" +( ::SY.Unit( :%s )%s ) * self\n" +
|
91
|
+
"else\n" +
|
92
|
+
" +( ::SY.Unit( :%s )%s ) ** exp * self\n" +
|
93
|
+
"end"
|
94
|
+
uς, pfxς, expς = process_triple.( *triples.shift )
|
95
|
+
method_body %= [uς, pfxς] * 2
|
96
|
+
else
|
97
|
+
method_skeleton = "def #{ß}\n" +
|
98
|
+
" %s\n" +
|
99
|
+
"end"
|
100
|
+
factors = [ "+( ::SY.Unit( :%s )%s )%s * self" %
|
101
|
+
process_triple.( *triples.shift ) ] +
|
102
|
+
triples.map do |ᴛ|
|
103
|
+
"( ::SY.Unit( :%s )%s.relative ) )%s" % process_triple.( *ᴛ )
|
104
|
+
end
|
105
|
+
# Multiply the factors toghether:
|
106
|
+
method_body = factors.join( " * \n " )
|
107
|
+
end
|
108
|
+
# Return the finished method string:
|
109
|
+
return ( method_skeleton % method_body ).tap { |ς| puts ς if SY::DEBUG }
|
110
|
+
end
|
111
|
+
|
112
|
+
# Takes a token as the first argument, a symbol of the instance variable to
|
113
|
+
# be used for storage of active tokens, grabs the token, executes the
|
114
|
+
# supplied block, and releases the token. The method guards against double
|
115
|
+
# execution for the same token, raising IllegalRecursionError in such case.
|
116
|
+
#
|
117
|
+
def anti_recursion_exec_with_token token, inst_var
|
118
|
+
registry = self.class.instance_variable_get( inst_var ) ||
|
119
|
+
self.class.instance_variable_set( inst_var, [] )
|
120
|
+
if registry.include? token then
|
121
|
+
raise SY::ExpressibleInUnits::RecursionError
|
122
|
+
end
|
123
|
+
begin
|
124
|
+
registry << token
|
125
|
+
yield if block_given?
|
126
|
+
ensure
|
127
|
+
registry.delete token
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# FIXME: There should be an option to define by default, already at the
|
132
|
+
# beginning, certain methods for certain classes, to get in front of possible
|
133
|
+
# collisions. Collision was detected for example for #second with
|
134
|
+
# active_support/duration.rb
|
135
|
+
end # module SY::UnitMethodsMixin
|
@@ -0,0 +1,287 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
|
3
|
+
# Here, fixed assets of the main module are set up.
|
4
|
+
#
|
5
|
+
module SY
|
6
|
+
# Basic physical dimensions.
|
7
|
+
#
|
8
|
+
BASE_DIMENSIONS = {
|
9
|
+
L: :LENGTH,
|
10
|
+
M: :MASS,
|
11
|
+
Q: :ELECTRIC_CHARGE,
|
12
|
+
Θ: :TEMPERATURE,
|
13
|
+
T: :TIME
|
14
|
+
}
|
15
|
+
|
16
|
+
class << BASE_DIMENSIONS
|
17
|
+
# Letters of base dimensions.
|
18
|
+
#
|
19
|
+
def letters
|
20
|
+
keys
|
21
|
+
end
|
22
|
+
|
23
|
+
# Base dimensions letters with prefixes.
|
24
|
+
#
|
25
|
+
def prefixed_letters
|
26
|
+
[] # none for now
|
27
|
+
end
|
28
|
+
|
29
|
+
# Base dimension symbols – letters and prefixed letters.
|
30
|
+
#
|
31
|
+
def base_symbols
|
32
|
+
@baseß ||= letters + prefixed_letters
|
33
|
+
end
|
34
|
+
alias basic_symbols base_symbols
|
35
|
+
|
36
|
+
# Takes an sps representing a dimension, and converts it to a hash of
|
37
|
+
# base dimension symbols => exponents.
|
38
|
+
#
|
39
|
+
def parse_sps( sps )
|
40
|
+
_, letters, exponents = ::SY::SPS_PARSER.( sps, self.letters )
|
41
|
+
return Hash[ letters.map( &:to_sym ).zip( exponents.map( &:to_i ) ) ]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Table of standard prefixes and their corresponding unit multiples.
|
46
|
+
#
|
47
|
+
PREFIX_TABLE = [ { full: "exa", short: "E", factor: 1e18 },
|
48
|
+
{ full: "peta", short: "P", factor: 1e15 },
|
49
|
+
{ full: "tera", short: "T", factor: 1e12 },
|
50
|
+
{ full: "giga", short: "G", factor: 1e9 },
|
51
|
+
{ full: "mega", short: "M", factor: 1e6 },
|
52
|
+
{ full: "kilo", short: "k", factor: 1e3 },
|
53
|
+
{ full: "hecto", short: "h", factor: 1e2 },
|
54
|
+
{ full: "deka", short: "dk", factor: 1e1 },
|
55
|
+
{ full: "", short: "", factor: 1 },
|
56
|
+
{ full: "deci", short: "d", factor: 1e-1 },
|
57
|
+
{ full: "centi", short: "c", factor: 1e-2 },
|
58
|
+
{ full: "mili", short: "m", factor: 1e-3 },
|
59
|
+
{ full: "micro", short: "µ", factor: 1e-6 },
|
60
|
+
{ full: "nano", short: "n", factor: 1e-9 },
|
61
|
+
{ full: "pico", short: "p", factor: 1e-12 },
|
62
|
+
{ full: "femto", short: "f", factor: 1e-15 },
|
63
|
+
{ full: "atto", short: "a", factor: 1e-18 } ]
|
64
|
+
|
65
|
+
|
66
|
+
class << PREFIX_TABLE
|
67
|
+
# List of full prefixes.
|
68
|
+
#
|
69
|
+
def full_prefixes
|
70
|
+
@full ||= map { |row| row[:full] }
|
71
|
+
end
|
72
|
+
|
73
|
+
# List of prefix abbreviations.
|
74
|
+
#
|
75
|
+
def prefix_abbreviations
|
76
|
+
@short ||= map { |row| row[:short] }
|
77
|
+
end
|
78
|
+
alias short_prefixes prefix_abbreviations
|
79
|
+
|
80
|
+
# List of full prefixes and short prefixes.
|
81
|
+
#
|
82
|
+
def all_prefixes
|
83
|
+
@all ||= full_prefixes + prefix_abbreviations
|
84
|
+
end
|
85
|
+
|
86
|
+
# Parses an SPS using a list of permitted unit symbols, currying it with
|
87
|
+
# own #all_prefixes.
|
88
|
+
#
|
89
|
+
def parse_sps sps, unit_symbols
|
90
|
+
SY::SPS_PARSER.( sps, unit_symbols, all_prefixes )
|
91
|
+
end
|
92
|
+
|
93
|
+
# A hash of clue => corresponding_row pairs.
|
94
|
+
#
|
95
|
+
def row clue
|
96
|
+
( @rowꜧ ||= Hash.new do |ꜧ, key|
|
97
|
+
case key
|
98
|
+
when Symbol then
|
99
|
+
rslt = ꜧ[key.to_s]
|
100
|
+
ꜧ[key] = rslt if rslt
|
101
|
+
else
|
102
|
+
r = find { |r|
|
103
|
+
r[:full] == key || r[:short] == key || r[:factor] == key
|
104
|
+
}
|
105
|
+
ꜧ[key] = r if r
|
106
|
+
end
|
107
|
+
end )[ clue ]
|
108
|
+
end
|
109
|
+
|
110
|
+
# Converts a clue to a full prefix.
|
111
|
+
#
|
112
|
+
def to_full clue
|
113
|
+
( @fullꜧ ||= Hash.new do |ꜧ, key|
|
114
|
+
result = row( key )
|
115
|
+
result = result[:full]
|
116
|
+
ꜧ[key] = result if result
|
117
|
+
end )[ clue ]
|
118
|
+
end
|
119
|
+
|
120
|
+
# Converts a clue to a prefix abbreviation.
|
121
|
+
#
|
122
|
+
def to_short clue
|
123
|
+
( @shortꜧ ||= Hash.new do |ꜧ, key|
|
124
|
+
result = row( key )[:short]
|
125
|
+
ꜧ[key] = result if result
|
126
|
+
end )[ clue ]
|
127
|
+
end
|
128
|
+
|
129
|
+
# Converts a clue to a factor.
|
130
|
+
#
|
131
|
+
def to_factor clue
|
132
|
+
( @factorꜧ ||= Hash.new do |ꜧ, key|
|
133
|
+
result = row( key )[:factor]
|
134
|
+
ꜧ[key] = result if result
|
135
|
+
end )[ clue ]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Unicode superscript exponents.
|
140
|
+
#
|
141
|
+
SUPERSCRIPT = Hash.new { |ꜧ, key|
|
142
|
+
if key.is_a? String then
|
143
|
+
key.size <= 1 ? nil : key.each_char.map{|c| ꜧ[c] }.join
|
144
|
+
else
|
145
|
+
ꜧ[key.to_s]
|
146
|
+
end
|
147
|
+
}.merge! Hash[ '-/0123456789'.each_char.zip( '⁻⎖⁰¹²³⁴⁵⁶⁷⁸⁹'.each_char ) ]
|
148
|
+
|
149
|
+
# Reverse conversion of Unicode superscript exponents (from exponent
|
150
|
+
# strings to fixnums).
|
151
|
+
#
|
152
|
+
SUPERSCRIPT_DOWN = Hash.new { |ꜧ, key|
|
153
|
+
if key.is_a? String then
|
154
|
+
key.size == 1 ? nil : key.each_char.map{|c| ꜧ[c] }.join
|
155
|
+
else
|
156
|
+
ꜧ[key.to_s]
|
157
|
+
end
|
158
|
+
}.merge!( SUPERSCRIPT.invert ).merge!( '¯' => '-', # other superscript chars
|
159
|
+
'´' => '/' )
|
160
|
+
|
161
|
+
# SPS stands for "superscripted product string", It is a string of specific
|
162
|
+
# symbols with or without Unicode exponents, separated by periods, such as
|
163
|
+
# "syma.symb².symc⁻³.symd.syme⁴" etc. This closure takes 2 arguments (array
|
164
|
+
# of symbols, and array of exponents) and produces an SPS out of them.
|
165
|
+
#
|
166
|
+
SPS = lambda { |ßs, exps|
|
167
|
+
raise ArgumentError unless ßs.size == exps.size
|
168
|
+
exps = exps.map{|e| Integer e }
|
169
|
+
zipped = ßs.zip( exps )
|
170
|
+
clean = zipped.reject {|e| e[1] == 0 }
|
171
|
+
# omit exponents equal to 1:
|
172
|
+
clean.map{|ß, exp| "#{ß}#{exp == 1 ? "" : SUPERSCRIPT[exp]}" }.join "."
|
173
|
+
}
|
174
|
+
|
175
|
+
# Singleton #inspect method for SPS-making closure.
|
176
|
+
#
|
177
|
+
def SPS.inspect
|
178
|
+
"Superscripted product string constructor lambda." +
|
179
|
+
"Takes 2 arguments. Example: [:a, :b], [-1, 2] #=> a⁻¹b²."
|
180
|
+
end
|
181
|
+
|
182
|
+
# A closure that parses superscripted product strings (SPSs). It takes 3
|
183
|
+
# arguments: a string to be parsed, an array of acceptable symbols, and
|
184
|
+
# an array of acceptable prefixes. It returns 3 equal-sized arrays: prefixes,
|
185
|
+
# symbols and exponents.
|
186
|
+
#
|
187
|
+
SPS_PARSER = lambda { |input_ς, ßs, prefixes = []|
|
188
|
+
input_ς = input_ς.to_s.strip
|
189
|
+
ßs = ßs.map &:to_s
|
190
|
+
prefixes = ( prefixes.map( &:to_s ) << '' ).uniq
|
191
|
+
# input string splitting
|
192
|
+
input_ς_sections = input_ς.split '.'
|
193
|
+
if input_ς_sections.empty?
|
194
|
+
raise NameError, "Bad input string: '#{input_ς}'!" unless input_ς.empty?
|
195
|
+
return [], [], []
|
196
|
+
end
|
197
|
+
# analysis of input string sections
|
198
|
+
input_ς_sections.each_with_object [[], [], []] do |_section_, memo|
|
199
|
+
section = _section_.dup
|
200
|
+
superscript_chars = SUPERSCRIPT.values
|
201
|
+
# chop off the superscript tail, if any
|
202
|
+
section.chop! while superscript_chars.any? { |ch| section.end_with? ch }
|
203
|
+
# the set of candidate unit symbols
|
204
|
+
candidate_ßs = ßs.select { |ß| section.end_with? ß }
|
205
|
+
# seek candidate prefixes corresponding to candidate_ßs
|
206
|
+
candidate_prefixes = candidate_ßs.map { |ß| section[ 0..((-1) - ß.size) ] }
|
207
|
+
# see which possible prefixes can be confirmed
|
208
|
+
confirmed_prefixes = candidate_prefixes.select { |x| prefixes.include? x }
|
209
|
+
# complain if no symbol matches sec
|
210
|
+
raise NameError, "Unknown unit: '#{section}'!" if confirmed_prefixes.empty?
|
211
|
+
# pay attention to ambiguity in prefix/symbol pair
|
212
|
+
if confirmed_prefixes.size > 1 then
|
213
|
+
if confirmed_prefixes.any? { |x| x == '' } then # prefer empty prefixes
|
214
|
+
chosen_prefix = ''
|
215
|
+
else
|
216
|
+
raise NameError, "Ambiguity in interpretation of '#{section}'!"
|
217
|
+
end
|
218
|
+
else
|
219
|
+
chosen_prefix = confirmed_prefixes[0]
|
220
|
+
end
|
221
|
+
# Based on it, interpret the section parts:
|
222
|
+
unit_ς = section[ (chosen_prefix.size)..(-1) ]
|
223
|
+
suffix = _section_[ ((-1) - chosen_prefix.size - unit_ς.size)..(-1) ]
|
224
|
+
# Make the exponent string suffix into the exponent number:
|
225
|
+
exponent_ς = SUPERSCRIPT_DOWN[ suffix ]
|
226
|
+
# Complain if bad:
|
227
|
+
raise NameError, "Malformed exponent in #{_section_}!" if exponent_ς.nil?
|
228
|
+
exponent_ς = "1" if exponent_ς == '' # empty exponent string means 1
|
229
|
+
exp = Integer exponent_ς
|
230
|
+
raise NameError, "Zero exponents not allowed: #{exponent_ς}" if exp == 0
|
231
|
+
# and store the interpretation
|
232
|
+
memo[0] << chosen_prefix; memo[1] << unit_ς; memo[2] << exp
|
233
|
+
memo
|
234
|
+
end
|
235
|
+
}
|
236
|
+
|
237
|
+
# Singleton #inspect method for SPS-parsing closure.
|
238
|
+
#
|
239
|
+
def SPS_PARSER.inspect
|
240
|
+
"Superscripted product string parser lambda. " +
|
241
|
+
"Takes 2 compulsory and 1 optional argument. Example: " +
|
242
|
+
'"kB.s⁻¹", [:g, :B, :s, :C], [:M, :k, :m, :µ] #=> ["k", ""], ' +
|
243
|
+
'["B", "s"], [1, -1]'
|
244
|
+
end
|
245
|
+
|
246
|
+
# Mainly for mixing incompatible quantities.
|
247
|
+
#
|
248
|
+
class QuantityError < StandardError; end
|
249
|
+
|
250
|
+
# Mainly for mixing incompatible dimensions.
|
251
|
+
#
|
252
|
+
class DimensionError < StandardError; end
|
253
|
+
|
254
|
+
# Mainly for negative or otherwise impossible physical amounts.
|
255
|
+
#
|
256
|
+
class MagnitudeError < StandardError; end
|
257
|
+
|
258
|
+
# Convenience dimension accessor.
|
259
|
+
#
|
260
|
+
def Dimension id=proc{ return ::SY::Dimension }.call
|
261
|
+
case id.to_s
|
262
|
+
when '', 'nil', 'null', 'zero', '0', '⊘', '∅', 'ø' then SY::Dimension.zero
|
263
|
+
else SY::Dimension.new id end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Convenience quantity instance accessor.
|
267
|
+
#
|
268
|
+
def Quantity id=proc{ return ::SY::Quantity }.call
|
269
|
+
SY::Quantity.instance id
|
270
|
+
end
|
271
|
+
|
272
|
+
# Convenience unit instance accessor.
|
273
|
+
#
|
274
|
+
def Unit id=proc{ return ::SY::Unit }.call
|
275
|
+
SY::Unit.instance id
|
276
|
+
end
|
277
|
+
|
278
|
+
# Explicit magnitude constructor.
|
279
|
+
#
|
280
|
+
def Magnitude args=proc{ return ::SY::Magnitude }.call
|
281
|
+
args.must_have :quantity, syn!: :of
|
282
|
+
qnt = args.delete :quantity
|
283
|
+
SY::Magnitude.of qnt, args
|
284
|
+
end
|
285
|
+
|
286
|
+
module_function :Dimension, :Quantity, :Unit, :Magnitude
|
287
|
+
end
|
data/lib/sy/magnitude.rb
ADDED
@@ -0,0 +1,515 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
|
3
|
+
# In physics, difference between absolute and relative magnitudes is well
|
4
|
+
# understood. The magnitude class here represents absolute magnitude – physical
|
5
|
+
# number of unit objects making up the amount of some metrological quantity.
|
6
|
+
# Amounts of absolute magnitudes may not be negative. When one desires to
|
7
|
+
# represent <em>difference</me> between magnitudes, which can be positive as
|
8
|
+
# well as negative, relative magnitude has to be used.
|
9
|
+
#
|
10
|
+
# While ordinary #+ and #- methods of absolute magnitudes return relative
|
11
|
+
# relative magnitudes, absolute magnitudes have additional methods #add and
|
12
|
+
# #subtract, which return absolute magnitudes (it is the responsibility of the
|
13
|
+
# caller to avoid negative results). Furthermore, absolute magnitudes have
|
14
|
+
# special subtraction method #take, which guards against subtracting more than
|
15
|
+
# the magnitude's amount.
|
16
|
+
#
|
17
|
+
module SY::Magnitude
|
18
|
+
class << self
|
19
|
+
# Constructor of absolute magnitudes of a given quantity.
|
20
|
+
#
|
21
|
+
def absolute *args
|
22
|
+
ꜧ = args.extract_options!
|
23
|
+
qnt = ꜧ[:quantity] || ꜧ[:of] || args.shift
|
24
|
+
return qnt.absolute.magnitude ꜧ[:amount]
|
25
|
+
end
|
26
|
+
|
27
|
+
# Constructor of relative magnitudes of a given quantity.
|
28
|
+
#
|
29
|
+
def difference *args
|
30
|
+
ꜧ = args.extract_options!
|
31
|
+
qnt = ꜧ[:quantity] || ꜧ[:of] || args.shift
|
32
|
+
return qnt.relative.magnitude ꜧ[:amount]
|
33
|
+
end
|
34
|
+
|
35
|
+
# Constructor of magnitudes of a given quantity.
|
36
|
+
#
|
37
|
+
def of qnt, args={}
|
38
|
+
return qnt.magnitude args[:amount]
|
39
|
+
end
|
40
|
+
|
41
|
+
# Constructor of zero magnitude of a given quantity.
|
42
|
+
#
|
43
|
+
def zero
|
44
|
+
return absolute 0
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Magnitudes are comparable.
|
49
|
+
#
|
50
|
+
include Comparable
|
51
|
+
|
52
|
+
# Magnitudes respond to unit methods.
|
53
|
+
#
|
54
|
+
include SY::ExpressibleInUnits
|
55
|
+
|
56
|
+
attr_reader :quantity, :amount
|
57
|
+
alias in_standard_unit amount
|
58
|
+
|
59
|
+
# Delegations to amount:
|
60
|
+
#
|
61
|
+
delegate :zero?, to: :amount
|
62
|
+
delegate :to_f, to: :amount
|
63
|
+
|
64
|
+
# Delegations to quantity:
|
65
|
+
#
|
66
|
+
delegate :dimension,
|
67
|
+
:dimensionless?,
|
68
|
+
:standard_unit,
|
69
|
+
:relative?,
|
70
|
+
:absolute?,
|
71
|
+
:magnitude,
|
72
|
+
:relationship,
|
73
|
+
to: :quantity
|
74
|
+
|
75
|
+
# Computes absolute value and reframes into the absolute quantity.
|
76
|
+
#
|
77
|
+
def absolute
|
78
|
+
quantity.absolute.magnitude amount.abs
|
79
|
+
end
|
80
|
+
|
81
|
+
# Reframes into the relative quantity.
|
82
|
+
#
|
83
|
+
def relative
|
84
|
+
quantity.relative.magnitude amount
|
85
|
+
end
|
86
|
+
|
87
|
+
# Reframes the magnitude into its relative quantity.
|
88
|
+
#
|
89
|
+
def +@
|
90
|
+
quantity.relative.magnitude( amount )
|
91
|
+
end
|
92
|
+
|
93
|
+
# Reframes the magnitude into its relative quantity, with negative amount.
|
94
|
+
#
|
95
|
+
def -@
|
96
|
+
quantity.relative.magnitude( -amount )
|
97
|
+
end
|
98
|
+
|
99
|
+
# Absolute value of a magnitude (no reframe).
|
100
|
+
#
|
101
|
+
def abs
|
102
|
+
magnitude amount.abs
|
103
|
+
end
|
104
|
+
|
105
|
+
# Rounded value of a Magnitude: A new magnitude with rounded amount.
|
106
|
+
#
|
107
|
+
def round *args
|
108
|
+
magnitude amount.round( *args )
|
109
|
+
end
|
110
|
+
|
111
|
+
# Compatible magnitudes compare by their amounts.
|
112
|
+
#
|
113
|
+
def <=> m2
|
114
|
+
return amount <=> m2.amount if quantity == m2.quantity
|
115
|
+
raise SY::QuantityError, "Mismatch: #{quantity} <=> #{m2.quantity}!"
|
116
|
+
end
|
117
|
+
|
118
|
+
# Addition.
|
119
|
+
#
|
120
|
+
def + m2
|
121
|
+
return magnitude amount + m2.amount if quantity == m2.quantity
|
122
|
+
# return self if m2 == SY::ZERO
|
123
|
+
raise SY::QuantityError, "Mismatch: #{quantity} + #{other.quantity}!"
|
124
|
+
end
|
125
|
+
|
126
|
+
# Subtraction.
|
127
|
+
#
|
128
|
+
def - m2
|
129
|
+
return magnitude amount - m2.amount if quantity == m2.quantity
|
130
|
+
# return self if m2 == SY::ZERO
|
131
|
+
raise SY::QuantityError, "Mismatch: #{quantity} - #{m2.quantity}!"
|
132
|
+
end
|
133
|
+
|
134
|
+
# Multiplication.
|
135
|
+
#
|
136
|
+
def * m2
|
137
|
+
case m2
|
138
|
+
when Numeric then
|
139
|
+
magnitude amount * m2
|
140
|
+
# when SY::ZERO then
|
141
|
+
# return magnitude 0
|
142
|
+
else
|
143
|
+
( quantity * m2.quantity ).magnitude( amount * m2.amount )
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Division.
|
148
|
+
#
|
149
|
+
def / m2
|
150
|
+
case m2
|
151
|
+
when Numeric then
|
152
|
+
magnitude amount / m2
|
153
|
+
# when SY::ZERO then
|
154
|
+
# raise ZeroDivisionError, "Attempt to divide #{self} by #{SY::ZERO}."
|
155
|
+
else
|
156
|
+
( quantity / m2.quantity ).magnitude( amount / m2.amount )
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Exponentiation.
|
161
|
+
#
|
162
|
+
def ** exp
|
163
|
+
case exp
|
164
|
+
when SY::Magnitude then
|
165
|
+
raise SY::DimensionError, "Exponent must have zero dimension! " +
|
166
|
+
"(exp given)" unless exp.dimension.zero?
|
167
|
+
( quantity ** exp.amount ).magnitude( amount ** exp.amount )
|
168
|
+
else
|
169
|
+
( quantity ** exp ).magnitude( amount ** exp )
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Type coercion for magnitudes.
|
174
|
+
#
|
175
|
+
def coerce m2
|
176
|
+
case m2
|
177
|
+
when Numeric then return SY::Amount.relative.magnitude( m2 ), self
|
178
|
+
else
|
179
|
+
raise TErr, "#{self} cannot be coerced into a #{m2.class}!"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Gives the magnitude as a plain number in multiples of another magnitude,
|
184
|
+
# supplied as argument. The quantities must match.
|
185
|
+
#
|
186
|
+
def in m2
|
187
|
+
case m2
|
188
|
+
when Symbol, String then
|
189
|
+
begin
|
190
|
+
self.in eval( "1.#{m2}" ).aT_kind_of SY::Magnitude # digest it
|
191
|
+
rescue TypeError
|
192
|
+
raise TypeError, "Evaluating 1.#{m2} does not result in a magnitude; " +
|
193
|
+
"method collision with another library?"
|
194
|
+
end
|
195
|
+
when SY::Magnitude then
|
196
|
+
return amount / m2.amount if quantity == m2.quantity
|
197
|
+
amount / m2.( quantity ).amount # reframe before division
|
198
|
+
else
|
199
|
+
raise TypeError, "Unexpected type for Magnitude#in method! (#{m2.class})"
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Reframes a magnitude into a different quantity. Dimension must match.
|
204
|
+
#
|
205
|
+
def reframe q2
|
206
|
+
case q2
|
207
|
+
when SY::Quantity then q2.import self
|
208
|
+
when SY::Unit then q2.quantity.import self
|
209
|
+
else raise TypeError, "Unable to reframe into a #{q2.class}!" end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Reframes a magnitude into a <em>relative</em> version of a given quantity.
|
213
|
+
# (If absolute quantity is supplied as an argument, its relative colleague
|
214
|
+
# is used to reframe.)
|
215
|
+
#
|
216
|
+
def call q2
|
217
|
+
case q2
|
218
|
+
when SY::Quantity then q2.relative.import self
|
219
|
+
when SY::Unit then q2.quantity.relative.import self
|
220
|
+
else raise TypeError, "Unable to reframe into a #{q2.class}!" end
|
221
|
+
end
|
222
|
+
|
223
|
+
# True if amount is negative. Implicitly false for absolute quantities.
|
224
|
+
#
|
225
|
+
def negative?
|
226
|
+
amount < 0
|
227
|
+
end
|
228
|
+
|
229
|
+
# Opposite of #negative?. Implicitly true for absolute quantities.
|
230
|
+
#
|
231
|
+
def nonnegative?
|
232
|
+
amount >= 0
|
233
|
+
end
|
234
|
+
|
235
|
+
# Gives the magnitude written "naturally", in its most favored units.
|
236
|
+
# It is also possible to supply a unit in which to show the magnitude
|
237
|
+
# as the 1st argument (by default, the most favored unit of its
|
238
|
+
# quantity), or even, as the 2nd argument, the number format (by default,
|
239
|
+
# 3 decimal places).
|
240
|
+
|
241
|
+
|
242
|
+
# further remarks: depending on the area of science the quantity
|
243
|
+
# is in, it should have different preferences for unit presentation.
|
244
|
+
# Different areas prefer different units for different dimensions.
|
245
|
+
|
246
|
+
# For example, if the quantity is "Molarity²", its standard unit will
|
247
|
+
# be anonymous and it magnitudes of this quantity should have preference
|
248
|
+
# for presenting themselves in μM², or in mΜ², or such
|
249
|
+
|
250
|
+
# when attempting to present number Molarity².amount 1.73e-7.mM
|
251
|
+
|
252
|
+
|
253
|
+
#
|
254
|
+
def to_s( unit=quantity.units.first || quantity.standard_unit,
|
255
|
+
number_format=default_amount_format )
|
256
|
+
# step 1: produce pairs [number, unit_presentation],
|
257
|
+
# where unit_presentation is an array of triples
|
258
|
+
# [prefix, unit, exponent], which together give the
|
259
|
+
# correct dimension for this magnitude, and correct
|
260
|
+
# factor so that number * factor == self.amount
|
261
|
+
# step 2: define a goodness function for them
|
262
|
+
# step 3: define a satisfaction criterion
|
263
|
+
# step 4: maximize this goodness function until the satisfaction
|
264
|
+
# criterion is met
|
265
|
+
# step 5: interpolate the string from the chosen choice
|
266
|
+
|
267
|
+
# so, let's start doing it
|
268
|
+
# how do we produce the first choice?
|
269
|
+
# if the standard unit for this quantity is named, we'll start with it
|
270
|
+
|
271
|
+
# let's say that the abbreviation of this std. unit is Uu, so the first
|
272
|
+
# choices will be:
|
273
|
+
#
|
274
|
+
# amount.Uu
|
275
|
+
# (amount * 1000).µUu
|
276
|
+
# (amount / 1000).kUu
|
277
|
+
# (amount * 1_000_000).nUu
|
278
|
+
# (amount / 1_000_000).MUu
|
279
|
+
# ...
|
280
|
+
#
|
281
|
+
# (let's say we'll use only short prefixes)
|
282
|
+
#
|
283
|
+
# which one do we use?
|
284
|
+
# That depends. For example, CelsiusTemperature is never rendered with
|
285
|
+
# SI prefixes, so their cost should be +Infinity
|
286
|
+
#
|
287
|
+
# Cost of the number could be eg.:
|
288
|
+
#
|
289
|
+
# style: cost:
|
290
|
+
# 3.141 0
|
291
|
+
# 31.41, 314.1 1
|
292
|
+
# 0.3141 2
|
293
|
+
# 3141.0 3
|
294
|
+
# 0.03141 4
|
295
|
+
# 31410.0 5n
|
296
|
+
# 0.003141 6
|
297
|
+
# ...
|
298
|
+
#
|
299
|
+
# Default cost of prefixes could be eg.
|
300
|
+
#
|
301
|
+
# unit representation: cost:
|
302
|
+
# U 0
|
303
|
+
# dU +Infinity
|
304
|
+
# cU +Infinity
|
305
|
+
# mU 1
|
306
|
+
# dkU +Infinity
|
307
|
+
# hU +Infinity
|
308
|
+
# kU 1
|
309
|
+
# µU 2
|
310
|
+
# MU 2
|
311
|
+
# nU 3
|
312
|
+
# GU 3
|
313
|
+
# pU 4
|
314
|
+
# TU 4
|
315
|
+
# fU 5
|
316
|
+
# PU 5
|
317
|
+
# aU 6
|
318
|
+
# EU 6
|
319
|
+
#
|
320
|
+
# Cost of exponents could be eg. their absolute value, and +1 for minus sign
|
321
|
+
#
|
322
|
+
# Same unit with two different prefixes may never be used (cost +Infinity)
|
323
|
+
#
|
324
|
+
# Afterwards, there should be cost of inconsistency. This could be implemented
|
325
|
+
# eg. as computing the first 10 possibilities for amount: 1 and giving them
|
326
|
+
# bonuses -20, -15, -11, -8, -6, -5, -4, -3, -2, -1. That would further reduce the variability of the
|
327
|
+
# unit representations.
|
328
|
+
#
|
329
|
+
# Commenting again upon default cost of prefixes, prefixes before second:
|
330
|
+
#
|
331
|
+
# prefix: cost:
|
332
|
+
# s 0
|
333
|
+
# ms 4
|
334
|
+
# ns 5
|
335
|
+
# ps 6
|
336
|
+
# fs 7
|
337
|
+
# as 9
|
338
|
+
# ks +Infinity
|
339
|
+
# Ms +Infinity
|
340
|
+
# ...
|
341
|
+
#
|
342
|
+
# Prefixes before metre
|
343
|
+
#
|
344
|
+
# prefix: cost:
|
345
|
+
# m 0
|
346
|
+
# mm 2
|
347
|
+
# µm 2
|
348
|
+
# nm 3
|
349
|
+
# km 3
|
350
|
+
# Mm +Infinity
|
351
|
+
# ...
|
352
|
+
#
|
353
|
+
|
354
|
+
# number, unit_presentation = choice
|
355
|
+
|
356
|
+
begin
|
357
|
+
|
358
|
+
un = unit.short || unit.name
|
359
|
+
|
360
|
+
if un then
|
361
|
+
number = self.in unit
|
362
|
+
number_ς = number_format % number
|
363
|
+
|
364
|
+
prefix = ''
|
365
|
+
exp = 1
|
366
|
+
# unit_presentation = prefix, unit, exp
|
367
|
+
|
368
|
+
unit_ς = SY::SPS.( [ "#{prefix}#{unit.short}" ], [ exp ] )
|
369
|
+
|
370
|
+
[ number_ς, unit_ς ].join '.'
|
371
|
+
else
|
372
|
+
number = amount
|
373
|
+
# otherwise, use units of component quantities
|
374
|
+
ꜧ = quantity.composition.to_hash
|
375
|
+
symbols, exponents = ꜧ.each_with_object Hash.new do |pair, memo|
|
376
|
+
qnt, exp = pair
|
377
|
+
if qnt.standard_unit.name
|
378
|
+
std_unit = qnt.standard_unit
|
379
|
+
memo[ std_unit.short || std_unit.name ] = exp
|
380
|
+
else
|
381
|
+
m = qnt.magnitude( 1 ).to_s
|
382
|
+
memo[ m[2..-1] ] = exp
|
383
|
+
number = m[0].to_i * number
|
384
|
+
end
|
385
|
+
end.to_a.transpose
|
386
|
+
# assemble SPS
|
387
|
+
unit_ς = SY::SPS.( symbols, exponents )
|
388
|
+
# interpolate
|
389
|
+
number_ς = number_format % number
|
390
|
+
return number_ς if unit_ς == '' || unit_ς == 'unit'
|
391
|
+
[ number_ς, unit_ς ].join '.'
|
392
|
+
end
|
393
|
+
|
394
|
+
rescue
|
395
|
+
number_ς = number_format % amount
|
396
|
+
[ number_ς, "unit[#{quantity}]" ].join '.'
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
# def to_s unit=quantity.units.first, number_format='%.3g'
|
401
|
+
# begin
|
402
|
+
# return to_string( unit ) if unit and unit.abbreviation
|
403
|
+
# rescue
|
404
|
+
# end
|
405
|
+
# # otherwise, use units of basic dimensions – here be the magic:
|
406
|
+
# hsh = dimension.to_hash
|
407
|
+
# symbols, exponents = hsh.each_with_object Hash.new do |pair, memo|
|
408
|
+
# dimension_letter, exponent = pair
|
409
|
+
# std_unit = SY::Dimension.basic( dimension_letter ).standard_unit
|
410
|
+
# memo[ std_unit.abbreviation || std_unit.name ] = exponent
|
411
|
+
# end.to_a.transpose
|
412
|
+
# # assemble the superscripted product string:
|
413
|
+
# sps = SY::SPS.( symbols, exponents )
|
414
|
+
# # and finally, interpolate the string
|
415
|
+
# "#{number_format}#{sps == '' ? '' : '.' + sps}" % amount
|
416
|
+
# "#{amount}#{sps == '' ? '' : '.' + sps}"
|
417
|
+
# end
|
418
|
+
|
419
|
+
# Inspect string of the magnitude
|
420
|
+
#
|
421
|
+
def inspect
|
422
|
+
"#<#{çς}: #{self} >"
|
423
|
+
end
|
424
|
+
|
425
|
+
# Without arguments, it returns a new magnitude equal to self. If argument
|
426
|
+
# is given, it is treated as factor, by which the amount is to be muliplied.
|
427
|
+
#
|
428
|
+
def to_magnitude
|
429
|
+
magnitude( amount )
|
430
|
+
end
|
431
|
+
|
432
|
+
private
|
433
|
+
|
434
|
+
# Gives the amount of standard quantity corresponding to this magnitude,
|
435
|
+
# if such conversion is possible.
|
436
|
+
#
|
437
|
+
def to_amount_of_standard_quantity
|
438
|
+
return amount if quantity.standard?
|
439
|
+
amount * quantity.relationship.to_amount_of_standard_quantity
|
440
|
+
end
|
441
|
+
|
442
|
+
def same_dimension? other
|
443
|
+
case other
|
444
|
+
when SY::Magnitude then dimension == other.dimension
|
445
|
+
when Numeric then dimension.zero?
|
446
|
+
when SY::Quantity then dimension == other.dimension
|
447
|
+
when SY::Dimension then dimension == other
|
448
|
+
else
|
449
|
+
raise TErr, "The object (#{other.class} class) does not " +
|
450
|
+
"have dimension comparable to SY::Dimension defined"
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
def same_quantity? other
|
455
|
+
case other
|
456
|
+
when SY::Quantity then quantity == other
|
457
|
+
else
|
458
|
+
begin
|
459
|
+
quantity == other.quantity
|
460
|
+
rescue NoMethodError
|
461
|
+
raise TErr, "#{other} does not have quantity!"
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
# The engine for constructing #to_s strings.
|
467
|
+
#
|
468
|
+
def construct_to_s( named_unit=default_named_unit,
|
469
|
+
number_format=default_amount_format )
|
470
|
+
name = named_unit.name.tE "must exist", "the unit name"
|
471
|
+
abbrev_or_name = named_unit.short || name
|
472
|
+
"#{number_format}.#{ str == '' ? unit : str }" %
|
473
|
+
numeric_value_in( unit )
|
474
|
+
end
|
475
|
+
|
476
|
+
def to_s_with_unit_using_abbreviation named_unit=default_named_unit
|
477
|
+
"%s.#{named_unit.abbreviation}"
|
478
|
+
end
|
479
|
+
|
480
|
+
def to_s_with_unit_using_name
|
481
|
+
# FIXME
|
482
|
+
end
|
483
|
+
|
484
|
+
# Error complaint about incompatible dimensions.
|
485
|
+
#
|
486
|
+
def dim_complaint obj1=self, obj2
|
487
|
+
"#{obj1} not of the same dimension as #{obj2}!"
|
488
|
+
end
|
489
|
+
|
490
|
+
# String describing this class.
|
491
|
+
#
|
492
|
+
def çς
|
493
|
+
"Magnitude"
|
494
|
+
end
|
495
|
+
|
496
|
+
# Default named unit to be used in expressing this magnitude.
|
497
|
+
#
|
498
|
+
def default_named_unit
|
499
|
+
standard_unit
|
500
|
+
end
|
501
|
+
|
502
|
+
# Default format string for expressing the amount of this magnitude.
|
503
|
+
#
|
504
|
+
def default_amount_format
|
505
|
+
"%.#{amount_formatting_precision}g"
|
506
|
+
end
|
507
|
+
|
508
|
+
def amount_formatting_precision
|
509
|
+
@amount_formatting_precision ||= default_amount_formatting_precision
|
510
|
+
end
|
511
|
+
|
512
|
+
def default_amount_formatting_precision
|
513
|
+
3
|
514
|
+
end
|
515
|
+
end
|