sy 2.0.2 → 2.0.4

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.
data/lib/sy/measure.rb ADDED
@@ -0,0 +1,135 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Represents a certain way, that a quantity <em>measures</em> another quantity
3
+ # (reference quantity). Instance has two attributes:
4
+ #
5
+ # * @r - read closure, for converting from the measured reference quantity.
6
+ # * @w - write closure, for converting back to the reference quantity.
7
+ #
8
+ # Convenience methods #read and #write facilitate their use.
9
+ #
10
+ class SY::Measure
11
+ class << self
12
+ # Identity measure.
13
+ #
14
+ def identity
15
+ simple_scale 1
16
+ end
17
+
18
+ # Simple scaling measure. (Eg. pounds vs kilograms)
19
+ #
20
+ def simple_scale scale
21
+ new( ratio: scale )
22
+ end
23
+
24
+ # Simple offset measure. (Such as °C)
25
+ #
26
+ def simple_offset offset
27
+ new( r: lambda { |ref_amnt| ref_amnt - offset },
28
+ w: lambda { |amnt| amnt + offset } )
29
+ end
30
+
31
+ # Linear (scaled offset) measure. Expects a hash with 2 points demonstrating
32
+ # the relationship: { ref_amount_1 => amount_1, ref_amount_2 => amount_2 }.
33
+ # (Example: Fahrenheit degrees vs. Kelvins.)
34
+ #
35
+ def linear hsh
36
+ ref_amnt_1, amnt_1, ref_amnt_2, amnt_2 = hsh.to_a.flatten
37
+ scale = ( ref_amnt_2 - ref_amnt_1 ) / ( amnt_2 - amnt_1 )
38
+ new( r: lambda { |ref_amnt| amnt_1 + ( ref_amnt - ref_amnt_1 ) / scale },
39
+ w: lambda { |amnt| ref_amnt_1 + ( amnt - amnt_1 ) * scale } )
40
+ end
41
+
42
+ # Logarithmic.
43
+ #
44
+ def logarithmic base=Math::E
45
+ new( r: lambda { |ref_amnt| Math.log ref_amnt, base },
46
+ w: lambda { |amnt| base ** amnt } )
47
+ end
48
+
49
+ # Negative logarithmic.
50
+ #
51
+ def negative_logarithmic base=Math::E
52
+ new( r: lambda { |ref_amnt| -Math.log( ref_amnt, base ) },
53
+ w: lambda { |amnt| base ** ( -amnt ) } )
54
+ end
55
+ end
56
+
57
+ attr_reader :r, :w, :ratio
58
+
59
+ # The constructor expects :r and :w arguments for read and write closure.
60
+ #
61
+ def initialize( ratio: nil, r: nil, w: nil )
62
+ if ratio.nil?
63
+ fail TypeError, ":r and :w arguments must both be closures if ratio " +
64
+ "not given!" unless r.is_a?( Proc ) && w.is_a?( Proc )
65
+ @ratio, @r, @w = nil, r, w
66
+ else
67
+ fail ArgumentError, ":r or :w must not be given if :ratio given!" if r || w
68
+ @ratio = ratio
69
+ @r = lambda { |ref_amnt| ref_amnt / ratio }
70
+ @w = lambda { |amnt| amnt * ratio }
71
+ end
72
+ end
73
+
74
+ # Convenience method to read a magnitude of a reference quantity.
75
+ #
76
+ def read magnitude_of_reference_quantity, quantity
77
+ quantity.magnitude r.( magnitude_of_reference_quantity.amount )
78
+ end
79
+
80
+ # Convenience method to convert a magnitude back to the reference quantity.
81
+ #
82
+ def write magnitude, reference_quantity
83
+ reference_quantity.magnitude w.( magnitude.amount )
84
+ end
85
+
86
+ # Inverse measure.
87
+ #
88
+ def inverse
89
+ if ratio.nil? then
90
+ self.class.new( r: w, w: r ) # swap closures
91
+ else
92
+ self.class.new( ratio: 1 / ratio )
93
+ end
94
+ end
95
+
96
+ # Measure composition (like f * g function composition).
97
+ #
98
+ def * other
99
+ if ratio.nil? then
100
+ r1, r2, w1, w2 = r, other.r, w, other.w
101
+ self.class.new( r: lambda { |ref_amnt| r1.( r2.( ref_amnt ) ) },
102
+ w: lambda { |amnt| w2.( w1.( amnt ) ) } )
103
+ else
104
+ self.class.new( ratio: ratio * other.ratio )
105
+ end
106
+ end
107
+
108
+ # Measure composition with inverse of another measure.
109
+ #
110
+ def / other
111
+ self * other.inverse
112
+ end
113
+
114
+ # Measure power.
115
+ #
116
+ def ** n
117
+ if ratio.nil? then
118
+ r_closure, w_closure = r, w
119
+ self.class.new( r: lambda { |ref_amnt|
120
+ n.times.inject ref_amnt do |m, _| r_closure.( m ) end
121
+ },
122
+ w: lambda { |amnt|
123
+ n.times.inject amnt do |m, _| w_closure.( m ) end
124
+ } )
125
+ else
126
+ self.class.new( ratio: ratio ** n )
127
+ end
128
+ end
129
+
130
+ protected
131
+
132
+ def []( *args )
133
+ send *args
134
+ end
135
+ end # class SY::Measure
data/lib/sy/quantity.rb CHANGED
@@ -12,7 +12,7 @@ class SY::Quantity
12
12
  RELATIVE_QUANTITY_NAME_SUFFIX = "±"
