workpattern 0.3.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,3 @@
1
1
  module Workpattern
2
- # Version Number for Workpattern gem
3
- # @since 0.0.1
4
- VERSION = '0.3.4'
5
- end
2
+ VERSION = '0.6.0'
3
+ end
@@ -1,363 +1,281 @@
1
1
  module Workpattern
2
-
3
- # @author Barrie Callender
4
- # @!attribute values
5
- # @return [Array] each day of the week
6
- # @!attribute days
7
- # @return [Integer] number of days in the week
8
- # @!attribute start
9
- # @return [DateTime] first date in the range
10
- # @!attribute finish
11
- # @return [DateTime] last date in the range
12
- # @!attribute week_total
13
- # @return [Integer] total number of minutes in a week
14
- # @!attribute total
15
- # @return [Integer] total number of minutes in the range
16
- #
17
- # Represents working and resting periods for each day in a week for a specified date range.
2
+ # The representation of a week might not be obvious so I am writing about it
3
+ # here. It will also help me if I ever need to come back to this in the
4
+ # future.
18
5
  #
19
- # @since 0.2.0
6
+ # Each day is represented by a binary number where a 1 represents a working
7
+ # minute and a 0 represents a resting minute.
20
8
  #
9
+ # @private
21
10
  class Week
22
-
23
- attr_accessor :values, :days, :start, :finish, :week_total, :total
24
-
25
- # The new <tt>Week</tt> object can be created as either working or resting.
26
- #
27
- # @param [DateTime] start first date in the range
28
- # @param [DateTime] finish last date in the range
29
- # @param [Integer] type working (1) or resting (0)
30
- # @return [Week] newly initialised Week object
31
- #
32
- def initialize(start,finish,type=1)
33
- hours_in_days_in_week=[24,24,24,24,24,24,24]
34
- @days=hours_in_days_in_week.size
35
- @values=Array.new(7) {|index| Day.new(type)}
36
- @start=DateTime.new(start.year,start.month,start.day)
37
- @finish=DateTime.new(finish.year,finish.month,finish.day)
38
-
39
- set_attributes
11
+ attr_accessor :hours_per_day, :start, :finish, :days
12
+ attr_writer :week_total, :total
13
+
14
+ def initialize(start, finish, type = WORK_TYPE, hours_per_day = HOURS_IN_DAY)
15
+ @hours_per_day = hours_per_day
16
+ @start = Time.gm(start.year, start.month, start.day)
17
+ @finish = Time.gm(finish.year, finish.month, finish.day)
18
+ @days = Array.new(LAST_DAY_OF_WEEK)
19
+ FIRST_DAY_OF_WEEK.upto(LAST_DAY_OF_WEEK) do |i|
20
+ @days[i] = Day.new(hours_per_day, type)
21
+ end
40
22
  end
41
-
42
- # Duplicates the current <tt>Week</tt> object
43
- #
44
- # @return [Week] a duplicated instance of the current <tt>Week</tt> object
45
- #
46
- def duplicate()
47
- duplicate_week=Week.new(@start,@finish)
48
- duplicate_values=Array.new(@values.size)
49
- @values.each_index {|index|
50
- duplicate_values[index]=@values[index].duplicate
51
- }
52
- duplicate_week.values=duplicate_values
53
- duplicate_week.days=@days
54
- duplicate_week.start=@start
55
- duplicate_week.finish=@finish
56
- duplicate_week.week_total=@week_total
57
- duplicate_week.total=@total
58
- duplicate_week.refresh
59
- return duplicate_week
23
+
24
+ def <=>(other)
25
+ return -1 if start < other.start
26
+ return 0 if start == other.start
27
+ 1
60
28
  end
61
-
62
- # Recalculates the attributes that define a <tt>Week</tt> object.
63
- # This was made public for <tt>#duplicate</tt> to work
64
- #
65
- def refresh
66
- set_attributes
29
+
30
+ def week_total
31
+ elapsed_days > 6 ? full_week_working_minutes : part_week_total_minutes
67
32
  end
