workpattern 0.5.0 → 0.7.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.
@@ -8,18 +8,36 @@ module Workpattern
8
8
  #
9
9
  # @private
10
10
  class Week
11
- attr_accessor :values, :hours_per_day, :start, :finish, :week_total, :total
11
+ attr_accessor :hours_per_day, :start, :finish, :days
12
12
 
13
- def initialize(start, finish, type = 1, hours_per_day = 24)
13
+ def initialize(start, finish, type = WORK_TYPE, hours_per_day = HOURS_IN_DAY)
14
14
  @hours_per_day = hours_per_day
15
15
  @start = Time.gm(start.year, start.month, start.day)
16
16
  @finish = Time.gm(finish.year, finish.month, finish.day)
17
- @values = Array.new(6)
18
- 0.upto(6) do |i|
19
- @values[i] = working_day * type
17
+ @days = Array.new(LAST_DAY_OF_WEEK + 1)
18
+ FIRST_DAY_OF_WEEK.upto(LAST_DAY_OF_WEEK) do |i|
19
+ @days[i] = Day.new(hours_per_day, type)
20
20
  end
21
21
  end
22
22
 
23
+ def to_h
24
+ { start: { year: @start.year, month: @start.month, day: @start.day },
25
+ finish: { year: @finish.year, month: @finish.month, day: @finish.day },
26
+ days: (FIRST_DAY_OF_WEEK..LAST_DAY_OF_WEEK).map { |i| @days[i].to_h } }
27
+ end
28
+
29
+ def self.from_h(h)
30
+ s = h[:start]
31
+ f = h[:finish]
32
+ week = allocate
33
+ week.hours_per_day = HOURS_IN_DAY
34
+ week.start = Time.gm(s[:year], s[:month], s[:day])
35
+ week.finish = Time.gm(f[:year], f[:month], f[:day])
36
+ week.days = Array.new(LAST_DAY_OF_WEEK + 1)
37
+ h[:days].each_with_index { |dh, i| week.days[i] = Day.from_h(dh) }
38
+ week
39
+ end
40
+
23
41
  def <=>(other)
24
42
  return -1 if start < other.start
25
43
  return 0 if start == other.start
@@ -27,7 +45,7 @@ module Workpattern
27
45
  end
28
46
 
29
47
  def week_total
30
- elapsed_days > 6 ? full_week_total_minutes : part_week_total_minutes
48
+ elapsed_days > 6 ? full_week_working_minutes : part_week_total_minutes
31
49
  end
32
50
 
33
51
  def total
@@ -36,56 +54,62 @@ module Workpattern
36
54
 
37
55
  def workpattern(days, from_time, to_time, type)
38
56
  DAYNAMES[days].each do |day|
39
- if type == 1
40
- work_on_day(day, from_time, to_time)
57
+ if type == WORK_TYPE
58
+ @days[day].set_working(from_time, to_time)
41
59
  else
42
- rest_on_day(day, from_time, to_time)
60
+ @days[day].set_resting(from_time, to_time)
43
61
  end
44
62
  end
45
63
  end
46
64
 
47
65
  def duplicate
48
- duplicate_week = Week.new(start, finish)
49
- 0.upto(6).each { |i| duplicate_week.values[i] = @values[i] }
66
+ duplicate_week = Week.new(@start, @finish)
67
+ FIRST_DAY_OF_WEEK.upto(LAST_DAY_OF_WEEK) do |i|
68
+ duplicate_week.days[i] = @days[i].clone
69
+ duplicate_week.days[i].hours_per_day = @days[i].hours_per_day
70
+ duplicate_week.days[i].pattern = @days[i].pattern
71
+ end
50
72
  duplicate_week
51
73
  end
52
74
 
53
- def calc(start_date, duration, midnight = false)
54
- return start_date, duration, false if duration == 0
55
- return add(start_date, duration) if duration > 0
56
- return subtract(start, duration, midnight) if total == 0 && duration < 0
57
- subtract(start_date, duration, midnight)
75
+ def calc(a_date, a_duration, a_day = SAME_DAY)
76
+ if a_duration == 0
77
+ return a_date, a_duration
78
+ elsif a_duration > 0
79
+ return add(a_date, a_duration)
80
+ else
81
+ subtract(a_date, a_duration, a_day)
82
+ end
58
83
  end
