unit_measurements 0.1.0 → 1.1.0

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