Git Product home page Git Product logo

biz's Introduction

biz

Gem Version repo-checks Code Climate Test Coverage

Time calculations using business hours.

Features

  • Support for:
    • Multiple intervals per day.
    • Multiple schedule configurations.
    • Intervals spanning the entire day.
    • Holidays.
    • Breaks (time-segment holidays).
    • Shifts (date-based intervals).
  • Second-level calculation precision.
  • Seamless Daylight Saving Time handling.
  • Schedule intersection.
  • Thread safety.

Anti-Features

  • No dependency on ActiveSupport.
  • No monkey patching by default.

Installation

Add this line to your application's Gemfile:

gem 'biz'

And then execute:

$ bundle

Or install it yourself as:

$ gem install biz

Configuration

Biz.configure do |config|
  config.hours = {
    mon: {'09:00' => '17:00'},
    tue: {'00:00' => '24:00'},
    wed: {'09:00' => '17:00'},
    thu: {'09:00' => '12:00', '13:00' => '17:00'},
    sat: {'10:00' => '14:00'}
  }

  config.shifts = {
    Date.new(2006, 1, 1) => {'09:00' => '12:00'},
    Date.new(2006, 1, 7) => {'08:00' => '10:00', '12:00' => '14:00'}
  }

  config.breaks = {
    Date.new(2006, 1, 2) => {'10:00' => '11:30'},
    Date.new(2006, 1, 3) => {'14:15' => '14:30', '15:40' => '15:50'}
  }

  config.holidays = [Date.new(2016, 1, 1), Date.new(2016, 12, 25)]

  config.time_zone = 'America/Los_Angeles'
end

Configured timestamps must be in either HH:MM or HH:MM:SS format.

Shifts act as exceptions to the hours configured for a particular date; that is, if a date is configured with both hours-based intervals and shifts, the shifts are in force and the intervals are disregarded.

Periods occurring on holidays are disregarded. Similarly, any segment of a period that overlaps with a break is treated as inactive.

If global configuration isn't your thing, configure an instance instead:

Biz::Schedule.new do |config|
  # ...
end

Note that times must be specified in 24-hour clock format and time zones must be IANA identifiers.

If you're operating in a threaded environment and want to use the same configuration across threads, save the configured schedule as a global variable:

$biz = Biz::Schedule.new

Usage

# Find the time an amount of business time *before* a specified starting time
Biz.time(30, :minutes).before(Time.utc(2015, 1, 1, 11, 45))

# Find the time an amount of business time *after* a specified starting time
Biz.time(2, :hours).after(Time.utc(2015, 12, 25, 9, 30))

# Calculations can be performed in seconds, minutes, hours, or days
Biz.time(1, :day).after(Time.utc(2015, 1, 8, 10))

# Find the previous business time
Biz.time(0, :hours).before(Time.utc(2016, 1, 8, 6))

# Find the next business time
Biz.time(0, :hours).after(Time.utc(2016, 1, 8, 20))

# Find the amount of business time between two times
Biz.within(Time.utc(2015, 3, 7), Time.utc(2015, 3, 14)).in_seconds

# Find the start of the business day
Biz.periods.on(Date.today).first.start_time

# Find the end of the business day
Biz.periods.on(Date.today).to_a.last.end_time

# Determine if a time is in business hours
Biz.in_hours?(Time.utc(2015, 1, 10, 9))

# Determine if a time is during a break
Biz.on_break?(Time.utc(2016, 6, 3))

# Determine if a time is during a holiday
Biz.on_holiday?(Time.utc(2014, 1, 1))

The same methods can be called on a configured instance:

schedule = Biz::Schedule.new

schedule.in_hours?(Time.utc(2015, 1, 1, 10))

All returned times are in UTC.

If a schedule will be configured with a large number of holidays and performance is a particular concern, it's recommended that holidays are filtered down to those relevant to the calculation(s) at hand before configuration to improve performance.

By dropping down a level, you can get access to the underlying time segments, which you can use to do your own custom calculations or just get a better idea of what's happening under the hood:

Biz.periods.after(Time.utc(2015, 1, 10, 10)).timeline
  .until(Time.utc(2015, 1, 17, 10)).to_a

#=> [#<Biz::TimeSegment start_time=2015-01-10 18:00:00 UTC end_time=2015-01-10 22:00:00 UTC>,
#  #<Biz::TimeSegment start_time=2015-01-12 17:00:00 UTC end_time=2015-01-13 01:00:00 UTC>,
#  #<Biz::TimeSegment start_time=2015-01-13 08:00:00 UTC end_time=2015-01-14 08:00:00 UTC>,
#  #<Biz::TimeSegment start_time=2015-01-14 17:00:00 UTC end_time=2015-01-15 01:00:00 UTC>,
#  #<Biz::TimeSegment start_time=2015-01-15 17:00:00 UTC end_time=2015-01-15 20:00:00 UTC>,
#  #<Biz::TimeSegment start_time=2015-01-15 21:00:00 UTC end_time=2015-01-16 01:00:00 UTC>]

Biz.periods
  .before(Time.utc(2015, 5, 5, 12, 34, 57)).timeline
  .for(Biz::Duration.minutes(3_598)).to_a