68
-
69
- # Changes the date range.
70
- # This method calls <tt>#refresh</tt> to update the attributes.
71
- #
72
- # @param [DateTime] start is the new starting date for the <tt>Week</tt>
73
- # @param [DateTime] finish is the new finish date for the <tt>Week</tt>
74
- #
75
- def adjust(start,finish)
76
- @start=DateTime.new(start.year,start.month,start.day)
77
- @finish=DateTime.new(finish.year,finish.month,finish.day)
78
- refresh
33
+
34
+ def total
35
+ elapsed_days < 8 ? week_total : range_total
79
36
  end
80
-
81
- # Sets a range of minutes in a week to be working or resting. The parameters supplied
82
- # to this method determine exactly what should be changed
83
- #
84
- # @param [Hash(DAYNAMES)] days identifies the days to be included in the range
85
- # @param [DateTime] from_time where the time portion is used to specify the first minute to be set
86
- # @param [DateTime] to_time where the time portion is used to specify the last minute to be set
87
- # @param [Integer] type where a 1 sets it to working and a 0 to resting
88
- #
89
- def workpattern(days,from_time,to_time,type)
90
- DAYNAMES[days].each {|day| @values[day].workpattern(from_time,to_time,type)}
91
- refresh
37
+
38
+ def workpattern(days, from_time, to_time, type)
39
+ DAYNAMES[days].each do |day|
40
+ if type == WORK_TYPE
41
+ @days[day].set_working(from_time, to_time)
42
+ else
43
+ @days[day].set_resting(from_time, to_time)
44
+ end
45
+ end
92
46
  end
93
-
94
- # Calculates a new date by adding or subtracting a duration in minutes.
95
- #
96
- # @param [DateTime] start original date
97
- # @param [Integer] duration minutes to add or subtract
98
- # @param [Boolean] midnight flag used for subtraction that indicates the start date is midnight
99
- #
100
- def calc(start,duration, midnight=false)
101
- return start,duration,false if duration==0
102
- return add(start,duration) if duration > 0
103
- return subtract(@start,duration, midnight) if (@total==0) && (duration <0)
104
- return subtract(start,duration, midnight) if duration <0
47
+
48
+ def duplicate
49
+ duplicate_week = Week.new(@start, @finish)
50
+ FIRST_DAY_OF_WEEK.upto(LAST_DAY_OF_WEEK) do |i|
51
+ duplicate_week.days[i] = @days[i].clone
52
+ duplicate_week.days[i].hours_per_day = @days[i].hours_per_day
53
+ duplicate_week.days[i].pattern = @days[i].pattern
54
+ end
55
+ duplicate_week
105
56
  end
106
-
107
- # Comparison Returns an integer (-1, 0, or +1) if week is less than, equal to, or greater than other_week
108
- #
109
- # @param [Week] other_week object to compare to
110
- # @return [Integer] -1,0 or +1 if week is less than, equal to or greater than other_week
111
- def <=>(other_week)
112
- if @start < other_week.start
113
- return -1
114
- elsif @start == other_week.start
115
- return 0
116
- else
117
- return 1
118
- end
57
+
58
+ def calc(a_date, a_duration, a_day = SAME_DAY)
59
+ if a_duration == 0
60
+ return a_date, a_duration
61
+ elsif a_duration > 0
62
+ return add(a_date, a_duration)
63
+ else
64
+ subtract(a_date, a_duration, a_day)
65
+ end
119
66
  end
