workpattern 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ doc
2
+ pkg
data/CHANGELOG ADDED
@@ -0,0 +1,4 @@
1
+ ## Workpattern v0.2.0 (May 31, 2012) ##
2
+
3
+ * Rewritten from scratch effectively first version * Barrie Callender *
4
+ * Please discard any version of Workpattern before this - some poor souls may have come across v0.1.0. - sorry! * Barrie Callender *
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in workpattern.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,71 @@
1
+ ## What?
2
+
3
+ Simple addition and subtraction of minutes on dates taking account of real-life working and resting periods.
4
+
5
+ This gem has the potential to serve as the engine for scheduling algorithms that are the core of products such as Microsoft Project and Oracle Primavera P6 as well as other applications that need to know when they can perform work and when they can’t.
6
+
7
+ ## Install
8
+
9
+ `sudo gem install workpattern`
10
+
11
+ ## Getting Started
12
+
13
+ The first step is to create a **Workpattern** to hold all the working and resting times. I'll start in 2011 and let it run for 10 years.
14
+
15
+ ``` ruby
16
+ mywp=Workpattern.new('My Workpattern',2011,10)
17
+ ```
18
+
19
+ My **Workpattern** will be created as a 24 hour a day full working time. Now it has to be told about the resting periods. First the weekends.
20
+
21
+ ``` ruby
22
+ mywp.resting(:days => :weekend)
23
+ ```
24
+
25
+ then the days in the week have specific working and resting times using the *Workpattern.clock* method, although anything that responds to **hour** and **min** methods will do ...
26
+
27
+ ``` ruby
28
+ mywp.resting(:days =>:weekday, :from_time=>Workpattern.clock(0,0),:to_time=>Workpattern.clock(8,59))
29
+ mywp.resting(:days =>:weekday, :from_time=>Workpattern.clock(12,0),:to_time=>Workpattern.clock(12,59))
30
+ mywp.resting(:days =>:weekday, :from_time=>Workpattern.clock(18,0),:to_time=>Workpattern.clock(23,59))
31
+ ```
32
+
33
+ Now we have the working and resting periods setup we can just add 32 hours as minutes (1920) to our date.
34
+
35
+ ``` ruby
36
+ mydate=DateTime.civil(2011,9,1,9,0)
37
+ result_date = mywp.calc(mydate,1920) # => 6/9/11@18:00
38
+ ```
39
+
40
+ ## Development
41
+
42
+ * Source hosted on [GitHub](http://github.com/callenb/workpattern).
43
+ * Direct questions and discussions to the [mailing list](http://groups.google.com/group/workpattern).
44
+ * Report issues on [GitHub Issues](http://github.com/callenb/workpattern/issues).
45
+ * Pull requests are very welcome, however I have never participated in Open Source so will be a bit slow as I am learning. Please be patient with me. Please include spec and/or feature coverage for every patch, and create a topic branch for every separate change you make.
46
+ * Advice, such as pointing out how I should really code in Ruby will be gratefully received.
47
+
48
+ ## License
49
+
50
+ (The MIT License)
51
+
52
+ Copyright (c) 2012
53
+
54
+ Permission is hereby granted, free of charge, to any person obtaining
55
+ a copy of this software and associated documentation files (the
56
+ 'Software'), to deal in the Software without restriction, including
57
+ without limitation the rights to use, copy, modify, merge, publish,
58
+ distribute, sublicense, and/or sell copies of the Software, and to
59
+ permit persons to whom the Software is furnished to do so, subject to
60
+ the following conditions:
61
+
62
+ The above copyright notice and this permission notice shall be
63
+ included in all copies or substantial portions of the Software.
64
+
65
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
66
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
67
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
68
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
69
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
70
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
71
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ task :default => [:test]
5
+
6
+ desc "Run basic tests"
7
+ Rake::TestTask.new do |test|
8
+ test.libs << "test"
9
+ test.test_files = Dir["test/test_*.rb"]
10
+ test.verbose = true
11
+ end
@@ -0,0 +1,2 @@
1
+ host: callenb@rubyforge.org
2
+ remote_dir: /var/www/gforge-projects/workpattern
@@ -0,0 +1,63 @@
1
+ module Workpattern
2
+ # Represents time on a clock in hours and minutes.
3
+ #
4
+ # myClock=Clock.new(3,32)
5
+ # myClock.minutes #=> 212
6
+ # myClock.hour #=> 3
7
+ # myClock.min #=> 32
8
+ # myClock.time #=> Time.new(1963,6,10,3,32)
9
+ #
10
+ #
11
+ # aClock=Clock.new(27,80)
12
+ # aClock.minutes #=> 1700
13
+ # aClock.hour #=> 4
14
+ # aClock.min #=> 20
15
+ # aClock.time #=> Time.new(1963,6,10,4,20)
16
+ #
17
+ class Clock
18
+
19
+ # :call-seq: new(hour,min) => Clock
20
+ # initialises <tt>Clock</tt> using the hours and minutes supplied
21
+ # or 0 if they are absent. Although there are 24 hours in a day
22
+ # (0-23) and 60 minutes in an hour (0-59), <tt>Clock</tt> calculates
23
+ # the full hours and remaining minutes of whatever is supplied.
24
+ #
25
+ def initialize(hour=0,min=0)
26
+ @hour=hour
27
+ @min=min
28
+ total_minutes = minutes
29
+ @hour=total_minutes.div(60)
30
+ @min=total_minutes % 60
31
+ end
32
+
33
+ # :call-seq: minutes => Integer
34
+ # returns the total number of minutes
35
+ #
36
+ def minutes
37
+ return (@hour*60)+@min
38
+ end
39
+
40
+ # :call-seq: hour => Integer
41
+ # returns the hour of the clock (0-23)
42
+ #
43
+ def hour
44
+ return @hour % 24
45
+ end
46
+
47
+ # :call-seq: min => Integer
48
+ # returns the minute of the clock (0-59)
49
+ #
50
+ def min
51
+ return @min % 60
52
+ end
53
+
54
+ # :call-seq: time => DateTime
55
+ # returns a <tt>Time</tt> object with the correct
56
+ # <tt>hour</tt> and <tt>min</tt> values. The date
57
+ # is 10th June 1963
58
+ #
59
+ def time
60
+ return DateTime.new(1963,6,10,hour,min)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,259 @@
1
+ module Workpattern
2
+ # Represents the 24 hours of a day using module <tt>hour</tt>
3
+ #
4
+ class Day
5
+ include Workpattern::Utility
6
+ attr_accessor :values, :hours, :first_hour, :first_min, :last_hour, :last_min, :total
7
+
8
+ # :call-seq: new(type=1) => Day
9
+ # Creates a 24 hour day defaulting to a working day.
10
+ # Pass 0 to create a non-working day.
11
+ #
12
+ def initialize(type=1)
13
+ @hours=24
14
+ hour=WORKING_HOUR if type==1
15
+ hour=RESTING_HOUR if type==0
16
+ @values=Array.new(@hours) {|index| hour }
17
+
18
+ set_attributes
19
+ end
20
+
21
+ # :call-seq: duplicate => Day
22
+ # Creates a duplicate of the current <tt>Day</tt> instance.
23
+ #
24
+ def duplicate
25
+ duplicate_day = Day.new()
26
+ duplicate_values=Array.new(@values.size)
27
+ @values.each_index {|index|
28
+ duplicate_values[index]=@values[index]
29
+ }
30
+ duplicate_day.values=duplicate_values
31
+ duplicate_day.hours = @hours
32
+ duplicate_day.first_hour=@first_hour
33
+ duplicate_day.first_min=@first_min
34
+ duplicate_day.last_hour=@last_hour
35
+ duplicate_day.last_min=@last_min
36
+ duplicate_day.total = @total
37
+ duplicate_day.refresh
38
+ return duplicate_day
39
+ end
40
+
41
+ # :call-seq: refresh
42
+ # Recalculates characteristics for this day
43
+ #
44
+ def refresh
45
+ set_attributes
46
+ end
47
+
48
+ # :call-seq: workpattern(start_time,finish_time,type)
49
+ # Sets all minutes in a date range to be working or resting.
50
+ # The <tt>start_time</tt> and <tt>finish_time</tt> need to have
51
+ # <tt>#hour</tt> and <tt>#min</tt> methods to return the time
52
+ # in hours and minutes respectively.
53
+ #
54
+ # Pass 1 as the <tt>type</tt> for working and 0 for resting.
55
+ #
56
+ def workpattern(start_time,finish_time,type)
57
+
58
+ if start_time.hour==finish_time.hour
59
+ @values[start_time.hour]=@values[start_time.hour].workpattern(start_time.min,finish_time.min,type)
60
+ else
61
+ test_hour=start_time.hour
62
+ @values[test_hour]=@values[test_hour].workpattern(start_time.min,59,type)
63
+
64
+ while ((test_hour+1)<finish_time.hour)
65
+ test_hour+=1
66
+ @values[test_hour]=@values[test_hour].workpattern(0,59,type)
67
+ end
68
+
69
+ @values[finish_time.hour]=@values[finish_time.hour].workpattern(0,finish_time.min,type)
70
+ end
71
+ set_attributes
72
+ end
73
+
74
+ # :call-seq: calc(time,duration) => time, duration
75
+ #
76
+ # Calculates the result of adding <tt>duration</tt> to
77
+ # <tt>time</tt>. The <tt>duration</tt> can be negative in
78
+ # which case it subtracts from <tt>time</tt>.
79
+ #
80
+ # An addition where there are less working minutes left in
81
+ # the day than are being added will result in the time
82
+ # returned having 60 as the value in <tt>min</tt>.
83
+ #
84
+ def calc(time,duration,midnight=false)
85
+
86
+ if (duration<0)
87
+ return subtract(time,duration, midnight)
88
+ elsif (duration>0)
89
+ return add(time,duration)
90
+ else
91
+ return time,duration, false
92
+ end
93
+
94
+ end
95
+
96
+ # :call-seq: working?(start) => Boolean
97
+ # Returns true if the given minute is working and false if it isn't
98
+ #
99
+ def working?(start)
100
+ return true if minutes(start.hour,start.min,start.hour,start.min)==1
101
+ return false
102
+ end
103
+
104
+ # :call-seq: diff(start,finish) => Duration, Date
105
+ # Returns the difference in minutes between two times. if the given
106
+ # minute is working and false if it isn't
107
+ #
108
+ def diff(start,finish)
109
+ start,finish=finish,start if ((start <=> finish))==1
110
+ # calculate to end of hour
111
+ #
112
+ if (start.jd==finish.jd) # same day
113
+ duration=minutes(start.hour,start.min,finish.hour, finish.min)
114
+ duration -=1 if working?(finish)
115
+ start=finish
116
+ else
117
+ duration=minutes(start.hour,start.min,23, 59)
118
+ start=start+((23-start.hour)*HOUR) +((60-start.min)*MINUTE)
119
+ end
120
+ return duration, start
121
+ end
122
+
123
+ # :call-seq: minutes(start_hour,start_min,finish_hour,finish_min) => duration
124
+ # Returns the total number of minutes between and including two minutes.
125
+ #
126
+ def minutes(start_hour,start_min,finish_hour,finish_min)
127
+ return 0 if (start_hour==finish_hour && start_hour==0 && start_min==finish_min && start_min==0)
128
+ if (start_hour > finish_hour) || ((finish_hour==start_hour) && (start_min > finish_min))
129
+ start_hour,start_min,finish_hour,finish_min=finish_hour,finish_min,start_hour,finish_min
130
+ end
131
+
132
+ if (start_hour==finish_hour)
133
+ retval=@values[start_hour].minutes(start_min,finish_min)
134
+ else
135
+
136
+ retval=@values[start_hour].minutes(start_min,59)
137
+ while (start_hour+1<finish_hour)
138
+ retval+=@values[start_hour+1].total
139
+ start_hour+=1
140
+ end
141
+ retval+=@values[finish_hour].minutes(0,finish_min)
142
+ end
143
+
144
+ return retval
145
+ end
146
+
147
+ private
148
+
149
+ def set_attributes
150
+ @first_hour=nil
151
+ @first_min=nil
152
+ @last_hour=nil
153
+ @last_min=nil
154
+ @total=0
155
+ 0.upto(@hours-1) {|index|
156
+ @first_hour=index if ((@first_hour.nil?) && (@values[index].total!=0))
157
+ @first_min=@values[index].first if ((@first_min.nil?) && (!@values[index].first.nil?))
158
+ @last_hour=index if (@values[index].total!=0)
159
+ @last_min=@values[index].last if (@values[index].total!=0)
160
+ @total+=@values[index].total
161
+ }
162
+ end
163
+
164
+ def first_working_minute(time)
165
+ if @first_hour.nil?
166
+ return time - (HOUR*time.hour) - (MINUTE*time.min)
167
+ else
168
+ time = time - HOUR * (time.hour - @first_hour)
169
+ time = time - MINUTE * (time.min - @first_min )
170
+ return time
171
+ end
172
+ end
173
+
174
+ def subtract(time,duration,midnight=false)
175
+ if (time.hour==0 && time.min==0)
176
+ if midnight
177
+ duration+=minutes(23,59,23,59)
178
+ time=time+(HOUR*23)+(MINUTE*59)
179
+ return calc(time,duration)
180
+ else
181
+ return time.prev_day, duration,true
182
+ end
183
+ elsif (time.hour==@first_hour && time.min==@first_min)
184
+ time=time-(HOUR*@first_hour) - (MINUTE*@first_min)
185
+ return time.prev_day, duration, true
186
+ elsif (time.min>0)
187
+ available_minutes=minutes(0,0,time.hour,time.min-1)
188
+ else
189
+ available_minutes=minutes(0,0,time.hour-1,59)
190
+ end
191
+ if ((duration+available_minutes)<0) # not enough minutes in the day
192
+ time = midnight_before(time.prev_day)
193
+ duration = duration + available_minutes
194
+ return time, duration, true
195
+ elsif ((duration+available_minutes)==0)
196
+ duration=0
197
+ time=first_working_minute(time)
198
+ else
199
+ minutes_this_hour=@values[time.hour].minutes(0,time.min-1)
200
+ this_hour=time.hour
201
+ until (duration==0)
202
+ if (minutes_this_hour<duration.abs)
203
+ duration+=minutes_this_hour
204
+ time = time - (MINUTE*time.min) - HOUR
205
+ this_hour-=1
206
+ minutes_this_hour=@values[this_hour].total
207
+ else
208
+ next_hour=(time.min==0)
209
+ time,duration=@values[this_hour].calc(time,duration, next_hour)
210
+ end
211
+ end
212
+ end
213
+ return time,duration, false
214
+ end
215
+
216
+ #
217
+ # Returns the result of adding #duration to the specified time
218
+ # When there are not enough minutes in the day it returns the date
219
+ # for the start of the following
220
+ #
221
+ def add(time,duration)
222
+ available_minutes=minutes(time.hour,time.min,@hours-1,59)
223
+ if ((duration-available_minutes)>0) # not enough minutes left in the day
224
+ result_date= time.next_day - (HOUR*time.hour) - (MINUTE*time.min)
225
+ duration = duration - available_minutes
226
+ else
227
+ total=@values[time.hour].minutes(time.min,59)
228
+ if (total==duration) # this hour satisfies
229
+ result_date=time + HOUR - (MINUTE*time.min)
230
+ duration = 0
231
+ else
232
+ result_date = time
233
+ until (duration==0)
234
+ if (total<duration)
235
+ duration-=total
236
+ result_date=result_date + HOUR - (MINUTE*result_date.min)
237
+ else
238
+ result_date,duration=@values[result_date.hour].calc(result_date,duration)
239
+ end
240
+ total=@values[result_date.hour].total
241
+ end
242
+ end
243
+ end
244
+ return result_date,duration, false
245
+ end
246
+
247
+ def next_hour(start)
248
+ return start+HOUR-(start.min*MINUTE)
249
+ end
250
+
251
+ def minutes_left_in_hour(start)
252
+ return @values[start.hour].diff(start.min,60)
253
+ end
254
+
255
+ def minutes_left_in_day(start)
256
+ start.hour
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,166 @@
1
+ module Workpattern
2
+
3
+ # Represents the 60 minutes of an hour using a <tt>Fixnum</tt> or <tt>Bignum</tt>
4
+ #
5
+ module Hour
6
+
7
+ # :call-seq: total => Integer
8
+ # Returns the total working minutes in the hour
9
+ #
10
+ def total
11
+ return minutes(0,59)
12
+ end
13
+
14
+ # :call-seq: workpattern(start,finish,type) => Fixnum
15
+ # Sets the minutes to either working (type=1) or resting (type=0)
16
+ #
17
+ def workpattern(start,finish,type)
18
+ return working(start,finish) if type==1
19
+ return resting(start,finish) if type==0
20
+ end
21
+
22
+ # :call-seq: first => Integer
23
+ # Returns the first working minute in the hour or 60 if there are none
24
+ #
25
+ def first
26
+ 0.upto(59) {|minute| return minute if self.minutes(minute,minute)==1}
27
+ return nil
28
+ end
29
+
30
+ # :call-seq: last => Integer
31
+ # Returns the last working minute in the hour or -1 if there are none
32
+ #
33
+ def last
34
+ 59.downto(0) {|minute| return minute if self.minutes(minute,minute)==1}
35
+ return nil
36
+ end
37
+
38
+ # :call-seq: working?(start) => Boolean
39
+ # Returns true if the given minute is working and false if it isn't
40
+ #
41
+ def working?(start)
42
+ return true if minutes(start,start)==1
43
+ return false
44
+ end
45
+
46
+ # :call-seq: minutes(start,finish) => Integer
47
+ # Returns the total number of minutes between and including two minutes
48
+ #
49
+ def minutes(start,finish)
50
+ start,finish=finish,start if start > finish
51
+ return (self & mask(start,finish)).to_s(2).count('1')
52
+ end
53
+
54
+ # :call-seq: calc(datetime,duration) => DateTime, Integer
55
+ # Returns the DateTime and remainding minutes when adding or subtracting duration
56
+ # to/from a minute in an hour.
57
+ # Subtraction with a remainder returns the time of the current date as 00:00.
58
+ #
59
+ def calc(time,duration,next_hour=false)
60
+
61
+ if (duration<0)
62
+ return subtract(time,duration, next_hour)
63
+ elsif (duration>0)
64
+ return add(time,duration)
65
+ else
66
+ return time,duration
67
+ end
68
+ end
69
+
70
+ # :call-seq: diff(start,finish) => Integer
71
+ # returns the number of minutes between two minutes
72
+ #
73
+ def diff(start,finish)
74
+ start,finish=finish,start if start > finish
75
+ return 0 if start==finish
76
+ return (self & mask(start,finish-1)).to_s(2).count('1')
77
+ end
78
+
79
+ private
80
+
81
+ # sets working pattern
82
+ def working(start,finish)
83
+ return self | mask(start,finish)
84
+ end
85
+
86
+ # sets resting pattern
87
+ def resting(start,finish)
88
+ return self & ((2**60-1)-mask(start,finish))
89
+ end
90
+
91
+ # creates a mask over the specified bits
92
+ def mask(start,finish)
93
+ return ((2**(finish+1)-1)-(2**start-1))
94
+ end
95
+
96
+ # adds a duration to a time
97
+ def add(time,duration)
98
+ start = time.min
99
+ available_minutes=minutes(start,59)
100
+
101
+ if ((duration-available_minutes)>=0)
102
+ result_date = time + HOUR - (MINUTE*start)
103
+ result_remainder = duration-available_minutes
104
+ elsif ((duration-available_minutes)==0)
105
+ result_date = time - (MINUTE*start) + last + 1
106
+ result_remainder = 0
107
+ elsif (minutes(start,start+duration-1)==duration)
108
+ result_date = time + (MINUTE*duration)
109
+ result_remainder = 0
110
+ else
111
+ step = start + duration
112
+ duration-=minutes(start,step)
113
+ until (duration==0)
114
+ step+=1
115
+ duration-=minutes(step,step)
116
+ end
117
+ step+=1
118
+ result_date = time + (MINUTE*step)
119
+ result_remainder = 0
120
+ end
121
+ return result_date, result_remainder
122
+ end
123
+
124
+ # subtracts a duration from a time
125
+ def subtract(time,duration,next_hour)
126
+ if next_hour
127
+ if working?(59)
128
+ duration+=1
129
+ time=time+(MINUTE*59)
130
+ return calc(time,duration)
131
+ end
132
+ else
133
+ start=time.min
134
+ available_minutes=0
135
+ available_minutes = minutes(0,start-1) if start > 0
136
+ end
137
+
138
+ if ((duration + available_minutes)<=0)
139
+ result_date = time - (MINUTE*start)
140
+ result_remainder = duration+available_minutes
141
+ elsif (minutes(start+duration,start-1)==duration.abs)
142
+ result_date = time + (MINUTE*duration)
143
+ result_remainder = 0
144
+ else
145
+ step = start + duration
146
+ duration+=minutes(step,start-1)
147
+ until (duration==0)
148
+ step-=1
149
+ duration+=minutes(step,step)
150
+ end
151
+ result_date = time - (MINUTE * (start-step))
152
+ result_remainder = 0
153
+ end
154
+ return result_date, result_remainder
155
+
156
+ end
157
+ end
158
+ end
159
+
160
+ class Fixnum
161
+ include Workpattern::Hour
162
+ end
163
+ class Bignum
164
+ include Workpattern::Hour
165
+ end
166
+
@@ -0,0 +1,14 @@
1
+ module Workpattern
2
+
3
+ module Utility
4
+
5
+ def midnight_before(adate)
6
+ return adate -(HOUR * adate.hour) - (MINUTE * adate.min)
7
+ end
8
+
9
+ def midnight_after(adate)
10
+ return midnight_before(adate.next_day)
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module Workpattern
2
+ VERSION = '0.2.0'
3
+ end