third_base 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +19 -0
- data/README +261 -0
- data/benchmark/date.rb +18 -0
- data/benchmark/datetime.rb +18 -0
- data/bin/third_base +4 -0
- data/lib/third_base/compat/date/format.rb +3 -0
- data/lib/third_base/compat/date.rb +3 -0
- data/lib/third_base/compat.rb +405 -0
- data/lib/third_base/date.rb +674 -0
- data/lib/third_base/datetime.rb +385 -0
- data/lib/third_base.rb +2 -0
- data/spec/compat/compat_class_methods_spec.rb +208 -0
- data/spec/compat/compat_instance_methods_spec.rb +54 -0
- data/spec/compat/date_spec.rb +56 -0
- data/spec/compat/datetime_spec.rb +77 -0
- data/spec/compat_spec_helper.rb +2 -0
- data/spec/date/accessor_spec.rb +134 -0
- data/spec/date/add_month_spec.rb +28 -0
- data/spec/date/add_spec.rb +24 -0
- data/spec/date/boat_spec.rb +31 -0
- data/spec/date/civil_spec.rb +47 -0
- data/spec/date/commercial_spec.rb +34 -0
- data/spec/date/constants_spec.rb +18 -0
- data/spec/date/downto_spec.rb +17 -0
- data/spec/date/eql_spec.rb +9 -0
- data/spec/date/hash_spec.rb +13 -0
- data/spec/date/julian_spec.rb +13 -0
- data/spec/date/leap_spec.rb +19 -0
- data/spec/date/minus_month_spec.rb +26 -0
- data/spec/date/minus_spec.rb +47 -0
- data/spec/date/ordinal_spec.rb +13 -0
- data/spec/date/parse_spec.rb +227 -0
- data/spec/date/step_spec.rb +55 -0
- data/spec/date/strftime_spec.rb +132 -0
- data/spec/date/strptime_spec.rb +118 -0
- data/spec/date/succ_spec.rb +16 -0
- data/spec/date/today_spec.rb +11 -0
- data/spec/date/upto_spec.rb +17 -0
- data/spec/date_spec_helper.rb +3 -0
- data/spec/datetime/accessor_spec.rb +53 -0
- data/spec/datetime/add_spec.rb +36 -0
- data/spec/datetime/boat_spec.rb +43 -0
- data/spec/datetime/constructor_spec.rb +58 -0
- data/spec/datetime/eql_spec.rb +11 -0
- data/spec/datetime/minus_spec.rb +65 -0
- data/spec/datetime/now_spec.rb +14 -0
- data/spec/datetime/parse_spec.rb +338 -0
- data/spec/datetime/strftime_spec.rb +102 -0
- data/spec/datetime/strptime_spec.rb +84 -0
- data/spec/datetime_spec_helper.rb +3 -0
- data/spec/spec_helper.rb +54 -0
- 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
|