marsdate 1.1.7 → 2.0.1

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/lib/marsdate.rb CHANGED
@@ -1,328 +1,378 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
4
+ require 'json'
5
+ require_relative 'marsdate/version'
6
+ require_relative 'marsdate/timescale'
7
+ require_relative 'marsdate/calendar'
2
8
 
3
9
  class MarsDateTime
4
10
 
5
- VERSION = "1.1.7"
6
-
7
11
  include Comparable
8
12
 
9
- MSEC_PER_SOL = 88775244 # Really + 0.09
10
- SOLS_PER_MYEAR = 668.5921
11
- MSEC_PER_DAY = 86400000
12
-
13
- FAKE_MSEC_PER_MYEAR = (668*MSEC_PER_SOL)
14
-
15
- TimeStretch = MSEC_PER_SOL/MSEC_PER_DAY.to_f
16
-
17
13
  Months = %w[ UNDEFINED
18
14
  M-January Gemini M-February Cancer
19
- M-March Leo M-April Virgo
20
- M-May Libra M-June Scorpio
21
- M-July Sagittarius M-August Capricorn
22
- M-September Aquarius M-October Pisces
23
- M-November Aries M-December Taurus ]
15
+ M-March Leo M-April Virgo
16
+ M-May Libra M-June Scorpio
17
+ M-July Sagittarius M-August Capricorn
18
+ M-September Aquarius M-October Pisces
19
+ M-November Aries M-December Taurus ]
24
20
  # no month 0
25
21
 
26
- Week = %w[ Sunday Monday Tuesday Wednesday Thursday Friday Saturday ]
22
+ Week = %w[ Sunday Monday Tuesday Wednesday Thursday Friday Saturday ]
27
23
 
28
- EpochMCE = DateTime.new(1,1,21)
29
- EpochCE = DateTime.new(1,1,1)
30
- JulianDay1 = 1721445 # Jan 1, 1 AD
24
+ EPOCH_MSD = Calendar::EPOCH_MSD
25
+ SOL_DAYS = TimeScale::SOL_DAYS
26
+ SOL_SI_SECONDS = TimeScale::SOL_SI_SECONDS
27
+ SOLS_PER_MYEAR = Calendar::MEAN_YEAR
31
28
 
29
+ attr_reader :msd
32
30
  attr_reader :year, :month, :sol, :epoch_sol, :year_sol
33
- attr_reader :shr, :smin, :ssec # stretched time
34
- attr_reader :mems
35
-
36
31
  attr_reader :dow, :day_of_week
37
- attr_reader :mhrs, :mmin, :msec
32
+ attr_reader :mtc_hour, :mtc_min, :mtc_sec
33
+ attr_reader :mxt_hour, :mxt_min, :mxt_sec
38
34
 
39
35
  alias myear year
40
- alias hr mhrs
41
- alias min mmin
42
- alias sec msec
36
+ alias day sol
37
+
38
+ # Named-clock readout of one instant. MarsDateTime.mxt(...) constructs;
39
+ # md.mxt reads. Date fields are the parent's; %H/%M/%S are this clock.
40
+ class ClockView
41
+ attr_reader :scale
42
+
43
+ def initialize(parent, scale)
44
+ @parent = parent
45
+ @scale = scale
46
+ end
47
+
48
+ def hour
49
+ @scale == :mxt ? @parent.mxt_hour : @parent.mtc_hour
50
+ end
51
+
52
+ def min
53
+ @scale == :mxt ? @parent.mxt_min : @parent.mtc_min
54
+ end
43
55
 
