sy 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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