workpattern 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/CHANGELOG +4 -0
- data/Gemfile +4 -0
- data/README.md +71 -0
- data/Rakefile +11 -0
- data/config/website.yml +2 -0
- data/lib/workpattern/clock.rb +63 -0
- data/lib/workpattern/day.rb +259 -0
- data/lib/workpattern/hour.rb +166 -0
- data/lib/workpattern/utility/base.rb +14 -0
- data/lib/workpattern/version.rb +3 -0
- data/lib/workpattern/week.rb +261 -0
- data/lib/workpattern/workpattern.rb +236 -0
- data/lib/workpattern.rb +226 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/script/txt2html +71 -0
- data/test/test_clock.rb +31 -0
- data/test/test_day.rb +402 -0
- data/test/test_helper.rb +25 -0
- data/test/test_hour.rb +252 -0
- data/test/test_week.rb +236 -0
- data/test/test_workpattern.rb +260 -0
- data/test/test_workpattern_module.rb +93 -0
- data/workpattern.gemspec +24 -0
- metadata +72 -0
@@ -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
|
+
|