44
- def self.leap?(myear) # class method for convenience
45
- return (myear % 2 == 1) if (myear % 10 != 0)
46
- return true if (myear % 1000 == 0)
47
- return false if (myear % 100 == 0)
48
- return true
56
+ def sec
57
+ @scale == :mxt ? @parent.mxt_sec : @parent.mtc_sec
58
+ end
59
+
60
+ def year; @parent.year; end
61
+ def month; @parent.month; end
62
+ def sol; @parent.sol; end
63
+ def day_of_week; @parent.day_of_week; end
64
+ def month_name; @parent.month_name; end
65
+ def dow; @parent.dow; end
66
+ def year_sol; @parent.year_sol; end
67
+ def epoch_sol; @parent.epoch_sol; end
68
+ def msd; @parent.msd; end
69
+ def earth_date; @parent.earth_date; end
70
+
71
+ def to_s
72
+ TimeScale.format_hms(hour, min, sec)
73
+ end
74
+
75
+ def inspect
76
+ "#<#{@scale.to_s.upcase} #{self}>"
77
+ end
78
+
79
+ # Specifier list inspired by Time#strftime. %H/%M/%S/%X are this clock;
80
+ # %Z is the clock name (MXT or MTC), not a timezone.
81
+ def format(fmt)
82
+ @parent.send(:format_with, fmt, hour, min, sec, @scale.to_s.upcase)
83
+ end
84
+ end
85
+
86
+ def self.leap?(myear)
87
+ Calendar.leap?(myear)
49
88
  end
50
89
 
51
- def self.short?(myear) # short year
90
+ def self.short?(myear)
52
91
  !leap?(myear)
53
92
  end
54
93
 
55
- def self.long?(myear) # long year
94
+ def self.long?(myear)
56
95
  leap?(myear)
57
96
  end
58
97
 
59
98
  def self.sols_in_month(m, year)
60
- return 28 if m < 24
61
- return 25 if leap?(year)
62
- return 24
99
+ Calendar.sols_in_month(m, year)
100
+ end
101
+
102
+ def self.from_msd(msd)
103
+ at(msd)
104
+ end
105
+
106
+ def self.at(msd)
107
+ obj = allocate
108
+ obj.send(:init_from_msd, msd)
109
+ obj
110
+ end
111
+
112
+ def self.mxt(year, month, sol, hour = 0, min = 0, sec = 0)
113
+ civil(year, month, sol, hour, min, sec, :mxt)
114
+ end
115
+
116
+ def self.mtc(year, month, sol, hour = 0, min = 0, sec = 0)
117
+ civil(year, month, sol, hour, min, sec, :mtc)
63
118
  end
64
119
 
65
120
  def self.now
66
- d = DateTime.now
67
- MarsDateTime.new(d)
121
+ new(DateTime.now)
68
122
  end
69
123
 
124
+ # Current MCE sol at 00:00 (MXT = MTC midnight). Not Earth Date.today.
70
125
  def self.today
71
- d = Date.today
72
- MarsDateTime.new(d)
126
+ n = now
127
+ mxt(n.year, n.month, n.sol)
73
128
  end
74
129
 
75
- def leaps(myr)
76
- n = 0
77
- 1.upto(myr) {|i| n+=1 if MarsDateTime.leap?(i) }
78
- n
130
+ # Earth caption of 1/1/1 00:00 and of Ls=0 on that sol.
131
+ # Times treat JD_TT as UT (no year-1 ΔT). See TimeScale.epoch_report.
132
+ def self.epoch_caption
133
+ TimeScale.epoch_report
79
134
  end
80
135
 
81
- def to_yaml_properties
82
- %w[@myear @month @sol @epoch_sol @year_sol @dow @day_of_week @msme @mhrs @mmin @msec]
136
+ def self.from_json(str)
137
+ hash = JSON.parse(str)
138
+ msd = hash['msd'] || hash[:msd]
139
+ raise ArgumentError, 'JSON must include msd' if msd.nil?
140
+ from_msd(msd)
83
141
  end
84
142
 
