chronos 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/CHANGELOG.rdoc +27 -0
  2. data/HISTORY.rdoc +4 -0
  3. data/LICENSE.txt +52 -0
  4. data/MANIFEST.txt +51 -0
  5. data/NOTES.rdoc +85 -0
  6. data/README.rdoc +125 -0
  7. data/Rakefile +34 -0
  8. data/TODO.rdoc +63 -0
  9. data/bench/completebench.rb +24 -0
  10. data/ext/cchronos/extconf.rb +5 -0
  11. data/ext/chronos_core/extconf.rb +5 -0
  12. data/lib/chronos.rb +208 -0
  13. data/lib/chronos/calendar.rb +16 -0
  14. data/lib/chronos/calendar/gregorian.rb +94 -0
  15. data/lib/chronos/data/zones.tab +424 -0
  16. data/lib/chronos/datetime.rb +299 -0
  17. data/lib/chronos/datetime/gregorian.rb +698 -0
  18. data/lib/chronos/duration.rb +141 -0
  19. data/lib/chronos/duration/gregorian.rb +261 -0
  20. data/lib/chronos/durationtotext.rb +42 -0
  21. data/lib/chronos/exceptions.rb +16 -0
  22. data/lib/chronos/gregorian.rb +27 -0
  23. data/lib/chronos/interval.rb +132 -0
  24. data/lib/chronos/interval/gregorian.rb +80 -0
  25. data/lib/chronos/locale/parsers/de_CH.rb +50 -0
  26. data/lib/chronos/locale/parsers/en_US.rb +1 -0
  27. data/lib/chronos/locale/parsers/generic.rb +21 -0
  28. data/lib/chronos/locale/strings/de_DE.yaml +76 -0
  29. data/lib/chronos/locale/strings/en_US.yaml +76 -0
  30. data/lib/chronos/minimalistic.rb +37 -0
  31. data/lib/chronos/numeric/gregorian.rb +100 -0
  32. data/lib/chronos/ruby.rb +6 -0
  33. data/lib/chronos/version.rb +21 -0
  34. data/lib/chronos/zone.rb +212 -0
  35. data/rake/initialize.rb +116 -0
  36. data/rake/lib/assesscode.rb +59 -0
  37. data/rake/lib/bonesplitter.rb +245 -0
  38. data/rake/lib/projectclass.rb +69 -0
  39. data/rake/tasks/copyright.rake +24 -0
  40. data/rake/tasks/gem.rake +119 -0
  41. data/rake/tasks/git.rake +40 -0
  42. data/rake/tasks/loc.rake +33 -0
  43. data/rake/tasks/manifest.rake +63 -0
  44. data/rake/tasks/meta.rake +16 -0
  45. data/rake/tasks/notes.rake +36 -0
  46. data/rake/tasks/post_load.rake +18 -0
  47. data/rake/tasks/rdoc.rake +73 -0
  48. data/rake/tasks/rubyforge.rake +67 -0
  49. data/rake/tasks/spec.rake +55 -0
  50. data/spec/bacon_helper.rb +43 -0
  51. data/spec/lib/chronos/datetime/gregorian_spec.rb +314 -0
  52. data/spec/lib/chronos/datetime_spec.rb +219 -0
  53. data/spec/lib/chronos_spec.rb +91 -0
  54. 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