59
84
 
60
85
  def working?(time)
61
- return true if bit_pos(time.hour, time.min) & @values[time.wday] > 0
62
- false
86
+ @days[time.wday].working?(time.hour, time.min)
63
87
  end
64
88
 
65
- def resting?(date)
66
- !working?(date)
89
+ def resting?(time)
90
+ @days[time.wday].resting?(time.hour, time.min)
67
91
  end
68
92
 
69
- def diff(start_d, finish_d)
70
- start_d, finish_d = finish_d, start_d if ((start_d <=> finish_d)) == 1
93
+ def diff(start_date, finish_date)
94
+ if start_date > finish_date
95
+ start_date, finish_date = finish_date, start_date
96
+ end
71
97
 
72
- return diff_in_same_day(start_d, finish_d) if jd(start_d) == jd(finish_d)
73
- return diff_in_same_weekpattern(start_d, finish_d) if jd(finish_d) <= jd(finish)
74
- diff_beyond_weekpattern(start_d, finish_d)
98
+ if jd(start_date) == jd(finish_date)
99
+ return diff_in_same_day(start_date, finish_date)
100
+ else
101
+ return diff_in_same_weekpattern(start_date, finish_date)
102
+ end
75
103
  end
76
-
104
+
77
105
  private
78
106
 
79
- def working_minutes_in(day)
80
- day.to_s(2).count('1')
81
- end
82
-
83
107
  def elapsed_days
84
- (finish - start).to_i / 86_400 + 1
108
+ (finish - start).to_i / DAY + 1
85
109
  end
86
110
 
87
- def full_week_total_minutes
88
- minutes_in_day_range 0, 6
111
+ def full_week_working_minutes
112
+ minutes_in_day_range FIRST_DAY_OF_WEEK, LAST_DAY_OF_WEEK
89
113
  end
90
114
 
91
115
  def part_week_total_minutes
@@ -114,342 +138,155 @@ module Workpattern
114
138
  end
115
139
 
116
140
  def minutes_to_first_saturday
117
- minutes_in_day_range(start.wday, 6)
141
+ minutes_in_day_range(start.wday, LAST_DAY_OF_WEEK)
118
142
  end
119
143
 
120
144
  def minutes_to_finish_day
121
- minutes_in_day_range(0, finish.wday)
145
+ minutes_in_day_range(FIRST_DAY_OF_WEEK, finish.wday)
122
146
  end
123
147
 
124
148
  def minutes_in_day_range(first, last)
125
- @values[first..last].inject(0) { |a, e| a + working_minutes_in(e) }
149
+ @days[first..last].inject(0) { |sum, day| sum + day.working_minutes }
126
150
  end
127
151
 
128
- def add(initial_date, duration)
129
- running_date, duration = add_to_end_of_day(initial_date, duration)
152
+ def add(a_date, a_duration)
130
153
 
131
- running_date, duration = add_to_finish_day running_date, duration
132
- running_date, duration = add_full_weeks running_date, duration
133
- running_date, duration = add_remaining_days running_date, duration
134
- [running_date, duration, false]
135
- end
136
-
137
- def add_to_end_of_day(initial_date, duration)
138
- available_minutes_in_day = minutes_to_end_of_day(initial_date)
154
+ r_date, r_duration = add_to_end_of_day(a_date, a_duration)
139
155
 
140
- if available_minutes_in_day < duration
141
- duration -= available_minutes_in_day
142
- initial_date = start_of_next_day(initial_date)
143
- elsif available_minutes_in_day == duration
144
- duration -= available_minutes_in_day
145
- initial_date = end_of_this_day(initial_date)
146
- else
147
- initial_date = consume_minutes(initial_date, duration)
148
- duration = 0
149
- end
150
- [initial_date, duration]
156
+ r_date, r_duration = add_to_finish_day r_date, r_duration
157
+ r_date, r_duration = add_full_weeks r_date, r_duration
158
+ r_date, r_duration = add_remaining_days r_date, r_duration
159
+ [r_date, r_duration, false]
151
160
  end
