horologium 0.0.1 → 0.0.3
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 +4 -4
- data/CHANGELOG.md +72 -0
- data/README.md +304 -9
- data/Rakefile +16 -3
- data/lib/horologium/configuration.rb +291 -0
- data/lib/horologium/data/barycentric_model.rb +901 -0
- data/lib/horologium/data/leap_seconds.rb +55 -0
- data/lib/horologium/duration.rb +170 -0
- data/lib/horologium/error.rb +120 -0
- data/lib/horologium/instant.rb +505 -0
- data/lib/horologium/numeric/exact.rb +116 -0
- data/lib/horologium/numeric/precision.rb +172 -0
- data/lib/horologium/numeric/two_part_float.rb +298 -0
- data/lib/horologium/precise_value.rb +79 -0
- data/lib/horologium/representations/civil.rb +518 -0
- data/lib/horologium/representations/civil_time.rb +162 -0
- data/lib/horologium/representations/iso8601.rb +286 -0
- data/lib/horologium/representations/julian_date.rb +227 -0
- data/lib/horologium/representations/modified_julian_date.rb +106 -0
- data/lib/horologium/scale_reading.rb +126 -0
- data/lib/horologium/scales/base.rb +118 -0
- data/lib/horologium/scales/tai.rb +41 -0
- data/lib/horologium/scales/tdb.rb +75 -0
- data/lib/horologium/scales/tt.rb +71 -0
- data/lib/horologium/scales/utc.rb +374 -0
- data/lib/horologium/version.rb +1 -1
- data/lib/horologium.rb +24 -2
- data/sig/horologium/configuration.rbs +41 -0
- data/sig/horologium/data/barycentric_model.rbs +19 -0
- data/sig/horologium/data/leap_seconds.rbs +11 -0
- data/sig/horologium/duration.rbs +37 -0
- data/sig/horologium/instant.rbs +51 -0
- data/sig/horologium/numeric/exact.rbs +31 -0
- data/sig/horologium/numeric/precision.rbs +27 -0
- data/sig/horologium/numeric/two_part_float.rbs +57 -0
- data/sig/horologium/numeric.rbs +5 -0
- data/sig/horologium/precise_value.rbs +25 -0
- data/sig/horologium/representations/civil.rbs +63 -0
- data/sig/horologium/representations/civil_time.rbs +49 -0
- data/sig/horologium/representations/iso8601.rbs +27 -0
- data/sig/horologium/representations/julian_date.rbs +25 -0
- data/sig/horologium/representations/modified_julian_date.rbs +15 -0
- data/sig/horologium/scale_reading.rbs +33 -0
- data/sig/horologium/scales/base.rbs +17 -0
- data/sig/horologium/scales/tai.rbs +9 -0
- data/sig/horologium/scales/tdb.rbs +11 -0
- data/sig/horologium/scales/tt.rbs +17 -0
- data/sig/horologium/scales/utc.rbs +49 -0
- data/sig/horologium.rbs +56 -0
- metadata +77 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 513d825c2d141d84274c76333af312631c4b9a077803d2e2921033782af518ce
|
|
4
|
+
data.tar.gz: '0108ee60e9bf9c1f149baefafe6e78f8d735dd824148ed91c69267b5ca10aad8'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: edfa8b82fc1b2d4f795abbf0c01b0a9753cbfb31aa03c37de952b5ea64e62687207bb4efde740d05246ec9df63b6226d682d998ef5d917e401f48fae2cf4402d
|
|
7
|
+
data.tar.gz: a75b50d13419e546ae0b8cca4d0dce80d078a90578e3e80664a65b2f207ae5535900eb9f36a6291b5a303ce41ccd834b9cd5c226c46e4992d081cf1949b2ba56
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.3 - 2026-08-22
|
|
4
|
+
|
|
5
|
+
The scale conversions arrive. An instant is now a point with no scale of its
|
|
6
|
+
own, given in one scale and read back in another, and it comes out as a Julian
|
|
7
|
+
Date, a calendar date, or an ISO 8601 string.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- Add time scales, each converting to and from TAI, the scale an instant is
|
|
12
|
+
stored in: `TAI`, `TT` at its fixed 32.184 s, `TDB` over the full 787-term
|
|
13
|
+
Fairhead and Bretagnon model, and `UTC` with its leap seconds
|
|
14
|
+
- Add `Instant#to`, which reads an instant in a scale, and `ScaleReading`,
|
|
15
|
+
which is that reading, with `#as` for the shape it comes out in
|
|
16
|
+
- Add the representations a reading comes out as: `julian_date`,
|
|
17
|
+
`modified_julian_date`, `civil`, and `iso8601`, each as a Float, a Rational,
|
|
18
|
+
or a two-part Float where that makes sense
|
|
19
|
+
- Add `Instant.from_julian_date`, `from_modified_julian_date`, `from_civil`,
|
|
20
|
+
and `from_iso8601`, each reading its value in a named scale
|
|
21
|
+
- Add `Instant.from_tai`, `from_tt`, `from_tdb`, and `from_utc`, the same
|
|
22
|
+
calendar constructor with the scale in the name
|
|
23
|
+
- Add `CivilTime`, the calendar fields a clock and a calendar show, in the
|
|
24
|
+
proleptic Gregorian calendar with astronomical year numbering
|
|
25
|
+
- Read and write a leap second as second 60, on the days that hold one, and
|
|
26
|
+
refuse it on the days that do not
|
|
27
|
+
- Add `ScaleReading#provenance`, `:measured` up to the date the leap second
|
|
28
|
+
data vouches for and `:extrapolated` after it, with
|
|
29
|
+
`Configuration#leap_second_horizon` to refuse an extrapolation instead
|
|
30
|
+
- Add `Configuration#register_scale`, so a caller can add a scale of its own,
|
|
31
|
+
and `Configuration#leap_second_source`, so it can supply its own leap seconds
|
|
32
|
+
- Add duration arithmetic: `+`, `-`, unary `-`, `zero?`, `negative?`,
|
|
33
|
+
`positive?`, and `to_r` and `to_f` in SI seconds
|
|
34
|
+
- Add value equality to `ScaleReading`, and `inspect` to `Instant`, `Duration`,
|
|
35
|
+
and `ScaleReading`
|
|
36
|
+
- Refuse a calendar reading before -4799, where the conversion stops, so a
|
|
37
|
+
reading out always reads back in
|
|
38
|
+
|
|
39
|
+
### Fixes
|
|
40
|
+
|
|
41
|
+
- Build the configuration under a lock, so two threads reaching it at once
|
|
42
|
+
cannot each build one and lose the other's scales
|
|
43
|
+
|
|
44
|
+
**Full Changelog**: https://github.com/rhannequin/horologium/compare/v0.0.2...v0.0.3
|
|
45
|
+
|
|
46
|
+
## 0.0.2 - 2026-07-14
|
|
47
|
+
|
|
48
|
+
The first functional release. It ships the numeric core and the two value
|
|
49
|
+
objects the rest of the library is built on: `Instant` and `Duration`. The
|
|
50
|
+
scale conversions are not here yet, so an instant is built directly from a
|
|
51
|
+
TAI Julian Date for now.
|
|
52
|
+
|
|
53
|
+
### Features
|
|
54
|
+
|
|
55
|
+
- Add `Numeric::TwoPartFloat`, a number kept as a high and a low `Float` for
|
|
56
|
+
about twice the precision of one, with Shewchuk error-free arithmetic
|
|
57
|
+
- Add `Numeric::Exact`, a value kept as an exact `Rational`, with no rounding
|
|
58
|
+
- Add the precision contract: every value carries a precision, `:standard` or
|
|
59
|
+
`:exact`, set when it is built and never changed. Mixing the two promotes
|
|
60
|
+
the result to `:exact` instead of dropping to `:standard`
|
|
61
|
+
- Add `Horologium.configure` for the set-once default precision, and
|
|
62
|
+
`Horologium.with_precision` for a scoped, per-fiber override
|
|
63
|
+
- Add `Instant`, a frozen point on the TAI timeline, built with
|
|
64
|
+
`Instant.from_tai_julian_date`
|
|
65
|
+
- Add `Duration`, a frozen span in SI seconds, built with `Duration.seconds`,
|
|
66
|
+
`Duration.days`, and `Duration.nanoseconds`
|
|
67
|
+
- Add instant and duration arithmetic: shift an instant by a duration, and
|
|
68
|
+
subtract two instants to measure the duration between them
|
|
69
|
+
- Add `Instant#equal_within?` for comparison inside a tolerance
|
|
70
|
+
- Guard against meaningless operations: adding two instants raises
|
|
71
|
+
`DimensionalError`
|
|
72
|
+
|
|
73
|
+
**Full Changelog**: https://github.com/rhannequin/horologium/compare/v0.0.1...v0.0.2
|
|
74
|
+
|
|
3
75
|
## 0.0.1 - 2026-07-06
|
|
4
76
|
|
|
5
77
|
- Gem creation
|
data/README.md
CHANGED
|
@@ -3,30 +3,322 @@
|
|
|
3
3
|
[](https://github.com/rhannequin/horologium/actions?query=workflow%3ACI)
|
|
4
4
|
|
|
5
5
|
Horologium is a Ruby library dedicated to **scientific time**: the time scales
|
|
6
|
-
(
|
|
6
|
+
(TAI, TT, TDB, and UTC so far), high-precision instants, Julian Dates,
|
|
7
7
|
intervals, and rigorous conversions between scales that astronomy and physics
|
|
8
8
|
require.
|
|
9
9
|
|
|
10
|
+
Ruby already has `Time`, `Date`, `DateTime`, and `ActiveSupport` for civil time:
|
|
11
|
+
time zones, calendars, human formatting. None of them knows the difference
|
|
12
|
+
between UTC and a continuous scale, the TAI, TT, and TDB scales an ephemeris
|
|
13
|
+
needs, or a Julian Date kept precise to the nanosecond. That is the gap
|
|
14
|
+
Horologium fills.
|
|
15
|
+
|
|
16
|
+
## Content
|
|
17
|
+
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Usage](#usage)
|
|
20
|
+
- [Precision](#precision)
|
|
21
|
+
- [Status](#status)
|
|
22
|
+
- [Development](#development)
|
|
23
|
+
- [Contributing](#contributing)
|
|
24
|
+
- [License](#license)
|
|
25
|
+
- [Code of Conduct](#code-of-conduct)
|
|
26
|
+
|
|
10
27
|
## Installation
|
|
11
28
|
|
|
12
|
-
Install the gem and add it to the application's Gemfile by
|
|
29
|
+
Install the gem and add it to the application's Gemfile by executing:
|
|
30
|
+
|
|
31
|
+
$ bundle add horologium
|
|
32
|
+
|
|
33
|
+
If [Bundler] is not being used to manage dependencies, install the gem by
|
|
34
|
+
executing:
|
|
35
|
+
|
|
36
|
+
$ gem install horologium
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
An `Instant` is a single point on the timeline, kept internally as a TAI Julian
|
|
41
|
+
Date. A `Duration` is an amount of time in SI seconds, with no date and no scale
|
|
42
|
+
attached. You shift an instant by a duration, and you subtract two instants to
|
|
43
|
+
get the duration between them.
|
|
44
|
+
|
|
45
|
+
```rb
|
|
46
|
+
require "horologium"
|
|
47
|
+
|
|
48
|
+
a = Horologium::Instant.from_julian_date(2_460_000.5, scale: :tai)
|
|
49
|
+
b = Horologium::Instant.from_julian_date(2_460_001.5, scale: :tai)
|
|
50
|
+
|
|
51
|
+
a + Horologium::Duration.days(1) == b # => true
|
|
52
|
+
a < b # => true
|
|
53
|
+
b - a == Horologium::Duration.days(1) # => true
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
An instant has no scale of its own. You give it a Julian Date read in a scale,
|
|
57
|
+
and you read it back in any scale the library knows: `to` chooses the scale, and
|
|
58
|
+
`as` chooses the shape it comes out in.
|
|
59
|
+
|
|
60
|
+
```rb
|
|
61
|
+
instant = Horologium::Instant.from_julian_date(2_443_144.5, scale: :tai)
|
|
62
|
+
|
|
63
|
+
instant.to(:tt).as(:julian_date) # => 2443144.5003725
|
|
64
|
+
instant.as(:modified_julian_date, scale: :tt) # => 43144.0003725
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A calendar date is a shape too, and the one a person reads. It comes out as a
|
|
68
|
+
`CivilTime`, whose fields are the ones a clock and a calendar show, in the
|
|
69
|
+
proleptic Gregorian calendar.
|
|
70
|
+
|
|
71
|
+
```rb
|
|
72
|
+
civil = instant.as(:civil, scale: :tt)
|
|
73
|
+
|
|
74
|
+
civil.year # => 1977
|
|
75
|
+
civil.month # => 1
|
|
76
|
+
civil.second # => 32
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
You can build an instant from those fields as well, and nothing is lost on the
|
|
80
|
+
way in: the date becomes a whole number of days and the time of day an exact
|
|
81
|
+
fraction of one. Give a fractional second as a `Rational` to say it exactly.
|
|
13
82
|
|
|
14
|
-
```
|
|
15
|
-
|
|
83
|
+
```rb
|
|
84
|
+
Horologium::Instant.from_civil(2025, 5, 1, 12, 0, 0, scale: :tt)
|
|
85
|
+
Horologium::Instant.from_civil(2025, 5, 1, 12, 0, Rational(1, 4), scale: :tt)
|
|
16
86
|
```
|
|
17
87
|
|
|
18
|
-
|
|
88
|
+
Each scale has its own shortcut, so the scale is in the name instead of a
|
|
89
|
+
keyword.
|
|
90
|
+
|
|
91
|
+
```rb
|
|
92
|
+
Horologium::Instant.from_tt(2025, 5, 1, 12, 0, 0)
|
|
93
|
+
Horologium::Instant.from_tai(2025, 5, 1, 12, 0, 0)
|
|
94
|
+
Horologium::Instant.from_tdb(2025, 5, 1, 12, 0, 0)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A date that does not exist is refused rather than rolled over, and the message
|
|
98
|
+
says which field is wrong.
|
|
99
|
+
|
|
100
|
+
```rb
|
|
101
|
+
Horologium::Instant.from_civil(1900, 2, 29, scale: :tt)
|
|
102
|
+
# => raises Horologium::InvalidCivilTimeError
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The same date and time write out as an extended ISO 8601 string, and read back
|
|
106
|
+
from one. The scale is not written into the string: there is no ISO 8601
|
|
107
|
+
designator for TAI or TT, and `Z` means UTC, so a bare time is a coordinate in
|
|
108
|
+
the scale you asked for.
|
|
109
|
+
|
|
110
|
+
```rb
|
|
111
|
+
instant.as(:iso8601, scale: :tt) # => "1977-01-01T00:00:32.184000000"
|
|
112
|
+
|
|
113
|
+
Horologium::Instant.from_iso8601("2025-05-01T12:00:00", scale: :tt)
|
|
114
|
+
Horologium::Instant.from_iso8601("2025-05-01", scale: :tt) # midnight
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The parser reads a strict subset: a calendar date, an optional time of day
|
|
118
|
+
after a `T`, a fraction of a second kept to every digit, and an optional `Z` or
|
|
119
|
+
numeric offset applied as plain arithmetic, not a time zone. A week date, an
|
|
120
|
+
ordinal date, or anything outside the subset is refused with a `ParseError`.
|
|
121
|
+
|
|
122
|
+
UTC is the scale of civil clocks, the one that holds a leap second now and
|
|
123
|
+
then to keep step with the Earth's rotation. `from_utc` reads a UTC date, and
|
|
124
|
+
a leap second is a legal reading: the second is 60 on a day that holds one, and
|
|
125
|
+
that moment really existed.
|
|
126
|
+
|
|
127
|
+
```rb
|
|
128
|
+
Horologium::Instant.from_utc(2025, 5, 1, 12, 0, 0)
|
|
129
|
+
|
|
130
|
+
leap = Horologium::Instant.from_utc(2016, 12, 31, 23, 59, 60)
|
|
131
|
+
leap.as(:iso8601, scale: :utc) # => "2016-12-31T23:59:60.000000000Z"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
A leap second is a real second on the timeline, so the arithmetic is right
|
|
135
|
+
across it: the second before 23:59:60, the leap second, and the next midnight
|
|
136
|
+
are one SI second apart each. Second 60 on a day with no leap second is
|
|
137
|
+
refused.
|
|
138
|
+
|
|
139
|
+
```rb
|
|
140
|
+
before = Horologium::Instant.from_utc(
|
|
141
|
+
2016, 12, 31, 23, 59, 59,
|
|
142
|
+
precision: :exact
|
|
143
|
+
)
|
|
144
|
+
leap = Horologium::Instant.from_utc(
|
|
145
|
+
2016, 12, 31, 23, 59, 60,
|
|
146
|
+
precision: :exact
|
|
147
|
+
)
|
|
148
|
+
after = Horologium::Instant.from_utc(
|
|
149
|
+
2017, 1, 1, 0, 0, 0,
|
|
150
|
+
precision: :exact
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
leap - before == Horologium::Duration.seconds(1) # => true
|
|
154
|
+
after - leap == Horologium::Duration.seconds(1) # => true
|
|
155
|
+
|
|
156
|
+
Horologium::Instant.from_utc(2020, 6, 15, 23, 59, 60)
|
|
157
|
+
# => raises Horologium::InvalidCivilTimeError
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The `:exact` above is what makes `==` the right question to ask. At the default
|
|
161
|
+
`:standard` precision the same three instants land a rounding step apart, well
|
|
162
|
+
under a nanosecond but not zero, so compare those with `equal_within?`.
|
|
163
|
+
|
|
164
|
+
UTC runs from 1961, whole leap seconds from 1972 and the earlier
|
|
165
|
+
rate-adjustment drift before that, where a UTC second was fractionally longer
|
|
166
|
+
than an SI one. An earlier UTC date raises `Horologium::OutOfRangeError`. The
|
|
167
|
+
instant is still reachable, only its UTC label is not, so the error names the
|
|
168
|
+
continuous scales, which have no lower bound. The leap seconds and the drift
|
|
169
|
+
come from the [iers] gem, with no network access: the data ships with the gem.
|
|
19
170
|
|
|
20
|
-
```
|
|
21
|
-
|
|
171
|
+
```rb
|
|
172
|
+
Horologium::Instant.from_utc(1960, 12, 31) # => OutOfRangeError
|
|
173
|
+
Horologium::Instant.from_civil(1960, 12, 31, scale: :tt) # reaches any date
|
|
22
174
|
```
|
|
23
175
|
|
|
176
|
+
Leap seconds are announced about six months ahead, so past the date its data
|
|
177
|
+
vouches for, the last known offset is the best there is. A UTC reading says
|
|
178
|
+
which it rests on: `:measured` up to that date, `:extrapolated` after, where a
|
|
179
|
+
leap second announced since would not be known.
|
|
180
|
+
|
|
181
|
+
```rb
|
|
182
|
+
instant.to(:utc).provenance # => :measured, or :extrapolated past the horizon
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
A pipeline that must not rest on an offset a leap second could overturn sets a
|
|
186
|
+
strict horizon, and a reading past it raises instead.
|
|
187
|
+
|
|
188
|
+
```rb
|
|
189
|
+
Horologium.configure { |c| c.leap_second_horizon = :raise }
|
|
190
|
+
# => reading a date past the data horizon raises OutOfDataRangeError
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The configuration is set once, in a single `Horologium.configure` block, and
|
|
194
|
+
frozen when the block returns. See [Precision](#precision).
|
|
195
|
+
|
|
196
|
+
A Julian Date is around 2.46 million, which leaves a single `Float` about 40
|
|
197
|
+
microseconds for the fraction of a day, and the loss is already in the literal
|
|
198
|
+
before Horologium sees it. So the lossless shapes come first: a `String` and a
|
|
199
|
+
`Rational` say the Julian Date exactly, and a high and a low part say it to
|
|
200
|
+
about twice what one `Float` holds.
|
|
201
|
+
|
|
202
|
+
```rb
|
|
203
|
+
Horologium::Instant.from_julian_date("2456463.052272", scale: :tt)
|
|
204
|
+
Horologium::Instant.from_julian_date(
|
|
205
|
+
Rational(2_456_463_052_272, 1_000_000),
|
|
206
|
+
scale: :tt
|
|
207
|
+
)
|
|
208
|
+
Horologium::Instant.from_julian_date(2_456_463.0, 0.052272, scale: :tt)
|
|
209
|
+
|
|
210
|
+
Horologium::Instant.from_modified_julian_date(60_796.0, scale: :tai)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
A `Duration` counts SI seconds, so `Duration.days(1)` is always 86,400 SI
|
|
214
|
+
seconds. Because of leap seconds a civil day can be a second longer or shorter,
|
|
215
|
+
so a duration and a calendar day are different things.
|
|
216
|
+
|
|
217
|
+
```rb
|
|
218
|
+
Horologium::Duration.days(1) == Horologium::Duration.seconds(86_400) # => true
|
|
219
|
+
Horologium::Duration.nanoseconds(1_000_000_000) ==
|
|
220
|
+
Horologium::Duration.seconds(1) # => true
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Durations add, subtract, and negate among themselves, and read back out in SI
|
|
224
|
+
seconds.
|
|
225
|
+
|
|
226
|
+
```rb
|
|
227
|
+
Horologium::Duration.seconds(30) + Horologium::Duration.seconds(12)
|
|
228
|
+
Horologium::Duration.seconds(30) - Horologium::Duration.seconds(42) # negative
|
|
229
|
+
-Horologium::Duration.seconds(3)
|
|
230
|
+
|
|
231
|
+
Horologium::Duration.days(1).to_r # => (86400/1), the whole value
|
|
232
|
+
Horologium::Duration.days(1).to_f # => 86400.0
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Adding a duration to an instant makes sense, but adding two instants together
|
|
236
|
+
does not, so it raises an error.
|
|
237
|
+
|
|
238
|
+
```rb
|
|
239
|
+
instant + instant # => raises Horologium::DimensionalError
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Exact equality is rarely what scientific code wants, so you can compare within a
|
|
243
|
+
tolerance:
|
|
244
|
+
|
|
245
|
+
```rb
|
|
246
|
+
a = Horologium::Instant.from_julian_date(2_460_000.5, scale: :tai)
|
|
247
|
+
near = a + Horologium::Duration.nanoseconds(1)
|
|
248
|
+
|
|
249
|
+
a.equal_within?(near, Horologium::Duration.nanoseconds(2)) # => true
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Precision
|
|
253
|
+
|
|
254
|
+
A modern Julian Date is around 2.46 million. A single `Float` spends most of its
|
|
255
|
+
digits on that large number and has only tens of microseconds left for the
|
|
256
|
+
fraction of a day. That is too coarse for scientific time. Horologium stores an
|
|
257
|
+
instant across two `Float`s whose sum is the Julian Date, so the second one
|
|
258
|
+
starts where the first runs out of digits. This is the representation [ERFA]
|
|
259
|
+
uses, it keeps the precision below a nanosecond for any date, and it does so
|
|
260
|
+
with ordinary floating-point arithmetic.
|
|
261
|
+
|
|
262
|
+
Every value carries one of two precisions, fixed when it is built:
|
|
263
|
+
|
|
264
|
+
- `:standard`, the default, keeps the value as a two-part float. It is fast and
|
|
265
|
+
stays within a few nanoseconds of the true value.
|
|
266
|
+
- `:exact` keeps the value as a `Rational`, with no rounding. The test suite
|
|
267
|
+
uses it to check that `:standard` stays within its stated precision.
|
|
268
|
+
|
|
269
|
+
Set the default once at boot. `Horologium.configure` freezes the configuration
|
|
270
|
+
when its block returns, so it is called once and everything is set in the one
|
|
271
|
+
block. A second call raises `Horologium::ConfigurationError`.
|
|
272
|
+
|
|
273
|
+
```rb
|
|
274
|
+
Horologium.configure do |c|
|
|
275
|
+
c.default_precision = :exact
|
|
276
|
+
c.leap_second_horizon = :raise
|
|
277
|
+
end
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Choose it for a single value, or for a scoped block:
|
|
281
|
+
|
|
282
|
+
```rb
|
|
283
|
+
Horologium::Instant.from_julian_date(
|
|
284
|
+
2_460_000.5,
|
|
285
|
+
scale: :tai,
|
|
286
|
+
precision: :exact
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
Horologium.with_precision(:exact) do
|
|
290
|
+
# instants and durations built here default to :exact
|
|
291
|
+
end
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Exactness is contagious. An operation between two `:standard` values stays
|
|
295
|
+
`:standard`. Mixing a `:standard` and an `:exact` value gives an `:exact`
|
|
296
|
+
result, so precision is not quietly lost. `:exact` guarantees the arithmetic
|
|
297
|
+
Horologium performs. It cannot bring back precision that an input already lost
|
|
298
|
+
when it was built.
|
|
299
|
+
|
|
300
|
+
## Status
|
|
301
|
+
|
|
302
|
+
This library is in early development, before its first public release. The
|
|
303
|
+
public API is not stable, so new versions will probably introduce breaking
|
|
304
|
+
changes until a 1.0 release. Changes are documented in the [CHANGELOG].
|
|
305
|
+
|
|
24
306
|
## Development
|
|
25
307
|
|
|
26
308
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
27
309
|
`rake` to run the tests and RuboCop, or `rake steep` to type-check the
|
|
28
|
-
signatures in `sig/`.
|
|
29
|
-
|
|
310
|
+
signatures in `sig/`. Run `COVERAGE=true rake test` to measure test coverage,
|
|
311
|
+
which is enforced at 100% of lines and branches in CI. You can also run
|
|
312
|
+
`bin/console` for an interactive prompt that will allow you to experiment.
|
|
313
|
+
|
|
314
|
+
`sig/` holds Horologium's own signatures and ships with the gem. `sig-vendor/`
|
|
315
|
+
holds stubs for gems that ship none of their own, and stays out of the gem so
|
|
316
|
+
it cannot clash with a downstream RBS collection.
|
|
317
|
+
|
|
318
|
+
Run `bin/ci` to run every check that GitHub Actions runs (RuboCop, Steep, YARD
|
|
319
|
+
documentation coverage, and the tests with coverage) in a single pass. It runs
|
|
320
|
+
each check even when an earlier one fails, so you see everything that needs
|
|
321
|
+
fixing at once.
|
|
30
322
|
|
|
31
323
|
To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
32
324
|
release a new version, update the version number in `version.rb`, and then run
|
|
@@ -47,6 +339,9 @@ The gem is available as open source under the terms of the [MIT License].
|
|
|
47
339
|
Everyone interacting in the Horologium project's codebases, issue trackers, chat
|
|
48
340
|
rooms and mailing lists is expected to follow the [code of conduct].
|
|
49
341
|
|
|
342
|
+
[Bundler]: https://bundler.io
|
|
343
|
+
[ERFA]: https://github.com/liberfa/erfa
|
|
344
|
+
[CHANGELOG]: https://github.com/rhannequin/horologium/blob/main/CHANGELOG.md
|
|
50
345
|
[rubygems.org]: https://rubygems.org
|
|
51
346
|
[MIT License]: https://opensource.org/licenses/MIT
|
|
52
347
|
[code of conduct]: https://github.com/rhannequin/horologium/blob/main/CODE_OF_CONDUCT.md
|
data/Rakefile
CHANGED
|
@@ -2,16 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
4
|
require "minitest/test_task"
|
|
5
|
-
|
|
6
|
-
Minitest::TestTask.create
|
|
7
|
-
|
|
8
5
|
require "rubocop/rake_task"
|
|
6
|
+
require "yard"
|
|
7
|
+
|
|
8
|
+
Minitest::TestTask.create do |t|
|
|
9
|
+
t.framework = %(require "test_helper")
|
|
10
|
+
end
|
|
9
11
|
|
|
10
12
|
RuboCop::RakeTask.new
|
|
13
|
+
YARD::Rake::YardocTask.new
|
|
11
14
|
|
|
12
15
|
desc "Type check with Steep"
|
|
13
16
|
task :steep do
|
|
14
17
|
sh "steep check"
|
|
15
18
|
end
|
|
16
19
|
|
|
20
|
+
desc "Verify YARD documentation coverage is 100%"
|
|
21
|
+
task :yard_coverage do
|
|
22
|
+
require "open3"
|
|
23
|
+
output, status = Open3.capture2e("yard", "stats", "--list-undoc")
|
|
24
|
+
puts output
|
|
25
|
+
unless status.success? && output.include?("100.00% documented")
|
|
26
|
+
abort "YARD documentation coverage is below 100%"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
17
30
|
task default: %i[test rubocop]
|