13
13
 
14
14
  attr_reader :MagnitudeModule, :Magnitude, :Unit
15
- attr_reader :dimension, :composition, :mapping
15
+ attr_reader :dimension, :composition
16
16
 
17
17
  class << self
18
18
  # Dimension-based quantity constructor. Examples:
@@ -23,10 +23,10 @@ class SY::Quantity
23
23
  ꜧ = args.extract_options!
24
24
  dim = case args.size
25
25
  when 0 then
26
- ꜧ.must_have :dimension, syn!: :of
26
+ ꜧ.must_have :dimension
27
27
  ꜧ.delete :dimension
28
28
  else args.shift end
29
- args << ꜧ.merge!( dimension: SY::Dimension.new( dim ) )
29
+ args << ꜧ.merge!( of: SY::Dimension.new( dim ) )
30
30
  return new( *args ).protect!
31
31
  end
32
32
 
@@ -36,14 +36,10 @@ class SY::Quantity
36
36
  # <tt>Quantity.standard of: "L.T⁻²"
37
37
  # (Both should give Acceleration as their result.)
38
38
  #
39
- def standard *args
40
- = args.extract_options!
41
- dim = case args.size
42
- when 0 then
43
- ꜧ.must_have :dimension, syn!: :of
44
- ꜧ.delete :dimension
45
- else args.shift end
46
- return SY.Dimension( dim ).standard_quantity
39
+ def standard( of: nil )
40
+ fail ArgumentError, "Dimension (:of argument) must be given!" if of.nil?
41
+ puts "Constructing standard quantity of #{of} dimension" if SY::DEBUG
42
+ return SY.Dimension( of ).standard_quantity
47
43
  end
48
44
 
49
45
  # Dimensionless quantity constructor alias.
@@ -52,7 +48,7 @@ class SY::Quantity
52
48
  ꜧ = args.extract_options!
53
49
  raise TErr, "Dimension not zero!" unless ꜧ[:dimension].zero? if
54
50
  ꜧ.has? :dimension, syn!: :of
55
- new( *( args << ꜧ.merge!( dimension: SY::Dimension.zero ) ) ).protect!
51
+ new( *( args << ꜧ.merge!( of: SY::Dimension.zero ) ) ).protect!
56
52
  end
57
53
  alias :zero :dimensionless
58
54
  end
@@ -60,21 +56,26 @@ class SY::Quantity
60
56
  # Standard constructor of a metrological quantity. A quantity may have
61
57
  # a name and a dimension.
62
58
  #