120
-
121
- # Returns true if the supplied DateTime is working and false if resting
122
- #
123
- # @param [DateTime] start DateTime to be tested
124
- # @return [Boolean] true if the minute is working otherwise false if it is a resting minute
125
- #
126
- def working?(start)
127
- @values[start.wday].working?(start)
128
- end
129
-
130
- # Returns the difference in minutes between two DateTime values.
131
- #
132
- # @param [DateTime] start starting DateTime
133
- # @param [DateTime] finish ending DateTime
134
- # @return [Integer, DateTime] number of minutes and start date for rest of calculation.
135
- #
136
- def diff(start,finish)
137
- start,finish=finish,start if ((start <=> finish))==1
138
- # calculate to end of day
139
- #
140
- if (start.jd==finish.jd) # same day
141
- duration, start=@values[start.wday].diff(start,finish)
142
- elsif (finish.jd<=@finish.jd) #within this week
143
- duration, start=diff_detail(start,finish,finish)
144
- else # after this week
145
- duration, start=diff_detail(start,finish,@finish)
67
+
68
+ def working?(time)
69
+ @days[time.wday].working?(time.hour, time.min)
70
+ end
71
+
72
+ def resting?(time)
73
+ @days[time.wday].resting?(time.hour, time.min)
74
+ end
75
+
76
+ def diff(start_date, finish_date)
77
+ if start_date > finish_date
78
+ start_date, finish_date = finish_date, start_date
79
+ end
80
+
81
+ if jd(start_date) == jd(finish_date)
82
+ return diff_in_same_day(start_date, finish_date)
83
+ else
84
+ return diff_in_same_weekpattern(start_date, finish_date)
146
85
  end
147
- return duration, start
148
86
  end
149
87
 
150
88
  private
151
-
152
- # Recalculates all the attributes for a Week object
153
- #
154
- def set_attributes
155
- @total=0
156
- @week_total=0
157
- days=(@finish-@start).to_i + 1 #/60/60/24+1
158
- if (7-@start.wday) < days and days < 8
159
- @total+=total_hours(@start.wday,@finish.wday)
160
- @week_total=@total
161
- else
162
- @total+=total_hours(@start.wday,6)
163
- days -= (7-@start.wday)
164
- @total+=total_hours(0,@finish.wday)
165
- days-=(@finish.wday+1)
166
- @week_total=@total if days==0
167
- week_total=total_hours(0,6)
168
- @total+=week_total * days / 7
169
- @week_total=week_total if days != 0
89
+
90
+ def elapsed_days
91
+ (finish - start).to_i / DAY + 1
92
+ end
93
+
94
+ def full_week_working_minutes
95
+ minutes_in_day_range FIRST_DAY_OF_WEEK, LAST_DAY_OF_WEEK
96
+ end
97
+
98
+ def part_week_total_minutes
99
+ start.wday <= finish.wday ? no_rollover_minutes : rollover_minutes
100
+ end
101
+
102
+ def no_rollover_minutes
103
+ minutes_in_day_range(start.wday, finish.wday)
104
+ end
105
+
106
+ def rollover_minutes
107
+ minutes_to_first_saturday + minutes_to_finish_day
108
+ end
109
+
110
+ def range_total
111
+ total_days = elapsed_days
112
+
113
+ sum = minutes_to_first_saturday
114
+ total_days -= (7 - start.wday)
115
+
116
+ sum += minutes_to_finish_day
117
+ total_days -= (finish.wday + 1)
118
+
119
+ sum += week_total * total_days / 7
120
+ sum
121
+ end
122
+
123
+ def minutes_to_first_saturday
124
+ minutes_in_day_range(start.wday, LAST_DAY_OF_WEEK)
125
+ end
126
+
127
+ def minutes_to_finish_day
128
+ minutes_in_day_range(FIRST_DAY_OF_WEEK, finish.wday)
129
+ end
130
+
131
+ def minutes_in_day_range(first, last)
132
+ @days[first..last].inject(0) { |sum, day| sum + day.working_minutes }
133
+ end
134
+
135
+ def add(a_date, a_duration)
136
+
137
+ r_date, r_duration = add_to_end_of_day(a_date, a_duration)
138
+
139
+ r_date, r_duration = add_to_finish_day r_date, r_duration
140
+ r_date, r_duration = add_full_weeks r_date, r_duration
141
+ r_date, r_duration = add_remaining_days r_date, r_duration
142
+ [r_date, r_duration, false]
143
+ end
144
+
145
+ def add_to_end_of_day(a_date, a_duration)
146
+ r_date, r_duration, r_day = @days[a_date.wday].calc(a_date,a_duration)
147
+
148
+ if r_day == NEXT_DAY
149
+ r_date = start_of_next_day(r_date)
150
+
170
151
  end
152
+
153
+ [r_date, r_duration]
171
154
  end
