phys-units 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ require "phys/units"
2
+
3
+ # Compatibility to Quanty, the former name.
4
+ # Activated by:
5
+ # requie "phys/units/quanty"
6
+ class Quanty < Phys::Quantity
7
+ end
8
+
9
+ def Quanty(*arg); Phys::Quantity(*a)
10
+ end
@@ -9,61 +9,110 @@
9
9
 
10
10
  module Phys
11
11
 
12
+ # Phys::Unit is a class to represent Physical Unit of Measure.
13
+ # It must have:
14
+ # * *Factor* of the unit. Conversion factor to +dimension+,
15
+ # i.e., its base units.
16
+ # * *Dimension* of the unit.
17
+ # Dimension is a hash table with base units and dimension values.
18
+ # Example:
19
+ # Phys::Unit["N"].dimension #=> {"kg"=>1, "m"=>1, "s"=>-2}
20
+ #== Usage
21
+ # require "phys/units"
22
+ # Q = Phys::Quantity
23
+ # U = Phys::Unit
24
+ #
25
+ # U["miles"] / U["hr"] #=> #<Phys::Unit 0.44704,{"m"=>1, "s"=>-1}>
26
+ # U["hr"] + U["30 min"] #=> #<Phys::Unit 5400,{"s"=>1}>
27
+ # U["(m/s)"]**2 #=> #<Phys::Unit 1,{"m"=>2, "s"=>-2}>
28
+ #
29
+ # case Q[1,"miles/hr"]
30
+ # when U["m"]
31
+ # "length"
32
+ # when U["s"]
33
+ # "time"
34
+ # when U["m/s"]
35
+ # "velocity"
36
+ # else
37
+ # "other"
38
+ # end #=> "velocity"
12
39
  class Unit
13
40
 
41
+ # @visibility private
14
42
  LIST = {}
43
+ # @visibility private
15
44
  PREFIX = {}
16
45
 
46
+ # @visibility private
17
47
  def self.prefix_regex
18
48
  @@prefix_regex
19
49
  end
20
50
 
21
- def initialize(arg,expr=nil,offset=nil)
51
+ # Initialize a new unit.
52
+ # @overload initialize(factor,dimension=nil)
53
+ # @param [Numeric] factor Unit conversion factor.
54
+ # @param [Hash] dimension Dimension hash.
55
+ # @overload initialize(expr,name=nil)
56
+ # @param [String] expr Unit string to be parsed later.
57
+ # @param [String] name Name of this unit.
58
+ # @overload initialize(unit,name=nil)
59
+ # @param [Phys::Unit] unit Copy contents from the argument.
60
+ # @param [String] name Name of this unit.
61
+ # @raise [TypeError] if invalit arg types.
62
+ #
63
+ def initialize(arg,extr=nil)
22
64
  case arg
23
65
  when Numeric
24
66
  arg = Rational(arg) if Integer===arg
25
67
  @factor = arg
26
- alloc_dim(expr)
68
+ alloc_dim(extr)
27
69
  when Phys::Unit
28
- replace(arg)
70
+ @factor = arg.factor
71
+ alloc_dim arg.dim
72
+ @name = extr
29
73
  when String
30
- @name = arg
31
- if expr.kind_of? Phys::Unit
32
- @factor = expr.factor
33
- @offset = expr.offset
34
- alloc_dim expr.dim
35
- else
36
- @expr = expr
37
- end
74
+ @expr = arg
75
+ @name = extr
38
76
  else
39
77
  raise TypeError,"invalid argument : #{arg.inspect}"
40
78
  end
41
79
  end
42
80
 
43
- attr_reader :name, :offset, :expr
81
+ # Unit expression to be parsed.
82
+ # @return [String, NilClass]
83
+ attr_reader :expr
84
+
85
+ # @visibility private
86
+ attr_reader :offset
87
+ # @visibility private
88
+ attr_reader :name
44
89
 
45
- def dim
90
+ # Dimension hash.
91
+ # @example
92
+ # Phys::Unit["N"].dimension #=> {"kg"=>1, "m"=>1, "s"=>-2}
93
+ # @return [Hash]
94
+ def dimension
46
95
  use_dimension
