workpattern 0.3.4 → 0.6.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.
@@ -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