172
-
173
- # Calculates the total number of minutes between two dates
174
- #
175
- # @param [DateTime] start is the first date in the range
176
- # @param [DateTime] finish is the last date in the range
177
- # @return [Integer] total number of minutes between supplied dates
178
- #
179
- def total_hours(start,finish)
180
- total=0
181
- start.upto(finish) {|day|
182
- total+=@values[day].total
183
- }
184
- return total
155
+
156
+ def add_to_finish_day(a_date, a_duration)
157
+ while ( a_duration != 0) && (a_date.wday != next_day(self.finish).wday) && (jd(a_date) <= jd(self.finish))
158
+ a_date, a_duration = add_to_end_of_day(a_date,a_duration)
159
+ end
160
+
161
+ [a_date, a_duration]
185
162
  end
186
-
187
- # Adds a duration in minutes to a date.
188
- #
189
- # The Boolean returned is always false.
190
- #
191
- # @param [DateTime] start original date
192
- # @param [Integer] duration minutes to add
193
- # @return [DateTime, Integer, Boolean] the calculated date, remaining minutes and flag used for subtraction
194
- #
195
- def add(start,duration)
196
- # aim to calculate to the end of the day
197
- start,duration = @values[start.wday].calc(start,duration)
198
- return start,duration,false if (duration==0) || (start.jd > @finish.jd)
199
- # aim to calculate to the end of the next week day that is the same as @finish
200
- while((duration!=0) && (start.wday!=@finish.next_day.wday) && (start.jd <= @finish.jd))
201
- if (duration>@values[start.wday].total)
202
- duration = duration - @values[start.wday].total
203
- start=start.next_day
204
- elsif (duration==@values[start.wday].total)
205
- start=after_last_work(start)
206
- duration = 0
207
- else
208
- start,duration = @values[start.wday].calc(start,duration)
209
- end
163
+
164
+ def add_full_weeks(a_date, a_duration)
165
+
166
+ while (a_duration != 0) && (a_duration >= self.week_total) && ((jd(a_date) + (6*86400)) <= jd(self.finish))
167
+ a_duration -= self.week_total
168
+ a_date += (7*86400)
210
169
  end
211
-
212
- return start,duration,false if (duration==0) || (start.jd > @finish.jd)
213
-
214
- # while duration accomodates full weeks
215
- while ((duration!=0) && (duration>=@week_total) && ((start.jd+6) <= @finish.jd))
216
- duration=duration - @week_total
217
- start=start+7
170
+
171
+ [a_date, a_duration]
172
+ end
173
+
174
+ def add_remaining_days(a_date, a_duration)
175
+ while (a_duration != 0) && (jd(a_date) <= jd(self.finish))
176
+ a_date, a_duration = add_to_end_of_day(a_date,a_duration)
218
177
  end
178
+ [a_date, a_duration]
179
+ end
219
180
 
220
- return start,duration,false if (duration==0) || (start.jd > @finish.jd)
181
+ def start_of_next_day(date)
182
+ next_day(date) - (HOUR * date.hour) - (MINUTE * date.min)
183
+ end
221
184
 
222
- # while duration accomodates full days
223
- while ((duration!=0) && (start.jd<= @finish.jd))
224
- if (duration>@values[start.wday].total)
225
- duration = duration - @values[start.wday].total
226
- start=start.next_day
227
- else
228
- start,duration = @values[start.wday].calc(start,duration)
229
- end
230
- end
231
- return start, duration, false
185
+ def subtract_to_start_of_day(a_date, a_duration, a_day)
186
+
232
187
 
188
+ a_date, a_duration, a_day = handle_midnight(a_date, a_duration, a_day)
189
+
190
+ r_date, r_duration, r_day = @days[a_date.wday].calc(a_date, a_duration)
191
+
192
+ [r_date, r_duration, r_day]
193
+
233
194
  end