63
- def initialize args
64
- puts "Quantity init #{args}" if SY::DEBUG
65
- @relative = args[:relative]
66
- comp = args[:composition]
67
- if comp.nil? then # composition not given
68
- puts "Composition not received, dimension expected." if SY::DEBUG
69
- dim = args[:dimension] || args[:of]
70
- @dimension = SY.Dimension( dim )
59
+ def initialize( relative: nil, composition: nil, of: nil, measure: nil, amount: nil, **nn )
60
+ puts "Quantity init relative: #{relative}, composition: #{composition}, measure: #{measure}, #{nn}" if SY::DEBUG
61
+ @relative = relative
62
+ if composition.nil? then
63
+ puts "Composition not given, dimension expected." if SY::DEBUG
64
+ @dimension = SY.Dimension( of )
71
65
  else
72
- puts "Composition received (#{comp})." if SY::DEBUG
73
- @composition = SY::Composition[ comp ]
66
+ puts "Composition received (#{composition})." if SY::DEBUG
67
+ @composition = SY::Composition[ composition ]
74
68
  @dimension = @composition.dimension
75
69
  end
76
- rel = args[:mapping] || args[:ratio]
77
- @mapping = SY::Mapping.new( rel ) if rel
70
+ @measure = measure.is_a?( SY::Measure ) ? measure :
71
+ if measure.nil? then
72
+ if amount.nil? then nil else
73
+ SY::Measure.simple_scale( amount )
74
+ end
75
+ else
76
+ fail ArgumentError, ":amount and :measure shouldn't be both supplied" unless amount.nil?
77
+ SY::Measure.simple_scale( measure )
78
+ end
78
79
  puts "Composition of the initialized instance is #{composition}." if SY::DEBUG
79
80
  end
80
81
 
@@ -132,56 +133,52 @@ class SY::Quantity
132
133
  "match the dimension" do |comp| comp.dimension == dimension end
133
134
  end
134
135
 
135
- # Acts as mapping setter.
136
+ # Acts as setter of measure (of the pertinent standard quantity).
136
137
  #
137
- def set_mapping mapping
138
- @mapping = SY::Mapping.new( mapping )
138
+ def set_measure measure
139
+ @measure = if measure.is_a?( SY::Measure ) then
140
+ measure
141
+ else
142
+ SY::Measure.simple_scale( measure )
143
+ end
139
144
  end
140
145
 
141
- def import magnitude2
142
- quantity2, amount2 = magnitude2.quantity, magnitude2.amount
143
- magnitude mapping_to( quantity2 ).im.( amount2 )
146
+ # Converts magnitude of another quantity to a magnitude of this quantity.
147
+ #
148
+ def read magnitude_of_other_quantity
149
+ other_quantity = magnitude_of_other_quantity.quantity
150
+ other_amount = magnitude_of_other_quantity.amount
151
+ magnitude measure( of: other_quantity ).r.( other_amount )
144
152
  end
145
153
 
146
- def export amount1, quantity2
147
- mapping_to( quantity2 ).export( magnitude( amount1 ), quantity2 )
154
+ # Converts an amount of this quantity to a magnitude of other quantity.
155
+ #
156
+ def write amount_of_this_quantity, other_quantity
157
+ measure( of: other_quantity )
158
+ .write( magnitude( amount_of_this_quantity ), other_quantity )
148
159
  end
149
160
 
150
- # Asks for a mapping of this quantity to another quantity.
161
+ # Creates a measure of a specified other quantity. If no :of is specified,
162
+ # simply acts as a getter of @measure attribute.
151
163
  #