47
96
  @dim
48
97
  end
49
- alias dimension dim
98
+ alias dim dimension
50
99
 
100
+ # Conversion factor except the dimension-value.
101
+ # @return [Numeric]
51
102
  def factor
52
103
  use_dimension
53
104
  @factor
54
105
  end
55
106
 
107
+ # Dimension value. Returns PI number for pi dimension,
108
+ # otherwise returns one. see BaseUnit.
109
+ # @return [Numeric]
56
110
  def dimension_value
57
111
  1
58
112
  end
59
113
 
60
- def replace(x)
61
- @name = x.name.dup if x.name
62
- @factor = x.factor
63
- @offset = x.offset
64
- alloc_dim x.dim
65
- end
66
-
114
+ # (internal use)
115
+ # @visibility private
67
116
  def alloc_dim(hash=nil)
68
117
  case hash
69
118
  when Hash
@@ -74,32 +123,40 @@ module Phys
74
123
  @dim.default = 0
75
124
  end
76
125
 
126
+ # (internal use)
127
+ # Parse @expr string if it has not been parsed yet.
128
+ # This function must be called before access to @dim or @factor.
129
+ # @return [nil]
130
+ # @raise [UnitError] if unit parse error.
131
+ # @visibility private
77
132
  def use_dimension
78
133
  return if @dim && @factor
79
134
  if @expr && @dim.nil?
80
- puts "unit='#{@name}', parsing '#{@expr}'..." if Unit.debug
135
+ #puts "unit='#{@name}', parsing '#{@expr}'..." if Unit.debug
81
136
  unit = Unit.parse(@expr)
82
137
  case unit
83
138
  when Unit
84
139
  @dim = unit.dim
85
140
  @factor = unit.factor
86
141
  if @dim.nil? || @factor.nil?
87
- raise UnitParseError,"parse error : #{unit.inspect}"
142
+ raise UnitError,"parse error : #{unit.inspect}"
88
143
  end
89
144
  when Numeric
90
145
  @factor = unit
91
146
  alloc_dim
92
147
  else
93
- raise UnitParseError,"parse error : #{self.inspect}"
148
+ raise UnitError,"parse error : #{self.inspect}"
94
149
  end
95
150
  else
96
- raise UnitParseError,"undefined unit?: #{self.inspect}"
151
+ raise UnitError,"undefined unit?: #{self.inspect}"
97
152
  end
98
153
  end
99
154
 
155
+ # Inspect string.
156
+ # @return [String]
100
157
  def inspect
101
158
  a = [Utils.num_inspect(@factor), @dim.inspect]
102
- a << "@name="+@name.inspect if @name
159
+ #a << "@name="+@name.inspect if @name
103
160
  a << "@expr="+@expr.inspect if @expr
104
161
  a << "@offset="+@offset.inspect if @offset
105
162
  a << "@dimensionless=true" if @dimensionless
@@ -110,6 +167,8 @@ module Phys
110
167
  "#<#{self.class} #{s}>"
111
168
  end
112
169
 
170
+ # Make unit string from dimension.
171
+ # @return [String]
113
172
  def unit_string
114
173
  use_dimension
115
174
  a = []
@@ -125,8 +184,8 @@ module Phys
125
184
  end
126
185
  alias string_form unit_string
127
186
 
128
- # Unit conversion
129
-
187
+ # Conversion Factor to base unit.
188
+ # @return [Numeric]
130
189
  def conversion_factor
131
190
  use_dimension
132
191
  f = @factor
@@ -141,39 +200,77 @@ module Phys
141
200
  f
142
201
  end
143
202
 
203
+ # Returns true if scalar unit.
204
+ # *Scalar* means the unit does not have any dimension
205
+ # including dimensionless-dimension, and its factor is one.
206
+ # @return [Boolean]
144
207
  def scalar?
145
208
  use_dimension
146
209
  (@dim.nil? || @dim.empty?) && @factor==1
147
210
  end
148
211
 
212
+ # (internal use)
213
+ # @visibility private
149
214
  def dimensionless_deleted
