iers 0.0.1 → 0.1.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/.yardopts +3 -0
- data/CHANGELOG.md +41 -2
- data/README.md +331 -1
- data/data/Leap_Second.dat +41 -0
- data/data/finals2000A.all +19830 -0
- data/lib/iers/celestial_pole_offset.rb +63 -0
- data/lib/iers/configuration.rb +86 -0
- data/lib/iers/data.rb +169 -0
- data/lib/iers/data_status.rb +22 -0
- data/lib/iers/delta_t.rb +102 -0
- data/lib/iers/downloader.rb +89 -0
- data/lib/iers/earth_rotation_angle.rb +39 -0
- data/lib/iers/eop.rb +104 -0
- data/lib/iers/eop_lookup.rb +72 -0
- data/lib/iers/eop_parameter.rb +42 -0
- data/lib/iers/errors.rb +86 -0
- data/lib/iers/gmst.rb +42 -0
- data/lib/iers/has_data_quality.rb +16 -0
- data/lib/iers/has_date.rb +11 -0
- data/lib/iers/interpolation.rb +50 -0
- data/lib/iers/leap_second.rb +69 -0
- data/lib/iers/length_of_day.rb +60 -0
- data/lib/iers/parsers/finals.rb +123 -0
- data/lib/iers/parsers/leap_second.rb +56 -0
- data/lib/iers/parsers.rb +4 -0
- data/lib/iers/polar_motion.rb +114 -0
- data/lib/iers/tai.rb +44 -0
- data/lib/iers/terrestrial_rotation.rb +48 -0
- data/lib/iers/time_scale.rb +45 -0
- data/lib/iers/update_result.rb +12 -0
- data/lib/iers/ut1.rb +84 -0
- data/lib/iers/version.rb +1 -1
- data/lib/iers.rb +48 -0
- metadata +58 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IERS
|
|
4
|
+
# @api private
|
|
5
|
+
module EopLookup
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# @param entries [Array] sorted finals entries
|
|
9
|
+
# @param mjd [Float]
|
|
10
|
+
# @param order [Integer] number of points in the window
|
|
11
|
+
# @return [Array]
|
|
12
|
+
# @raise [OutOfRangeError]
|
|
13
|
+
def window(entries, mjd, order: 4)
|
|
14
|
+
validate_range!(entries, mjd)
|
|
15
|
+
|
|
16
|
+
index = entries.bsearch_index { |e| e.mjd > mjd } || entries.size
|
|
17
|
+
center = index - 1
|
|
18
|
+
|
|
19
|
+
half = order / 2
|
|
20
|
+
start = center - half + 1
|
|
21
|
+
start = start.clamp(0, entries.size - order)
|
|
22
|
+
|
|
23
|
+
entries[start, order]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param entries [Array] sorted finals entries
|
|
27
|
+
# @param start_mjd [Float]
|
|
28
|
+
# @param end_mjd [Float]
|
|
29
|
+
# @return [Array] entries within the MJD range (inclusive)
|
|
30
|
+
def range(entries, start_mjd, end_mjd)
|
|
31
|
+
first = entries.bsearch_index { |e| e.mjd >= start_mjd } || entries.size
|
|
32
|
+
last = entries.bsearch_index { |e| e.mjd > end_mjd } || entries.size
|
|
33
|
+
entries[first...last]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param entries [Array] sorted finals entries
|
|
37
|
+
# @param mjd [Float]
|
|
38
|
+
# @return [Array] two-element array bracketing the query point
|
|
39
|
+
# @raise [OutOfRangeError]
|
|
40
|
+
def bracket(entries, mjd)
|
|
41
|
+
validate_range!(entries, mjd)
|
|
42
|
+
|
|
43
|
+
index = entries.bsearch_index { |e| e.mjd > mjd }
|
|
44
|
+
|
|
45
|
+
if index.nil? || index == 0
|
|
46
|
+
raise OutOfRangeError.new(
|
|
47
|
+
"No bracket available for MJD #{mjd}",
|
|
48
|
+
requested_mjd: mjd,
|
|
49
|
+
available_range: entries.first.mjd..entries.last.mjd
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
[entries[index - 1], entries[index]]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def validate_range!(entries, mjd)
|
|
57
|
+
first_mjd = entries.first.mjd
|
|
58
|
+
last_mjd = entries.last.mjd
|
|
59
|
+
|
|
60
|
+
return if mjd.between?(first_mjd, last_mjd)
|
|
61
|
+
|
|
62
|
+
raise OutOfRangeError.new(
|
|
63
|
+
"Requested MJD #{mjd} is outside available data " \
|
|
64
|
+
"(#{first_mjd}..#{last_mjd})",
|
|
65
|
+
requested_mjd: mjd,
|
|
66
|
+
available_range: first_mjd..last_mjd
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private_class_method :validate_range!
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IERS
|
|
4
|
+
# @api private
|
|
5
|
+
module EopParameter
|
|
6
|
+
FLAG_TO_QUALITY = {"I" => :observed, "P" => :predicted}.freeze
|
|
7
|
+
|
|
8
|
+
def resolve(input, jd:, mjd:, interpolation:)
|
|
9
|
+
query_mjd = TimeScale.to_mjd(input, jd: jd, mjd: mjd)
|
|
10
|
+
entries = Data.finals_entries
|
|
11
|
+
method = interpolation || IERS.configuration.interpolation
|
|
12
|
+
|
|
13
|
+
window = case method
|
|
14
|
+
when :lagrange
|
|
15
|
+
order = IERS.configuration.lagrange_order
|
|
16
|
+
EopLookup.window(entries, query_mjd, order: order)
|
|
17
|
+
when :linear
|
|
18
|
+
EopLookup.bracket(entries, query_mjd)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
[query_mjd, window, method]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def interpolate_field(window, query_mjd, method)
|
|
25
|
+
xs = window.map(&:mjd)
|
|
26
|
+
ys = window.map { |e| yield e }
|
|
27
|
+
|
|
28
|
+
case method
|
|
29
|
+
when :lagrange then Interpolation.lagrange(xs, ys, query_mjd)
|
|
30
|
+
when :linear then Interpolation.linear(xs, ys, query_mjd)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def derive_quality(window, flag)
|
|
35
|
+
if window.any? { |e| e.public_send(flag) == "P" }
|
|
36
|
+
:predicted
|
|
37
|
+
else
|
|
38
|
+
:observed
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/iers/errors.rb
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IERS
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class DataError < Error; end
|
|
7
|
+
|
|
8
|
+
class ParseError < DataError
|
|
9
|
+
# @return [Pathname, nil]
|
|
10
|
+
attr_reader :path
|
|
11
|
+
# @return [Integer, nil]
|
|
12
|
+
attr_reader :line_number
|
|
13
|
+
|
|
14
|
+
def initialize(message = nil, path: nil, line_number: nil)
|
|
15
|
+
@path = path
|
|
16
|
+
@line_number = line_number
|
|
17
|
+
super(message)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class FileNotFoundError < DataError
|
|
22
|
+
# @return [Pathname, nil]
|
|
23
|
+
attr_reader :path
|
|
24
|
+
|
|
25
|
+
def initialize(message = nil, path: nil)
|
|
26
|
+
@path = path
|
|
27
|
+
super(message)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class DownloadError < Error; end
|
|
32
|
+
|
|
33
|
+
class NetworkError < DownloadError
|
|
34
|
+
# @return [String, nil]
|
|
35
|
+
attr_reader :url
|
|
36
|
+
# @return [Integer, nil]
|
|
37
|
+
attr_reader :status_code
|
|
38
|
+
|
|
39
|
+
def initialize(message = nil, url: nil, status_code: nil)
|
|
40
|
+
@url = url
|
|
41
|
+
@status_code = status_code
|
|
42
|
+
super(message)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class ValidationError < DownloadError
|
|
47
|
+
# @return [Pathname, nil]
|
|
48
|
+
attr_reader :path
|
|
49
|
+
# @return [String, nil]
|
|
50
|
+
attr_reader :reason
|
|
51
|
+
|
|
52
|
+
def initialize(message = nil, path: nil, reason: nil)
|
|
53
|
+
@path = path
|
|
54
|
+
@reason = reason
|
|
55
|
+
super(message)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class StaleDataError < DataError
|
|
60
|
+
# @return [Date, nil]
|
|
61
|
+
attr_reader :predicted_until
|
|
62
|
+
# @return [Date, nil]
|
|
63
|
+
attr_reader :required_until
|
|
64
|
+
|
|
65
|
+
def initialize(message = nil, predicted_until: nil, required_until: nil)
|
|
66
|
+
@predicted_until = predicted_until
|
|
67
|
+
@required_until = required_until
|
|
68
|
+
super(message)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class ConfigurationError < Error; end
|
|
73
|
+
|
|
74
|
+
class OutOfRangeError < Error
|
|
75
|
+
# @return [Float, nil]
|
|
76
|
+
attr_reader :requested_mjd
|
|
77
|
+
# @return [Range<Float>, nil]
|
|
78
|
+
attr_reader :available_range
|
|
79
|
+
|
|
80
|
+
def initialize(message = nil, requested_mjd: nil, available_range: nil)
|
|
81
|
+
@requested_mjd = requested_mjd
|
|
82
|
+
@available_range = available_range
|
|
83
|
+
super(message)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/iers/gmst.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IERS
|
|
4
|
+
module GMST
|
|
5
|
+
TWO_PI = 2.0 * Math::PI
|
|
6
|
+
|
|
7
|
+
# IERS Conventions 2010, eq. 5.32 (arcseconds)
|
|
8
|
+
POLYNOMIAL = [
|
|
9
|
+
0.014506,
|
|
10
|
+
4612.156534,
|
|
11
|
+
1.3915817,
|
|
12
|
+
-0.00000044,
|
|
13
|
+
-0.000029956,
|
|
14
|
+
-0.0000000368
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
private_constant :TWO_PI, :POLYNOMIAL
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# @param input [Time, Date, DateTime, nil]
|
|
22
|
+
# @param jd [Float, nil] Julian Date
|
|
23
|
+
# @param mjd [Float, nil] Modified Julian Date
|
|
24
|
+
# @param interpolation [Symbol, nil] +:lagrange+ or +:linear+
|
|
25
|
+
# @return [Float] Greenwich Mean Sidereal Time in radians, norm. to [0, 2π)
|
|
26
|
+
# @raise [OutOfRangeError]
|
|
27
|
+
def at(input = nil, jd: nil, mjd: nil, interpolation: nil)
|
|
28
|
+
query_mjd = TimeScale.to_mjd(input, jd: jd, mjd: mjd)
|
|
29
|
+
era = EarthRotationAngle.at(mjd: query_mjd, interpolation: interpolation)
|
|
30
|
+
|
|
31
|
+
tai_utc = LeapSecond.at(mjd: query_mjd)
|
|
32
|
+
tt_mjd = query_mjd +
|
|
33
|
+
(tai_utc + TimeScale::TT_TAI) / TimeScale::SECONDS_PER_DAY
|
|
34
|
+
t = (tt_mjd - TimeScale::MJD_J2000) / TimeScale::DAYS_PER_JULIAN_CENTURY
|
|
35
|
+
|
|
36
|
+
poly = POLYNOMIAL.reverse.reduce { |acc, c| acc * t + c }
|
|
37
|
+
gmst = (era + poly * TimeScale::ARCSEC_TO_RAD) % TWO_PI
|
|
38
|
+
gmst += TWO_PI if gmst < 0.0
|
|
39
|
+
gmst
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IERS
|
|
4
|
+
# @api private
|
|
5
|
+
module HasDataQuality
|
|
6
|
+
# @return [Boolean]
|
|
7
|
+
def observed?
|
|
8
|
+
data_quality == :observed
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @return [Boolean]
|
|
12
|
+
def predicted?
|
|
13
|
+
data_quality == :predicted
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IERS
|
|
4
|
+
# @api private
|
|
5
|
+
module Interpolation
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# @param xs [Array<Float>] x-coordinates (exactly 2)
|
|
9
|
+
# @param ys [Array<Float>] y-coordinates (exactly 2)
|
|
10
|
+
# @param x [Float] interpolation point
|
|
11
|
+
# @return [Float]
|
|
12
|
+
def linear(xs, ys, x)
|
|
13
|
+
unless xs.size == 2 && ys.size == 2
|
|
14
|
+
raise ArgumentError, "linear interpolation requires exactly 2 points"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
x0, x1 = xs
|
|
18
|
+
y0, y1 = ys
|
|
19
|
+
t = (x - x0) / (x1 - x0)
|
|
20
|
+
y0 + t * (y1 - y0)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @param xs [Array<Float>] x-coordinates
|
|
24
|
+
# @param ys [Array<Float>] y-coordinates
|
|
25
|
+
# @param x [Float] interpolation point
|
|
26
|
+
# @return [Float]
|
|
27
|
+
def lagrange(xs, ys, x)
|
|
28
|
+
n = xs.size
|
|
29
|
+
|
|
30
|
+
unless n == ys.size
|
|
31
|
+
raise ArgumentError, "xs and ys must have the same size"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
unless n >= 2
|
|
35
|
+
raise ArgumentError, "lagrange interpolation requires at least 2 points"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
result = 0.0
|
|
39
|
+
n.times do |i|
|
|
40
|
+
basis = 1.0
|
|
41
|
+
n.times do |j|
|
|
42
|
+
next if i == j
|
|
43
|
+
basis *= (x - xs[j]) / (xs[i] - xs[j])
|
|
44
|
+
end
|
|
45
|
+
result += ys[i] * basis
|
|
46
|
+
end
|
|
47
|
+
result
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IERS
|
|
4
|
+
module LeapSecond
|
|
5
|
+
# @attr effective_date [Date]
|
|
6
|
+
# @attr tai_utc [Integer] cumulative TAI−UTC offset in seconds
|
|
7
|
+
Entry = ::Data.define(:effective_date, :tai_utc)
|
|
8
|
+
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
@all = nil
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# @return [Array<Entry>]
|
|
15
|
+
def all
|
|
16
|
+
@mutex.synchronize do
|
|
17
|
+
@all ||= IERS::Data.leap_second_entries.map do |parser_entry|
|
|
18
|
+
Entry.new(
|
|
19
|
+
effective_date: parser_entry.date,
|
|
20
|
+
tai_utc: parser_entry.tai_utc
|
|
21
|
+
)
|
|
22
|
+
end.freeze
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [void]
|
|
27
|
+
def clear_cached!
|
|
28
|
+
@mutex.synchronize do
|
|
29
|
+
@all = nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Entry, nil]
|
|
34
|
+
def next_scheduled
|
|
35
|
+
today = Date.today
|
|
36
|
+
all.find { |entry| entry.effective_date > today }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param input [Time, Date, DateTime, nil]
|
|
40
|
+
# @param jd [Float, nil] Julian Date
|
|
41
|
+
# @param mjd [Float, nil] Modified Julian Date
|
|
42
|
+
# @return [Integer] TAI−UTC in seconds
|
|
43
|
+
# @raise [OutOfRangeError]
|
|
44
|
+
def at(input = nil, jd: nil, mjd: nil)
|
|
45
|
+
query_mjd = TimeScale.to_mjd(input, jd: jd, mjd: mjd)
|
|
46
|
+
parser_entries = IERS::Data.leap_second_entries
|
|
47
|
+
|
|
48
|
+
first_mjd = parser_entries.first.mjd
|
|
49
|
+
last_mjd = parser_entries.last.mjd
|
|
50
|
+
|
|
51
|
+
if query_mjd < first_mjd
|
|
52
|
+
raise OutOfRangeError.new(
|
|
53
|
+
"Requested MJD #{query_mjd} is before the first leap second " \
|
|
54
|
+
"entry (MJD #{first_mjd})",
|
|
55
|
+
requested_mjd: query_mjd,
|
|
56
|
+
available_range: first_mjd..last_mjd
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
index = parser_entries.bsearch_index { |e| e.mjd > query_mjd }
|
|
61
|
+
|
|
62
|
+
if index.nil?
|
|
63
|
+
parser_entries.last.tai_utc
|
|
64
|
+
else
|
|
65
|
+
parser_entries[index - 1].tai_utc
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IERS
|
|
4
|
+
module LengthOfDay
|
|
5
|
+
# @attr length_of_day [Float] excess LOD in seconds
|
|
6
|
+
# @attr mjd [Float] Modified Julian Date of the query
|
|
7
|
+
# @attr data_quality [Symbol] +:observed+ or +:predicted+
|
|
8
|
+
Entry = ::Data.define(:length_of_day, :mjd, :data_quality) do
|
|
9
|
+
include HasDate
|
|
10
|
+
include HasDataQuality
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
extend EopParameter
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# @param input [Time, Date, DateTime, nil]
|
|
18
|
+
# @param jd [Float, nil] Julian Date
|
|
19
|
+
# @param mjd [Float, nil] Modified Julian Date
|
|
20
|
+
# @param interpolation [Symbol, nil] +:lagrange+ or +:linear+
|
|
21
|
+
# @return [Entry]
|
|
22
|
+
# @raise [OutOfRangeError]
|
|
23
|
+
def at(input = nil, jd: nil, mjd: nil, interpolation: nil)
|
|
24
|
+
query_mjd, window, method = resolve(
|
|
25
|
+
input,
|
|
26
|
+
jd: jd,
|
|
27
|
+
mjd: mjd,
|
|
28
|
+
interpolation: interpolation
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
lod = interpolate_field(window, query_mjd, method) { |e| e.lod / 1000.0 }
|
|
32
|
+
|
|
33
|
+
Entry.new(
|
|
34
|
+
length_of_day: lod,
|
|
35
|
+
mjd: query_mjd,
|
|
36
|
+
data_quality: derive_quality(window, :ut1_flag)
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param start_date [Date]
|
|
41
|
+
# @param end_date [Date]
|
|
42
|
+
# @return [Enumerator::Lazy<Entry>]
|
|
43
|
+
def between(start_date, end_date)
|
|
44
|
+
start_mjd = TimeScale.to_mjd(start_date)
|
|
45
|
+
end_mjd = TimeScale.to_mjd(end_date)
|
|
46
|
+
entries = Data.finals_entries
|
|
47
|
+
|
|
48
|
+
EopLookup
|
|
49
|
+
.range(entries, start_mjd, end_mjd)
|
|
50
|
+
.lazy
|
|
51
|
+
.map do |e|
|
|
52
|
+
Entry.new(
|
|
53
|
+
length_of_day: e.lod / 1000.0,
|
|
54
|
+
mjd: e.mjd,
|
|
55
|
+
data_quality: EopParameter::FLAG_TO_QUALITY.fetch(e.ut1_flag, :observed)
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module IERS
|
|
6
|
+
module Parsers
|
|
7
|
+
module Finals
|
|
8
|
+
Entry = Data.define(
|
|
9
|
+
:date, :mjd,
|
|
10
|
+
:pm_flag, :pm_x, :pm_x_error, :pm_y, :pm_y_error,
|
|
11
|
+
:ut1_flag, :ut1_utc, :ut1_utc_error,
|
|
12
|
+
:lod, :lod_error,
|
|
13
|
+
:nutation_flag, :dx, :dx_error, :dy, :dy_error,
|
|
14
|
+
:bulletin_b_pm_x, :bulletin_b_pm_y, :bulletin_b_ut1_utc,
|
|
15
|
+
:bulletin_b_dx, :bulletin_b_dy
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
MJD_Y2K_PIVOT = 51544.0
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def parse(path)
|
|
23
|
+
path = Pathname(path)
|
|
24
|
+
|
|
25
|
+
unless path.exist?
|
|
26
|
+
raise FileNotFoundError.new(
|
|
27
|
+
"File not found: #{path}",
|
|
28
|
+
path: path.to_s
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
entries = []
|
|
33
|
+
|
|
34
|
+
path.each_line.with_index(1) do |line, line_number|
|
|
35
|
+
next if line.strip.empty?
|
|
36
|
+
next if no_eop_data?(line)
|
|
37
|
+
|
|
38
|
+
entries << parse_line(line, path, line_number)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
entries.freeze
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_line(line, path, line_number)
|
|
45
|
+
mjd = parse_float(line, 7, 8)
|
|
46
|
+
yy = parse_int(line, 0, 2)
|
|
47
|
+
month = parse_int(line, 2, 2)
|
|
48
|
+
day = parse_int(line, 4, 2)
|
|
49
|
+
year = (mjd < MJD_Y2K_PIVOT) ? 1900 + yy : 2000 + yy
|
|
50
|
+
|
|
51
|
+
Entry.new(
|
|
52
|
+
date: Date.new(year, month, day),
|
|
53
|
+
mjd: mjd,
|
|
54
|
+
pm_flag: parse_flag(line, 16),
|
|
55
|
+
pm_x: parse_float(line, 18, 9),
|
|
56
|
+
pm_x_error: parse_float(line, 27, 9),
|
|
57
|
+
pm_y: parse_float(line, 37, 9),
|
|
58
|
+
pm_y_error: parse_float(line, 46, 9),
|
|
59
|
+
ut1_flag: parse_flag(line, 57),
|
|
60
|
+
ut1_utc: parse_float(line, 58, 10),
|
|
61
|
+
ut1_utc_error: parse_float(line, 68, 10),
|
|
62
|
+
lod: parse_optional_float(line, 79, 7),
|
|
63
|
+
lod_error: parse_optional_float(line, 86, 7),
|
|
64
|
+
nutation_flag: parse_optional_flag(line, 95),
|
|
65
|
+
dx: parse_optional_float(line, 97, 9),
|
|
66
|
+
dx_error: parse_optional_float(line, 106, 9),
|
|
67
|
+
dy: parse_optional_float(line, 116, 9),
|
|
68
|
+
dy_error: parse_optional_float(line, 125, 9),
|
|
69
|
+
bulletin_b_pm_x: parse_optional_float(line, 134, 10),
|
|
70
|
+
bulletin_b_pm_y: parse_optional_float(line, 144, 10),
|
|
71
|
+
bulletin_b_ut1_utc: parse_optional_float(line, 154, 11),
|
|
72
|
+
bulletin_b_dx: parse_optional_float(line, 165, 10),
|
|
73
|
+
bulletin_b_dy: parse_optional_float(line, 175, 10)
|
|
74
|
+
)
|
|
75
|
+
rescue ArgumentError, TypeError => e
|
|
76
|
+
raise ParseError.new(
|
|
77
|
+
"Failed to parse line #{line_number}: #{e.message}",
|
|
78
|
+
path: path.to_s,
|
|
79
|
+
line_number: line_number
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def parse_float(line, offset, length)
|
|
84
|
+
Float(line[offset, length])
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def parse_int(line, offset, length)
|
|
88
|
+
Integer(line[offset, length])
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def parse_flag(line, offset)
|
|
92
|
+
line[offset, 1].strip
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def parse_optional_float(line, offset, length)
|
|
96
|
+
raw = line[offset, length]
|
|
97
|
+
return nil if raw.nil? || raw.strip.empty?
|
|
98
|
+
|
|
99
|
+
Float(raw)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def parse_optional_flag(line, offset)
|
|
103
|
+
raw = line[offset, 1]
|
|
104
|
+
return nil if raw.nil? || raw.strip.empty?
|
|
105
|
+
|
|
106
|
+
raw.strip
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def no_eop_data?(line)
|
|
110
|
+
flag = line[16, 1]
|
|
111
|
+
flag.nil? || flag.strip.empty?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private_class_method :parse_line,
|
|
115
|
+
:no_eop_data?,
|
|
116
|
+
:parse_float,
|
|
117
|
+
:parse_int,
|
|
118
|
+
:parse_flag,
|
|
119
|
+
:parse_optional_float,
|
|
120
|
+
:parse_optional_flag
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module IERS
|
|
6
|
+
module Parsers
|
|
7
|
+
module LeapSecond
|
|
8
|
+
Entry = Data.define(:mjd, :date, :tai_utc)
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def parse(path)
|
|
13
|
+
path = Pathname(path)
|
|
14
|
+
|
|
15
|
+
unless path.exist?
|
|
16
|
+
raise FileNotFoundError.new(
|
|
17
|
+
"File not found: #{path}",
|
|
18
|
+
path: path.to_s
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
entries = []
|
|
23
|
+
|
|
24
|
+
path.each_line.with_index(1) do |line, line_number|
|
|
25
|
+
next if line.strip.empty? || line.strip.start_with?("#")
|
|
26
|
+
|
|
27
|
+
entries << parse_line(line, path, line_number)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
entries.freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def parse_line(line, path, line_number)
|
|
34
|
+
parts = line.split
|
|
35
|
+
|
|
36
|
+
Entry.new(
|
|
37
|
+
mjd: Float(parts[0]),
|
|
38
|
+
date: Date.new(
|
|
39
|
+
Integer(parts[3]),
|
|
40
|
+
Integer(parts[2]),
|
|
41
|
+
Integer(parts[1])
|
|
42
|
+
),
|
|
43
|
+
tai_utc: Integer(parts[4])
|
|
44
|
+
)
|
|
45
|
+
rescue ArgumentError, TypeError => e
|
|
46
|
+
raise ParseError.new(
|
|
47
|
+
"Failed to parse line #{line_number}: #{e.message}",
|
|
48
|
+
path: path.to_s,
|
|
49
|
+
line_number: line_number
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private_class_method :parse_line
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/iers/parsers.rb
ADDED