working-time 0.3.4

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/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Carl Hicks
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,17 @@
1
+ = working-time
2
+
3
+ Description goes here.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2010 Carl Hicks. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "working-time"
8
+ gem.summary = %Q{Calculate work hours for a given date range}
9
+ gem.description = %Q{Need to figure out how many working hours are in a date range? Need to calculate around holidays, on-call schedules, etc? Use this gem!}
10
+ gem.email = "carl.hicks@gmail.com"
11
+ gem.homepage = "http://github.com/chicks/working-time"
12
+ gem.authors = ["Carl Hicks"]
13
+ gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:test) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/test_*.rb'
25
+ test.verbose = true
26
+ end
27
+
28
+ begin
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |test|
31
+ test.libs << 'test'
32
+ test.pattern = 'test/**/test_*.rb'
33
+ test.verbose = true
34
+ end
35
+ rescue LoadError
36
+ task :rcov do
37
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
+ end
39
+ end
40
+
41
+ task :test => :check_dependencies
42
+
43
+ task :default => :test
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "working-time #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.4
@@ -0,0 +1,12 @@
1
+ module WorkingTime
2
+ # 8am to 6pm
3
+ WORKING_HOURS = (8..18).to_a
4
+
5
+ # Monday to Friday
6
+ WORKING_DAYS = (1..5).to_a
7
+
8
+ require 'date'
9
+ require 'working_time/date'
10
+ require 'working_time/interval'
11
+ end
12
+
@@ -0,0 +1,175 @@
1
+ module WorkingTime
2
+
3
+ class Hour
4
+ attr :value
5
+ def initialize(num)
6
+ @value = num
7
+ end
8
+ def to_i
9
+ @value
10
+ end
11
+ end
12
+
13
+ class Date < Date
14
+ def + (n)
15
+ case n
16
+ when Numeric;
17
+ new_date = self.class.new!(@ajd + n, @of, @sg)
18
+ if new_date.is_work_day?
19
+ return new_date
20
+ else
21
+ until new_date.is_work_day?
22
+ #puts "incrementing days! #{new_date}"
23
+ n += 1
24
+ new_date = self.class.new!(@ajd + n, @of, @sg)
25
+ end
26
+ return new_date
27
+ end
28
+ end
29
+ raise TypeError, 'expected numeric'
30
+ end
31
+
32
+ def is_work_day?
33
+ # check the day of week
34
+ WORKING_DAYS.include? self.cwday
35
+ end
36
+
37
+ end
38
+
39
+ class Time < Time
40
+
41
+ # def to_time() getlocal end
42
+
43
+ def to_date
44
+ jd = Date.civil_to_jd(year, mon, mday, Date::ITALY)
45
+ Date.new!(Date.jd_to_ajd(jd, 0, 0), 0, Date::ITALY)
46
+ end
47
+
48
+ def to_datetime
49
+ jd = WorkingTime::DateTime.civil_to_jd(year, mon, mday, DateTime::ITALY)
50
+ fr = WorkingTime::DateTime.time_to_day_fraction(hour, min, [sec, 59].min) +
51
+ usec.to_r/86400000000
52
+ of = utc_offset.to_r/86400
53
+ WorkingTime::DateTime.new!(WorkingTime::DateTime.jd_to_ajd(jd, fr, of), of, DateTime::ITALY)
54
+ end
55
+
56
+ private :to_date, :to_datetime
57
+
58
+ end
59
+
60
+ # Lets just modify the basic DateTime class
61
+ class DateTime < DateTime
62
+
63
+ def self.now (sg=ITALY) WorkingTime::Time.now.__send__(:to_datetime).new_start(sg) end
64
+
65
+ # Returns the number of hours left in the day.
66
+ def hours_left
67
+ remaining = WorkingTime::Interval.new(self, close_of_business)
68
+ return remaining.duration
69
+ end
70
+
71
+ def mins_left
72
+ hours_left * 60
73
+ end
74
+
75
+ def secs_left
76
+ hours_left * 3600
77
+ end
78
+
79
+ def open_of_business
80
+ self.class.new(self.year, self.mon, self.day, WORKING_HOURS[0], 0, 0, @of, @sg)
81
+ end
82
+
83
+ def close_of_business
84
+ self.class.new(self.year, self.mon, self.day, WORKING_HOURS[-1], 0, 0, @of, @sg)
85
+ end
86
+
87
+ def close_of_week
88
+ date = Date.new(self.year, self.mon, self.day)
89
+ date = date.next until date.cwday == WORKING_DAYS[-1]
90
+ self.class.new(date.year, date.mon, date.day, WORKING_HOURS[-1], 0, 0, @of, @sg)
91
+ end
92
+
93
+ def close_of_next_week
94
+ date = Date.commercial(year, cweek + 1, cwday)
95
+ date = date.next until date.cwday == WORKING_DAYS[-1]
96
+ self.class.new(date.year, date.mon, date.day, WORKING_HOURS[-1], 0, 0, @of, @sg)
97
+ end
98
+
99
+ def is_working_time?
100
+ # check the day of week
101
+ if WORKING_DAYS.include? cwday
102
+ #check the hour
103
+ if WORKING_HOURS.include? hour
104
+ return true
105
+ else
106
+ return false
107
+ end
108
+ else
109
+ return false
110
+ end
111
+ end
112
+
113
+ # override addition
114
+ def + (n)
115
+ case n
116
+ when Numeric; return self.class.new!(@ajd + n, @of, @sg)
117
+ when WorkingTime::Hour
118
+
119
+ cur_hour = self.hour
120
+ hours_remaining = n.to_i
121
+
122
+ min = self.min
123
+ sec = self.sec
124
+
125
+ date = Date.new(self.year,self.mon,self.day)
126
+
127
+ # if our initial value is before the start of the work day, set the cur_hour to the start of the workday
128
+ if cur_hour < WORKING_HOURS[0]
129
+ cur_hour = WORKING_HOURS[0]
130
+ # if our initial value is after the end of the current work day, set the cur_hour to the start of the NEXT workday
131
+ elsif cur_hour > WORKING_HOURS[-1]
132
+ cur_hour = WORKING_HOURS[0]
133
+ date = date.next
134
+ end
135
+
136
+ #puts "Incrementing #{self}"
137
+ # Add hours to the current hour, incrementing the day until we run out of hours
138
+ while hours_remaining > 0
139
+ until cur_hour == WORKING_HOURS[-1] || hours_remaining < 1
140
+ hours_remaining -= 1
141
+ cur_hour += 1
142
+ #puts "Cur: #{cur_hour} / Rem: #{hours_remaining}"
143
+ end
144
+ if hours_remaining > 0
145
+ hours_remaining -= 1
146
+ cur_hour = WORKING_HOURS[0]
147
+ # go to the next day
148
+ date = date.next
149
+ #puts "Rolling Date: #{date} #{cur_hour}"
150
+ end
151
+ end
152
+ return self.class.new(date.year, date.mon, date.day, cur_hour, min, sec, @of, @sg)
153
+ end
154
+ raise TypeError, 'expected numeric or hour'
155
+ end
156
+
157
+ def to_gm_time
158
+ to_time(new_offset, :gm)
159
+ end
160
+
161
+ def to_local_time
162
+ to_time(new_offset(DateTime.now.offset-offset), :local)
163
+ end
164
+
165
+ private
166
+ def to_time(dest, method)
167
+ #Convert a fraction of a day to a number of microseconds
168
+ usec = (dest.sec_fraction * 60 * 60 * 24 * (10**6)).to_i
169
+ Time.send(method, dest.year, dest.month, dest.day, dest.hour, dest.min, dest.sec, usec)
170
+ end
171
+
172
+
173
+ end
174
+
175
+ end
@@ -0,0 +1,97 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ module WorkingTime
4
+
5
+ class Interval
6
+
7
+ attr :start, true
8
+ attr :stop, true
9
+ attr :duration, true
10
+ attr :log, true
11
+
12
+ def initialize(start, stop)
13
+ @start = start
14
+ @stop = stop
15
+ @duration = hours_between
16
+ end
17
+
18
+ # Calculates the number of seconds in a work day between two times in the same day
19
+ def seconds_between(start, stop)
20
+
21
+ # we need this to be a float
22
+ duration = 0.0
23
+
24
+ # we just return if the start comes after the stop
25
+ return duration if start > stop
26
+
27
+ # If it's a weekday, examine it.
28
+ if WORKING_DAYS.include? start.wday
29
+
30
+ # If it's inside working hours, calculate
31
+ if WORKING_HOURS.include? start.hour
32
+
33
+ # Figure out how many seconds between now and then
34
+ hours, mins, secs, ignore_fractions = Date::day_fraction_to_time(stop - start)
35
+ duration = hours * 60 * 60 + mins * 60 + secs
36
+
37
+ end
38
+
39
+ end
40
+
41
+ return duration
42
+ end
43
+
44
+ def hours_between
45
+
46
+ #LOG.puts "Calculating: #{@start} to #{@stop}" if DEBUG
47
+
48
+ # Our placeholder in seconds
49
+ duration = 0.0
50
+
51
+ start_date = Date.new(@start.year, @start.mon, @start.day)
52
+ stop_date = Date.new(@stop.year, @stop.mon, @stop.day)
53
+
54
+ if start_date == stop_date
55
+
56
+ # If we closed it in the same day, we need to do a simple calculation
57
+ result = seconds_between(@start,@stop)
58
+ duration += result
59
+ #LOG.puts "Same day: #{@start} to #{@stop} (#{result / 3600})" if DEBUG
60
+
61
+ else
62
+
63
+ # Loop over each day
64
+ start_date.upto(stop_date) do |date|
65
+
66
+ #LOG.puts "Looking at: #{date}"
67
+ start_work_time = DateTime.new(date.year, date.mon, date.day, WORKING_HOURS[0])
68
+ stop_work_time = DateTime.new(date.year, date.mon, date.day, WORKING_HOURS[-1])
69
+
70
+ # If we are looking at the first day, we only want to count from the create time, to the end of the working day
71
+ if date == start_date
72
+ result = seconds_between(@start,stop_work_time)
73
+ duration += result
74
+ #LOG.puts "Start date: #{@start} to #{stop_work_time} (#{result / 3600})" if DEBUG
75
+ # If we are looking at the end date we only want to count from the beginning of the day to the close time
76
+ elsif date == stop_date
77
+ result = seconds_between(start_work_time,@stop)
78
+ duration += result
79
+ #LOG.puts "Stop date: #{start_work_time} to #{@stop} (#{result / 3600})" if DEBUG
80
+ # Otherwise we assume this date falls between start and end
81
+ else
82
+ result = seconds_between(start_work_time,stop_work_time)
83
+ duration += result
84
+ #LOG.puts "In-between date: #{start_work_time} to #{stop_work_time} (#{result / 3600 })" if DEBUG
85
+ end
86
+
87
+ end
88
+
89
+ end
90
+
91
+ #LOG.puts ""
92
+ return duration / 3600
93
+ end
94
+
95
+ end
96
+
97
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'working-time'
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,102 @@
1
+ require 'helper'
2
+
3
+ class TestWorkingTime < Test::Unit::TestCase
4
+ def test_same_day_hours_left
5
+ remaining = WorkingTime::DateTime.parse("2009-07-23T12:00:00+00:00").hours_left
6
+ assert_equal(6, remaining)
7
+ end
8
+
9
+ def test_multi_day_add_hours
10
+ start = WorkingTime::DateTime.parse("2009-07-23T12:00:00+00:00")
11
+ stop = WorkingTime::DateTime.parse("2009-07-24T11:00:00+00:00")
12
+ start = start + WorkingTime::Hour.new(10)
13
+ assert_equal(stop.to_s,start.to_s)
14
+ end
15
+
16
+ def test_multi_month_add_hours
17
+ start = WorkingTime::DateTime.parse("2009-07-23T12:00:00+00:00")
18
+ stop = WorkingTime::DateTime.parse("2009-08-31T15:00:00+00:00")
19
+ start = start + WorkingTime::Hour.new(300)
20
+ assert_equal(stop.to_s,start.to_s)
21
+ end
22
+
23
+ def test_start_of_business
24
+ today = WorkingTime::DateTime.parse("2009-07-23T12:12:15-07:00").open_of_business
25
+ start = WorkingTime::DateTime.parse("2009-07-23T08:00:00-07:00")
26
+ assert_equal(start.to_s,today.to_s)
27
+ end
28
+
29
+ def test_datetime_now_class
30
+ assert_equal("WorkingTime::DateTime", WorkingTime::DateTime.now.class.to_s)
31
+ end
32
+
33
+ def test_after_hours_add
34
+ start = WorkingTime::DateTime.parse("2009-07-23T23:00:00+00:00")
35
+ stop = WorkingTime::DateTime.parse("2009-07-24T18:00:00+00:00")
36
+ start = start + WorkingTime::Hour.new(10)
37
+ assert_equal(stop.to_s,start.to_s)
38
+ end
39
+
40
+ def test_after_hours_completion
41
+ start = WorkingTime::DateTime.parse("2009-07-23T23:00:00+00:00")
42
+ stop = WorkingTime::DateTime.parse("2009-07-27T08:00:00+00:00")
43
+ start = start + WorkingTime::Hour.new(11)
44
+ assert_equal(stop.to_s,start.to_s)
45
+ end
46
+
47
+ def test_multi_day_rollover
48
+ start = WorkingTime::DateTime.parse("2009-09-23T13:14:00+00:00")
49
+ stop = WorkingTime::DateTime.parse("2009-09-24T10:14:00+00:00")
50
+ start = start + WorkingTime::Hour.new(8)
51
+ assert_equal(stop.to_s,start.to_s)
52
+ end
53
+
54
+ def test_close_of_week
55
+ start = WorkingTime::DateTime.parse("2009-09-23T13:14:00+00:00")
56
+ stop = WorkingTime::DateTime.parse("2009-09-25T18:00:00+00:00")
57
+ start = start.close_of_week
58
+ assert_equal(stop.to_s,start.to_s)
59
+ end
60
+
61
+ def test_close_of_next_week
62
+ start = WorkingTime::DateTime.parse("2009-09-23T13:14:00+00:00")
63
+ stop = WorkingTime::DateTime.parse("2009-10-02T18:00:00+00:00")
64
+ start = start.close_of_next_week
65
+ assert_equal(stop.to_s,start.to_s)
66
+ end
67
+ end
68
+
69
+ class TestWorkingTimeInterval < Test::Unit::TestCase
70
+
71
+ def test_same_day_close
72
+ start = DateTime.parse("2009-07-29T18:30:00+00:00")
73
+ stop = DateTime.parse("2009-07-29T22:00:00+00:00")
74
+
75
+ interval = WorkingTime::Interval.new(start,stop)
76
+ assert_equal(3.5, interval.duration)
77
+ end
78
+
79
+ def test_multi_day_close_no_weekend
80
+ start = DateTime.parse("2009-07-23T12:00:00+00:00")
81
+ stop = DateTime.parse("2009-07-24T12:00:00+00:00")
82
+
83
+ interval = WorkingTime::Interval.new(start,stop)
84
+ assert_equal(10.0, interval.duration)
85
+ end
86
+
87
+ def test_multi_day_close_weekend
88
+ start = DateTime.parse("2009-07-23T12:00:00+00:00")
89
+ stop = DateTime.parse("2009-07-27T12:00:00+00:00")
90
+
91
+ interval = WorkingTime::Interval.new(start,stop)
92
+ assert_equal(20.0, interval.duration)
93
+ end
94
+
95
+ def test_multi_day_after_hours
96
+ start = DateTime.parse("2009-07-28T18:59:00+00:00")
97
+ stop = DateTime.parse("2009-07-29T16:00:00+00:00")
98
+
99
+ interval = WorkingTime::Interval.new(start,stop)
100
+ assert_equal(8.0, interval.duration)
101
+ end
102
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: working-time
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 3
9
+ - 4
10
+ version: 0.3.4
11
+ platform: ruby
12
+ authors:
13
+ - Carl Hicks
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-07-27 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: thoughtbot-shoulda
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :development
34
+ version_requirements: *id001
35
+ description: Need to figure out how many working hours are in a date range? Need to calculate around holidays, on-call schedules, etc? Use this gem!
36
+ email: carl.hicks@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - LICENSE
43
+ - README.rdoc
44
+ files:
45
+ - .document
46
+ - .gitignore
47
+ - LICENSE
48
+ - README.rdoc
49
+ - Rakefile
50
+ - VERSION
51
+ - lib/working-time.rb
52
+ - lib/working_time/date.rb
53
+ - lib/working_time/interval.rb
54
+ - test/helper.rb
55
+ - test/test_working-time.rb
56
+ has_rdoc: true
57
+ homepage: http://github.com/chicks/working-time
58
+ licenses: []
59
+
60
+ post_install_message:
61
+ rdoc_options:
62
+ - --charset=UTF-8
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ hash: 3
71
+ segments:
72
+ - 0
73
+ version: "0"
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ hash: 3
80
+ segments:
81
+ - 0
82
+ version: "0"
83
+ requirements: []
84
+
85
+ rubyforge_project:
86
+ rubygems_version: 1.3.7
87
+ signing_key:
88
+ specification_version: 3
89
+ summary: Calculate work hours for a given date range
90
+ test_files:
91
+ - test/helper.rb
92
+ - test/test_working-time.rb