marsdate 1.1.3 → 2.0.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 +5 -5
- data/CHANGELOG.md +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +650 -0
- data/bin/marsdate +167 -15
- data/lib/marsdate/calendar.rb +128 -0
- data/lib/marsdate/timescale.rb +314 -0
- data/lib/marsdate/version.rb +3 -0
- data/lib/marsdate.rb +289 -241
- data/marsdate.gemspec +31 -0
- data/test/calendar_test.rb +142 -0
- data/test/cli_test.rb +57 -0
- data/test/test.rb +305 -313
- data/test/timescale_test.rb +124 -0
- metadata +51 -19
- data/bin/marsdate-version +0 -5
- data/bin/mcal.rb +0 -50
- data/bin/meh.html +0 -2941
- data/bin/mkcal.rb +0 -51
- data/bin/mtcon.rb +0 -29
- data/bin/mtoday.rb +0 -5
- data/test/spec/marsdate_spec.rb +0 -382
data/bin/marsdate
CHANGED
|
@@ -8,8 +8,8 @@ def usage
|
|
|
8
8
|
|
|
9
9
|
marsdate # usage (this message)
|
|
10
10
|
marsdate version # current version number
|
|
11
|
-
marsdate today #
|
|
12
|
-
marsdate now #
|
|
11
|
+
marsdate today # current Martian sol at 00:00
|
|
12
|
+
marsdate now # current instant
|
|
13
13
|
|
|
14
14
|
marsdate calendar yyyy # Martian calendar for yyyy (HTML)
|
|
15
15
|
marsdate calendar # this year's Martian calendar (HTML)
|
|
@@ -31,24 +31,25 @@ def command(cmd)
|
|
|
31
31
|
puts MarsDateTime.today
|
|
32
32
|
when "now"
|
|
33
33
|
puts MarsDateTime.now
|
|
34
|
+
when "calendar"
|
|
35
|
+
calendar
|
|
36
|
+
else
|
|
37
|
+
abort "Unknown command '#{cmd}'"
|
|
34
38
|
end
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
def get_data(ymd, hms = nil)
|
|
38
42
|
hms ||= "00:00:00"
|
|
39
|
-
yyyy,mm,dd = ymd.split(%r[-/]).map {|x| x.to_i }
|
|
40
|
-
hh,mi,ss = hms.split(":").map {|x| x.to_i }
|
|
43
|
+
yyyy, mm, dd = ymd.split(%r{[-/]}).map {|x| x.to_i }
|
|
44
|
+
hh, mi, ss = hms.split(":").map {|x| x.to_i }
|
|
41
45
|
[yyyy, mm, dd, hh, mi, ss]
|
|
42
46
|
end
|
|
43
47
|
|
|
44
|
-
def method_missing(meth, *args)
|
|
45
|
-
puts "Command #{meth} not known\n "
|
|
46
|
-
usage
|
|
47
|
-
1/0
|
|
48
|
-
end
|
|
49
|
-
|
|
50
48
|
def invoke(cmd, *args)
|
|
51
49
|
send(cmd, *args)
|
|
50
|
+
rescue => err
|
|
51
|
+
warn "Error in calling #{cmd}: #{err.message}"
|
|
52
|
+
exit 1
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
def e2m(ymd, hms)
|
|
@@ -61,17 +62,164 @@ end
|
|
|
61
62
|
|
|
62
63
|
def m2e(ymd, hms)
|
|
63
64
|
yyyy, mm, dd, hh, mi, ss = *get_data(ymd, hms)
|
|
64
|
-
mdate = MarsDateTime.new(yyyy,mm,dd)
|
|
65
|
+
mdate = MarsDateTime.new(yyyy, mm, dd, hh, mi, ss) # MXT
|
|
65
66
|
date = mdate.earth_date
|
|
66
67
|
puts date.strftime("%x %X")
|
|
67
68
|
end
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
class CalendarHTML
|
|
71
|
+
def month_header(n)
|
|
72
|
+
@out.puts "<tr><td colspan=7>"
|
|
73
|
+
@out.puts "<b><center><font size=4 face=arial color=red>#{MarsDateTime::Months[n]}</font></center></b>"
|
|
74
|
+
@out.puts "</td></tr>"
|
|
75
|
+
@out.puts "<tr bgcolor=#FFA833>"
|
|
76
|
+
@out.puts "<td width=3%><font size=3 face='arial'>Sun</font></td>"
|
|
77
|
+
@out.puts "<td width=3%><font size=3 face='arial'>Mon</font></td>"
|
|
78
|
+
@out.puts "<td width=3%><font size=3 face='arial'>Tue</font></td>"
|
|
79
|
+
@out.puts "<td width=3%><font size=3 face='arial'>Wed</font></td>"
|
|
80
|
+
@out.puts "<td width=3%><font size=3 face='arial'>Thu</font></td>"
|
|
81
|
+
@out.puts "<td width=3%><font size=3 face='arial'>Fri</font></td>"
|
|
82
|
+
@out.puts "<td width=3%><font size=3 face='arial'>Sat</font></td>"
|
|
83
|
+
@out.puts "</tr>"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def month(year, n)
|
|
87
|
+
dow = MarsDateTime.new(year, n, 1).dow
|
|
88
|
+
ndays = 28
|
|
89
|
+
ndays = MarsDateTime.sols_in_month(n, year) if n == 24
|
|
90
|
+
mtoday = MarsDateTime.today
|
|
91
|
+
@curmonth = mtoday.year == year && mtoday.month == n
|
|
92
|
+
list = [""]*dow + (1..ndays).to_a
|
|
93
|
+
l2 = []
|
|
94
|
+
loop do
|
|
95
|
+
l2 << list.slice!(0..6)
|
|
96
|
+
break if list.empty?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
@out.puts "<table border=0 cellspacing=0 cellpadding=2>"
|
|
100
|
+
month_header(n)
|
|
101
|
+
|
|
102
|
+
l2.each do |week|
|
|
103
|
+
@out.puts "<tr border=1>"
|
|
104
|
+
week.each do |sol|
|
|
105
|
+
@cursol = @curmonth && (mtoday.sol == sol)
|
|
106
|
+
bgcolor =
|
|
107
|
+
case [@curmonth, @cursol]
|
|
108
|
+
when [false, false], [false, true]
|
|
109
|
+
"#FFFFFF"
|
|
110
|
+
when [true, false]
|
|
111
|
+
"FFD0BF"
|
|
112
|
+
when [true, true]
|
|
113
|
+
"FFB0A0"
|
|
114
|
+
end
|
|
115
|
+
if sol == ""
|
|
116
|
+
@out.puts " <td bgcolor=#{bgcolor}> </td>"
|
|
117
|
+
else
|
|
118
|
+
mtime = MarsDateTime.new(year, n, sol)
|
|
119
|
+
etime = mtime.earth_date
|
|
120
|
+
@out.puts " <td align=right bgcolor=#{bgcolor}>"
|
|
121
|
+
@out.puts "<span title='#{etime.strftime('Day starts: Earth %F at %X UTC')}'>#{sol}</span>"
|
|
122
|
+
@out.puts " </td>"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
@out.puts "</tr>"
|
|
126
|
+
end
|
|
127
|
+
@out.puts "</table>"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def row(yr,i)
|
|
131
|
+
@out.puts "<tr>"
|
|
132
|
+
@out.puts "<td valign=top>"; month yr, i+1; @out.puts "</td>"
|
|
133
|
+
@out.puts "<td valign=top>"; month yr, i+2; @out.puts "</td>"
|
|
134
|
+
@out.puts "<td valign=top>"; month yr, i+3; @out.puts "</td>"
|
|
135
|
+
@out.puts "<td valign=top>"; month yr, i+4; @out.puts "</td>"
|
|
136
|
+
@out.puts "</tr>"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def initialize(year = MarsDateTime.today.year)
|
|
140
|
+
outfile = "/tmp/m#{year}.html"
|
|
141
|
+
@out = File.new(outfile, "w")
|
|
142
|
+
|
|
143
|
+
year = MarsDateTime.today.year unless year
|
|
144
|
+
year = year.to_i # in case of ARGV
|
|
145
|
+
|
|
146
|
+
@out.puts "<table>"
|
|
147
|
+
@out.puts "<tr bgcolor=red>"
|
|
148
|
+
@out.puts "<td align=center colspan=4>"
|
|
149
|
+
@out.puts "<font face=arial size=6 color=white>Year #{year} MCE</font>"
|
|
150
|
+
@out.puts "</td>"
|
|
151
|
+
@out.puts "</tr>"
|
|
152
|
+
6.times {|i| row(year, i*4) }
|
|
153
|
+
|
|
154
|
+
@out.puts "</table>"
|
|
155
|
+
@out.close
|
|
156
|
+
|
|
157
|
+
puts "\n See file #{outfile}\n "
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def calendar(year = nil, _extra = nil)
|
|
163
|
+
year = (year || MarsDateTime.today.year).to_i
|
|
164
|
+
CalendarHTML.new(year)
|
|
73
165
|
end
|
|
74
166
|
|
|
167
|
+
=begin
|
|
168
|
+
# LATER??
|
|
169
|
+
require 'marsdate'
|
|
170
|
+
|
|
171
|
+
def put_week(week)
|
|
172
|
+
week.each {|x| printf("%3s ",x.to_s) }
|
|
173
|
+
puts
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
mon, yr = ARGV
|
|
177
|
+
# p [mon, yr]
|
|
178
|
+
now = MarsDateTime.now
|
|
179
|
+
mon ||= now.month
|
|
180
|
+
yr ||= now.year
|
|
181
|
+
# p [mon, yr]
|
|
182
|
+
mon = mon.to_i
|
|
183
|
+
yr = yr.to_i
|
|
184
|
+
# p [mon, yr]
|
|
185
|
+
|
|
186
|
+
ftm = MarsDateTime.new(yr, mon, 1)
|
|
187
|
+
|
|
188
|
+
header = %w[Sun Mon Tue Wed Thu Fri Sat]
|
|
189
|
+
|
|
190
|
+
week1 = Array.new(7)
|
|
191
|
+
sol = 0
|
|
192
|
+
ftm.dow.upto(6) do |d|
|
|
193
|
+
week1[d] = (sol += 1)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
solN = MarsDateTime.sols_in_month(mon, yr)
|
|
197
|
+
remaining = ((sol += 1)..solN).to_a
|
|
198
|
+
|
|
199
|
+
weeks = []
|
|
200
|
+
weeks << header << week1
|
|
201
|
+
|
|
202
|
+
loop do
|
|
203
|
+
week = []
|
|
204
|
+
7.times { week << remaining.shift }
|
|
205
|
+
weeks << week
|
|
206
|
+
break if remaining.empty?
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
label = ftm.month_name + " " + ftm.year.to_s
|
|
210
|
+
spacing = " "*((27 - label.length)/2)
|
|
211
|
+
|
|
212
|
+
puts
|
|
213
|
+
puts spacing + label
|
|
214
|
+
puts
|
|
215
|
+
|
|
216
|
+
weeks.each {|week| put_week(week) }
|
|
217
|
+
|
|
218
|
+
puts
|
|
219
|
+
=end
|
|
220
|
+
|
|
221
|
+
#### Main
|
|
222
|
+
|
|
75
223
|
args = ARGV.size
|
|
76
224
|
cmd = ARGV.first
|
|
77
225
|
|
|
@@ -82,5 +230,9 @@ case
|
|
|
82
230
|
command(cmd)
|
|
83
231
|
when [2,3].include?(args)
|
|
84
232
|
invoke(ARGV[0], ARGV[1], ARGV[2])
|
|
233
|
+
else
|
|
234
|
+
warn "Wrong number of arguments (given #{args}, expected 0..3)"
|
|
235
|
+
usage
|
|
236
|
+
exit 1
|
|
85
237
|
end
|
|
86
238
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# MCE civil calendar on the official Mars timeline.
|
|
2
|
+
#
|
|
3
|
+
# integer MSD = EPOCH_MSD + sol_index(year, month, sol)
|
|
4
|
+
#
|
|
5
|
+
# EPOCH_MSD is the integer Mars Sol Date of 1/1/1 00:00 MTC: the
|
|
6
|
+
# Airy-0 midnight of the sol that contains the year-1 northern
|
|
7
|
+
# vernal equinox (Allison Ls=0). Provisional: Allison is fitted
|
|
8
|
+
# ~1874–2127; year 1 is an extrapolation. See TimeScale.epoch_report.
|
|
9
|
+
|
|
10
|
+
class MarsDateTime
|
|
11
|
+
module Calendar
|
|
12
|
+
EPOCH_MSD = -665_773
|
|
13
|
+
SOLS_COMMON = 668
|
|
14
|
+
SOLS_LEAP = 669
|
|
15
|
+
MONTHS_OF_28 = 23
|
|
16
|
+
MEAN_YEAR = 668.591
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# Same intercalation as MarsDateTime.leap? (MCE: /100 except /1000).
|
|
21
|
+
def leap?(year)
|
|
22
|
+
return (year % 2 == 1) if (year % 10 != 0)
|
|
23
|
+
return true if (year % 1000 == 0)
|
|
24
|
+
return false if (year % 100 == 0)
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def sols_in_year(year)
|
|
29
|
+
leap?(year) ? SOLS_LEAP : SOLS_COMMON
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def sols_in_month(month, year)
|
|
33
|
+
return 28 if month < 24
|
|
34
|
+
leap?(year) ? 25 : 24
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Leap years in 1..n inclusive. Closed form of the MCE rule.
|
|
38
|
+
def leaps_through(n)
|
|
39
|
+
return 0 if n <= 0
|
|
40
|
+
odds = (n + 1) / 2
|
|
41
|
+
tens = n / 10
|
|
42
|
+
centuries = n / 100
|
|
43
|
+
millennia = n / 1000
|
|
44
|
+
odds + tens - centuries + millennia
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def leaps_before(year)
|
|
48
|
+
year <= 1 ? 0 : leaps_through(year - 1)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# 0-based sol index of 1/1 of +year+ (1/1/1 → 0).
|
|
52
|
+
def sols_before_year(year)
|
|
53
|
+
(year - 1) * SOLS_COMMON + leaps_before(year)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def check_ymd!(year, month, sol)
|
|
57
|
+
text = +""
|
|
58
|
+
text << "year #{year} is not a positive integer\n" unless year.is_a?(Integer) && year >= 1
|
|
59
|
+
text << "month #{month} is out of range\n" unless month.is_a?(Integer) && (1..24).include?(month)
|
|
60
|
+
text << "sol #{sol} is out of range\n" unless sol.is_a?(Integer) && (1..28).include?(sol)
|
|
61
|
+
if year.is_a?(Integer) && month == 24 && sol.is_a?(Integer)
|
|
62
|
+
max = sols_in_month(24, year)
|
|
63
|
+
text << "sol #{sol} is invalid in Taurus of year #{year}\n" if sol > max
|
|
64
|
+
end
|
|
65
|
+
raise ArgumentError, text unless text.empty?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# 0-based sols since 1/1/1.
|
|
69
|
+
def sol_index(year, month, sol)
|
|
70
|
+
check_ymd!(year, month, sol)
|
|
71
|
+
sols_before_year(year) + (month - 1) * 28 + (sol - 1)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Integer MSD of Airy-0 midnight of that MCE date.
|
|
75
|
+
def msd_midnight(year, month, sol)
|
|
76
|
+
EPOCH_MSD + sol_index(year, month, sol)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Instant MSD: date + fraction of a sol in [0, 1).
|
|
80
|
+
def msd_at(year, month, sol, frac = 0.0)
|
|
81
|
+
unless frac.is_a?(Numeric) && frac >= 0.0 && frac < 1.0
|
|
82
|
+
raise ArgumentError, "sol fraction must be in [0, 1) (got #{frac.inspect})"
|
|
83
|
+
end
|
|
84
|
+
msd_midnight(year, month, sol) + frac.to_f
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def ymd(index)
|
|
88
|
+
unless index.is_a?(Integer) && index >= 0
|
|
89
|
+
raise ArgumentError,
|
|
90
|
+
"sol index must be a non-negative integer (got #{index.inspect})"
|
|
91
|
+
end
|
|
92
|
+
year = (index / MEAN_YEAR).floor + 1
|
|
93
|
+
year = 1 if year < 1
|
|
94
|
+
16.times do
|
|
95
|
+
start = sols_before_year(year)
|
|
96
|
+
stop = start + sols_in_year(year)
|
|
97
|
+
if index >= start && index < stop
|
|
98
|
+
return ymd_in_year(year, index - start)
|
|
99
|
+
end
|
|
100
|
+
year += 1 if index >= stop
|
|
101
|
+
year -= 1 if index < start
|
|
102
|
+
year = 1 if year < 1
|
|
103
|
+
end
|
|
104
|
+
raise "year search failed for sol index #{index}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def ymd_from_msd(msd)
|
|
108
|
+
ymd(msd.to_f.floor - EPOCH_MSD)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Sunday = 0, same as MarsDateTime#dow on 1/1/1.
|
|
112
|
+
def dow(year, month, sol)
|
|
113
|
+
sol_index(year, month, sol) % 7
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def ymd_in_year(year, offset)
|
|
117
|
+
if offset < MONTHS_OF_28 * 28
|
|
118
|
+
month = offset / 28 + 1
|
|
119
|
+
sol = offset % 28 + 1
|
|
120
|
+
else
|
|
121
|
+
month = 24
|
|
122
|
+
sol = offset - MONTHS_OF_28 * 28 + 1
|
|
123
|
+
end
|
|
124
|
+
[year, month, sol]
|
|
125
|
+
end
|
|
126
|
+
private_class_method :ymd_in_year
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Official Mars time scales (NASA/Allison / Mars24).
|
|
2
|
+
#
|
|
3
|
+
# Earth instant → JD (UT) → JD (TT) → MSD → MTC / MXT
|
|
4
|
+
#
|
|
5
|
+
# MTC = Coordinated Mars Time (24 stretched hours, Airy-0 mean midnight)
|
|
6
|
+
# MXT = Mars eXtended Time (SI hours, same midnight, day to ~24:39)
|
|
7
|
+
|
|
8
|
+
require 'date'
|
|
9
|
+
|
|
10
|
+
class MarsDateTime
|
|
11
|
+
module TimeScale
|
|
12
|
+
SOL_DAYS = 1.0274912517
|
|
13
|
+
SECONDS_PER_DAY = 86_400.0
|
|
14
|
+
SOL_SI_SECONDS = SOL_DAYS * SECONDS_PER_DAY # ~88775.244147
|
|
15
|
+
TT_MINUS_TAI = 32.184
|
|
16
|
+
|
|
17
|
+
# Mars24 C-2
|
|
18
|
+
MSD_JDTT_OFFSET = 2_451_549.5
|
|
19
|
+
MSD_EPOCH_SHIFT = 44_796.0
|
|
20
|
+
MSD_FINE = 0.0009626
|
|
21
|
+
|
|
22
|
+
J2000_JDTT = 2_451_545.0
|
|
23
|
+
|
|
24
|
+
# IERS Leap_Second.dat (Bulletin 72, July 2026). Each row is the
|
|
25
|
+
# UTC calendar date when TAI−UTC becomes that many seconds.
|
|
26
|
+
TAI_UTC_STEPS = [
|
|
27
|
+
[Date.new(1972, 1, 1), 10],
|
|
28
|
+
[Date.new(1972, 7, 1), 11],
|
|
29
|
+
[Date.new(1973, 1, 1), 12],
|
|
30
|
+
[Date.new(1974, 1, 1), 13],
|
|
31
|
+
[Date.new(1975, 1, 1), 14],
|
|
32
|
+
[Date.new(1976, 1, 1), 15],
|
|
33
|
+
[Date.new(1977, 1, 1), 16],
|
|
34
|
+
[Date.new(1978, 1, 1), 17],
|
|
35
|
+
[Date.new(1979, 1, 1), 18],
|
|
36
|
+
[Date.new(1980, 1, 1), 19],
|
|
37
|
+
[Date.new(1981, 7, 1), 20],
|
|
38
|
+
[Date.new(1982, 7, 1), 21],
|
|
39
|
+
[Date.new(1983, 7, 1), 22],
|
|
40
|
+
[Date.new(1985, 7, 1), 23],
|
|
41
|
+
[Date.new(1988, 1, 1), 24],
|
|
42
|
+
[Date.new(1990, 1, 1), 25],
|
|
43
|
+
[Date.new(1991, 1, 1), 26],
|
|
44
|
+
[Date.new(1992, 7, 1), 27],
|
|
45
|
+
[Date.new(1993, 7, 1), 28],
|
|
46
|
+
[Date.new(1994, 7, 1), 29],
|
|
47
|
+
[Date.new(1996, 1, 1), 30],
|
|
48
|
+
[Date.new(1997, 7, 1), 31],
|
|
49
|
+
[Date.new(1999, 1, 1), 32],
|
|
50
|
+
[Date.new(2006, 1, 1), 33],
|
|
51
|
+
[Date.new(2009, 1, 1), 34],
|
|
52
|
+
[Date.new(2012, 7, 1), 35],
|
|
53
|
+
[Date.new(2015, 7, 1), 36],
|
|
54
|
+
[Date.new(2017, 1, 1), 37]
|
|
55
|
+
].freeze
|
|
56
|
+
|
|
57
|
+
module_function
|
|
58
|
+
|
|
59
|
+
# Astronomical Julian Date of the instant (offset-independent).
|
|
60
|
+
def jd_ut(earth)
|
|
61
|
+
earth.to_datetime.ajd.to_f
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# TAI−UTC in seconds. Leap table from 1972-01-01; Mars24 polynomial
|
|
65
|
+
# for 1900-01-01...1972-01-01. Earlier dates raise (no UTC).
|
|
66
|
+
def tai_utc(earth)
|
|
67
|
+
dt = earth.to_datetime
|
|
68
|
+
date = dt.to_date
|
|
69
|
+
if date >= TAI_UTC_STEPS.first[0]
|
|
70
|
+
tai_utc_from_table(date)
|
|
71
|
+
elsif date >= Date.new(1900, 1, 1)
|
|
72
|
+
tt_utc_polynomial(jd_ut(dt)) - TT_MINUS_TAI
|
|
73
|
+
else
|
|
74
|
+
raise ArgumentError,
|
|
75
|
+
"TAI−UTC is undefined before 1900-01-01 (got #{date}); " \
|
|
76
|
+
"pass jd_tt: for antiquity"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def tt_utc(earth)
|
|
81
|
+
tai_utc(earth) + TT_MINUS_TAI
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def jd_tt(earth, jd_tt: nil)
|
|
85
|
+
return jd_tt.to_f if jd_tt
|
|
86
|
+
jd_ut(earth) + tt_minus_ut(earth) / SECONDS_PER_DAY
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# TT−UT in seconds. Leap table / polynomial from 1900; before
|
|
90
|
+
# that, 0 (treat the civil digits as TT). This is not a
|
|
91
|
+
# historical ΔT model. Year-1 Earth times are captions only.
|
|
92
|
+
def tt_minus_ut(earth)
|
|
93
|
+
dt = earth.to_datetime
|
|
94
|
+
return 0.0 if dt.to_date < Date.new(1900, 1, 1)
|
|
95
|
+
tt_utc(dt)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Mars Sol Date (Mars24 C-2).
|
|
99
|
+
def msd(earth = nil, jd_tt: nil)
|
|
100
|
+
j = jd_tt(earth, jd_tt: jd_tt)
|
|
101
|
+
(j - MSD_JDTT_OFFSET) / SOL_DAYS + MSD_EPOCH_SHIFT - MSD_FINE
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Fractional sol in [0, 1).
|
|
105
|
+
def sol_fraction(earth = nil, jd_tt: nil)
|
|
106
|
+
frac = msd(earth, jd_tt: jd_tt) % 1.0
|
|
107
|
+
frac += 1.0 if frac < 0
|
|
108
|
+
frac
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# MTC as hours in [0, 24).
|
|
112
|
+
def mtc_hours(earth = nil, jd_tt: nil)
|
|
113
|
+
sol_fraction(earth, jd_tt: jd_tt) * 24.0
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def mtc_hms(earth = nil, jd_tt: nil)
|
|
117
|
+
hours_to_hms(mtc_hours(earth, jd_tt: jd_tt), limit: 24)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# SI seconds since Airy-0 mean midnight (0 ... SOL_SI_SECONDS).
|
|
121
|
+
def mxt_si_seconds(earth = nil, jd_tt: nil)
|
|
122
|
+
sol_fraction(earth, jd_tt: jd_tt) * SOL_SI_SECONDS
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def mxt_hms(earth = nil, jd_tt: nil)
|
|
126
|
+
hours_to_hms(mxt_si_seconds(earth, jd_tt: jd_tt) / 3600.0)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def format_hms(h, m, s)
|
|
130
|
+
format('%02d:%02d:%06.3f', h, m, s)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Areocentric solar longitude Ls (degrees), Mars24 B-1…B-5.
|
|
134
|
+
def ls_deg(jd_tt)
|
|
135
|
+
dt = jd_tt.to_f - J2000_JDTT
|
|
136
|
+
mean_anom = 19.3871 + 0.52402073 * dt
|
|
137
|
+
alpha_fms = 270.3871 + 0.524038496 * dt
|
|
138
|
+
pbs = mars_pbs(dt)
|
|
139
|
+
nu_m = (10.691 + 3.0e-7 * dt) * sind(mean_anom) +
|
|
140
|
+
0.623 * sind(2 * mean_anom) +
|
|
141
|
+
0.050 * sind(3 * mean_anom) +
|
|
142
|
+
0.005 * sind(4 * mean_anom) +
|
|
143
|
+
0.0005 * sind(5 * mean_anom) + pbs
|
|
144
|
+
wrap360(alpha_fms + nu_m)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# JD_TT of the northern vernal equinox (Ls = 0) nearest +around_jd_tt+.
|
|
148
|
+
def vernal_equinox_jd_tt(around_jd_tt)
|
|
149
|
+
year_days = 686.98 # Earth days per Mars tropical year, rough
|
|
150
|
+
lo = around_jd_tt - year_days * 0.6
|
|
151
|
+
hi = around_jd_tt + year_days * 0.6
|
|
152
|
+
# Bracket a rising-through-0 crossing of Ls.
|
|
153
|
+
step = 2.0
|
|
154
|
+
t = lo
|
|
155
|
+
prev = wrap180(ls_deg(t))
|
|
156
|
+
pair = nil
|
|
157
|
+
while t <= hi
|
|
158
|
+
t += step
|
|
159
|
+
cur = wrap180(ls_deg(t))
|
|
160
|
+
if prev < 0 && cur >= 0
|
|
161
|
+
pair = [t - step, t]
|
|
162
|
+
break
|
|
163
|
+
end
|
|
164
|
+
prev = cur
|
|
165
|
+
end
|
|
166
|
+
raise "no Ls=0 crossing near JD_TT #{around_jd_tt}" unless pair
|
|
167
|
+
a, b = pair
|
|
168
|
+
60.times do
|
|
169
|
+
mid = (a + b) / 2.0
|
|
170
|
+
if wrap180(ls_deg(mid)) < 0
|
|
171
|
+
a = mid
|
|
172
|
+
else
|
|
173
|
+
b = mid
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
(a + b) / 2.0
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Civil DateTime corresponding to an astronomical JD, treated as UT.
|
|
180
|
+
def datetime_from_ajd(ajd)
|
|
181
|
+
DateTime.jd(ajd + 0.5)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def epoch_report
|
|
185
|
+
# Allison Ls is fitted for ~1874–2127; year 1 is an extrapolation.
|
|
186
|
+
# Seed the search at the article’s “late January of Year 1.”
|
|
187
|
+
# Calendar::EPOCH_MSD is the frozen floor of this search.
|
|
188
|
+
#
|
|
189
|
+
# Earth fields treat JD_TT as UT (ΔT = 0). A real 1 CE ΔT is a
|
|
190
|
+
# few hours and would only slide these captions, not EPOCH_MSD.
|
|
191
|
+
hint = DateTime.new(1, 1, 22, 12, 0, 0)
|
|
192
|
+
around = hint.ajd.to_f
|
|
193
|
+
eq_tt = vernal_equinox_jd_tt(around)
|
|
194
|
+
eq_msd = msd(jd_tt: eq_tt)
|
|
195
|
+
epoch_msd = eq_msd.floor.to_i
|
|
196
|
+
midnight_tt = jd_tt_from_msd(epoch_msd)
|
|
197
|
+
frac = eq_msd - epoch_msd
|
|
198
|
+
{
|
|
199
|
+
caveat: 'Allison Ls extrapolated to 1 CE; Earth times treat JD_TT as UT (no year-1 ΔT)',
|
|
200
|
+
ls0_jd_tt: eq_tt,
|
|
201
|
+
ls0_msd: eq_msd,
|
|
202
|
+
ls0_mtc: format_hms(*hours_to_hms(frac * 24.0)),
|
|
203
|
+
ls0_earth_if_tt_were_ut: datetime_from_ajd(eq_tt),
|
|
204
|
+
epoch_msd: epoch_msd,
|
|
205
|
+
epoch_jd_tt: midnight_tt,
|
|
206
|
+
epoch_earth_if_tt_were_ut: datetime_from_ajd(midnight_tt)
|
|
207
|
+
}
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def jd_tt_from_msd(msd)
|
|
211
|
+
# invert Mars24 C-2
|
|
212
|
+
((msd - MSD_EPOCH_SHIFT + MSD_FINE) * SOL_DAYS) + MSD_JDTT_OFFSET
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Invert jd_tt: TT → UT. Before 1900, treat TT as UT.
|
|
216
|
+
def jd_ut_from_jd_tt(jd_tt)
|
|
217
|
+
j = jd_tt.to_f
|
|
218
|
+
guess = j - 69.184 / SECONDS_PER_DAY
|
|
219
|
+
8.times do
|
|
220
|
+
earth = datetime_from_ajd(guess)
|
|
221
|
+
if earth.to_date < Date.new(1900, 1, 1)
|
|
222
|
+
return j
|
|
223
|
+
end
|
|
224
|
+
nxt = j - tt_utc(earth) / SECONDS_PER_DAY
|
|
225
|
+
return nxt if (nxt - guess).abs < 1e-14
|
|
226
|
+
guess = nxt
|
|
227
|
+
end
|
|
228
|
+
guess
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def earth_from_msd(msd)
|
|
232
|
+
datetime_from_ajd(jd_ut_from_jd_tt(jd_tt_from_msd(msd)))
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def sol_fraction_of(msd)
|
|
236
|
+
frac = msd.to_f % 1.0
|
|
237
|
+
frac += 1.0 if frac < 0
|
|
238
|
+
frac = 0.0 if frac >= 1.0
|
|
239
|
+
frac
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def tai_utc_from_table(date)
|
|
243
|
+
value = TAI_UTC_STEPS.first[1]
|
|
244
|
+
TAI_UTC_STEPS.each do |d, v|
|
|
245
|
+
break if date < d
|
|
246
|
+
value = v
|
|
247
|
+
end
|
|
248
|
+
value.to_f
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Mars24 A-4, dates before 1972 (used here only back to 1900).
|
|
252
|
+
def tt_utc_polynomial(jd_ut)
|
|
253
|
+
t = (jd_ut - J2000_JDTT) / 36_525.0
|
|
254
|
+
64.184 + 59.0 * t - 51.2 * t**2 - 67.1 * t**3 - 16.4 * t**4
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def mars_pbs(dt)
|
|
258
|
+
terms = [
|
|
259
|
+
[0.0071, 2.2353, 49.409],
|
|
260
|
+
[0.0057, 2.7543, 168.173],
|
|
261
|
+
[0.0039, 1.1177, 191.837],
|
|
262
|
+
[0.0037, 15.7866, 21.736],
|
|
263
|
+
[0.0021, 2.1354, 15.704],
|
|
264
|
+
[0.0020, 2.4694, 95.528],
|
|
265
|
+
[0.0018, 32.8493, 49.095]
|
|
266
|
+
]
|
|
267
|
+
terms.sum do |amp, tau, phase|
|
|
268
|
+
amp * cosd(0.985626 * dt / tau + phase)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def hours_to_hms(hours, limit: nil)
|
|
273
|
+
h = hours.floor
|
|
274
|
+
minf = (hours - h) * 60.0
|
|
275
|
+
m = minf.floor
|
|
276
|
+
s = ((minf - m) * 60.0).round(4)
|
|
277
|
+
if s >= 59.9995
|
|
278
|
+
if limit && h == limit - 1 && m == 59
|
|
279
|
+
# The instant is still inside the half-open clock interval.
|
|
280
|
+
# Keep its rounded display below the following midnight.
|
|
281
|
+
s = 59.9994
|
|
282
|
+
else
|
|
283
|
+
s = 0.0
|
|
284
|
+
m += 1
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
if m >= 60
|
|
288
|
+
m = 0
|
|
289
|
+
h += 1
|
|
290
|
+
end
|
|
291
|
+
[h, m, s]
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def sind(deg)
|
|
295
|
+
Math.sin(deg * Math::PI / 180.0)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def cosd(deg)
|
|
299
|
+
Math.cos(deg * Math::PI / 180.0)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def wrap360(deg)
|
|
303
|
+
d = deg % 360.0
|
|
304
|
+
d += 360.0 if d < 0
|
|
305
|
+
d
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def wrap180(deg)
|
|
309
|
+
d = wrap360(deg)
|
|
310
|
+
d -= 360.0 if d > 180.0
|
|
311
|
+
d
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|