unit_measurements 0.1.0 → 1.0.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: 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