weekling 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,339 @@
1
+ # encoding: UTF-8
2
+ =begin
3
+ Copyright Alexander E. Fischer <aef@raxys.net>, 2012
4
+
5
+ This file is part of Weekling.
6
+
7
+ Permission to use, copy, modify, and/or distribute this software for any
8
+ purpose with or without fee is hereby granted, provided that the above
9
+ copyright notice and this permission notice appear in all copies.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
12
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
13
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
14
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
15
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
16
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
17
+ PERFORMANCE OF THIS SOFTWARE.
18
+ =end
19
+
20
+ require 'aef/weekling'
21
+
22
+ module Aef
23
+ module Weekling
24
+
25
+ # Immutable object representing a calendar week day (according to ISO 8601).
26
+ class WeekDay
27
+ include Comparable
28
+
29
+ # Table to translate symbolic lowercase english day names to day numbers.
30
+ # @private
31
+ SYMBOL_TO_INDEX_TABLE = {
32
+ :monday => 1,
33
+ :tuesday => 2,
34
+ :wednesday => 3,
35
+ :thursday => 4,
36
+ :friday => 5,
37
+ :saturday => 6,
38
+ :sunday => 7
39
+ }.freeze
40
+
41
+ # Regular expression for week-day extraction from strings.
42
+ # @private
43
+ PARSE_PATTERN = /(0|-?\d+)-W(0[1-9]|(?:1|2|3|4)\d|5(?:0|1|2|3))-([1-7])/
44
+
45
+ class << self
46
+ # Initializes the current week day.
47
+ #
48
+ # @return [Aef::Weekling::WeekDay] the current week day
49
+ def today
50
+ today = Date.today
51
+
52
+ new(today, ((today.wday - 1) % 7) + 1)
53
+ end
54
+
55
+ alias now today
56
+
57
+ # Parses the first week day out of a string.
58
+ #
59
+ # @note Looks for patterns like this:
60
+ # 2011-W03-5
61
+ # @param [String] string a string containing a week-day representation
62
+ # @return [Aef::Weekling::WeekDay] the week day parsed from input
63
+ # @raise [ArgumentError] if pattern cannot be found
64
+ def parse(string)
65
+ if sub_matches = PARSE_PATTERN.match(string.to_s)
66
+ original, year, week_index, day_index = *sub_matches
67
+ new(year.to_i, week_index.to_i, day_index.to_i)
68
+ else
69
+ raise ArgumentError, 'No week day found for parsing'
70
+ end
71
+ end
72
+ end
73
+
74
+ # @return [Aef::Weekling::Week] the week the day is part of
75
+ attr_reader :week
76
+
77
+ # @return [Integer] the number of the day in its week
78
+ attr_reader :index
79
+
80
+ # @overload initialize(week_day)
81
+ # Initialize by a week-day-like object.
82
+ # @param [Aef::Weekling::WeekDay] week_day a week-day-like object
83
+ #
84
+ # @overload initialize(date)
85
+ # Initialize by a date-like object.
86
+ # @param [Date, DateTime, Time] date a date-like object
87
+ #
88
+ # @overload initialize(week, day)
89
+ # Initialize by week-like-object and a day.
90
+ # @param [Aef::Weekling::Week] week a week-like object
91
+ # @param [Integer, Symbol] day either a day number or a lowercase
92
+ # english day name
93
+ #
94
+ # @overload initialize(year, week_index, day)
95
+ # Initialize by year, week number and day.
96
+ # @param [Integer, Aef::Weekling::Year] year a year
97
+ # @param [Integer] week_number a weeks index
98
+ # @param [Integer, Symbol] day either a day number or a lowercase english day name
99
+ def initialize(*arguments)
100
+ case arguments.count
101
+ when 1
102
+ object = arguments.first
103
+ if [:week, :index].all?{|method_name| object.respond_to?(method_name) }
104
+ @week = object.week.to_week
105
+ @index = object.index.to_i
106
+ elsif object.respond_to?(:to_date)
107
+ date = object.to_date
108
+ @week = Week.new(date)
109
+ @index = ((date.wday - 1) % 7) + 1
110
+ else
111
+ raise ArgumentError, 'A single argument must either respond to #week and #index or to #to_date'
112
+ end
113
+ when 2
114
+ week, day = *arguments
115
+ @week = week.to_week
116
+ if day.respond_to?(:to_i)
117
+ @index = day.to_i
118
+ else
119
+ raise ArgumentError, 'Invalid day symbol' unless @index = SYMBOL_TO_INDEX_TABLE[day.to_sym]
120
+ end
121
+ when 3
122
+ year, week_index, day = *arguments
123
+ @week = Week.new(year, week_index)
124
+ if day.respond_to?(:to_i)
125
+ @index = day.to_i
126
+ else
127
+ raise ArgumentError, 'Invalid day symbol' unless @index = SYMBOL_TO_INDEX_TABLE[day.to_sym]
128
+ end
129
+ else
130
+ raise ArgumentError, "wrong number of arguments (#{arguments.count} for 1..3)"
131
+ end
132
+
133
+ raise ArgumentError, 'Index must be in 1..7' unless (1..7).include?(index)
134
+ end
135
+
136
+ # Represents a week-day as String in ISO 8601 format.
137
+ #
138
+ # @example Output of friday in 42nd week of 2525
139
+ # Aef::Weekling::WeekDay.new(2525, 42, 5).to_s
140
+ # # => "2525-W42-5"
141
+ #
142
+ # @return [String] a character representation of the week day
143
+ def to_s
144
+ "#{week}-#{index}"
145
+ end
146
+
147
+ # Represents a week-day as String for debugging.
148
+ #
149
+ # @example Output of friday in 42nd week of 2525
150
+ # Aef::Weekling::WeekDay.new(2525, 42, 5)
151
+ # # => "#<Aef::Weekling::WeekDay: 2525-W42-5>"
152
+ #
153
+ # @return [String] a character representation for debugging
154
+ def inspect
155
+ "#<#{self.class.name}: #{to_s}>"
156
+ end
157
+
158
+ # @return [Symbol] a symbolic representation of the week day
159
+ def to_sym
160
+ SYMBOL_TO_INDEX_TABLE.invert[index]
161
+ end
162
+
163
+ # Returns the date of the week-day.
164
+ #
165
+ # @return [Date] the date of the week-day
166
+ def to_date
167
+ date = Date.new(week.year.to_i, 1, 1)
168
+
169
+ days_to_add = 7 * week.index
170
+ days_to_add -= 7 if date.cweek == 1
171
+ days_to_add -= ((date.wday - 1) % 7) + 1
172
+ days_to_add += index
173
+
174
+ date + days_to_add
175
+ end
176
+
177
+ # @return [Aef::Weekling::WeekDay] self reference
178
+ def to_week_day
179
+ self
180
+ end
181
+
182
+ # @param [Aef::Weekling::WeekDay] other a week-day-like object to be compared
183
+ # @return [true, false] true if other lies in the same week and has the
184
+ # same index
185
+ def ==(other)
186
+ other_week_day = self.class.new(other)
187
+
188
+ week == other_week_day.week and index == other_week_day.index
189
+ end
190
+
191
+ # @param [Aef::Weekling::WeekDay] other a week-day object to be compared
192
+ # @return [true, false] true if other lies in the same year, has the same
193
+ # index and is of the same or a descending class
194
+ def eql?(other)
195
+ other.is_a?(self.class) and self == other
196
+ end
197
+
198
+ # @return [see Array#hash] identity hash for hash table usage
199
+ def hash
200
+ [week, index].hash
201
+ end
202
+
203
+ # Compares the week-day with another to determine its relative position.
204
+ #
205
+ # @param [Aef::Weekling::WeekDay] other a week-day-like object to be compared
206
+ # @return [-1, 0, 1] -1 if other is greater, 0 if other is equal and 1 if
207
+ # other is lesser than self
208
+ def <=>(other)
209
+ other_week_day = self.class.new(other)
210
+
211
+ week_comparison = week <=> other_week_day.week
212
+
213
+ return index <=> other_week_day.index if week_comparison == 0
214
+ return week_comparison
215
+ end
216
+
217
+ # Finds the following week-day.
218
+ #
219
+ # @example The thursday after some wednesday
220
+ # some_day = Aef::Weekling::WeekDay.new(2013, 33, 3)
221
+ # some_day.next
222
+ # # => #<Aef::Weekling::WeekDay: 2013-W33-4>
223
+ #
224
+ # @example The week-day after a year's last week-day
225
+ # last_week_day_in_year = Aef::Weekling::WeekDay.new(1998, 53, 7)
226
+ # last_week_day_in_year.next
227
+ # # => #<Aef::Weekling::WeekDay: 1999-W01-1>
228
+ #
229
+ # @return [Aef::Weekling::WeekDay] the following week-day
230
+ def next
231
+ if index == 7
232
+ self.class.new(week.next, 1)
233
+ else
234
+ self.class.new(week, index + 1)
235
+ end
236
+ end
237
+
238
+ alias succ next
239
+ # Find the previous week-day
240
+ #
241
+ # @example The sunday before some monday
242
+ # some_week = Aef::Weekling::WeekDay.new(1783, 16, 1)
243
+ # some_week.previous
244
+ # # => #<Aef::Weekling::WeekDay: 1783-W15-7>
245
+ #
246
+ # @example The week-day before first week-day of a year
247
+ # first_week_in_year = Aef::Weekling::WeekDay.new(2014, 1, 1)
248
+ # first_week_in_year.previous
249
+ # # => #<Aef::Weekling::WeekDay: 2013-W52-7>
250
+ #
251
+ # @return [Aef::Weekling::WeekDay] the previous week-day
252
+ def previous
253
+ if index == 1
254
+ self.class.new(week.previous, 7)
255
+ else
256
+ self.class.new(week, index - 1)
257
+ end
258
+ end
259
+
260
+ alias pred previous
261
+
262
+ # Adds days to the week-day.
263
+ #
264
+ # @example 28 days after 2007-W01-1
265
+ # Aef::Weekling::WeekDay.new(2007, 1, 1) + 28
266
+ # # => #<Aef::Weekling::WeekDay: 2007-W05-1>
267
+ #
268
+ # @param [Integer] other number of days to add
269
+ # @return [Aef::Weekling::WeekDay] the resulting week-day
270
+ def +(other)
271
+ result = self
272
+ number = other.to_i
273
+
274
+ number.abs.times do
275
+ if number < 0
276
+ result = result.previous
277
+ else
278
+ result = result.next
279
+ end
280
+ end
281
+
282
+ result
283
+ end
284
+
285
+ # Subtracts days from the week-day.
286
+ #
287
+ # @example 5 days before 2012-W45-3
288
+ # Aef::Weekling::WeekDay.new(2012, 45, 3) - 5
289
+ # # => #<Aef::Weekling::WeekDay: 2012-W44-5>
290
+ #
291
+ # @param [Integer] other number of days to subtract
292
+ # @return [Aef::Weekling::WeekDay] the resulting week-day
293
+ def -(other)
294
+ self + -other.to_i
295
+ end
296
+
297
+ # @return [true, false] true if week day is monday
298
+ def monday?
299
+ to_sym == :monday
300
+ end
301
+
302
+ # @return [true, false] true if week day is tuesday
303
+ def tuesday?
304
+ to_sym == :tuesday
305
+ end
306
+
307
+ # @return [true, false] true if week day is wednesday
308
+ def wednesday?
309
+ to_sym == :wednesday
310
+ end
311
+
312
+ # @return [true, false] true if week day is thursday
313
+ def thursday?
314
+ to_sym == :thursday
315
+ end
316
+
317
+ # @return [true, false] true if week day is friday
318
+ def friday?
319
+ to_sym == :friday
320
+ end
321
+
322
+ # @return [true, false] true if week day is saturday
323
+ def saturday?
324
+ to_sym == :saturday
325
+ end
326
+
327
+ # @return [true, false] true if week day is sunday
328
+ def sunday?
329
+ to_sym == :sunday
330
+ end
331
+
332
+ # @return [true, false] true if week day is saturday or sunday
333
+ def weekend?
334
+ saturday? or sunday?
335
+ end
336
+
337
+ end
338
+ end
339
+ end
@@ -0,0 +1,251 @@
1
+ # encoding: UTF-8
2
+ =begin
3
+ Copyright Alexander E. Fischer <aef@raxys.net>, 2012
4
+
5
+ This file is part of Weekling.
6
+
7
+ Permission to use, copy, modify, and/or distribute this software for any
8
+ purpose with or without fee is hereby granted, provided that the above
9
+ copyright notice and this permission notice appear in all copies.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
12
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
13
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
14
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
15
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
16
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
17
+ PERFORMANCE OF THIS SOFTWARE.
18
+ =end
19
+
20
+ require 'aef/weekling'
21
+
22
+ module Aef
23
+ module Weekling
24
+
25
+ # Immutable object representing a calendar year (according to ISO 8601).
26
+ class Year
27
+ include Comparable
28
+
29
+ # Regular expression for Year extraction from strings.
30
+ # @private
31
+ PARSE_PATTERN = /(0|-?\d+)/
32
+
33
+ class << self
34
+ # Initializes the current year.
35
+ #
36
+ # @return [Aef::Weekling::Year] the current year
37
+ def today
38
+ today = Date.today
39
+
40
+ new(today.year)
41
+ end
42
+
43
+ alias now today
44
+
45
+ # Parses the first year out of a string.
46
+ #
47
+ # @note Looks for patterns like this:
48
+ # 2011
49
+ # @param [String] string a string containing a year representation
50
+ # @return [Aef::Weekling::Year] the year parsed from input
51
+ # @raise [ArgumentError] if pattern cannot be found
52
+ def parse(string)
53
+ if sub_matches = PARSE_PATTERN.match(string.to_s)
54
+ original, year = *sub_matches
55
+ new(year.to_i)
56
+ else
57
+ raise ArgumentError, 'No year found for parsing'
58
+ end
59
+ end
60
+ end
61
+
62
+ # @return [Integer] the number of the year
63
+ attr_reader :index
64
+
65
+ # @overload initialize(index)
66
+ # Initialize by number.
67
+ # @param [Integer] index the year's number
68
+ #
69
+ # @overload initialize(date)
70
+ # Initialize by a date-like object.
71
+ # @param [Date, DateTime, Time] date a date-like object
72
+ def initialize(index_or_date)
73
+ if index_or_date.respond_to?(:to_date)
74
+ date = index_or_date.to_date
75
+ @index = date.year
76
+ elsif index_or_date.respond_to?(:to_i)
77
+ @index = index_or_date.to_i
78
+ else
79
+ raise ArgumentError, 'A single argument must either respond to #to_date or to #to_i'
80
+ end
81
+ end
82
+
83
+ # Represents the year as Integer.
84
+ #
85
+ # @return [Integer] the year's number
86
+ def to_i
87
+ @index
88
+ end
89
+
90
+ # Represents the year as String in ISO 8601 format.
91
+ #
92
+ # @example Output of the year 2012
93
+ # Aef::Weekling::Year.new(2012).to_s
94
+ # # => "2012"
95
+ #
96
+ # @return [String] a character representation of the year
97
+ def to_s
98
+ @index.to_s
99
+ end
100
+
101
+ # Represents a week as String for debugging.
102
+ #
103
+ # @example Output of the year 2012
104
+ # Aef::Weekling::Year.new(2012).inspect
105
+ # # => "<#Aef::Weekling::Year: 2012>"
106
+ def inspect
107
+ "#<#{self.class.name}: #{to_s}>"
108
+ end
109
+
110
+ # @return [Aef::Weekling::Year] self reference
111
+ def to_year
112
+ self
113
+ end
114
+
115
+ # @param [Aef::Weekling::Year] other a year-like object to be compared
116
+ # @return [true, false] true if other has the same index
117
+ def ==(other)
118
+ other_year = self.class.new(other)
119
+
120
+ self.index == other_year.index
121
+ end
122
+
123
+ # @param [Aef::Weekling::Year] other a year object to be compared
124
+ # @return [true, false] true if other has the same index and is of the
125
+ # same or descending class
126
+ def eql?(other)
127
+ self == other and other.is_a?(self.class)
128
+ end
129
+
130
+ # @return [see Integer#hash] identity hash for hash table usage
131
+ def hash
132
+ @index.hash
133
+ end
134
+
135
+ # Compares the year with another to determine its relative position.
136
+ #
137
+ # @param [Aef::Weekling::Year] other a year-like object to be compared
138
+ # @return [-1, 0, 1] -1 if other is greater, 0 if other is equal and 1 if
139
+ # other is lesser than self
140
+ def <=>(other)
141
+ other_year = other.to_year
142
+
143
+ self.index <=> other_year.index
144
+ end
145
+
146
+ # Finds the following year.
147
+ #
148
+ # @example The year after some other year
149
+ # some_year = Aef::Weekling::Year.new(2012)
150
+ # some_year.next
151
+ # # => #<Aef::Weekling::Year: 2013>
152
+ #
153
+ # @return [Aef::Weekling::Year] the following year
154
+ def next
155
+ self.class.new(@index + 1)
156
+ end
157
+
158
+ alias succ next
159
+
160
+ # Finds the previous year.
161
+ #
162
+ # @example The year before some other year
163
+ # some_year = Aef::Weekling::Year.new(2012)
164
+ # some_year.previous
165
+ # # => #<Aef::Weekling::Year: 2011>
166
+ #
167
+ # @return [Aef::Weekling::Year] the previous year
168
+ def previous
169
+ self.class.new(@index - 1)
170
+ end
171
+
172
+ alias pred previous
173
+
174
+ # Adds years to the year.
175
+ #
176
+ # @example 3 year after 2000
177
+ # Aef::Weekling::Year.new(2000) + 3
178
+ # # => #<Aef::Weekling::Year: 2003>
179
+ #
180
+ # @param [Integer] other number of years to add
181
+ # @return [Aef::Weekling::Year] the resulting year
182
+ def +(other)
183
+ self.class.new(@index + other.to_i)
184
+ end
185
+
186
+ # Subtracts years from the year.
187
+ #
188
+ # @example 3 before 2000
189
+ # Aef::Weekling::Year.new(2000) - 3
190
+ # # => #<Aef::Weekling::Year: 1997>
191
+ #
192
+ # @param [Integer] other number of years to subtract
193
+ # @return [Aef::Weekling::Year] the resulting year
194
+ def -(other)
195
+ self.class.new(@index - other.to_i)
196
+ end
197
+
198
+ # States if the year's index is odd.
199
+ #
200
+ # @return [true, false] true if the year is odd
201
+ def odd?
202
+ @index.odd?
203
+ end
204
+
205
+ # States if the year's index is even.
206
+ #
207
+ # @return [true, false] true if the year is even
208
+ def even?
209
+ @index.even?
210
+ end
211
+
212
+ # States if the year is a leap year.
213
+ #
214
+ # @return [true, false] true is the year is a leap year
215
+ def leap?
216
+ Date.leap?(to_i)
217
+ end
218
+
219
+ # Calculates the amount of weeks in year.
220
+ #
221
+ # @example Acquire the amount of weeks for 2015
222
+ # Aef::Weekling::Year.new(2015).week_count
223
+ # # => 53
224
+ #
225
+ # @return [Integer] the amount of weeks
226
+ def week_count
227
+ date = Date.new(index, 12, 31)
228
+
229
+ date = date - 7 if date.cweek == 1
230
+
231
+ date.cweek
232
+ end
233
+
234
+ # Finds a week in the year.
235
+ #
236
+ # @param [Integer] index the week's index
237
+ # @return [Aef::Weekling::Week] the week in year with the given index
238
+ def week(index)
239
+ Week.new(self, index.to_i)
240
+ end
241
+
242
+ # Returns a range of all weeks in year.
243
+ #
244
+ # @return [Range<Aef::Weekling::Week>] all weeks in year
245
+ def weeks
246
+ week(1)..week(week_count)
247
+ end
248
+
249
+ end
250
+ end
251
+ end