business_time 0.7.3 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5fe356eb4f4e620e3f3db2aa9242df1c63213500
4
+ data.tar.gz: d1f662a4f576662e3f6d140970d5b2191016fda8
5
+ SHA512:
6
+ metadata.gz: 86434d31fbf26aa3372510db883266643431a449fa2980cee43e9d34198c140a560d0606d41911aa0a47e030b373eeba0442f38dad07c36a5b5113a811c97213
7
+ data.tar.gz: 5c1ae7866418a21538030ba2129698eaafb2016b5d715cd2c13e31e1739b8732ff625af6f87e48e98bbe697ea06005d74c35bfab7c8610b306f7ba7c9d0121b5
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009,2010,2011,2012 bokmann
1
+ Copyright (c) 2009-2016 bokmann
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.rdoc CHANGED
@@ -15,11 +15,11 @@ as well as helpers to do that from any provided date or time.
15
15
  I needed this, but taking into account business hours/days and holidays.
16
16
 
17
17
  == Usage
18
- * install the gem
18
+ === install the gem
19
19
 
20
20
  gem install business_time
21
21
 
22
- * open up your console
22
+ === open up your console
23
23
 
24
24
  # if in irb, add these lines:
25
25
 
@@ -43,7 +43,11 @@ I needed this, but taking into account business hours/days and holidays.
43
43
  4.business_days.ago
44
44
  8.business_days.ago
45
45
 
46
- # and we can do it from any Date or Time object.
46
+ Date.today.workday?
47
+ Date.parse("2015-12-09").workday?
48
+ Date.parse("2015-12-12").workday?
49
+
50
+ And we can do it from any Date or Time object.
47
51
  my_birthday = Date.parse("August 4th, 1969")
48
52
  8.business_days.after(my_birthday)
49
53
  8.business_days.before(my_birthday)
@@ -53,24 +57,24 @@ I needed this, but taking into account business hours/days and holidays.
53
57
  8.business_days.before(my_birthday)
54
58
 
55
59
 
56
- # We can adjust the start and end time of our business hours
60
+ We can adjust the start and end time of our business hours
57
61
  BusinessTime::Config.beginning_of_workday = "8:30 am"
58
62
  BusinessTime::Config.end_of_workday = "5:30 pm"
59
63
 
60
- # and we can add holidays that don't count as business days
61
- # July 5 in 2010 is a monday that the U.S. takes off because our independence day falls on that Sunday.
64
+ and we can add holidays that don't count as business days
65
+ July 5 in 2010 is a monday that the U.S. takes off because our independence day falls on that Sunday.
62
66
  three_day_weekend = Date.parse("July 5th, 2010")
63
67
  BusinessTime::Config.holidays << three_day_weekend
64
68
  friday_afternoon = Time.parse("July 2nd, 2010, 4:50 pm")
65
69
  tuesday_morning = 1.business_hour.after(friday_afternoon)
66
70
 
67
- # plus, we can change the work week:
71
+ plus, we can change the work week:
68
72
  # July 9th in 2010 is a Friday.
69
73
  BusinessTime::Config.work_week = [:sun, :mon, :tue, :wed, :thu]
70
74
  thursday_afternoon = Time.parse("July 8th, 2010, 4:50 pm")
71
75
  sunday_morning = 1.business_hour.after(thursday_afternoon)
72
76
 
