workpattern 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,261 @@
1
+ module Workpattern
2
+
3
+ # Represents working and resting times of each day in a week from one date to another.
4
+ # The two dates could be the same day or they could be several weeks or years apart.
5
+ #
6
+ class Week
7
+
8
+ attr_accessor :values, :days, :start, :finish, :week_total, :total
9
+
10
+ # :call-seq: new(start,finish,type) => Week
11
+ #
12
+ #
13
+ def initialize(start,finish,type=1)
14
+ hours_in_days_in_week=[24,24,24,24,24,24,24]
15
+ @days=hours_in_days_in_week.size
16
+ @values=Array.new(7) {|index| Day.new(type)}
17
+ @start=DateTime.new(start.year,start.month,start.day)
18
+ @finish=DateTime.new(finish.year,finish.month,finish.day)
19
+
20
+ set_attributes
21
+ end
22
+
23
+ def duplicate()
24
+ duplicate_week=Week.new(@start,@finish)
25
+ duplicate_values=Array.new(@values.size)
26
+ @values.each_index {|index|
27
+ duplicate_values[index]=@values[index].duplicate
28
+ }
29
+ duplicate_week.values=duplicate_values
30
+ duplicate_week.days=@days
31
+ duplicate_week.start=@start
32
+ duplicate_week.finish=@finish
33
+ duplicate_week.week_total=@week_total
34
+ duplicate_week.total=@total
35
+ duplicate_week.refresh
36
+ return duplicate_week
37
+ end
38
+
39
+ def refresh
40
+ set_attributes
41
+ end
42
+
43
+ def adjust(start,finish)
44
+ @start=DateTime.new(start.year,start.month,start.day)
45
+ @finish=DateTime.new(finish.year,finish.month,finish.day)
46
+ refresh
47
+ end
48
+
49
+ def workpattern(days,from_time,to_time,type)
50
+ DAYNAMES[days].each {|day| @values[day].workpattern(from_time,to_time,type)}
51
+ refresh
52
+ end
53
+
54
+ def calc(start,duration, midnight=false)
55
+ return start,duration if duration==0
56
+ return add(start,duration) if duration > 0
57
+ return subtract(start,duration, midnight) if duration <0
58
+ end
59
+
60
+ def <=>(obj)
61
+ if @start < obj.start
62
+ return -1
63
+ elsif @start == obj.start
64
+ return 0
65
+ else
66
+ return 1
67
+ end
68
+ end
69
+
70
+ # :call-seq: working?(start) => Boolean
71
+ # Returns true if the given minute is working and false if it isn't
72
+ #
73
+ def working?(start)
74
+ @values[start.wday].working?(start)
75
+ end
76
+
77
+
78
+ # :call-seq: diff(start,finish) => Duration, Date
79
+ # Returns the difference in minutes between two times.
80
+ #
81
+ def diff(start,finish)
82
+ start,finish=finish,start if ((start <=> finish))==1
83
+ # calculate to end of day
84
+ #
85
+ if (start.jd==finish.jd) # same day
86
+ duration, start=@values[start.wday].diff(start,finish)
87
+ elsif (finish.jd<=@finish.jd) #within this week
88
+ duration, start=diff_detail(start,finish,finish)
89
+ else # after this week
90
+ duration, start=diff_detail(start,finish,@finish)
91
+ end
92
+ return duration, start
93
+ end
94
+
95
+ private
96
+
97
+ def set_attributes
98
+ @total=0
99
+ @week_total=0
100
+ days=(@finish-@start).to_i + 1 #/60/60/24+1
101
+ if (7-@start.wday) < days and days < 8
102
+ @total+=total_hours(@start.wday,@finish.wday)
103
+ @week_total=@total
104
+ else
105
+ @total+=total_hours(@start.wday,6)
106
+ days -= (7-@start.wday)
107
+ @total+=total_hours(0,@finish.wday)
108
+ days-=(@finish.wday+1)
109
+ @week_total=@total if days==0
110
+ week_total=total_hours(0,6)
111
+ @total+=week_total * days / 7
112
+ @week_total=week_total if days != 0
113
+ end
114
+ end
115
+
116
+ def total_hours(start,finish)
117
+ total=0
118
+ start.upto(finish) {|day|
119
+ total+=@values[day].total
120
+ }
121
+ return total
122
+ end
123
+
124
+ def add(start,duration)
125
+ # aim to calculate to the end of the day
126
+ start,duration = @values[start.wday].calc(start,duration)
127
+ return start,duration if (duration==0) || (start.jd > @finish.jd)
128
+ # aim to calculate to the end of the next week day that is the same as @finish
129
+ while((duration!=0) && (start.wday!=@finish.next_day.wday) && (start.jd <= @finish.jd))
130
+ if (duration>@values[start.wday].total)
131
+ duration = duration - @values[start.wday].total
132
+ start=start.next_day
133
+ elsif (duration==@values[start.wday].total)
134
+ start=after_last_work(start)
135
+ duration = 0
136
+ else
137
+ start,duration = @values[start.wday].calc(start,duration)
138
+ end
139
+ end
140
+
141
+ return start,duration if (duration==0) || (start.jd > @finish.jd)
142
+
143
+ #while duration accomodates full weeks
144
+ while ((duration!=0) && (duration>=@week_total) && ((start.jd+6) <= @finish.jd))
145
+ duration=duration - @week_total
146
+ start=start+7
147
+ end
148
+
149
+ return start,duration if (duration==0) || (start.jd > @finish.jd)
150
+
151
+ #while duration accomodates full days
152
+ while ((duration!=0) && (start.jd<= @finish.jd))
153
+ if (duration>@values[start.wday].total)
154
+ duration = duration - @values[start.wday].total
155
+ start=start.next_day
156
+ else
157
+ start,duration = @values[start.wday].calc(start,duration)
158
+ end
159
+ end
160
+ return start, duration
161
+
162
+ end
163
+
164
+ def subtract(start,duration,midnight=false)
165
+
166
+ # Handle subtraction from start of day
167
+ if midnight
168
+ start,duration=minute_b4_midnight(start,duration)
169
+ midnight=false
170
+ end
171
+
172
+ # aim to calculate to the start of the day
173
+ start,duration, midnight = @values[start.wday].calc(start,duration)
174
+
175
+ if midnight && (start.jd >= @start.jd)
176
+ start,duration=minute_b4_midnight(start,duration)
177
+ return subtract(start,duration, false)
178
+ elsif midnight
179
+ return start,duration,midnight
180
+ elsif (duration==0) || (start.jd ==@start.jd)
181
+ return start,duration, midnight
182
+ end
183
+
184
+ # aim to calculate to the start of the previous week day that is the same as @start
185
+ while((duration!=0) && (start.wday!=@start.wday) && (start.jd >= @start.jd))
186
+
187
+ if (duration.abs>=@values[start.wday].total)
188
+ duration = duration + @values[start.wday].total
189
+ start=start.prev_day
190
+ else
191
+ start,duration=minute_b4_midnight(start,duration)
192
+ start,duration = @values[start.wday].calc(start,duration)
193
+ end
194
+ end
195
+
196
+ return start,duration if (duration==0) || (start.jd ==@start.jd)
197
+
198
+ #while duration accomodates full weeks
199
+ while ((duration!=0) && (duration.abs>=@week_total) && ((start.jd-6) >= @start.jd))
200
+ duration=duration + @week_total
201
+ start=start-7
202
+ end
203
+
204
+ return start,duration if (duration==0) || (start.jd ==@start.jd)
205
+
206
+ #while duration accomodates full days
207
+ while ((duration!=0) && (start.jd>= @start.jd))
208
+ if (duration.abs>=@values[start.wday].total)
209
+ duration = duration + @values[start.wday].total
210
+ start=start.prev_day
211
+ else
212
+ start,duration=minute_b4_midnight(start,duration)
213
+ start,duration = @values[start.wday].calc(start,duration)
214
+ end
215
+ end
216
+
217
+ return start, duration , midnight
218
+
219
+ end
220
+
221
+ def minute_b4_midnight(start,duration)
222
+ duration += @values[start.wday].minutes(23,59,23,59)
223
+ start = start.next_day - MINUTE
224
+ return start,duration
225
+ end
226
+
227
+ def after_last_work(start)
228
+ if @values[start.wday].last_hour.nil?
229
+ return start.next_day
230
+ else
231
+ start = start + HOUR * (@values[start.wday].last_hour - start.hour)
232
+ start = start + MINUTE * (@values[start.wday].last_min - start.min + 1)
233
+ return start
234
+ end
235
+ end
236
+
237
+ def diff_detail(start,finish,finish_on)
238
+ duration, start=@values[start.wday].diff(start,finish)
239
+ #rest of week to finish day
240
+ while (start.wday<finish.wday) do
241
+ duration+=@values[start.wday].total
242
+ start=start.next_day
243
+ end
244
+ #weeks
245
+ while (start.jd+7<finish_on.jd) do
246
+ duration+=@week_total
247
+ start+=7
248
+ end
249
+ #days
250
+ while (start.jd < finish_on.jd) do
251
+ duration+=@values[start.wday].total
252
+ start=start.next_day
253
+ end
254
+ #day
255
+ day_duration, start=@values[start.wday].diff(start,finish)
256
+ duration+=day_duration
257
+ return duration, start
258
+ end
259
+
260
+ end
261
+ end
@@ -0,0 +1,236 @@
1
+
2
+
3
+ module Workpattern
4
+ require 'set'
5
+
6
+ # Represents the working and resting periods across a number of whole years. The #base year
7
+ # is the first year and the #span is the number of years including that year that is covered.
8
+ # The #Workpattern is given a unique name so it can be easily identified amongst other Workpatterns.
9
+ #
10
+ class Workpattern
11
+
12
+ # Holds collection of <tt>Workpattern</tt> objects
13
+ @@workpatterns = Hash.new()
14
+ attr_accessor :name, :base, :span, :from, :to, :weeks
15
+
16
+ def initialize(name=DEFAULT_NAME,base_year=DEFAULT_BASE_YEAR,span=DEFAULT_SPAN)
17
+
18
+ raise(NameError, "Workpattern '#{name}' already exists and can't be created again") if @@workpatterns.key?(name)
19
+
20
+ if span < 0
21
+ offset = span.abs - 1
22
+ else
23
+ offset = 0
24
+ end
25
+
26
+ @name = name
27
+ @base = base_year
28
+ @span = span
29
+ @from = DateTime.new(base_year.abs - offset)
30
+ @to = DateTime.new(@from.year + span.abs - 1,12,31,23,59)
31
+ @weeks = SortedSet.new
32
+ @weeks << Week.new(@from,@to,1)
33
+
34
+
35
+ @@workpatterns[name]=self
36
+ end
37
+
38
+ def self.clear
39
+ @@workpatterns.clear
40
+ end
41
+
42
+ def self.to_a
43
+ @@workpatterns.to_a
44
+ end
45
+
46
+ def self.get(name)
47
+ return @@workpatterns[name] if @@workpatterns.key?(name)
48
+ raise(NameError, "Workpattern '#{name}' doesn't exist so can't be retrieved")
49
+ end
50
+
51
+ def self.delete(name)
52
+ if @@workpatterns.delete(name).nil?
53
+ return false
54
+ else
55
+ return true
56
+ end
57
+ end
58
+
59
+ # Sets a work or resting pattern in the _Workpattern_.
60
+ #
61
+ # Can also use <tt>resting</tt> and <tt>working</tt> methods leaving off the
62
+ # :work_type
63
+ #
64
+ # === Parameters
65
+ #
66
+ # * <tt>:start</tt> - The first date to apply the pattern. Defaults
67
+ # to the _Workpattern_ <tt>start</tt>.
68
+ # * <tt>:finish</tt> - The last date to apply the pattern. Defaults to
69
+ # the _Workpattern_ <tt>finish</tt>.
70
+ # * <tt>:days</tt> - The specific day or days the pattern will apply to. This
71
+ # references _Workpattern::DAYNAMES_. It defailts to <tt>:all</tt> which is
72
+ # everyday. Valid values are <tt>:sun, :mon, :tue, :wed, :thu, :fri, :sat,
73
+ # :weekend, :weekday</tt> and <tt>:all</tt>
74
+ # * <tt>:start_time</tt> - The first time in the selected days to apply the pattern.
75
+ # Must implement #hour and #min to get the Hours and Minutes for the time. It will default to
76
+ # the first time in the day <tt>00:00</tt>.
77
+ # * <tt>:finish_time</tt> - The last time in the selected days to apply the pattern.
78
+ # Must implement #hour and #min to get the Hours and Minutes for the time. It will default to
79
+ # to the last time in the day <tt>23:59</tt>.
80
+ # * <tt>:work_type</tt> - type of pattern is either working (1 or <tt>Workpattern::WORK</tt>) or
81
+ # resting (0 or <tt>Workpattern::REST</tt>). Alternatively make use of the <tt>working</tt>
82
+ # or <tt>resting</tt> methods that will set this value for you
83
+ #
84
+ def workpattern(args={})
85
+
86
+ #
87
+ upd_start = args[:start] || @from
88
+ upd_start = dmy_date(upd_start)
89
+ args[:start] = upd_start
90
+
91
+ upd_finish = args[:finish] || @to
92
+ upd_finish = dmy_date(upd_finish)
93
+ args[:finish] = upd_finish
94
+
95
+ #args[:days] = args[:days] || :all
96
+ days= args[:days] || :all
97
+ from_time = args[:from_time] || FIRST_TIME_IN_DAY
98
+ from_time = hhmn_date(from_time)
99
+ #args[:from_time] = upd_from_time
100
+
101
+ to_time = args[:to_time] || LAST_TIME_IN_DAY
102
+ to_time = hhmn_date(to_time)
103
+ #args[:to_time] = upd_to_time
104
+
105
+ args[:work_type] = args[:work_type] || WORK
106
+ type= args[:work_type] || WORK
107
+
108
+ while (upd_start <= upd_finish)
109
+
110
+ current_wp=find_weekpattern(upd_start)
111
+ if (current_wp.start == upd_start)
112
+ if (current_wp.finish > upd_finish)
113
+ clone_wp=current_wp.duplicate
114
+ current_wp.adjust(upd_finish+1,current_wp.finish)
115
+ clone_wp.adjust(upd_start,upd_finish)
116
+ clone_wp.workpattern(days,from_time,to_time,type)
117
+ @weeks<< clone_wp
118
+ upd_start=upd_finish+1
119
+ else # (current_wp.finish == upd_finish)
120
+ current_wp.workpattern(days,from_time,to_time,type)
121
+ upd_start=current_wp.finish + 1
122
+ end
123
+ else
124
+ clone_wp=current_wp.duplicate
125
+ current_wp.adjust(current_wp.start,upd_start-1)
126
+ clone_wp.adjust(upd_start,clone_wp.finish)
127
+ if (clone_wp.finish <= upd_finish)
128
+ clone_wp.workpattern(days,from_time,to_time,type)
129
+ @weeks<< clone_wp
130
+ upd_start=clone_wp.finish+1
131
+ else
132
+ after_wp=clone_wp.duplicate
133
+ after_wp.adjust(upd_finish+1,after_wp.finish)
134
+ @weeks<< after_wp
135
+ clone_wp.adjust(upd_start,upd_finish)
136
+ clone_wp.workpattern(days,from_time,to_time,type)
137
+ @weeks<< clone_wp
138
+ upd_start=clone_wp.finish+1
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ # Identical to the <tt>workpattern</tt> method apart from it always creates
145
+ # resting patterns so there is no need to set the <tt>:work_type</tt> argument
146
+ #
147
+ def resting(args={})
148
+ args[:work_type]=REST
149
+ workpattern(args)
150
+ end
151
+
152
+ # Identical to the <tt>workpattern</tt> method apart from it always creates
153
+ # working patterns so there is no need to set the <tt>:work_type</tt> argument
154
+ #
155
+ def working(args={})
156
+ args[:work_type]=WORK
157
+ workpattern(args)
158
+ end
159
+
160
+ # :call-seq: calc(start,duration) => DateTime
161
+ # Calculates the resulting date when #duration is added to #start date using the #Workpattern.
162
+ # Duration is always in whole minutes and can be a negative number, in which case it subtracts
163
+ # the minutes from the date.
164
+ #
165
+ def calc(start,duration)
166
+ return start if duration==0
167
+ midnight=false
168
+
169
+ while (duration !=0)
170
+ week=find_weekpattern(start)
171
+ if (week.start==start) && (duration<0) && (!midnight)
172
+ start=start.prev_day
173
+ week=find_weekpattern(start)
174
+ midnight=true
175
+ end
176
+
177
+ start,duration,midnight=week.calc(start,duration,midnight)
178
+ end
179
+
180
+ return start
181
+ end
182
+
183
+ # :call-seq: working?(start) => Boolean
184
+ # Returns true if the given minute is working and false if it isn't
185
+ #
186
+ def working?(start)
187
+ return find_weekpattern(start).working?(start)
188
+ end
189
+
190
+ # :call-seq: diff(start,finish) => Duration
191
+ # Returns number of minutes between two dates
192
+ #
193
+ def diff(start,finish)
194
+
195
+ start,finish=finish,start if finish<start
196
+ duration=0
197
+ while(start!=finish) do
198
+ week=find_weekpattern(start)
199
+ result_duration,start=week.diff(start,finish)
200
+ duration+=result_duration
201
+ end
202
+ return duration
203
+ end
204
+ private
205
+
206
+ # Retrieve the correct pattern for the supplied date
207
+ #
208
+ def find_weekpattern(date)
209
+ # find the pattern that fits the date
210
+ # TODO: What if there is no pattern?
211
+ #
212
+ if date<@from
213
+ result = Week.new(DateTime.jd(0),@from-MINUTE,1)
214
+ elsif date>@to
215
+ result = Week.new(@to+MINUTE,DateTime.new(9999),1)
216
+ else
217
+
218
+ date = DateTime.new(date.year,date.month,date.day)
219
+
220
+ result=@weeks.find {|week| week.start <= date and week.finish >= date}
221
+ end
222
+ return result
223
+ end
224
+
225
+
226
+ def dmy_date(date)
227
+ return DateTime.new(date.year,date.month,date.day)
228
+ end
229
+
230
+ def hhmn_date(date)
231
+ return DateTime.new(2000,1,1,date.hour,date.min)
232
+ end
233
+
234
+ end
235
+ end
236
+