workpattern 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -13
- data/.gitignore +4 -1
- data/.travis.yml +8 -2
- data/CHANGELOG +19 -0
- data/Gemfile +0 -0
- data/README.md +58 -57
- data/Rakefile +0 -0
- data/lib/workpattern.rb +65 -55
- data/lib/workpattern/clock.rb +17 -20
- data/lib/workpattern/utility/base.rb +16 -21
- data/lib/workpattern/version.rb +2 -2
- data/lib/workpattern/week.rb +266 -189
- data/lib/workpattern/workpattern.rb +170 -142
- data/script/console +0 -0
- data/script/destroy +0 -0
- data/script/generate +0 -0
- data/script/txt2html +0 -0
- data/test/test_clock.rb +19 -22
- data/test/test_helper.rb +0 -0
- data/test/test_week.rb +393 -368
- data/test/test_workpattern.rb +238 -207
- data/test/test_workpattern_module.rb +57 -51
- data/workpattern.gemspec +7 -6
- metadata +44 -16
- data/config/website.yml +0 -2
@@ -1,19 +1,23 @@
|
|
1
1
|
module Workpattern
|
2
2
|
require 'set'
|
3
|
-
|
4
|
-
|
5
|
-
#
|
3
|
+
require 'tzinfo'
|
4
|
+
|
5
|
+
# Represents the working and resting periods across a given number of whole
|
6
|
+
# years. Each <tt>Workpattern</tt>has a unique name so it can be easily
|
7
|
+
# identified amongst all the other <tt>Workpattern</tt> objects.
|
6
8
|
#
|
7
|
-
# This and the <tt>Clock</tt> class are the only two that should be
|
9
|
+
# This and the <tt>Clock</tt> class are the only two that should be
|
10
|
+
# referenced by calling applications when
|
8
11
|
# using this gem.
|
9
12
|
#
|
10
13
|
# @since 0.2.0
|
11
14
|
#
|
12
15
|
class Workpattern
|
13
|
-
|
16
|
+
include Base
|
17
|
+
|
14
18
|
# Holds collection of <tt>Workpattern</tt> objects
|
15
|
-
@@workpatterns =
|
16
|
-
|
19
|
+
@@workpatterns = {}
|
20
|
+
|
17
21
|
# @!attribute [r] name
|
18
22
|
# Name given to the <tt>Workpattern</tt>
|
19
23
|
# @!attribute [r] base
|
@@ -28,74 +32,77 @@ module Workpattern
|
|
28
32
|
# The <tt>Week</tt> objects that make up this workpattern
|
29
33
|
#
|
30
34
|
attr_reader :name, :base, :span, :from, :to, :weeks
|
31
|
-
|
35
|
+
|
32
36
|
# Class for handling persistence in user's own way
|
33
37
|
#
|
34
38
|
def self.persistence_class=(klass)
|
35
|
-
@@
|
39
|
+
@@persist = klass
|
36
40
|
end
|
37
41
|
|
38
42
|
def self.persistence?
|
39
|
-
@@
|
43
|
+
@@persist ||= nil
|
40
44
|
end
|
41
45
|
|
42
46
|
# The new <tt>Workpattern</tt> object is created with all working minutes.
|
43
47
|
#
|
44
48
|
# @param [String] name Every workpattern has a unique name
|
45
49
|
# @param [Integer] base Workpattern starts on the 1st January of this year.
|
46
|
-
# @param [Integer] span Workpattern spans this number of years ending on
|
50
|
+
# @param [Integer] span Workpattern spans this number of years ending on
|
51
|
+
# 31st December.
|
47
52
|
# @raise [NameError] if the given name already exists
|
48
53
|
#
|
49
|
-
def initialize(name=DEFAULT_NAME,base=DEFAULT_BASE_YEAR,span=DEFAULT_SPAN)
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
span < 0 ?
|
54
|
+
def initialize(name = DEFAULT_NAME, base = DEFAULT_BASE_YEAR, span = DEFAULT_SPAN)
|
55
|
+
if @@workpatterns.key?(name)
|
56
|
+
raise(NameError, "Workpattern '#{name}' already exists and can't be created again")
|
57
|
+
end
|
58
|
+
offset = span < 0 ? span.abs - 1 : 0
|
54
59
|
|
55
60
|
@name = name
|
56
61
|
@base = base
|
57
62
|
@span = span
|
58
|
-
@from =
|
59
|
-
@to =
|
63
|
+
@from = Time.gm(base.abs - offset)
|
64
|
+
@to = Time.gm(from.year + span.abs - 1, 12, 31, 23, 59)
|
60
65
|
@weeks = SortedSet.new
|
61
|
-
@weeks << Week.new(from,to,1)
|
62
|
-
|
63
|
-
@@workpatterns[name]=self
|
66
|
+
@weeks << Week.new(from, to, 1)
|
67
|
+
|
68
|
+
@@workpatterns[name] = self
|
64
69
|
end
|
65
|
-
|
70
|
+
|
66
71
|
# Deletes all <tt>Workpattern</tt> objects
|
67
72
|
#
|
68
73
|
def self.clear
|
69
74
|
@@workpatterns.clear
|
70
75
|
end
|
71
|
-
|
76
|
+
|
72
77
|
# Returns an Array containing all the <tt>Workpattern</tt> objects
|
73
78
|
# @return [Array] all <tt>Workpattern</tt> objects
|
74
|
-
#
|
79
|
+
#
|
75
80
|
def self.to_a
|
76
81
|
@@workpatterns.to_a
|
77
82
|
end
|
78
|
-
|
83
|
+
|
79
84
|
# Returns the specific named <tt>Workpattern</tt>
|
80
85
|
# @param [String] name of the required <tt>Workpattern</tt>
|
81
|
-
# @raise [NameError] if a <tt>Workpattern</tt> of the supplied name does not
|
86
|
+
# @raise [NameError] if a <tt>Workpattern</tt> of the supplied name does not
|
87
|
+
# exist
|
82
88
|
#
|
83
89
|
def self.get(name)
|
84
|
-
return @@workpatterns[name] if @@workpatterns.key?(name)
|
90
|
+
return @@workpatterns[name] if @@workpatterns.key?(name)
|
85
91
|
raise(NameError, "Workpattern '#{name}' doesn't exist so can't be retrieved")
|
86
92
|
end
|
87
|
-
|
93
|
+
|
88
94
|
# Deletes the specific named <tt>Workpattern</tt>
|
89
95
|
# @param [String] name of the required <tt>Workpattern</tt>
|
90
|
-
# @return [Boolean] true if the named <tt>Workpattern</tt> existed or false
|
96
|
+
# @return [Boolean] true if the named <tt>Workpattern</tt> existed or false
|
97
|
+
# if it doesn't
|
91
98
|
#
|
92
99
|
def self.delete(name)
|
93
100
|
@@workpatterns.delete(name).nil? ? false : true
|
94
101
|
end
|
95
|
-
|
102
|
+
|
96
103
|
# Applys a working or resting pattern to the <tt>Workpattern</tt> object.
|
97
104
|
#
|
98
|
-
# The #resting and #working methods are convenience methods that call
|
105
|
+
# The #resting and #working methods are convenience methods that call
|
99
106
|
# this with the appropriate <tt>:work_type</tt> already set.
|
100
107
|
#
|
101
108
|
# @param [Hash] opts the options used to apply a workpattern
|
@@ -103,103 +110,114 @@ module Workpattern
|
|
103
110
|
# to the <tt>start</tt> attribute.
|
104
111
|
# @option opts [Date] :finish The last date to apply the pattern. Defaults
|
105
112
|
# to the <tt>finish</tt> attribute.
|
106
|
-
# @option opts [DAYNAMES] :days The specific day or days the pattern will
|
107
|
-
#
|
108
|
-
# @option opts [(#hour, #min)] :start_time The first time in the selected
|
109
|
-
#
|
110
|
-
# @option opts [(#hour, #min)] :finish_time The last time in the selected
|
111
|
-
#
|
112
|
-
# @option opts [(WORK || REST)] :work_type Either working or resting.
|
113
|
+
# @option opts [DAYNAMES] :days The specific day or days the pattern will
|
114
|
+
# apply to.It defaults to <tt>:all</tt>
|
115
|
+
# @option opts [(#hour, #min)] :start_time The first time in the selected
|
116
|
+
# days to apply the pattern. Defaults to <tt>00:00</tt>.
|
117
|
+
# @option opts [(#hour, #min)] :finish_time The last time in the selected
|
118
|
+
# days to apply the pattern. Defaults to <tt>23:59</tt>.
|
119
|
+
# @option opts [(WORK || REST)] :work_type Either working or resting.
|
120
|
+
# Defaults to working.
|
113
121
|
# @see #working
|
114
122
|
# @see #resting
|
115
123
|
#
|
116
|
-
def workpattern(opts={})
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
124
|
+
def workpattern(opts = {})
|
125
|
+
args = { start: from, finish: to, days: :all,
|
126
|
+
from_time: FIRST_TIME_IN_DAY, to_time: LAST_TIME_IN_DAY,
|
127
|
+
work_type: WORK }
|
128
|
+
|
122
129
|
args.merge! opts
|
123
|
-
|
124
|
-
@@
|
130
|
+
|
131
|
+
@@persist.store(name: name, workpattern: args) if self.class.persistence?
|
125
132
|
|
126
133
|
args[:start] = dmy_date(args[:start])
|
127
134
|
args[:finish] = dmy_date(args[:finish])
|
128
135
|
args[:from_time] = hhmn_date(args[:from_time])
|
129
136
|
args[:to_time] = hhmn_date(args[:to_time])
|
130
137
|
|
131
|
-
upd_start=args[:start]
|
132
|
-
upd_finish=args[:finish]
|
133
|
-
while
|
138
|
+
upd_start = to_utc(args[:start])
|
139
|
+
upd_finish = to_utc(args[:finish])
|
140
|
+
while upd_start <= upd_finish
|
134
141
|
|
135
|
-
current_wp=find_weekpattern(upd_start)
|
142
|
+
current_wp = find_weekpattern(upd_start)
|
136
143
|
|
137
|
-
if
|
138
|
-
if
|
139
|
-
clone_wp=clone_and_adjust_current_wp(current_wp,
|
140
|
-
|
141
|
-
|
144
|
+
if current_wp.start == upd_start
|
145
|
+
if current_wp.finish > upd_finish
|
146
|
+
clone_wp = clone_and_adjust_current_wp(current_wp,
|
147
|
+
upd_finish + DAY,
|
148
|
+
current_wp.finish,
|
149
|
+
upd_start,
|
150
|
+
upd_finish)
|
151
|
+
set_workpattern_and_store(clone_wp, args)
|
152
|
+
upd_start = upd_finish + DAY
|
142
153
|
else # (current_wp.finish == upd_finish)
|
143
|
-
current_wp.workpattern(args[:days],args[:from_time],
|
144
|
-
|
154
|
+
current_wp.workpattern(args[:days], args[:from_time],
|
155
|
+
args[:to_time], args[:work_type])
|
156
|
+
upd_start = current_wp.finish + DAY
|
145
157
|
end
|
146
158
|
else
|
147
|
-
clone_wp=clone_and_adjust_current_wp(current_wp, current_wp.start,
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
upd_start=clone_wp.finish+1
|
159
|
+
clone_wp = clone_and_adjust_current_wp(current_wp, current_wp.start,
|
160
|
+
upd_start - DAY, upd_start)
|
161
|
+
if clone_wp.finish > upd_finish
|
162
|
+
after_wp = clone_and_adjust_current_wp(clone_wp,
|
163
|
+
upd_start,
|
164
|
+
upd_finish,
|
165
|
+
upd_finish + DAY)
|
166
|
+
weeks << after_wp
|
156
167
|
end
|
157
|
-
|
168
|
+
set_workpattern_and_store(clone_wp, args)
|
169
|
+
upd_start = clone_wp.finish + DAY
|
170
|
+
end
|
158
171
|
end
|
159
172
|
end
|
160
|
-
|
161
|
-
# Convenience method that calls <tt>#workpattern</tt> with the
|
173
|
+
|
174
|
+
# Convenience method that calls <tt>#workpattern</tt> with the
|
175
|
+
# <tt>:work_type</tt> specified as resting.
|
162
176
|
#
|
163
177
|
# @see #workpattern
|
164
178
|
#
|
165
|
-
def resting(args={})
|
166
|
-
args[:work_type]=REST
|
179
|
+
def resting(args = {})
|
180
|
+
args[:work_type] = REST
|
167
181
|
workpattern(args)
|
168
182
|
end
|
169
|
-
|
170
|
-
# Convenience method that calls <tt>#workpattern</tt> with the
|
183
|
+
|
184
|
+
# Convenience method that calls <tt>#workpattern</tt> with the
|
185
|
+
# <tt>:work_type</tt> specified as working.
|
171
186
|
#
|
172
187
|
# @see #workpattern
|
173
188
|
#
|
174
|
-
def working(args={})
|
175
|
-
args[:work_type]=WORK
|
189
|
+
def working(args = {})
|
190
|
+
args[:work_type] = WORK
|
176
191
|
workpattern(args)
|
177
192
|
end
|
178
|
-
|
179
|
-
# Calculates the resulting date when the <tt>duration</tt> in minutes
|
180
|
-
#
|
181
|
-
#
|
193
|
+
|
194
|
+
# Calculates the resulting date when the <tt>duration</tt> in minutes
|
195
|
+
# is added to the <tt>start</tt> date.
|
196
|
+
# The <tt>duration</tt> is always in whole minutes and subtracts from
|
197
|
+
# <tt>start</tt> when it is a negative number.
|
182
198
|
#
|
183
199
|
# @param [DateTime] start date to add or subtract minutes
|
184
200
|
# @param [Integer] duration in minutes to add or subtract to date
|
185
|
-
# @return [DateTime] the date when <tt>duration</tt> is added to
|
201
|
+
# @return [DateTime] the date when <tt>duration</tt> is added to
|
202
|
+
# <tt>start</tt>
|
186
203
|
#
|
187
|
-
def calc(start,duration)
|
188
|
-
return start if duration==0
|
189
|
-
midnight=false
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
204
|
+
def calc(start, duration)
|
205
|
+
return start if duration == 0
|
206
|
+
midnight = false
|
207
|
+
|
208
|
+
utc_start = to_utc(start)
|
209
|
+
while duration != 0
|
210
|
+
week = find_weekpattern(utc_start)
|
211
|
+
if (week.start == utc_start) && (duration < 0) && !midnight
|
212
|
+
utc_start = utc_start.prev_day
|
213
|
+
week = find_weekpattern(utc_start)
|
214
|
+
midnight = true
|
215
|
+
end
|
216
|
+
|
217
|
+
utc_start, duration, midnight = week.calc(utc_start, duration, midnight)
|
218
|
+
end
|
219
|
+
|
220
|
+
to_local(utc_start)
|
203
221
|
end
|
204
222
|
|
205
223
|
# Returns true if the given minute is working and false if it is resting.
|
@@ -208,89 +226,99 @@ module Workpattern
|
|
208
226
|
# @return [Boolean] true if working and false if resting
|
209
227
|
#
|
210
228
|
def working?(start)
|
211
|
-
|
212
|
-
|
213
|
-
|
229
|
+
utc_start = to_utc(start)
|
230
|
+
find_weekpattern(utc_start).working?(utc_start)
|
231
|
+
end
|
232
|
+
|
214
233
|
# Returns number of minutes between two dates
|
215
234
|
#
|
216
235
|
# @param [DateTime] start is the date to start from
|
217
236
|
# @param [DateTime] finish is the date to end with
|
218
237
|
# @return [Integer] number of minutes between the two dates
|
219
238
|
#
|
220
|
-
def diff(start,finish)
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
239
|
+
def diff(start, finish)
|
240
|
+
utc_start = to_utc(start)
|
241
|
+
utc_finish = to_utc(finish)
|
242
|
+
utc_start, utc_finish = utc_finish, utc_start if finish < start
|
243
|
+
duration = 0
|
244
|
+
while utc_start != utc_finish
|
245
|
+
week = find_weekpattern(utc_start)
|
246
|
+
result_duration, utc_start = week.diff(utc_start, utc_finish)
|
247
|
+
duration += result_duration
|
228
248
|
end
|
229
|
-
|
230
|
-
end
|
231
|
-
|
249
|
+
duration
|
250
|
+
end
|
251
|
+
|
232
252
|
private
|
233
|
-
|
253
|
+
|
234
254
|
# Retrieve the correct <tt>Week</tt> pattern for the supplied date.
|
235
255
|
#
|
236
|
-
# If the supplied <tt>date</tt> is outside the span of the
|
237
|
-
# then it returns an all working <tt>Week</tt>
|
238
|
-
#
|
256
|
+
# If the supplied <tt>date</tt> is outside the span of the
|
257
|
+
# <tt>Workpattern</tt> object then it returns an all working <tt>Week</tt>
|
258
|
+
# object for the calculation.
|
259
|
+
#
|
239
260
|
# @param [DateTime] date whose containing <tt>Week</tt> pattern is required
|
240
|
-
# @return [Week] <tt>Week</tt> object that includes the supplied
|
261
|
+
# @return [Week] <tt>Week</tt> object that includes the supplied
|
262
|
+
# <tt>date</tt> in it's range
|
241
263
|
#
|
242
264
|
def find_weekpattern(date)
|
243
265
|
# find the pattern that fits the date
|
244
266
|
#
|
245
|
-
if date<from
|
246
|
-
result = Week.new(
|
247
|
-
elsif date>to
|
248
|
-
result = Week.new(to+MINUTE,
|
267
|
+
if date < from
|
268
|
+
result = Week.new(Time.at(0), from - MINUTE, 1)
|
269
|
+
elsif date > to
|
270
|
+
result = Week.new(to + MINUTE, Time.new(9999), 1)
|
249
271
|
else
|
250
|
-
|
251
|
-
date = DateTime.new(date.year,date.month,date.day)
|
252
272
|
|
253
|
-
|
273
|
+
date = Time.gm(date.year, date.month, date.day)
|
274
|
+
|
275
|
+
result = weeks.find { |week| week.start <= date && week.finish >= date }
|
254
276
|
end
|
255
|
-
|
277
|
+
result
|
256
278
|
end
|
257
|
-
|
258
|
-
# Strips off hours, minutes, seconds
|
279
|
+
|
280
|
+
# Strips off hours, minutes, seconds etc from a supplied <tt>Date</tt> or
|
259
281
|
# <tt>DateTime</tt>
|
260
282
|
#
|
261
|
-
# @param [DateTime] date
|
283
|
+
# @param [DateTime] date
|
262
284
|
# @return [DateTime] with zero hours, minutes, seconds and so forth.
|
263
|
-
#
|
285
|
+
#
|
264
286
|
def dmy_date(date)
|
265
|
-
|
287
|
+
Time.gm(date.year, date.month, date.day)
|
266
288
|
end
|
267
|
-
|
289
|
+
|
268
290
|
# Extract the time into a <tt>Clock</tt> object
|
269
291
|
#
|
270
292
|
# @param [DateTime] date
|
271
|
-
# @return [Clock]
|
293
|
+
# @return [Clock]
|
272
294
|
def hhmn_date(date)
|
273
|
-
|
295
|
+
Clock.new(date.hour, date.min)
|
274
296
|
end
|
275
|
-
|
297
|
+
|
276
298
|
# Handles cloning of Week Pattern including date adjustments
|
277
|
-
#
|
278
|
-
def clone_and_adjust_current_wp(current_wp, current_start,current_finish,
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
299
|
+
#
|
300
|
+
def clone_and_adjust_current_wp(current_wp, current_start, current_finish,
|
301
|
+
clone_start, clone_finish = nil)
|
302
|
+
clone_wp = current_wp.duplicate
|
303
|
+
adjust_date_range(current_wp, current_start, current_finish)
|
304
|
+
if clone_finish.nil?
|
305
|
+
adjust_date_range(clone_wp, clone_start, clone_wp.finish)
|
306
|
+
else
|
307
|
+
adjust_date_range(clone_wp, clone_start, clone_finish)
|
308
|
+
end
|
309
|
+
clone_wp
|
283
310
|
end
|
284
311
|
|
285
312
|
def set_workpattern_and_store(new_wp, args)
|
286
|
-
new_wp.workpattern(args[:days],args[:from_time],
|
287
|
-
|
313
|
+
new_wp.workpattern(args[:days], args[:from_time],
|
314
|
+
args[:to_time], args[:work_type])
|
315
|
+
weeks << new_wp
|
288
316
|
end
|
289
317
|
|
290
|
-
def adjust_date_range(week_pattern,start_date,finish_date)
|
318
|
+
def adjust_date_range(week_pattern, start_date, finish_date)
|
291
319
|
week_pattern.start = start_date
|
292
320
|
week_pattern.finish = finish_date
|
293
321
|
end
|
322
|
+
|
294
323
|
end
|
295
324
|
end
|
296
|
-
|