chronos 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +27 -0
- data/HISTORY.rdoc +4 -0
- data/LICENSE.txt +52 -0
- data/MANIFEST.txt +51 -0
- data/NOTES.rdoc +85 -0
- data/README.rdoc +125 -0
- data/Rakefile +34 -0
- data/TODO.rdoc +63 -0
- data/bench/completebench.rb +24 -0
- data/ext/cchronos/extconf.rb +5 -0
- data/ext/chronos_core/extconf.rb +5 -0
- data/lib/chronos.rb +208 -0
- data/lib/chronos/calendar.rb +16 -0
- data/lib/chronos/calendar/gregorian.rb +94 -0
- data/lib/chronos/data/zones.tab +424 -0
- data/lib/chronos/datetime.rb +299 -0
- data/lib/chronos/datetime/gregorian.rb +698 -0
- data/lib/chronos/duration.rb +141 -0
- data/lib/chronos/duration/gregorian.rb +261 -0
- data/lib/chronos/durationtotext.rb +42 -0
- data/lib/chronos/exceptions.rb +16 -0
- data/lib/chronos/gregorian.rb +27 -0
- data/lib/chronos/interval.rb +132 -0
- data/lib/chronos/interval/gregorian.rb +80 -0
- data/lib/chronos/locale/parsers/de_CH.rb +50 -0
- data/lib/chronos/locale/parsers/en_US.rb +1 -0
- data/lib/chronos/locale/parsers/generic.rb +21 -0
- data/lib/chronos/locale/strings/de_DE.yaml +76 -0
- data/lib/chronos/locale/strings/en_US.yaml +76 -0
- data/lib/chronos/minimalistic.rb +37 -0
- data/lib/chronos/numeric/gregorian.rb +100 -0
- data/lib/chronos/ruby.rb +6 -0
- data/lib/chronos/version.rb +21 -0
- data/lib/chronos/zone.rb +212 -0
- data/rake/initialize.rb +116 -0
- data/rake/lib/assesscode.rb +59 -0
- data/rake/lib/bonesplitter.rb +245 -0
- data/rake/lib/projectclass.rb +69 -0
- data/rake/tasks/copyright.rake +24 -0
- data/rake/tasks/gem.rake +119 -0
- data/rake/tasks/git.rake +40 -0
- data/rake/tasks/loc.rake +33 -0
- data/rake/tasks/manifest.rake +63 -0
- data/rake/tasks/meta.rake +16 -0
- data/rake/tasks/notes.rake +36 -0
- data/rake/tasks/post_load.rake +18 -0
- data/rake/tasks/rdoc.rake +73 -0
- data/rake/tasks/rubyforge.rake +67 -0
- data/rake/tasks/spec.rake +55 -0
- data/spec/bacon_helper.rb +43 -0
- data/spec/lib/chronos/datetime/gregorian_spec.rb +314 -0
- data/spec/lib/chronos/datetime_spec.rb +219 -0
- data/spec/lib/chronos_spec.rb +91 -0
- metadata +111 -0
@@ -0,0 +1,299 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007-2008 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
require 'chronos'
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
module Chronos
|
14
|
+
|
15
|
+
# == Summary
|
16
|
+
# Datetime represents dates, times and combinations thereof.
|
17
|
+
#
|
18
|
+
# == Synopsis Example:
|
19
|
+
# require 'chronos/gregorian'
|
20
|
+
# date = Datetime.civil(year, month, day)
|
21
|
+
# datetime = date.at(hour, minute, second)
|
22
|
+
# datetimezonelanguage = datetime.in("Europe/Zurich", "de-de")
|
23
|
+
# dtz = Datetime.civil(y, m, d).at(hour, min, sec).in("Europe/Zurich", "de-de")
|
24
|
+
# datetime = Datetime.ordinal(year, day_of_year).at(0,0).in("UTC+1", "en-us")
|
25
|
+
#
|
26
|
+
# == Description
|
27
|
+
# A Datetime represents a singular point on a time axis which has its origin
|
28
|
+
# on the backdated gregorian date 0000-01-01 (january 1st in the year 0).
|
29
|
+
# A Datetime consists of the days and picoseconds since that origin.
|
30
|
+
# The range of Datetimes ruby implementation is only limited by your memory, the
|
31
|
+
# C implementation can represent any date within +/- 2^63 days around the origin.
|
32
|
+
# That means you can have dates before the assumed beginning of this universe which
|
33
|
+
# should be enough even for scientific purposes.
|
34
|
+
#
|
35
|
+
# == Notes
|
36
|
+
# The methods <, <=, ==, >=, > and between? are implemented via Comparable
|
37
|
+
# Chronos::Datetime is calendar system agnostic and does NOT provide any calendar specific
|
38
|
+
# methods, use the subclasses such as Datetime::Gregorian for those.
|
39
|
+
#
|
40
|
+
class Datetime
|
41
|
+
|
42
|
+
Inspect = "#<%s daynumber=%p picosecondnumber=%p timezone=%p language=%p>".freeze
|
43
|
+
|
44
|
+
include Comparable
|
45
|
+
|
46
|
+
# Delegate all methods to the current calendary
|
47
|
+
def self.method_missing(*args, &block)
|
48
|
+
calendar = Chronos.calendar
|
49
|
+
if calendar && klass = const_get(calendar) then
|
50
|
+
klass.__send__(*args, &block)
|
51
|
+
else
|
52
|
+
super
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Convert a Date, DateTime or Time to Chronos::Datetime object
|
57
|
+
def self.import(obj, timezone=nil, language=nil)
|
58
|
+
case obj
|
59
|
+
when ::Chronos::Datetime
|
60
|
+
if obj.class == self.class then
|
61
|
+
obj
|
62
|
+
else
|
63
|
+
new(obj.day_number, obj.ps_number, timezone||obj.timezone, language||obj.language)
|
64
|
+
end
|
65
|
+
|
66
|
+
# uses Chronos::Datetime::Gregorian::ordinal and Chronos::Datetime::Gregorian::time's code
|
67
|
+
when ::Time
|
68
|
+
time = obj.utc
|
69
|
+
year = time.year
|
70
|
+
day_of_year = time.yday
|
71
|
+
leaps = (year/4.0).ceil-(year/100.0).ceil+(year/400.0).ceil
|
72
|
+
daynumber = year*365+leaps+day_of_year
|
73
|
+
ps_number = ((time.hour*3600+time.min*60+time.sec)*1_000_000+time.usec)*1_000_000
|
74
|
+
new(daynumber, ps_number, timezone || time.strftime("%Z"), language)
|
75
|
+
|
76
|
+
# uses Chronos::Datetime::Gregorian::ordinal and Chronos::Datetime::Gregorian::time's code
|
77
|
+
when ::DateTime
|
78
|
+
year = obj.year
|
79
|
+
day_of_year = obj.yday
|
80
|
+
leaps = (year/4.0).ceil-(year/100.0).ceil+(year/400.0).ceil
|
81
|
+
daynumber = year*365+leaps+day_of_year
|
82
|
+
seconds = (obj.hour*3600+obj.min*60+obj.sec+(obj.sec_fraction*86400).to_f)
|
83
|
+
over, seconds = (seconds-(obj.offset*86400).to_i).divmod(86400)
|
84
|
+
ps_number = seconds*1_000_000_000_000
|
85
|
+
daynumber += over
|
86
|
+
new(daynumber, ps_number, timezone || obj.strftime("%Z"), language)
|
87
|
+
|
88
|
+
# uses Chronos::Datetime::Gregorian::ordinal's code
|
89
|
+
when ::Date # *must* be after ::DateTime as ::DateTime is a child of ::Date and would trigger on this too
|
90
|
+
year = obj.year
|
91
|
+
day_of_year = obj.yday
|
92
|
+
leaps = (year/4.0).ceil-(year/100.0).ceil+(year/400.0).ceil
|
93
|
+
daynumber = year*365+leaps+day_of_year
|
94
|
+
new(daynumber, nil, timezone, language)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# create a datetime with date and time part set to the current system time
|
99
|
+
# and date
|
100
|
+
def self.now(timezone=nil, language=nil)
|
101
|
+
import(Time.now, timezone, language)
|
102
|
+
end
|
103
|
+
|
104
|
+
# create a datetime with only the date part set to the current system date
|
105
|
+
# for timezone/language append a .in(timezone, language) or set a global
|
106
|
+
# (see Chronos::Datetime)
|
107
|
+
def self.today(timezone=nil, language=nil)
|
108
|
+
# uses Chronos::Datetime::Gregorian::ordinal's code
|
109
|
+
time = Time.now.utc
|
110
|
+
year = time.year
|
111
|
+
day_of_year = time.yday
|
112
|
+
leaps = (year/4.0).ceil-(year/100.0).ceil+(year/400.0).ceil
|
113
|
+
daynumber = year*365+leaps+day_of_year
|
114
|
+
new(daynumber, nil, timezone || time.strftime("%Z"), language)
|
115
|
+
end
|
116
|
+
|
117
|
+
# create a datetime with date and time part from a unix-epoch-stamp
|
118
|
+
# for timezone/language append a .in(timezone, language) or set a global
|
119
|
+
# (see Chronos::Datetime)
|
120
|
+
def self.epoch(unix_epoch_time, timezone=nil, language=nil)
|
121
|
+
import(Time.at(unix_epoch_time), timezone, language)
|
122
|
+
end
|
123
|
+
|
124
|
+
# From a hash with components, mainly intended for parsers.
|
125
|
+
# Datetime::components accepts :daynumber, :picosecondnumber, :timezone and :language
|
126
|
+
# Also see each calendar systems class for what parts they accept, e.g.
|
127
|
+
# Chronos::Datetime::Gregorian::components.
|
128
|
+
def self.components(components)
|
129
|
+
daynumber, ps_number, timezone, language = *components.values_at(:daynumber, :picosecondnumber, :timezone, :language)
|
130
|
+
raise ArgumentError, "Neither :daynumber nor :picosecondnumber given" unless (daynumber or ps_number)
|
131
|
+
new(daynumber, ps_number, timezone, language)
|
132
|
+
end
|
133
|
+
|
134
|
+
# the absolute day_number - the internal representation of the date
|
135
|
+
attr_reader :day_number
|
136
|
+
# the absolute second_number - the internal representation of the time
|
137
|
+
# together with fraction
|
138
|
+
attr_reader :ps_number
|
139
|
+
|
140
|
+
# the amount of days the dates representation is shifted (caused by a time-
|
141
|
+
# part with an offset that 'overflows' into the previous or next day)
|
142
|
+
attr_reader :overflow
|
143
|
+
|
144
|
+
# the Zone instance used to retrieve offset
|
145
|
+
attr_reader :timezone
|
146
|
+
# the language used to output names (weekday-names, month-names)
|
147
|
+
attr_reader :language
|
148
|
+
|
149
|
+
# Create a new datetime from daynumber, secondnumber, timezone and language
|
150
|
+
def initialize(day, picosecond, timezone=nil, language=nil)
|
151
|
+
@day_number = day ? day.round : nil
|
152
|
+
@ps_number = picosecond ? picosecond.round : nil
|
153
|
+
@timezone = Chronos.timezone(timezone)
|
154
|
+
@language = Chronos.language(language)
|
155
|
+
@offset = (@timezone && @timezone.offset) || 0
|
156
|
+
if @ps_number then
|
157
|
+
@overflow = (@ps_number.div(1_000_000_000_000)+@offset).div(86400)
|
158
|
+
else
|
159
|
+
@overflow = 0 # overflow is created by time + timezone offset + dst
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def offset
|
164
|
+
@offset_duration ||= begin
|
165
|
+
Duration::Gregorian.new(@offset*PS_IN_SECOND, 0, @language)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# add a/modify the time component to/of a date only datetime
|
170
|
+
def at(hour, minute=0, second=0, fraction=0.0)
|
171
|
+
self.class.new(
|
172
|
+
@day_number,
|
173
|
+
(hour*3600+minute*60+second+fraction)*1_000_000_000_000,
|
174
|
+
@timezone,
|
175
|
+
@language
|
176
|
+
)
|
177
|
+
end
|
178
|
+
|
179
|
+
# converts the datetime object to given timezone/language
|
180
|
+
# keeps the time the same as is, if you want to know the corresponding time
|
181
|
+
# for a given other timezone, see #change_zone
|
182
|
+
# TODO: go over this again, seems wrong
|
183
|
+
def in(timezone=nil, language=nil)
|
184
|
+
timezone = Chronos.timezone(timezone)
|
185
|
+
if timezone then
|
186
|
+
overflow, ps_number = *(@ps_number-timezone.offset*1_000_000_000_000).divmod(86400_000_000_000_000)
|
187
|
+
else
|
188
|
+
overflow = 0
|
189
|
+
ps_number = @ps_number
|
190
|
+
end
|
191
|
+
self.class.new(@day_number+overflow, ps_number, timezone, language)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Change to another timezone, also gives the opportunity to change language
|
195
|
+
def change_zone(timezone=nil, language=nil)
|
196
|
+
timezone ||= @timezone
|
197
|
+
timezone = Zone[timezone] unless timezone.kind_of?(Zone)
|
198
|
+
self.class.new(@day_number, @ps_number, timezone, language)
|
199
|
+
end
|
200
|
+
|
201
|
+
# returns a date-only datetime from this
|
202
|
+
def strip_time
|
203
|
+
raise TypeError, "This Datetime does not contain a date" unless @day_number
|
204
|
+
self.class.new(@day_number+@overflow, nil, @timezone, @language)
|
205
|
+
end
|
206
|
+
|
207
|
+
# returns a time-only datetime from this
|
208
|
+
def strip_date
|
209
|
+
raise TypeError, "This Datetime does not contain a time" unless @ps_number
|
210
|
+
self.class.new(nil, @ps_number, @timezone, @language)
|
211
|
+
end
|
212
|
+
|
213
|
+
# You can add a Duration
|
214
|
+
def +(duration)
|
215
|
+
duration = Chronos::Duration.import(duration)
|
216
|
+
if @ps_number then
|
217
|
+
over, ps = (@ps_number+duration.picoseconds).divmod(PS_IN_DAY)
|
218
|
+
else
|
219
|
+
over = 0
|
220
|
+
ps = nil
|
221
|
+
end
|
222
|
+
day_number = @day_number+duration.days.floor+over if @day_number
|
223
|
+
|
224
|
+
self.class.new(
|
225
|
+
day_number,
|
226
|
+
ps,
|
227
|
+
@timezone,
|
228
|
+
@language
|
229
|
+
)
|
230
|
+
end
|
231
|
+
|
232
|
+
def -(other)
|
233
|
+
if other.respond_to?(:to_duration) then
|
234
|
+
self+(-other)
|
235
|
+
else
|
236
|
+
Interval.new(self, self.class.import(other))
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# compare two datetimes.
|
241
|
+
# not allowed if only one of both doesn't have no date.
|
242
|
+
# if only one of both doesn't have time, 0h 0m 0.0s is used as time.
|
243
|
+
def <=>(other)
|
244
|
+
return nil if @day_number.nil? ^ other.day_number.nil? # either both or none must be nil
|
245
|
+
[@day_number||0,@ps_number||0] <=> [other.day_number||0, other.ps_number||0]
|
246
|
+
end
|
247
|
+
|
248
|
+
# true if this instance has date and time part
|
249
|
+
def datetime?
|
250
|
+
@day_number && @ps_number
|
251
|
+
end
|
252
|
+
|
253
|
+
# true if this instance has a date part
|
254
|
+
def date?
|
255
|
+
!!@day_number # we do not expose the internal structure, not that somebody starts relying on it returning the daynumber
|
256
|
+
end
|
257
|
+
|
258
|
+
# true if this instance has a time part
|
259
|
+
def time?
|
260
|
+
!!@ps_number # we do not expose the internal structure, not that somebody starts relying on it returning the picosecondnumber
|
261
|
+
end
|
262
|
+
|
263
|
+
# convert to ::Time (core Time class)
|
264
|
+
# be aware that due to a lack of possibility to provide the
|
265
|
+
# timezone, all results are returned
|
266
|
+
# - in utc if this Datetime instance has a timezone set
|
267
|
+
# - in the local timezone if this instance has no timezone set
|
268
|
+
# will raise if the Datetime object is time_only?
|
269
|
+
# TODO: make independent of Datetime::Gregorian
|
270
|
+
def export(to_class)
|
271
|
+
if to_class == Time then
|
272
|
+
raise TypeError, "Can't export a Datetime without date part to Time" unless date?
|
273
|
+
ref = ::Chronos::Datetime::Gregorian.new(@day_number, @ps_number)
|
274
|
+
items = [ref.year, ref.month, ref.day_of_month]
|
275
|
+
items.push ref.hour, ref.minute, ref.second, ref.usec*1000000 if @ps_number
|
276
|
+
if @timezone then
|
277
|
+
Time.utc(*items)
|
278
|
+
else
|
279
|
+
Time.local(*items)
|
280
|
+
end
|
281
|
+
elsif to_class == DateTime then
|
282
|
+
elsif to_class == Date
|
283
|
+
else
|
284
|
+
raise ArgumentError, "Can't export to #{to_class}"
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
def inspect
|
289
|
+
sprintf Inspect, self.class, @day_number, @ps_number, @timezone, @language
|
290
|
+
end
|
291
|
+
|
292
|
+
def eql?(other) # :nodoc:
|
293
|
+
@ps_number == other.ps_number &&
|
294
|
+
@day_number == other.day_number &&
|
295
|
+
@timezone == other.timezone &&
|
296
|
+
@language == other.language
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
@@ -0,0 +1,698 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright 2007-2008 by Stefan Rusterholz.
|
3
|
+
# All rights reserved.
|
4
|
+
# See LICENSE.txt for permissions.
|
5
|
+
#++
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
require 'chronos'
|
10
|
+
require 'chronos/calendar/gregorian'
|
11
|
+
require 'chronos/duration/gregorian'
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
module Chronos
|
16
|
+
|
17
|
+
class Datetime
|
18
|
+
|
19
|
+
# == Summary
|
20
|
+
# Can represent dates and times in gregorian notation, provides various methods
|
21
|
+
# for different parts like days, daynames, week, month, monthname, year, iterating etc.
|
22
|
+
#
|
23
|
+
# == Synopsis
|
24
|
+
# require 'chronos/gregorian'
|
25
|
+
# date = Datetime.civil(year, month, day)
|
26
|
+
# datetime = date.at(hour, minute, second)
|
27
|
+
# datetimezonelanguage = datetime.in("Europe/Zurich", "de-de")
|
28
|
+
# dtz = Datetime.civil(y, m, d).at(hour, min, sec).in("Europe/Zurich", "de-de")
|
29
|
+
# datetime = Datetime.ordinal(year, day_of_year).at(0,0).in("UTC+1", "en-us")
|
30
|
+
class Gregorian < ::Chronos::Datetime
|
31
|
+
ISO_8601_Datetime = "%04d-%02d-%02dT%02d:%02d:%02d%s%02d:%02d".freeze
|
32
|
+
ISO_8601_Date = "%04d-%02d-%02d".freeze
|
33
|
+
ISO_8601_Time = "%02d:%02d:%02d%s%02d:%02d".freeze
|
34
|
+
Inspect = "#<%s %s (%p, %p)>".freeze
|
35
|
+
|
36
|
+
DAYS_IN_MONTH1 = [0,31,28,31,30,31,30,31,31,30,31,30,31].freeze
|
37
|
+
DAYS_IN_MONTH2 = [0,31,29,31,30,31,30,31,31,30,31,30,31].freeze
|
38
|
+
DAYS_UNTIL_MONTH1 = [0,31,59,90,120,151,181,212,243,273,304,334,365].freeze
|
39
|
+
DAYS_UNTIL_MONTH2 = [0,31,60,91,121,152,182,213,244,274,305,335,366].freeze
|
40
|
+
|
41
|
+
# symbol => index (reverse map for succ/current/previous)
|
42
|
+
DAY_OF_WEEK = {
|
43
|
+
:monday => 0,
|
44
|
+
:tuesday => 1,
|
45
|
+
:wednesday => 2,
|
46
|
+
:thursday => 3,
|
47
|
+
:friday => 4,
|
48
|
+
:saturday => 5,
|
49
|
+
:sunday => 6,
|
50
|
+
}.freeze
|
51
|
+
|
52
|
+
def self.leap_year?(year)
|
53
|
+
year.leap_year?
|
54
|
+
end
|
55
|
+
|
56
|
+
# returns the number of days in a given month for a given year
|
57
|
+
def self.days_in_month(month, year=nil)
|
58
|
+
(year.leap_year? ? DAYS_IN_MONTH2 : DAYS_IN_MONTH1).at(month)
|
59
|
+
end
|
60
|
+
|
61
|
+
# returns the number of days since origin
|
62
|
+
# TODO: check with negative years
|
63
|
+
# TODO: use integer arithmetic only instead (divmod + test for zero)
|
64
|
+
def self.days_since(year)
|
65
|
+
year*365+(year/4.0).ceil-(year/100.0).ceil+(year/400.0).ceil
|
66
|
+
end
|
67
|
+
|
68
|
+
# create a datetime with date part only from year, month and day_of_month
|
69
|
+
# for timezone/language append a .in(timezone, language) or set a global
|
70
|
+
# (see Chronos::Datetime)
|
71
|
+
def self.civil(year, month, day_of_month, hour=nil, minute=nil, second=nil, timezone=nil, language=nil)
|
72
|
+
ps = nil
|
73
|
+
if hour || minute || second || timezone then
|
74
|
+
timezone = Chronos.timezone(timezone)
|
75
|
+
ps = ps_components(hour, minute, second, nil, nil, timezone.offset)
|
76
|
+
end
|
77
|
+
new(date_components(year, month, nil, nil, day_of_month, nil), ps, timezone, language)
|
78
|
+
end
|
79
|
+
|
80
|
+
# see Datetime#format
|
81
|
+
# for timezone/language append a .in(timezone, language) or set a global
|
82
|
+
# (see Chronos::Datetime)
|
83
|
+
def self.commercial(year, week, day_of_week, hour=nil, minute=nil, second=nil, timezone=nil, language=nil)
|
84
|
+
ps = nil
|
85
|
+
if hour || minute || second || timezone then
|
86
|
+
timezone = Chronos.timezone(timezone)
|
87
|
+
ps = ps_components(hour, minute, second, nil, nil, timezone.offset)
|
88
|
+
end
|
89
|
+
new(date_components(year, nil, week, nil, nil, day_of_week), ps, timezone, language)
|
90
|
+
end
|
91
|
+
|
92
|
+
# create a datetime with date part only from year and day_of_year
|
93
|
+
# for timezone/language append a .in(timezone, language) or set a global
|
94
|
+
# (see Chronos::Datetime)
|
95
|
+
def self.ordinal(year, day_of_year, hour=nil, minute=nil, second=nil, timezone=nil, language=nil)
|
96
|
+
ps = nil
|
97
|
+
if hour || minute || second || timezone then
|
98
|
+
timezone = Chronos.timezone(timezone)
|
99
|
+
ps = ps_components(hour, minute, second, nil, nil, timezone.offset)
|
100
|
+
end
|
101
|
+
new(date_components(year, nil, nil, day_of_year, nil, nil), ps, timezone, language)
|
102
|
+
end
|
103
|
+
|
104
|
+
# create a datetime with time part only from hour, minute, second,
|
105
|
+
# fraction of second (alternatively you can use a float as second)
|
106
|
+
# for timezone/language append a .in(timezone, language) or set a global
|
107
|
+
# (see Chronos::Datetime)
|
108
|
+
def self.at(hour, minute=0, second=0, fraction=0.0, timezone=nil, language=nil)
|
109
|
+
timezone = Chronos.timezone(timezone)
|
110
|
+
new(nil, ps_components(hour, minute, second, fraction, nil, timezone.offset), timezone, language)
|
111
|
+
end
|
112
|
+
|
113
|
+
# parses an ISO 8601 string
|
114
|
+
# this can be either date, time or date and time
|
115
|
+
# date parts must be fully qualified (year+month+day or year+day_of_year or
|
116
|
+
# year+week+day_of_week)
|
117
|
+
# this is in here too to be consistent with Datetime#to_s, for other parsers
|
118
|
+
# see Chronos::Parse
|
119
|
+
def self.iso_8601(string, language=nil)
|
120
|
+
day_number = nil
|
121
|
+
ps_number = nil
|
122
|
+
zone = nil
|
123
|
+
|
124
|
+
# date & time
|
125
|
+
if string.include?('T') then
|
126
|
+
case string
|
127
|
+
# (year ) (month ) (day ) hour minute second fraction timezone
|
128
|
+
when /\A(-?\d\d|-?\d{4})(?:-?(\d\d)(?:-?(\d\d))?)?T(\d\d)(?::?(\d\d)(?::?(\d\d(?:\.\d+)?)?)?)?(Z|[-+]\d\d:\d\d)?\z/
|
129
|
+
year = $1.to_i
|
130
|
+
month = $2.to_i
|
131
|
+
day = $3.to_i
|
132
|
+
zone = Chronos.timezone($7)
|
133
|
+
ps_number = ps_components($4.to_i, $5.to_i, $6.include?('.') ? $6.to_f : $6.to_i, nil, nil, zone.offset)
|
134
|
+
day_number = date_components(year, month, nil, nil, day, nil)
|
135
|
+
when /\A(-?\d\d|-?\d{4})(?:-?W(\d\d)(?:-?(\d))?)?T(\d\d)(?::?(\d\d)(?::?(\d\d(?:\.\d+)?)?)?)?(Z|[-+]\d\d:\d\d)?\z/
|
136
|
+
year = $1.to_i
|
137
|
+
week = $2.to_i
|
138
|
+
day = $3.to_i
|
139
|
+
zone = Chronos.timezone($7)
|
140
|
+
ps_number = ps_components($4.to_i, $5.to_i, $6.include?('.') ? $6.to_f : $6.to_i, nil, nil, zone.offset)
|
141
|
+
day_number = date_components(year, nil, week, nil, nil, day)
|
142
|
+
when /\A(-?\d\d|-?\d{4})(?:-?(\d{3}))?T(\d\d)(?::?(\d\d)(?::?(\d\d(?:\.\d+)?)?)?)?(Z|[-+]\d\d:\d\d)?\z/
|
143
|
+
year = $1.to_i
|
144
|
+
day = $2.to_i
|
145
|
+
zone = Chronos.timezone($6)
|
146
|
+
ps_number = ps_components($3.to_i, $4.to_i, $5.include?('.') ? $5.to_f : $5.to_i, nil, zone.offset)
|
147
|
+
day_number = date_components(year, nil, nil, day, nil, nil)
|
148
|
+
end
|
149
|
+
# date | time
|
150
|
+
else
|
151
|
+
case string
|
152
|
+
when //
|
153
|
+
date_components()
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
new(day_number, ps_number, zone, language)
|
158
|
+
end
|
159
|
+
|
160
|
+
# convert hours, minutes, seconds and fraction to picoseconds required by ::new
|
161
|
+
def self.ps_components(hour, minute, second, fraction=nil, ps=nil, offset=nil)
|
162
|
+
(
|
163
|
+
(hour||0)*3600+
|
164
|
+
(minute||0)*60+
|
165
|
+
(second||0)+
|
166
|
+
(fraction||0)-(offset||0)
|
167
|
+
)*PS_IN_SECOND+
|
168
|
+
(ps||0)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Get a day_number from various date components.
|
172
|
+
# If at least one date component is set, a day_number will be generated.
|
173
|
+
# The default for year is the current year, the default for month, week, dayofyear, dayofmonth and
|
174
|
+
# dayofweek is 1.
|
175
|
+
# day_of_month_mode has 3 possible values:
|
176
|
+
# * :restrict:: This mode will raise if day_of_month is invalid (default)
|
177
|
+
# * :reduce:: This mode will reduce the day_of_month to its maximum in case it it exceeds the maximum
|
178
|
+
# * :overflow:: This mode will increase the month + year until it becomes valid
|
179
|
+
def self.date_components(year, month, week, dayofyear, dayofmonth, dayofweek, day_of_month_mode=:restrict)
|
180
|
+
return nil unless (year || month || week || dayofyear || dayofmonth || dayofweek)
|
181
|
+
day_number = nil
|
182
|
+
|
183
|
+
year ||= Time.now.year
|
184
|
+
|
185
|
+
# year-month-day_of_month
|
186
|
+
if (month || dayofmonth) then
|
187
|
+
month ||= 1
|
188
|
+
dayofmonth ||= 1
|
189
|
+
# calculate how many days passed until this year
|
190
|
+
leap = year.leap_year?
|
191
|
+
raise ArgumentError, "Invalid month (#{year}-#{month}-#{day_of_month})" if month < 1 or month > 12
|
192
|
+
maxdayofmonth = (leap ? DAYS_IN_MONTH2 : DAYS_IN_MONTH1)[month]
|
193
|
+
if dayofmonth > maxdayofmonth then
|
194
|
+
case day_of_month_mode
|
195
|
+
when :restrict
|
196
|
+
raise ArgumentError, "Invalid day of month (#{year}-#{month}-#{dayofmonth})"
|
197
|
+
when :reduce
|
198
|
+
dayofmonth = maxdayofmonth
|
199
|
+
when :overflow
|
200
|
+
raise "Not yet implemented (day_of_month_mode = :overflow)"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
doy = (leap ? DAYS_UNTIL_MONTH2 : DAYS_UNTIL_MONTH1)[month-1]+dayofmonth
|
204
|
+
day_number = days_since(year)+doy
|
205
|
+
|
206
|
+
# year-week-day_of_week
|
207
|
+
elsif (week || dayofweek) then
|
208
|
+
week ||= 1
|
209
|
+
dayofweek ||= 1
|
210
|
+
fdy = days_since(year)+1
|
211
|
+
fwd = (fdy+4)%7
|
212
|
+
off = (10-fwd)%7-3
|
213
|
+
day_number = fdy+off+(week-1)*7+dayofweek
|
214
|
+
|
215
|
+
# year-day_of_year
|
216
|
+
else
|
217
|
+
dayofyear ||= 1
|
218
|
+
day_number = days_since(year)+dayofyear
|
219
|
+
end
|
220
|
+
day_number
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
# You can add a Duration
|
225
|
+
def +(duration)
|
226
|
+
duration = duration.class == Chronos::Duration::Gregorian ? duration : Chronos::Duration::Gregorian.import(duration)
|
227
|
+
year, month = (year()*12+(month()-1)+duration.months).divmod(12)
|
228
|
+
over, ps_number = (@ps_number+duration.picoseconds).divmod(Chronos::PS_IN_DAY)
|
229
|
+
day_number = Chronos::Datetime::Gregorian.date_components(year, month+1, nil, nil, day_of_month(), nil, :reduce)+over
|
230
|
+
Chronos::Datetime::Gregorian.new(day_number, ps_number, @timezone, @language)
|
231
|
+
end
|
232
|
+
|
233
|
+
def -(duration_or_datetime)
|
234
|
+
klass = duration_or_datetime.class
|
235
|
+
if klass == Chronos::Duration::Gregorian then
|
236
|
+
self+(-duration_or_datetime)
|
237
|
+
elsif klass == Chronos::Datetime::Gregorian then
|
238
|
+
raise "not yet implemented"
|
239
|
+
elsif duration_or_datetime.respond_to?(:to_duration) then
|
240
|
+
self+(-Chronos::Duration::Gregorian.import(duration_or_datetime))
|
241
|
+
else
|
242
|
+
raise "not yet implemented"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# add a/modify the time component to/of a date only datetime
|
247
|
+
def at(hour, minute=0, second=0, fraction=0.0)
|
248
|
+
overflow, second = *(hour*3600+minute*60+second+fraction-@timezone.offset).divmod(86400)
|
249
|
+
self.class.new(
|
250
|
+
@day_number+overflow,
|
251
|
+
second*1_000_000_000_000,
|
252
|
+
@timezone,
|
253
|
+
@language
|
254
|
+
)
|
255
|
+
end
|
256
|
+
|
257
|
+
# change to another timezone, also gives the opportunity to change language
|
258
|
+
def change_zone(timezone=nil, language=nil)
|
259
|
+
timezone ||= @timezone
|
260
|
+
timezone = Zone[timezone] unless timezone.kind_of?(Zone)
|
261
|
+
Datetime.new(@day_number, @ps_number, timezone, language)
|
262
|
+
end
|
263
|
+
|
264
|
+
# this method calculates @day_of_year and @year from @day_number - only used internally
|
265
|
+
def year_and_day_of_year # :nodoc:
|
266
|
+
raise NoDatePart unless @day_number
|
267
|
+
y4c, days = *(@day_number+@overflow-1).divmod(146097)
|
268
|
+
|
269
|
+
if days == 0 then
|
270
|
+
y1c, days = 0, 0
|
271
|
+
else
|
272
|
+
y1c, days = *(days-1).divmod(36524)
|
273
|
+
days += 1
|
274
|
+
end
|
275
|
+
|
276
|
+
y4, days = *days.divmod(1461) # if y4 == 0: leapyear, else: not
|
277
|
+
days -= 1 if (y1c != 0 && y4 == 0)
|
278
|
+
|
279
|
+
if days == 0 then
|
280
|
+
y1, days = 0, 0
|
281
|
+
elsif (y1c != 0 && y4 == 0) then # no leapyear at start
|
282
|
+
y1, days = *days.divmod(365)
|
283
|
+
else
|
284
|
+
y1, days = *(days-1).divmod(365)
|
285
|
+
days += 1 if y1 == 0
|
286
|
+
end
|
287
|
+
@year = y4c*400+y1c*100+y4*4+y1
|
288
|
+
@day_of_year = days+1
|
289
|
+
[@year, @day_of_year]
|
290
|
+
end
|
291
|
+
|
292
|
+
# this method calculates @day_of_month and @month from @day_number - only used internally
|
293
|
+
def month_and_day_of_month # :nodoc:
|
294
|
+
raise NoDatePart unless @day_number
|
295
|
+
lookup = year.leap_year? ? DAYS_UNTIL_MONTH2 : DAYS_UNTIL_MONTH1
|
296
|
+
doy = day_of_year()
|
297
|
+
month = (day_of_year/31.0).ceil
|
298
|
+
@month = lookup[month] < doy ? month + 1 : month
|
299
|
+
@day_of_month = doy - lookup[@month-1]
|
300
|
+
[@month, @day_of_month]
|
301
|
+
end
|
302
|
+
|
303
|
+
# returns whether or not the year of this date is a leap-year
|
304
|
+
def leap_year?
|
305
|
+
year.leap_year?
|
306
|
+
end
|
307
|
+
|
308
|
+
# the gregorian year of this date (only limited by memory)
|
309
|
+
def year
|
310
|
+
@year ||= year_and_day_of_year[0]
|
311
|
+
end
|
312
|
+
|
313
|
+
# the gregorian commercial year - always starts with a monday, always
|
314
|
+
# ends with a sunday, has either exactly 52 or 53 weeks.
|
315
|
+
def commercial_year
|
316
|
+
if week == 1 && day_of_year > 14
|
317
|
+
year+1
|
318
|
+
elsif week > 51 && day_of_year < 14 then
|
319
|
+
year-1
|
320
|
+
else
|
321
|
+
year
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
# the gregorian day of year (1-366)
|
326
|
+
def day_of_year
|
327
|
+
@day_of_year ||= year_and_day_of_year[1]
|
328
|
+
end
|
329
|
+
|
330
|
+
# the day of week of this date. 0: monday, 6: sunday
|
331
|
+
# 7 days, 2000-01-01 beeing a 5 (saturday)
|
332
|
+
# the additional parameter can be used to shift the monday to that number
|
333
|
+
def day_of_week(monday=0)
|
334
|
+
begin
|
335
|
+
(@day_number+@overflow+4+monday)%7
|
336
|
+
rescue
|
337
|
+
raise NoDatePart unless @day_number
|
338
|
+
raise
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
# the dayname in the given language or the Datetime-instances default language
|
343
|
+
# Datetime.civil(2000,1,1).day_name # => "Saturday"
|
344
|
+
def day_name(language=nil)
|
345
|
+
language ||= @language
|
346
|
+
begin
|
347
|
+
Chronos.string(language ? Chronos.language(language) : @language, :dayname, (@day_number+@overflow+4)%7)
|
348
|
+
rescue
|
349
|
+
raise NoDatePart unless @day_number
|
350
|
+
raise
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
# the monthname in the given language or the Datetime-instances default language
|
355
|
+
# Datetime.civil(2000,1,1).month_name # => "January"
|
356
|
+
def month_name(language=nil)
|
357
|
+
Chronos.string(language ? Chronos.language(language) : @language, :monthname, month-1)
|
358
|
+
end
|
359
|
+
|
360
|
+
# ISO 8601 week
|
361
|
+
def week
|
362
|
+
@week ||= begin
|
363
|
+
doy = day_of_year # day of year
|
364
|
+
fdy = @day_number+@overflow-doy+1 # first day of year
|
365
|
+
fwd = (fdy+4)%7 # calculate weekday of first day in year
|
366
|
+
if doy <= 3 && doy <= 7-fwd then # last week of last year
|
367
|
+
case fwd
|
368
|
+
when 6: 52
|
369
|
+
when 5: (year-1).leap_year? ? 53 : 52
|
370
|
+
when 4: 53
|
371
|
+
else 1
|
372
|
+
end
|
373
|
+
else # calculate week number
|
374
|
+
off = (10-fwd)%7-2 # calculate offset of the first week
|
375
|
+
week = (doy-off).div(7)+1
|
376
|
+
if week > 52 then
|
377
|
+
week = (fwd == 3 || (leap_year? && fwd == 2)) ? 53 : 1
|
378
|
+
end
|
379
|
+
week
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
def weeks
|
385
|
+
fwd = (@day_number+@overflow-day_of_year+5)%7 # calculate weekday of first day in year
|
386
|
+
(fwd == 3 || (leap_year? && fwd == 2)) ? 53 : 52
|
387
|
+
end
|
388
|
+
|
389
|
+
# this dates day of month (if it has a date part)
|
390
|
+
def day_of_month
|
391
|
+
@day_of_month ||= month_and_day_of_month[1]
|
392
|
+
end
|
393
|
+
|
394
|
+
alias day day_of_month
|
395
|
+
|
396
|
+
# this dates month (if it has a date part)
|
397
|
+
def month
|
398
|
+
@month ||= month_and_day_of_month[0]
|
399
|
+
end
|
400
|
+
|
401
|
+
# the hour of the day (0..23, if it has a time part)
|
402
|
+
def hour
|
403
|
+
begin
|
404
|
+
@hour ||= (@ps_number.div(1_000_000_000_000)+@offset).div(3600)
|
405
|
+
rescue => e
|
406
|
+
raise NoTimePart unless @ps_number
|
407
|
+
raise
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
# the minute of the hour (0..59, if it has a time part)
|
412
|
+
def minute
|
413
|
+
begin
|
414
|
+
@minute ||= (@ps_number.div(1_000_000_000_000)+@offset).div(60)%60
|
415
|
+
rescue => e
|
416
|
+
raise NoTimePart unless @ps_number
|
417
|
+
raise
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
# the minute of the minute (0..59, if it has a time part)
|
422
|
+
def second
|
423
|
+
begin
|
424
|
+
@second ||= (@ps_number.div(1_000_000_000_000)+@offset)%60
|
425
|
+
rescue => e
|
426
|
+
raise NoTimePart unless @ps_number
|
427
|
+
raise
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
# the absolute fraction of a second
|
432
|
+
# returned as a rational if Rational was required, a Float otherwise
|
433
|
+
def fraction
|
434
|
+
begin
|
435
|
+
@ps_number.modulo(PS_IN_SECOND).quo(PS_IN_SECOND)
|
436
|
+
rescue => e
|
437
|
+
raise NoTimePart unless @ps_number
|
438
|
+
raise
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
# the microseconds (0..999999, if it has a time part)
|
443
|
+
def usec
|
444
|
+
begin
|
445
|
+
@ps_number.div(PS_IN_MICROSECOND).modulo(PS_IN_MICROSECOND)
|
446
|
+
rescue => e
|
447
|
+
raise NoTimePart unless @ps_number
|
448
|
+
raise
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
# will raise if you try to do e.g.: Datetime.civil(2000,3,31).next(:month)
|
453
|
+
# since april only has 30 days, so 2000,4,31 is invalid
|
454
|
+
# same with Datetime.civil(2004,2,29).next(:year)
|
455
|
+
# as in 2004, february has a leap-day, but not so in 2005
|
456
|
+
def succeeding(unit, step=1, upper_limit=nil)
|
457
|
+
if block_given?
|
458
|
+
date = self
|
459
|
+
if step > 0 then
|
460
|
+
while((date = date.succeeding(unit,step)) < upper_limit)
|
461
|
+
yield(date)
|
462
|
+
end
|
463
|
+
elsif step < 0 then
|
464
|
+
while((date = date.succeeding(unit,step)) < upper_limit)
|
465
|
+
yield(date)
|
466
|
+
end
|
467
|
+
else
|
468
|
+
raise ArgumentError, "Step may not be 0"
|
469
|
+
end
|
470
|
+
else
|
471
|
+
case unit
|
472
|
+
when :second
|
473
|
+
overflow, ps_number = *(@ps_number+step*PS_IN_SECOND).divmod(PS_IN_DAY)
|
474
|
+
day_number = @day_number ? @day_number + overflow : nil
|
475
|
+
Datetime.new(day_number, ps_number, @timezone, @language)
|
476
|
+
when :minute
|
477
|
+
overflow, ps_number = *(@ps_number+(step*PS_IN_MINUTE)).divmod(PS_IN_DAY)
|
478
|
+
day_number = @day_number ? @day_number + overflow : nil
|
479
|
+
Datetime.new(day_number, ps_number, @timezone, @language)
|
480
|
+
when :hour
|
481
|
+
overflow, ps_number = *(@ps_number+(step*PS_IN_HOUR)).divmod(PS_IN_DAY)
|
482
|
+
day_number = @day_number ? @day_number + overflow : nil
|
483
|
+
Datetime.new(day_number, ps_number, @timezone, @language)
|
484
|
+
when :day
|
485
|
+
day_number = @day_number + step.floor
|
486
|
+
Datetime.new(day_number, @ps_number, @timezone, @language)
|
487
|
+
when :monday,:tuesday,:wednesday,:thursday,:friday,:saturday,:sunday
|
488
|
+
begin
|
489
|
+
Datetime.new(@day_number+(DAY_OF_WEEK[unit]-@day_number-5)%7+1+7*(step >= 1 ? step-1 : step).floor, @ps_number, @timezone, @language)
|
490
|
+
rescue
|
491
|
+
raise NoDatePart unless @day_number
|
492
|
+
raise
|
493
|
+
end
|
494
|
+
when :week
|
495
|
+
day_number = @day_number + step.floor*7
|
496
|
+
Datetime.new(day_number, @ps_number, @timezone, @language)
|
497
|
+
when :month
|
498
|
+
overflow, month = *(month()-1+step.floor).divmod(12)
|
499
|
+
year = (year()+overflow).to_f
|
500
|
+
leap = year.leap_year?
|
501
|
+
raise ArgumentError, "Invalid day of month (#{year}-#{month}-#{day_of_month})" if day_of_month > (leap ? DAYS_IN_MONTH2 : DAYS_IN_MONTH1)[month]
|
502
|
+
leaps = (year/4.0).ceil-(year/100.0).ceil+(year/400.0).ceil
|
503
|
+
doy = (leap ? DAYS_UNTIL_MONTH2 : DAYS_UNTIL_MONTH1)[month]+day_of_month
|
504
|
+
Datetime.new(year*365+leaps+doy, @ps_number, @timezone, @language)
|
505
|
+
when :year
|
506
|
+
month = month()
|
507
|
+
year = (year()+step.floor).to_f
|
508
|
+
leap = year.leap_year?
|
509
|
+
raise ArgumentError, "Invalid day of month (#{year}-#{month}-#{day_of_month})" if day_of_month > (leap ? DAYS_IN_MONTH2 : DAYS_IN_MONTH1)[month]
|
510
|
+
leaps = (year/4.0).ceil-(year/100.0).ceil+(year/400.0).ceil
|
511
|
+
doy = (leap ? DAYS_UNTIL_MONTH2 : DAYS_UNTIL_MONTH1)[month-1]+day_of_month
|
512
|
+
Datetime.new(year*365+leaps+doy, @ps_number, @timezone, @language)
|
513
|
+
end
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
# similar to Datetime#succ
|
518
|
+
# returns a new date with the given unit altered as wished
|
519
|
+
# Datetime#current tries to not modify any of the other parameters, i.e. no
|
520
|
+
# overflows are passed down
|
521
|
+
#
|
522
|
+
def current(unit, at=0)
|
523
|
+
case unit
|
524
|
+
when :second
|
525
|
+
ps_number = (@ps_number-(at*PS_IN_SECOND).to_i)
|
526
|
+
Datetime.new(@day_number, ps_number, fraction, @timezone, @language)
|
527
|
+
when :minute
|
528
|
+
ps_number = (@ps_number-(minute*PS_IN_MINUTE)+(at*PS_IN_MINUTE).floor)
|
529
|
+
Datetime.new(@day_number, ps_number, @timezone, @language)
|
530
|
+
when :hour
|
531
|
+
ps_number = (@ps_number-(hour*PS_IN_HOUR)+(at*PS_IN_HOUR).floor)
|
532
|
+
Datetime.new(@day_number, ps_number, @timezone, @language)
|
533
|
+
when :day
|
534
|
+
raise ArgumentError, "Does not make sense"
|
535
|
+
when :monday,:tuesday,:wednesday,:thursday,:friday,:saturday,:sunday
|
536
|
+
begin
|
537
|
+
Datetime.new(@day_number-(@day_number+4)%7+DAY_OF_WEEK[unit], @ps_number, @timezone, @language)
|
538
|
+
rescue
|
539
|
+
raise NoDatePart unless @day_number
|
540
|
+
raise
|
541
|
+
end
|
542
|
+
when :week
|
543
|
+
year = year().to_f
|
544
|
+
leaps = (year/4.0).ceil-(year/100.0).ceil+(year/400.0).ceil
|
545
|
+
fdy = year*365+leaps+1 # first day of year
|
546
|
+
fwd = (fdy+4)%7 # first day of years weekday
|
547
|
+
off = (10-fwd)%7-3 # calculate offset of the first week
|
548
|
+
Datetime.new(fdy+off+at*7+day_of_week(), @ps_number, @timezone, @language)
|
549
|
+
when :month
|
550
|
+
month = at.floor
|
551
|
+
year = year().to_f
|
552
|
+
leap = year.leap_year?
|
553
|
+
raise ArgumentError, "Invalid day of month (#{year}-#{month}-#{day_of_month})" if day_of_month > (leap ? DAYS_IN_MONTH2 : DAYS_IN_MONTH1)[month]
|
554
|
+
leaps = (year/4.0).ceil-(year/100.0).ceil+(year/400.0).ceil
|
555
|
+
doy = (leap ? DAYS_UNTIL_MONTH2 : DAYS_UNTIL_MONTH1)[month]+day_of_month
|
556
|
+
Datetime.new(year*365+leaps+doy, @ps_number, @timezone, @language)
|
557
|
+
when :year
|
558
|
+
month = month()
|
559
|
+
year = at.floor.to_f
|
560
|
+
leap = year.leap_year?
|
561
|
+
raise ArgumentError, "Invalid day of month (#{year}-#{month}-#{day_of_month})" if day_of_month > (leap ? DAYS_IN_MONTH2 : DAYS_IN_MONTH1)[month]
|
562
|
+
leaps = (year/4.0).ceil-(year/100.0).ceil+(year/400.0).ceil
|
563
|
+
doy = (leap ? DAYS_UNTIL_MONTH2 : DAYS_UNTIL_MONTH1)[month-1]+day_of_month
|
564
|
+
Datetime.new(year*365+leaps+doy, @ps_number, @timezone, @language)
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
# see Datetime#next
|
569
|
+
def previous(unit, step=1, lower_limit=nil, &block)
|
570
|
+
succeeding(unit, -step, lower_limit, &block)
|
571
|
+
end
|
572
|
+
|
573
|
+
# format(string, language, timezone)
|
574
|
+
# format datetime, similar to strftime. Format strings can contain:
|
575
|
+
# %a: monthname, can be formatted like %s in sprintf
|
576
|
+
# %b: dayname, can be formatted like %s in sprintf
|
577
|
+
# %y: year, 4 digits (0000..9999)
|
578
|
+
# %-2y: last 2 digits
|
579
|
+
# %2y: first 2 digits
|
580
|
+
# %m: month of year, 1..31, can be formatted as %d in sprintf
|
581
|
+
# %d: day of month, 1..31, can be formatted as %d in sprintf
|
582
|
+
# %j: day of year, 1..366, can be formatted as %d in sprintf
|
583
|
+
# %k: day of week, 0=monday, 6=sunday
|
584
|
+
# %+k: 1..7 (changes range from 0..6 to 1..7)
|
585
|
+
# %2k: 0=saturday, 2=monday, 6=friday
|
586
|
+
# %+2k: 1=saturday, 3=monday, 7=friday
|
587
|
+
# %w: week of year (iso 8601) 1..53, can be formatted as %d in sprintf
|
588
|
+
#
|
589
|
+
# %H: hour of day, 0..23, can be formatted as %d in sprintf
|
590
|
+
# %I: hour of day, 1..12, can be formatted as %d in sprintf
|
591
|
+
# %M: minute of hour 0..59, can be formatted as %d in sprintf
|
592
|
+
# %S: second of minute, 0..59, can be formatted as %d in sprintf
|
593
|
+
# %O: offset in format ±HHMM
|
594
|
+
# %Z: timezone
|
595
|
+
# %P: meridian indicator (AM/PM)
|
596
|
+
#
|
597
|
+
# %%: Literal % character
|
598
|
+
def format(string=nil, language=nil)
|
599
|
+
unless string
|
600
|
+
string = if @day_number.nil? then
|
601
|
+
ISO_8601_Time
|
602
|
+
elsif @ps_number.nil? then
|
603
|
+
ISO_8601_Date
|
604
|
+
else
|
605
|
+
ISO_8601_Datetime
|
606
|
+
end
|
607
|
+
end
|
608
|
+
|
609
|
+
string.gsub(/%(%|\{[^}]\}|.*?[A-Za-z])/) { |m|
|
610
|
+
case m[-1,1]
|
611
|
+
when '{'
|
612
|
+
call,*args = *m[2..-2].split(",")
|
613
|
+
call = c.to_sym
|
614
|
+
args.map! { |arg|
|
615
|
+
if arg[0,1] == ":" then
|
616
|
+
arg[1..-1].to_sym
|
617
|
+
elsif arg =~ /\A\d+\z/ then
|
618
|
+
Integer(arg)
|
619
|
+
else
|
620
|
+
Float(arg)
|
621
|
+
end
|
622
|
+
}
|
623
|
+
|
624
|
+
respond_to?(call)
|
625
|
+
send(call, *args)
|
626
|
+
when 'a'
|
627
|
+
"#{m[0..-2]}s"%month_name
|
628
|
+
when 'b'
|
629
|
+
"#{m[0..-2]}s"%day_name
|
630
|
+
when 'y'
|
631
|
+
s = "%04d"%year
|
632
|
+
if m.length > 2 then
|
633
|
+
o = m[1..-2].to_i
|
634
|
+
o > 0 ? s[0,o] : s[o..-1]
|
635
|
+
else
|
636
|
+
s
|
637
|
+
end
|
638
|
+
when 'm'
|
639
|
+
"#{m[0..-2]}d"%month
|
640
|
+
when 'd'
|
641
|
+
"#{m[0..-2]}d"%day_of_month
|
642
|
+
when 'j'
|
643
|
+
"#{m[0..-2]}d"%day_of_year
|
644
|
+
when 'k'
|
645
|
+
dow = day_of_week
|
646
|
+
dow = (dow+m[-2,1].to_i)%7 if (m[-2,1] =~ /\d/)
|
647
|
+
dow += 1 if (m[1,1] == "+")
|
648
|
+
dow
|
649
|
+
when 'w'
|
650
|
+
"#{m[0..-2]}d"%week
|
651
|
+
|
652
|
+
when 'H'
|
653
|
+
"#{m[0..-2]}d"%hour
|
654
|
+
when 'I'
|
655
|
+
"#{m[0..-2]}d"%(hour%12+1)
|
656
|
+
when 'M'
|
657
|
+
"#{m[0..-2]}d"%minute
|
658
|
+
when 'S'
|
659
|
+
"#{m[0..-2]}d"%second
|
660
|
+
when 'O'
|
661
|
+
"%s%02d%02d"%[@offset < 0 ? "-" : "+",@offset.div(3600),@offset.div(60)%60]
|
662
|
+
when 'Z'
|
663
|
+
"FIXME"
|
664
|
+
when 'P'
|
665
|
+
hour <= 12 ? "AM" : "PM"
|
666
|
+
when '%'
|
667
|
+
"%"
|
668
|
+
end
|
669
|
+
}
|
670
|
+
end
|
671
|
+
|
672
|
+
# prints the datetime as ISO-8601, examples:
|
673
|
+
# datetime: 2007-01-31T14:31:25-04:00
|
674
|
+
# date: 2007-01-31
|
675
|
+
# time: 14:31:25-04:00
|
676
|
+
def to_s
|
677
|
+
if @day_number then
|
678
|
+
if @ps_number then
|
679
|
+
sprintf ISO_8601_Datetime, year, month, day, hour, minute, second, @offset < 0 ? '-' : '+', *(@offset/60).floor.divmod(60)
|
680
|
+
else
|
681
|
+
sprintf ISO_8601_Date, year, month, day
|
682
|
+
end
|
683
|
+
else
|
684
|
+
sprintf ISO_8601_Time, hour, minute, second, @offset < 0 ? '-' : '+', *(@offset/60).floor.divmod(60)
|
685
|
+
end
|
686
|
+
end
|
687
|
+
|
688
|
+
def inspect
|
689
|
+
sprintf Inspect,
|
690
|
+
self.class,
|
691
|
+
self,
|
692
|
+
@day_number,
|
693
|
+
@ps_number
|
694
|
+
# / sprintf
|
695
|
+
end
|
696
|
+
end # Datetime::Gregorian
|
697
|
+
end # Datetime
|
698
|
+
end # Chronos
|