150
215
  use_dimension
151
216
  hash = @dim.dup
152
217
  hash.delete_if{|k,v| LIST[k].dimensionless?}
153
218
  end
154
219
 
220
+ # (internal use)
155
221
  def dimensionless?
156
222
  use_dimension
157
223
  @dim.each_key.all?{|k| LIST[k].dimensionless?}
158
224
  end
159
225
 
160
- def same_dimension?(x)
161
- dimensionless_deleted == x.dimensionless_deleted
226
+ # (internal use)
227
+ # @visibility private
228
+ def same_dimension?(unit)
229
+ dimensionless_deleted == unit.dimensionless_deleted
162
230
  end
163
231
  alias same_dim? same_dimension?
164
232
 
233
+ # (internal use)
234
+ # @visibility private
235
+ # @raise [UnitError] if not dimensionless.
165
236
  def assert_dimensionless
166
237
  if !dimensionless?
167
- raise UnitConversionError,"Not dimensionless: #{self.inspect}"
238
+ raise UnitError,"Not dimensionless: #{self.inspect}"
168
239
  end
169
240
  end
170
241
 
171
- def assert_same_dimension(x)
172
- if !same_dimension?(x)
173
- raise UnitConversionError,"Different dimension: #{self.inspect} and #{x.inspect}"
242
+ # (internal use)
243
+ # @visibility private
244
+ # @raise [UnitError] if different dimensions.
245
+ def assert_same_dimension(unit)
246
+ if !same_dimension?(unit)
247
+ raise UnitError,"Different dimension: #{self.inspect} and #{unit.inspect}"
174
248
  end
175
249
  end
176
250
 
251
+ # Comformability of units. Returns true if unit conversion is allowed.
252
+ # @param [Object] x other object (unit or quantity or numeric or other)
253
+ # @return [Boolean]
254
+ def conformable?(x)
255
+ case x
256
+ when Unit
257
+ dimensionless_deleted == x.dimensionless_deleted
258
+ when Quantity
259
+ dimensionless_deleted == x.unit.dimensionless_deleted
260
+ when Numeric
261
+ dimensionless?
262
+ else
263
+ false
264
+ end
265
+ end
266
+ alias === conformable?
267
+ alias compatible? conformable?
268
+ alias conversion_allowed? conformable?
269
+
270
+ # Convert a quantity to this unit.
271
+ # @param [Phys::Quantity] quantity to be converted.
272
+ # @return [Phys::Quantity]
273
+ # @raise [UnitError] if unit conversion is failed.
177
274
  def convert(quantity)
178
275
  if Quantity===quantity
179
276
  assert_same_dimension(quantity.unit)
@@ -184,55 +281,75 @@ module Phys
184
281
  end
185
282
  end
186
283
 
284
+ # Convert a quantity to this unit only in scale.
285
+ # @param [Phys::Quantity] quantity to be converted.
286
+ # @return [Phys::Quantity]
287
+ # @raise [UnitError] if unit conversion is failed.
187
288
  def convert_scale(quantity)
188
289
  convert(quantity)
189
290
  end
190
291
 
191
- def convert_value_to_base_unit(x)
192
- x * conversion_factor
193
- end
194
-
195
- def convert_value_from_base_unit(x)
196
- x / conversion_factor
292
+ # Convert from a value in this unit to a value in base unit.
293
+ # @param [Numeric] value
294
+ # @return [Numeric]
295
+ def convert_value_to_base_unit(value)
296
+ value * conversion_factor
197
297
  end
198
298
 
199
- def convert_to_numeric(x)
200
- assert_dimensionless
201
- x * conversion_factor
299
+ # Convert from a value in base unit to a value in this unit.
300
+ # @param [Numeric] value
301
+ # @return [Numeric]
302
+ def convert_value_from_base_unit(value)
303
+ value / conversion_factor
202
304
  end
203
305
 
306
+ # Returns numeric value of this unit, i.e. conversion factor.
307
+ # Raises UnitError if not dimensionless.
308
+ # @return [Numeric]
309
+ # @raise [UnitError] if not dimensionless.
204
310
  def to_numeric
