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