#=> [#<Biz::TimeSegment start_time=2015-05-05 07:00:00 UTC end_time=2015-05-05 12:34:57 UTC>,
#  #<Biz::TimeSegment start_time=2015-05-04 16:00:00 UTC end_time=2015-05-05 00:00:00 UTC>,
#  #<Biz::TimeSegment start_time=2015-05-02 17:00:00 UTC end_time=2015-05-02 21:00:00 UTC>,
#  #<Biz::TimeSegment start_time=2015-04-30 20:00:00 UTC end_time=2015-05-01 00:00:00 UTC>,
#  #<Biz::TimeSegment start_time=2015-04-30 16:00:00 UTC end_time=2015-04-30 19:00:00 UTC>,
#  #<Biz::TimeSegment start_time=2015-04-29 16:00:00 UTC end_time=2015-04-30 00:00:00 UTC>,
#  #<Biz::TimeSegment start_time=2015-04-28 07:00:00 UTC end_time=2015-04-29 07:00:00 UTC>,
#  #<Biz::TimeSegment start_time=2015-04-27 20:36:57 UTC end_time=2015-04-28 00:00:00 UTC>]

Day calculation semantics

Unlike seconds, minutes, or hours, a "day" is an ambiguous concept, particularly in relation to the vast number of potential schedule configurations. Because of that, day calculations are implemented with the principle of making the logic as straightforward as possible while knowing not all use cases will be satisfied out of the box.

Here's the logic that's followed:

Find the next day that contains business hours. Starting from the same minute on that day as the specified time, look forward (or back) to find the next moment in time that is in business hours.

Schedule intersection

An intersection of two schedules can be found using &:

schedule_1 = Biz::Schedule.new do |config|
  config.hours = {
    mon: {'09:00' => '17:00'},
    tue: {'10:00' => '16:00'},
    wed: {'09:00' => '17:00'},
    thu: {'10:00' => '16:00'},
    fri: {'09:00' => '17:00'},
    sat: {'11:00' => '14:30'}
  }

  config.shifts = {
    Date.new(2016, 7, 1) => {'10:00' => '13:00', '15:00' => '16:00'},
    Date.new(2016, 7, 2) => {'14:00' => '17:00'}
  }

  config.breaks = {
    Date.new(2016, 6, 2) => {'09:00' => '10:30', '16:00' => '16:30'},
    Date.new(2016, 6, 3) => {'12:15' => '12:45'}
  }

  config.holidays = [Date.new(2016, 1, 1), Date.new(2016, 12, 25)]

  config.time_zone = 'Etc/UTC'
end

schedule_2 = Biz::Schedule.new do |config|
  config.hours = {
    sun: {'10:00' => '12:00'},
    mon: {'08:00' => '10:00'},
    tue: {'11:00' => '15:00'},
    wed: {'16:00' => '18:00'},
    thu: {'11:00' => '12:00', '13:00' => '14:00'}
  }

  config.shifts = {
    Date.new(2016, 7, 1) => {'15:30' => '16:30'},
    Date.new(2016, 7, 5) => {'14:00' => '18:00'}
  }

  config.breaks = {
    Date.new(2016, 6, 3) => {'13:30' => '14:00'},
    Date.new(2016, 6, 4) => {'11:00' => '12:00'}
  }

  config.holidays = [
    Date.new(2016, 1, 1),
    Date.new(2016, 7, 4),
    Date.new(2016, 11, 24)
  ]

  config.time_zone = 'America/Los_Angeles'
end

schedule_1 & schedule_2

The resulting schedule will be a combination of the two schedules: an intersection of the intervals, a union of the breaks and holidays, and the time zone of the first schedule. Any configured shifts will be disregarded.

For the above example, the resulting schedule would be equivalent to one with the following configuration:

Biz::Schedule.new do |config|
  config.hours = {
    mon: {'09:00' => '10:00'},
    tue: {'11:00' => '15:00'},
    wed: {'16:00' => '17:00'},
    thu: {'11:00' => '12:00', '13:00' => '14:00'}
  }

  config.shifts = {
    Date.new(2016, 7, 1) => {'15:30' => '16:00'},
    Date.new(2016, 7, 5) => {'14:00' => '16:00'}
  }

  config.breaks = {
    Date.new(2016, 6, 2) => {'09:00' => '10:30', '16:00' => '16:30'},
    Date.new(2016, 6, 3) => {'12:15' => '12:45', '13:30' => '14:00'},
    Date.new(2016, 6, 4) => {'11:00' => '12:00'}
  }

  config.holidays = [
    Date.new(2016, 1, 1),
    Date.new(2016, 7, 4),
    Date.new(2016, 11, 24),
    Date.new(2016, 12, 25)
  ]

  config.time_zone = 'Etc/UTC'
end

Core extensions

Optional extensions to core classes (Date, Integer, and Time) are available for additional expressiveness:

require 'biz/core_ext'

75.business_seconds.after(Time.utc(2015, 3, 5, 12, 30))

30.business_minutes.before(Time.utc(2015, 1, 1, 11, 45))

5.business_hours.after(Time.utc(2015, 4, 7, 8, 20))

3.business_days.before(Time.utc(2015, 5, 9, 4, 12))

Time.utc(2015, 8, 20, 9, 30).business_hours?