205
311
  assert_dimensionless
206
312
  conversion_factor
207
313
  end
208
314
  alias to_num to_numeric
209
315
 
210
- def convert_to_float(x)
211
- convert_to_numeric(x).to_f
212
- end
213
316
 
317
+ # Returns Base Unit excluding dimensionless-dimension.
318
+ # @return [Phys::Unit]
214
319
  def base_unit
215
320
  Unit.new(1,dimensionless_deleted)
216
321
  end
217
322
 
218
- # Unit operation
323
+ #--
219
324
 
325
+ # Return true if this unit is operable.
326
+ # @return [Boolean]
220
327
  def operable?
221
328
  true
222
329
  end
223
330
 
331
+ # Raise error if this unit is not operable.
332
+ # @return [nil]
333
+ # @visibility private
224
334
  def check_operable
225
335
  if !operable?
226
- raise UnitOperationError,"non-operable for #{inspect}"
336
+ raise UnitError,"non-operable for #{inspect}"
227
337
  end
338
+ nil
228
339
  end
229
340
 
230
- def check_operable2(x)
231
- if !(operable? && x.operable?)
232
- raise UnitOperationError,"non-operable: #{inspect} and #{x.inspect}"
341
+ # Raise error if this unit or argument is not operable.
342
+ # @return [nil]
343
+ # @visibility private
344
+ def check_operable2(unit)
345
+ if !(operable? && unit.operable?)
346
+ raise UnitError,"non-operable: #{inspect} and #{unit.inspect}"
233
347
  end
348
+ nil
234
349
  end
235
350
 
351
+ # (internal use)
352
+ # @visibility private
236
353
  def dimension_binop(other)
237
354
  x = self.dim
238
355
  y = other.dim
@@ -241,7 +358,7 @@ module Phys
241
358
  keys = x.keys | y.keys
242
359
  dims = {}
243
360
  dims.default = 0
244
- keys.each do |k|
361
+ keys.each do |k|
245
362
  v = yield( x[k]||0, y[k]||0 )
246
363
  dims[k] = v if v!=0
247
364
  end
@@ -254,6 +371,8 @@ module Phys
254
371
  end
255
372
  end
256
373
 
374
+ # (internal use)
375
+ # @visibility private
257
376
  def dimension_uop
258
377
  x = self.dim
259
378
  if Hash===x
@@ -269,6 +388,11 @@ module Phys
269
388
  end
270
389
  end
271
390
 
391
+ # Addition of units.
392
+ # Both units must be operable and conversion-allowed.
393
+ # @param [Phys::Unit, Numeric] x other unit
394
+ # @return [Phys::Unit]
395
+ # @raise [Phys::UnitError] if unit conversion is failed.
272
396
  def +(x)
273
397
  x = Unit.cast(x)
274
398
  check_operable2(x)
@@ -276,6 +400,11 @@ module Phys
276
400
  Unit.new(@factor+x.factor,@dim.dup)
277
401
  end
278
402
 
403
+ # Subtraction of units.
404
+ # Both units must be operable and conversion-allowed.
405
+ # @param [Phys::Unit, Numeric] x other unit
406
+ # @return [Phys::Unit]
407
+ # @raise [Phys::UnitError] if not conformable unit conversion is failed.
279
408
  def -(x)
280
409
  x = Unit.cast(x)
281
410
  check_operable2(x)
@@ -283,16 +412,28 @@ module Phys
283
412
  Unit.new(@factor-x.factor,@dim.dup)
284
413
  end
285
414
 
415
+ # Unary minus.
416
+ # This unit must be operable.
417
+ # @return [Phys::Unit]
418
+ # @raise [Phys::UnitError] if not operable.
286
419
  def -@
287
420
  check_operable
288
421
  use_dimension
289
422
  Unit.new(-@factor,@dim.dup)
290
423
  end
291
424
 
425
+ # Unary plus.
426
+ # Returns self.
427
+ # @return [Phys::Unit]
292
428
  def +@
293
429
  self
294
430
  end
295
431
 
