timesteps 0.9.3
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 +7 -0
- data/LICENSES +22 -0
- data/Note.ja.md +76 -0
- data/README.md +57 -0
- data/Rakefile +11 -0
- data/lib/timesteps/calendar.rb +104 -0
- data/lib/timesteps/datetimelike.rb +530 -0
- data/lib/timesteps/format.rb +251 -0
- data/lib/timesteps/grads.rb +43 -0
- data/lib/timesteps/parse_timestamp.rb +48 -0
- data/lib/timesteps/timestep.rb +362 -0
- data/lib/timesteps/timestepconverter.rb +63 -0
- data/lib/timesteps/timestepdatetimeext.rb +79 -0
- data/lib/timesteps/timesteppair.rb +202 -0
- data/lib/timesteps.rb +10 -0
- data/spec/allleap_spec.rb +330 -0
- data/spec/fixed360day_spec.rb +341 -0
- data/spec/noleap_spec.rb +330 -0
- data/spec/timestep_spec.rb +405 -0
- data/spec/timesteppair_spec.rb +99 -0
- data/timesteps.gemspec +26 -0
- metadata +64 -0
@@ -0,0 +1,530 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# Base class for DateTime::NoLeap, DateTime::AllLeap, DateTime::Fixed360Day
|
4
|
+
#
|
5
|
+
class DateTimeLike
|
6
|
+
|
7
|
+
module DateTimeLikeExtension
|
8
|
+
|
9
|
+
# Calculate Year/Month/Day fron Julian day number
|
10
|
+
#
|
11
|
+
# @param jday [Integer]
|
12
|
+
#
|
13
|
+
# @return [Array] [year, month, day]
|
14
|
+
def jday2date (jday)
|
15
|
+
dpy = self::DPY
|
16
|
+
dpm = self::DPM
|
17
|
+
jday = jday.round
|
18
|
+
year = jday/dpy
|
19
|
+
doy = jday - year*dpy + 1
|
20
|
+
year = year - 4712
|
21
|
+
month = 0
|
22
|
+
day = 0
|
23
|
+
days = 0
|
24
|
+
(1..12).each do |m|
|
25
|
+
if days + dpm[m] >= doy
|
26
|
+
month = m
|
27
|
+
day = doy - days
|
28
|
+
break
|
29
|
+
end
|
30
|
+
days += dpm[m]
|
31
|
+
end
|
32
|
+
return year, month, day
|
33
|
+
end
|
34
|
+
|
35
|
+
# Creates a DateTimeFixedDPY object
|
36
|
+
# from the given Julian day number and Hour/Minute/Second.
|
37
|
+
#
|
38
|
+
# @param jday [Interger]
|
39
|
+
# @param hour [Interger]
|
40
|
+
# @param minute [Integer]
|
41
|
+
# @param second [Float]
|
42
|
+
# @param offset [Rational]
|
43
|
+
#
|
44
|
+
# @return [DateTimeFixedDPY]
|
45
|
+
def jd (jday, hour = 0, minute = 0, second = 0, offset = 0)
|
46
|
+
year, month, day = jday2date(jday)
|
47
|
+
return self.new(year, month, day, hour, minute, second, offset)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
include Comparable
|
53
|
+
|
54
|
+
# Creates a DateTime object denoting the given calendar date.
|
55
|
+
#
|
56
|
+
# @param year [Integer]
|
57
|
+
# @param month [Integer]
|
58
|
+
# @param day [Integer]
|
59
|
+
# @param hour [Integer]
|
60
|
+
# @param minute [Integer]
|
61
|
+
# @param second [Integer]
|
62
|
+
# @param offset [Integer]
|
63
|
+
# @param start [Integer]
|
64
|
+
#
|
65
|
+
def initialize (year = -4712, month = 1, day = 1, hour = 0, minute = 0, second = 0.0, offset = 0, start = nil)
|
66
|
+
@year = year.to_i
|
67
|
+
@month = month.to_i
|
68
|
+
@day = day.to_i
|
69
|
+
@hour = hour.to_i
|
70
|
+
@minute = minute.to_i
|
71
|
+
@second = second
|
72
|
+
@offset = offset
|
73
|
+
check_valid_datetime()
|
74
|
+
end
|
75
|
+
|
76
|
+
def check_valid_datetime
|
77
|
+
unless valid_date?
|
78
|
+
raise "invalid date"
|
79
|
+
end
|
80
|
+
unless valid_time?
|
81
|
+
raise "invalid time"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def valid_time?
|
86
|
+
begin
|
87
|
+
DateTime.new(2001,1,1,@hour,@minute,@second)
|
88
|
+
true
|
89
|
+
rescue
|
90
|
+
false
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private :check_valid_datetime, :valid_time?
|
95
|
+
|
96
|
+
attr_reader :year,
|
97
|
+
:month,
|
98
|
+
:day,
|
99
|
+
:hour,
|
100
|
+
:minute,
|
101
|
+
:offset
|
102
|
+
|
103
|
+
# Returns the second (0-59)
|
104
|
+
#
|
105
|
+
# @return [Rational]
|
106
|
+
def second
|
107
|
+
return @second.floor
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns the fractional part of the second.
|
111
|
+
#
|
112
|
+
# @return [Rational]
|
113
|
+
def second_fraction
|
114
|
+
return @second.to_r - @second.floor
|
115
|
+
end
|
116
|
+
|
117
|
+
alias mon month
|
118
|
+
alias min minute
|
119
|
+
alias mday day
|
120
|
+
alias sec second
|
121
|
+
alias sec_fraction second_fraction
|
122
|
+
|
123
|
+
# Duplicates self and resets its offset.
|
124
|
+
#
|
125
|
+
# @param offset [Numeric]
|
126
|
+
#
|
127
|
+
# @return [DateTime]
|
128
|
+
def new_offset (offset = 0)
|
129
|
+
gmt = jd + fraction - @offset + offset.to_r
|
130
|
+
jday = gmt.floor
|
131
|
+
fday = gmt - gmt.floor
|
132
|
+
return self.class.jd(jday, 0, 0, 0, offset) + fday
|
133
|
+
end
|
134
|
+
|
135
|
+
# Returns Day of Year
|
136
|
+
#
|
137
|
+
# @return [Integer]
|
138
|
+
def yday
|
139
|
+
dpm = self.class::DPM
|
140
|
+
doy = @day
|
141
|
+
(@month-1).step(1,-1) do |m|
|
142
|
+
doy += dpm[m]
|
143
|
+
end
|
144
|
+
return doy
|
145
|
+
end
|
146
|
+
|
147
|
+
# Calculates Julian day number from date part.
|
148
|
+
# Note that this method does not take into account the offset and time.
|
149
|
+
# If you need a Julian day number that takes the time into account, use #ajd.
|
150
|
+
#
|
151
|
+
# @return [Integer]
|
152
|
+
def jd
|
153
|
+
return (self.class::DPY) * (@year + 4712) + (yday - 1)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Calculates astronomical Julian day number.
|
157
|
+
#
|
158
|
+
# @return [Numeric]
|
159
|
+
def ajd
|
160
|
+
return jd + fraction - @offset - 1.quo(2)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Returns the day of week (0-6, Sunday is zero).
|
164
|
+
#
|
165
|
+
# @return [Integer]
|
166
|
+
def wday
|
167
|
+
return (jd + 1 ) % 7
|
168
|
+
end
|
169
|
+
|
170
|
+
# Returns the day of week (0-6, Sunday is zero).
|
171
|
+
#
|
172
|
+
# @return [Integer]
|
173
|
+
def commercial
|
174
|
+
jd0 = (self.class::DPY) * (@year + 4712)
|
175
|
+
wday0 = (jd0 + 1 ) % 7
|
176
|
+
cwyear = @year
|
177
|
+
yday1 = yday - wday0
|
178
|
+
cweek = yday / 7 + 1
|
179
|
+
cwday = yday % 7 + 1
|
180
|
+
return cwyear, cweek, cwday
|
181
|
+
end
|
182
|
+
|
183
|
+
private :commercial
|
184
|
+
|
185
|
+
# Returns a date object pointing other days after self.
|
186
|
+
#
|
187
|
+
# @param days [Numeric]
|
188
|
+
#
|
189
|
+
# @return [DateTimeFixedDPY]
|
190
|
+
def + (days)
|
191
|
+
days = days.to_r + fraction
|
192
|
+
jday = jd.floor + days.floor
|
193
|
+
fday = (days - days.floor)*24
|
194
|
+
hour = fday.floor
|
195
|
+
fday = (fday - hour)*60
|
196
|
+
min = fday.floor
|
197
|
+
sec = (fday - min)*60
|
198
|
+
return self.class.jd(jday, hour, min, sec, offset)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Returns the difference between the two dates
|
202
|
+
# if the other is a date object.
|
203
|
+
# If the other is a numeric value,
|
204
|
+
# returns a date object pointing other days before self.
|
205
|
+
def - (other_or_days)
|
206
|
+
case other_or_days
|
207
|
+
when Numeric
|
208
|
+
return self + (-other_or_days)
|
209
|
+
when self.class
|
210
|
+
return self.jd - other_or_days.jd
|
211
|
+
else
|
212
|
+
raise "invalid object for other"
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Returns a date object pointing n months after self.
|
217
|
+
#
|
218
|
+
# @param n [Integer]
|
219
|
+
#
|
220
|
+
# @return [DateTimeFixedDPY]
|
221
|
+
def >> (n)
|
222
|
+
return add_months(n)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Returns a date object pointing n months before self.
|
226
|
+
#
|
227
|
+
# @param n [Integer]
|
228
|
+
#
|
229
|
+
# @return [DateTimeFixedDPY]
|
230
|
+
def << (n)
|
231
|
+
return add_months(-n)
|
232
|
+
end
|
233
|
+
|
234
|
+
# Returns a date object pointing n years after self.
|
235
|
+
#
|
236
|
+
# @param n [Integer]
|
237
|
+
#
|
238
|
+
# @return [DateTimeFixedDPY]
|
239
|
+
def next_year (n = 1)
|
240
|
+
return self.class::new(@year+n, @month, @day, @hour, @minute, @second, @offset)
|
241
|
+
end
|
242
|
+
|
243
|
+
# Returns a date object pointing n years before self.
|
244
|
+
#
|
245
|
+
# @param n [Integer]
|
246
|
+
#
|
247
|
+
# @return [DateTimeFixedDPY]
|
248
|
+
def prev_year (n = 1)
|
249
|
+
return self.class::new(@year-n, @month, @day, @hour, @minute, @second, @offset)
|
250
|
+
end
|
251
|
+
|
252
|
+
# Returns a date object pointing n months after self.
|
253
|
+
#
|
254
|
+
# @param n [Integer]
|
255
|
+
#
|
256
|
+
# @return [DateTimeFixedDPY]
|
257
|
+
def next_month (n = 1)
|
258
|
+
return add_months(n)
|
259
|
+
end
|
260
|
+
|
261
|
+
# Returns a date object pointing n months before self.
|
262
|
+
#
|
263
|
+
# @param n [Integer]
|
264
|
+
#
|
265
|
+
# @return [DateTimeFixedDPY]
|
266
|
+
def prev_month (n = 1)
|
267
|
+
return add_months(-n)
|
268
|
+
end
|
269
|
+
|
270
|
+
def add_months (months)
|
271
|
+
months = (@month + months).to_i - 1
|
272
|
+
years = months / 12
|
273
|
+
month = months % 12 + 1
|
274
|
+
return self.class::new(@year+years, month, @day, @hour, @minute, @second, @offset)
|
275
|
+
end
|
276
|
+
|
277
|
+
private :add_months
|
278
|
+
|
279
|
+
# Returns a date object pointing n days after self.
|
280
|
+
#
|
281
|
+
# @param n [Integer]
|
282
|
+
#
|
283
|
+
# @return [DateTimeFixedDPY]
|
284
|
+
def next_day (n = 1)
|
285
|
+
return add_days(n)
|
286
|
+
end
|
287
|
+
|
288
|
+
# Returns a date object pointing n days before self.
|
289
|
+
#
|
290
|
+
# @param n [Integer]
|
291
|
+
#
|
292
|
+
# @return [DateTimeFixedDPY]
|
293
|
+
def prev_day (n = 1)
|
294
|
+
return add_days(-n)
|
295
|
+
end
|
296
|
+
|
297
|
+
def add_days (days)
|
298
|
+
return self.class::jd(jd+days.to_i, @hour, @minute, @second, @offset)
|
299
|
+
end
|
300
|
+
|
301
|
+
private :add_days
|
302
|
+
|
303
|
+
# Returns a datetime object denoting the following day.
|
304
|
+
#
|
305
|
+
# @return [DateTime]
|
306
|
+
def succ
|
307
|
+
return add_days(1)
|
308
|
+
end
|
309
|
+
|
310
|
+
alias next succ
|
311
|
+
|
312
|
+
# Returns time fraction in day units.
|
313
|
+
#
|
314
|
+
# @return [Rational]
|
315
|
+
def fraction ()
|
316
|
+
return (60*(60*@hour + @minute) + @second.to_r).quo(86400)
|
317
|
+
end
|
318
|
+
|
319
|
+
def format_offset
|
320
|
+
oh = @offset*24
|
321
|
+
return [
|
322
|
+
oh >= 0 ? "+" : "-",
|
323
|
+
"%02i" % oh.abs.to_i,
|
324
|
+
":",
|
325
|
+
"%02i" % ((oh.abs - oh.abs.to_i)*60).round
|
326
|
+
].join("")
|
327
|
+
end
|
328
|
+
|
329
|
+
def format_microsec
|
330
|
+
microsec = ((@second - @second.floor)*1000000).round
|
331
|
+
if microsec == 0
|
332
|
+
""
|
333
|
+
else
|
334
|
+
".%06d" % microsec
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
private :format_offset, :format_microsec
|
339
|
+
|
340
|
+
# Returns a string in an ISO 8601 format.
|
341
|
+
#
|
342
|
+
def to_s
|
343
|
+
format("%04d-%02d-%02dT%02d:%02d:%02d%s%s",
|
344
|
+
@year, @month, @day,
|
345
|
+
@hour, @minute, @second.floor,
|
346
|
+
format_microsec,
|
347
|
+
format_offset)
|
348
|
+
end
|
349
|
+
|
350
|
+
# Returns the value as a string for inspection.
|
351
|
+
#
|
352
|
+
def inspect
|
353
|
+
sec = fraction*86400
|
354
|
+
isec = sec.floor
|
355
|
+
fsec = ((sec - isec)*1000000).round
|
356
|
+
offs = (offset*86400).round
|
357
|
+
format("#<%s: %s ((%dj,%ds,%dn),%+ds)>", self.class, to_s, jd, isec, fsec, offs)
|
358
|
+
end
|
359
|
+
|
360
|
+
# Compares the two dates and returns -1, zero, 1 or nil.
|
361
|
+
# The other should be a date object or a numeric value
|
362
|
+
# as an astronomical Julian day number.
|
363
|
+
def <=> (other)
|
364
|
+
return self.ajd <=> other.ajd
|
365
|
+
end
|
366
|
+
|
367
|
+
def compare_md (other)
|
368
|
+
sday = self.day + self.fraction + self.offset
|
369
|
+
oday = other.day + other.fraction + other.offset
|
370
|
+
if self.month > other.month
|
371
|
+
return 1
|
372
|
+
elsif self.month < other.month
|
373
|
+
return -1
|
374
|
+
else
|
375
|
+
if sday > oday
|
376
|
+
return 1
|
377
|
+
elsif sday < oday
|
378
|
+
return -1
|
379
|
+
else
|
380
|
+
return 0
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
def compare_d (other)
|
386
|
+
sday = self.day + self.fraction + self.offset
|
387
|
+
oday = other.day + other.fraction + other.offset
|
388
|
+
if sday > oday
|
389
|
+
return 1
|
390
|
+
elsif sday < oday
|
391
|
+
return -1
|
392
|
+
else
|
393
|
+
return 0
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
# Calculate difference between the object and other object in years.
|
398
|
+
#
|
399
|
+
# @return [Integer]
|
400
|
+
def difference_in_years (other)
|
401
|
+
extra = 0
|
402
|
+
if self.year > other.year && self.compare_md(other) < 0
|
403
|
+
extra = -1
|
404
|
+
end
|
405
|
+
if self.year < other.year && self.compare_md(other) > 0
|
406
|
+
extra = 1
|
407
|
+
end
|
408
|
+
return self.year - other.year + extra
|
409
|
+
end
|
410
|
+
|
411
|
+
# Calculate difference between the object and other object in months.
|
412
|
+
#
|
413
|
+
# @return [Integer]
|
414
|
+
def difference_in_months (other)
|
415
|
+
extra = 0
|
416
|
+
if self.month > other.month && self.compare_d(other) < 0
|
417
|
+
extra = -1
|
418
|
+
end
|
419
|
+
if self.month < other.month && self.compare_d(other) > 0
|
420
|
+
extra = 1
|
421
|
+
end
|
422
|
+
return 12*(self.year - other.year) + self.month - other.month + extra
|
423
|
+
end
|
424
|
+
|
425
|
+
end
|
426
|
+
|
427
|
+
class DateTime
|
428
|
+
|
429
|
+
# datetime class represents `noleap` or `365_day` calendar
|
430
|
+
#
|
431
|
+
class NoLeap < DateTimeLike
|
432
|
+
|
433
|
+
extend DateTimeLikeExtension
|
434
|
+
|
435
|
+
# Number of days per year
|
436
|
+
DPY = 365
|
437
|
+
|
438
|
+
# Numbers of days per months
|
439
|
+
DPM = [0,31,28,31,30,31,30,31,31,30,31,30,31]
|
440
|
+
|
441
|
+
# Astronomical Julian day number of UNIX epoch
|
442
|
+
UNIX_EPOCH_IN_AJD = Rational(4877859,2)
|
443
|
+
|
444
|
+
def valid_date?
|
445
|
+
if @month != 2
|
446
|
+
return Date.valid_date?(@year, @month, @day)
|
447
|
+
else
|
448
|
+
return ( @day >= 1 and @day <= 28 )
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
private :valid_date?
|
453
|
+
|
454
|
+
def strftime (spec)
|
455
|
+
DateTime.new(@year, @month, @day, @hour, @minute, @second, @offset).strftime(spec)
|
456
|
+
end
|
457
|
+
|
458
|
+
def leap?
|
459
|
+
false
|
460
|
+
end
|
461
|
+
|
462
|
+
end
|
463
|
+
|
464
|
+
# datetime class represents `allleap` or `366_day` calendar
|
465
|
+
#
|
466
|
+
class AllLeap < DateTimeLike
|
467
|
+
|
468
|
+
extend DateTimeLikeExtension
|
469
|
+
|
470
|
+
# Number of days per year
|
471
|
+
DPY = 366
|
472
|
+
|
473
|
+
# Numbers of days per months
|
474
|
+
DPM = [0,31,29,31,30,31,30,31,31,30,31,30,31]
|
475
|
+
|
476
|
+
# Astronomical Julian day number of UNIX epoch
|
477
|
+
UNIX_EPOCH_IN_AJD = Rational(4891223,2)
|
478
|
+
|
479
|
+
def valid_date?
|
480
|
+
if @month != 2
|
481
|
+
return Date.valid_date?(@year, @month, @day)
|
482
|
+
else
|
483
|
+
return ( @day >= 1 and @day <= 29 )
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
private :valid_date?
|
488
|
+
|
489
|
+
def leap?
|
490
|
+
true
|
491
|
+
end
|
492
|
+
|
493
|
+
end
|
494
|
+
|
495
|
+
# datetime class represents `360_day` calendar
|
496
|
+
#
|
497
|
+
class Fixed360Day < DateTimeLike
|
498
|
+
|
499
|
+
extend DateTimeLikeExtension
|
500
|
+
|
501
|
+
# Number of days per year
|
502
|
+
DPY = 360
|
503
|
+
|
504
|
+
# Numbers of days per months
|
505
|
+
DPM = [0,30,30,30,30,30,30,30,30,30,30,30,30]
|
506
|
+
|
507
|
+
# Astronomical Julian day number of UNIX epoch
|
508
|
+
UNIX_EPOCH_IN_AJD = Rational(4811039,2)
|
509
|
+
|
510
|
+
def valid_date?
|
511
|
+
if @day >= 31
|
512
|
+
return false
|
513
|
+
end
|
514
|
+
if @month != 2
|
515
|
+
return Date.valid_date?(@year, @month, @day)
|
516
|
+
else
|
517
|
+
return ( @day >= 1 and @day <= 30 )
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
private :valid_date?
|
522
|
+
|
523
|
+
def leap?
|
524
|
+
raise NotImplementedError
|
525
|
+
end
|
526
|
+
|
527
|
+
end
|
528
|
+
|
529
|
+
end
|
530
|
+
|