iers 0.1.1 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fe885be7911b137f1052c4671b28d5ed63ec1f0a887745fbeede898e1d15eb68
4
- data.tar.gz: 42763ecd8056ee961e489b666e6445553726c8dbe0d1606f1857eea6a427ba38
3
+ metadata.gz: 4132b4709810d88306f0ea6bde33b1664d1fdbfeef985e0191cba65c98c01f8a
4
+ data.tar.gz: 5e3bef64e668f1320bc9c3876e571bf4bb452ea2df52cd09539303bafbf03ea0
5
5
  SHA512:
6
- metadata.gz: c2282980b523a94f9befbaf3146cb70bbe3284d73305cebf6073826bd60280da002b5a82dc9228fc6552d7db31c5ad38f6f05e9d6150aded290425aad2e47f56
7
- data.tar.gz: b0579c1cfb2b2ef069e649ad80df7fddfe1d3828e360e28a6626a42f0a6d03ba1d563e622ee1e861e81bde0c4bc1a2373e112b5980ea2d1f8679bf66cd7e800e
6
+ metadata.gz: c2bd5f8b1d3a088bc9aa8d822013a9c04570cea94c6036f080eb4d34d8ad53ac0346e6547092f5ce95fe61f95df6de32006e59371894e3b77768565be15354a1
7
+ data.tar.gz: 8b32049edecc0955026664f02cbf89a1af9d518050374538cbf1b1c3b08d572509b971d522a1d368490d6d07bcbd90bb807bd9fd5c4223d074431a60583244f5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 - 2026-07-25
4
+
5
+ ### Added
6
+
7
+ - `LeapSecond.expires_on`, `LeapSecond.expired?` and
8
+ `LeapSecond.updated_through` expose the expiry date and IERS bulletin that
9
+ `Leap_Second.dat` states in its header. Both were previously discarded with
10
+ the rest of the comment lines. Unlike `Data.status.cache_age`, which measures
11
+ when a file was downloaded, these describe how long the data itself stays
12
+ authoritative.
13
+ - `LeapSecond.at` now answers 1961-01-01 through 1972-01-01, the era when UTC
14
+ was steered by rate adjustments rather than whole leap seconds. It returns
15
+ those values as exact `Rational`s. The coefficients come from the USNO
16
+ `tai-utc.dat` file and are bundled in source, since they are fixed. The
17
+ change is additive: the pre-1972 range previously raised, so no successful
18
+ call returns anything different. `TAI.utc_to_tai` and `TAI.tai_to_utc` gain
19
+ the same range for free.
20
+
21
+ ### Changed
22
+
23
+ - `Parsers::LeapSecond.parse` returns a `Table` of `entries` and `metadata`
24
+ instead of an array of entries. `Parsers` is internal; the public API is
25
+ unaffected.
26
+ - `Parsers::LeapSecond.parse` no longer raises `Encoding::CompatibilityError`
27
+ on a file containing invalid UTF-8. Header metadata is read on a best-effort
28
+ basis and an unreadable data row raises `ParseError` as before.
29
+
3
30
  ## 0.1.1 - 2026-06-27
4
31
 
5
32
  ### Changed