Time.utc(2016, 6, 3, 12).on_break?

Time.utc(2014, 1, 1, 12).on_holiday?

Date.new(2015, 12, 10).business_day?

Contributing

Pull requests are welcome, but consider asking for a feature or bug fix first through the issue tracker. When contributing code, please squash sloppy commits aggressively and follow Tim Pope's guidelines for commit messages.

There are a number of ways to get started after cloning the repository.

To set up your environment:

script/bootstrap

To run the spec suite:

script/spec

To open a console with the gem and sample schedule loaded:

script/console

Alternatives

Copyright and license

Copyright 2015-19 Zendesk

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this gem except in compliance with the License.

You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

biz's People

Contributors

craiglittle avatar danielstjules avatar dependabot-support avatar everops-john avatar joshk avatar joshlam avatar razumau avatar take avatar westonganger avatar zenspider avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

biz's Issues

TypeError Exception when using Ruby 2.4.0

When running the following code (Rails 5.0.1):

Biz.time(1, :hours).after(Time.zone.now)
*** TypeError Exception: initialize_copy should take same class object

Biz configuration:

#<Biz::Schedule:0x007ff934d11fc0
 @configuration=
  #<Biz::Configuration:0x007ff934d13208
   @raw=
    #<struct Biz::Configuration::Raw
     hours=
      {:mon=>{"09:00"=>"17:00"},
       :tue=>{"09:00"=>"17:00"},
       :wed=>{"09:00"=>"17:00"},
       :thu=>{"09:00"=>"17:00"},
       :fri=>{"09:00"=>"17:00"}},
     breaks=[],
     holidays=[],
     time_zone="Etc/UTC">>>

Inner rounding TimeSegments

I would like to be able to get the biggest rounded TimeSegment contained in a TimeSegment.
We should be able to round using :hour, :minute and :second.
An example for hour only :

    def inner_round(:hour)
      new((@start_time + 1.hour).beginning_of_hour, @end_time.beginning_of_hour)
    end

NoMethodError: undefined method `on' for #<Biz::Periods::Proxy:0x007f9b164d5d10>

Thanks for this great gem.


      biz = Biz::Schedule.new do |config|
        config.hours = {
          mon: {'09:00' => '17:00'},
          tue: {'00:00' => '24:00'},
          wed: {'09:00' => '17:00'},
          thu: {'09:00' => '12:00', '13:00' => '17:00'},
          sat: {'10:00' => '14:00'}
        }
        config.holidays = [Date.new(2014, 1, 1), Date.new(2014, 12, 25)]
        config.time_zone = 'Asia/Jerusalem'
      end
     biz.periods.on(Date.today)

gives me:
NoMethodError: undefined method `on' for #Biz::Periods::Proxy:0x007f9b164d5d10

How to handle cases where two schedules don't intersect at all?

Description:
I want to check whether two schedules intersect (or overlap) with each other. I can simply use schedule1 & schedule2 which would return me the combined schedule which would be the intersection of the provided schedules.

Problem:
The intersection ability works great when schedules intersect, but when intersecting two non-overlapping, it throws following error:

Biz::Error::Configuration: hours not provided

While this is technically correct, as the non-overlapping schedules (in my case) the hours component didn't overlapped at all, thus resulting in an nil for hours in the resulting combined schedule. But from usability standpoint, this actually does not lets me serve my use case which is checking for overlapping schedules.

Suggestions:
I'm not sure what would be the implications of this, but shouldn't intersecting two completely exclusive objects should return {} instead of nil here? I can certainly attempt to rescue the raised error in my case, but that doesn't seems efficient and fully error free at all. Can there be any other ways to do this?

Thanks!

Hours normalization

While working with the gem I realized that it does not support hours in the following format:

  {
    mon: { "22:00" => "02:00" },  # because the period spans multiple days
    wed: { "21:00" => "00:00" }  # because the end of the interval should be 24:00 instead of 00:00
  }

My data source has output in the form and I wanted that functionality baked in the gem so I made some changes to it, which "normalize" the input before doing the processing.

I don't know if such a functionality is helpful for you therefore, I did not open a PR. Let me know if you'd like such a feature to be integrated into the gem, and I can help with that if needed.

Thanks!

ForDuration calculation with 0 raises error

I just ran into an issue where I tried to calculate 0 days after a given time, and got an error.

schedule = Biz::Schedule.new do |config|
  config.hours = {
    mon: { '07:00' => '18:00' },
    tue: { '07:00' => '18:00' },
    wed: { '07:00' => '18:00' },
    thu: { '07:00' => '18:00' },
    fri: { '07:00' => '18:00' }
  }
end

Biz::Calculation::ForDuration.days(schedule, 0).after(Time.now)
NoMethodError: undefined method `year' for nil:NilClass
from /Users/Matt/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/biz-1.5.1/lib/biz/time.rb:80:in `on_date'

It looks like the issue is here:

def after(date)
  schedule.after(date).take(number_of_days).to_a.last
end

Where it's taking 0 from the array. Is this a case you would like to handle? If not, it might be good to add a descriptive error, because it took a little while to hunt down the problem.

The ability to know the next business time after X days of a certain time

Hi thanks for the gem! Really well organized and clean, I love it.