432
+ # Multiplication of units.
433
+ # Both units must be operable.
434
+ # @param [Phys::Unit, Numeric] x other unit
435
+ # @return [Phys::Unit]
436
+ # @raise [Phys::UnitError] if not operable.
296
437
  def *(x)
297
438
  x = Unit.cast(x)
298
439
  if scalar?
@@ -306,10 +447,15 @@ module Phys
306
447
  Unit.new(factor,dims)
307
448
  end
308
449
 
450
+ # Division of units.
451
+ # Both units must be operable.
452
+ # @param [Phys::Unit, Numeric] x other unit
453
+ # @return [Phys::Unit]
454
+ # @raise [Phys::UnitError] if not operable.
309
455
  def /(x)
310
456
  x = Unit.cast(x)
311
457
  if scalar?
312
- return x.inv
458
+ return x.inverse
313
459
  elsif x.scalar?
314
460
  return self
315
461
  end
@@ -319,10 +465,15 @@ module Phys
319
465
  Unit.new(factor,dims)
320
466
  end
321
467
 
468
+ # Rational division of units.
469
+ # Both units must be operable.
470
+ # @param [Phys::Unit, Numeric] x other unit
471
+ # @return [Phys::Unit]
472
+ # @raise [Phys::UnitError] if not operable.
322
473
  def rdiv(x)
323
474
  x = Unit.cast(x)
324
475
  if scalar?
325
- return x.inv
476
+ return x.inverse
326
477
  elsif x.scalar?
327
478
  return self
328
479
  end
@@ -332,20 +483,32 @@ module Phys
332
483
  Unit.new(factor,dims)
333
484
  end
334
485
 
486
+ # @visibility private
335
487
  def self.rdiv(x,y)
336
488
  Unit.cast(x).rdiv(y)
337
489
  end
338
490
 
339
- def inv
491
+ # Inverse of units.
492
+ # This unit must be operable.
493
+ # @param [Phys::Unit, Numeric] unit
494
+ # @return [Phys::Unit]
495
+ # @raise [Phys::UnitError] if not operable.
496
+ def inverse
340
497
  check_operable
341
498
  dims = dimension_uop{|a| -a}
342
499
  Unit.new(Rational(1,self.factor), dims)
343
500
  end
344
501
 
345
- def self.inv(x)
346
- Unit.cast(x).inv
502
+ # @visibility private
503
+ def self.inverse(x)
504
+ Unit.cast(x).inverse
347
505
  end
348
506
 
507
+ # Exponentiation of units.
508
+ # This units must be operable.
509
+ # @param [Numeric] x numeric
510
+ # @return [Phys::Unit]
511
+ # @raise [Phys::UnitError] if not operable.
349
512
  def **(x)
350
513
  check_operable
351
514
  m = Utils.as_numeric(x)
@@ -353,18 +516,31 @@ module Phys
353
516
  Unit.new(@factor**m,dims)
354
517
  end
355
518
 
519
+ # @visibility private
356
520
  def self.func(fn, x)
357
521
  fn = 'log' if fn == 'ln'
358
522
  m = Unit.new(x).to_numeric
359
523
  Unit.new( Math.send(fn,m) )
360
524
  end
361
525
 
526
+ # Equality of units
527
+ # @param [Object] x other unit or object
528
+ # @return [Boolean]
362
529
  def ==(x)
530
+ case x
531
+ when Numeric
532
+ x = Unit.cast(x)
533
+ when Unit
534
+ else
535
+ return false
536
+ end
363
537
  use_dimension
364
- @factor == x.factor && @dim == x.dim &&
538
+ @factor == x.factor && @dim == x.dim &&
365
539
  offset == x.offset && dimension_value == x.dimension_value
366
540
  end
367
541
 
542
+ # Coerce.
543
+ # @return [Array]
368
544
  def coerce(x)
369
545
  [Unit.find_unit(x), self]
370
546
  end
@@ -372,27 +548,30 @@ module Phys
372
548
  end # Unit
373
549
 
374
550
 
551
+ # BaseUnit is a class to represent units defined by "!" in unit.dat
552
+ # including SI units.
375
553
  class BaseUnit < Unit
