unit_measurements_us_complete 5.17.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.
Files changed (91) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +31 -0
  3. data/.github/workflows/pages.yml +31 -0
  4. data/.gitignore +12 -0
  5. data/.rspec +3 -0
  6. data/.yardopts +2 -0
  7. data/CHANGELOG.md +645 -0
  8. data/Gemfile +7 -0
  9. data/Gemfile.lock +56 -0
  10. data/LICENSE.md +21 -0
  11. data/README.md +511 -0
  12. data/Rakefile +10 -0
  13. data/lib/unit_measurements/arithmetic.rb +225 -0
  14. data/lib/unit_measurements/base.rb +177 -0
  15. data/lib/unit_measurements/cache.rb +173 -0
  16. data/lib/unit_measurements/comparison.rb +64 -0
  17. data/lib/unit_measurements/configuration.rb +64 -0
  18. data/lib/unit_measurements/conversion.rb +92 -0
  19. data/lib/unit_measurements/errors/blank_quantity_error.rb +21 -0
  20. data/lib/unit_measurements/errors/blank_unit_error.rb +21 -0
  21. data/lib/unit_measurements/errors/missing_primitive_unit_error.rb +24 -0
  22. data/lib/unit_measurements/errors/parse_error.rb +37 -0
  23. data/lib/unit_measurements/errors/primitive_unit_already_set_error.rb +22 -0
  24. data/lib/unit_measurements/errors/unit_already_defined_error.rb +37 -0
  25. data/lib/unit_measurements/errors/unit_error.rb +36 -0
  26. data/lib/unit_measurements/extras/conversion_methods.rb +87 -0
  27. data/lib/unit_measurements/extras/numeric_methods.rb +85 -0
  28. data/lib/unit_measurements/formatter.rb +58 -0
  29. data/lib/unit_measurements/math.rb +120 -0
  30. data/lib/unit_measurements/measurement.rb +539 -0
  31. data/lib/unit_measurements/normalizer.rb +155 -0
  32. data/lib/unit_measurements/parser.rb +220 -0
  33. data/lib/unit_measurements/unit.rb +243 -0
  34. data/lib/unit_measurements/unit_group.rb +297 -0
  35. data/lib/unit_measurements/unit_group_builder.rb +224 -0
  36. data/lib/unit_measurements/unit_groups/acceleration.rb +36 -0
  37. data/lib/unit_measurements/unit_groups/all.rb +56 -0
  38. data/lib/unit_measurements/unit_groups/amount_of_substance.rb +13 -0
  39. data/lib/unit_measurements/unit_groups/angular_acceleration.rb +14 -0
  40. data/lib/unit_measurements/unit_groups/angular_velocity.rb +25 -0
  41. data/lib/unit_measurements/unit_groups/area.rb +49 -0
  42. data/lib/unit_measurements/unit_groups/catalytic_activity.rb +13 -0
  43. data/lib/unit_measurements/unit_groups/density.rb +33 -0
  44. data/lib/unit_measurements/unit_groups/dynamic_viscosity.rb +22 -0
  45. data/lib/unit_measurements/unit_groups/electric_charge.rb +20 -0
  46. data/lib/unit_measurements/unit_groups/electric_conductance.rb +15 -0
  47. data/lib/unit_measurements/unit_groups/electric_current.rb +19 -0
  48. data/lib/unit_measurements/unit_groups/electric_dipole_moment.rb +13 -0
  49. data/lib/unit_measurements/unit_groups/electric_potential.rb +17 -0
  50. data/lib/unit_measurements/unit_groups/electric_quadrupole_moment.rb +14 -0
  51. data/lib/unit_measurements/unit_groups/electrical_capacitance.rb +15 -0
  52. data/lib/unit_measurements/unit_groups/electrical_elastance.rb +13 -0
  53. data/lib/unit_measurements/unit_groups/electrical_inductance.rb +15 -0
  54. data/lib/unit_measurements/unit_groups/electrical_resistance.rb +16 -0
  55. data/lib/unit_measurements/unit_groups/energy.rb +58 -0
  56. data/lib/unit_measurements/unit_groups/force.rb +47 -0
  57. data/lib/unit_measurements/unit_groups/frequency.rb +16 -0
  58. data/lib/unit_measurements/unit_groups/illuminance.rb +18 -0
  59. data/lib/unit_measurements/unit_groups/information_entropy.rb +15 -0
  60. data/lib/unit_measurements/unit_groups/kinetic_viscosity.rb +18 -0
  61. data/lib/unit_measurements/unit_groups/length.rb +67 -0
  62. data/lib/unit_measurements/unit_groups/luminance.rb +21 -0
  63. data/lib/unit_measurements/unit_groups/luminous_flux.rb +11 -0
  64. data/lib/unit_measurements/unit_groups/luminous_intensity.rb +13 -0
  65. data/lib/unit_measurements/unit_groups/magnetic_field.rb +13 -0
  66. data/lib/unit_measurements/unit_groups/magnetic_flux.rb +15 -0
  67. data/lib/unit_measurements/unit_groups/magnetic_induction.rb +13 -0
  68. data/lib/unit_measurements/unit_groups/magnetomotive_force.rb +13 -0
  69. data/lib/unit_measurements/unit_groups/mass_flow_rate.rb +49 -0
  70. data/lib/unit_measurements/unit_groups/plane_angle.rb +30 -0
  71. data/lib/unit_measurements/unit_groups/power.rb +54 -0
  72. data/lib/unit_measurements/unit_groups/pressure.rb +60 -0
  73. data/lib/unit_measurements/unit_groups/quantity.rb +14 -0
  74. data/lib/unit_measurements/unit_groups/radiation_absorbed_dose.rb +14 -0
  75. data/lib/unit_measurements/unit_groups/radiation_equivalent_dose.rb +13 -0
  76. data/lib/unit_measurements/unit_groups/radiation_exposure.rb +15 -0
  77. data/lib/unit_measurements/unit_groups/radioactivity.rb +14 -0
  78. data/lib/unit_measurements/unit_groups/solid_angle.rb +18 -0
  79. data/lib/unit_measurements/unit_groups/sound_level.rb +13 -0
  80. data/lib/unit_measurements/unit_groups/temperature.rb +19 -0
  81. data/lib/unit_measurements/unit_groups/time.rb +29 -0
  82. data/lib/unit_measurements/unit_groups/torque.rb +40 -0
  83. data/lib/unit_measurements/unit_groups/velocity.rb +37 -0
  84. data/lib/unit_measurements/unit_groups/volume.rb +67 -0
  85. data/lib/unit_measurements/unit_groups/volumetric_flow_rate.rb +35 -0
  86. data/lib/unit_measurements/unit_groups/weight.rb +55 -0
  87. data/lib/unit_measurements/version.rb +8 -0
  88. data/lib/unit_measurements.rb +7 -0
  89. data/unit_measurements.gemspec +43 -0
  90. data/units.md +843 -0
  91. metadata +216 -0
