third_base 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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