unit_measurements 0.1.0 → 1.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a244f904f5d302745d9ba41a499eba18381cd6398a73fc50fbd1a4b24fbba6ec
4
- data.tar.gz: 51728722a5dba75a93982ce5e6bbff4e064546698992c2dc7dcdef63d20bc3a2
3
+ metadata.gz: 24b2504d7feb77f54138260f7e0947bd86e00225dbe3c6d9e575da3050a24eeb
4
+ data.tar.gz: 7d9b84113fbf2a2cd5ca1c5a46467296b15f1bf1016c1775b854098245998cee
5
5
  SHA512:
6
- metadata.gz: a875fd0267562a5a63cd1914acedb4d2231950401d99427134366f8b80dabe335c0102f927c710555e13ab4746718e12b203f41a96747926f62b57c0f9869e0a
7
- data.tar.gz: 4d6b39afd51c5e7787305156e43663f0231f46ba45738b2f81888b9a8e07ccbcf661c3f8d73ff4d8d9c0236673c73643e0eae0baa3f96700579a8f3d15dbb547
6
+ metadata.gz: b5fc7c6e1b2b580fcd5477941e239e8d06a79416b90a7c36145f8d50d32c40c7430b83273038b2974ec51df90673e46672207f8d8ae975c31e99b3612cd3b446
7
+ data.tar.gz: 7730a9813366e4b36dbf98873a575cf89a33963ee998da6c84d7e6b5fd7c7f5c197530e23f1acaeb2a44e339667f9665d7e3bc62c5164579df06765e01426d85
data/CHANGELOG.md CHANGED
@@ -1,6 +1,27 @@
1
+ ## [1.1.0](https://github.com/shivam091/unit_measurements/compare/v1.0.0...v1.1.0) - 2023-08-14
2
+
3
+ ### What's new
4
+
5
+ - Added `#format` method to format quantity of the measurement.
6
+
7
+ ----------
8
+
9
+ ## [1.0.0](https://github.com/shivam091/unit_measurements/compare/v0.1.0...v1.0.0) - 2023-09-14
10
+
11
+ ### What's new
12
+
13
+ - Added support to build unit groups.
14
+ - Added unit group for `length` units.
15
+ - Added unit group for `weight` units.
16
+ - Added support to build `si` units.
17
+ - Added support to parse `Complex`, `Rational`, `Scientific` numbers, and `ratios`.
18
+ - Added support to convert quantity between two units using `#convert_to`, `#convert_to!`, and `#parse` methods.
19
+
20
+ ----------
21
+
1
22
  ## [0.1.0] - 2023-09-13
2
23
 
3
- - Initial release
24
+ ### Initial release
4
25
 
5
26
  -----------
6
27
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- unit_measurements (0.1.0)
4
+ unit_measurements (1.1.0)
5
5
  activesupport (~> 7.0)
6
6
 
7
7
  GEM