376
- def initialize(s,dimless=false,v=nil)
377
- case s
554
+
555
+ def self.define(name,expr,dimval=nil)
556
+ dimles = (expr == "!dimensionless")
557
+ LIST[name] = self.new(name,dimles,dimval)
558
+ end
559
+
560
+ def initialize(name,dimless=false,dimval=nil)
561
+ case name
378
562
  when String
379
- @name = s
563
+ @name = name
380
564
  @factor = 1
381
- @dim = {s=>1}
565
+ @dim = {name=>1}
382
566
  @dim.default = 0
383
567
  @dimensionless = dimless
384
- @dimension_value = v || 1
568
+ @dimension_value = dimval || 1
385
569
  else
386
570
  raise ArgumentError "BaseUnit#initialize: arg must be string: #{s}"
387
571
  end
388
572
  end
389
573
 
390
- def replace(x)
391
- super(x)
392
- @dimensionless = x.dimensionless
393
- @dimension_value = x.dimension_value
394
- end
395
-
574
+ # @visibility private
396
575
  def use_dimension
397
576
  end
398
577
 
@@ -400,6 +579,7 @@ module Phys
400
579
  @dimensionless
401
580
  end
402
581
 
582
+ # @visibility private
403
583
  def dimensionless_deleted
404
584
  if @dimensionless
405
585
  {}
@@ -408,42 +588,59 @@ module Phys
408
588
  end
409
589
  end
410
590
 
591
+ # Dimension value.
592
+ # Returns PI number for pi dimension, otherwise returns one.
593
+ # @return [Numeric]
594
+ # @example
595
+ # Phys::Unit["pi"].dimension_value #=> 3.141592653589793
411
596
  attr_reader :dimension_value
412
597
  end
413
598
 
414
599
 
600
+ # OffsetUnit is a class to represent units with offset value.
601
+ # Focused on Farenheight/Celsius temperature.
415
602
  class OffsetUnit < Unit
416
603
 
417
604
  def self.define(name,unit,offset=nil)
418
- LIST[name] = self.new(name,unit,offset)
605
+ LIST[name] = self.new(unit,name,offset)
419
606
  end
420
607
 
421
- def initialize(name,arg,offset)
422
- super(name,arg)
423
- @offset = offset
608
+ def initialize(arg,name=nil,offset=nil)
424
609
  if offset.nil?
425
610
  raise ArgumentError,"offset is not supplied"
426
611
  end
612
+ super(arg,name)
613
+ @offset = offset
427
614
  end
428
615
 
429
- def convert_value_to_base_unit(x)
430
- x * conversion_factor + @offset
431
- end
432
-
433
- def convert_value_from_base_unit(x)
434
- (x - @offset) / conversion_factor
435
- end
436
-
616
+ # Convert a quantity to this unit only in scale.
617
+ # @param [Phys::Quantity] quantity to be converted.
618
+ # @return [Phys::Quantity]
619
+ # @raise [UnitError] if unit conversion is failed.
437
620
  def convert_scale(quantity)
438
621
  if Quantity===quantity
439
- assert_same_dimension(quantity.unit)
622
+ assert_same_dimension(quantity.unit)
440
623
  v = quantity.value * quantity.unit.conversion_factor
441
624
  v = v / self.conversion_factor
442
625
  else
443
- raise TypeError,"not Quantitiy: #{quantity.inspect}"
626
+ raise UnitError,"not Quantitiy: #{quantity.inspect}"
444
627
  end
445
628
  end
446
629
 
630
+ # Convert from a value in this unit to a value in base unit.
631
+ # @param [Numeric] value
632
+ # @return [Numeric]
633
+ def convert_value_to_base_unit(value)
634
+ value * conversion_factor + @offset
635
+ end
636
+
637
+ # Convert from a value in base unit to a value in this unit.
638
+ # @param [Numeric] value
639
+ # @return [Numeric]
640
+ def convert_value_from_base_unit(value)
641
+ (value - @offset) / conversion_factor
642
+ end
643
+
447
644
  def operable?
448
645
  false
449
646
  end