iers 0.1.0 → 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 +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +44 -2
- data/Rakefile +28 -0
- data/data/finals2000A.all +926 -800
- data/lib/iers/data.rb +17 -7
- data/lib/iers/data_status.rb +1 -1
- data/lib/iers/leap_second.rb +32 -5
- data/lib/iers/parsers/finals.rb +1 -1
- data/lib/iers/parsers/leap_second.rb +54 -4
- data/lib/iers/tai_utc_drift.rb +144 -0
- data/lib/iers/update_result.rb +1 -1
- data/lib/iers/version.rb +1 -1
- data/lib/iers.rb +1 -0
- metadata +2 -1
data/lib/iers/data.rb
CHANGED
|
@@ -11,13 +11,13 @@ module IERS
|
|
|
11
11
|
|
|
12
12
|
@mutex = Mutex.new
|
|
13
13
|
@finals = nil
|
|
14
|
-
@
|
|
14
|
+
@leap_second_table = nil
|
|
15
15
|
|
|
16
16
|
module_function
|
|
17
17
|
|
|
18
18
|
# @return [Boolean]
|
|
19
19
|
def loaded?
|
|
20
|
-
!@finals.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 [
|
|
100
|
-
def
|
|
99
|
+
# @return [Parsers::LeapSecond::Table]
|
|
100
|
+
def leap_second_table
|
|
101
101
|
@mutex.synchronize do
|
|
102
|
-
@
|
|
102
|
+
@leap_second_table ||= begin
|
|
103
103
|
path = resolve_read_path(:leap_seconds)
|
|
104
|
-
Parsers::LeapSecond.parse(path)
|
|
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
|
-
@
|
|
168
|
+
@leap_second_table = nil
|
|
159
169
|
end
|
|
160
170
|
end
|
|
161
171
|
|
data/lib/iers/data_status.rb
CHANGED
|
@@ -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
|
data/lib/iers/leap_second.rb
CHANGED
|
@@ -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
|
|
54
|
-
"
|
|
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:
|
|
83
|
+
available_range: TaiUtcDrift::FIRST_MJD..last_mjd
|
|
57
84
|
)
|
|
58
85
|
end
|
|
59
86
|
|
data/lib/iers/parsers/finals.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/iers/update_result.rb
CHANGED
data/lib/iers/version.rb
CHANGED
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.
|
|
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
|