152
161
 
153
- def add_to_finish_day(date, duration)
154
- while (duration != 0) && (date.wday != next_day(finish).wday) && (jd(date) <= jd(finish))
155
- date, duration = add_to_end_of_day(date, duration)
156
- end
162
+ def add_to_end_of_day(a_date, a_duration)
163
+ r_date, r_duration, r_day = @days[a_date.wday].calc(a_date,a_duration)
157
164
 
158
- [date, duration]
159
- end
165
+ if r_day == NEXT_DAY
166
+ r_date = start_of_next_day(r_date)
160
167
 
161
- def add_full_weeks(date, duration)
162
- while (duration != 0) && (duration >= week_total) && ((jd(date) + (6 * 86_400)) <= jd(finish))
163
- duration -= week_total
164
- date += (7 * 86_400)
165
168
  end
166
169
 
167
- [date, duration]
170
+ [r_date, r_duration]
168
171
  end
169
172
 
170
- def add_remaining_days(date, duration)
171
- while (duration != 0) && (jd(date) <= jd(finish))
172
- date, duration = add_to_end_of_day(date, duration)
173
- end
174
- [date, duration]
175
- end
176
-
177
- def add_to_finish_day(date, duration)
178
- while ( duration != 0) && (date.wday != next_day(self.finish).wday) && (jd(date) <= jd(self.finish))
179
- date, duration = add_to_end_of_day(date,duration)
173
+ def add_to_finish_day(a_date, a_duration)
174
+ while ( a_duration != 0) && (a_date.wday != next_day(self.finish).wday) && (jd(a_date) <= jd(self.finish))
175
+ a_date, a_duration = add_to_end_of_day(a_date,a_duration)
180
176
  end
181
177
 
182
- return date, duration
178
+ [a_date, a_duration]
183
179
  end
184
180
 
185
- def add_full_weeks(date, duration)
181
+ def add_full_weeks(a_date, a_duration)
186
182
 
187
- while (duration != 0) && (duration >= self.week_total) && ((jd(date) + (6*86400)) <= jd(self.finish))
188
- duration -= self.week_total
189
- date += (7*86400)
183
+ while (a_duration != 0) && (a_duration >= self.week_total) && ((jd(a_date) + (6*86400)) <= jd(self.finish))
184
+ a_duration -= self.week_total
185
+ a_date += (7*86400)
190
186
  end
191
187
 
192
- return date, duration
188
+ [a_date, a_duration]
193
189
  end
194
190
 
195
- def add_remaining_days(date, duration)
196
- while (duration != 0) && (jd(date) <= jd(self.finish))
197
- date, duration = add_to_end_of_day(date,duration)
191
+ def add_remaining_days(a_date, a_duration)
192
+ while (a_duration != 0) && (jd(a_date) <= jd(self.finish))
193
+ a_date, a_duration = add_to_end_of_day(a_date,a_duration)
198
194
  end
