time_crisis 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,696 @@
1
+ # Top level module for holding ThirdBase classes.
2
+ module ThirdBase
3
+ # ThirdBase's date class, a simple class which, unlike the standard
4
+ # Date class, does not include any time information.
5
+ #
6
+ # This class is significantly faster than the standard Date class
7
+ # for two reasons. First, it does not depend on the Rational class
8
+ # (which is slow). Second, it doesn't convert all dates to julian
9
+ # dates unless it is necessary.
10
+ class Date
11
+ include Comparable
12
+
13
+ MONTHNAMES = [nil] + %w(January February March April May June July August September October November December)
14
+ ABBR_MONTHNAMES = [nil] + %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)
15
+ MONTH_NUM_MAP = {}
16
+ MONTHNAMES.each_with_index{|x, i| MONTH_NUM_MAP[x.downcase] = i if x}
17
+ ABBR_MONTHNAMES.each_with_index{|x, i| MONTH_NUM_MAP[x.downcase] = i if x}
18
+
19
+ DAYNAMES = %w(Sunday Monday Tuesday Wednesday Thursday Friday Saturday)
20
+ ABBR_DAYNAMES = %w(Sun Mon Tue Wed Thu Fri Sat)
21
+ DAY_NUM_MAP = {}
22
+ DAYNAMES.each_with_index{|x, i| DAY_NUM_MAP[x.downcase] = i}
23
+ ABBR_DAYNAMES.each_with_index{|x, i| DAY_NUM_MAP[x.downcase] = i}
24
+
25
+ CUMMULATIVE_MONTH_DAYS = {1=>0, 2=>31, 3=>59, 4=>90, 5=>120, 6=>151, 7=>181, 8=>212, 9=>243, 10=>273, 11=>304, 12=>334}
26
+ LEAP_CUMMULATIVE_MONTH_DAYS = {1=>0, 2=>31, 3=>60, 4=>91, 5=>121, 6=>152, 7=>182, 8=>213, 9=>244, 10=>274, 11=>305, 12=>335}
27
+ DAYS_IN_MONTH = {1=>31, 2=>28, 3=>31, 4=>30, 5=>31, 6=>30, 7=>31, 8=>31, 9=>30, 10=>31, 11=>30, 12=>31}
28
+ LEAP_DAYS_IN_MONTH = {1=>31, 2=>29, 3=>31, 4=>30, 5=>31, 6=>30, 7=>31, 8=>31, 9=>30, 10=>31, 11=>30, 12=>31}
29
+
30
+ MONTHNAME_RE_PATTERN = "(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec|january|february|march|april|may|june|july|august|september|october|november|december)"
31
+ FULL_MONTHNAME_RE_PATTERN = "(january|february|march|april|may|june|july|august|september|october|november|december)"
32
+ ABBR_MONTHNAME_RE_PATTERN = "(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)"
33
+ FULL_DAYNAME_RE_PATTERN = "(sunday|monday|tuesday|wednesday|thursday|friday|saturday)"
34
+ ABBR_DAYNAME_RE_PATTERN = "(sun|mon|tue|wed|thu|fri|sat)"
35
+
36
+ PARSER_LIST = []
37
+ DEFAULT_PARSER_LIST = [:iso, :us, :num]
38
+ PARSERS = {}
39
+ DEFAULT_PARSERS = {}
40
+ DEFAULT_PARSERS[:iso] = [[%r{\A(-?\d{4})[-./ ](\d\d)[-./ ](\d\d)\z}o, proc{|m| {:civil=>[m[1].to_i, m[2].to_i, m[3].to_i]}}]]
41
+ DEFAULT_PARSERS[:us] = [[%r{\A(\d\d?)[-./ ](\d\d?)[-./ ](\d\d(?:\d\d)?)\z}o, proc{|m| {:civil=>[two_digit_year(m[3]), m[1].to_i, m[2].to_i]}}],
42
+ [%r{\A(\d\d?)/(\d?\d)\z}o, proc{|m| {:civil=>[Time.now.year, m[1].to_i, m[2].to_i]}}],
43
+ [%r{\A#{MONTHNAME_RE_PATTERN}[-./ ](\d\d?)(?:st|nd|rd|th)?,?(?:[-./ ](-?(?:\d\d(?:\d\d)?)))?\z}io, proc{|m| {:civil=>[m[3] ? two_digit_year(m[3]) : Time.now.year, MONTH_NUM_MAP[m[1].downcase], m[2].to_i]}}],
44
+ [%r{\A(\d\d?)(?:st|nd|rd|th)?[-./ ]#{MONTHNAME_RE_PATTERN}[-./ ](-?\d{4})\z}io, proc{|m| {:civil=>[m[3].to_i, MONTH_NUM_MAP[m[2].downcase], m[1].to_i]}}],
45
+ [%r{\A(-?\d{4})[-./ ]#{MONTHNAME_RE_PATTERN}[-./ ](\d\d?)(?:st|nd|rd|th)?\z}io, proc{|m| {:civil=>[m[1].to_i, MONTH_NUM_MAP[m[2].downcase], m[3].to_i]}}],
46
+ [%r{\A#{MONTHNAME_RE_PATTERN}[-./ ](-?\d{4})\z}io, proc{|m| {:civil=>[m[2].to_i, MONTH_NUM_MAP[m[1].downcase], 1]}}],
47
+ [%r{\A#{ABBR_DAYNAME_RE_PATTERN} #{ABBR_MONTHNAME_RE_PATTERN} (\d\d?) (-?\d{4})\z}io, proc{|m| {:civil=>[m[4].to_i, MONTH_NUM_MAP[m[2].downcase], m[3].to_i]}}]]
48
+ DEFAULT_PARSERS[:eu] = [[%r{\A(\d\d?)[-./ ](\d\d?)[-./ ](\d\d\d\d)\z}o, proc{|m| {:civil=>[m[3].to_i, m[2].to_i, m[1].to_i]}}],
49
+ [%r{\A(\d\d?)[-./ ](\d?\d)[-./ ](\d?\d)\z}o, proc{|m| {:civil=>[two_digit_year(m[1]), m[2].to_i, m[3].to_i]}}]]
50
+ DEFAULT_PARSERS[:num] = [[%r{\A\d{2,8}\z}o, proc do |m|
51
+ m = m[0]
52
+ case m.length
53
+ when 2
54
+ t = Time.now
55
+ {:civil=>[t.year, t.mon, m.to_i]}
56
+ when 3
57
+ {:ordinal=>[Time.now.year, m.to_i]}
58
+ when 4
59
+ {:civil=>[Time.now.year, m[0..1].to_i, m[2..3].to_i]}
60
+ when 5
61
+ {:ordinal=>[two_digit_year(m[0..1]), m[2..4].to_i]}
62
+ when 6
63
+ {:civil=>[two_digit_year(m[0..1]), m[2..3].to_i, m[4..5].to_i]}
64
+ when 7
65
+ {:ordinal=>[m[0..3].to_i, m[4..6].to_i]}
66
+ when 8
67
+ {:civil=>[m[0..3].to_i, m[4..5].to_i, m[6..7].to_i]}
68
+ end
69
+ end
70
+ ]]
71
+
72
+ STRFTIME_RE = /%./o
73
+
74
+ STRPTIME_PROC_A = proc{|h,x| h[:cwday] = DAY_NUM_MAP[x.downcase]}
75
+ STRPTIME_PROC_B = proc{|h,x| h[:month] = MONTH_NUM_MAP[x.downcase]}
76
+ STRPTIME_PROC_C = proc{|h,x| h[:year] ||= x.to_i*100}
77
+ STRPTIME_PROC_d = proc{|h,x| h[:day] = x.to_i}
78
+ STRPTIME_PROC_G = proc{|h,x| h[:cwyear] = x.to_i}
79
+ STRPTIME_PROC_g = proc{|h,x| h[:cwyear] = two_digit_year(x)}
80
+ STRPTIME_PROC_j = proc{|h,x| h[:yday] = x.to_i}
81
+ STRPTIME_PROC_m = proc{|h,x| h[:month] = x.to_i}
82
+ STRPTIME_PROC_u = proc{|h,x| h[:cwday] = x.to_i}
83
+ STRPTIME_PROC_V = proc{|h,x| h[:cweek] = x.to_i}
84
+ STRPTIME_PROC_y = proc{|h,x| h[:year] = two_digit_year(x)}
85
+ STRPTIME_PROC_Y = proc{|h,x| h[:year] = x.to_i}
86
+
87
+ UNIXEPOCH = 2440588
88
+
89
+ # Public Class Methods
90
+
91
+ class << self
92
+ alias new! new
93
+ end
94
+
95
+ # Add a parser to the parser type. Arguments:
96
+ # * type - The parser type to which to add the parser, should be a Symbol.
97
+ # * pattern - Can be either a Regexp or String:
98
+ # * String - A strptime parser regular expression is created using pattern as the format string.
99
+ # If a block is given, it is used. If no block is given, the parser will
100
+ # operate identically to strptime.
101
+ # * Regexp - The regular expression is used directly. In this case, a block must be provided,
102
+ # or an error is raised.
103
+ #
104
+ # The block, if provided, should take a single MatchData argument. It should return
105
+ # nil if it cannot successfully parse the string, an instance of this class, or a hash of
106
+ # values to be passed to new!.
107
+ def self.add_parser(type, pattern, &block)
108
+ if pattern.is_a?(String)
109
+ pattern, blk = strptime_pattern_and_block(pattern)
110
+ block ||= blk
111
+ else
112
+ raise(ArgumentError, 'must provide block for Regexp parser') unless block_given?
113
+ end
114
+ parser_hash[type].unshift([pattern, block])
115
+ end
116
+
117
+ # Add a parser type to the list of parser types.
118
+ # Should be used if you want to add your own parser
119
+ # types.
120
+ def self.add_parser_type(type)
121
+ parser_hash[type] ||= []
122
+ end
123
+
124
+ # Returns a new Date with the given year, month, and day.
125
+ def self.civil(year, mon, day)
126
+ new!(:civil=>[year, mon, day])
127
+ end
128
+
129
+ # Returns a new Date with the given commercial week year,
130
+ # commercial week, and commercial week day.
131
+ def self.commercial(cwyear, cweek, cwday=5)
132
+ new!(:commercial=>[cwyear, cweek, cwday])
133
+ end
134
+
135
+ # Returns a new Date with the given julian date.
136
+ def self.jd(j)
137
+ new!(:jd=>j)
138
+ end
139
+
140
+ # Calls civil with the given arguments.
141
+ def self.new(*args)
142
+ civil(*args)
143
+ end
144
+
145
+ # Returns a new Date with the given year and day of year.
146
+ def self.ordinal(year, yday)
147
+ new!(:ordinal=>[year, yday])
148
+ end
149
+
150
+ # Parses the given string and returns a Date. Raises an ArgumentError if no
151
+ # parser can correctly parse the date. Takes the following options:
152
+ #
153
+ # * :parser_types : an array of parser types to use,
154
+ # overriding the default or the ones specified by
155
+ # use_parsers.
156
+ def self.parse(str, opts={})
157
+ s = str.strip
158
+ parsers(opts[:parser_types]) do |pattern, block|
159
+ if m = pattern.match(s)
160
+ if res = block.call(m)
161
+ return res.is_a?(Hash) ? new!(res) : res
162
+ end
163
+ end
164
+ end
165
+ raise ArgumentError, 'invalid date'
166
+ end
167
+
168
+ # Reset the parsers, parser types, and order of parsers used to the default.
169
+ def self.reset_parsers!
170
+ parser_hash.clear
171
+ default_parser_hash.each do |type, parsers|
172
+ add_parser_type(type)
173
+ parsers.reverse.each do |re, parser|
174
+ add_parser(type, re, &parser)
175
+ end
176
+ end
177
+ use_parsers(*default_parser_list)
178
+ end
179
+
180
+ # Parse the string using the provided format (or the default format).
181
+ # Raises an ArgumentError if the format does not match the string.
182
+ def self.strptime(str, fmt=strptime_default)
183
+ pattern, block = strptime_pattern_and_block(fmt)
184
+ s = str.strip
185
+ if m = pattern.match(s)
186
+ block.call(m)
187
+ else
188
+ raise ArgumentError, 'invalid date'
189
+ end
190
+ end
191
+
192
+ # Returns a date with the current year, month, and date.
193
+ def self.today
194
+ t = Time.now
195
+ civil(t.year, t.mon, t.day)
196
+ end
197
+
198
+ # Set the order of parser types to use to the given parser types.
199
+ def self.use_parsers(*parsers)
200
+ parser_list.replace(parsers)
201
+ end
202
+
203
+ # Private Class Methods
204
+
205
+ def self._expand_strptime_format(v)
206
+ case v
207
+ when '%D', '%x' then '%m/%d/%y'
208
+ when '%F' then '%Y-%m-%d'
209
+ when '%v' then '%e-%b-%Y'
210
+ else v
211
+ end
212
+ end
213
+
214
+ def self._strptime_part(v)
215
+ case v
216
+ when 'A' then [FULL_DAYNAME_RE_PATTERN, STRPTIME_PROC_A]
217
+ when 'a' then [ABBR_DAYNAME_RE_PATTERN, STRPTIME_PROC_A]
218
+ when 'B' then [FULL_MONTHNAME_RE_PATTERN, STRPTIME_PROC_B]
219
+ when 'b', 'h' then [ABBR_MONTHNAME_RE_PATTERN, STRPTIME_PROC_B]
220
+ when 'C' then ['([+-]?\d\d?)', STRPTIME_PROC_C]
221
+ when 'd' then ['([0-3]?\d)', STRPTIME_PROC_d]
222
+ when 'e' then ['([1-3 ]?\d)', STRPTIME_PROC_d]
223
+ when 'G' then ['(\d{4})', STRPTIME_PROC_G]
224
+ when 'g' then ['(\d\d)', STRPTIME_PROC_g]
225
+ when 'j' then ['(\d{1,3})', STRPTIME_PROC_j]
226
+ when 'm' then ['(\d\d?)', STRPTIME_PROC_m]
227
+ when 'n' then ['\n']
228
+ when 't' then ['\t']
229
+ when 'u' then ['(\d)', STRPTIME_PROC_u]
230
+ when 'V' then ['(\d\d?)', STRPTIME_PROC_V]
231
+ when 'y' then ['(\d\d)', STRPTIME_PROC_y]
232
+ when 'Y' then ['([+-]?\d{4})', STRPTIME_PROC_Y]
233
+ when '%' then ['%']
234
+ else ["%#{v}"]
235
+ end
236
+ end
237
+
238
+ def self.default_parser_hash
239
+ DEFAULT_PARSERS
240
+ end
241
+
242
+ def self.default_parser_list
243
+ DEFAULT_PARSER_LIST
244
+ end
245
+
246
+ def self.expand_strptime_format(fmt)
247
+ fmt.gsub(STRFTIME_RE){|x| _expand_strptime_format(x)}
248
+ end
249
+
250
+ def self.new_from_parts(date_hash)
251
+ d = today
252
+ if date_hash[:year] || date_hash[:yday] || date_hash[:month] || date_hash[:day]
253
+ if date_hash[:yday]
254
+ ordinal(date_hash[:year]||d.year, date_hash[:yday])
255
+ else
256
+ civil(date_hash[:year]||d.year, date_hash[:month]||(date_hash[:day] ? d.mon : 1), date_hash[:day]||1)
257
+ end
258
+ elsif date_hash[:cwyear] || date_hash[:cweek] || date_hash[:cwday]
259
+ commercial(date_hash[:cwyear]||d.cwyear, date_hash[:cweek]||(date_hash[:cwday] ? d.cweek : 1), date_hash[:cwday]||1)
260
+ else
261
+ raise ArgumentError, 'invalid date'
262
+ end
263
+ end
264
+
265
+ def self.parser_hash
266
+ PARSERS
267
+ end
268
+
269
+ def self.parser_list
270
+ PARSER_LIST
271
+ end
272
+
273
+ def self.parsers(parser_families=nil)
274
+ (parser_families||parser_list).each do |parser_family|
275
+ parsers_for_family(parser_family) do |pattern, block|
276
+ yield(pattern, block)
277
+ end
278
+ end
279
+ end
280
+
281
+ def self.parsers_for_family(parser_family)
282
+ parser_hash[parser_family].each do |pattern, block|
283
+ yield(pattern, block)
284
+ end
285
+ end
286
+
287
+ def self.strptime_default
288
+ '%Y-%m-%d'
289
+ end
290
+
291
+ def self.strptime_pattern_and_block(fmt)
292
+ blocks = []
293
+ pattern = Regexp.escape(expand_strptime_format(fmt)).gsub(STRFTIME_RE) do |x|
294
+ pat, *blks = _strptime_part(x[1..1])
295
+ blocks += blks
296
+ pat
297
+ end
298
+ block = proc do |m|
299
+ h = {}
300
+ m.to_a[1..-1].zip(blocks) do |x, blk|
301
+ blk.call(h, x)
302
+ end
303
+ new_from_parts(h)
304
+ end
305
+ [/\A#{pattern}\z/i, block]
306
+ end
307
+
308
+ def self.two_digit_year(y)
309
+ y = if y.length == 2
310
+ y = y.to_i
311
+ (y < 69 ? 2000 : 1900) + y
312
+ else
313
+ y.to_i
314
+ end
315
+ end
316
+
317
+ private_class_method :_expand_strptime_format, :_strptime_part, :default_parser_hash, :default_parser_list, :expand_strptime_format, :new_from_parts, :parser_hash, :parser_list, :parsers, :parsers_for_family, :strptime_default, :strptime_pattern_and_block, :two_digit_year
318
+
319
+ reset_parsers!
320
+
321
+ # Public Instance Methods
322
+
323
+ # Called by Date.new!, Takes a hash with one of the following keys:
324
+ #
325
+ # * :civil : should be an array with 3 elements, a year, month, and day
326
+ # * :commercial : should be an array with 3 elements, a commercial week year, commercial week, and commercial week day
327
+ # * :jd : should be an integer specifying the julian date
328
+ # * :ordinal : should be an array with 2 elements, a year and day of year.
329
+ #
330
+ # An ArgumentError is raised if the date is invalid. All Date objects are immutable once created.
331
+ def initialize(opts)
332
+ if opts[:civil]
333
+ @year, @mon, @day = opts[:civil]
334
+ raise(ArgumentError, "invalid date") unless @year.is_a?(Integer) && @mon.is_a?(Integer) && @day.is_a?(Integer) && valid_civil?
335
+ elsif opts[:ordinal]
336
+ @year, @yday = opts[:ordinal]
337
+ raise(ArgumentError, "invalid date") unless @year.is_a?(Integer) && @yday.is_a?(Integer) && valid_ordinal?
338
+ elsif opts[:jd]
339
+ @jd = opts[:jd]
340
+ raise(ArgumentError, "invalid date") unless @jd.is_a?(Integer)
341
+ elsif opts[:commercial]
342
+ @cwyear, @cweek, @cwday = opts[:commercial]
343
+ raise(ArgumentError, "invalid date") unless @cwyear.is_a?(Integer) && @cweek.is_a?(Integer) && @cwday.is_a?(Integer) && valid_commercial?
344
+ else
345
+ raise(ArgumentError, "invalid date format")
346
+ end
347
+ end
348
+
349
+ # Returns a new date with d number of days added to this date.
350
+ def +(d)
351
+ raise(TypeError, "d must be an integer") unless d.is_a?(Integer)
352
+ jd_to_civil(jd + d)
353
+ end
354
+
355
+ # Returns a new date with d number of days subtracted from this date.
356
+ # If d is a Date, returns the number of days between the two dates.
357
+ def -(d)
358
+ if d.is_a?(self.class)
359
+ jd - d.jd
360
+ elsif d.is_a?(Integer)
361
+ self + -d
362
+ else
363
+ raise TypeError, "d should be #{self.class} or Integer"
364
+ end
365
+ end
366
+
367
+ # Returns a new date with m number of months added to this date.
368
+ # If the day of self does not exist in the new month, set the
369
+ # new day to be the last day of the new month.
370
+ def >>(m)
371
+ raise(TypeError, "m must be an integer") unless m.is_a?(Integer)
372
+ y = year
373
+ n = mon + m
374
+ if n > 12 or n <= 0
375
+ a, n = n.divmod(12)
376
+ if n == 0
377
+ n = 12
378
+ y += a - 1
379
+ else
380
+ y += a
381
+ end
382
+ end
383
+ ndays = days_in_month(n, y)
384
+ d = day > ndays ? ndays : day
385
+ new_civil(y, n, d)
386
+ end
387
+
388
+ # Returns a new date with m number of months subtracted from this date.
389
+ def <<(m)
390
+ self >> -m
391
+ end
392
+
393
+ # Compare two dates. If the given date is greater than self, return -1, if it is less,
394
+ # return 1, and if it is equal, return 0. If given date is a number, compare this date's julian
395
+ # date to it.
396
+ def <=>(date)
397
+ if date.is_a?(Numeric)
398
+ jd <=> date
399
+ else
400
+ ((d = (year <=> date.year)) == 0) && ((d = (mon <=> date.mon)) == 0) && ((d = (day <=> date.day)) == 0)
401
+ d
402
+ end
403
+ end
404
+
405
+ # Dates are equel only if their year, month, and day match.
406
+ def ==(date)
407
+ return false unless Date === date
408
+ year == date.year and mon == date.mon and day == date.day
409
+ end
410
+ alias_method :eql?, :==
411
+
412
+ # If d is a date, only true if it is equal to this date. If d is Numeric, only true if it equals this date's julian date.
413
+ def ===(d)
414
+ case d
415
+ when Numeric then jd == d
416
+ when Date then self == d
417
+ else false
418
+ end
419
+ end
420
+
421
+ # The commercial week day for this date.
422
+ def cwday
423
+ @cwday || commercial[2]
424
+ end
425
+
426
+ # The commercial week for this date.
427
+ def cweek
428
+ @cweek || commercial[1]
429
+ end
430
+
431
+ # The commercial week year for this date.
432
+ def cwyear
433
+ @cwyear || commercial[0]
434
+ end
435
+
436
+ # The day of the month for this date.
437
+ def day
438
+ @day || civil[2]
439
+ end
440
+ alias mday day
441
+
442
+ # Yield every date between this date and given date to the block. The
443
+ # given date should be less than this date.
444
+ def downto(d, &block)
445
+ step(d, -1, &block)
446
+ end
447
+
448
+ # Unique value for this date, based on it's year, month, and day of month.
449
+ def hash
450
+ civil.hash
451
+ end
452
+
453
+ # Programmer friendly readable string, much more friendly than the one
454
+ # in the standard date class.
455
+ def inspect
456
+ "#<#{self.class} #{self}>"
457
+ end
458
+
459
+ # This date's julian date.
460
+ def jd
461
+ @jd ||= (
462
+ y = year
463
+ m = mon
464
+ d = day
465
+ if m <= 2
466
+ y -= 1
467
+ m += 12
468
+ end
469
+ a = (y / 100.0).floor
470
+ jd = (365.25 * (y + 4716)).floor +
471
+ (30.6001 * (m + 1)).floor +
472
+ d - 1524 + (2 - a + (a / 4.0).floor)
473
+ )
474
+ end
475
+
476
+ # Whether this date is in a leap year.
477
+ def leap?
478
+ _leap?(year)
479
+ end
480
+
481
+ # The month number for this date (January is 1, December is 12).
482
+ def mon
483
+ @mon || civil[1]
484
+ end
485
+ alias month mon
486
+
487
+ # Yield each date between this date and limit, adding step number
488
+ # of days in each iteration. Returns current date.
489
+ def step(limit, step=1)
490
+ da = self
491
+ op = %w(== <= >=)[step <=> 0]
492
+ while da.__send__(op, limit)
493
+ yield da
494
+ da += step
495
+ end
496
+ self
497
+ end
498
+
499
+ # Format the time using a format string, or the default format string.
500
+ def strftime(fmt=strftime_default)
501
+ fmt.gsub(STRFTIME_RE){|x| _strftime(x[1..1])}
502
+ end
503
+
504
+ # Return the day after this date.
505
+ def succ
506
+ self + 1
507
+ end
508
+ alias next succ
509
+
510
+ # Alias for strftime with the default format
511
+ def to_s
512
+ strftime
513
+ end
514
+
515
+ # Yield every date between this date and the given date to the block. The given date
516
+ # should be greater than this date.
517
+ def upto(d, &block)
518
+ step(d, &block)
519
+ end
520
+
521
+ # Return the day of the week for this date. Sunday is 0, Saturday is 6.
522
+ def wday
523
+ (jd + 1) % 7
524
+ end
525
+
526
+ # Return the day of the year for this date. January 1 is 1.
527
+ def yday
528
+ h = leap? ? LEAP_CUMMULATIVE_MONTH_DAYS : CUMMULATIVE_MONTH_DAYS
529
+ @yday ||= h[mon] + day
530
+ end
531
+
532
+ # Return the year for this date.
533
+ def year
534
+ @year || civil[0]
535
+ end
536
+
537
+ protected
538
+
539
+ def civil
540
+ unless @year && @mon && @day
541
+ if @year && @yday
542
+ @mon, @day = month_day_from_yday
543
+ else
544
+ date = jd_to_civil(@jd || commercial_to_jd(*commercial))
545
+ @year = date.year
546
+ @mon = date.mon
547
+ @day = date.day
548
+ end
549
+ end
550
+ [@year, @mon, @day]
551
+ end
552
+
553
+ def commercial
554
+ unless @cwyear && @cweek && @cwday
555
+ a = jd_to_civil(jd - 3).year
556
+ @cwyear = if jd >= commercial_to_jd(a + 1, 1, 1) then a + 1 else a end
557
+ @cweek = 1 + ((jd - commercial_to_jd(@cwyear, 1, 1)) / 7).floor
558
+ @cwday = (jd + 1) % 7
559
+ @cwday = 7 if @cwday == 0
560
+ end
561
+ [@cwyear, @cweek, @cwday]
562
+ end
563
+
564
+ def ordinal
565
+ [year, yday]
566
+ end
567
+
568
+ private
569
+
570
+ def _leap?(year)
571
+ if year % 400 == 0
572
+ true
573
+ elsif year % 100 == 0
574
+ false
575
+ elsif year % 4 == 0
576
+ true
577
+ else
578
+ false
579
+ end
580
+ end
581
+
582
+ def _strftime(v)
583
+ case v
584
+ when '%' then '%'
585
+ when 'A' then DAYNAMES[wday]
586
+ when 'a' then ABBR_DAYNAMES[wday]
587
+ when 'B' then MONTHNAMES[mon]
588
+ when 'b', 'h' then ABBR_MONTHNAMES[mon]
589
+ when 'C' then '%02d' % (year / 100.0).floor
590
+ when 'D', 'x' then strftime('%m/%d/%y')
591
+ when 'd' then '%02d' % day
592
+ when 'e' then '%2d' % day
593
+ when 'F' then strftime('%Y-%m-%d')
594
+ when 'G' then '%.4d' % cwyear
595
+ when 'g' then '%02d' % (cwyear % 100)
596
+ when 'j' then '%03d' % yday
597
+ when 'm' then '%02d' % mon
598
+ when 'n' then "\n"
599
+ when 't' then "\t"
600
+ when 'U', 'W'
601
+ firstday = self - ((v == 'W' and wday == 0) ? 6 : wday)
602
+ y = firstday.year
603
+ '%02d' % (y != year ? 0 : ((firstday - new_civil(y, 1, 1))/7 + 1))
604
+ when 'u' then '%d' % cwday
605
+ when 'V' then '%02d' % cweek
606
+ when 'v' then strftime('%e-%b-%Y')
607
+ when 'w' then '%d' % wday
608
+ when 'Y' then '%04d' % year
609
+ when 'y' then '%02d' % (year % 100)
610
+ else "%#{v}"
611
+ end
612
+ end
613
+
614
+ def commercial_to_jd(y, w, d)
615
+ jd = new_civil(y, 1, 4).jd
616
+ jd - (jd % 7) + 7 * (w - 1) + (d - 1)
617
+ end
618
+
619
+ def days_in_month(m=nil, y=nil)
620
+ (_leap?(y||year) ? LEAP_DAYS_IN_MONTH : DAYS_IN_MONTH)[m||mon]
621
+ end
622
+
623
+ def jd_to_civil(jd)
624
+ new_civil(*jd_to_ymd(jd))
625
+ end
626
+
627
+ def jd_to_ymd(jd)
628
+ x = ((jd - 1867216.25) / 36524.25).floor
629
+ a = jd + 1 + x - (x / 4.0).floor
630
+ b = a + 1524
631
+ c = ((b - 122.1) / 365.25).floor
632
+ d = (365.25 * c).floor
633
+ e = ((b - d) / 30.6001).floor
634
+ dom = b - d - (30.6001 * e).floor
635
+ if e <= 13
636
+ m = e - 1
637
+ y = c - 4716
638
+ else
639
+ m = e - 13
640
+ y = c - 4715
641
+ end
642
+ [y, m, dom]
643
+ end
644
+
645
+ def julian_jd?(jd)
646
+ jd < 2299161
647
+ end
648
+
649
+ def last_yday
650
+ leap? ? 366 : 365
651
+ end
652
+
653
+ def month_day_from_yday
654
+ yday = @yday
655
+ y = @year
656
+ h = leap? ? LEAP_CUMMULATIVE_MONTH_DAYS : CUMMULATIVE_MONTH_DAYS
657
+ 12.downto(0) do |i|
658
+ if (c = h[i]) < yday
659
+ return [i, yday - c]
660
+ end
661
+ end
662
+ end
663
+
664
+ def new_civil(y, m, d)
665
+ self.class.new(y, m, d)
666
+ end
667
+
668
+ def new_jd(j)
669
+ self.class.jd(jd)
670
+ end
671
+
672
+ def strftime_default
673
+ '%Y-%m-%d'
674
+ end
675
+
676
+ def valid_civil?
677
+ day >= 1 and day <= days_in_month(mon) and mon >= 1 and mon <= 12
678
+ end
679
+
680
+ def valid_commercial?
681
+ if cwday >= 1 and cwday <= 7 and cweek >= 1 and cweek <= 53
682
+ new_jd(jd).commercial == commercial
683
+ else
684
+ false
685
+ end
686
+ end
687
+
688
+ def valid_ordinal?
689
+ yday >= 1 and yday <= (_leap?(year) ? 366 : 365)
690
+ end
691
+
692
+ def yday_from_month_day
693
+ CUMMULATIVE_MONTH_DAYS[mon] + day + ((month > 2 and leap?) ? 1 : 0)
694
+ end
695
+ end
696
+ end