I've been using the gem but I found out that I can't calculate the next business time after X days of a certain time.
Thought that Biz.time(X, :days).after(certain_time) will do it but seems like it's returning the end of the business time after X + 1 business day.

irb(main):115:0> Biz.configure do |config|
irb(main):116:1*   config.hours = {
irb(main):117:2*     mon: {'09:00' => '17:00'},
irb(main):118:2*     tue: {'09:00' => '17:00'},
irb(main):119:2*     wed: {'09:00' => '17:00'},
irb(main):120:2*     thu: {'09:00' => '17:00'},
irb(main):121:2*     fri: {'09:00' => '17:00'}
irb(main):122:2>   }
irb(main):123:1>
irb(main):124:1*   config.holidays = []
irb(main):125:1>
irb(main):126:1*   config.time_zone = 'Etc/GMT'
irb(main):127:1> end
=> #<Biz::Schedule:0x007ffc62cc4870 @configuration=#<Biz::Configuration:0x007ffc62cc47f8 @raw=#<struct Biz::Configuration::Raw hours={:mon=>{"09:00"=>"17:00"}, :tue=>{"09:00"=>"17:00"}, :wed=>{"09:00"=>"17:00"}, :thu=>{"09:00"=>"17:00"}, :fri=>{"09:00"=>"17:00"}}, holidays=[], time_zone="Etc/GMT">>>
irb(main):128:0> time = Time.utc(2015, 3, 9, 17, 1)
=> 2015-03-09 17:01:00 UTC
irb(main):129:0> time.business_hours?
=> false
irb(main):130:0> Biz.time(1, :days).after(time)
=> 2015-03-12 17:00:00 UTC
irb(main):131:0> Biz.time(1, :days).after(time).business_hours?
=> false

In above's case, I expected Biz.time(1, :days).after(time) to return 2015-03-11 09:00:00 UTC,
since 1 business day after 2015-03-09 17:01:00 UTC is the next business time after 2015-03-10 17:00:00 UTC.

I guess Biz.time(X, :days) isn't officially supported?(don't see them in the docs, and in the core extensions)

Thanks!

Store configuration in a database

Hi,

is it possible to store the configuration somewhere as JSON or serialized ruby, and then apply this configuration in a new schedule?

I'm in the situation where I have multiple business hours for multiple shops and they should be able to edit them.

If this is not currently implemented, can you give me a hint how to do it? I would be happy to do a PR.

thanks!

TimeZone Support

Is there a way to calculate using in_hours? based on the given date/time object without having to configure the time_zone for each evaluations? Showing an example would be best:

Assuming config.time_zone = "America/Los_Angeles" is set.

irb> start # start time in los angeles
=> Fri, 20 Mar 2015 15:00:00 PDT -07:00
irb> end # end time in los angeles
=> Fri, 20 Mar 2015 20:00:00 PDT -07:00

irb> Biz.in_hours?(start)
=> true
irb> Biz.in_hours?(end)
=> true

All good so far. Let's focus on New York; leaving the current time_zone configuration alone temporarily.

irb> start # start time in new york
=> Fri, 20 Mar 2015 15:00:00 EDT -04:00
irb> end # end time in new york
=> Fri, 20 Mar 2015 20:00:00 EDT -04:00

irb> Biz.in_hours?(start)
=> true
irb> Biz.in_hours?(end)
=> true # should be false

It would be nice to have Biz automatically adjust the time_zone configuration BASED on the given date/time object which is EDT -04:00. I had to adjust the time_zone on the fly like so:

irb> Biz.configure { |c| c.time_zone = "America/New_York" }
irb> Biz.in_hours?(start)
=> true
irb> Biz.in_hours?(end)
=> false # yay, that worked

Is there a way to have Biz automatically calculate based on the given date/time object like PDT vs EDT?

My fear is with having to do Biz.configure { |c| c.time_zone = "America/New_York" } would cause time zone collision when another thread needs to set it to something else causing skewed results? I appreciate any tips/advice especially when my app deals with timezones all over the world.

Lastly but not least, thanks for making this gem available to us!

Business day calculation on weekend ignores time

2.3.1 :028 > biz = Biz::Schedule.new do |config|
2.3.1 :029 >     config.time_zone = "America/New_York"
2.3.1 :030?>   end
 => #<Biz::Schedule:0x007fa5cd591eb8 @configuration=#<Biz::Configuration:0x007fa5cd591e40 @raw=#<struct Biz::Configuration::Raw hours={:mon=>{"09:00"=>"17:00"}, :tue=>{"09:00"=>"17:00"}, :wed=>{"09:00"=>"17:00"}, :thu=>{"09:00"=>"17:00"}, :fri=>{"09:00"=>"17:00"}}, breaks=[], holidays=[], time_zone="America/New_York">>> 

2.3.1 :031 > time = Time.parse("Sat, 06 Aug 2016 13:30:01 EDT -04:00")
 => 2016-08-06 13:30:01 -0400 

2.3.1 :032 > biz.time(1, :seconds).after(time)
 => 2016-08-08 13:00:01 UTC 

2.3.1 :033 > biz.time(1, :seconds).after(time).in_time_zone("America/New_York")
 => Mon, 08 Aug 2016 09:00:01 EDT -04:00 

