workpattern 0.2.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,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
+