85
- def inspect
86
- time = ('%02d' % @mhrs) + ":" + ('%02d' % @mmin) + ":" + ('%02d' % @msec)
87
- "#@year/#{'%02d' % @month}/#{'%02d' % @sol} " +
88
- "(#@year_sol, #@epoch_sol) #@day_of_week " +
89
- time
143
+ def initialize(*params)
144
+ n = params.size
145
+ case n
146
+ when 3..6
147
+ init_yms(*params, clock: :mxt)
148
+ when 0
149
+ init_from_msd(TimeScale.msd(DateTime.now))
150
+ when 1
151
+ case params.first
152
+ when Integer, Float
153
+ init_from_msd(params.first)
154
+ when Date, DateTime
155
+ init_from_msd(TimeScale.msd(params.first))
156
+ else
157
+ raise ArgumentError, "Expected MSD, Date, or DateTime (got #{params.first.class})"
158
+ end
159
+ else
160
+ raise ArgumentError, "Bad params: #{params.inspect}"
161
+ end
90
162
  end
91
163
 
92
- def to_s
93
- time = self.strftime('%H:%M:%S [%P:%Q:%R]')
94
- "#@day_of_week, #{Months[@month]} #@sol, #@year at #{time}"
164
+ def mxt
165
+ ClockView.new(self, :mxt)
95
166
  end
96
167
 
97
- def leap?
98
- MarsDateTime.leap?(@year) # DRY
168
+ def mtc
169
+ ClockView.new(self, :mtc)
99
170
  end
100
171
 
101
- def short?
102
- ! leap?
172
+ def as_json(_options = {})
173
+ { msd: @msd,
174
+ year: @year,
175
+ month: @month,
176
+ sol: @sol,
177
+ epoch_sol: @epoch_sol,
178
+ year_sol: @year_sol,
179
+ dow: @dow,
180
+ day_of_week: @day_of_week,
181
+ mtc: format_mtc,
182
+ mxt: format_mxt }
103
183
  end
104
184
 
105
- def long?
106
- leap?
185
+ def to_json(*options)
186
+ as_json.to_json(*options)
107
187
  end
108
188
 
109
- def month_name
110
- Months[@month]
189
+ def to_yaml_properties
190
+ %w[@msd]
111
191
  end
112
192
 
113
- ###########
193
+ def inspect
194
+ sprintf('%d/%02d/%02d (%d, %d) %s MXT %s MTC %s MSD %.8f',
195
+ @year, @month, @sol, @year_sol, @epoch_sol, @day_of_week,
196
+ format_mxt, format_mtc, @msd)
197
+ end
114
198
 
