Horologium

Tests

Horologium is a Ruby library dedicated to scientific time: the time scales (UTC, TAI, TT, TDB, TCG, TCB, UT1, GPS), high-precision instants, Julian Dates, intervals, and rigorous conversions between scales that astronomy and physics require.

Ruby already has Time, Date, DateTime, and ActiveSupport for civil time: time zones, calendars, human formatting. None of them knows the difference between UTC and a continuous scale, the TAI, TT, and TDB scales an ephemeris needs, or a Julian Date kept precise to the nanosecond. That is the gap Horologium fills.

Content

Installation

Install the gem and add it to the application's Gemfile by executing:

$ bundle add horologium

If Bundler is not being used to manage dependencies, install the gem by executing:

$ gem install horologium

Usage

An Instant is a single point on the timeline, kept internally as a TAI Julian Date. A Duration is an amount of time in SI seconds, with no date and no scale attached. You shift an instant by a duration, and you subtract two instants to get the duration between them.

require "horologium"

instant = Horologium::Instant.from_tai_julian_date(2_460_000.5)

later = instant + Horologium::Duration.days(1)
later == Horologium::Instant.from_tai_julian_date(2_460_001.5)  # => true
instant < later                                                 # => true

b = Horologium::Instant.from_tai_julian_date(2_460_001.5)
b - instant == Horologium::Duration.days(1)                     # => true

A Duration counts SI seconds, so Duration.days(1) is always 86,400 SI seconds. Because of leap seconds a civil day can be a second longer or shorter, so a duration and a calendar day are different things.

Horologium::Duration.days(1) == Horologium::Duration.seconds(86_400)  # => true
Horologium::Duration.nanoseconds(1_000_000_000) ==
  Horologium::Duration.seconds(1)                                     # => true

Adding a duration to an instant makes sense, but adding two instants together does not, so it raises an error.

instant + instant  # => raises Horologium::DimensionalError

Exact equality is rarely what scientific code wants, so you can compare within a tolerance:

a = Horologium::Instant.from_tai_julian_date(2_460_000.5)
near = a + Horologium::Duration.nanoseconds(1)

a.equal_within?(near, Horologium::Duration.nanoseconds(2))  # => true

Precision

A modern Julian Date is around 2.46 million. A single Float spends most of its digits on that large number and has only tens of microseconds left for the fraction of a day. That is too coarse for scientific time. Horologium stores an instant across two Floats whose sum is the Julian Date, so the second one starts where the first runs out of digits. This is the representation ERFA uses, it keeps the precision below a nanosecond for any date, and it does so with ordinary floating-point arithmetic.

Every value carries one of two precisions, fixed when it is built:

  • :standard, the default, keeps the value as a two-part float. It is fast and stays within a few nanoseconds of the true value.
  • :exact keeps the value as a Rational, with no rounding. The test suite uses it to check that :standard stays within its stated precision.

Set the default once at boot:

Horologium.configure do |c|
  c.default_precision = :exact
end

Choose it for a single value, or for a scoped block:

Horologium::Instant.from_tai_julian_date(2_460_000.5, precision: :exact)

Horologium.with_precision(:exact) do
  # instants and durations built here default to :exact
end

Exactness is contagious. An operation between two :standard values stays :standard. Mixing a :standard and an :exact value gives an :exact result, so precision is not quietly lost. :exact guarantees the arithmetic Horologium performs. It cannot bring back precision that an input already lost when it was built.

Status

This library is in early development, before its first public release. The public API is not stable, so new versions will probably introduce breaking changes until a 1.0 release. Changes are documented in the CHANGELOG.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake to run the tests and RuboCop, or rake steep to type-check the signatures in sig/. Run COVERAGE=true rake test to measure test coverage, which is enforced at a 95% line minimum in CI. You can also run bin/console for an interactive prompt that will allow you to experiment.

Run bin/ci to run every check that GitHub Actions runs (RuboCop, Steep, YARD documentation coverage, and the tests with coverage) in a single pass. It runs each check even when an earlier one fails, so you see everything that needs fixing at once.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/rhannequin/horologium.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Horologium project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.