Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
99afd22
Refactor unit methods to use unit_class for improved inheritance support
olbrich Dec 27, 2025
99e8d9e
Refactor unit handling for improved clarity and maintainability
olbrich Dec 27, 2025
0f22035
Add ruby-lsp gem to optional group in Gemfile and update Gemfile.lock
olbrich Dec 27, 2025
0b73e70
Refactor unit conversion methods for improved clarity and consistency
olbrich Dec 27, 2025
8d41285
Refactor unit conversion logic for improved readability and maintaina…
olbrich Dec 27, 2025
a20eda6
Add temperature and imperial conversion constants for improved clarity
olbrich Dec 27, 2025
4d541f0
Add default precision configuration and enhance unit formatting methods
olbrich Dec 27, 2025
dab5a55
Refactor unit conversion methods for improved clarity and consistency
olbrich Dec 27, 2025
bfbeefb
Refactor configuration options for clarity and deprecation warnings
olbrich Dec 27, 2025
e49f105
Refactor configuration documentation for clarity and completeness
olbrich Dec 27, 2025
8183e9f
Enhance unit definition documentation and initialization for clarity …
olbrich Dec 27, 2025
9920424
Enhance time arithmetic methods for unit-based calculations and impro…
olbrich Dec 27, 2025
aade926
Enhance documentation for Math module to clarify unit-aware mathemati…
olbrich Dec 27, 2025
520b9a0
Enhance Date module to support addition and subtraction of durations,…
olbrich Dec 27, 2025
f438de0
Enhance string conversion methods to support formatting and direct un…
olbrich Dec 27, 2025
6caaba7
Add ruby-lsp gem to optional group for improved language server support
olbrich Dec 27, 2025
b2af823
Remove version constraint for ruby-lsp in Gemfile.lock
olbrich Dec 27, 2025
70a08fc
Refactor documentation for clarity in Time and Unit modules
olbrich Dec 27, 2025
0ccb6d4
Remove deprecated parameters from inspect method in Date module
olbrich Dec 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ group :optional do
gem "pry"
gem "redcarpet", platform: :mri # redcarpet doesn't support jruby
gem "reek"
gem "ruby-lsp"
gem "ruby-maven", platform: :jruby
gem "ruby-prof", platform: :mri
gem "simplecov-html"
Expand Down
5 changes: 5 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ GEM
rubocop-rspec (3.8.0)
lint_roller (~> 1.1)
rubocop (~> 1.81)
ruby-lsp (0.26.4)
language_server-protocol (~> 3.17.0)
prism (>= 1.2, < 2.0)
rbs (>= 3, < 5)
ruby-maven (3.9.3)
ruby-maven-libs (~> 3.9.9)
ruby-maven-libs (3.9.9)
Expand Down Expand Up @@ -261,6 +265,7 @@ DEPENDENCIES
rubocop
rubocop-rake
rubocop-rspec
ruby-lsp
ruby-maven
ruby-prof
ruby-units!
Expand Down
231 changes: 211 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
# Ruby Units