152
- def mapping_to( q2 )
153
- puts "#{self.inspect} asked about mapping to #{q2}" if SY::DEBUG
154
- return SY::Mapping.identity if q2 == self or q2 == colleague
155
- puts "this mapping is not an identity" if SY::DEBUG
156
- raise SY::DimensionError, "#{self} vs. #{q2}!" unless same_dimension? q2
157
- if standardish? then
158
- puts "#{self} is a standardish quantity, will invert the #{q2} mapping" if SY::DEBUG
159
- return q2.mapping_to( self ).inverse
160
- end
161
- puts "#{self} is not a standardish quantity" if SY::DEBUG
162
- m1 = begin
163
- if @mapping then
164
- puts "#{self} has @mapping defined" if SY::DEBUG
165
- @mapping
166
- elsif colleague.mapping then
167
- puts "#{colleague} has @mapping defined" if SY::DEBUG
168
- colleague.mapping
169
- else
170
- puts "Neither #{self} nor its colleague has @mapping defined" if SY::DEBUG
171
- puts "Will ask #{self}.composition to infer the mapping" if SY::DEBUG
172
- composition.infer_mapping
173
- end
174
- rescue NoMethodError
175
- raise SY::QuantityError,"Mapping from #{self} to #{q2} cannot be inferred!"
176
- end
177
- if q2.standardish? then
178
- puts "#{q2} is standardish, obtained mapping can be returned directly." if SY::DEBUG
179
- return m1
180
- else
181
- puts "#{q2} not standardish, obtained mapping maps only to #{standard}, and " +
182
- "therefrom, composition with mapping from #{standard} to #{q2} will be needed" if SY::DEBUG
183
- return m1 * standard.mapping_to( q2 )
184
- end
164
+ def measure( of: nil )
165
+ return @measure if of.nil? # act as simple getter if :of not specified
166
+ puts "#{self.inspect} asked about measure of #{of}" if SY::DEBUG
167
+ return SY::Measure.identity if of == self or of == colleague
168
+ raise SY::DimensionError, "#{self} vs. #{of}!" unless same_dimension? of
169
+ return of.measure( of: of.standard ).inverse if standardish?
170
+ m = begin
171
+ puts "composition is #{composition}, class #{composition.class}" if SY::DEBUG
172
+ measure ||
173
+ colleague.measure ||
174
+ composition.infer_measure
175
+ rescue NoMethodError
176
+ fail SY::QuantityError, "Measure of #{of} by #{self} impossible!"
177
+ end
178
+ return m if of.standardish?
179
+ puts "#{of} not standardish, obtained measure relates to #{standard}, and " +
180
+ "it will have to be extended to #{of}." if SY::DEBUG
181
+ return m * standard.measure( of: of )
185
182
  end
186
183
 
187
184
  # Is the quantity relative?
@@ -216,9 +213,9 @@ class SY::Quantity
216
213
  same_dimension? q2
217
214
  raise SY::QuantityError, "#{self} an #{q2} are both " +
218
215
  "{relative? ? 'relative' : 'absolute'}!" if relative? == q2.relative?
219
- if mapping && q2.mapping then
220
- raise SY::QuantityError, "Mapping mismatch: #{self}, #{q2}!" unless
221
- mapping == q2.mapping
216
+ if measure && q2.measure then
217
+ raise SY::QuantityError, "Measure mismatch: #{self}, #{q2}!" unless
218
+ measure == q2.measure
222
219
  end
223
220
  @colleague = q2
224
221
  q2.instance_variable_set :@colleague, self
@@ -244,31 +241,38 @@ class SY::Quantity
244
241
 
245
242
  # Constructs a new absolute magnitude of this quantity.
246
243
  #
247
- def magnitude arg
248
- Magnitude().new quantity: self, amount: arg
244
+ def magnitude amount
245
+ Magnitude().new of: self, amount: amount
249
246
  end
250
247
 
251
248
  # Constructs a new unit of this quantity.
252
249
  #
253
- def unit args={}
254
- Unit().new( args.merge( quantity: self ) ).tap { |u| ( units << u ).uniq! }
250
+ def unit **nn
251
+ Unit().new( nn.update( of: self ) ).tap { |u| ( units << u ).uniq! }
255
252
  end
256
253
 
257
- # Constructor of a new standard unit (replacing the current @standard_unit).
258
- # For standard units, amount is implicitly 1. So :amount name argument, when
259
- # supplied, has a different meaning – sets the mapping of its quantity.
254
+ # Constructor of a new standard unit (replacing current @standard_unit).
255
+ # For standard units, amount is implicitly 1. So :amount argument here has
256
+ # different meaning – it sets the measure of the quantity. Measure can also
257
+ # be specified more explicitly by :measure named argument.
260
258
  #
261
- def new_standard_unit args={}
262
- explain_amount_of_standard_units if args[:amount].is_a? Numeric # n00b help
259
+ def new_standard_unit( amount: nil, measure: nil, **nn )
260
+ explain_amount_of_standard_units if amount.is_a? Numeric # n00b help
263
261
  # For standard units, amount has special meaning of setting up mapping.