data/README.md CHANGED
@@ -242,14 +242,28 @@ entries = IERS::EOP.between(
242
242
  Look up TAI−UTC at a given date:
243
243
 
244
244
  ```ruby
245
- IERS::LeapSecond.at(Time.utc(2017, 1, 1)) # => 37.0 (seconds)
245
+ IERS::LeapSecond.at(Time.utc(2017, 1, 1)) # => 37 (seconds)
246
246
  ```
247
247
 
248
+ `at` covers 1961-01-01 onward. From 1972 it returns a whole number of seconds
249
+ as an `Integer`. Between 1961 and 1972, UTC was steered by small rate
250
+ adjustments rather than whole leap seconds, so TAI−UTC was a fraction of a
251
+ second that changed daily. `at` returns those values as exact `Rational`s:
252
+
253
+ ```ruby
254
+ IERS::LeapSecond.at(mjd: 38_900) # => (1910137/500000)
255
+ IERS::LeapSecond.at(mjd: 38_900).to_f # => 3.820274 (seconds, 1965-05-20)
256
+ ```
257
+
258
+ The coefficients for that era are fixed and will never change, so they are
259
+ bundled in the gem rather than downloaded. Anything before 1961-01-01 has no
260
+ published UTC and raises `OutOfRangeError`.
261
+
248
262
  List all leap seconds:
249
263
 
250
264
  ```ruby
251
265
  IERS::LeapSecond.all
252
- # => [#<data IERS::LeapSecond::Entry effective_date=#<Date: 1972-01-01>, tai_utc=10.0>, ...]
266
+ # => [#<data IERS::LeapSecond::Entry effective_date=#<Date: 1972-01-01>, tai_utc=10>, ...]
253
267
  ```
254
268
 
255
269
  Check for a future scheduled leap second:
@@ -258,6 +272,27 @@ Check for a future scheduled leap second:
258
272
  IERS::LeapSecond.next_scheduled # => #<data IERS::LeapSecond::Entry ...> or nil
259
273
  ```
260
274
 
275
+ #### Leap second file validity
276
+
277
+ `Leap_Second.dat` states when it stops being authoritative and which IERS
278
+ bulletin it was updated through. Both are read from the file that is actually
279
+ loaded, so a custom or cached file reports its own header:
280
+
281
+ ```ruby
282
+ IERS::LeapSecond.expires_on # => #<Date: 2026-12-28> or nil
283
+ IERS::LeapSecond.expired? # => false
284
+ IERS::LeapSecond.updated_through # => "IERS Bulletin 71 issued in January 2026"
285
+ ```
286
+
287
+ The stated date is itself still valid, so a file expiring on 28 December 2026
288
+ is expired on the 29th. An expired file is not wrong: it means the IERS has
289
+ published a newer one and a leap second may have been announced since.
290
+
291
+ This is unrelated to `Data.status.cache_age`, which measures when the file was
292
+ downloaded rather than how long it stays valid. A bundled snapshot can be a year
293
+ old and still valid while a file downloaded this morning can be a week from
294
+ expiry.
295
+
261
296
  ### TAI
262
297
 
263
298
  Convert between UTC and TAI time scales:
@@ -295,6 +330,10 @@ end
295
330
 
296
331
  Without `coverage_days_ahead`, the check ensures predictions cover today.
297
332
 
333
+ `ensure_fresh!` is about prediction coverage in `finals2000A.all` only. Leap
334
+ second file validity is a separate question, answered by
335
+ `IERS::LeapSecond.expired?`.
336
+
298
337
  ### Data status and cache management
299
338
 
300
339
  ```ruby
@@ -358,6 +397,9 @@ release a new version, update the version number in `version.rb`, and then run
358
397
  `bundle exec rake release`, which will create a git tag for the version, push
359
398
  git commits and the created tag, and push the `.gem` file to [rubygems.org].
360
399
 
400
+ To refresh the bundled snapshot in `data/`, run `bundle exec rake data:update`.
401
+ A monthly workflow runs this and opens a pull request automatically.
402
+
361
403
  ## License
362
404
 
363
405
  The gem is available as open source under the terms of the [MIT License].
data/Rakefile CHANGED
@@ -10,3 +10,31 @@ require "rubocop/rake_task"
10
10
  RuboCop::RakeTask.new
11
11
 
12
12
  task default: %i[test rubocop]
13
+
14
+ namespace :data do
15
+ desc "Download the latest IERS data"
16
+ task :update do
17
+ require "iers"
18
+
19
+ data_dir = File.expand_path("data", __dir__)
20
+ IERS.configure do |config|
21
+ config.finals_path = File.join(data_dir, "finals2000A.all")
22
+ config.leap_second_path = File.join(data_dir, "Leap_Second.dat")
23
+ end
24
+
25
+ result = IERS::Data.update!
26
+
27
+ unless result.success?
28
+ result.errors.each do |source, error|
29
+ warn "#{source}: #{error.class} - #{error.message}"
30
+ end
31
+ abort "IERS data update failed"
32
+ end
33
+
34
+ IERS::Data.clear_loaded!
35
+ IERS::Data.ensure_fresh!(coverage_days_ahead: 90)
36
+ IERS::Data.leap_second_entries
37
+
38
+ puts "Updated: #{result.updated_files.join(", ")}"
39
+ end
40
+ end
data/lib/iers/data.rb CHANGED
@@ -11,13 +11,13 @@ module IERS
11
11
 
12
12
  @mutex = Mutex.new
13
13
  @finals = nil
14
- @leap_seconds = nil
14
+ @leap_second_table = nil
15
15
 
16
16
  module_function
17
17
 
18
18
  # @return [Boolean]
19
19
  def loaded?
20
- !@finals.nil? || !@leap_seconds.nil?
20
+ !@finals.nil? || !@leap_second_table.nil?
21
21
  end
22
22
 
23
23
  # @param sources [Array<Symbol>] data sources to update (default: all)
@@ -96,16 +96,26 @@ module IERS
96
96
  end
97
97
  end
98
98
 
99
- # @return [Array<Parsers::LeapSecond::Entry>]
100
- def leap_second_entries
99
+ # @return [Parsers::LeapSecond::Table]
100
+ def leap_second_table
101
101
  @mutex.synchronize do
102
- @leap_seconds ||= begin
102
+ @leap_second_table ||= begin
103
103
  path = resolve_read_path(:leap_seconds)
104
- Parsers::LeapSecond.parse(path).freeze
104
+ Parsers::LeapSecond.parse(path)
105
105
  end
106
106
  end
107
107
  end
108
108
 
109
+ # @return [Array<Parsers::LeapSecond::Entry>]
110
+ def leap_second_entries
111
+ leap_second_table.entries
112
+ end
113
+
114
+ # @return [Parsers::LeapSecond::Metadata]
115
+ def leap_second_metadata
116
+ leap_second_table.metadata
117
+ end
118
+
109
119
  def resolve_path(source, config = IERS.configuration)
110
120
  case source
111
121
  when :finals
@@ -155,7 +165,7 @@ module IERS
155
165
  def clear_loaded!
156
166
  @mutex.synchronize do
157
167
  @finals = nil
158
- @leap_seconds = nil
168
+ @leap_second_table = nil
159
169
  end
160
170
  end
161
171
 
@@ -3,7 +3,7 @@
3
3
  module IERS
4
4
  # @attr source [Symbol] +:cached+, +:custom+, or +:bundled+
5
5
  # @attr cache_age [Integer, nil] age in seconds, or +nil+
6
- DataStatus = Data.define(:source, :cache_age) do
6
+ DataStatus = ::Data.define(:source, :cache_age) do
7
7
  # @return [Boolean]
8
8
  def cached?
9
9
  source == :cached
@@ -30,17 +30,41 @@ module IERS
30
30
  end
31
31
  end
32
32
 
33
+ # @return [Date, nil]
34
+ def expires_on
35
+ IERS::Data.leap_second_metadata.expires_on
36
+ end
37
+
38
+ # @param as_of [Date]
39
+ # @return [Boolean]
40
+ def expired?(as_of: Date.today)
41
+ expiry = expires_on
42
+ return false if expiry.nil?
43
+
44
+ as_of > expiry
45
+ end
46
+
47
+ # @return [String, nil]
48
+ def updated_through
49
+ IERS::Data.leap_second_metadata.updated_through
50
+ end
51
+
33
52
  # @return [Entry, nil]
34
53
  def next_scheduled
35
54
  today = Date.today
36
55
  all.find { |entry| entry.effective_date > today }
37
56
  end
38
57
 
58
+ # TAI−UTC covers 1961-01-01 onward. From 1972 the value is a whole number
59
+ # of seconds read from Leap_Second.dat and returned as an Integer. Between
60
+ # 1961 and 1972 UTC was steered by rate adjustments rather than whole leap
61
+ # seconds, so the value is a fraction of a second returned as a Rational.
62
+ #
39
63
  # @param input [Time, Date, DateTime, nil]
40
64
  # @param jd [Float, nil] Julian Date
41
65
  # @param mjd [Float, nil] Modified Julian Date
42
- # @return [Integer] TAI−UTC in seconds
43
- # @raise [OutOfRangeError]
66
+ # @return [Integer, Rational] TAI−UTC in seconds
67
+ # @raise [OutOfRangeError] before 1961-01-01
44
68
  def at(input = nil, jd: nil, mjd: nil)
45
69
  query_mjd = TimeScale.to_mjd(input, jd: jd, mjd: mjd)
46
70
  parser_entries = IERS::Data.leap_second_entries
@@ -49,11 +73,14 @@ module IERS
49
73
  last_mjd = parser_entries.last.mjd
50
74
 
51
75
  if query_mjd < first_mjd
76
+ return TaiUtcDrift.at(query_mjd) if TaiUtcDrift.covers?(query_mjd)
77
+
52
78
  raise OutOfRangeError.new(
53
- "Requested MJD #{query_mjd} is before the first leap second " \
54
- "entry (MJD #{first_mjd})",
79
+ "Requested MJD #{query_mjd} is before TAI−UTC was defined " \
80
+ "(MJD #{TaiUtcDrift::FIRST_MJD}, 1961-01-01; no published UTC " \
81
+ "before then)",
55
82
  requested_mjd: query_mjd,
56
- available_range: first_mjd..last_mjd
83
+ available_range: TaiUtcDrift::FIRST_MJD..last_mjd
57
84
  )
58
85
  end
59
86
 
@@ -5,7 +5,7 @@ require "date"
5
5
  module IERS
6
6
  module Parsers
7
7
  module Finals
8
- Entry = Data.define(
8
+ Entry = ::Data.define(
9
9
  :date, :mjd,
10
10
  :pm_flag, :pm_x, :pm_x_error, :pm_y, :pm_y_error,
11
11
  :ut1_flag, :ut1_utc, :ut1_utc_error,
@@ -5,10 +5,23 @@ require "date"
5
5
  module IERS
6
6
  module Parsers
7
7
  module LeapSecond
8
- Entry = Data.define(:mjd, :date, :tai_utc)
8
+ Entry = ::Data.define(:mjd, :date, :tai_utc)
9
+
10
+ # @attr expires_on [Date, nil]
11
+ # @attr updated_through [String, nil]
12
+ Metadata = ::Data.define(:expires_on, :updated_through)
13
+
14
+ # @attr entries [Array<Entry>]
15
+ # @attr metadata [Metadata]
16
+ Table = ::Data.define(:entries, :metadata)
17
+
18
+ EXPIRES_ON = /File expires on\s+(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})/i
19
+ UPDATED_THROUGH = /\AUpdated through\s+(.+?)\s*\z/i
20
+ COMMENT_PREFIX = /\A#+\s*/
9
21
 
10
22
  module_function
11
23
 
24
+ # @return [Table]
12
25
  def parse(path)
13
26
  path = Pathname(path)
14
27
 
@@ -20,14 +33,30 @@ module IERS
20
33
  end
21
34
 
22
35
  entries = []
36
+ expires_on = nil
37
+ updated_through = nil
23
38
 
24
39
  path.each_line.with_index(1) do |line, line_number|
25
- next if line.strip.empty? || line.strip.start_with?("#")
40
+ stripped = line.scrub.strip
41
+ next if stripped.empty?
42
+
43
+ if stripped.start_with?("#")
44
+ comment = stripped.sub(COMMENT_PREFIX, "")
45
+ expires_on ||= parse_expiry(comment)
46
+ updated_through ||= parse_updated_through(comment)
47
+ next
48
+ end
26
49
 
27
50
  entries << parse_line(line, path, line_number)
28
51
  end
29
52
 
30
- entries.freeze
53
+ Table.new(
54
+ entries: entries.freeze,
55
+ metadata: Metadata.new(
56
+ expires_on: expires_on,
57
+ updated_through: updated_through
58
+ )
59
+ )
31
60
  end
32
61
 
33
62
  def parse_line(line, path, line_number)
@@ -50,7 +79,28 @@ module IERS
50
79
  )
51
80
  end
52
81
 
53
- private_class_method :parse_line
82
+ # @return [Date, nil]
83
+ def parse_expiry(comment)
84
+ match = EXPIRES_ON.match(comment)
85
+ return nil if match.nil?
86
+
87
+ month = Date::MONTHNAMES.index do |name|
88
+ name&.casecmp?(match[2])
89
+ end
90
+ return nil if month.nil?
91
+
92
+ Date.new(Integer(match[3], 10), month, Integer(match[1], 10))
93
+ rescue ArgumentError
94
+ nil
95
+ end
96
+
97
+ # @return [String, nil]
98
+ def parse_updated_through(comment)
99
+ match = UPDATED_THROUGH.match(comment)
100
+ match && match[1]
101
+ end
102
+
103
+ private_class_method :parse_line, :parse_expiry, :parse_updated_through
54
104
  end
55
105
  end
56
106
  end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IERS
4
+ # TAI-UTC from 1961-01-01 to 1972-01-01, when UTC was steered by rate
5
+ # adjustments instead of whole leap seconds. In this era TAI-UTC is a linear
6
+ # function of MJD, restarting at each published step, so a value is
7
+ # +offset + (mjd - reference_mjd) * rate+.
8
+ #
9
+ # The table is closed: the last segment ends where Leap_Second.dat begins and
10
+ # no new segment can ever be added.
11
+ #
12
+ # Coefficients from the USNO tai-utc.dat file, the same values ERFA carries in
13
+ # dat.c. They are exact decimals, so they are stored as rationals rather than
14
+ # floats.
15
+ #
16
+ # @api private
17
+ module TaiUtcDrift
18
+ # @attr start_mjd [Integer] first MJD the segment applies to
19
+ # @attr offset [Rational] TAI-UTC in seconds at reference_mjd (not at
20
+ # start_mjd: several segments measure their rate from an earlier MJD)
21
+ # @attr reference_mjd [Integer] the MJD the rate is measured from
22
+ # @attr rate [Rational] seconds of TAI-UTC per day of MJD
23
+ Segment = ::Data.define(:start_mjd, :offset, :reference_mjd, :rate)
24
+
25
+ # 1961-01-01. Below this there is no published UTC, so TAI-UTC is undefined.
26
+ FIRST_MJD = 37_300
27
+
28
+ # 1972-01-01, where Leap_Second.dat takes over. The drift era stops here.
29
+ LAST_MJD = 41_317
30
+
31
+ module_function
32
+
33
+ # @param mjd [Numeric]
34
+ # @return [Rational] TAI-UTC in seconds
35
+ def at(mjd)
36
+ segment = SEGMENTS.reverse_each.find { |s| mjd >= s.start_mjd }
37
+
38
+ segment.offset +
39
+ (Rational(mjd) - segment.reference_mjd) * segment.rate
40
+ end
41
+
42
+ # Whether +at+ has a value for this MJD: 1961-01-01 up to but not including
43
+ # 1972-01-01, where Leap_Second.dat takes over. A caller with a custom leap
44
+ # second file starting after 1972 relies on the upper bound to fall through
45
+ # to its own error rather than reading a stale drift value.
46
+ #
47
+ # @param mjd [Numeric]
48
+ # @return [Boolean]
49
+ def covers?(mjd)
50
+ mjd >= FIRST_MJD && mjd < LAST_MJD
51
+ end
52
+
53
+ SEGMENTS = [
54
+ Segment.new(
55
+ start_mjd: 37_300,
56
+ offset: Rational(14_228_180, 10_000_000),
57
+ reference_mjd: 37_300,
58
+ rate: Rational(1_296, 1_000_000)
59
+ ),
60
+ Segment.new(
61
+ start_mjd: 37_512,
62
+ offset: Rational(13_728_180, 10_000_000),
63
+ reference_mjd: 37_300,
64
+ rate: Rational(1_296, 1_000_000)
65
+ ),
66
+ Segment.new(
67
+ start_mjd: 37_665,
68
+ offset: Rational(18_458_580, 10_000_000),
69
+ reference_mjd: 37_665,
70
+ rate: Rational(11_232, 10_000_000)
71
+ ),
72
+ Segment.new(
73
+ start_mjd: 38_334,
74
+ offset: Rational(19_458_580, 10_000_000),
75
+ reference_mjd: 37_665,
76
+ rate: Rational(11_232, 10_000_000)
77
+ ),
78
+ Segment.new(
79
+ start_mjd: 38_395,
80
+ offset: Rational(32_401_300, 10_000_000),
81
+ reference_mjd: 38_761,
82
+ rate: Rational(1_296, 1_000_000)
83
+ ),
84
+ Segment.new(
85
+ start_mjd: 38_486,
86
+ offset: Rational(33_401_300, 10_000_000),
87
+ reference_mjd: 38_761,
88
+ rate: Rational(1_296, 1_000_000)
89
+ ),
90
+ Segment.new(
91
+ start_mjd: 38_639,
92
+ offset: Rational(34_401_300, 10_000_000),
93
+ reference_mjd: 38_761,
94
+ rate: Rational(1_296, 1_000_000)
95
+ ),
96
+ Segment.new(
97
+ start_mjd: 38_761,
98
+ offset: Rational(35_401_300, 10_000_000),
99
+ reference_mjd: 38_761,
100
+ rate: Rational(1_296, 1_000_000)
101
+ ),
102
+ Segment.new(
103
+ start_mjd: 38_820,
104
+ offset: Rational(36_401_300, 10_000_000),
105
+ reference_mjd: 38_761,
106
+ rate: Rational(1_296, 1_000_000)
107
+ ),
108
+ Segment.new(
109
+ start_mjd: 38_942,
110
+ offset: Rational(37_401_300, 10_000_000),
111
+ reference_mjd: 38_761,
112
+ rate: Rational(1_296, 1_000_000)
113
+ ),
114
+ Segment.new(
115
+ start_mjd: 39_004,
116
+ offset: Rational(38_401_300, 10_000_000),
117
+ reference_mjd: 38_761,
118
+ rate: Rational(1_296, 1_000_000)
119
+ ),
120
+ Segment.new(
121
+ start_mjd: 39_126,
122
+ offset: Rational(43_131_700, 10_000_000),
123
+ reference_mjd: 39_126,
124
+ rate: Rational(2_592, 1_000_000)
125
+ ),
126
+ Segment.new(
127
+ start_mjd: 39_887,
128
+ offset: Rational(42_131_700, 10_000_000),
129
+ reference_mjd: 39_126,
130
+ rate: Rational(2_592, 1_000_000)
131
+ ),
132
+ # Reached only by a direct TaiUtcDrift.at(41_317): LeapSecond.at's drift
133
+ # branch requires query_mjd < 41_317. It pins the join to exactly 10,
134
+ # matching Leap_Second.dat's first row; without it the 1968 segment would
135
+ # give ~9.8922 here. Do not remove.
136
+ Segment.new(
137
+ start_mjd: 41_317,
138
+ offset: Rational(10),
139
+ reference_mjd: 41_317,
140
+ rate: Rational(0)
141
+ )
142
+ ].freeze
143
+ end
144
+ end
@@ -3,7 +3,7 @@
3
3
  module IERS
4
4
  # @attr updated_files [Array<Symbol>]
5
5
  # @attr errors [Hash{Symbol => Error}]
6
- UpdateResult = Data.define(:updated_files, :errors) do
6
+ UpdateResult = ::Data.define(:updated_files, :errors) do
7
7
  # @return [Boolean]
8
8
  def success?
9
9
  errors.empty?
data/lib/iers/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IERS
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/iers.rb CHANGED
@@ -14,6 +14,7 @@ require_relative "iers/has_data_quality"
14
14
  require_relative "iers/interpolation"
15
15
  require_relative "iers/eop_lookup"
16
16
  require_relative "iers/eop_parameter"
17
+ require_relative "iers/tai_utc_drift"
17
18
  require_relative "iers/leap_second"
18
19
  require_relative "iers/tai"
19
20
  require_relative "iers/ut1"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rémy Hannequin
@@ -174,6 +174,7 @@ files:
174
174
  - lib/iers/parsers/leap_second.rb
175
175
  - lib/iers/polar_motion.rb
176
176
  - lib/iers/tai.rb
177
+ - lib/iers/tai_utc_drift.rb
177
178
  - lib/iers/terrestrial_rotation.rb
178
179
  - lib/iers/time_scale.rb
179
180
  - lib/iers/update_result.rb