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