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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -0
  3. data/README.md +304 -9
  4. data/Rakefile +16 -3
  5. data/lib/horologium/configuration.rb +291 -0
  6. data/lib/horologium/data/barycentric_model.rb +901 -0
  7. data/lib/horologium/data/leap_seconds.rb +55 -0
  8. data/lib/horologium/duration.rb +170 -0
  9. data/lib/horologium/error.rb +120 -0
  10. data/lib/horologium/instant.rb +505 -0
  11. data/lib/horologium/numeric/exact.rb +116 -0
  12. data/lib/horologium/numeric/precision.rb +172 -0
  13. data/lib/horologium/numeric/two_part_float.rb +298 -0
  14. data/lib/horologium/precise_value.rb +79 -0
  15. data/lib/horologium/representations/civil.rb +518 -0
  16. data/lib/horologium/representations/civil_time.rb +162 -0
  17. data/lib/horologium/representations/iso8601.rb +286 -0
  18. data/lib/horologium/representations/julian_date.rb +227 -0
  19. data/lib/horologium/representations/modified_julian_date.rb +106 -0
  20. data/lib/horologium/scale_reading.rb +126 -0
  21. data/lib/horologium/scales/base.rb +118 -0
  22. data/lib/horologium/scales/tai.rb +41 -0
  23. data/lib/horologium/scales/tdb.rb +75 -0
  24. data/lib/horologium/scales/tt.rb +71 -0
  25. data/lib/horologium/scales/utc.rb +374 -0
  26. data/lib/horologium/version.rb +1 -1
  27. data/lib/horologium.rb +24 -2
  28. data/sig/horologium/configuration.rbs +41 -0
  29. data/sig/horologium/data/barycentric_model.rbs +19 -0
  30. data/sig/horologium/data/leap_seconds.rbs +11 -0
  31. data/sig/horologium/duration.rbs +37 -0
  32. data/sig/horologium/instant.rbs +51 -0
  33. data/sig/horologium/numeric/exact.rbs +31 -0
  34. data/sig/horologium/numeric/precision.rbs +27 -0
  35. data/sig/horologium/numeric/two_part_float.rbs +57 -0
  36. data/sig/horologium/numeric.rbs +5 -0
  37. data/sig/horologium/precise_value.rbs +25 -0
  38. data/sig/horologium/representations/civil.rbs +63 -0
  39. data/sig/horologium/representations/civil_time.rbs +49 -0
  40. data/sig/horologium/representations/iso8601.rbs +27 -0
  41. data/sig/horologium/representations/julian_date.rbs +25 -0
  42. data/sig/horologium/representations/modified_julian_date.rbs +15 -0
  43. data/sig/horologium/scale_reading.rbs +33 -0
  44. data/sig/horologium/scales/base.rbs +17 -0
  45. data/sig/horologium/scales/tai.rbs +9 -0
  46. data/sig/horologium/scales/tdb.rbs +11 -0
  47. data/sig/horologium/scales/tt.rbs +17 -0
  48. data/sig/horologium/scales/utc.rbs +49 -0
  49. data/sig/horologium.rbs +56 -0
  50. metadata +77 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7acd6f601c9ea6b6023d91c7b6891d82bede556ce4a744ab821b722615bb4f1c
4
- data.tar.gz: f968a52ed82141bdadaef004483cfe56f48511e05d4c2c6db1b27c6557b53166
3
+ metadata.gz: 513d825c2d141d84274c76333af312631c4b9a077803d2e2921033782af518ce
4
+ data.tar.gz: '0108ee60e9bf9c1f149baefafe6e78f8d735dd824148ed91c69267b5ca10aad8'
5
5
  SHA512:
6
- metadata.gz: f6e0f093c7f13b748beed1a6c8a7f53dd18fdcddce28b3bd33caa8b06cd2ffd1a1042ca77569dd6766ca3572fc6e3cc108cb6defbbfd34c9bc389963c4cfda0f
7
- data.tar.gz: c3f4bd6142298f451311c76bbbb210023ba04002f6677488d8b22ee850d58f4f127d671bd8d9b425034930ba27d8c00fe4fab6865e32d6b3b46bd80cb5d6ed14
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
  [![Tests](https://github.com/rhannequin/horologium/workflows/CI/badge.svg)](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
- (UTC, TAI, TT, TDB, TCG, TCB, UT1, GPS), high-precision instants, Julian Dates,
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 running:
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
- ```sh
15
- bundle add horologium
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
- Or install it directly:
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
- ```sh
21
- gem install horologium
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/`. You can also run `bin/console` for an interactive prompt
29
- that will allow you to experiment.
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]