unit_measurements 0.1.0 → 1.0.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: 286c6438c249edf7fe0872a2ad8956f42cb5150819885e8bebe353a7955c97c9
4
+ data.tar.gz: 1e82f6cf7ec7dc4f51670be13980cc30ccfe56583fb2b94622f12614d9a0c91e
5
5
  SHA512:
6
- metadata.gz: a875fd0267562a5a63cd1914acedb4d2231950401d99427134366f8b80dabe335c0102f927c710555e13ab4746718e12b203f41a96747926f62b57c0f9869e0a
7
- data.tar.gz: 4d6b39afd51c5e7787305156e43663f0231f46ba45738b2f81888b9a8e07ccbcf661c3f8d73ff4d8d9c0236673c73643e0eae0baa3f96700579a8f3d15dbb547
6
+ metadata.gz: 5353470b9101426b12d5fd34474d41fe04de2e4ef8286dd42c78605f01f35116f62341626299708f5ea0cc53613977437876b906d7fd79d91888ed9f1221a6e9
7
+ data.tar.gz: 7bda4b4f0598d53e2b3cf933da8d8485d8052ca6172d772bbaf55aa4400542782c62b99d583d0109ef5af2a7ed3504eb4991eb06b5a5f9cbcdcc4f87575ac919
data/CHANGELOG.md CHANGED
@@ -1,6 +1,19 @@
1
+ ## [1.0.0](https://github.com/shivam091/unit_measurements/compare/v0.1.0...v1.0.0) - 2023-09-14
2
+
3
+ ### What's new
4
+
5
+ - Added support to build unit groups.
6
+ - Added unit group for `length` units.
7
+ - Added unit group for `weight` units.
8
+ - Added support to build `si` units.
9
+ - Added support to parse `Complex`, `Rational`, `Scientific` numbers, and `ratios`.
10
+ - Added support to convert quantity between two units using `#convert_to`, `#convert_to!`, and `#parse` methods.
11
+
12
+ ----------
13
+
1
14
  ## [0.1.0] - 2023-09-13
2
15
 
3
- - Initial release
16
+ ### Initial release
4
17
 
5
18
  -----------
6
19
 
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.0.0)
5
5
  activesupport (~> 7.0)
6
6
 
7
7
  GEM
