sy 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.
- 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
|