234
-
235
- # Subtracts a duration in minutes from a date
236
- #
237
- # @param [DateTime] start original date
238
- # @param [Integer] duration minutes to subtract - always a negative
239
- # @param [Boolean] midnight flag indicates the start date is midnight when true
240
- # @return [DateTime, Integer, Boolean] the calculated date, remaining number of minutes and
241
- # true if the time is midnight on the date
242
- #
243
- def subtract(start,duration,midnight=false)
195
+
196
+ def handle_midnight(a_date, a_duration, a_day)
244
197
 
245
- # Handle subtraction from start of day
246
- if midnight
247
- start,duration=minute_b4_midnight(start,duration)
248
- midnight=false
198
+ if a_day == PREVIOUS_DAY
199
+ a_date -= DAY
200
+ a_date = Time.gm(a_date.year, a_date.month, a_date.day,LAST_TIME_IN_DAY.hour, LAST_TIME_IN_DAY.min)
201
+
202
+ if @days[a_date.wday].working?(a_date.hour, a_date.min)
203
+ a_duration += 1
204
+ end
249
205
  end
250
206
 
251
- # aim to calculate to the start of the day
252
- start,duration, midnight = @values[start.wday].calc(start,duration)
207
+ [a_date, a_duration, SAME_DAY]
208
+ end
253
209
 
254
- if midnight && (start.jd >= @start.jd)
255
- start,duration=minute_b4_midnight(start,duration)
256
- return subtract(start,duration, false)
257
- elsif midnight
258
- return start,duration,midnight
259
- elsif (duration==0) || (start.jd ==@start.jd)
260
- return start,duration, midnight
261
- end
210
+ def subtract(a_date, a_duration, a_day)
211
+ a_date, a_duration, a_day = handle_midnight(a_date, a_duration, a_day)
212
+ a_date, a_duration, a_day = subtract_to_start_of_day(a_date, a_duration, a_day)
213
+
214
+ while (a_duration != 0) && (a_date.wday != start.wday) && (jd(a_date) > jd(start))
215
+ a_date, a_duration, a_day = handle_midnight(a_date, a_duration, a_day)
216
+ a_date, a_duration, a_day = subtract_to_start_of_day(a_date, a_duration, a_day)
217
+ end
262
218
 
263
- # aim to calculate to the start of the previous week day that is the same as @start
264
- while((duration!=0) && (start.wday!=@start.wday) && (start.jd >= @start.jd))
219
+ while (a_duration != 0) && (a_duration >= week_total) && ((jd(a_date) - (6 * DAY)) >= jd(start))
220
+ a_duration += week_total
221
+ a_date -= 7
222
+ end
265
223
 
266
- if (duration.abs>=@values[start.wday].total)
267
- duration = duration + @values[start.wday].total
268
- start=start.prev_day
269
- else
270
- start,duration=minute_b4_midnight(start,duration)
271
- start,duration = @values[start.wday].calc(start,duration)
272
- end
224
+ while (a_duration != 0) && (jd(a_date) > jd(start))
225
+ a_date, a_duration, a_day = subtract_to_start_of_day(a_date,a_duration,a_day)
273
226
  end
274
227
 
275
- return start,duration if (duration==0) || (start.jd ==@start.jd)
228
+ [a_date, a_duration, a_day]
229
+ end
230
+
231
+ def diff_in_same_weekpattern(start_date, finish_date)
232
+ minutes = @days[start_date.wday].working_minutes(start_date, LAST_TIME_IN_DAY)
233
+ run_date = start_of_next_day(start_date)
234
+ while (run_date.wday != start.wday) && (jd(run_date) < jd(finish)) && (jd(run_date) != jd(finish_date))
235
+ minutes += @days[run_date.wday].working_minutes
236
+ run_date += DAY
237
+ end
276
238
 
277
- #while duration accomodates full weeks
278
- while ((duration!=0) && (duration.abs>=@week_total) && ((start.jd-6) >= @start.jd))
279
- duration=duration + @week_total
280
- start=start-7
239
+ while ((jd(run_date) + (7 * DAY)) < jd(finish_date)) && ((jd(run_date) + (7 * DAY)) < jd(finish))
240
+ minutes += week_total
241
+ run_date += (7 * DAY)
281
242
  end
282
243
 
