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