Skip to content

Commit ff0d70f

Browse files
authored
Code quality and documentation improvements (#381)
1 parent 5c0af38 commit ff0d70f

11 files changed

Lines changed: 1375 additions & 530 deletions

File tree

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ group :optional do
1111
gem "pry"
1212
gem "redcarpet", platform: :mri # redcarpet doesn't support jruby
1313
gem "reek"
14+
gem "ruby-lsp"
1415
gem "ruby-maven", platform: :jruby
1516
gem "ruby-prof", platform: :mri
1617
gem "simplecov-html"

Gemfile.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ GEM
188188
rubocop-rspec (3.8.0)
189189
lint_roller (~> 1.1)
190190
rubocop (~> 1.81)
191+
ruby-lsp (0.26.4)
192+
language_server-protocol (~> 3.17.0)
193+
prism (>= 1.2, < 2.0)
194+
rbs (>= 3, < 5)
191195
ruby-maven (3.9.3)
192196
ruby-maven-libs (~> 3.9.9)
193197
ruby-maven-libs (3.9.9)
@@ -261,6 +265,7 @@ DEPENDENCIES
261265
rubocop
262266
rubocop-rake
263267
rubocop-rspec
268+
ruby-lsp
264269
ruby-maven
265270
ruby-prof
266271
ruby-units!

README.md

Lines changed: 211 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
# Ruby Units
22

3-
[![Maintainability](https://api.codeclimate.com/v1/badges/4e858d14a07dd453f748/maintainability.svg)](https://codeclimate.com/github/olbrich/ruby-units/maintainability)
4-
[![CodeClimate Status](https://api.codeclimate.com/v1/badges/4e858d14a07dd453f748/test_coverage.svg)](https://codeclimate.com/github/olbrich/ruby-units/test_coverage)
5-
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Folbrich%2Fruby-units.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Folbrich%2Fruby-units?ref=badge_shield)
6-
73
Kevin C. Olbrich, Ph.D.
84

95
Project page:
@@ -146,21 +142,89 @@ Unit.new("100 kg").to_s(:lbs) # returns 220 lbs, 7 oz
146142
Unit.new("100 kg").to_s(:stone) # returns 15 stone, 10 lb
147143
```
148144

145+
You can also use Ruby's standard string formatting operator (`%`) with units:
146+
147+
```ruby
148+
'%0.2f' % '1 mm'.to_unit # "1.00 mm"
149+
'%0.2f in' % '1 mm'.to_unit # "0.04 in" - format and convert
150+
'%.2e' % '1000 m'.to_unit # "1.00e+03 m" - scientific notation
151+
```
152+
153+
Strings can be converted to units using the `convert_to` method:
154+
155+
```ruby
156+
'10 mm'.convert_to('cm') # converts directly to centimeters
157+
'100 km/h'.convert_to('m/s') # converts compound units
158+
```
159+
149160
### Time Helpers
150161

151-
`Time`, `Date`, and `DateTime` objects can have time units added or subtracted.
162+
Ruby-units extends the `Time`, `Date`, and `DateTime` classes to support unit-based arithmetic,
163+
allowing you to add or subtract durations from time objects naturally.
164+
165+
#### Adding and Subtracting Durations
152166

153167
```ruby
154-
Time.now + Unit.new("10 min")
168+
Time.now + Unit.new("10 min") #=> 10 minutes from now
169+
Time.now - Unit.new("2 hours") #=> 2 hours ago
170+
171+
Date.today + Unit.new("1 week") #=> 7 days from today
172+
Date.today - Unit.new("30 days") #=> 30 days ago
155173
```
156174

157-
Several helpers have also been defined. Note: If you include the 'Chronic' gem,
158-
you can specify times in natural language.
175+
**Important:** When adding or subtracting large time units (years, decades, centuries),
176+
the duration is first converted to days and rounded to maintain calendar accuracy.
177+
This means `1 year` is treated as approximately 365 days rather than an exact number of seconds.
159178

160179
```ruby
161-
Unit.new('min').since(DateTime.parse('9/18/06 3:00pm'))
180+
Time.now + Unit.new("1 year") #=> Approximately 365 days from now
181+
Time.now - Unit.new("1 decade") #=> Approximately 3650 days ago
182+
```
183+
184+
For more precise durations, use smaller units (hours, minutes, seconds):
185+
186+
```ruby
187+
Time.now + Unit.new("24 hours") #=> Exactly 24 hours from now
188+
```
189+
190+
#### Converting Time and Date to Units
191+
192+
You can convert `Time` objects to units representing the duration since the Unix epoch:
193+
194+
```ruby
195+
Time.now.to_unit #=> Duration in seconds since epoch
196+
Time.now.to_unit('hours') #=> Duration in hours since epoch
197+
Time.now.to_unit('days') #=> Duration in days since epoch
198+
```
199+
200+
You can convert `Date` objects to units representing days since the Julian calendar start:
201+
202+
```ruby
203+
Date.today.to_unit #=> Duration in days since Julian calendar start
204+
Date.today.to_unit('week') #=> Duration in weeks since Julian calendar start
205+
Date.today.to_unit('year') #=> Duration in years since Julian calendar start
206+
```
207+
208+
#### Creating Time from Units
209+
210+
Use `Time.at` to create a Time object from a duration unit:
211+
212+
```ruby
213+
Time.at(Unit.new("1000 seconds")) #=> Time 1000 seconds after epoch
214+
Time.at(Unit.new("1 hour"), 500, :ms) #=> Time 1 hour + 500 milliseconds after epoch
162215
```
163216

217+
#### Convenience Methods
218+
219+
The `Time.in` method provides a shorthand for calculating future times:
220+
221+
```ruby
222+
Time.in('5 min') #=> 5 minutes from now
223+
Time.in('2 hours') #=> 2 hours from now
224+
```
225+
226+
#### Duration Formats
227+
164228
Durations may be entered as 'HH:MM:SS, usec' and will be returned in 'hours'.
165229

166230
```ruby
@@ -172,6 +236,21 @@ Unit.new('0:30:30') #=> 0.5 h + 30 sec
172236
If only one ":" is present, it is interpreted as the separator between hours and
173237
minutes.
174238

239+
#### Compatibility with Chronic
240+
241+
Several helpers are available for working with natural language time parsing.
242+
Note: If you include the 'Chronic' gem, you can specify times in natural language.
243+
244+
```ruby
245+
Unit.new('min').since(DateTime.parse('9/18/06 3:00pm'))
246+
```
247+
248+
#### Range Errors and DateTime Fallback
249+
250+
If time arithmetic would result in a date outside the valid range for the `Time` class
251+
(typically 1970-2038 on 32-bit systems), ruby-units automatically falls back to using
252+
`DateTime` objects to handle the calculation.
253+
175254
### Ranges
176255

177256
```ruby
@@ -180,10 +259,68 @@ minutes.
180259

181260
works so long as the starting point has an integer scalar
182261

183-
### Math functions
262+
### Math Functions
263+
264+
Ruby-units extends the `Math` module to support Unit objects seamlessly. All trigonometric
265+
and mathematical functions work with units, handling conversions automatically.
266+
267+
#### Supported Functions
268+
269+
**Trigonometric Functions** (angles converted to radians automatically):
270+
- `sin`, `cos`, `tan` - Standard trigonometric functions
271+
- `sinh`, `cosh`, `tanh` - Hyperbolic trigonometric functions
272+
273+
**Inverse Trigonometric Functions** (return angles in radians as Unit objects):
274+
- `asin`, `acos`, `atan` - Inverse trigonometric functions
275+
- `atan2` - Two-argument arctangent for full quadrant determination
276+
277+
**Root Functions** (preserve dimensional analysis):
278+
- `sqrt` - Square root (e.g., √(4 m²) = 2 m)
279+
- `cbrt` - Cube root (e.g., ³√(27 m³) = 3 m)
280+
281+
**Other Functions**:
282+
- `hypot` - Euclidean distance calculation with units
283+
- `log`, `log10` - Logarithmic functions (extract scalar from units)
184284

185-
All Trig math functions (sin, cos, sinh, hypot...) can take a unit as their
186-
parameter. It will be converted to radians and then used if possible.
285+
#### Examples
286+
287+
```ruby
288+
# Trigonometric functions with angle units
289+
Math.sin(Unit.new("90 deg")) #=> 1.0
290+
Math.cos(Unit.new("180 deg")) #=> -1.0
291+
Math.tan(Unit.new("45 deg")) #=> 1.0
292+
293+
# Works with different angle units
294+
Math.sin(Unit.new("1.571 rad")) #=> 1.0 (approximately π/2)
295+
Math.cos(Unit.new("3.14159 rad")) #=> -1.0 (approximately π)
296+
297+
# Inverse functions return Unit objects in radians
298+
Math.asin(0.5) #=> Unit.new("0.524 rad") (30°)
299+
Math.atan(1) #=> Unit.new("0.785 rad") (45°)
300+
Math.acos(0) #=> Unit.new("1.571 rad") (90°)
301+
302+
# Root functions preserve dimensional analysis
303+
Math.sqrt(Unit.new("4 m^2")) #=> Unit.new("2 m")
304+
Math.cbrt(Unit.new("27 m^3")) #=> Unit.new("3 m")
305+
Math.sqrt(Unit.new("9 kg*m/s^2")) #=> Unit.new("3 kg^(1/2)*m^(1/2)/s")
306+
307+
# Hypot for distance calculations (Pythagorean theorem)
308+
Math.hypot(Unit.new("3 m"), Unit.new("4 m")) #=> Unit.new("5 m")
309+
Math.hypot(Unit.new("30 cm"), Unit.new("40 cm")) #=> Unit.new("50 cm")
310+
311+
# atan2 for converting Cartesian to polar coordinates
312+
Math.atan2(Unit.new("1 m"), Unit.new("1 m")) #=> Unit.new("0.785 rad") (45°)
313+
Math.atan2(Unit.new("1 m"), Unit.new("0 m")) #=> Unit.new("1.571 rad") (90°)
314+
315+
# Logarithmic functions (units must be compatible for input)
316+
Math.log10(Unit.new("100")) #=> 2.0
317+
Math.log(Unit.new("2.718")) #=> 1.0 (natural log, approximately)
318+
Math.log(Unit.new("8"), 2) #=> 3.0 (log base 2)
319+
```
320+
321+
**Note:** Trigonometric functions expect angular units or dimensionless numbers. If you pass
322+
a Unit with dimensions (like meters), it will be converted to radians, which may produce
323+
unexpected results.
187324

188325
### Temperatures
189326

@@ -238,13 +375,37 @@ It is possible to define new units or redefine existing ones.
238375

239376
#### Define New Unit
240377

241-
The easiest approach is to define a unit in terms of other units.
378+
The easiest approach is to define a unit in terms of other units using the block form.
242379

243380
```ruby
244381
Unit.define("foobar") do |foobar|
245382
foobar.definition = Unit.new("1 foo") * Unit.new("1 bar") # anything that results in a Unit object
246-
foobar.aliases = %w{foobar fb} # array of synonyms for the unit
247-
foobar.display_name = "Foobar" # How unit is displayed when output
383+
foobar.aliases = %w{foobar fb} # array of synonyms for the unit
384+
foobar.display_name = "Foobar" # How unit is displayed when output
385+
end
386+
```
387+
388+
You can also create a unit definition directly and pass it to `Unit.define`:
389+
390+
```ruby
391+
unit_definition = Unit::Definition.new("foobar") do |foobar|
392+
foobar.definition = Unit.new("1 baz")
393+
foobar.aliases = %w{foobar fb}
394+
foobar.display_name = "Foobar"
395+
end
396+
Unit.define(unit_definition)
397+
```
398+
399+
For more control, you can set the unit attributes explicitly:
400+
401+
```ruby
402+
Unit.define("electron-volt") do |ev|
403+
ev.aliases = %w{eV electron-volt electron_volt}
404+
ev.scalar = 1.602e-19
405+
ev.kind = :energy
406+
ev.numerator = %w{<kilogram> <meter> <meter>}
407+
ev.denominator = %w{<second> <second>}
408+
ev.display_name = "electron-volt"
248409
end
249410
```
250411

@@ -306,14 +467,44 @@ Configuration options can be set like:
306467
```ruby
307468
RubyUnits.configure do |config|
308469
config.format = :rational
309-
config.separator = false
470+
config.separator = :none
471+
config.default_precision = 0.001
310472
end
311473
```
312474

313-
| Option | Description | Valid Values | Default |
314-
| --------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------- | ----------- |
315-
| format | Only used for output formatting. `:rational` is formatted like `3 m/s^2`. `:exponential` is formatted like `3 m*s^-2`. | `:rational, :exponential` | `:rational` |
316-
| separator | Use a space separator for output. `true` is formatted like `3 m/s`, `false` is like `3m/s`. | `true, false` | `true` |
475+
#### Configuration Options
476+
477+
| Option | Description | Valid Values | Default |
478+
|---------------------|------------------------------------------------------------------------------------------------------------------------|-----------------------------|-------------|
479+
| `format` | Only used for output formatting. `:rational` is formatted like `3 m/s^2`. `:exponential` is formatted like `3 m*s^-2`. | `:rational`, `:exponential` | `:rational` |
480+
| `separator` | Use a space separator for output. `:space` is formatted like `3 m/s`, `:none` is like `3m/s`. | `:space`, `:none` | `:space` |
481+
| `default_precision` | The precision used when rationalizing fractional values in unit output. | Any positive number | `0.0001` |
482+
483+
#### Examples
484+
485+
```ruby
486+
# Change output format to exponential notation
487+
RubyUnits.configure do |config|
488+
config.format = :exponential
489+
end
490+
# => "1 m*s^-2"
491+
492+
# Remove spaces between numbers and units
493+
RubyUnits.configure do |config|
494+
config.separator = :none
495+
end
496+
# => "1m/s"
497+
498+
# Adjust precision for rational number conversion
499+
RubyUnits.configure do |config|
500+
config.default_precision = 0.001
501+
end
502+
503+
# Reset to defaults
504+
RubyUnits.reset
505+
```
506+
507+
**Note:** Boolean values (`true`/`false`) for `separator` are deprecated but still supported for backward compatibility. Use `:space` instead of `true` and `:none` instead of `false`.
317508

318509
### NOTES
319510

0 commit comments

Comments
 (0)