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.
- 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
|
+
|