{"id":13483807,"url":"https://github.com/zendesk/biz","last_synced_at":"2025-05-14T23:07:57.630Z","repository":{"id":27455316,"uuid":"30934073","full_name":"zendesk/biz","owner":"zendesk","description":"Time calculations using business hours.","archived":false,"fork":false,"pushed_at":"2024-06-13T11:22:57.000Z","size":414,"stargazers_count":490,"open_issues_count":2,"forks_count":24,"subscribers_count":420,"default_branch":"master","last_synced_at":"2025-05-14T23:07:45.013Z","etag":null,"topics":["computation","dates","holidays","ruby","time"],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zendesk.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-02-17T19:41:30.000Z","updated_at":"2025-05-13T04:17:04.000Z","dependencies_parsed_at":"2022-09-03T14:42:47.971Z","dependency_job_id":"9fb4e81b-14f3-46f7-85d9-9982ce32440e","html_url":"https://github.com/zendesk/biz","commit_stats":{"total_commits":264,"total_committers":11,"mean_commits":24.0,"dds":0.05303030303030298,"last_synced_commit":"d6196cd047a2f669e8a01c315859dcf8503978b3"},"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zendesk%2Fbiz","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zendesk%2Fbiz/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zendesk%2Fbiz/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zendesk%2Fbiz/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zendesk","download_url":"https://codeload.github.com/zendesk/biz/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254243363,"owners_count":22038046,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["computation","dates","holidays","ruby","time"],"created_at":"2024-07-31T17:01:15.476Z","updated_at":"2025-05-14T23:07:52.621Z","avatar_url":"https://github.com/zendesk.png","language":"Ruby","funding_links":[],"categories":["Ruby","Date and Time Processing"],"sub_categories":[],"readme":"![biz](http://d26a57ydsghvgx.cloudfront.net/www/public/assets/images/bizlogo.png)\n\n[![Gem Version](https://badge.fury.io/rb/biz.svg)](http://badge.fury.io/rb/biz)\n![repo-checks](https://github.com/zendesk/biz/workflows/repo-checks/badge.svg)\n[![Code Climate](https://codeclimate.com/github/zendesk/biz/badges/gpa.svg)](https://codeclimate.com/github/zendesk/biz)\n[![Test Coverage](https://codeclimate.com/github/zendesk/biz/badges/coverage.svg)](https://codeclimate.com/github/zendesk/biz)\n\nTime calculations using business hours.\n\n## Features\n\n* Support for:\n  - Multiple intervals per day.\n  - Multiple schedule configurations.\n  - Intervals spanning the entire day.\n  - Holidays.\n  - Breaks (time-segment holidays).\n  - Shifts (date-based intervals).\n* Second-level calculation precision.\n* Seamless Daylight Saving Time handling.\n* Schedule intersection.\n* Thread safety.\n\n## Anti-Features\n\n* No dependency on ActiveSupport.\n* No monkey patching by default.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n    gem 'biz'\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install biz\n\n## Configuration\n\n```ruby\nBiz.configure do |config|\n  config.hours = {\n    mon: {'09:00' =\u003e '17:00'},\n    tue: {'00:00' =\u003e '24:00'},\n    wed: {'09:00' =\u003e '17:00'},\n    thu: {'09:00' =\u003e '12:00', '13:00' =\u003e '17:00'},\n    sat: {'10:00' =\u003e '14:00'}\n  }\n\n  config.shifts = {\n    Date.new(2006, 1, 1) =\u003e {'09:00' =\u003e '12:00'},\n    Date.new(2006, 1, 7) =\u003e {'08:00' =\u003e '10:00', '12:00' =\u003e '14:00'}\n  }\n\n  config.breaks = {\n    Date.new(2006, 1, 2) =\u003e {'10:00' =\u003e '11:30'},\n    Date.new(2006, 1, 3) =\u003e {'14:15' =\u003e '14:30', '15:40' =\u003e '15:50'}\n  }\n\n  config.holidays = [Date.new(2016, 1, 1), Date.new(2016, 12, 25)]\n\n  config.time_zone = 'America/Los_Angeles'\nend\n```\n\nConfigured timestamps must be in either `HH:MM` or `HH:MM:SS` format.\n\nShifts act as exceptions to the hours configured for a particular date; that is,\nif a date is configured with both hours-based intervals and shifts, the shifts\nare in force and the intervals are disregarded.\n\nPeriods occurring on holidays are disregarded. Similarly, any segment of a\nperiod that overlaps with a break is treated as inactive.\n\nIf global configuration isn't your thing, configure an instance instead:\n\n```ruby\nBiz::Schedule.new do |config|\n  # ...\nend\n```\n\nNote that times must be specified in 24-hour clock format and time zones\nmust be [IANA identifiers](http://en.wikipedia.org/wiki/List_of_tz_database_time_zones).\n\nIf you're operating in a threaded environment and want to use the same\nconfiguration across threads, save the configured schedule as a global variable:\n\n```ruby\n$biz = Biz::Schedule.new\n```\n\n## Usage\n\n```ruby\n# Find the time an amount of business time *before* a specified starting time\nBiz.time(30, :minutes).before(Time.utc(2015, 1, 1, 11, 45))\n\n# Find the time an amount of business time *after* a specified starting time\nBiz.time(2, :hours).after(Time.utc(2015, 12, 25, 9, 30))\n\n# Calculations can be performed in seconds, minutes, hours, or days\nBiz.time(1, :day).after(Time.utc(2015, 1, 8, 10))\n\n# Find the previous business time\nBiz.time(0, :hours).before(Time.utc(2016, 1, 8, 6))\n\n# Find the next business time\nBiz.time(0, :hours).after(Time.utc(2016, 1, 8, 20))\n\n# Find the amount of business time between two times\nBiz.within(Time.utc(2015, 3, 7), Time.utc(2015, 3, 14)).in_seconds\n\n# Find the start of the business day\nBiz.periods.on(Date.today).first.start_time\n\n# Find the end of the business day\nBiz.periods.on(Date.today).to_a.last.end_time\n\n# Determine if a time is in business hours\nBiz.in_hours?(Time.utc(2015, 1, 10, 9))\n\n# Determine if a time is during a break\nBiz.on_break?(Time.utc(2016, 6, 3))\n\n# Determine if a time is during a holiday\nBiz.on_holiday?(Time.utc(2014, 1, 1))\n```\n\nThe same methods can be called on a configured instance:\n\n```ruby\nschedule = Biz::Schedule.new\n\nschedule.in_hours?(Time.utc(2015, 1, 1, 10))\n```\n\nAll returned times are in UTC.\n\nIf a schedule will be configured with a large number of holidays and performance\nis a particular concern, it's recommended that holidays are filtered down to\nthose relevant to the calculation(s) at hand before configuration to improve\nperformance.\n\nBy dropping down a level, you can get access to the underlying time segments,\nwhich you can use to do your own custom calculations or just get a better idea\nof what's happening under the hood:\n\n```ruby\nBiz.periods.after(Time.utc(2015, 1, 10, 10)).timeline\n  .until(Time.utc(2015, 1, 17, 10)).to_a\n\n#=\u003e [#\u003cBiz::TimeSegment start_time=2015-01-10 18:00:00 UTC end_time=2015-01-10 22:00:00 UTC\u003e,\n#  #\u003cBiz::TimeSegment start_time=2015-01-12 17:00:00 UTC end_time=2015-01-13 01:00:00 UTC\u003e,\n#  #\u003cBiz::TimeSegment start_time=2015-01-13 08:00:00 UTC end_time=2015-01-14 08:00:00 UTC\u003e,\n#  #\u003cBiz::TimeSegment start_time=2015-01-14 17:00:00 UTC end_time=2015-01-15 01:00:00 UTC\u003e,\n#  #\u003cBiz::TimeSegment start_time=2015-01-15 17:00:00 UTC end_time=2015-01-15 20:00:00 UTC\u003e,\n#  #\u003cBiz::TimeSegment start_time=2015-01-15 21:00:00 UTC end_time=2015-01-16 01:00:00 UTC\u003e]\n\nBiz.periods\n  .before(Time.utc(2015, 5, 5, 12, 34, 57)).timeline\n  .for(Biz::Duration.minutes(3_598)).to_a\n\n#=\u003e [#\u003cBiz::TimeSegment start_time=2015-05-05 07:00:00 UTC end_time=2015-05-05 12:34:57 UTC\u003e,\n#  #\u003cBiz::TimeSegment start_time=2015-05-04 16:00:00 UTC end_time=2015-05-05 00:00:00 UTC\u003e,\n#  #\u003cBiz::TimeSegment start_time=2015-05-02 17:00:00 UTC end_time=2015-05-02 21:00:00 UTC\u003e,\n#  #\u003cBiz::TimeSegment start_time=2015-04-30 20:00:00 UTC end_time=2015-05-01 00:00:00 UTC\u003e,\n#  #\u003cBiz::TimeSegment start_time=2015-04-30 16:00:00 UTC end_time=2015-04-30 19:00:00 UTC\u003e,\n#  #\u003cBiz::TimeSegment start_time=2015-04-29 16:00:00 UTC end_time=2015-04-30 00:00:00 UTC\u003e,\n#  #\u003cBiz::TimeSegment start_time=2015-04-28 07:00:00 UTC end_time=2015-04-29 07:00:00 UTC\u003e,\n#  #\u003cBiz::TimeSegment start_time=2015-04-27 20:36:57 UTC end_time=2015-04-28 00:00:00 UTC\u003e]\n```\n\n#### Day calculation semantics\n\nUnlike seconds, minutes, or hours, a \"day\" is an ambiguous concept, particularly\nin relation to the vast number of potential schedule configurations. Because of\nthat, day calculations are implemented with the principle of making the logic as\nstraightforward as possible while knowing not all use cases will be satisfied\nout of the box.\n\nHere's the logic that's followed:\n\n\u003e Find the next day that contains business hours. Starting from the same minute\n\u003e on that day as the specified time, look forward (or back) to find the next\n\u003e moment in time that is in business hours.\n\n## Schedule intersection\n\nAn intersection of two schedules can be found using `\u0026`:\n\n```ruby\nschedule_1 = Biz::Schedule.new do |config|\n  config.hours = {\n    mon: {'09:00' =\u003e '17:00'},\n    tue: {'10:00' =\u003e '16:00'},\n    wed: {'09:00' =\u003e '17:00'},\n    thu: {'10:00' =\u003e '16:00'},\n    fri: {'09:00' =\u003e '17:00'},\n    sat: {'11:00' =\u003e '14:30'}\n  }\n\n  config.shifts = {\n    Date.new(2016, 7, 1) =\u003e {'10:00' =\u003e '13:00', '15:00' =\u003e '16:00'},\n    Date.new(2016, 7, 2) =\u003e {'14:00' =\u003e '17:00'}\n  }\n\n  config.breaks = {\n    Date.new(2016, 6, 2) =\u003e {'09:00' =\u003e '10:30', '16:00' =\u003e '16:30'},\n    Date.new(2016, 6, 3) =\u003e {'12:15' =\u003e '12:45'}\n  }\n\n  config.holidays = [Date.new(2016, 1, 1), Date.new(2016, 12, 25)]\n\n  config.time_zone = 'Etc/UTC'\nend\n\nschedule_2 = Biz::Schedule.new do |config|\n  config.hours = {\n    sun: {'10:00' =\u003e '12:00'},\n    mon: {'08:00' =\u003e '10:00'},\n    tue: {'11:00' =\u003e '15:00'},\n    wed: {'16:00' =\u003e '18:00'},\n    thu: {'11:00' =\u003e '12:00', '13:00' =\u003e '14:00'}\n  }\n\n  config.shifts = {\n    Date.new(2016, 7, 1) =\u003e {'15:30' =\u003e '16:30'},\n    Date.new(2016, 7, 5) =\u003e {'14:00' =\u003e '18:00'}\n  }\n\n  config.breaks = {\n    Date.new(2016, 6, 3) =\u003e {'13:30' =\u003e '14:00'},\n    Date.new(2016, 6, 4) =\u003e {'11:00' =\u003e '12:00'}\n  }\n\n  config.holidays = [\n    Date.new(2016, 1, 1),\n    Date.new(2016, 7, 4),\n    Date.new(2016, 11, 24)\n  ]\n\n  config.time_zone = 'America/Los_Angeles'\nend\n\nschedule_1 \u0026 schedule_2\n```\n\nThe resulting schedule will be a combination of the two schedules: an\nintersection of the intervals, a union of the breaks and holidays,\nand the time zone of the first schedule. Any configured shifts will be\ndisregarded.\n\nFor the above example, the resulting schedule would be equivalent to one with\nthe following configuration:\n\n```ruby\nBiz::Schedule.new do |config|\n  config.hours = {\n    mon: {'09:00' =\u003e '10:00'},\n    tue: {'11:00' =\u003e '15:00'},\n    wed: {'16:00' =\u003e '17:00'},\n    thu: {'11:00' =\u003e '12:00', '13:00' =\u003e '14:00'}\n  }\n\n  config.shifts = {\n    Date.new(2016, 7, 1) =\u003e {'15:30' =\u003e '16:00'},\n    Date.new(2016, 7, 5) =\u003e {'14:00' =\u003e '16:00'}\n  }\n\n  config.breaks = {\n    Date.new(2016, 6, 2) =\u003e {'09:00' =\u003e '10:30', '16:00' =\u003e '16:30'},\n    Date.new(2016, 6, 3) =\u003e {'12:15' =\u003e '12:45', '13:30' =\u003e '14:00'},\n    Date.new(2016, 6, 4) =\u003e {'11:00' =\u003e '12:00'}\n  }\n\n  config.holidays = [\n    Date.new(2016, 1, 1),\n    Date.new(2016, 7, 4),\n    Date.new(2016, 11, 24),\n    Date.new(2016, 12, 25)\n  ]\n\n  config.time_zone = 'Etc/UTC'\nend\n```\n\n## Core extensions\n\nOptional extensions to core classes (`Date`, `Integer`, and `Time`) are\navailable for additional expressiveness:\n\n```ruby\nrequire 'biz/core_ext'\n\n75.business_seconds.after(Time.utc(2015, 3, 5, 12, 30))\n\n30.business_minutes.before(Time.utc(2015, 1, 1, 11, 45))\n\n5.business_hours.after(Time.utc(2015, 4, 7, 8, 20))\n\n3.business_days.before(Time.utc(2015, 5, 9, 4, 12))\n\nTime.utc(2015, 8, 20, 9, 30).business_hours?\n\nTime.utc(2016, 6, 3, 12).on_break?\n\nTime.utc(2014, 1, 1, 12).on_holiday?\n\nDate.new(2015, 12, 10).business_day?\n```\n\n## Contributing\n\nPull requests are welcome, but consider asking for a feature or bug fix first\nthrough the issue tracker. When contributing code, please squash sloppy commits\naggressively and follow [Tim Pope's guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)\nfor commit messages.\n\nThere are a number of ways to get started after cloning the repository.\n\nTo set up your environment:\n\n    script/bootstrap\n\nTo run the spec suite:\n\n    script/spec\n\nTo open a console with the gem and sample schedule loaded:\n\n    script/console\n\n## Alternatives\n\n* [`business_time`](https://github.com/bokmann/business_time)\n* [`working_hours`](https://github.com/Intrepidd/working_hours)\n\n## Copyright and license\n\nCopyright 2015-19 Zendesk\n\nLicensed under the Apache License, Version 2.0 (the \"License\"); you may not use\nthis gem except in compliance with the License.\n\nYou may obtain a copy of the License at\nhttp://www.apache.org/licenses/LICENSE-2.0.\n\nUnless required by applicable law or agreed to in writing, software distributed\nunder the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\nCONDITIONS OF ANY KIND, either express or implied. See the License for the\nspecific language governing permissions and limitations under the License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzendesk%2Fbiz","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzendesk%2Fbiz","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzendesk%2Fbiz/lists"}