2.3.1 :034 > biz.time(1, :hour).after(time).in_time_zone("America/New_York")
 => Mon, 08 Aug 2016 10:00:00 EDT -04:00 

2.3.1 :035 > biz.time(1, :day).after(time).in_time_zone("America/New_York")
 => Mon, 08 Aug 2016 13:30:01 EDT -04:00 

This final example here is unintuitive to me. Unlike seconds and hours, day is ignoring the time aspect of the datetime. I would expect the result to be end of day Monday / beginning of day Tuesday.

In short, asking for days after on any time during a weekend will ignore the time component. So the answer it gives isn't really a full business day after- it's only half a business day after.

However, I see this same behavior in other business day gems as well (working_hours, and business_time), so I'm not sure if this is a bug or just thinking about weekend originating business day calculations in the wrong way.

One workaround we've discussed is to use seconds calculations instead of day calculations. Alternatively, we could manually fast forward if we occur on a weekend to the beginning of the next working day.

Any thoughts- is this a bug or should I be thinking about business days differently?

Split Timeline in TimeSegment with fixed duration

I would like to be able to split a timeline in multiple TimeSegment with a specific Biz::Duration.
It's similar to partitioning a TimeSegment in multiple TimeSegments.
see : Lists.partition in Java

Biz.periods
  .before(Time.utc(2015, 5, 5, 12, 34, 57)).timeline
  .for(Biz::Duration.minutes(3_598))
  .each_slice(Biz::Duration.minutes(120)).to_a

#=> [#<Biz::TimeSegment start_time=2015-05-05 11:00:00 UTC end_time=2015-05-05 12:34:57 UTC>,
#  #<Biz::TimeSegment start_time=2015-05-05 10:00:00 UTC end_time=2015-05-05 12:00:00 UTC>,
#  #<Biz::TimeSegment start_time=2015-05-05 09:00:00 UTC end_time=2015-05-05 11:00:00 UTC>,
#  #<Biz::TimeSegment start_time=2015-05-05 08:00:00 UTC end_time=2015-05-05 10:00:00 UTC>,
#  #<Biz::TimeSegment start_time=2015-05-05 07:00:00 UTC end_time=2015-05-05 09:00:00 UTC>,
...]

Times as Holidays?

Hi Craig, firstly thanks for this gem really loving it so far.

I do have a question relating to holidays though. Is it possible to have a Time or Period as a holiday instead of the whole Day/Date?

I'm trying to use the gem as time slots for individual members of staff. As an example, a staff member might be booked between 9:00am–9:30am and 2:00pm–2:15pm, I was thinking I could use the holidays functionality within the gem to to add those time slots as "holidays" so that I could find the next available working hour for that member of staff.

Hope this makes sense?

Thanks,
Chris

Empty hash for config.hours

I would like to be able to do

irb(main):010:0> Biz::Schedule.new do |config|
irb(main):011:1* config.hours = {}
irb(main):012:1> end
Biz::Error::Configuration: Hours must be provided.

tricking the system with config.hours = {mon: {}} doesn't seem to be working and ends up in an infinite loop.

Short days before holiday

Is there any way or any plans to add work hours to specific dates? In our country, we have short days ( usually minus 1 hour) in a day before each holiday.

Bug with Biz.time(...).before(...) calculations

Hi,

Given this configuration

schedule = Biz::Schedule.new do |config| 
  config.hours = {
    mon: { '09:30' => '16:00' },
    tue: { '09:30' => '16:00' },
    wed: { '09:30' => '16:00' },
    thu: { '09:30' => '16:00' },
    fri: { '09:30' => '16:00' }
  }
  config.holidays = []
  config.breaks = {}
  config.time_zone = 'America/New_York'
end

sunday_beginning_of_the_day = ActiveSupport::TimeZone.new('America/New_York').local(2016,6,12,0,1,0)
sunday_end_of_the_day = ActiveSupport::TimeZone.new('America/New_York').local(2016,6,12,23,59,0)

puts schedule.time(1, :day).before(sunday_beginning_of_the_day)
puts schedule.time(1, :day).before(sunday_end_of_the_day)

it will print two different days

2016-06-09 20:00:00 UTC
2016-06-10 20:00:00 UTC

and since Sunday is free day then it should return the same day (thursday or friday it's debatable) regarding what hour we pass.

I dig a little and in for_duration.rb file the advanced_time returns Friday both times but with different hours and then the periods calculation happen and the problem seems to be somewhere there.

dates.days(0).after returns nil

To calculate the next business day as a Date, I am using the suggestion from #82 . This is working fine until I try to add a zero duration: Biz.dates.days(0).after(Date.today). I would expect to just return the given date, or to return the next business day if the provided date is on the weekend. Instead, nil is returned. Is this expected behavior?

The ability to specify work days? :)

Hi,

Just come across this gem and I was wondering whether it's on your roadmap to add support for specifying weekend days as working days.

Thanks! :)

Incorrect periods and working hours when shifts and hours fall on same day

I am trying to create a schedule where a person works on every Thursday 9:00 a.m. - 12:00 pm. and has shifts every alternate Thursday from 1:00 p.m. - 5:00 p.m.

Sample Code:

date = Date.new(2021, 8, 5)