283
- return start,duration if (duration==0) || (start.jd ==@start.jd)
244
+ while (jd(run_date) < jd(finish_date)) && (jd(run_date) <= jd(finish))
245
+ minutes += @days[run_date.wday].working_minutes
246
+ run_date += DAY
247
+ end
284
248
 
285
- #while duration accomodates full days
286
- while ((duration!=0) && (start.jd>= @start.jd))
287
- if (duration.abs>=@values[start.wday].total)
288
- duration = duration + @values[start.wday].total
289
- start=start.prev_day
290
- else
291
- start,duration=minute_b4_midnight(start,duration)
292
- start,duration = @values[start.wday].calc(start,duration)
249
+ if run_date != finish_date
250
+
251
+ if (jd(run_date) == jd(finish_date)) && (jd(run_date) <= jd(finish))
252
+ minutes += @days[run_date.wday].working_minutes(run_date, finish_date - MINUTE)
253
+ run_date = finish_date
254
+ elsif (jd(run_date) <= jd(finish))
255
+ minutes += @days[run_date.wday].working_minutes
256
+ run_date += DAY
293
257
  end
294
- end
295
-
296
- return start, duration , midnight
297
-
258
+ end
259
+
260
+ [minutes, run_date]
261
+
298
262
  end
299
-
300
- # Supports calculating from midnight by updating the given duration depending on whether the
301
- # last minute in the day is resting or working. It then sets the time to this minute.
302
- #
303
- # @param [DateTime] start is the date whose midnight is to be used as the start date
304
- # @param [Integer] duration is the number of minutes to subtract
305
- # @return [DateTime, Integer] the date with a time of 23:59 and remaining duration
306
- # adjusted according to whether 23:59 is resting or not
307
- #
308
- def minute_b4_midnight(start,duration)
309
- start -= start.hour * HOUR
310
- start -= start.min * MINUTE
311
- duration += @values[start.wday].minutes(23,59,23,59)
312
- start = start.next_day - MINUTE
313
- return start,duration
314
- end
315
-
316
- # Calculates the date and time after the last working minute of the current date
317
- #
318
- # @param [DateTime] start is the current date
319
- # @return [DateTime] the new date
320
- #
321
- def after_last_work(start)
322
- if @values[start.wday].last_hour.nil?
323
- return start.next_day
324
- else
325
- start = start + HOUR * (@values[start.wday].last_hour - start.hour)
326
- start = start + MINUTE * (@values[start.wday].last_min - start.min + 1)
327
- return start
328
- end
263
+
264
+ def diff_in_same_day(start_date, finish_date)
265
+ minutes = @days[start_date.wday].working_minutes(start_date, finish_date - MINUTE)
266
+ [minutes, finish_date]
329
267
  end
330
-
331
- # Calculates the difference between two dates that exist in this Week object.
332
- #
333
- # @param [DateTime] start first date
334
- # @param [DateTime] finish last date
335
- # @param [DateTime] finish_on the range to be used in this Week object.
336
- # @return [DateTime, Integer] new date for rest of calculation and total number of minutes calculated thus far.
337
- #
338
- def diff_detail(start,finish,finish_on)
339
- duration, start=@values[start.wday].diff(start,finish)
340
- return duration,start if start > finish_on
341
- #rest of week to finish day
342
- while (start.wday<finish.wday) do
343
- duration+=@values[start.wday].total
344
- start=start.next_day
345
- end
346
- #weeks
347
- while (start.jd+7<finish_on.jd) do
348
- duration+=@week_total
349
- start+=7
350
- end
351
- #days
352
- while (start.jd < finish_on.jd) do
353
- duration+=@values[start.wday].total
354
- start=start.next_day
355
- end
356
- #day
357
- day_duration, start=@values[start.wday].diff(start,finish)
358
- duration+=day_duration
359
- return duration, start
268
+
269
+ def next_day(time)
270
+ time + DAY
271
+ end
272
+
273
+ def prev_day(time)
274
+ time - DAY
275
+ end
276
+
277
+ def jd(time)
278
+ Time.gm(time.year, time.month, time.day)
360
279
  end
361
-
362
280
  end
363
281
  end