199
- return date, duration
200
- end
201
-
202
- def work_on_day(day,from_time,to_time)
203
- self.values[day] = self.values[day] | time_mask(from_time, to_time)
204
- end
205
-
206
- def rest_on_day(day,from_time,to_time)
207
- mask_of_1s = time_mask(from_time, to_time)
208
- mask = mask_of_1s ^ working_day & working_day
209
- self.values[day] = self.values[day] & mask
210
- end
211
-
212
- def time_mask(from_time, to_time)
213
- bit_pos(to_time.hour, to_time.min + 1) - bit_pos(from_time.hour, from_time.min)
214
- end
215
-
216
- def bit_pos(hour,minute)
217
- 2**( (hour * 60) + minute )
218
- end
219
-
220
- def work_on_day(day, from_time, to_time)
221
- values[day] = values[day] | time_mask(from_time, to_time)
222
- end
223
-
224
- def rest_on_day(day, from_time, to_time)
225
- mask_of_ones = time_mask(from_time, to_time)
226
- mask = mask_of_ones ^ working_day & working_day
227
- values[day] = values[day] & mask
228
- end
229
-
230
- def time_mask(from_time, to_time)
231
- bit_pos(to_time.hour, to_time.min + 1) - bit_pos(from_time.hour, from_time.min)
232
- end
233
-
234
- def bit_pos(hour, minute)
235
- 2**((hour * 60) + minute)
236
- end
237
-
238
- def minutes_to_end_of_day(date)
239
- working_minutes_in pattern_to_end_of_day(date)
240
- end
241
-
242
- def pattern_to_end_of_day(date)
243
- mask = mask_to_end_of_day(date)
244
- (values[date.wday] & mask)
245
- end
246
-
247
- def mask_to_end_of_day(date)
248
- bit_pos(hours_per_day, 0) - bit_pos(date.hour, date.min)
249
- end
250
-
251
- def working_day
252
- 2**(60 * hours_per_day) - 1
195
+ [a_date, a_duration]
253
196
  end
254
197
 
255
198
  def start_of_next_day(date)
256
199
  next_day(date) - (HOUR * date.hour) - (MINUTE * date.min)
257
200
  end
258
201
 
259
- def start_of_previous_day(date)
260
- prev_day(prev_day(start_of_next_day(date)))
261
- end
262
-
263
- def start_of_today(date)
264
- start_of_next_day(prev_day(date))
265
- end
266
-
267
- def end_of_this_day(date)
268
- position = pattern_to_end_of_day(date).to_s(2).size
269
- adjust_date(date, position)
270
- end
271
-
272
- def adjust_date(date, adjustment)
273
- date - (HOUR * date.hour) - (MINUTE * date.min) + (MINUTE * adjustment)
274
- end
275
-
276
- def mask_to_start_of_day(date)
277
- bit_pos(date.hour, date.min) - bit_pos(0, 0)
278
- end
279
-
280
- def pattern_to_start_of_day(date)
281
- mask = mask_to_start_of_day(date)
282
- (values[date.wday] & mask)
283
- end
284
-
285
- def minutes_to_start_of_day(date)
286
- working_minutes_in pattern_to_start_of_day(date)
287
- end
288
-
289
- def consume_minutes(date, duration)
290
- minutes = pattern_to_end_of_day(date).to_s(2).reverse! if duration > 0
291
- minutes = pattern_to_start_of_day(date).to_s(2) if duration < 0
292
-
293
- top = minutes.size
294
- bottom = 1
295
- mark = top / 2
296
-
297
- while minutes[0, mark].count('1') != duration.abs
298
- last_mark = mark
299
- if minutes[0, mark].count('1') < duration.abs
300
-
301
- bottom = mark
302
- mark = (top - mark) / 2 + mark
303
- mark = top if last_mark == mark
304
-
305
- else
306
-
307
- top = mark
308
- mark = (mark - bottom) / 2 + bottom
309
- mark = bottom if last_mark == mark
310
-
311
- end
312
- end
313
-
314
- mark = minutes_addition_adjustment(minutes, mark) if duration > 0
315
- mark = minutes_subtraction_adjustment(minutes, mark) if duration < 0
316
-
317
- return adjust_date(date, mark) if duration > 0
318
- return start_of_today(date) + (MINUTE * mark) if duration < 0
319
- end
202
+ def subtract_to_start_of_day(a_date, a_duration, a_day)
203
+
204
+
205
+ a_date, a_duration, a_day = handle_midnight(a_date, a_duration, a_day)
320
206
 
321
- def minutes_subtraction_adjustment(minutes, mark)
322
- i = mark - 1
207
+ r_date, r_duration, r_day = @days[a_date.wday].calc(a_date, a_duration)
323
208
 
324
- while minutes[i] == '0'
325
- i -= 1
326
- end
209
+ [r_date, r_duration, r_day]
327
210
 