data/README.md ADDED
@@ -0,0 +1,440 @@
1
+ # Unit Measurements
2
+
3
+ A library that encapsulate measurements and their units in Ruby.
4
+
5
+ [![Ruby](https://github.com/shivam091/unit_measurements/actions/workflows/main.yml/badge.svg)](https://github.com/shivam091/unit_measurements/actions/workflows/main.yml)
6
+ [![Gem Version](https://badge.fury.io/rb/unit_measurements.svg)](https://badge.fury.io/rb/unit_measurements)
7
+ [![Gem Downloads](https://img.shields.io/gem/dt/unit_measurements.svg)](http://rubygems.org/gems/unit_measurements)
8
+ [![Maintainability](https://api.codeclimate.com/v1/badges/b8aec9bffa356d108784/maintainability)](https://codeclimate.com/github/shivam091/unit_measurements/maintainability)
9
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/b8aec9bffa356d108784/test_coverage)](https://codeclimate.com/github/shivam091/unit_measurements/test_coverage)
10
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/shivam091/unit_measurements/blob/main/LICENSE)
11
+
12
+ **Harshal V. Ladhe, M.Sc. Computer Science.**
13
+
14
+ ## Introduction
15
+
16
+ Many technical applications need use of specialized calculations at some point of time.
17
+ Frequently, these calculations require unit conversions to ensure accurate
18
+ results. Needless to say, this is a pain to properly keep track of, and is prone
19
+ to numerous errors.
20
+
21
+ ## Solution
22
+
23
+ The `unit_measurements` gem is designed to simplify the handling of units for scientific calculations.
24
+
25
+ ## Advantages
26
+
27
+ 1. Provides easy conversion between units.
28
+ 2. Built in support for various [unit groups](units.md).
29
+ 3. Lightweight and easily extensible to include other units and conversions.
30
+ 4. Can convert `complex`, `rational`, `fractions`, `exponents`, `scientific notations`, and `ratios`.
31
+
32
+ ## Disclaimer
33
+
34
+ _The unit conversions presented in `unit_measurements` are provided for reference and general informational purposes.
35
+ While we aim to offer accurate conversions, we cannot guarantee their precision in all scenarios.
36
+ Users are advised to cross-verify conversions as needed for their specific use cases._
37
+
38
+ ## Minimum Requirements
39
+
40
+ * Ruby 3.2.2+ (https://www.ruby-lang.org/en/downloads/branches/)
41
+
42
+ ## Installation
43
+
44
+ If using bundler, first add this line to your application's Gemfile:
45
+
46
+ ```ruby
47
+ gem "unit_measurements"
48
+ ```
49
+
50
+ And then execute:
51
+
52
+ `$ bundle install`
53
+
54
+ Or otherwise simply install it yourself as:
55
+
56
+ `$ gem install unit_measurements`
57
+
58
+ ## Usage
59
+
60
+ The **`UnitMeasurements::Measurement`** class is responsible for conversion of quantity to various compatble units.
61
+
62
+ Measurements can't be initialized or converted to other units directly with the `UnitMeasurements::Measurement` class,
63
+ but rather with the unit group classes viz., `UnitMeasurements::Weight`, `UnitMeasurements::Length`, etc.
64
+
65
+ **Initialize a measurement:**
66
+
67
+ ```ruby
68
+ UnitMeasurements::Weight.new(1, :kg)
69
+ #=> 1 kg
70
+ ```
71
+
72
+ **Converting to other units:**
73
+
74
+ This gem allows you to convert among units of same unit group.
75
+ You can convert measurement to other unit using `#convert_to` (aliased as `#to`)
76
+ or `#convert_to!` (aliased as `#to!`) methods.
77
+
78
+ You can use `#convert_to` as:
79
+
80
+ ```ruby
81
+ UnitMeasurements::Weight.new(1, :kg).convert_to(:g)
82
+ #=> 1000 g
83
+ ```
84
+
85
+ If you want to modify measurement object itself, you can use `#convert_to!` method as:
86
+
87
+ ```ruby
88
+ UnitMeasurements::Weight.new(1, :kg).convert_to!(:g)
89
+ #=> 1000 g
90
+ ```
91
+
92
+ You can also chain call of `#convert_to` and `#convert_to!` methods as:
93
+
94
+ ```ruby
95
+ UnitMeasurements::Weight.new(1, :kg).convert_to(:g).convert_to(:t).convert_to!(:q)
96
+ #=> 0.01 q
97
+ ```
98
+
99
+ **Parse string without having to split out the quantity and source unit:**
100
+
101
+ ```ruby
102
+ UnitMeasurements::Weight.parse("1 kg")
103
+ #=> 1 kg
104
+ ```
105
+
106
+ **Parse string that mentions quantity, source unit, and target unit:**
107
+
108
+ ```ruby
109
+ UnitMeasurements::Weight.parse("1 kg to g")
110
+ #=> 1000 g
111
+ UnitMeasurements::Weight.parse("1 kg as g")
112
+ #=> 1000 g
113
+ UnitMeasurements::Weight.parse("1 kg in g")
114
+ #=> 1000 g
115
+ ```
116
+
117
+ **Parse rational numbers, source unit, and (or) target unit:**
118
+
119
+ ```ruby
120
+ UnitMeasurements::Weight.new(Rational(2, 3), :kg).convert_to(:g)
121
+ #=> 666.666666666667 g
122
+ UnitMeasurements::Weight.new("2/3", :kg).convert_to(:g)
123
+ #=> 666.666666666667 g
124
+ UnitMeasurements::Weight.parse("2/3 kg").convert_to(:g)
125
+ #=> 666.666666666667 g
126
+ UnitMeasurements::Weight.parse("2/3 kg to g")
127
+ #=> 666.666666666667 g
128
+ ```
129
+
130
+ **Parse complex numbers, source unit, and (or) target unit:**
131
+
132
+ ```ruby
133
+ UnitMeasurements::Weight.new(Complex(2, 3), :kg).convert_to(:g)
134
+ #=> 2000.0+3000.0i g
135
+ UnitMeasurements::Weight.new("2+3i", :kg).convert_to(:g)
136
+ #=> 2000.0+3000.0i g
137
+ UnitMeasurements::Weight.parse("2+3i kg").convert_to(:g)
138
+ #=> 2000.0+3000.0i g
139
+ UnitMeasurements::Weight.parse("2+3i kg to g")
140
+ #=> 2000.0+3000.0i g
141
+ ```
142
+
143
+ **Parse scientific numbers, source unit, and (or) target unit:**
144
+
145
+ ```ruby
146
+ UnitMeasurements::Weight.new(BigDecimal(2), :kg).convert_to(:g)
147
+ #=> 2000 g
148
+ UnitMeasurements::Weight.new(0.2e1, :kg).convert_to(:g)
149
+ #=> 2000 g
150
+ UnitMeasurements::Weight.parse("0.2e1 kg").convert_to(:g)
151
+ #=> 2000 g
152
+ UnitMeasurements::Weight.parse("0.2e1 kg to g")
153
+ #=> 2000 g
154
+ ```
155
+
156
+ **Parse ratios, source unit, and (or) target unit:**
157
+
158
+ ```ruby
159
+ UnitMeasurements::Weight.new("1:2", :kg).convert_to(:g)
160
+ #=> 500 g
161
+ UnitMeasurements::Weight.parse("1:2 kg").convert_to(:g)
162
+ #=> 500 g
163
+ UnitMeasurements::Weight.parse("1:2 kg to g")
164
+ #=> 500 g
165
+ ```
166
+
167
+ **Parse fractional notations, source unit, and (or) target unit:**
168
+
169
+ ```ruby
170
+ UnitMeasurements::Weight.new("1/2", :kg).convert_to(:g)
171
+ #=> 500 g
172
+ UnitMeasurements::Weight.parse("1/2 kg").convert_to(:g)
173
+ #=> 500 g
174
+ UnitMeasurements::Weight.parse("1/2 kg to g")
175
+ #=> 500 g
176
+ UnitMeasurements::Weight.new("½", :kg).convert_to(:g)
177
+ #=> 500 g
178
+ UnitMeasurements::Weight.parse("½ kg").convert_to(:g)
179
+ #=> 500 g
180
+ UnitMeasurements::Weight.parse("½ kg to g")
181
+ #=> 500 g
182
+ ```
183
+
184
+ **Parse mixed fractional notations, source unit, and (or) target unit:**
185
+
186
+ ```ruby
187
+ UnitMeasurements::Weight.new("2 1/2", :kg).convert_to(:g)
188
+ #=> 2500 g
189
+ UnitMeasurements::Weight.parse("2 1/2 kg").convert_to(:g)
190
+ #=> 2500 g
191
+ UnitMeasurements::Weight.parse("2 1/2 kg to g")
192
+ #=> 2500 g
193
+ UnitMeasurements::Weight.new("2 ½", :kg).convert_to(:g)
194
+ #=> 2500 g
195
+ UnitMeasurements::Weight.parse("2 ½ kg").convert_to(:g)
196
+ #=> 2500 g
197
+ UnitMeasurements::Weight.parse("2 ½ kg to g")
198
+ #=> 2500 g
199
+ ```
200
+
201
+ Supported special characters for fractional notations are `¼`, `½`, `¾`, `⅓`, `⅔`, `⅕`, `⅖`, `⅗`, `⅘`, `⅙`, `⅚`, `⅐`, `⅛`, `⅜`, `⅝`, `⅞`, `⅑`, `⅒`, `↉`, `⁄`.
202
+
203
+ **Parse exponents, source unit, and (or) target unit:**
204
+
205
+ ```ruby
206
+ UnitMeasurements::Weight.new("2e+2", :kg).convert_to(:g)
207
+ #=> 200000 g
208
+ UnitMeasurements::Weight.parse("2e² kg").convert_to(:g)
209
+ #=> 200000 g
210
+ UnitMeasurements::Weight.parse("2e+2 kg to g")
211
+ #=> 200000 g
212
+ UnitMeasurements::Weight.new("2e⁺²", :kg).convert_to(:g)
213
+ #=> 200000 g
214
+ UnitMeasurements::Weight.parse("2e⁺2 kg").convert_to(:g)
215
+ #=> 200000 g
216
+ UnitMeasurements::Weight.parse("2e⁻² kg to g")
217
+ #=> 20 g
218
+ ```
219
+
220
+ Supported special characters for exponents are `⁰`, `¹`, `²`, `³`, `⁴`, `⁵`, `⁶`, `⁷`, `⁸`, `⁹`, `⁺`, `⁻`.
221
+
222
+ **Formatting measurement:**
223
+
224
+ If you want to format measurement to certain format, you can use `#format` method.
225
+ If format is not specified, it defaults to `"%.2<value>f %<unit>s"`.
226
+
227
+ ```ruby
228
+ UnitMeasurements::Weight.parse("2 kg").to(:st).format
229
+ #=> "0.31 st"
230
+ UnitMeasurements::Weight.parse("2 kg").to(:st).format("%.4<quantity>f %<unit>s")
231
+ #=> "0.3149 st"
232
+ UnitMeasurements::Weight.parse("2 kg").to(:st).format("%.4<quantity>f")
233
+ #=> "0.3149"
234
+ ```
235
+
236
+ **Extract the unit and the quantity from measurement:**
237
+
238
+ ```ruby
239
+ weight = UnitMeasurements::Weight.new(1.0, :kg)
240
+ weight.quantity
241
+ #=> 0.1e1
242
+ weight.unit
243
+ #=> #<UnitMeasurements::Unit: kg (kilogram, kilogramme, kilogrammes, kilograms)>
244
+ ```
245
+
246
+ **See all units of the unit group:**
247
+
248
+ ```ruby
249
+ UnitMeasurements::Weight.units
250
+ #=> [#<UnitMeasurements::Unit: g (gram, gramme, grammes, grams)>, ..., ...]
251
+ ```
252
+
253
+ **See names of all valid units of the unit group:**
254
+
255
+ ```ruby
256
+ UnitMeasurements::Weight.unit_names
257
+ #=> ["g", "kg", "lb", "oz", ...]
258
+ ```
259
+
260
+ **See all valid units of the unit group along with their aliases:**
261
+
262
+ ```ruby
263
+ UnitMeasurements::Weight.unit_names_with_aliases
264
+ #=> ["g", "gram", "gramme", "grammes", "grams", "kg", "kilogram", "kilogramme", "kilogrammes", "kilograms", "lb", "ounce", "ounces", "oz", "pound", "pounds", ...]
265
+ ```
266
+
267
+ **Finding units within the unit group:**
268
+
269
+ You can use `#unit_for` or `#unit_for!` (aliased as `#[]`) to find units within
270
+ the unit group. `#unit_for!` method returns error if a unit is not present in the
271
+ unit group.
272
+
273
+ ```ruby
274
+ UnitMeasurements::Weight.unit_for(:g)
275
+ #=> #<UnitMeasurements::Unit: g (gram, gramme, grammes, grams)>
276
+ UnitMeasurements::Weight.unit_for(:z)
277
+ #=> nil
278
+ UnitMeasurements::Weight.unit_for!(:g)
279
+ #=> #<UnitMeasurements::Unit: g (gram, gramme, grammes, grams)>
280
+ UnitMeasurements::Weight.unit_for!(:z)
281
+ #=> Invalid unit: 'z'. (UnitMeasurements::UnitError)
282
+ ```
283
+
284
+ **Finding whether the unit is defined within the unit group:**
285
+
286
+ ```ruby
287
+ UnitMeasurements::Weight.defined?(:g)
288
+ #=> true
289
+ UnitMeasurements::Weight.defined?(:kg)
290
+ #=> true
291
+ UnitMeasurements::Weight.defined?(:gramme)
292
+ #=> false
293
+ ```
294
+
295
+ **Check if the unit is a valid unit or alias within the unit group:**
296
+
297
+ ```ruby
298
+ UnitMeasurements::Weight.unit_or_alias?(:g)
299
+ #=> true
300
+ UnitMeasurements::Weight.unit_or_alias?(:kg)
301
+ #=> true
302
+ UnitMeasurements::Weight.unit_or_alias?(:gramme)
303
+ #=> true
304
+ ```
305
+
306
+ ## Units
307
+
308
+ The **`UnitMeasurements::Unit`** class is used to represent the units for a measurement.
309
+
310
+ ### SI units support
311
+
312
+ There is support for SI units through the use of `si_unit` method.
313
+ Units declared through it will have automatic support for all SI prefixes:
314
+
315
+ | Multiplying Factor | SI Prefix | Scientific Notation |
316
+ | ----------------------------------------- | ---------- | ------------------- |
317
+ | 1 000 000 000 000 000 000 000 000 000 000 | quetta (Q) | 10^30 |
318
+ | 1 000 000 000 000 000 000 000 000 000 | ronna (R) | 10^27 |
319
+ | 1 000 000 000 000 000 000 000 000 | yotta (Y) | 10^24 |
320
+ | 1 000 000 000 000 000 000 000 | zetta (Z) | 10^21 |
321
+ | 1 000 000 000 000 000 000 | exa (E) | 10^18 |
322
+ | 1 000 000 000 000 000 | peta (P) | 10^15 |
323
+ | 1 000 000 000 000 | tera (T) | 10^12 |
324
+ | 1 000 000 000 | giga (G) | 10^9 |
325
+ | 1 000 000 | mega (M) | 10^6 |
326
+ | 1 000 | kilo (k) | 10^3 |
327
+ | 1 00 | hecto (h) | 10^2 |
328
+ | 1 0 | deca (da) | 10^1 |
329
+ | 0.1 | deci (d) | 10^-1 |
330
+ | 0.01 | centi (c) | 10^-2 |
331
+ | 0.001 | milli (m) | 10^-3 |
332
+ | 0.000 001 | micro (µ) | 10^-6 |
333
+ | 0.000 000 001 | nano (n) | 10^-9 |
334
+ | 0.000 000 000 001 | pico (p) | 10^-12 |
335
+ | 0.000 000 000 000 001 | femto (f) | 10^-15 |
336
+ | 0.000 000 000 000 000 001 | atto (a) | 10^-18 |
337
+ | 0.000 000 000 000 000 000 001 | zepto (z) | 10^-21 |
338
+ | 0.000 000 000 000 000 000 000 001 | yocto (y) | 10^-24 |
339
+ | 0.000 000 000 000 000 000 000 000 001 | ronto (r) | 10^-27 |
340
+ | 0.000 000 000 000 000 000 000 000 000 001 | quecto (q) | 10^-30 |
341
+
342
+ ### Bundled units
343
+
344
+ There are tons of units that are bundled in `unit_measurements`. You can check them out [here](units.md).
345
+
346
+ ### Specifing units
347
+
348
+ By default, `unit_measurements` ships with all the unit groups and this happens automatically
349
+ when requiring the gem in the following manner.
350
+
351
+ ```ruby
352
+ require "unit_measurements"
353
+ ```
354
+
355
+ **You can skip these unit groups and only [build your own unit groups](#building-new-unit-groups) by doing:**
356
+
357
+ ```ruby
358
+ require "unit_measurements/base"
359
+ ```
360
+
361
+ or simply
362
+
363
+ ```ruby
364
+ gem "unit_measurements", require: "unit_measurements/base"
365
+ ```
366
+
367
+ **You can also use unit groups in your application as per your need as:**
368
+
369
+ ```ruby
370
+ require "unit_measurements/base"
371
+
372
+ require "unit_measurements/unit_groups/length"
373
+ require "unit_measurements/unit_groups/weight"
374
+ require "unit_measurements/unit_groups/volume"
375
+ ```
376
+
377
+ or
378
+
379
+ ```ruby
380
+ gem "unit_measurements", require: ["unit_measurements/base", "unit_measurements/unit_groups/length"]
381
+ ```
382
+
383
+ ### Building new unit groups
384
+
385
+ This library provides simpler way to build your own unit groups. To build new unit group,
386
+ use `UnitMeasurements.build` in order to define base units and conversion units within it:
387
+
388
+ If you build unit using `base` method, base unit automatically gets set for the unit group.
389
+
390
+ ```ruby
391
+ UnitMeasurements::Time = UnitMeasurements.build do
392
+ # Add a base unit to the group.
393
+ base :s, aliases: [:second, :seconds]
394
+
395
+ # Add other units to the group, along with their conversion multipliers against
396
+ # base unit.
397
+ unit :min, value: 60.0, aliases: [:minute, :minutes]
398
+
399
+ # You can also specify conversion string if it's converted against a unit other
400
+ # than the unit group's base unit.
401
+ unit :h, value: "60 min", aliases: [:hour, :hours]
402
+ end
403
+ ```
404
+
405
+ If the unit is supporting [si prefixes](#si-units-support), you can use `si_unit` method to build it.
406
+ If you build unit using `si_unit`, Base unit is automatically added to the group along with all SI prefixes for it.
407
+
408
+ ```ruby
409
+ UnitMeasurements::Time = UnitMeasurements.build do
410
+ # Add a SI unit to the unit group
411
+ si_unit :s, aliases: [:second, :seconds]
412
+
413
+ unit :min, value: "60 s", aliases: [:minute, :minutes]
414
+ end
415
+ ```
416
+
417
+ All units allow aliases, as long as they are unique. Unit symbol can be used to
418
+ define the unit as long as it is unique. All unit names are case sensitive.
419
+
420
+ ### Namespaces
421
+
422
+ All unit groups and their definition classes are namespaced by default, but can be aliased in your application.
423
+
424
+ ```ruby
425
+ Weight = UnitMeasurements::Weight
426
+ Length = UnitMeasurements::Length
427
+ Volume = UnitMeasurements::Volume
428
+ ```
429
+
430
+ ## Contributing
431
+
432
+ 1. Fork it
433
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
434
+ 3. Commit your changes (`git commit -am "Add some feature"`)
435
+ 4. Push to the branch (`git push origin my-new-feature`)
436
+ 5. Create new Pull Request
437
+
438
+ ## License
439
+
440
+ Copyright 2023 [Harshal V. LADHE](https://github.com/shivam091), Released under the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,35 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # -*- frozen_string_literal: true -*-
3
+ # -*- warn_indent: true -*-
4
+
5
+ require "active_support/all"
6
+ require "unit_measurements/version"
7
+
8
+ module UnitMeasurements
9
+ class << self
10
+ def build(&block)
11
+ builder = UnitGroupBuilder.new
12
+ builder.instance_eval(&block)
13
+
14
+ Class.new(Measurement) do
15
+ class << self
16
+ attr_reader :unit_group
17
+ end
18
+
19
+ @unit_group = builder.build
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ require "unit_measurements/unit_group_builder"
26
+ require "unit_measurements/unit"
27
+ require "unit_measurements/unit_group"
28
+ require "unit_measurements/normalizer"
29
+ require "unit_measurements/parser"
30
+ require "unit_measurements/formatter"
31
+ require "unit_measurements/measurement"
32
+
33
+ require "unit_measurements/errors/unit_error"
34
+ require "unit_measurements/errors/unit_already_defined_error"
35
+ require "unit_measurements/errors/parse_error"
@@ -0,0 +1,14 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # -*- frozen_string_literal: true -*-
3
+ # -*- warn_indent: true -*-
4
+
5
+ module UnitMeasurements
6
+ class ParseError < BaseError
7
+ attr_reader :string
8
+
9
+ def initialize(string)
10
+ @string = string
11
+ super("Unable to parse: '#{string}'.")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # -*- frozen_string_literal: true -*-
3
+ # -*- warn_indent: true -*-
4
+
5
+ module UnitMeasurements
6
+ class UnitAlreadyDefinedError < BaseError
7
+ attr_reader :unit
8
+
9
+ def initialize(unit)
10
+ @unit = unit
11
+ super("Unit already defined: '#{unit}'.")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # -*- frozen_string_literal: true -*-
3
+ # -*- warn_indent: true -*-
4
+
5
+ module UnitMeasurements
6
+ class UnitError < BaseError
7
+ attr_reader :unit
8
+
9
+ def initialize(unit)
10
+ @unit = unit
11
+ super("Invalid unit: '#{unit}'.")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # -*- frozen_string_literal: true -*-
3
+ # -*- warn_indent: true -*-
4
+
5
+ module UnitMeasurements
6
+ module Formatter
7
+ # The default format for formatting measurements.
8
+ DEFAULT_FORMAT = "%.2<quantity>f %<unit>s".freeze
9
+
10
+ # Formats measurement to certain formatted string specified by +format+.
11
+ # If +format+ is not specified, it uses +DEFAULT_FORMAT+ for
12
+ # formatting the measurement
13
+ #
14
+ # @example
15
+ # UnitMeasurements::Weight.parse("2 kg").to(:st).format
16
+ # => "0.31 st"
17
+ # UnitMeasurements::Weight.parse("2 kg").to(:st).format("%.4<quantity>f %<unit>s")
18
+ # => "0.3149 st"
19
+ #
20
+ # @param [String] format
21
+ #
22
+ # @return [String]
23
+ def format(format = nil)
24
+ kwargs = {quantity: quantity, unit: unit.to_s}
25
+ (format || DEFAULT_FORMAT) % kwargs
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,103 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # -*- frozen_string_literal: true -*-
3
+ # -*- warn_indent: true -*-
4
+
5
+ module UnitMeasurements
6
+ class Measurement
7
+ include Formatter
8
+ CONVERSION_STRING_REGEXP = /(.+?)\s?(?:\s+(?:in|to|as)\s+(.+)|\z)/i.freeze
9
+
10
+ attr_reader :quantity, :unit
11
+
12
+ def initialize(quantity, unit)
13
+ raise BaseError, "Quantity cannot be blank." if quantity.blank?
14
+ raise BaseError, "Unit cannot be blank." if unit.blank?
15
+
16
+ @quantity = convert_quantity(quantity)
17
+ @unit = unit_from_unit_or_name!(unit)
18
+ end
19
+
20
+ def convert_to(target_unit)
21
+ target_unit = unit_from_unit_or_name!(target_unit)
22
+
23
+ return self if target_unit == unit
24
+
25
+ conversion_factor = (unit.conversion_factor / target_unit.conversion_factor)
26
+ self.class.new((quantity * conversion_factor), target_unit)
27
+ end
28
+ alias_method :to, :convert_to
29
+
30
+ def convert_to!(target_unit)
31
+ measurement = convert_to(target_unit)
32
+ @quantity, @unit = measurement.quantity, measurement.unit
33
+ self
34
+ end
35
+ alias_method :to!, :convert_to!
36
+
37
+ def inspect(dump: false)
38
+ return super() if dump
39
+
40
+ to_s
41
+ end
42
+
43
+ def to_s
44
+ "#{humanized_quantity} #{unit.to_s}"
45
+ end
46
+
47
+ class << self
48
+ extend Forwardable
49
+
50
+ def unit_group
51
+ raise "`Measurement` does not have a `unit_group` object. You cannot directly use `Measurement`. Instead, build a new unit group by calling `UnitMeasurements.build`."
52
+ end
53
+
54
+ def_delegators :unit_group, :units, :unit_names, :unit_with_name_and_aliases,
55
+ :unit_names_with_aliases, :unit_for, :unit_for!, :defined?,
56
+ :unit_or_alias?, :[]
57
+
58
+ def parse(input)
59
+ input = Normalizer.normalize(input)
60
+ source, target = input.match(CONVERSION_STRING_REGEXP)&.captures
61
+ target ? _parse(source).convert_to(target) : _parse(source)
62
+ end
63
+
64
+ private
65
+
66
+ def _parse(string)
67
+ quantity, unit = Parser.parse(string)
68
+ new(quantity, unit)
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def convert_quantity(quantity)
75
+ case quantity
76
+ when Float
77
+ BigDecimal(quantity, Float::DIG)
78
+ when Integer
79
+ Rational(quantity)
80
+ when String
81
+ quantity = Normalizer.normalize(quantity)
82
+ quantity, _ = Parser.parse(quantity)
83
+ quantity
84
+ else
85
+ quantity
86
+ end
87
+ end
88
+
89
+ def unit_from_unit_or_name!(value)
90
+ value.is_a?(Unit) ? value : self.class.unit_group.unit_for!(value)
91
+ end
92
+
93
+ def humanized_quantity
94
+ case quantity
95
+ when Complex
96
+ quantity
97
+ when Numeric
98
+ num = quantity.to_r
99
+ num.denominator == 1 ? num.numerator.to_s : num.to_f.to_s
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,73 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # -*- frozen_stringing_literal: true -*-
3
+ # -*- warn_indent: true -*-
4
+
5
+ module UnitMeasurements
6
+ class Normalizer
7
+ FRACTIONS_SYMBOLS = {
8
+ "¼" => "1/4",
9
+ "½" => "1/2",
10
+ "¾" => "3/4",
11
+ "⅓" => "1/3",
12
+ "⅔" => "2/3",
13
+ "⅕" => "1/5",
14
+ "⅖" => "2/5",
15
+ "⅗" => "3/5",
16
+ "⅘" => "4/5",
17
+ "⅙" => "1/6",
18
+ "⅚" => "5/6",
19
+ "⅐" => "1/7",
20
+ "⅛" => "1/8",
21
+ "⅜" => "3/8",
22
+ "⅝" => "5/8",
23
+ "⅞" => "7/8",
24
+ "⅑" => "1/9",
25
+ "⅒" => "1/10",
26
+ "↉" => "0/3",
27
+ "⁄" => "/"
28
+ }.freeze
29
+
30
+ EXPONENTS_SYMBOLS = {
31
+ "⁰" => "0",
32
+ "¹" => "1",
33
+ "²" => "2",
34
+ "³" => "3",
35
+ "⁴" => "4",
36
+ "⁵" => "5",
37
+ "⁶" => "6",
38
+ "⁷" => "7",
39
+ "⁸" => "8",
40
+ "⁹" => "9",
41
+ "⁺" => "+",
42
+ "⁻" => "-",
43
+ }
44
+
45
+ FRACTION_REGEX = /(#{FRACTIONS_SYMBOLS.keys.join("|")})/
46
+ EXPONENT_REGEX = /([\d]+[Ee]?[+-]?)(#{EXPONENTS_SYMBOLS.keys.join("|")})/
47
+ RATIO_REGEX = /([\d]+):([\d]+)/
48
+
49
+ class << self
50
+ def normalize(string)
51
+ string.dup.tap do |str|
52
+ if str =~ Regexp.new(FRACTION_REGEX)
53
+ FRACTIONS_SYMBOLS.each do |search, replace|
54
+ str.gsub!(search) { " #{replace}" }
55
+ end
56
+ end
57
+
58
+ if str =~ Regexp.new(EXPONENT_REGEX)
59
+ EXPONENTS_SYMBOLS.each do |search, replace|
60
+ str.gsub!(search) { "#{replace}" }
61
+ end
62
+ end
63
+
64
+ if str =~ Regexp.new(RATIO_REGEX)
65
+ str.gsub!(RATIO_REGEX) { "#{$1.to_i}/#{$2.to_i}" }
66
+ end
67
+
68
+ str.strip!
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,58 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # -*- frozen_stringing_literal: true -*-
3
+ # -*- warn_indent: true -*-
4
+
5
+ module UnitMeasurements
6
+ class Parser
7
+ UNIT_REGEX = /([^\d\s\/].*)/.freeze
8
+
9
+ SCIENTIFIC_NUMBER = /([+-]?\d*\.?\d+(?:[Ee][+-]?)?\d*)/.freeze
10
+ RATIONAL_NUMBER = /([+-]?\d+\s+)?((\d+)\/(\d+))/.freeze
11
+ COMPLEX_NUMBER = /#{SCIENTIFIC_NUMBER}#{SCIENTIFIC_NUMBER}i/.freeze
12
+
13
+ SCIENTIFIC_REGEX = /\A#{SCIENTIFIC_NUMBER}\s*#{UNIT_REGEX}?\z/.freeze
14
+ RATIONAL_REGEX = /\A#{RATIONAL_NUMBER}\s*#{UNIT_REGEX}?\z/.freeze
15
+ COMPLEX_REGEX = /\A#{COMPLEX_NUMBER}\s*#{UNIT_REGEX}?\z/.freeze
16
+
17
+ class << self
18
+ def parse(string)
19
+ case string
20
+ when COMPLEX_REGEX then parse_complex(string)
21
+ when SCIENTIFIC_REGEX then parse_scientific(string)
22
+ when RATIONAL_REGEX then parse_rational(string)
23
+ else raise ParseError, string
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def parse_complex(string)
30
+ real, imaginary, unit = string.match(COMPLEX_REGEX)&.captures
31
+ quantity = Complex(real.to_f, imaginary.to_f)
32
+
33
+ [quantity, unit]
34
+ end
35
+
36
+ def parse_scientific(string)
37
+ whole, unit = string.match(SCIENTIFIC_REGEX)&.captures
38
+ quantity = whole.to_f
39
+
40
+ [quantity, unit]
41
+ end
42
+
43
+ def parse_rational(string)
44
+ whole, _, numerator, denominator, unit = string.match(RATIONAL_REGEX)&.captures
45
+
46
+ if numerator && denominator
47
+ numerator = numerator.to_f + (denominator.to_f * whole.to_f)
48
+ denominator = denominator.to_f
49
+ quantity = Rational(numerator, denominator).to_f
50
+ else
51
+ quantity = whole.to_f
52
+ end
53
+
54
+ [quantity, unit]
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,79 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # -*- frozen_string_literal: true -*-
3
+ # -*- warn_indent: true -*-
4
+
5
+ require "set"
6
+
7
+ module UnitMeasurements
8
+ class Unit
9
+ attr_reader :name, :value, :aliases, :unit_group
10
+
11
+ def initialize(name, value:, aliases:, unit_group: nil)
12
+ @name = name.to_s.freeze
13
+ @value = value
14
+ @aliases = Set.new(aliases.sort.map(&:to_s).map(&:freeze)).freeze
15
+ @unit_group = unit_group
16
+ end
17
+
18
+ def with(name: nil, value: nil, aliases: nil, unit_group: nil)
19
+ self.class.new(
20
+ (name || self.name),
21
+ value: (value || self.value),
22
+ aliases: (aliases || self.aliases),
23
+ unit_group: (unit_group || self.unit_group)
24
+ )
25
+ end
26
+
27
+ def names
28
+ (aliases + [name]).sort.freeze
29
+ end
30
+
31
+ def to_s
32
+ name
33
+ end
34
+
35
+ def inspect
36
+ aliases = "(#{@aliases.join(", ")})" if @aliases.any?
37
+ "#<#{self.class.name}: #{name} #{aliases}>"
38
+ end
39
+
40
+ def conversion_factor
41
+ if value.is_a?(String)
42
+ measurement_value, measurement_unit = Parser.parse(value)
43
+ conversion_factor = unit_group.unit_for!(measurement_unit).conversion_factor
44
+ conversion_factor * measurement_value
45
+ else
46
+ value
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ SI_PREFIXES = [
53
+ ["q", %w(quecto), 1e-30],
54
+ ["r", %w(ronto), 1e-27],
55
+ ["y", %w(yocto), 1e-24],
56
+ ["z", %w(zepto), 1e-21],
57
+ ["a", %w(atto), 1e-18],
58
+ ["f", %w(femto), 1e-15],
59
+ ["p", %w(pico), 1e-12],
60
+ ["n", %w(nano), 1e-9],
61
+ ["μ", %w(micro), 1e-6],
62
+ ["m", %w(milli), 1e-3],
63
+ ["c", %w(centi), 1e-2],
64
+ ["d", %w(deci), 1e-1],
65
+ ["da", %w(deca deka), 1e+1],
66
+ ["h", %w(hecto), 1e+2],
67
+ ["k", %w(kilo), 1e+3],
68
+ ["M", %w(mega), 1e+6],
69
+ ["G", %w(giga), 1e+9],
70
+ ["T", %w(tera), 1e+12],
71
+ ["P", %w(peta), 1e+15],
72
+ ["E", %w(exa), 1e+18],
73
+ ["Z", %w(zetta), 1e+21],
74
+ ["Y", %w(yotta), 1e+24],
75
+ ["R", %w(ronna), 1e+27],
76
+ ["Q", %w(quetta), 1e+30]
77
+ ].map(&:freeze).freeze
78
+ end
79
+ end
@@ -0,0 +1,51 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # -*- frozen_string_literal: true -*-
3
+ # -*- warn_indent: true -*-
4
+
5
+ module UnitMeasurements
6
+ class UnitGroup
7
+ attr_reader :units
8
+
9
+ def initialize(units)
10
+ @units = units.map { |unit| unit.with(unit_group: self) }
11
+ end
12
+
13
+ def unit_for(name)
14
+ unit_name_to_unit(name)
15
+ end
16
+
17
+ def unit_for!(name)
18
+ unit = unit_for(name)
19
+ raise UnitError, name unless unit
20
+ unit
21
+ end
22
+ alias_method :[], :unit_for!
23
+
24
+ def unit_with_name_and_aliases
25
+ units.each_with_object({}) do |unit, hash|
26
+ unit.names.each { |name| hash[name.to_s] = unit }
27
+ end
28
+ end
29
+
30
+ def unit_names
31
+ units.map(&:name).sort
32
+ end
33
+
34
+ def unit_names_with_aliases
35
+ units.flat_map(&:names).sort
36
+ end
37
+
38
+ def unit_name_to_unit(name)
39
+ unit_with_name_and_aliases[name.to_s]
40
+ end
41
+
42
+ def defined?(name)
43
+ unit = unit_for(name)
44
+ unit ? unit.name == name.to_s : false
45
+ end
46
+
47
+ def unit_or_alias?(name)
48
+ !!unit_for(name)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,56 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # -*- frozen_string_literal: true -*-
3
+ # -*- warn_indent: true -*-
4
+
5
+ module UnitMeasurements
6
+ class UnitGroupBuilder
7
+ attr_reader :units
8
+
9
+ def initialize
10
+ @units = []
11
+ end
12
+
13
+ def base(name, aliases: [])
14
+ @units << build_unit(name, value: 1.0, aliases: aliases)
15
+ end
16
+
17
+ def unit(name, value: 1.0, aliases: [])
18
+ @units << build_unit(name, value: value, aliases: aliases)
19
+ end
20
+
21
+ def si_unit(name, value: 1.0, aliases: [])
22
+ @units += build_si_units(name, value: value, aliases: aliases)
23
+ end
24
+
25
+ def build
26
+ UnitGroup.new(@units)
27
+ end
28
+
29
+ private
30
+
31
+ def build_si_units(name, value:, aliases:)
32
+ si_units = [build_unit(name, value: value, aliases: aliases)]
33
+
34
+ Unit::SI_PREFIXES.each do |short_prefix, long_prefix, multiplier|
35
+ si_aliases = long_prefix.product(aliases.to_a).flat_map do |prefix, unit|
36
+ aliases.map { |alias_unit| prefix + alias_unit.to_s }
37
+ end
38
+ si_units << build_unit("#{short_prefix}#{name}", value: "#{multiplier} #{name}", aliases: si_aliases)
39
+ end
40
+ si_units
41
+ end
42
+
43
+ def build_unit(name, value:, aliases:)
44
+ unit = Unit.new(name, value: value, aliases: aliases)
45
+ check_for_duplicate_unit_names!(unit)
46
+ unit
47
+ end
48
+
49
+ def check_for_duplicate_unit_names!(unit)
50
+ names = @units.flat_map(&:names)
51
+ if names.any? { |name| unit.names.include?(name) }
52
+ raise UnitAlreadyDefinedError.new(unit.name)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,6 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # -*- frozen_string_literal: true -*-
3
+ # -*- warn_indent: true -*-
4
+
5
+ require_relative "length"
6
+ require_relative "weight"
@@ -0,0 +1,12 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # -*- frozen_string_literal: true -*-
3
+ # -*- warn_indent: true -*-
4
+
5
+ UnitMeasurements::Length = UnitMeasurements.build do
6
+ si_unit :m, aliases: %i[meter metre meters metres]
7
+
8
+ unit :in, value: "25.4 mm", aliases: %i[" inch inches]
9
+ unit :ft, value: "12 in", aliases: %i[' foot feet]
10
+ unit :yd, value: "3 ft", aliases: %i[yard yards]
11
+ unit :mi, value: "1760 yd", aliases: %i[mile miles]
12
+ end
@@ -0,0 +1,10 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # -*- frozen_string_literal: true -*-
3
+ # -*- warn_indent: true -*-
4
+
5
+ UnitMeasurements::Weight = UnitMeasurements.build do
6
+ si_unit :g, aliases: %i[gram grams gramme grammes]
7
+
8
+ unit :q, value: "100 kg", aliases: %i[quintal quintals]
9
+ unit :t, value: "10 q", aliases: %i[tonne tonnes metric\ tonne metric\ tonnes]
10
+ end
@@ -3,5 +3,5 @@
3
3
  # -*- warn_indent: true -*-
4
4
 
5
5
  module UnitMeasurements
6
- VERSION = "0.1.0"
6
+ VERSION = "1.1.0"
7
7
  end
@@ -1,3 +1,11 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  # -*- frozen_string_literal: true -*-
3
3
  # -*- warn_indent: true -*-
4
+
5
+ module UnitMeasurements
6
+ class BaseError < StandardError; end
7
+ end
8
+
9
+ require "unit_measurements/base"
10
+
11
+ require "unit_measurements/unit_groups/all"
data/units.md ADDED
@@ -0,0 +1,32 @@
1
+ # Bundled unit groups and units
2
+
3
+ As there are lots of units bundled with `unit_measurements`, we recommend you to check below list of
4
+ bundled units before converting your measurements.
5
+
6
+ **Notes:**
7
+ 1. Base unit for each unit group is highlighted.
8
+ 2. Unit numbers suffixed with `*` support all [SI prefixes](README.md#si-units-support).
9
+
10
+ Below are the units which are bundled in the unit_measurements.
11
+
12
+ ## 1. Length/Distance
13
+
14
+ These units are defined in `UnitMeasurements::Length`.
15
+
16
+ | # | Symbol | Aliases |
17
+ |:--|:--|:--|
18
+ | **1\*** | **m** | **meter, metre, meters, metres** |
19
+ | 2 | in | ", inch, inches |
20
+ | 3 | ft | ', foot, feet |
21
+ | 4 | yd | yard, yards |
22
+ | 5 | mi | mile, miles |
23
+
24
+ ## 2. Weight/Mass
25
+
26
+ These units are defined in `UnitMeasurements::Weight`.
27
+
28
+ | # | Symbol | Aliases |
29
+ |--|--|--|
30
+ | **1\*** | **g** | **gram, grams, gramme, grammes** |
31
+ | 2 | q | quintal, quintals |
32
+ | 3 | t | tonne, tonnes, metric tonne, metric tonnes |
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unit_measurements
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harshal LADHE
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-13 00:00:00.000000000 Z
11
+ date: 2023-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -100,10 +100,26 @@ files:
100
100
  - Gemfile
101
101
  - Gemfile.lock
102
102
  - LICENSE
103
+ - README.md
103
104
  - Rakefile
104
105
  - lib/unit_measurements.rb
106
+ - lib/unit_measurements/base.rb
107
+ - lib/unit_measurements/errors/parse_error.rb
108
+ - lib/unit_measurements/errors/unit_already_defined_error.rb
109
+ - lib/unit_measurements/errors/unit_error.rb
110
+ - lib/unit_measurements/formatter.rb
111
+ - lib/unit_measurements/measurement.rb
112
+ - lib/unit_measurements/normalizer.rb
113
+ - lib/unit_measurements/parser.rb
114
+ - lib/unit_measurements/unit.rb
115
+ - lib/unit_measurements/unit_group.rb
116
+ - lib/unit_measurements/unit_group_builder.rb
117
+ - lib/unit_measurements/unit_groups/all.rb
118
+ - lib/unit_measurements/unit_groups/length.rb
119
+ - lib/unit_measurements/unit_groups/weight.rb
105
120
  - lib/unit_measurements/version.rb
106
121
  - unit_measurements.gemspec
122
+ - units.md
107
123
  homepage: https://github.com/shivam091/unit_measurements
108
124
  licenses:
109
125
  - MIT