73
- # as alternative we also can change the business hours for each work day:
77
+ As alternative we also can change the business hours for each work day:
74
78
  BusinessTime::Config.work_hours = {
75
79
  :mon=>["9:00","17:00"],
76
80
  :fri=>["9:00","17:00"],
@@ -80,26 +84,46 @@ I needed this, but taking into account business hours/days and holidays.
80
84
  monday = Time.parse("December 27, 2010 11:00")
81
85
  working_hours = friday.business_time_until(monday) # 9.hours
82
86
 
83
- # you can also calculate business duration between two dates
87
+ You can also calculate business duration between two dates
84
88
  friday = Date.parse("December 24, 2010")
85
89
  monday = Date.parse("December 27, 2010")
86
90
  friday.business_days_until(monday) #=> 1
87
91
 
88
- # or you can calculate business duration between two Time objects
92
+ Or you can calculate business duration between two Time objects
89
93
  ticket_reported = Time.parse("February 3, 2012, 10:40 am")
90
94
  ticket_resolved = Time.parse("February 4, 2012, 10:50 am")
91
95
  ticket_reported.business_time_until(ticket_resolved) #=> 8.hours + 10.minutes
92
96
 
93
- # note that counterintuitively, durations might not be quite what you expect when involving weekends.
94
- # Consider the following example:
97
+ You can also determine if a given time is within business hours
98
+ Time.parse("February 3, 2012, 10:00 am").during_business_hours?
99
+
100
+ Note that counterintuitively, durations might not be quite what you expect when involving weekends.
101
+ Consider the following example:
95
102
  ticket_reported = Time.parse("February 3, 2012, 10:40 am")
96
103
  ticket_resolved = Time.parse("February 4, 2012, 10:40 am")
97
104
  ticket_reported.business_time_until(ticket_resolved) # will equal 6 hours and 20 minutes!
98
105
 
99
- # why does this happen? Feb 4 2012 is a Saturday. That time will roll over to
100
- # Monday, Feb 6th 2012, 9:00am. The business time between 10:40am friday and 9am monday is
101
- # 6 hours and 20 minutes. From a quick inspection of the code, it looks like it should be 8 hours.
102
-
106
+ Why does this happen? Feb 4 2012 is a Saturday. That time will roll over to
107
+ Monday, Feb 6th 2012, 9:00am. The business time between 10:40am friday and 9am monday is
108
+ 6 hours and 20 minutes. From a quick inspection of the code, it looks like it should be 8 hours.
109
+
110
+ Or you can calculate business dates between two dates
111
+ monday = Date.parse("December 20, 2010")
112
+ wednesday = Date.parse("December 22, 2010")
113
+ monday.business_dates_until(wednesday) #=> [Mon, 20 Dec 2010, Tue, 21 Dec 2010]
114
+
115
+ You can get the first workday after a time or return itself if it is a workday
116
+ saturday = Time.parse("Sat Aug 9, 18:00:00, 2014")
117
+ monday = Time.parse("Mon Aug 11, 18:00:00, 2014")
118
+ Time.first_business_day(saturday) #=> "Mon Aug 11, 18:00:00, 2014"
119
+ Time.first_business_day(monday) #=> "Mon Aug 11, 18:00:00, 2014"
120
+
121
+ # similar to Time#first_business_day Time#previous_business_day only cares about
122
+ # workdays:
123
+ saturday = Time.parse("Sat Aug 9, 18:00:00, 2014")
124
+ monday = Time.parse("Mon Aug 11, 18:00:00, 2014")
125
+ Time.previous_business_day(saturday) #=> "Fri Aug 8, 18:00:00, 2014"
126
+ Time.previous_business_day(monday) #=> "Mon Aug 11, 18:00:00, 2014"
103
127
  == Rails generator
104
128
 
105
129
  rails generate business_time:config
@@ -123,12 +147,11 @@ and some complexities (bugs?) in the timeWithZone class, this was harder than ex
123
147
  preserved and the beginning and end of times for the business day are
124
148
  referenced in that time zone.
125
149
 
126
- This can lead to some wierd looking effects if, say, you are in the Eastern time zone but doing everything in UTC times...
150
+ This can lead to some weird looking effects if, say, you are in the Eastern time zone but doing everything in UTC times...
127
151
  Your business day will appear to start and end at 9:00 and 5:00 UTC.
128
152
  If this seems perplexing to you, I can almost guarantee you are in over your head with timezones in other ways too,
129
153
  this is just the first place you encountered it.
130
154
  Timezone relative date handling gets more and more complicated every time you look at it and takes a long time before it starts to seem simple again.
131
- I'm hoping Arild and I write some good blog entries on the subject at http://blog.codesherpas.com.
132
155
 
133
156
  == Integration with the Holidays gem
134
157
 
@@ -141,16 +164,6 @@ I'm hoping Arild and I write some good blog entries on the subject at http://blo
141
164
  # BusinessTime::Config.holidays << holiday[:date].next_week if !holiday[:date].weekday?
142
165
  end
143
166
 
144
- == Releases
145
-
146
- 0.7.1 - fixing a multithreaded issue, upgrading some dependencies, loosening the dependency on TZInfo
147
-
148
- 0.7.0 - major maintenance upgrade on the process of constructing the gem, testing the gem, and updating dependencies.
149
- the api has not changed.
150
-
151
-
152
- 0.6.2 - rchady pointed out that issue #14 didn't appear to be released. This fixes that, as well as confirms that all tests run as expected on Ruby 2.0.0p195
153
-
154
167
  == Contributors
155
168
  * David Bock http://github.com/bokmann
156
169
  * Enrico Bianco http://github.com/enricob
@@ -158,6 +171,8 @@ I'm hoping Arild and I write some good blog entries on the subject at http://blo
158
171
  * Piotr Jakubowski http://github.com/piotrj
159
172
  * Glenn Vanderburg http://github.com/glv
160
173
  * Michael Grosser http://github.com/grosser
174
+ * Michael Curtis http://github.com/mcurtis
175
+ * Brian Ewins http://github.com/bazzargh
161
176
 
162
177
  (Special thanks for Arild on the complexities of dealing with TimeWithZone)
163
178
 
@@ -189,6 +204,20 @@ I'm hoping Arild and I write some good blog entries on the subject at http://blo
189
204
  need the baggage.
190
205
 
191
206
 
207
+ == A note on stability and change
208
+
209
+ Sometimes people ask me why this gem doesn't release more often. My opinions on that are best discussed in person in a friendly discussion, but I'll attempt some of that here.
210
+
211
+ First, a big part of the reason is that the projects I do use this gem on are happy with it's current functionality. It is 'suitable for the purpose' for which I released it, and as such, maintenance I do on this gem is a gift to the community.
212
+
213
+ Second, out of the ~1.3 million downloads (according to rubygems.org), the number of real 'issues' with this gem have been minimal. Most of the issues that are opened are really people with slightly different requirements than I have regarding whether 'off hours' work counts as the previous or the next business day, a disagreement on the semantics of days vs. hours, etc. I take care to try to explain these choices in the open issues, but to my mind, they aren't true issues if it's just a difference of opinion. Even so, I'll gladly accept pull requests that resolve this difference of opinion as a configuration option... just don't expect me to do your job for you. I've already given you 90% of what you need.
214
+
215
+ Third, a business time gem is, well, relevant to businesses. Many businesses don't move quickly. My government clients move even more slowly. Stability is favored in these environments.
216
+
217
+ Fourth, new features can wait. To the person that adds them they can be mission critical, but with modern packaging processes, they can use their version without waiting for their changes to be included in the upstream version. Their changes don't break your code.
218
+
219
+ I'm proud of the work in this gem; the stability is a big part of that. This gem has lived longer than many others that have attempted to do the same thing. I expect it to be here chugging away when Ruby has become the next COBOL.
220
+
192
221
  == Copyright
193
222
 
194
- Copyright (c) 2010,2011,2012,2013 bokmann. See LICENSE for details.
223
+ Copyright (c) 2010-2017 bokmann. See LICENSE for details.
data/lib/business_time.rb CHANGED
@@ -4,12 +4,14 @@ require 'active_support/time'
4
4
  require 'time'
5
5
  require 'yaml'
6
6
 
7
+ require 'business_time/parsed_time'
8
+ require 'business_time/version'
7
9
  require 'business_time/config'
8
10
  require 'business_time/business_hours'
9
11
  require 'business_time/business_days'
10
- require 'business_time/core_ext/date'
11
- require 'business_time/core_ext/fixnum'
12
+ require 'business_time/core_ext/integer'
12
13
 
13
14
  require 'business_time/time_extensions'
15
+ require 'business_time/core_ext/date'
14
16
  require 'business_time/core_ext/time'
15
17
  require 'business_time/core_ext/active_support/time_with_zone'
@@ -1,38 +1,67 @@
1
1
  require 'active_support/time'
2
2
 
3
3
  module BusinessTime
4
-
5
4
  class BusinessDays
5
+ include Comparable
6
+ attr_reader :days
7
+
6
8
  def initialize(days)
7
9
  @days = days
8
10
  end
9
11
 
10
- def after(time = Time.now)
11
- time = Time.zone ? Time.zone.parse(time.strftime('%Y-%m-%d %H:%M:%S %z')) : Time.parse(time.strftime('%Y-%m-%d %H:%M:%S %z'))
12
- days = @days
13
- while days > 0 || !Time.workday?(time)
14
- days -= 1 if Time.workday?(time)
15
- time = time + 1.day
12
+ def <=>(other)
13
+ if other.class != self.class
14
+ raise ArgumentError.new("#{self.class} can't be compared with #{other.class}")
16
15
  end
17
- time
16
+ self.days <=> other.days
17
+ end
18
+
19
+ def after(time = Time.current)
20
+ non_negative_days? ? calculate_after(time, @days) : calculate_before(time, -@days)
18
21
  end
19
-
22
+
20
23
  alias_method :from_now, :after
21
24
  alias_method :since, :after
22
-
23
- def before(time = Time.now)
24
- time = Time.zone ? Time.zone.parse(time.rfc822) : Time.parse(time.rfc822)
25
- days = @days
26
- while days > 0 || !Time.workday?(time)
27
- days -= 1 if Time.workday?(time)
28
- time = time - 1.day
29
- end
30
- time
25
+
26
+ def before(time = Time.current)
27
+ non_negative_days? ? calculate_before(time, @days) : calculate_after(time, -@days)
31
28
  end
32
-
29
+
33
30
  alias_method :ago, :before
34
31
  alias_method :until, :before
35
-
36
- end
37
-
32
+
33
+ private
34
+
35
+ def non_negative_days?
36
+ @days >= 0
37
+ end
38
+
39
+ def calculate_after(time, days)
40
+ while days > 0 || !time.workday?
41
+ days -= 1 if time.workday?
42
+ time += 1.day
43
+ end
44
+ # If we have a Time or DateTime object, we can roll_forward to the
45
+ # beginning of the next business day
46
+ if time.is_a?(Time) || time.is_a?(DateTime)
47
+ time = Time.roll_forward(time) unless time.during_business_hours?
48
+ end
49
+ time
50
+ end
51
+
52
+ def calculate_before(time, days)
53
+ while days > 0 || !time.workday?
54
+ days -= 1 if time.workday?
55
+ time -= 1.day
56
+ end
57
+ # If we have a Time or DateTime object, we can roll_backward to the
58
+ # beginning of the previous business day
59
+ if time.is_a?(Time) || time.is_a?(DateTime)
60
+ unless time.during_business_hours?
61
+ time = Time.beginning_of_workday(Time.roll_backward(time))
62
+ end
63
+ end
64
+ time
65
+ end
66
+ end
38
67
  end
@@ -1,10 +1,20 @@
1
1
  module BusinessTime
2
2
 
3
3
  class BusinessHours
4
+ include Comparable
5
+ attr_reader :hours
6
+
4
7
  def initialize(hours)
5
8
  @hours = hours
6
9
  end
7
10
 
11
+ def <=>(other)
12
+ if other.class != self.class
13
+ raise ArgumentError.new("#{self.class.to_s} can't be compared with #{other.class.to_s}")
14
+ end
15
+ self.hours <=> other.hours
16
+ end
17
+
8
18
  def ago
9
19
  Time.zone ? before(Time.zone.now) : before(Time.now)
10
20
  end
@@ -14,9 +24,24 @@ module BusinessTime
14
24
  end
15
25
 
16
26
  def after(time)
27
+ non_negative_hours? ? calculate_after(time, @hours) : calculate_before(time, -@hours)
28
+ end
29
+ alias_method :since, :after
30
+
31
+ def before(time)
32
+ non_negative_hours? ? calculate_before(time, @hours) : calculate_after(time, -@hours)
33
+ end
34
+
35
+ private
36
+
37
+ def non_negative_hours?
38
+ @hours >= 0
39
+ end
40
+
41
+ def calculate_after(time, hours)
17
42
  after_time = Time.roll_forward(time)
18
43
  # Step through the hours, skipping over non-business hours
19
- @hours.times do
44
+ hours.times do
20
45
  after_time = after_time + 1.hour
21
46
 
22
47
  if after_time.hour == 0 && after_time.min == 0 && after_time.sec == 0
@@ -28,18 +53,17 @@ module BusinessTime
28
53
  end
29
54
 
30
55
  # Ignore weekends and holidays
31
- while !Time.workday?(after_time)
56
+ while !after_time.workday?
32
57
  after_time = after_time + 1.day
33
58
  end
34
59
  end
35
60
  after_time
36
61
  end
37
- alias_method :since, :after
38
62
 
39
- def before(time)
63
+ def calculate_before(time, hours)
40
64
  before_time = Time.roll_backward(time)
41
65
  # Step through the hours, skipping over non-business hours
42
- @hours.times do
66
+ hours.times do
43
67
  before_time = before_time - 1.hour
44
68
 
45
69
  if before_time.hour == 0 && before_time.min == 0 && before_time.sec == 0
@@ -50,13 +74,13 @@ module BusinessTime
50
74
 
51
75
  # Due to the 23:59:59 end-of-workday exception
52
76
  time_roll_backward = Time.roll_backward(before_time)
53
- time_roll_backward += 1.second if time_roll_backward.to_s =~ /23:59:59/
77
+ time_roll_backward += 1.second if time_roll_backward.iso8601 =~ /23:59:59/
54
78
 
55
79
  before_time = time_roll_backward - delta
56
80
  end
57
81
 
58
82
  # Ignore weekends and holidays
59
- while !Time.workday?(before_time)
83
+ while !before_time.workday?
60
84
  before_time = before_time - 1.day
61
85
  end
62
86
  end
@@ -7,30 +7,73 @@ module BusinessTime
7
7
  # manually, or with a yaml file and the load method.
8
8
  class Config
9
9
  DEFAULT_CONFIG = {
10
- holidays: [],
11
- beginning_of_workday: '9:00 am',
12
- end_of_workday: '5:00 pm',
10
+ holidays: SortedSet.new,
11
+ beginning_of_workday: ParsedTime.parse('9:00 am'),
12
+ end_of_workday: ParsedTime.parse('5:00 pm'),
13
13
  work_week: %w(mon tue wed thu fri),
14
14
  work_hours: {},
15
15
  work_hours_total: {},
16
16
  _weekdays: nil,
17
+ fiscal_month_offset: 10,
17
18
  }
18
19
 
19
20
  class << self
21
+ def beginning_of_workday=(time)
22
+ config[:beginning_of_workday] = ParsedTime.parse(time)
23
+ end
24
+
25
+ def end_of_workday=(time)
26
+ config[:end_of_workday] = ParsedTime.parse(time)
27
+ end
28
+
29
+ def work_hours=(work_hours)
30
+ work_hours.each_with_object(config[:work_hours] = {}) do |(day, hours), c|
31
+ c[day] = hours.map do |time|
32
+ ParsedTime.parse(time)
33
+ end
34
+ end
35
+ end
36
+
20
37
  private
21
38
 
22
39
  def config
40
+ return local_config if local_config?
23
41
  Thread.main[:business_time_config] ||= default_config
24
42
  end
25
43
 
26
44
  def config=(config)
45
+ return self.local_config = config if local_config?
27
46
  Thread.main[:business_time_config] = config
28
47
  end
29
48
 
49
+ def local_config
50
+ local_config_stack.last
51
+ end
52
+
53
+ def local_config=(config)
54
+ local_config_stack.last.replace(config)
55
+ end
56
+
57
+ def local_config_stack
58
+ Thread.current[:business_time_local_config] ||= []
59
+ end
60
+
61
+ def local_config?
62
+ !local_config_stack.empty?
63
+ end
64
+
30
65
  def threadsafe_cattr_accessor(name)
66
+ threadsafe_cattr_reader(name)
67
+ threadsafe_cattr_setter(name)
68
+ end
69
+
70
+ def threadsafe_cattr_reader(name)
31
71
  define_singleton_method name do
32
72
  config[name]
33
73
  end
74
+ end
75
+
76
+ def threadsafe_cattr_setter(name)
34
77
  define_singleton_method "#{name}=" do |value|
35
78
  config[name] = value
36
79
  end
@@ -41,13 +84,13 @@ module BusinessTime
41
84
  # by saying
42
85
  # BusinessTime::Config.beginning_of_workday = "8:30 am"
43
86
  # someplace in the initializers of your application.
44
- threadsafe_cattr_accessor :beginning_of_workday
87
+ threadsafe_cattr_reader :beginning_of_workday
45
88
 
46
89
  # You can set this yourself, either by the load method below, or
47
90
  # by saying
48
91
  # BusinessTime::Config.end_of_workday = "5:30 pm"
49
92
  # someplace in the initializers of your application.
50
- threadsafe_cattr_accessor :end_of_workday
93
+ threadsafe_cattr_reader :end_of_workday
51
94
 
52
95
  # You can set this yourself, either by the load method below, or
53
96
  # by saying
@@ -65,18 +108,20 @@ module BusinessTime
65
108
  # and end_of_workday. Keys will be added ad weekdays.
66
109
  # Example:
67
110
  # {:mon => ["9:00","17:00"],:tue => ["9:00","17:00"].....}
68
- threadsafe_cattr_accessor :work_hours
111
+ threadsafe_cattr_reader :work_hours
69
112
 
70
113
  # total work hours for a day. Never set, always calculated.
71
114
  threadsafe_cattr_accessor :work_hours_total
72
115
 
73
116
  threadsafe_cattr_accessor :_weekdays # internal
74
117
 
118
+ threadsafe_cattr_accessor :fiscal_month_offset
119
+
75
120
  class << self
76
121
  def end_of_workday(day=nil)
77
122
  if day
78
123
  wday = work_hours[int_to_wday(day.wday)]
79
- wday ? (wday.last =~ /^0{1,2}\:0{1,2}$/ ? "23:59:59" : wday.last) : config[:end_of_workday]
124
+ wday ? (wday.last == ParsedTime.new(0, 0) ? ParsedTime.new(23, 59, 59) : wday.last) : config[:end_of_workday]
80
125
  else
81
126
  config[:end_of_workday]
82
127
  end
@@ -99,10 +144,11 @@ module BusinessTime
99
144
  def weekdays
100
145
  return _weekdays unless _weekdays.nil?
101
146
 
102
- self._weekdays = (!work_hours.empty? ? work_hours.keys : work_week).each_with_object([]) do |day_name, days|
103
- day_num = wday_to_int(day_name)
104
- days << day_num unless day_num.nil?
105
- end
147
+ days = (!work_hours.empty? ? work_hours.keys : work_week).map do |day_name|
148
+ wday_to_int(day_name)
149
+ end.compact
150
+
151
+ self._weekdays = SortedSet.new(days)
106
152
  end
107
153
 
108
154
  # loads the config data from a yaml file written as:
@@ -131,11 +177,11 @@ module BusinessTime
131
177
  end
132
178
 
133
179
  def with(config)
134
- old = config().dup
180
+ local_config_stack.push(config().dup)
135
181
  config.each { |k,v| send("#{k}=", v) } # calculations are done on setting
136
182
  yield
137
183
  ensure
138
- self.config = old
184
+ local_config_stack.pop
139
185
  end
140
186
 
141
187
  def default_config
@@ -154,6 +200,7 @@ module BusinessTime
154
200
  end
155
201
 
156
202
  def reset
203
+ local_config_stack.clear
157
204
  self.config = default_config
158
205
  end
159
206
 
@@ -1,3 +1,4 @@
1
1
  class ActiveSupport::TimeWithZone
2
2
  include BusinessTime::TimeExtensions
3
+ extend BusinessTime::TimeExtensions::ClassMethods
3
4
  end
@@ -1,14 +1,61 @@
1
1
  # Add workday and weekday concepts to the Date class
2
2
  class Date
3
- def workday?
4
- weekday? && !BusinessTime::Config.holidays.include?(self)
3
+ include BusinessTime::TimeExtensions
4
+
5
+ def business_days_until(to_date, inclusive = false)
6
+ business_dates_until(to_date, inclusive).size
7
+ end
8
+
9
+ def business_dates_until(to_date, inclusive = false)
10
+ if inclusive
11
+ (self..to_date).select(&:workday?)
12
+ else
13
+ (self...to_date).select(&:workday?)
14
+ end
15
+ end
16
+
17
+ # Adapted from:
18
+ # https://github.com/activewarehouse/activewarehouse/blob/master/lib/active_warehouse/core_ext/time/calculations.rb
19
+
20
+ def week
21
+ cyw = ((yday - 1) / 7) + 1
22
+ cyw = 52 if cyw == 53
23
+ cyw
24
+ end
25
+
26
+ def quarter
27
+ ((month - 1) / 3) + 1
28
+ end
29
+
30
+ def fiscal_month_offset
31
+ BusinessTime::Config.fiscal_month_offset
32
+ end
33
+
34
+ def fiscal_year_week
35
+ fyw = ((fiscal_year_yday - 1) / 7) + 1
36
+ fyw = 52 if fyw == 53
37
+ fyw
38
+ end
39
+
40
+ def fiscal_year_month
41
+ shifted_month = month - (fiscal_month_offset - 1)
42
+ shifted_month += 12 if shifted_month <= 0
43
+ shifted_month
44
+ end
45
+
46
+ def fiscal_year_quarter
47
+ ((fiscal_year_month - 1) / 3) + 1
5
48
  end
6
49
 
7
- def weekday?
8
- BusinessTime::Config.weekdays.include? wday
50
+ def fiscal_year
51
+ month >= fiscal_month_offset ? year + 1 : year
9
52
  end
10
53
 
11
- def business_days_until(to_date)
12
- (self...to_date).select{ |day| day.workday? }.size
54
+ def fiscal_year_yday
55
+ offset_days = 0
56
+ 1.upto(fiscal_month_offset - 1) { |m| offset_days += ::Time.days_in_month(m, year) }
57
+ shifted_year_day = yday - offset_days
58
+ shifted_year_day += 365 if shifted_year_day <= 0
59
+ shifted_year_day
13
60
  end
14
61
  end
@@ -1,9 +1,9 @@
1
- # hook into fixnum so we can say things like:
1
+ # hook into Integer so we can say things like:
2
2
  # 5.business_hours.from_now
3
3
  # 7.business_days.ago
4
4
  # 3.business_days.after(some_date)
5
5
  # 4.business_hours.before(some_date_time)
6
- class Fixnum
6
+ class Integer
7
7
  def business_hours
8
8
  BusinessTime::BusinessHours.new(self)
9
9
  end
@@ -1,4 +1,5 @@
1
1
  # Add workday and weekday concepts to the Time class
2
2
  class Time
3
3
  include BusinessTime::TimeExtensions
4
+ extend BusinessTime::TimeExtensions::ClassMethods
4
5
  end
@@ -0,0 +1,34 @@
1
+ module BusinessTime
2
+ class ParsedTime
3
+ include Comparable
4
+
5
+ attr_reader :hour, :min, :sec
6
+
7
+ def initialize(hour, min = 0, sec = 0)
8
+ @hour = hour
9
+ @min = min
10
+ @sec = sec
11
+ end
12
+
13
+ def self.parse(time_or_string)
14
+ if time_or_string.is_a?(String)
15
+ time = Time.parse(time_or_string)
16
+ else
17
+ time = time_or_string
18
+ end
19
+ new(time.hour, time.min, time.sec)
20
+ end
21
+
22
+ def to_s
23
+ "#{hour}:#{min}:#{sec}"
24
+ end
25
+
26
+ def -(other)
27
+ (hour - other.hour) * 3600 + (min - other.min) * 60 + sec - other.sec
28
+ end
29
+
30
+ def <=>(other)
31
+ [hour, min, sec] <=> [other.hour, other.min, other.sec]
32
+ end
33
+ end
34
+ end
@@ -1,7 +1,14 @@
1
1
  module BusinessTime
2
2
  module TimeExtensions
3
- def self.included(base)
4
- base.extend(ClassMethods)
3
+ # True if this time is on a workday (between 00:00:00 and 23:59:59), even if
4
+ # this time falls outside of normal business hours.
5
+ def workday?
6
+ weekday? && !BusinessTime::Config.holidays.include?(to_date)
7
+ end
8
+
9
+ # True if this time falls on a weekday.
10
+ def weekday?
11
+ BusinessTime::Config.weekdays.include?(wday)
5
12
  end
6
13
 
7
14
  module ClassMethods
@@ -10,7 +17,7 @@ module BusinessTime
10
17
  # Note: It pretends that this day is a workday whether or not it really is a
11
18
  # workday.
12
19
  def end_of_workday(day)
13
- end_of_workday = Time.parse(BusinessTime::Config.end_of_workday(day))
20
+ end_of_workday = BusinessTime::Config.end_of_workday(day)
14
21
  change_business_time(day,end_of_workday.hour,end_of_workday.min,end_of_workday.sec)
15
22
  end
16
23
 
@@ -19,20 +26,21 @@ module BusinessTime
19
26
  # Note: It pretends that this day is a workday whether or not it really is a
20
27
  # workday.
21
28
  def beginning_of_workday(day)
22
- beginning_of_workday = Time.parse(BusinessTime::Config.beginning_of_workday(day))
29
+ beginning_of_workday = BusinessTime::Config.beginning_of_workday(day)
23
30
  change_business_time(day,beginning_of_workday.hour,beginning_of_workday.min,beginning_of_workday.sec)
24
31
  end
25
32
 
26
33
  # True if this time is on a workday (between 00:00:00 and 23:59:59), even if
27
34
  # this time falls outside of normal business hours.
28
35
  def workday?(day)
29
- Time.weekday?(day) &&
30
- !BusinessTime::Config.holidays.include?(day.to_date)
36
+ ActiveSupport::Deprecation.warn("`Time.workday?(time)` is deprecated. Please use `time.workday?`")
37
+ day.workday?
31
38
  end
32
39
 
33
40
  # True if this time falls on a weekday.
34
41
  def weekday?(day)
35
- BusinessTime::Config.weekdays.include? day.wday
42
+ ActiveSupport::Deprecation.warn("`Time.weekday?(time)` is deprecated. Please use `time.weekday?`")
43
+ day.weekday?
36
44
  end
37
45
 
38
46
  def before_business_hours?(time)
@@ -47,7 +55,7 @@ module BusinessTime
47
55
  # when the time is outside of business hours
48
56
  def roll_forward(time)
49
57
 
50
- if Time.before_business_hours?(time) || !Time.workday?(time)
58
+ if Time.before_business_hours?(time) || !time.workday?
51
59
  next_business_time = Time.beginning_of_workday(time)
52
60
  elsif Time.after_business_hours?(time) || Time.end_of_workday(time) == time
53
61
  next_business_time = Time.beginning_of_workday(time + 1.day)
@@ -55,17 +63,27 @@ module BusinessTime
55
63
  next_business_time = time.clone
56
64
  end
57
65
 
58
- while !Time.workday?(next_business_time)
66
+ while !next_business_time.workday?
59
67
  next_business_time = Time.beginning_of_workday(next_business_time + 1.day)
60
68
  end
61
69
 
62
70
  next_business_time
63
71
  end
64
72
 
73
+ # Returns the time parameter itself if it is a business day
74
+ # or else returns the next business day
75
+ def first_business_day(time)
76
+ while !time.workday?
77
+ time = time + 1.day
78
+ end
79
+
80
+ time
81
+ end
82
+
65
83
  # Rolls backwards to the previous end_of_workday when the time is outside
66
84
  # of business hours
67
85
  def roll_backward(time)
68
- prev_business_time = if (Time.before_business_hours?(time) || !Time.workday?(time))
86
+ prev_business_time = if (Time.before_business_hours?(time) || !time.workday?)
69
87
  Time.end_of_workday(time - 1.day)
70
88
  elsif Time.after_business_hours?(time)
71
89
  Time.end_of_workday(time)
@@ -73,30 +91,40 @@ module BusinessTime
73
91
  time.clone
74
92
  end
75
93
 
76
- while !Time.workday?(prev_business_time)
94
+ while !prev_business_time.workday?
77
95
  prev_business_time = Time.end_of_workday(prev_business_time - 1.day)
78
96
  end
79
97
 
80
98
  prev_business_time
81
99
  end
82
100
 
101
+ # Returns the time parameter itself if it is a business day
102
+ # or else returns the previous business day
103
+ def previous_business_day(time)
104
+ while !time.workday?
105
+ time = time - 1.day
106
+ end
107
+
108
+ time
109
+ end
110
+
83
111
  def work_hours_total(day)
84
- return 0 unless Time.workday?(day)
112
+ return 0 unless day.workday?
85
113
 
86
114
  day = day.strftime('%a').downcase.to_sym
87
115
 
88
116
  if hours = BusinessTime::Config.work_hours[day]
89
117
  BusinessTime::Config.work_hours_total[day] ||= begin
90
118
  hours_last = hours.last
91
- if hours_last == '00:00'
92
- (Time.parse('23:59') - Time.parse(hours.first)) + 1.minute
119
+ if hours_last == ParsedTime.new(0, 0)
120
+ (ParsedTime.new(23, 59) - hours.first) + 1.minute
93
121
  else
94
- Time.parse(hours_last) - Time.parse(hours.first)
122
+ hours_last - hours.first
95
123
  end
96
124
  end
97
125
  else
98
126
  BusinessTime::Config.work_hours_total[:default] ||= begin
99
- Time.parse(BusinessTime::Config.end_of_workday) - Time.parse(BusinessTime::Config.beginning_of_workday)
127
+ BusinessTime::Config.end_of_workday - BusinessTime::Config.beginning_of_workday
100
128
  end
101
129
  end
102
130
  end
@@ -104,11 +132,7 @@ module BusinessTime
104
132
  private
105
133
 
106
134
  def change_business_time time, hour, min=0, sec=0
107
- if Time.zone
108
- time.in_time_zone(Time.zone).change(:hour => hour, :min => min, :sec => sec)
109
- else
110
- time.change(:hour => hour, :min => min, :sec => sec)
111
- end
135
+ time.change(:hour => hour, :min => min, :sec => sec)
112
136
  end
113
137
  end
114
138
 
@@ -141,5 +165,34 @@ module BusinessTime
141
165
  first_day + days_in_between + last_day
142
166
  end * direction
143
167
  end
168
+
169
+ def during_business_hours?
170
+ self.workday? && self.to_i.between?(Time.beginning_of_workday(self).to_i, Time.end_of_workday(self).to_i)
171
+ end
172
+
173
+ def consecutive_workdays
174
+ workday? ? consecutive_days { |date| date.workday? } : []
175
+ end
176
+
177
+ def consecutive_non_working_days
178
+ !workday? ? consecutive_days { |date| !date.workday? } : []
179
+ end
180
+
181
+ private
182
+
183
+ def consecutive_days
184
+ days = []
185
+ date = self + 1.day
186
+ while yield(date)
187
+ days << date
188
+ date += 1.day
189
+ end
190
+ date = self - 1.day
191
+ while yield(date)
192
+ days << date
193
+ date -= 1.day
194
+ end
195
+ (days << self).sort
196
+ end
144
197
  end
145
198
  end
@@ -1,3 +1,3 @@
1
1
  module BusinessTime
2
- VERSION = "0.7.3"
2
+ VERSION = "0.9.3"
3
3
  end
metadata CHANGED
@@ -1,115 +1,102 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: business_time
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.3
5
- prerelease:
4
+ version: 0.9.3
6
5
  platform: ruby
7
6
  authors:
8
7
  - bokmann
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2014-06-10 00:00:00.000000000 Z
11
+ date: 2017-11-04 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: activesupport
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
- - - ! '>='
17
+ - - ">="
20
18
  - !ruby/object:Gem::Version
21
- version: 3.1.0
19
+ version: 3.2.0
22
20
  type: :runtime
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
- - - ! '>='
24
+ - - ">="
28
25
  - !ruby/object:Gem::Version
29
- version: 3.1.0
26
+ version: 3.2.0
30
27
  - !ruby/object:Gem::Dependency
31
28
  name: tzinfo
32
29
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
30
  requirements:
35
- - - ! '>='
31
+ - - ">="
36
32
  - !ruby/object:Gem::Version
37
33
  version: '0'
38
34
  type: :runtime
39
35
  prerelease: false
40
36
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
37
  requirements:
43
- - - ! '>='
38
+ - - ">="
44
39
  - !ruby/object:Gem::Version
45
40
  version: '0'
46
41
  - !ruby/object:Gem::Dependency
47
42
  name: rake
48
43
  requirement: !ruby/object:Gem::Requirement
49
- none: false
50
44
  requirements:
51
- - - ! '>='
45
+ - - ">="
52
46
  - !ruby/object:Gem::Version
53
47
  version: '0'
54
48
  type: :development
55
49
  prerelease: false
56
50
  version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
51
  requirements:
59
- - - ! '>='
52
+ - - ">="
60
53
  - !ruby/object:Gem::Version
61
54
  version: '0'
62
55
  - !ruby/object:Gem::Dependency
63
56
  name: rdoc
64
57
  requirement: !ruby/object:Gem::Requirement
65
- none: false
66
58
  requirements:
67
- - - ! '>='
59
+ - - ">="
68
60
  - !ruby/object:Gem::Version
69
61
  version: '0'
70
62
  type: :development
71
63
  prerelease: false
72
64
  version_requirements: !ruby/object:Gem::Requirement
73
- none: false
74
65
  requirements:
75
- - - ! '>='
66
+ - - ">="
76
67
  - !ruby/object:Gem::Version
77
68
  version: '0'
78
69
  - !ruby/object:Gem::Dependency
79
70
  name: minitest
80
71
  requirement: !ruby/object:Gem::Requirement
81
- none: false
82
72
  requirements:
83
- - - ! '>='
73
+ - - ">="
84
74
  - !ruby/object:Gem::Version
85
75
  version: '0'
86
76
  type: :development
87
77
  prerelease: false
88
78
  version_requirements: !ruby/object:Gem::Requirement
89
- none: false
90
79
  requirements:
91
- - - ! '>='
80
+ - - ">="
92
81
  - !ruby/object:Gem::Version
93
82
  version: '0'
94
83
  - !ruby/object:Gem::Dependency
95
84
  name: minitest-rg
96
85
  requirement: !ruby/object:Gem::Requirement
97
- none: false
98
86
  requirements:
99
- - - ! '>='
87
+ - - ">="
100
88
  - !ruby/object:Gem::Version
101
89
  version: '0'
102
90
  type: :development
103
91
  prerelease: false
104
92
  version_requirements: !ruby/object:Gem::Requirement
105
- none: false
106
93
  requirements:
107
- - - ! '>='
94
+ - - ">="
108
95
  - !ruby/object:Gem::Version
109
96
  version: '0'
110
97
  description: Have you ever wanted to do things like "6.business_days.from_now" and
111
98
  have weekends and holidays taken into account? Now you can.
112
- email: dbock@codesherpas.com
99
+ email: dbock@javaguy.org
113
100
  executables: []
114
101
  extensions: []
115
102
  extra_rdoc_files: []
@@ -122,8 +109,9 @@ files:
122
109
  - lib/business_time/config.rb
123
110
  - lib/business_time/core_ext/active_support/time_with_zone.rb
124
111
  - lib/business_time/core_ext/date.rb
125
- - lib/business_time/core_ext/fixnum.rb
112
+ - lib/business_time/core_ext/integer.rb
126
113
  - lib/business_time/core_ext/time.rb
114
+ - lib/business_time/parsed_time.rb
127
115
  - lib/business_time/time_extensions.rb
128
116
  - lib/business_time/version.rb
129
117
  - lib/generators/business_time/config_generator.rb
@@ -133,32 +121,25 @@ files:
133
121
  homepage: https://github.com/bokmann/business_time
134
122
  licenses:
135
123
  - MIT
124
+ metadata: {}
136
125
  post_install_message:
137
126
  rdoc_options: []
138
127
  require_paths:
139
128
  - lib
140
129
  required_ruby_version: !ruby/object:Gem::Requirement
141
- none: false
142
130
  requirements:
143
- - - ! '>='
131
+ - - ">="
144
132
  - !ruby/object:Gem::Version
145
133
  version: '0'
146
- segments:
147
- - 0
148
- hash: -1720097037897540308
149
134
  required_rubygems_version: !ruby/object:Gem::Requirement
150
- none: false
151
135
  requirements:
152
- - - ! '>='
136
+ - - ">="
153
137
  - !ruby/object:Gem::Version
154
138
  version: '0'
155
- segments:
156
- - 0
157
- hash: -1720097037897540308
158
139
  requirements: []
159
140
  rubyforge_project:
160
- rubygems_version: 1.8.23
141
+ rubygems_version: 2.6.13
161
142
  signing_key:
162
- specification_version: 3
143
+ specification_version: 4
163
144
  summary: Support for doing time math in business hours and days
164
145
  test_files: []