data/README.md ADDED
@@ -0,0 +1,426 @@
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
+ **Extract the unit and the quantity from measurement:**
223
+
224
+ ```ruby
225
+ weight = UnitMeasurements::Weight.new(1.0, :kg)
226
+ weight.quantity
227
+ #=> 0.1e1
228
+ weight.unit
229
+ #=> #<UnitMeasurements::Unit: kg (kilogram, kilogramme, kilogrammes, kilograms)>
230
+ ```
231
+
232
+ **See all units of the unit group:**
233
+
234
+ ```ruby
235
+ UnitMeasurements::Weight.units
236
+ #=> [#<UnitMeasurements::Unit: g (gram, gramme, grammes, grams)>, ..., ...]
237
+ ```
238
+
239
+ **See names of all valid units of the unit group:**
240
+
241
+ ```ruby
242
+ UnitMeasurements::Weight.unit_names
243
+ #=> ["g", "kg", "lb", "oz", ...]
244
+ ```
245
+
246
+ **See all valid units of the unit group along with their aliases:**
247
+
248
+ ```ruby
249
+ UnitMeasurements::Weight.unit_names_with_aliases
250
+ #=> ["g", "gram", "gramme", "grammes", "grams", "kg", "kilogram", "kilogramme", "kilogrammes", "kilograms", "lb", "ounce", "ounces", "oz", "pound", "pounds", ...]
251
+ ```
252
+
253
+ **Finding units within the unit group:**
254
+
255
+ You can use `#unit_for` or `#unit_for!` (aliased as `#[]`) to find units within
256
+ the unit group. `#unit_for!` method returns error if a unit is not present in the
257
+ unit group.
258
+
259
+ ```ruby
260
+ UnitMeasurements::Weight.unit_for(:g)
261
+ #=> #<UnitMeasurements::Unit: g (gram, gramme, grammes, grams)>
262
+ UnitMeasurements::Weight.unit_for(:z)
263
+ #=> nil
264
+ UnitMeasurements::Weight.unit_for!(:g)
265
+ #=> #<UnitMeasurements::Unit: g (gram, gramme, grammes, grams)>
266
+ UnitMeasurements::Weight.unit_for!(:z)
267
+ #=> Invalid unit: 'z'. (UnitMeasurements::UnitError)
268
+ ```
269
+
270
+ **Finding whether the unit is defined within the unit group:**
271
+
272
+ ```ruby
273
+ UnitMeasurements::Weight.defined?(:g)
274
+ #=> true
275
+ UnitMeasurements::Weight.defined?(:kg)
276
+ #=> true
277
+ UnitMeasurements::Weight.defined?(:gramme)
278
+ #=> false
279
+ ```
280
+
281
+ **Check if the unit is a valid unit or alias within the unit group:**
282
+
283
+ ```ruby
284
+ UnitMeasurements::Weight.unit_or_alias?(:g)
285
+ #=> true
286
+ UnitMeasurements::Weight.unit_or_alias?(:kg)
287
+ #=> true
288
+ UnitMeasurements::Weight.unit_or_alias?(:gramme)
289
+ #=> true
290
+ ```
291
+
292
+ ## Units
293
+
294
+ The **`UnitMeasurements::Unit`** class is used to represent the units for a measurement.
295
+
296
+ ### SI units support
297
+
298
+ There is support for SI units through the use of `si_unit` method.
299
+ Units declared through it will have automatic support for all SI prefixes:
300
+
301
+ | Multiplying Factor | SI Prefix | Scientific Notation |
302
+ | ----------------------------------------- | ---------- | ------------------- |
303
+ | 1 000 000 000 000 000 000 000 000 000 000 | quetta (Q) | 10^30 |
304
+ | 1 000 000 000 000 000 000 000 000 000 | ronna (R) | 10^27 |
305
+ | 1 000 000 000 000 000 000 000 000 | yotta (Y) | 10^24 |
306
+ | 1 000 000 000 000 000 000 000 | zetta (Z) | 10^21 |
307
+ | 1 000 000 000 000 000 000 | exa (E) | 10^18 |
308
+ | 1 000 000 000 000 000 | peta (P) | 10^15 |
309
+ | 1 000 000 000 000 | tera (T) | 10^12 |
310
+ | 1 000 000 000 | giga (G) | 10^9 |
311
+ | 1 000 000 | mega (M) | 10^6 |
312
+ | 1 000 | kilo (k) | 10^3 |
313
+ | 1 00 | hecto (h) | 10^2 |
314
+ | 1 0 | deca (da) | 10^1 |
315
+ | 0.1 | deci (d) | 10^-1 |
316
+ | 0.01 | centi (c) | 10^-2 |
317
+ | 0.001 | milli (m) | 10^-3 |
318
+ | 0.000 001 | micro (µ) | 10^-6 |
319
+ | 0.000 000 001 | nano (n) | 10^-9 |
320
+ | 0.000 000 000 001 | pico (p) | 10^-12 |
321
+ | 0.000 000 000 000 001 | femto (f) | 10^-15 |
322
+ | 0.000 000 000 000 000 001 | atto (a) | 10^-18 |
323
+ | 0.000 000 000 000 000 000 001 | zepto (z) | 10^-21 |
324
+ | 0.000 000 000 000 000 000 000 001 | yocto (y) | 10^-24 |
325
+ | 0.000 000 000 000 000 000 000 000 001 | ronto (r) | 10^-27 |
326
+ | 0.000 000 000 000 000 000 000 000 000 001 | quecto (q) | 10^-30 |
327
+
328
+ ### Bundled units
329
+
330
+ There are tons of units that are bundled in `unit_measurements`. You can check them out [here](units.md).
331
+
332
+ ### Specifing units
333
+
334
+ By default, `unit_measurements` ships with all the unit groups and this happens automatically
335
+ when requiring the gem in the following manner.
336
+
337
+ ```ruby
338
+ require "unit_measurements"
339
+ ```
340
+
341
+ **You can skip these unit groups and only [build your own unit groups](#building-new-unit-groups) by doing:**
342
+
343
+ ```ruby
344
+ require "unit_measurements/base"
345
+ ```
346
+
347
+ or simply
348
+
349
+ ```ruby
350
+ gem "unit_measurements", require: "unit_measurements/base"
351
+ ```
352
+
353
+ **You can also use unit groups in your application as per your need as:**
354
+
355
+ ```ruby
356
+ require "unit_measurements/base"
357
+
358
+ require "unit_measurements/unit_groups/length"
359
+ require "unit_measurements/unit_groups/weight"
360
+ require "unit_measurements/unit_groups/volume"
361
+ ```
362
+
363
+ or
364
+
365
+ ```ruby
366
+ gem "unit_measurements", require: ["unit_measurements/base", "unit_measurements/unit_groups/length"]
367
+ ```
368
+
369
+ ### Building new unit groups
370
+
371
+ This library provides simpler way to build your own unit groups. To build new unit group,
372
+ use `UnitMeasurements.build` in order to define base units and conversion units within it:
373
+
374
+ If you build unit using `base` method, base unit automatically gets set for the unit group.
375
+
376
+ ```ruby
377
+ UnitMeasurements::Time = UnitMeasurements.build do
378
+ # Add a base unit to the group.
379
+ base :s, aliases: [:second, :seconds]
380
+
381
+ # Add other units to the group, along with their conversion multipliers against
382
+ # base unit.
383
+ unit :min, value: 60.0, aliases: [:minute, :minutes]
384
+
385
+ # You can also specify conversion string if it's converted against a unit other
386
+ # than the unit group's base unit.
387
+ unit :h, value: "60 min", aliases: [:hour, :hours]
388
+ end
389
+ ```
390
+
391
+ If the unit is supporting [si prefixes](#si-units-support), you can use `si_unit` method to build it.
392
+ If you build unit using `si_unit`, Base unit is automatically added to the group along with all SI prefixes for it.
393
+
394
+ ```ruby
395
+ UnitMeasurements::Time = UnitMeasurements.build do
396
+ # Add a SI unit to the unit group
397
+ si_unit :s, aliases: [:second, :seconds]
398
+
399
+ unit :min, value: "60 s", aliases: [:minute, :minutes]
400
+ end
401
+ ```
402
+
403
+ All units allow aliases, as long as they are unique. Unit symbol can be used to
404
+ define the unit as long as it is unique. All unit names are case sensitive.
405
+
406
+ ### Namespaces
407
+
408
+ All unit groups and their definition classes are namespaced by default, but can be aliased in your application.
409
+
410
+ ```ruby
411
+ Weight = UnitMeasurements::Weight
412
+ Length = UnitMeasurements::Length
413
+ Volume = UnitMeasurements::Volume
414
+ ```
415
+
416
+ ## Contributing
417
+
418
+ 1. Fork it
419
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
420
+ 3. Commit your changes (`git commit -am "Add some feature"`)
421
+ 4. Push to the branch (`git push origin my-new-feature`)
422
+ 5. Create new Pull Request
423
+
424
+ ## License
425
+
426
+ Copyright 2023 [Harshal V. LADHE](https://github.com/shivam091), Released under the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,34 @@
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/measurement"
31
+
32
+ require "unit_measurements/errors/unit_error"
33
+ require "unit_measurements/errors/unit_already_defined_error"
34
+ 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 < StandardError
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 < StandardError
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 < StandardError
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,102 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # -*- frozen_string_literal: true -*-
3
+ # -*- warn_indent: true -*-
4
+
5
+ module UnitMeasurements
6
+ class Measurement
7
+ CONVERSION_STRING_REGEXP = /(.+?)\s?(?:\s+(?:in|to|as)\s+(.+)|\z)/i.freeze
8
+
9
+ attr_reader :quantity, :unit
10
+
11
+ def initialize(quantity, unit)
12
+ raise ArgumentError, "Quantity cannot be blank." if quantity.blank?
13
+ raise ArgumentError, "Unit cannot be blank." if unit.blank?
14
+
15
+ @quantity = convert_quantity(quantity)
16
+ @unit = unit_from_unit_or_name!(unit)
17
+ end
18
+
19
+ def convert_to(target_unit)
20
+ target_unit = unit_from_unit_or_name!(target_unit)
21
+
22
+ return self if target_unit == unit
23
+
24
+ conversion_factor = (unit.conversion_factor / target_unit.conversion_factor)
25
+ self.class.new((quantity * conversion_factor), target_unit)
26
+ end
27
+ alias_method :to, :convert_to
28
+
29
+ def convert_to!(target_unit)
30
+ measurement = convert_to(target_unit)
31
+ @quantity, @unit = measurement.quantity, measurement.unit
32
+ self
33
+ end
34
+ alias_method :to!, :convert_to!
35
+
36
+ def inspect(dump: false)
37
+ return super() if dump
38
+
39
+ to_s
40
+ end
41
+
42
+ def to_s
43
+ "#{humanized_quantity} #{unit.to_s}"
44
+ end
45
+
46
+ class << self
47
+ extend Forwardable
48
+
49
+ def unit_group
50
+ raise "`Measurement` does not have a `unit_group` object. You cannot directly use `Measurement`. Instead, build a new unit group by calling `UnitMeasurements.build`."
51
+ end
52
+
53
+ def_delegators :unit_group, :units, :unit_names, :unit_with_name_and_aliases,
54
+ :unit_names_with_aliases, :unit_for, :unit_for!, :defined?,
55
+ :unit_or_alias?, :[]
56
+
57
+ def parse(input)
58
+ input = Normalizer.normalize(input)
59
+ source, target = input.match(CONVERSION_STRING_REGEXP)&.captures
60
+ target ? _parse(source).convert_to(target) : _parse(source)
61
+ end
62
+
63
+ private
64
+
65
+ def _parse(string)
66
+ quantity, unit = Parser.parse(string)
67
+ new(quantity, unit)
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def convert_quantity(quantity)
74
+ case quantity
75
+ when Float
76
+ BigDecimal(quantity, Float::DIG)
77
+ when Integer
78
+ Rational(quantity)
79
+ when String
80
+ quantity = Normalizer.normalize(quantity)
81
+ quantity, _ = Parser.parse(quantity)
82
+ quantity
83
+ else
84
+ quantity
85
+ end
86
+ end
87
+
88
+ def unit_from_unit_or_name!(value)
89
+ value.is_a?(Unit) ? value : self.class.unit_group.unit_for!(value)
90
+ end
91
+
92
+ def humanized_quantity
93
+ case quantity
94
+ when Complex
95
+ quantity
96
+ when Numeric
97
+ num = quantity.to_r
98
+ num.denominator == 1 ? num.numerator.to_s : num.to_f.to_s
99
+ end
100
+ end
101
+ end
102
+ 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.0.0"
7
7
  end
@@ -1,3 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  # -*- frozen_string_literal: true -*-
3
3
  # -*- warn_indent: true -*-
4
+
5
+ require "unit_measurements/base"
6
+
7
+ 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.0.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,25 @@ 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/measurement.rb
111
+ - lib/unit_measurements/normalizer.rb
112
+ - lib/unit_measurements/parser.rb
113
+ - lib/unit_measurements/unit.rb
114
+ - lib/unit_measurements/unit_group.rb
115
+ - lib/unit_measurements/unit_group_builder.rb
116
+ - lib/unit_measurements/unit_groups/all.rb
117
+ - lib/unit_measurements/unit_groups/length.rb
118
+ - lib/unit_measurements/unit_groups/weight.rb
105
119
  - lib/unit_measurements/version.rb
106
120
  - unit_measurements.gemspec
121
+ - units.md
107
122
  homepage: https://github.com/shivam091/unit_measurements
108
123
  licenses:
109
124
  - MIT