chronos 0.1.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/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
|