115
- def initialize(*params)
116
- n = params.size
117
- case n
118
- when 3..6
119
- init_yms(*params)
120
- when 0
121
- init_datetime(DateTime.now)
122
- when 1
123
- case params.first
124
- when Integer, Float
125
- init_mems(params.first)
126
- when Date
127
- init_date(params.first)
128
- when DateTime
129
- init_datetime(params.first)
130
- else
131
- raise "Expected number or DateTime"
132
- end
133
- else
134
- raise "Bad params: #{params.inspect}"
135
- end
136
- compute_stretched
137
- end
138
-
139
- def check_ymshms(my, mm, msol, mhr=0, mmin=0, msec=0)
140
- text = ""
141
- text << "year #{my} is not an integer\n" unless my.is_a? Integer
142
- text << "month #{mm} is out of range" unless (1..24).include? mm
143
- text << "sol #{msol} is out of range" unless (1..28).include? msol
144
- text << "hour #{mhr} is out of range" unless (0..24).include? mhr
145
- text << "minute #{mmin} is out of range" unless (0..59).include? mmin
146
- text << "second #{msec} is out of range" unless (0..59).include? msec
147
- if !MarsDateTime.leap?(my) && mm == 24 && msol > 24
148
- text << "sol #{msol} is invalid in a non-leap year"
149
- end
150
- raise text unless text.empty?
151
- end
152
-
153
- def init_yms(my, mm, msol, mhr=0, mmin=0, msec=0)
154
- check_ymshms(my, mm, msol, mhr, mmin, msec)
155
- zsol = msol - 1 # z means zero-based
156
- zmy = my - 1 # my means Martian year
157
- zesol = zmy*668 + leaps(my-1) + (mm-1)*28 + zsol
158
- # @mems is "Martian (time since) epoch in milliseconds"
159
- @mems = zesol*MSEC_PER_SOL + (mhr*3600 + mmin*60 + msec)*1000
160
- @year, @month, @sol, @mhrs, @mmin, @msec = my, mm, msol, mhr, mmin, msec
161
- @epoch_sol = zesol + 1
162
- @dow = (@epoch_sol-1) % 7
163
- @day_of_week = Week[@dow]
164
- @year_sol = (mm-1)*28 + msol
165
- end
166
-
167
- def compute_stretched
168
- # Handle stretched time...
169
- sec = @mhrs*3600 + @mmin*60 + @msec
170
- sec /= TimeStretch
171
- @shr, sec = sec.divmod(3600)
172
- @smin, sec = sec.divmod(60)
173
- @ssec = sec.round
174
- end
175
-
176
- def init_mems(mems)
177
- # Note: The sol length is off by 0.09 msec -- to properly fix this
178
- # will require measuring in microseconds so as to avoid floating-point math.
179
- # The "round" calls below were experimental and were "mostly" successful.
180
- full_years = 0
181
- loop do
182
- millisec = FAKE_MSEC_PER_MYEAR
183
- millisec += MSEC_PER_SOL if MarsDateTime.leap?(full_years+1)
184
- break if mems < millisec
185
- mems -= millisec
186
- # puts "Subtracting #{millisec} -- one full year => #{mems}"
187
- full_years += 1
188
- end
199
+ def to_s
200
+ "#{@day_of_week}, #{month_name} #{@sol}, #{@year} at MXT #{format_mxt} / MTC #{format_mtc}"
201
+ end
202
+
203
+ def format_mtc(fmt = nil)
204
+ fmt ? format_with(fmt, @mtc_hour, @mtc_min, @mtc_sec, 'MTC') : mtc.to_s
205
+ end
189
206
 
190
- mspm = MSEC_PER_SOL*28
191
- full_months,mems = mems.divmod(mspm)
192
- full_days, mems = mems.divmod(MSEC_PER_SOL)
193
- full_hrs, mems = mems.divmod(3_600_000)
194
- full_min, mems = mems.divmod(60_000)
207
+ def format_mxt(fmt = nil)
208
+ fmt ? format_with(fmt, @mxt_hour, @mxt_min, @mxt_sec, 'MXT') : mxt.to_s
209
+ end
195
210
 
196
- sec = mems/1000.0
197
- my = full_years + 1 # 1-based
198
- mm = full_months + 1
199
- ms = full_days + 1
200
- mhr = full_hrs # 0-based
201
- mmin = full_min
202
- msec = sec.to_i
203
- frac = sec - msec # fraction of a sec
211
+ def leap?
212
+ MarsDateTime.leap?(@year)
213
+ end
204
214
 
205
- init_yms(my, mm, ms, mhr, mmin, msec)
215
+ def short?
216
+ !leap?
206
217
  end
207
218
 
208
- def init_date(date)
209
- dt = date.to_datetime
210
- days = dt.jd - JulianDay1
211
- secs = days*86400 + dt.hour*3600 + dt.min*60 + dt.sec
212
- init_mems(secs*1000)
219
+ def long?
220
+ leap?
213
221
  end
214
222
 
215
- def init_datetime(dt)
216
- days = dt.jd - JulianDay1
217
- secs = days*86400 + dt.hour*3600 + dt.min*60 + dt.sec
218
- init_mems(secs*1000)
223
+ def month_name
224
+ Months[@month]
219
225
  end
220
226
 
221
227
  def ymshms
222
- [@year, @month, @sol, @mhrs, @mmin, @msec]
228
+ [@year, @month, @sol, @mxt_hour, @mxt_min, @mxt_sec.to_i]
223
229
  end
224
230
 