biz_schedule = Biz::Schedule.new do |config|
  config.hours = {
    thu: { "09:00" => "12:00" }
  }
  config.shifts = {
    Date.new(2021, 8, 5) => { "13:00" => "17:00" },
    Date.new(2021, 8, 19) => { "13:00" => "17:00" }
  }
end

hours = biz_schedule.within(
  date.beginning_of_day.utc,
  date.end_of_day.utc
).in_seconds
periods = biz_schedule.periods.on(date)&.to_a

puts "hours #{hours}"
puts "periods #{periods.inspect}"

Expected:
Every alternate Thursday (when there is a shift), I expect two periods (9 - 12, 13 - 17) and working hours to be 7 hours.

Actual:
Every alternate Thursday (when there is a shift), I get one period corresponding to shift (13 - 17) and working hours to be 4 hours.

hours 14400
periods [#<Biz::TimeSegment:0x00007fdff42c3130 @start_time=2021-08-05 13:00:00 UTC, @end_time=2021-08-05 17:00:00 UTC, @date=Thu, 05 Aug 2021>]

Version:
1.8.2

Calculations starting on non-business days

I'm switching over to biz from business_time, and an issue I ran into was that calculations behave differently when the start time is not a business day. biz calculates a duration starting on Sunday as if it had started from Friday, whereas business_time treats it as if it started on Monday. For example:

schedule = Biz::Schedule.new do |config|
  config.hours = {
    mon: { '07:00' => '18:00' },
    tue: { '07:00' => '18:00' },
    wed: { '07:00' => '18:00' },
    thu: { '07:00' => '18:00' },
    fri: { '07:00' => '18:00' }
  }
end

friday = Time.new(2016, 4, 1, 12)
sunday = Time.new(2016, 4, 3, 12)

Biz::Calculation::ForDuration.days(schedule, 1).after(sunday)
#=> 2016-04-04 12:00:00 UTC

Biz::Calculation::ForDuration.days(schedule, 1).after(friday)
#=> 2016-04-04 12:00:00 UTC

And with business_time:

1.business_day.after(sunday)
#=> Tue, 05 Apr 2016 12:00:00 UTC +00:00

1.business_day.after(friday)
#=> Mon, 04 Apr 2016 12:00:00 UTC +00:00

I'm using time calculations for some compliance features, and I confirmed that the business_time behavior is correct. You can see their thinking in this comment. Would you be willing to change this behavior?

Intersecting two Biz::Schedule

Hi,

I would like to create Biz::Schedule as an intersection of two other Biz::Schedule.
They should have the same time_zone, the union of holidays and the intersection of hours.

Is that already implemented or should I do it ?
Thanks.

Specify extra working periods

The similar situation has already been discussed in Issue #5.

Sometimes when there's a holiday on Tuesday (or Thursday), we tend to set Monday (or Friday) as holidays as well to give people a continuous holidays. And then set the next Saturday as

In #5, you mentioned that we can directly set business hours for Saturday then set every Saturday that is not a working day as holidays, which will calculate business hours correctly in this case.

But since feeding a lot of holidays into configuration might slow down calculation, and it's not intuitive since Saturday as working day is an exceptional conditions, marking every Saturday as working day seems not an elegant way.

So I am wondering whether there's any possibility that Biz can provide a configuration options for us to set a day that is not originally a business hours as working day.

Timezone not included in returned time objects

Hi,

I'm not sure if this is intentional or I'm missing something. But I noticed that returned time objects from biz do not include the configured time zone:

Biz.configure do |config|
  config.hours = { mon: { "09:00" => "17:00" } }
  config.time_zone = "Europe/Berlin"
end

Biz.time(0, :hours).after(Time.zone.now)
=> "2018-10-15T07:00:00.000Z"
Biz.time(0, :hours).after(Time.zone.now).zone
=> "UTC"

Is this the intended behaviour? I could imagine in order to support multiple timezones, one could rely on a common ground of truth like the UTC time.

So, is it up to the user to convert the returned UTC times to a specific time zone, or am I doing something wrong?

NoMethodError: undefined method `wday' for nil:NilClass

Hi guys,
I have next simple configuration:

biz = Biz::Schedule.new do |config|
  config.hours = {
          mon: {'09:00' => '17:00'},
          tue: {'08:00' => '17:00'},
          wed: {'09:00' => '17:00'},
          thu: {'09:00' => '12:00', '13:00' => '17:00'},
          fri: {'08:00' => '14:00'},
          sat: {'10:00' => '14:00'}
  }
  config.time_zone = 'Pacific/Auckland'
end

call next statement

biz.periods.after(Time.current).timeline.until(Time.current + 1.week).to_a

throw error:

NoMethodError: undefined method `wday' for nil:NilClass
from /Users/aprotsyk/.rvm/gems/ruby-2.3.1@panacea2/gems/biz-1.7.0/lib/biz/time.rb:94:in `during_week'

I can't figure out what I'm doing wrong
Thanks

Rails - global configuration doesn't work

I try to use it with Rails.
Created config/initializers/biz.rb

Biz.configure do |config|
  config.hours = {
    mon: {'10:00' => '19:00'},
    tue: {'10:00' => '19:00'},
    wed: {'10:00' => '19:00'},
    thu: {'10:00' => '19:00'},
    sat: {'10:00' => '19:00'}
  }

  config.holidays =  [Date.new(2016, 1, 1), Date.new(2017, 1, 1)]

  config.time_zone = 'Europe/Kiev'

end

Biz::Schedule.new do |config|
  config.hours = {
    mon: {'10:00' => '19:00'},
    tue: {'10:00' => '19:00'},
    wed: {'10:00' => '19:00'},
    thu: {'10:00' => '19:00'},
    sat: {'10:00' => '19:00'}
  }

  config.holidays =  [Date.new(2016, 1, 1), Date.new(2017, 1, 1)]

  config.time_zone = 'Europe/Kiev'
end

In config/application.rb added

require 'biz'
require 'biz/core_ext'

But when try to use Biz in view I got exception: ActionView::Template::Error (Biz has not been configured)

Works only if configure Biz in Controller method.

How to configure it to use with Rails?

Find the next busniess day during work hours

Hey,
Not sure this is an issue or by design, but once try get next busniess day during work hours, you get today busniess day instead of the next day.

for example,
if your busniess day on 2016/1/8 is from 08:00 am till 16:00pm
Biz.time(0, :hours).after(Time.utc(2016, 1, 8, 20))
will give you next busniess day 2016/2/8 as expected but:
Biz.time(0, :hours).after(Time.utc(2016, 1, 8, 14))
will give you 2016/1/8 date which is toda'y date. actually I expected to get the next busniess day anyway:)

If this is a by design issue, so my bad and this is just a question of how can I get next busniess day during work hours?

Times spanning over midnight (nonsensical hours provided)

How would one deal with opening hours like 17:00 - 01:00? When I try to configure this, I get an error:

   Biz::Schedule.new do |config|
     config.hours = {
       mon: { "17:00" => "01:00" }
     }
   end
 
 Biz::Error::Configuration:
   nonsensical hours provided
 # /usr/local/bundle/gems/biz-1.8.1/lib/biz/validation.rb:32:in `check'
 # /usr/local/bundle/gems/biz-1.8.1/lib/biz/validation.rb:15:in `block in perform'
 # /usr/local/bundle/gems/biz-1.8.1/lib/biz/validation.rb:15:in `each'
 # /usr/local/bundle/gems/biz-1.8.1/lib/biz/validation.rb:15:in `perform'
 # /usr/local/bundle/gems/biz-1.8.1/lib/biz/validation.rb:7:in `perform'
 # /usr/local/bundle/gems/biz-1.8.1/lib/biz/configuration.rb:13:in `initialize'
 # /usr/local/bundle/gems/biz-1.8.1/lib/biz/schedule.rb:9:in `new'
 # /usr/local/bundle/gems/biz-1.8.1/lib/biz/schedule.rb:9:in `initialize'
 # ./spec/lib/opening_hours/schedule_spec.rb:66:in `new'
 # ./spec/lib/opening_hours/schedule_spec.rb:66:in `block (3 levels) in <top (required)>'

Is this something that is planned? Or can I use some kind of workaround for this use case?

in_hours? doesn't consider breaks

Hi,

It looks like in_hours? implementation doesn't consider breaks. I think this change in the in the lib/biz/calculation/active.rb will fix it:

def result
  schedule.intervals.any? { |interval| interval.contains?(time) } &&
    schedule.breaks.none? { |b| b.contains?(time) } &&
    schedule.holidays.none? { |holiday| holiday.contains?(time) }
end

but before creating pull-request I want to make sure that this isn't somehow intentional?

Creating empty Biz::Schedule

Hi,

before 1.6.0, I was using

    Biz::Schedule.new do |config|
      config.hours = {mon:{"00:00"=>"00:00"}}
      config.breaks = {}
      config.holidays = []
      config.time_zone = "Europe/Paris"
    end

but now, it leads to infinite loop when calling #within on this Schedule.
Could you provide a simple way to generate an empty Schedule ?

Period end time is not business hours

I just found that the end_time of a period is not considered a business hour.

schedule = Biz::Schedule.new do |config|
  config.hours = {
    mon: { '07:00' => '18:00' },
    tue: { '07:00' => '18:00' },
    wed: { '07:00' => '18:00' },
    thu: { '07:00' => '18:00' },
    fri: { '07:00' => '18:00' }
  }
end

wednesday_night = Time.utc(2016, 4, 13, 20)
#=> 2016-04-13 20:00:00 UTC

end_of_business = schedule.periods.before(wednesday_night).first.end_time
#=> 2016-04-13 18:00:00 UTC

schedule.business_hours?(end_of_business)
#=> false

I didn't dig too deeply, but it looks like the issue is here (start_time...end_time).cover? where it's not including the end_time in the range. Before I made a PR to change this, I wanted to check if this exclusion was intentional. Thanks!

Can you give me a pointer...

Hey Craig,

I want to use Biz to create a list of forthcoming time slots within business hours. For example, if I have business hour monday-friday 10am - 3pm, I want to be able to offer a list of slots of 2 hours, every hour until closing, i.e. mon: 10-12, 11-13, 12-4, 13-15, and onwards thru tuesday etc.

Not asking for a full solution, but could you give me a hint of where to start?

Thanks.
James

TDD + Biz

We are using TDD and we are trying to find out a way to mock dates in a way that works with biz. Do you know any solution that would work?

In particular we have a "expire date" set in working hours in our task model and we need to test our methods to see if a task is expired or not and all the things that are done if so.

Thanks!

Documentation for setting global config in Rails

Could you please include an example for global config for Rails in the README. Im not quite sure the best way to go about this.

I see the gem biz-rails for easier integration has been created but no work has been done.

Strange behavior with 24-hour work days

I'm using biz on a project that doesn't really have a concept of shifts or working hours (just work days), and for the most part, schedules in full days. As a result, we work mostly with midnights on the start of a given work day. I'm running into a strange issue when trying to find the work day before a given midnight, when the returned time should be midnight on a Monday. Check out these examples:

Biz.configure do |config|
  config.hours = {
    mon: {'00:00' => '24:00'},
    tue: {'00:00' => '24:00'},
    wed: {'00:00' => '24:00'},
    thu: {'00:00' => '24:00'},
    fri: {'00:00' => '24:00'}
  }
end

monday = Time.utc(2019, 1, 14)

Biz.time(1, :days).before(monday) #=> 2019-01-11 00:00:00 UTC
Biz.time(2, :days).before(monday) #=> 2019-01-10 00:00:00 UTC
Biz.time(4, :days).before(monday) #=> 2019-01-08 00:00:00 UTC
Biz.time(5, :days).before(monday) #=> 2019-01-05 00:00:00 UTC <- Expected 1/7/19
Biz.time(0, :days).before(monday) #=> 2019-01-12 00:00:00 UTC <- Expected 1/14/19
Biz.time(0, :days).before(monday + 1) #=> 2019-01-14 00:00:01 UTC <- Correct

tuesday = Time.utc(2019, 1, 15)

Biz.time(0, :days).before(tuesday) #=> 2019-01-15 00:00:00 UTC
Biz.time(1, :days).before(tuesday) #=> 2019-01-12 00:00:00 UTC <- Expected 1/14/19
Biz.time(1, :day).before(tuesday + 1) #=> 2019-01-14 00:00:01 UTC <- Correct
Biz.time(5, :days).before(tuesday) #=> 2019-01-08 00:00:00 UTC

All is well up until the returned time should be midnight on a Monday, at which point biz returns midnight on the prior Saturday. When we want 5 work days before midnight on Monday 1/14/19, we get midnight on Saturday 1/5/19 (a non work day) instead of midnight on Monday 1/7/19. Similarly, when we want 0 work days before midnight on Monday, 1/14/19 (i.e. itself), we get midnight on Saturday, 1/12/19. This happens when starting with any day, not just Mondays, which is why I included some examples using a Tuesday date.

Is this a bug or am I missing something? The obvious workaround is to avoid midnights, but that feels hacky to me. Any insight would be much appreciated. Thanks!

How to handle 24 hours setup?

Hi,

I'm wondering how you would suggest handling 24h working day? Is this the way to go:

config.hours = {
   mon: { "00:00": "23:59" }
}

If yes, then 23:59:10 would be consider open or closed?

invalid configuration causes app to explode

Just ran into this in my production system.

If you pass in an empty hours hash in config.hours, or if a single day has an empty hash, and you do something like:

biz = Biz::Schedule.new do |config|
  config.hours = {}
end
biz.periods
  .after(local_date.beginning_of_day)
  .timeline
  .forward
  .until(local_date.end_of_day)
  .to_a

then it'll spin forever and blow up your server.

This happened to me because I have subtractive hours, so sometimes it turns out that there can be no hours for a certain timeframe.

help! 在设置shifts之后,时长计算有误,麻烦关注一下

这是我的biz配置:

  Biz::Schedule.new do |config|
    config.hours = {
        mon: {'07:40' => '11:30', '12:30' => '16:40'},
        tue: {'07:40' => '11:30', '12:30' => '16:40'},
        wed: {'07:40' => '11:30', '12:30' => '16:40'},
        thu: {'07:40' => '11:30', '12:30' => '16:40'},
        fri: {'07:40' => '11:30', '12:30' => '16:40'}
    }

    config.shifts = {
        Date.new(2019, 4, 28) => {'07:40' => '11:30', '12:30' => '16:40'},
        Date.new(2019, 4, 29) => {'07:40' => '11:30', '12:30' => '16:40'},
    }
    config.time_zone = 'Asia/Shanghai'
  end

但是在计算时长以及判断是否为工作时段时却出错了:

Time.parse('2019-04-29 10:00').business_hours? # 结果为 false
seconds = Biz.periods.after(start_time).timeline.until(end_time).map(&:duration).reduce(Biz::Duration.new(0), :+).in_seconds.to_f  #结果为0 秒

Infinity loop when no hours are configured

When you configure biz with no hours, it runs into an infinity loop

Biz.configure do |config|
  config.hours = { mon: {} }
end
Biz.periods.on(Date.today).first.start_time
^CInterrupt: 
from /usr/local/bundle/gems/biz-1.8.0/lib/biz/periods/abstract.rb:40:in `block in week_periods'

This is also the case, when you provide every day of the week, but just with an empty hash. I would expect a nil as the return value of Biz.periods.on(Date.today).first when no periods where found.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.