workpattern 0.4.0 → 0.5.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.
- 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
|
-
|