225
- def -(other) # FIXME? sols or secs?
231
+ def -(other)
226
232
  case other
227
- when MarsDateTime
228
- diff = @mems - other.mems
229
- diff.to_f / MSEC_PER_SOL
230
- when DateTime
231
- other = MarsDateTime.new(other)
232
- diff = @mems - other.mems
233
- diff.to_f / MSEC_PER_SOL
234
- when Integer, Float
235
- self + (-other)
233
+ when MarsDateTime
234
+ @msd - other.msd
235
+ when Date, DateTime
236
+ @msd - MarsDateTime.new(other).msd
237
+ when Integer, Float
238
+ self + (-other)
236
239
  else
237
- raise "Unexpected data type"
240
+ raise ArgumentError, "Unexpected data type #{other.class}"
238
241
  end
239
242
  end
240
243
 
241
- def +(sols) # FIXME? sols or secs?
242
- millisec = sols * MSEC_PER_SOL
243
- MarsDateTime.new(@mems + millisec)
244
+ def +(sols)
245
+ MarsDateTime.at(@msd + sols.to_f)
244
246
  end
245
247
 
246
248
  def <=>(other)
247
249
  case other
248
- when MarsDateTime
249
- @mems <=> other.mems
250
- when DateTime
251
- @mems <=> MarsDateTime.new(other).mems
250
+ when MarsDateTime
251
+ @msd <=> other.msd
252
+ when Date, DateTime
253
+ @msd <=> MarsDateTime.new(other).msd
252
254
  else
253
- raise "Invalid comparison"
255
+ raise ArgumentError, "Invalid comparison with #{other.class}"
254
256
  end
255
257
  end
256
258
 
257
259
  def earth_date
258
- secs = @mems/1000
259
- days,secs = secs.divmod(86400)
260
- hrs, secs = secs.divmod(3600)
261
- min, secs = secs.divmod(60)
262
- jday = days + JulianDay1
263
- DateTime.jd(jday, hrs, min, secs)
264
- end
265
-
266
- def strftime(fmt)
267
- str = fmt.dup
268
- pieces = str.scan(/(%.|[^%]+)/).flatten
269
- final = ""
260
+ TimeScale.earth_from_msd(@msd)
261
+ end
262
+
263
+ # Date tokens plus MXT for %H/%M/%S/%X. Specifier list inspired by
264
+ # Time#strftime. Same as format_mxt(fmt). MTC: format_mtc(fmt) or md.mtc.format.
265
+ def format(fmt)
266
+ format_mxt(fmt)
267
+ end
268
+
269
+ def self.civil(year, month, sol, hour, min, sec, clock)
270
+ obj = allocate
271
+ obj.send(:init_yms, year, month, sol, hour, min, sec, clock: clock)
272
+ obj
273
+ end
274
+ private_class_method :civil
275
+
276
+ def format_with(fmt, hour, min, sec, clock)
277
+ pieces = fmt.to_s.scan(/(%%|%.|%|[^%]+)/).flatten
278
+ final = String.new
270
279
  zmonth = '%02d' % @month
271
280
  zsol = '%02d' % @sol
272
- zhh = '%02d' % @mhrs # stretched
273
- zmm = '%02d' % @mmin
274
- zss = '%02d' % @msec
275
- zhc = '%02d' % @shr # canonical
276
- zmc = '%02d' % @smin
277
- zsc = '%02d' % @ssec
278
-
281
+ zhh = '%02d' % hour
282
+ zmm = '%02d' % min
283
+ zss = '%02d' % sec.to_i
284
+
279
285
  pieces.each do |piece|
280
286
  case piece