328
- minutes.size - (i + 1)
329
211
  end
330
212
 
331
- def minutes_addition_adjustment(minutes, mark)
332
- minutes = minutes[0, mark]
333
-
334
- while minutes[minutes.size - 1] == '0'
335
- minutes.chop!
336
- end
337
-
338
- minutes.size
339
- end
340
-
341
- def subtract_to_start_of_day(initial_date, duration, midnight)
342
- initial_date, duration, midnight = handle_midnight(initial_date, duration) if midnight
343
- available_minutes_in_day = minutes_to_start_of_day(initial_date)
344
-
345
- if duration != 0
346
- if available_minutes_in_day < duration.abs
347
- duration += available_minutes_in_day
348
- initial_date = start_of_previous_day(initial_date)
349
- midnight = true
350
- else
351
- initial_date = consume_minutes(initial_date, duration)
352
- duration = 0
353
- midnight = false
354
- end
355
- end
356
- [initial_date, duration, midnight]
357
- end
213
+ def handle_midnight(a_date, a_duration, a_day)
214
+
215
+ if a_day == PREVIOUS_DAY
216
+ a_date -= DAY
217
+ a_date = Time.gm(a_date.year, a_date.month, a_date.day,LAST_TIME_IN_DAY.hour, LAST_TIME_IN_DAY.min)
358
218
 
359
- def handle_midnight(initial_date, duration)
360
- if working?(start_of_next_day(initial_date) - MINUTE)
361
- duration += 1
219
+ if @days[a_date.wday].working?(a_date.hour, a_date.min)
220
+ a_duration += 1
221
+ end
362
222
  end
363
223
 
364
- initial_date -= (HOUR * initial_date.hour)
365
- initial_date -= (MINUTE * initial_date.min)
366
- initial_date = next_day(initial_date) - MINUTE
367
-
368
- [initial_date, duration, false]
224
+ [a_date, a_duration, SAME_DAY]
369
225
  end
370
226
 
371
- def subtract(initial_date, duration, midnight)
372
- initial_date, duration, midnight = handle_midnight(initial_date, duration) if midnight
373
-
374
- initial_date, duration, midnight = subtract_to_start_of_day(initial_date, duration, midnight)
375
-
376
- while (duration != 0) && (initial_date.wday != prev_day(start.wday)) && (jd(initial_date) >= jd(start))
377
- initial_date, duration, midnight = subtract_to_start_of_day(initial_date, duration, midnight)
227
+ def subtract(a_date, a_duration, a_day)
228
+ a_date, a_duration, a_day = handle_midnight(a_date, a_duration, a_day)
229
+ a_date, a_duration, a_day = subtract_to_start_of_day(a_date, a_duration, a_day)
230
+
231
+ while (a_duration != 0) && (a_date.wday != start.wday) && (jd(a_date) > jd(start))
232
+ a_date, a_duration, a_day = handle_midnight(a_date, a_duration, a_day)
233
+ a_date, a_duration, a_day = subtract_to_start_of_day(a_date, a_duration, a_day)
378
234
  end
379
235
 
380
- while (duration != 0) && (duration >= week_total) && ((jd(initial_date) - (6 * 86_400)) >= jd(start))
381
- duration += week_total
382
- initial_date -= 7
236
+ while (a_duration != 0) && (a_duration >= week_total) && ((jd(a_date) - (6 * DAY)) >= jd(start))
237
+ a_duration += week_total
238
+ a_date -= 7
383
239
  end
384
240
 
385
- while (duration != 0) && (jd(initial_date) >= jd(start))
386
- initial_date, duration, midnight = subtract_to_start_of_day(initial_date,
387
- duration,
388
- midnight)
241
+ while (a_duration != 0) && (jd(a_date) > jd(start))
242
+ a_date, a_duration, a_day = subtract_to_start_of_day(a_date,a_duration,a_day)
389
243
  end
390
244
 
391
- [initial_date, duration, midnight]
245
+ [a_date, a_duration, a_day]
392
246
  end