[![Maintainability](https://api.codeclimate.com/v1/badges/4e858d14a07dd453f748/maintainability.svg)](https://codeclimate.com/github/olbrich/ruby-units/maintainability)
[![CodeClimate Status](https://api.codeclimate.com/v1/badges/4e858d14a07dd453f748/test_coverage.svg)](https://codeclimate.com/github/olbrich/ruby-units/test_coverage)
[![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)

Kevin C. Olbrich, Ph.D.

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

You can also use Ruby's standard string formatting operator (`%`) with units:

```ruby
'%0.2f' % '1 mm'.to_unit # "1.00 mm"
'%0.2f in' % '1 mm'.to_unit # "0.04 in" - format and convert
'%.2e' % '1000 m'.to_unit # "1.00e+03 m" - scientific notation
```

Strings can be converted to units using the `convert_to` method:

```ruby
'10 mm'.convert_to('cm') # converts directly to centimeters
'100 km/h'.convert_to('m/s') # converts compound units
```

### Time Helpers

`Time`, `Date`, and `DateTime` objects can have time units added or subtracted.
Ruby-units extends the `Time`, `Date`, and `DateTime` classes to support unit-based arithmetic,
allowing you to add or subtract durations from time objects naturally.

#### Adding and Subtracting Durations

```ruby
Time.now + Unit.new("10 min")
Time.now + Unit.new("10 min") #=> 10 minutes from now
Time.now - Unit.new("2 hours") #=> 2 hours ago

Date.today + Unit.new("1 week") #=> 7 days from today
Date.today - Unit.new("30 days") #=> 30 days ago
```

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

```ruby
Unit.new('min').since(DateTime.parse('9/18/06 3:00pm'))
Time.now + Unit.new("1 year") #=> Approximately 365 days from now
Time.now - Unit.new("1 decade") #=> Approximately 3650 days ago
```

For more precise durations, use smaller units (hours, minutes, seconds):

```ruby
Time.now + Unit.new("24 hours") #=> Exactly 24 hours from now
```

#### Converting Time and Date to Units

You can convert `Time` objects to units representing the duration since the Unix epoch:

```ruby
Time.now.to_unit #=> Duration in seconds since epoch
Time.now.to_unit('hours') #=> Duration in hours since epoch
Time.now.to_unit('days') #=> Duration in days since epoch
```

You can convert `Date` objects to units representing days since the Julian calendar start:

```ruby
Date.today.to_unit #=> Duration in days since Julian calendar start
Date.today.to_unit('week') #=> Duration in weeks since Julian calendar start
Date.today.to_unit('year') #=> Duration in years since Julian calendar start
```

#### Creating Time from Units

Use `Time.at` to create a Time object from a duration unit:

```ruby
Time.at(Unit.new("1000 seconds")) #=> Time 1000 seconds after epoch
Time.at(Unit.new("1 hour"), 500, :ms) #=> Time 1 hour + 500 milliseconds after epoch
```

#### Convenience Methods

The `Time.in` method provides a shorthand for calculating future times:

```ruby
Time.in('5 min') #=> 5 minutes from now
Time.in('2 hours') #=> 2 hours from now
```

#### Duration Formats

Durations may be entered as 'HH:MM:SS, usec' and will be returned in 'hours'.

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

#### Compatibility with Chronic

Several helpers are available for working with natural language time parsing.
Note: If you include the 'Chronic' gem, you can specify times in natural language.

```ruby
Unit.new('min').since(DateTime.parse('9/18/06 3:00pm'))
```

#### Range Errors and DateTime Fallback

If time arithmetic would result in a date outside the valid range for the `Time` class
(typically 1970-2038 on 32-bit systems), ruby-units automatically falls back to using
`DateTime` objects to handle the calculation.

### Ranges

```ruby
Expand All @@ -180,10 +259,68 @@ minutes.

works so long as the starting point has an integer scalar

### Math functions
### Math Functions

Ruby-units extends the `Math` module to support Unit objects seamlessly. All trigonometric
and mathematical functions work with units, handling conversions automatically.

#### Supported Functions

**Trigonometric Functions** (angles converted to radians automatically):
- `sin`, `cos`, `tan` - Standard trigonometric functions
- `sinh`, `cosh`, `tanh` - Hyperbolic trigonometric functions

**Inverse Trigonometric Functions** (return angles in radians as Unit objects):
- `asin`, `acos`, `atan` - Inverse trigonometric functions
- `atan2` - Two-argument arctangent for full quadrant determination

**Root Functions** (preserve dimensional analysis):
- `sqrt` - Square root (e.g., √(4 m²) = 2 m)
- `cbrt` - Cube root (e.g., ³√(27 m³) = 3 m)

**Other Functions**:
- `hypot` - Euclidean distance calculation with units
- `log`, `log10` - Logarithmic functions (extract scalar from units)

All Trig math functions (sin, cos, sinh, hypot...) can take a unit as their
parameter. It will be converted to radians and then used if possible.
#### Examples

```ruby
# Trigonometric functions with angle units
Math.sin(Unit.new("90 deg")) #=> 1.0
Math.cos(Unit.new("180 deg")) #=> -1.0
Math.tan(Unit.new("45 deg")) #=> 1.0

# Works with different angle units
Math.sin(Unit.new("1.571 rad")) #=> 1.0 (approximately π/2)
Math.cos(Unit.new("3.14159 rad")) #=> -1.0 (approximately π)

# Inverse functions return Unit objects in radians
Math.asin(0.5) #=> Unit.new("0.524 rad") (30°)
Math.atan(1) #=> Unit.new("0.785 rad") (45°)
Math.acos(0) #=> Unit.new("1.571 rad") (90°)

# Root functions preserve dimensional analysis
Math.sqrt(Unit.new("4 m^2")) #=> Unit.new("2 m")
Math.cbrt(Unit.new("27 m^3")) #=> Unit.new("3 m")
Math.sqrt(Unit.new("9 kg*m/s^2")) #=> Unit.new("3 kg^(1/2)*m^(1/2)/s")

# Hypot for distance calculations (Pythagorean theorem)
Math.hypot(Unit.new("3 m"), Unit.new("4 m")) #=> Unit.new("5 m")
Math.hypot(Unit.new("30 cm"), Unit.new("40 cm")) #=> Unit.new("50 cm")

# atan2 for converting Cartesian to polar coordinates
Math.atan2(Unit.new("1 m"), Unit.new("1 m")) #=> Unit.new("0.785 rad") (45°)
Math.atan2(Unit.new("1 m"), Unit.new("0 m")) #=> Unit.new("1.571 rad") (90°)

# Logarithmic functions (units must be compatible for input)
Math.log10(Unit.new("100")) #=> 2.0
Math.log(Unit.new("2.718")) #=> 1.0 (natural log, approximately)
Math.log(Unit.new("8"), 2) #=> 3.0 (log base 2)
```

**Note:** Trigonometric functions expect angular units or dimensionless numbers. If you pass
a Unit with dimensions (like meters), it will be converted to radians, which may produce
unexpected results.

### Temperatures

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

#### Define New Unit

The easiest approach is to define a unit in terms of other units.
The easiest approach is to define a unit in terms of other units using the block form.

```ruby
Unit.define("foobar") do |foobar|
foobar.definition = Unit.new("1 foo") * Unit.new("1 bar") # anything that results in a Unit object
foobar.aliases = %w{foobar fb} # array of synonyms for the unit
foobar.display_name = "Foobar" # How unit is displayed when output
foobar.aliases = %w{foobar fb} # array of synonyms for the unit
foobar.display_name = "Foobar" # How unit is displayed when output
end
```

You can also create a unit definition directly and pass it to `Unit.define`:

```ruby
unit_definition = Unit::Definition.new("foobar") do |foobar|
foobar.definition = Unit.new("1 baz")
foobar.aliases = %w{foobar fb}
foobar.display_name = "Foobar"
end
Unit.define(unit_definition)
```

For more control, you can set the unit attributes explicitly:

```ruby
Unit.define("electron-volt") do |ev|
ev.aliases = %w{eV electron-volt electron_volt}
ev.scalar = 1.602e-19
ev.kind = :energy
ev.numerator = %w{<kilogram> <meter> <meter>}
ev.denominator = %w{<second> <second>}
ev.display_name = "electron-volt"
end
```

Expand Down Expand Up @@ -306,14 +467,44 @@ Configuration options can be set like:
```ruby
RubyUnits.configure do |config|
config.format = :rational
config.separator = false
config.separator = :none
config.default_precision = 0.001
end
```

| Option | Description | Valid Values | Default |
| --------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------- | ----------- |
| 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` |
| separator | Use a space separator for output. `true` is formatted like `3 m/s`, `false` is like `3m/s`. | `true, false` | `true` |
#### Configuration Options

| Option | Description | Valid Values | Default |
|---------------------|------------------------------------------------------------------------------------------------------------------------|-----------------------------|-------------|
| `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` |
| `separator` | Use a space separator for output. `:space` is formatted like `3 m/s`, `:none` is like `3m/s`. | `:space`, `:none` | `:space` |
| `default_precision` | The precision used when rationalizing fractional values in unit output. | Any positive number | `0.0001` |

#### Examples

```ruby
# Change output format to exponential notation
RubyUnits.configure do |config|
config.format = :exponential
end
# => "1 m*s^-2"

# Remove spaces between numbers and units
RubyUnits.configure do |config|
config.separator = :none
end
# => "1m/s"

# Adjust precision for rational number conversion
RubyUnits.configure do |config|
config.default_precision = 0.001
end

# Reset to defaults
RubyUnits.reset
```

**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`.

### NOTES

Expand Down
Loading