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.
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 # today's Martian date
12
- marsdate now # today's Martian date/time
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
- def calendar(year, invalid)
70
- puts "'calendar' takes only a year parameter" unless invalid.nil?
71
- year ||= MarsDateTime.today.year
72
- html_calendar(year)
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}>&nbsp;</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
@@ -0,0 +1,3 @@
1
+ class MarsDateTime
2
+ VERSION = '2.0.0'
3
+ end