281
- when "%a"; final << @day_of_week[0..2]
282
- when "%A"; final << @day_of_week
283
- when "%b"; final << (@month.odd? ? month_name[2..4] : month_name[0..2])
284
- when "%B"; final << month_name
285
- when "%d"; final << zsol
286
- when "%e"; final << ('%2d' % @sol)
287
- when "%F"; final << "#@year-#{zmonth}-#{zsol}"
288
- when "%H"; final << zhh
289
- when "%j"; final << @year_sol.to_s
290
- when "%m"; final << zmonth # @month.to_s
291
- when "%M"; final << zmm
292
- when "%s"; final << @msec.to_s # was: (@mems*1000).to_i.to_s
293
- when "%S"; final << zss
294
- when "%u"; final << (@dow + 1).to_s
295
- when "%U"; final << (@year_sol/7 + 1).to_s
296
- when "%w"; final << @dow.to_s
297
- when "%x"; final << "#@year/#{zmonth}/#{zsol}"
298
- when "%X"; final << "#{zhh}:#{zmm}:#{zss}"
299
- when "%Y"; final << @year.to_s
300
- when "%n"; final << "\n"
301
- when "%t"; final << "\t"
302
- when "%%"; final << "%"
303
- when "%P"; final << ("%02d" % @shr)
304
- when "%Q"; final << ("%02d" % @smin)
305
- when "%R"; final << ("%02d" % @ssec)
306
- else
307
- final << piece
287
+ when '%a'; final << @day_of_week[0..2]
288
+ when '%A'; final << @day_of_week
289
+ when '%b'; final << (@month.odd? ? month_name[2..4] : month_name[0..2])
290
+ when '%B'; final << month_name
291
+ when '%d'; final << zsol
292
+ when '%e'; final << ('%2d' % @sol)
293
+ when '%F'; final << "#{@year}-#{zmonth}-#{zsol}"
294
+ when '%H'; final << zhh
295
+ when '%j'; final << @year_sol.to_s
296
+ when '%m'; final << zmonth
297
+ when '%M'; final << zmm
298
+ when '%s'; final << sec.to_i.to_s
299
+ when '%S'; final << zss
300
+ when '%u'; final << (@dow + 1).to_s
301
+ when '%U'; final << (@year_sol / 7 + 1).to_s
302
+ when '%w'; final << @dow.to_s
303
+ when '%x'; final << "#{@year}/#{zmonth}/#{zsol}"
304
+ when '%X'; final << "#{zhh}:#{zmm}:#{zss}"
305
+ when '%Y'; final << @year.to_s
306
+ when '%Z'; final << clock
307
+ when '%n'; final << "\n"
308
+ when '%t'; final << "\t"
309
+ when '%%'; final << '%'
310
+ else
311
+ final << piece
308
312
  end
309
313
  end
310
314
  final
311
315
  end
312
-
313
- =begin
314
- * %I - Hour of the day, 12-hour clock (01..12)
315
- * %p - Meridian indicator (``AM'' or ``PM'')
316
- %U - Week number of the current year,
317
- starting with the first Sunday as the first
318
- day of the first week (00..53)
319
- %W - Week number of the current year,
320
- starting with the first Monday as the first
321
- day of the first week (00..53)
322
- * %y - Year without a century (00..99)
323
- %Z - Time zone name
324
-
325
- =end
316
+ private :format_with
326
317
 
