weekling 1.0.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.
@@ -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