393
247
 
394
248
  def diff_in_same_weekpattern(start_date, finish_date)
395
- duration, start_date = diff_to_tomorrow(start_date)
396
- loop do
397
- break if start_date.wday == (finish.wday + 1)
398
- break if jd(start_date) == jd(finish)
399
- break if jd(start_date) == jd(finish_date)
400
- duration += minutes_to_end_of_day(start_date)
401
- start_date = start_of_next_day(start_date)
249
+ minutes = @days[start_date.wday].working_minutes(start_date, LAST_TIME_IN_DAY)
250
+ run_date = start_of_next_day(start_date)
251
+ while (run_date.wday != start.wday) && (jd(run_date) < jd(finish)) && (jd(run_date) != jd(finish_date))
252
+ minutes += @days[run_date.wday].working_minutes
253
+ run_date += DAY
402
254
  end
403
255
 
404
- loop do
405
- break if (start_date + (7 * 86_400)) > finish_date
406
- break if jd(start_date + (6 * 86_400)) > jd(finish)
407
- duration += week_total
408
- start_date += (7 * 86_400)
256
+ while ((jd(run_date) + (7 * DAY)) < jd(finish_date)) && ((jd(run_date) + (7 * DAY)) < jd(finish))
257
+ minutes += week_total
258
+ run_date += (7 * DAY)
409
259
  end
410
260
 
411
- loop do
412
- break if jd(start_date) >= jd(finish)
413
- break if jd(start_date) >= jd(finish_date)
414
- duration += minutes_to_end_of_day(start_date)
415
- start_date = start_of_next_day(start_date)
261
+ while (jd(run_date) < jd(finish_date)) && (jd(run_date) <= jd(finish))
262
+ minutes += @days[run_date.wday].working_minutes
263
+ run_date += DAY
416
264
  end
417
265
 
418
- if start_date < finish
419
- interim_duration, start_date = diff_in_same_day(start_date, finish_date)
266
+ if run_date != finish_date
267
+
268
+ if (jd(run_date) == jd(finish_date)) && (jd(run_date) <= jd(finish))
269
+ minutes += @days[run_date.wday].working_minutes(run_date, finish_date - MINUTE)
270
+ run_date = finish_date
271
+ elsif (jd(run_date) <= jd(finish))
272
+ minutes += @days[run_date.wday].working_minutes
273
+ run_date += DAY
274
+ end
420
275
  end
421
- duration += interim_duration unless interim_duration.nil?
422
- [duration, start_date]
423
- end
424
276
 
425
- def diff_beyond_weekpattern(start_date, finish_date)
426
- duration, start_date = diff_in_same_weekpattern(start_date, finish_date)
427
- [duration, start_date]
428
- end
277
+ [minutes, run_date]
429
278
 
430
- def diff_to_tomorrow(start_date)
431
- finish_bit_pos = bit_pos(hours_per_day, 0)
432
- start_bit_pos = bit_pos(start_date.hour, start_date.min)
433
- mask = finish_bit_pos - start_bit_pos
434
- minutes = working_minutes_in(values[start_date.wday] & mask)
435
- [minutes, start_of_next_day(start_date)]
436
279
  end
437
280
 
438
281
  def diff_in_same_day(start_date, finish_date)
439
- finish_bit_pos = bit_pos(finish_date.hour, finish_date.min)
440
- start_bit_pos = bit_pos(start_date.hour, start_date.min)
441
- mask = finish_bit_pos - start_bit_pos
442
- minutes = working_minutes_in(values[start_date.wday] & mask)
282
+ minutes = @days[start_date.wday].working_minutes(start_date, finish_date - MINUTE)
443
283
  [minutes, finish_date]
444
284
  end
445
285
 
446
286
  def next_day(time)
447
- time + 86_400
287
+ time + DAY
448
288
  end
449
289
 
450
- def prev_day(time)
451
- time - 86_400
452
- end
453
290
 
454
291
  def jd(time)
455
292
  Time.gm(time.year, time.month, time.day)