327
- end
318
+ private
319
+
320
+ def init_yms(my, mm, msol, mhr = 0, mmin = 0, msec = 0, clock: :mxt)
321
+ Calendar.check_ymd!(my, mm, msol)
322
+ check_hms!(mhr, mmin, msec, clock)
323
+ frac = frac_from_hms(mhr, mmin, msec, clock)
324
+ init_from_msd(Calendar.msd_at(my, mm, msol, frac))
325
+ end
326
+
327
+ def init_from_msd(msd)
328
+ @msd = msd.to_f
329
+ index = @msd.floor - EPOCH_MSD
330
+ if index < 0
331
+ raise ArgumentError, "MSD #{@msd} is before MCE 1/1/1 (EPOCH_MSD=#{EPOCH_MSD})"
332
+ end
333
+ @year, @month, @sol = Calendar.ymd_from_msd(@msd)
334
+ @epoch_sol = Calendar.sol_index(@year, @month, @sol) + 1
335
+ @year_sol = (@month - 1) * 28 + @sol
336
+ @dow = Calendar.dow(@year, @month, @sol)
337
+ @day_of_week = Week[@dow]
338
+ frac = TimeScale.sol_fraction_of(@msd)
339
+ @mtc_hour, @mtc_min, @mtc_sec = TimeScale.hours_to_hms(
340
+ frac * 24.0, limit: 24
341
+ )
342
+ @mxt_hour, @mxt_min, @mxt_sec = TimeScale.hours_to_hms(
343
+ frac * SOL_SI_SECONDS / 3600.0
344
+ )
345
+ end
346
+
347
+ def check_hms!(h, min, sec, clock)
348
+ unless h.is_a?(Numeric) && min.is_a?(Numeric) && sec.is_a?(Numeric)
349
+ raise ArgumentError, 'hour/minute/second must be numeric'
350
+ end
351
+ unless min >= 0 && min < 60 && sec >= 0 && sec < 60
352
+ raise ArgumentError, "minute/second out of range (#{min}:#{sec})"
353
+ end
354
+ case clock.to_sym
355
+ when :mtc
356
+ hours = h.to_f + min.to_f / 60.0 + sec.to_f / 3600.0
357
+ unless hours >= 0.0 && hours < 24.0
358
+ raise ArgumentError, "MTC #{h}:#{min}:#{sec} is outside [00:00:00, 24:00:00)"
359
+ end
360
+ when :mxt
361
+ si = h.to_f * 3600.0 + min.to_f * 60.0 + sec.to_f
362
+ unless si >= 0.0 && si < SOL_SI_SECONDS
363
+ raise ArgumentError, "MXT #{h}:#{min}:#{sec} is outside one sol"
364
+ end
365
+ else
366
+ raise ArgumentError, "clock must be :mtc or :mxt (got #{clock.inspect})"
367
+ end
368
+ end
328
369
 
370
+ def frac_from_hms(h, min, sec, clock)
371
+ case clock.to_sym
372
+ when :mtc
373
+ (h.to_f + min.to_f / 60.0 + sec.to_f / 3600.0) / 24.0
374
+ when :mxt
375
+ (h.to_f * 3600.0 + min.to_f * 60.0 + sec.to_f) / SOL_SI_SECONDS
376
+ end
377
+ end
378
+ end
data/marsdate.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ require_relative 'lib/marsdate/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'marsdate'
5
+ s.version = MarsDateTime::VERSION
6
+ s.license = 'MIT'
7
+ s.summary = 'Date/time library for Mars'
8
+ s.description = <<-EOS
9
+ This is a library for handling dates and times on Mars
10
+ for the Martian Common Era calendar (created by Hal Fulton).
11
+ The functionality closely follows that of Ruby's Time class.
12
+ EOS
13
+ s.authors = ['Hal Fulton']
14
+ s.email = 'rubyhacker@gmail.com'
15
+ s.files = Dir['lib/**/*.rb', 'bin/*', 'test/**/*.rb'] +
16
+ %w[CHANGELOG.md LICENSE.txt README.md marsdate.gemspec]
17
+ s.bindir = 'bin'
18
+ s.executables = ['marsdate']
19
+ s.require_paths = ['lib']
20
+ s.required_ruby_version = '>= 2.7'
21
+ s.homepage = 'https://github.com/Hal9000/marsdate'
22
+ s.metadata = {
23
+ 'bug_tracker_uri' => "#{s.homepage}/issues",
24
+ 'changelog_uri' => "#{s.homepage}/blob/master/CHANGELOG.md",
25
+ 'source_code_uri' => s.homepage
26
+ }
27
+ s.add_development_dependency 'minitest', '~> 6.0'
28
+ s.add_development_dependency 'rake', '~> 13.4'
29
+ s.post_install_message = "\n Success! Run executable 'marsdate' for help.\n "
30
+ end
31
+