@@ -0,0 +1,539 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # -*- frozen_string_literal: true -*-
3
+ # -*- warn_indent: true -*-
4
+
5
+ module UnitMeasurements
6
+ # @abstract
7
+ # The +UnitMeasurements::Measurement+ is the abstract class and serves as superclass
8
+ # for all the unit groups. It includes several modules that provide mathematical
9
+ # operations, comparison, conversion, formatting, and other functionalities.
10
+ #
11
+ # This class provides a comprehensive set of methods and functionality for working
12
+ # with measurements in different units. It includes robust error handling and
13
+ # supports conversion between units. Additionally, it ensures that measurements
14
+ # are consistently represented.
15
+ #
16
+ # You should not directly initialize a +Measurement+ instance. Instead, create
17
+ # specialized measurement types by subclassing +Measurement+ and providing
18
+ # specific units and conversions through the +build+ method defined in the
19
+ # +UnitMeasurements+ module.
20
+ #
21
+ # @see Arithmetic
22
+ # @see Comparison
23
+ # @see Conversion
24
+ # @see Formatter
25
+ # @see Math
26
+ # @see NumericMethods
27
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
28
+ # @since 1.0.0
29
+ class Measurement
30
+ include Arithmetic
31
+ include Comparison
32
+ include Conversion
33
+ include Formatter
34
+ include Math
35
+
36
+ extend NumericMethods
37
+ extend ConversionMethods
38
+
39
+ # Regular expression to match conversion strings.
40
+ #
41
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
42
+ # @since 1.0.0
43
+ CONVERSION_STRING_REGEXP = /(.+?)\s?(?:\s+(?:in|to|as)\s+(.+)|\z)/i.freeze
44
+
45
+ # Quantity of the measurement.
46
+ #
47
+ # @return [Numeric] Quantity of the measurement.
48
+ #
49
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
50
+ # @since 1.0.0
51
+ attr_reader :quantity
52
+
53
+ # The unit associated with the measurement.
54
+ #
55
+ # @return [Unit] The +unit+ instance associated with the measurement.
56
+ #
57
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
58
+ # @since 1.0.0
59
+ attr_reader :unit
60
+
61
+ # Initializes a new instance of +Measurement+ with a specified +quantity+
62
+ # and +unit+.
63
+ #
64
+ # This method is intended to be overridden by subclasses and serves as a
65
+ # placeholder for common initialization logic. It raises an error if called
66
+ # directly on the abstract +Measurement+ class.
67
+ #
68
+ # @example Initializing the measurement with scientific number and unit:
69
+ # UnitMeasurements::Length.new(BigDecimal(2), "km")
70
+ # => 2.0 km
71
+ #
72
+ # UnitMeasurements::Length.new("2e+2", "km")
73
+ # => 200.0 km
74
+ #
75
+ # UnitMeasurements::Length.new("2e²", "km")
76
+ # => 200.0 km
77
+ #
78
+ # UnitMeasurements::Length.new("2e⁻²", "km")
79
+ # => 0.02 km
80
+ #
81
+ # @example Initializing the measurement with complex number and unit:
82
+ # UnitMeasurements::Length.new(Complex(2, 3), "km")
83
+ # => 2+3i km
84
+ #
85
+ # UnitMeasurements::Length.new("2+3i", "km")
86
+ # => 2.0+3.0i km
87
+ #
88
+ # @example Initializing the measurement with rational or mixed rational number and unit:
89
+ # UnitMeasurements::Length.new(Rational(2, 3), "km")
90
+ # => 0.6666666666666666 km
91
+ #
92
+ # UnitMeasurements::Length.new(2/3r, "km")
93
+ # => 2/3 km
94
+ #
95
+ # UnitMeasurements::Length.new("2/3", "km")
96
+ # => 0.6666666666666666 km
97
+ #
98
+ # UnitMeasurements::Length.new("½", "km")
99
+ # => 0.5 km
100
+ #
101
+ # UnitMeasurements::Length.new("2 ½", "km")
102
+ # => 2.5 km
103
+ #
104
+ # @example Initializing the measurement with ratio and unit:
105
+ # UnitMeasurements::Length.new("1:2", "km")
106
+ # => 0.5 km
107
+ #
108
+ # @param [Numeric|String] quantity The quantity of the measurement.
109
+ # @param [String|Unit] unit The unit of the measurement.
110
+ #
111
+ # @raise [BlankQuantityError] If +quantity+ is blank.
112
+ # @raise [BlankUnitError] If +unit+ is blank.
113
+ #
114
+ # @see BlankQuantityError
115
+ # @see BlankUnitError
116
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
117
+ # @since 1.0.0
118
+ def initialize(quantity, unit)
119
+ raise BlankQuantityError if quantity.blank?
120
+ raise BlankUnitError if unit.blank?
121
+
122
+ @quantity = convert_quantity(quantity)
123
+ @unit = unit_from_unit_or_name!(unit)
124
+ end
125
+
126
+ # Converts the measurement to a +target_unit+ and returns new instance of the
127
+ # measurement.
128
+ #
129
+ # When +use_cache+ value is true, conversion factor between units are checked
130
+ # in cache file of the unit group. If cached conversion factor is present in
131
+ # the cache file, it is used for conversion otherwise conversion factor is
132
+ # stored in the cache before converting the measurement to the +target_unit+.
133
+ #
134
+ # @example
135
+ # UnitMeasurements::Length.new(1, "m").convert_to("cm")
136
+ # => 100.0 cm
137
+ #
138
+ # UnitMeasurements::Length.new(1, "m").convert_to("mm", use_cache: true)
139
+ # => 1000.0 cm
140
+ #
141
+ # @param [String|Symbol] target_unit
142
+ # The target unit for conversion.
143
+ # @param [TrueClass|FalseClass] use_cache
144
+ # Indicates whether to use cached conversion factors.
145
+ #
146
+ # @return [Measurement]
147
+ # A new +Measurement+ instance with the converted +quantity+ and
148
+ # +target unit+.
149
+ #
150
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
151
+ # @since 1.0.0
152
+ def convert_to(target_unit, use_cache: false)
153
+ target_unit = unit_from_unit_or_name!(target_unit)
154
+ return self if target_unit == unit
155
+
156
+ conversion_factor = calculate_conversion_factor(target_unit, use_cache)
157
+
158
+ self.class.new((quantity * conversion_factor), target_unit)
159
+ end
160
+ alias_method :to, :convert_to
161
+ alias_method :in, :convert_to
162
+ alias_method :as, :convert_to
163
+
164
+ # Converts the measurement to a +target_unit+ and updates the current instance.
165
+ #
166
+ # @example
167
+ # UnitMeasurements::Length.new(1, "m").convert_to!("cm")
168
+ # => 100.0 cm
169
+ #
170
+ # UnitMeasurements::Length.new(1, "m").convert_to!("mm", use_cache: true)
171
+ # => 1000.0 mm
172
+ #
173
+ # @param [String|Symbol] target_unit
174
+ # The target unit for conversion.
175
+ # @param [TrueClass|FalseClass] use_cache
176
+ # Indicates whether to use cached conversion factors.
177
+ #
178
+ # @return [Measurement]
179
+ # The current +Measurement+ instance with updated +quantity+ and +unit+.
180
+ #
181
+ # @see #convert_to
182
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
183
+ # @since 1.0.0
184
+ def convert_to!(target_unit, use_cache: false)
185
+ measurement = convert_to(target_unit, use_cache: use_cache)
186
+ @quantity, @unit = measurement.quantity, measurement.unit
187
+
188
+ self
189
+ end
190
+ alias_method :to!, :convert_to!
191
+ alias_method :in!, :convert_to!
192
+ alias_method :as!, :convert_to!
193
+
194
+ # Converts the measurement to its primitive unit and returns a new instance
195
+ # of the +Measurement+.
196
+ #
197
+ # The method first retrieves the primitive unit of the unit group associated
198
+ # with the measurement. If the primitive unit is not set, it raises a
199
+ # +MissingPrimitiveUnitError+.
200
+ #
201
+ # @example
202
+ # UnitMeasurements::Length.new(1, "m").to_primitive
203
+ # => 1 m
204
+ #
205
+ # UnitMeasurements::Length.new(1, "cm").to_primitive
206
+ # => 0.01 m
207
+ #
208
+ # @param [TrueClass|FalseClass] use_cache
209
+ # Indicates whether to use cached conversion factors.
210
+ # @return [Measurement]
211
+ # A new +Measurement+ instance representing the measurement in its
212
+ # primitive unit.
213
+ #
214
+ # @raise [MissingPrimitiveUnitError]
215
+ # If the primitive unit is not set for the unit group associated with the
216
+ # measurement.
217
+ #
218
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
219
+ # @since 5.13.0
220
+ def to_primitive(use_cache: false)
221
+ primitive_unit = self.class.primitive
222
+ raise MissingPrimitiveUnitError if primitive_unit.nil?
223
+
224
+ convert_to(primitive_unit, use_cache: use_cache)
225
+ end
226
+ alias_method :in_primitive, :to_primitive
227
+ alias_method :as_primitive, :to_primitive
228
+
229
+ # Returns an object representation of the +Measurement+.
230
+ #
231
+ # @param [TrueClass|FalseClass] dump If +true+, returns the dump representation.
232
+ #
233
+ # @return [Object] An object representation of the +Measurement+.
234
+ #
235
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
236
+ # @since 1.0.0
237
+ def inspect(dump: false)
238
+ dump ? super() : to_s
239
+ end
240
+
241
+ # Returns a string representation of the +Measurement+.
242
+ #
243
+ # @return [String] A string representation of the +Measurement+.
244
+ #
245
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
246
+ # @since 1.0.0
247
+ def to_s
248
+ "#{quantity} #{unit}"
249
+ end
250
+
251
+ # Returns the +quantity+ of the +measurement+.
252
+ #
253
+ # @return [Numeric] Quantity of the measurement.
254
+ #
255
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
256
+ # @since 1.5.0
257
+ def quantity
258
+ case @quantity
259
+ when Rational
260
+ @quantity.denominator == 1 ? @quantity.numerator : @quantity
261
+ else
262
+ @quantity
263
+ end
264
+ end
265
+
266
+ class << self
267
+ extend Forwardable
268
+
269
+ # Methods delegated from the unit group.
270
+ def_delegators :unit_group, :primitive, :units, :cache_file, :unit_names,
271
+ :unit_with_name_and_aliases, :unit_names_with_aliases,
272
+ :unit_for, :unit_for!, :defined?, :unit_or_alias?, :[],
273
+ :units_for, :units_for!, :systems
274
+
275
+ # Parses an input string and returns a +Measurement+ instance depending on
276
+ # the input string. This method first normalizes the +input+ internally,
277
+ # using the +Normalizer+ before parsing it using the +Parser+.
278
+ #
279
+ # You can separate *source* and *target* units from each other in +input+
280
+ # using +to+, +in+, or +as+.
281
+ #
282
+ # If only the source unit is provided, it returns a new +Measurement+
283
+ # instance with the quantity in the source unit. If both source and target
284
+ # units are provided in the input string, it returns a new +Measurement+
285
+ # instance with the quantity converted to the target unit.
286
+ #
287
+ # @example Parsing string representing a complex number and source unit:
288
+ # UnitMeasurements::Length.parse("2+3i km")
289
+ # => 2.0+3.0i km
290
+ #
291
+ # @example Parsing string representing a complex number, source, and target units:
292
+ # UnitMeasurements::Length.parse("2+3i km in m")
293
+ # => 2000.0+3000.0i m
294
+ #
295
+ # @example Parsing string representing a rational or mixed rational number and source unit:
296
+ # UnitMeasurements::Length.parse("½ km")
297
+ # => 0.5 km
298
+ #
299
+ # UnitMeasurements::Length.parse("2/3 km")
300
+ # => 0.666666666666667 km
301
+ #
302
+ # UnitMeasurements::Length.parse("2 ½ km")
303
+ # => 2.5 km
304
+ #
305
+ # UnitMeasurements::Length.parse("2 1/2 km")
306
+ # => 2.5 km
307
+ #
308
+ # @example Parsing string representing a rational or mixed rational number, source, and target units:
309
+ # UnitMeasurements::Length.parse("½ km to m")
310
+ # => 500.0 km
311
+ #
312
+ # UnitMeasurements::Length.parse("2/3 km to m")
313
+ # => 666.666666666667 m
314
+ #
315
+ # UnitMeasurements::Length.parse("2 ½ km in m")
316
+ # => 2500.0 m
317
+ #
318
+ # UnitMeasurements::Length.parse("2 1/2 km as m")
319
+ # => 2500.0 m
320
+ #
321
+ # @example Parsing string representing a scientific number and source unit:
322
+ # UnitMeasurements::Length.parse("2e² km")
323
+ # => 200.0 km
324
+ #
325
+ # UnitMeasurements::Length.parse("2e+2 km")
326
+ # => 200.0 km
327
+ #
328
+ # UnitMeasurements::Length.parse("2e⁻² km")
329
+ # => 0.02 km
330
+ #
331
+ # @example Parsing string representing a scientific number, source, and target units:
332
+ #
333
+ # UnitMeasurements::Length.parse("2e+2 km to m")
334
+ # => 200000.0 m
335
+ #
336
+ # UnitMeasurements::Length.parse("2e⁻² km as m")
337
+ # => 20.0 m
338
+ #
339
+ # @example Parsing string representing a ratio and source unit:
340
+ # UnitMeasurements::Length.parse("1:2 km")
341
+ # => 0.5 km
342
+ #
343
+ # @example Parsing string representing a ratio, source, and target units:
344
+ # UnitMeasurements::Length.parse("1:2 km in m")
345
+ # => 500.0 m
346
+ #
347
+ # @param [String] input The input string to be parsed.
348
+ # @param [TrueClass|FalseClass] use_cache
349
+ # Indicates whether to use cached conversion factors.
350
+ #
351
+ # @return [Measurement] The +Measurement+ instance.
352
+ #
353
+ # @see Parser
354
+ # @see Normalizer
355
+ # @see CONVERSION_STRING_REGEXP
356
+ # @see ._parse
357
+ # @see #convert_to
358
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
359
+ # @since 1.0.0
360
+ def parse(input, use_cache: false)
361
+ input = Normalizer.normalize(input)
362
+ source, target = input.match(CONVERSION_STRING_REGEXP)&.captures
363
+
364
+ target ? _parse(source).convert_to(target, use_cache: use_cache) : _parse(source)
365
+ end
366
+
367
+ # Returns the +Cache+ instance for the unit group to store and retrieve
368
+ # conversion factors.
369
+ #
370
+ # @return [Cache] The +Cache+ instance.
371
+ #
372
+ # @example
373
+ # UnitMeasurements::Length.cached
374
+ # => #<UnitMeasurements::Cache:0x00007fe407249750>
375
+ #
376
+ # @see Cache
377
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
378
+ # @since 5.2.0
379
+ def cached
380
+ @cached ||= Cache.new(self)
381
+ end
382
+
383
+ # Clears the cached conversion factors of the unit group.
384
+ #
385
+ # @return [void]
386
+ #
387
+ # @example
388
+ # UnitMeasurements::Length.clear_cache
389
+ #
390
+ # @see Cache#clear_cache
391
+ #
392
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
393
+ # @since 5.2.0
394
+ def clear_cache
395
+ cached.clear_cache
396
+ end
397
+
398
+ # Calculates the ratio between two units.
399
+ #
400
+ # This method takes a source unit and a target unit, and returns the ratio
401
+ # between them as a string representation.
402
+ #
403
+ # @example Calculating the ratio between 'in' and 'ft':
404
+ # UnitMeasurements::Length.ratio("in", "ft")
405
+ # => "12.0 in/ft"
406
+ #
407
+ # UnitMeasurements::Length.ratio(UnitMeasurements::Length.unit_for("in"), "ft")
408
+ # => "12.0 in/ft"
409
+ #
410
+ # @param [Unit|String|Symbol] source_unit
411
+ # The source unit for the ratio calculation.
412
+ # @param [Unit|String|Symbol] target_unit
413
+ # The target unit for the ratio calculation.
414
+ #
415
+ # @return [String] The ratio between the source and target units.
416
+ #
417
+ # @raise [UnitError]
418
+ # If either the source unit or the target unit is not found in the unit group.
419
+ #
420
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
421
+ # @since 5.11.0
422
+ def ratio(source_unit, target_unit)
423
+ source_unit = source_unit.is_a?(Unit) ? source_unit : unit_for!(source_unit)
424
+ target_unit = target_unit.is_a?(Unit) ? target_unit : unit_for!(target_unit)
425
+
426
+ source_quantity = 1
427
+ target_quantity = new(source_quantity, target_unit).convert_to(source_unit).quantity
428
+
429
+ "#{target_quantity} #{source_unit}/#{target_unit}"
430
+ end
431
+
432
+ private
433
+
434
+ # @private
435
+ # The class attribute representing an instance of +UnitGroup+.
436
+ #
437
+ # @return [UnitGroup] An instance of +UnitGroup+.
438
+ #
439
+ # @raise [BaseError]
440
+ # If directly invoked on +Measurement+ rather than its subclasses.
441
+ #
442
+ # @see UnitGroup
443
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
444
+ # @since 1.0.0
445
+ def unit_group
446
+ raise BaseError, "`Measurement` does not have a `unit_group` instance. You cannot directly use `Measurement`. Instead, build a new unit group by calling `UnitMeasurements.build`."
447
+ end
448
+
449
+ # @private
450
+ # Parses the normalized string to return the +Measurement+ instance.
451
+ #
452
+ # @param [String] string String to be parsed.
453
+ #
454
+ # @return [Measurement] The +Measurement+ instance.
455
+ #
456
+ # @see Parser.parse
457
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
458
+ # @since 1.0.0
459
+ def _parse(string)
460
+ quantity, unit = Parser.parse(string)
461
+
462
+ new(quantity, unit)
463
+ end
464
+ end
465
+
466
+ private
467
+
468
+ # @private
469
+ # Converts the measurement quantity to a suitable format for internal use.
470
+ #
471
+ # @param [Numeric|String] quantity The quantity of the measurement.
472
+ #
473
+ # @return [Numeric] The converted quantity.
474
+ #
475
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
476
+ # @since 1.0.0
477
+ def convert_quantity(quantity)
478
+ case quantity
479
+ when Float
480
+ BigDecimal(quantity, Float::DIG)
481
+ when Integer
482
+ Rational(quantity)
483
+ when String
484
+ quantity = Normalizer.normalize(quantity)
485
+ quantity, _ = Parser.parse(quantity)
486
+
487
+ quantity
488
+ else
489
+ quantity
490
+ end
491
+ end
492
+
493
+ # @private
494
+ # Returns the +Unit+ instance associated with the +value+ provided.
495
+ #
496
+ # @param [String|Unit] value
497
+ # The value representing a unit name or +Unit+ instance.
498
+ #
499
+ # @return [Unit] The +Unit+ instance associated with +value+.
500
+ #
501
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
502
+ # @since 1.0.0
503
+ def unit_from_unit_or_name!(value)
504
+ value.is_a?(Unit) ? value : self.class.unit_for!(value)
505
+ end
506
+
507
+ # Calculates the conversion factor between the current unit and the target
508
+ # unit.
509
+ #
510
+ # If caching is enabled and a cached factor is available, it will be used.
511
+ # Otherwise, the conversion factor will be computed and, if caching is
512
+ # enabled, stored in the cache.
513
+ #
514
+ # @param [Unit] target_unit The target unit for conversion.
515
+ # @param [TrueClass|FalseClass] use_cache
516
+ # Indicates whether caching should be used.
517
+ #
518
+ # @return [Numeric] The conversion factor.
519
+ #
520
+ # @see Unit
521
+ # @see #convert_to
522
+ #
523
+ # @note If caching is enabled, the calculated conversion factor will be stored in the cache.
524
+ #
525
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
526
+ # @since 5.2.0
527
+ def calculate_conversion_factor(target_unit, use_cache)
528
+ use_cache = (UnitMeasurements.configuration.use_cache || use_cache)
529
+
530
+ if use_cache && (cached_factor = self.class.cached.get(unit.name, target_unit.name))
531
+ cached_factor
532
+ else
533
+ factor = unit.conversion_factor / target_unit.conversion_factor
534
+ self.class.cached.set(unit.name, target_unit.name, factor) if use_cache
535
+ factor
536
+ end
537
+ end
538
+ end
539
+ end
@@ -0,0 +1,155 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # -*- frozen_stringing_literal: true -*-
3
+ # -*- warn_indent: true -*-
4
+
5
+ module UnitMeasurements
6
+ # The +UnitMeasurements::Normalizer+ class defines useful methods for handling
7
+ # input strings that may contain non-standard representations of numbers.
8
+ #
9
+ # It maps special symbols of exponents and fractions to their standard
10
+ # counterparts. The class ensures consistent handling of input strings within
11
+ # the library.
12
+ #
13
+ # @see Measurement
14
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
15
+ # @since 1.0.0
16
+ class Normalizer
17
+ # A mapping of superscript numbers, plus, and minus signs to their regular
18
+ # counterparts.
19
+ #
20
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
21
+ # @since 1.0.0
22
+ EXPONENTS_SYMBOLS = {
23
+ "⁰" => "0",
24
+ "¹" => "1",
25
+ "²" => "2",
26
+ "³" => "3",
27
+ "⁴" => "4",
28
+ "⁵" => "5",
29
+ "⁶" => "6",
30
+ "⁷" => "7",
31
+ "⁸" => "8",
32
+ "⁹" => "9",
33
+ "⁺" => "+",
34
+ "⁻" => "-",
35
+ }.freeze
36
+
37
+ # A mapping of special fraction symbols to their equivalent numeric fractions.
38
+ #
39
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
40
+ # @since 1.0.0
41
+ FRACTIONS_SYMBOLS = {
42
+ "¼" => "1/4",
43
+ "½" => "1/2",
44
+ "¾" => "3/4",
45
+ "⅓" => "1/3",
46
+ "⅔" => "2/3",
47
+ "⅕" => "1/5",
48
+ "⅖" => "2/5",
49
+ "⅗" => "3/5",
50
+ "⅘" => "4/5",
51
+ "⅙" => "1/6",
52
+ "⅚" => "5/6",
53
+ "⅐" => "1/7",
54
+ "⅛" => "1/8",
55
+ "⅜" => "3/8",
56
+ "⅝" => "5/8",
57
+ "⅞" => "7/8",
58
+ "⅑" => "1/9",
59
+ "⅒" => "1/10",
60
+ "↉" => "0/3",
61
+ }.freeze
62
+
63
+ # Matches a combination of digits, optional exponent notation, and optional plus or minus sign.
64
+ #
65
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
66
+ # @since 1.0.0
67
+ EXPONENT_REGEX = /
68
+ ( # Start of the first capturing group
69
+ [\d]+ # One or more digits
70
+ [Ee]? # Match an optional 'E' or 'e' for scientific notation
71
+ [+-]? # Match an optional plus or minus sign
72
+ ) # End of the first capturing group
73
+ ( # Start of the second capturing group
74
+ #{EXPONENTS_SYMBOLS.keys.join("|")} # Match any of the exponent symbols defined in EXPONENTS_SYMBOLS
75
+ ) # End of the second capturing group
76
+ /x.freeze
77
+
78
+ # Matches special fraction symbols.
79
+ #
80
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
81
+ # @since 1.0.0
82
+ FRACTION_REGEX = /
83
+ ( # Start of the capturing group
84
+ #{FRACTIONS_SYMBOLS.keys.join("|")} # Match any of the fraction symbols defined in FRACTIONS_SYMBOLS
85
+ ) # End of the capturing group
86
+ /x.freeze
87
+
88
+ # Matches a ratio in the format of +numerator:denominator+.
89
+ #
90
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
91
+ # @since 1.0.0
92
+ RATIO_REGEX = /
93
+ ( # Start of the first capturing group
94
+ [\d]+ # One or more digits, representing the numerator
95
+ ) # End of the first capturing group
96
+ : # Match a colon (:)
97
+ ( # Start of the second capturing group
98
+ [\d]+ # One or more digits, representing the denominator
99
+ ) # End of the second capturing group
100
+ /x.freeze
101
+
102
+
103
+ class << self
104
+ # Normalizes a string containing special symbols of exponents and fractions,
105
+ # and ratio.
106
+ #
107
+ # The +normalize+ method in the +UnitMeasurements::Normalizer+ module processes
108
+ # the input string to replace special symbols with their equivalent representations,
109
+ # such as converting superscript numbers to regular numbers, special fraction
110
+ # symbols to their numeric fractions, and ratios to fractional format, and
111
+ # remove leading and trailing whitespaces from the +string+.
112
+ #
113
+ # @example Normalizing special symbol of fraction:
114
+ # UnitMeasurements::Normalizer.normalize("¾")
115
+ # => "3/4"
116
+ #
117
+ # UnitMeasurements::Normalizer.normalize(" 1¾ ")
118
+ # => "1 3/4"
119
+ #
120
+ # @example Normalizing ratio:
121
+ # UnitMeasurements::Normalizer.normalize("1:2")
122
+ # => "1/2"
123
+ #
124
+ # @example Normalizing special symbol of exponents:
125
+ # UnitMeasurements::Normalizer.normalize("1e⁺²")
126
+ # => "1e+2"
127
+ #
128
+ # UnitMeasurements::Normalizer.normalize("1e⁻²")
129
+ # => "1e-2"
130
+ #
131
+ # @param [String] string The input string to be normalized.
132
+ #
133
+ # @return [String] The normalized version of the input string.
134
+ #
135
+ # @see Measurement
136
+ # @author {Harshal V. Ladhe}[https://shivam091.github.io/]
137
+ # @since 1.0.0
138
+ def normalize(string)
139
+ string.dup.tap do |str|
140
+ if str =~ Regexp.new(EXPONENT_REGEX)
141
+ EXPONENTS_SYMBOLS.each do |search, replace|
142
+ str.gsub!(search) { "#{replace}" }
143
+ end
144
+ end
145
+
146
+ str.gsub!(FRACTION_REGEX) { " #{FRACTIONS_SYMBOLS[$1]}" }
147
+
148
+ str.gsub!(RATIO_REGEX) { "#{$1.to_i}/#{$2.to_i}" }
149
+
150
+ str.strip!
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end