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