workpattern 0.3.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
-