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.
- 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
|