264
- args.may_have( :mapping, syn!: :amount )
265
- = args.delete( :mapping )
266
- set_mapping( ᴍ.amount ) if ᴍ
267
- args.update amount: 1 # substitute amount 1 as required for standard units
262
+ if measure then
263
+ raise ArgumentError, "When :measure is specified, :amount must not be " +
264
+ "expliticly specified." unless amount.nil?
265
+ raise TypeError, ":measure argument must be a SY::Measure!" unless
266
+ measure.is_a? SY::Measure
267
+ set_measure( measure )
268
+ else
269
+ set_measure( SY::Measure.simple_scale( amount.nil? ? 1 : amount.amount ) )
270
+ end
268
271
  # Replace @standard_unit with the newly constructed unit.
269
272
  Unit().instance_variable_set( :@standard,
270
- unit( args )
271
- .tap { |u| ( units.unshift u ).uniq! } )
273
+ unit( **nn ).tap do |u|
274
+ ( units.unshift u ).uniq!
275
+ end )
272
276
  end
273
277
 
274
278
  # Quantity multiplication.
@@ -311,6 +315,8 @@ class SY::Quantity
311
315
  # Returns the standard quantity for this quantity's dimension.
312
316
  #
313
317
  def standard
318
+ puts "Dimension of this quantity is #{dimension}" if SY::DEBUG
319
+ puts "Its standard quantity is #{dimension.standard_quantity}" if SY::DEBUG
314
320
  dimension.standard_quantity
315
321
  end
316
322
 
@@ -363,7 +369,7 @@ class SY::Quantity
363
369
  #
364
370
  def MagnitudeModule
365
371
  @MagnitudeModule ||= if absolute? then
366
- Module.new { include SY::Magnitude }
372
+ Module.new { include ::SY::Magnitude }
367
373
  else
368
374
  absolute.MagnitudeModule
369
375
  end
@@ -372,26 +378,25 @@ class SY::Quantity
372
378
  # Parametrized magnitude class.
373
379
  #
374
380
  def Magnitude
375
- if @Magnitude then @Magnitude else
376
- mmod = MagnitudeModule()
377
- mixin = relative? ? SY::SignedMagnitude : SY::AbsoluteMagnitude
378
- qnt_ɴ_λ = -> { name ? "#{name}@%s" : "#<Quantity:#{object_id}@%s>" }
379
-
380
- @Magnitude = Class.new do
381
- include mmod
382
- include mixin
383
-
384
- singleton_class.class_exec do
385
- define_method :zero do # Costructor of zero magnitudes
386
- new amount: 0
381
+ @Magnitude or
382
+ ( mmod = MagnitudeModule()
383
+ mixin = relative? ? ::SY::SignedMagnitude : ::SY::AbsoluteMagnitude
384
+ qnt_ɴ_λ = -> { name ? "#{name}@%s" : "#<Quantity:#{object_id}@%s>" }
385
+
386
+ @Magnitude = Class.new do
387
+ include mmod
388
+ include mixin
389
+
390
+ singleton_class.class_exec do
391
+ define_method :zero do # Costructor of zero magnitudes
392
+ new amount: 0
393
+ end
394
+
395
+ define_method :to_s do # Customized #to_s. It must be a proc,
396
+ qnt_ɴ_λ.call % "Magnitude" # since the quantity owning @Magnitude
397
+ end # might not be named yet as of now.
387
398
  end
388
-
389
- define_method :to_s do # Customized #to_s. It must be a proc,
390
- qnt_ɴ_λ.call % "Magnitude" # since the quantity owning @Magnitude
391
- end # might not be named yet as of now.
392
- end
393
- end
394
- end
399
+ end )
395
400
  end
396
401
 
397
402
  # Parametrized unit class.
@@ -405,12 +410,12 @@ class SY::Quantity
405
410
  include SY::Unit
406
411
 
407
412
  singleton_class.class_exec do
408
- define_method :standard do |args={}| # Customized #standard.
409
- @standard ||= new args.merge( quantity: qnt )
413
+ define_method :standard do |**nn| # Customized #standard.
414
+ @standard ||= new **nn.update( of: qnt )
410
415
  end
411
416
 
412
417
  define_method :to_s do # Customized #to_s. (Same consideration
413
- ɴλ.call % "Unit" # as for @Magnitude applies.)
418
+ ɴλ.call % "Unit" # as for @Magnitude applies.)
414
419
  end
415
420
  end
416
421
  end