{"id":13878922,"url":"https://github.com/billaul/active_period","last_synced_at":"2025-04-13T09:36:50.712Z","repository":{"id":56842151,"uuid":"357844275","full_name":"billaul/active_period","owner":"billaul","description":"Smart-Period aims to simplify Time-range manipulation ","archived":false,"fork":false,"pushed_at":"2024-07-17T23:28:10.000Z","size":207,"stargazers_count":79,"open_issues_count":0,"forks_count":2,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-17T11:11:19.930Z","etag":null,"topics":["holiday","holiday-calculation","holidays","iterable","periods","range","ruby","ruby-on-rails","time","timerange"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/billaul.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","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":"2021-04-14T09:14:10.000Z","updated_at":"2024-09-22T10:40:59.000Z","dependencies_parsed_at":"2025-01-28T19:34:41.359Z","dependency_job_id":"845b137d-c83a-40a8-aaca-70187f16e049","html_url":"https://github.com/billaul/active_period","commit_stats":{"total_commits":89,"total_committers":1,"mean_commits":89.0,"dds":0.0,"last_synced_commit":"60bd7ae0641cbf391610e4761ea59b0179eb609f"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/billaul%2Factive_period","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/billaul%2Factive_period/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/billaul%2Factive_period/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/billaul%2Factive_period/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/billaul","download_url":"https://codeload.github.com/billaul/active_period/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245761294,"owners_count":20667895,"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":["holiday","holiday-calculation","holidays","iterable","periods","range","ruby","ruby-on-rails","time","timerange"],"created_at":"2024-08-06T08:02:04.324Z","updated_at":"2025-03-27T01:09:38.164Z","avatar_url":"https://github.com/billaul.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# ActivePeriod\n[![Gem Version](https://badge.fury.io/rb/active_period.svg)](https://badge.fury.io/rb/active_period)\n[![Code Climate](https://codeclimate.com/github/billaul/period.svg)](https://codeclimate.com/github/billaul/period)\n[![Inline docs](http://inch-ci.org/github/billaul/period.svg)](http://inch-ci.org/github/billaul/period)\n[![RubyGems](http://img.shields.io/gem/dt/active_period.svg?style=flat)](http://rubygems.org/gems/active_period)\n\nActivePeriod aims to simplify Time-range manipulation.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'active_period'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install active_period\n\n## Usage\n\n**ActivePeriod** was designed to simplify time-range manipulation, specialy with rails (\u003e= 5) and user input   \n\n**Warning** :\n- A time-range take place between two **date** and it's different from an abstract duration of time\n- **ActivePeriod** is limited at full day of time and will always round the starting and ending to the beginning and the ending of the day\n\n\n## Quick view (TL;DR)\n``` ruby\nrequire 'active_period'\n\n# Get all user created today\nUser.where(created_at: Period.today)\n\n# Get how many days there is from the Voyager 2 launch\n('20/07/1977'..Time.now).to_period.days.count\n\n# Are we in 2021 ?\nTime.now.in? Period.year('01/01/2021')\n\n# Boundless period are also supported\nPeriod.new('24/04/1990'..).days.each # =\u003e Enumerable\nPeriod.new(..Time.now).months.reverse_each # =\u003e Enumerable\n\n# Write a date for me (I18n supported)\nPeriod.new('20/01/2017'...'20/01/2021').to_s\n=\u003e \"From the 20 January 2017 to the 20 January 2021 excluded\"\n\n# Get all US holidays in the next week (see holiday's section below)\nPeriod.next_week.holidays(:us)\n```\n\n## Detailed view\n\nThere's two way to create and manipulate a period of time `FreePeriod` and `StandardPeriod`\n\n### FreePeriod of time\n\nYou can declare **FreePeriod** as simply as :\n\n```ruby\n# With Date objects\nPeriod.new(3.month.ago..Date.today)\n\n# or with Strings\nPeriod.new('01/01/2000'...'01/02/2000')\n\n# or with a mix\nPeriod.new('01/01/2000'..1.week.ago)\n\n# with one bound only\nPeriod.new('01/01/2000'..)\n\n# or in a rails Controller with params\nPeriod.new(params[:start_date]..params[:end_date])\n\n# or from a range\n('01/01/2000'...'01/02/2000').to_period\n\n# you can also use [] if .new is too long for you\nPeriod['01/01/2000'...'01/02/2000']\n```\n\n**Note** : `to_period` will always return a **FreePeriod**\n\n**FreePeriod** can be manipulated with `+` and `-`          \nDoing so will move the start **and** the end of the period   \n```ruby\nPeriod.new('01/01/2000'..'05/01/2000') + 3.day\n# is equal to\nPeriod.new('04/01/2000'..'08/01/2000')\n```\n\n### StandardPeriod of time\n\nUsing **StandardPeriod** you are limited to strictly bordered periods of time      \nThese periods are `day`, `week`, `month`, `quarter` and `year`\n\n```ruby\n# To get the week, 42th day ago\nPeriod.week(42.day.ago)\n\n# To get the first month of 2020\nPeriod.month('01/01/2020')\n\n# or if you like it verbious\nActivePeriod::Month.new('01/01/2020')\n\n# or if you need the current week\nPeriod.week(Time.now)\n```\n\n**Note** : If you ask for a `month`, `quarter` of `year`, the day part of your param doesn't matter `01/01/2020` give the same result as `14/01/2020` or `29/01/2020`\n\n**StandardPeriod** can be manipulated with `+` and `-` and will always return a **StandardPeriod** of the same type           \n```ruby\n# Subtraction are made from the start of the period\nPeriod.month('10/02/2000') - 1.day\n# Return the previous month\n\n# Addition are made from the end\nPeriod.month('10/02/2000') + 1.day\n# Return the next month\n\nPeriod.week('10/02/2000') + 67.day\n# Return a week\n```\n**StandardPeriod** also respond to `.next` and `.prev`\n```ruby\nPeriod.month('01/01/2000').next.next.next\n# Return the month of April 2000\n```\n\nYou can quickly access convenient periods of time with `.(last|this|next)_(day|week|month|quarter|year)` and `.yesterday` `.today` `.tomorrow`\n\n```ruby\nPeriod.this_week\n# Same as Period.week(Time.now) but shorter\n\nPeriod.next_month\n# Return the next month\n\nPeriod.last_year\n# Return the last year\n\nPeriod.today\n# No comment\n```\n\n## HasMany\n\n**FreePeriod** and some **StandardPeriod** respond to `.days`, `.weeks`, `.months`, `.quarters` and `.years`    \n\n| HasMany -\u003e [\\\u003cStandardPeriod\u003e] | .days | .weeks | .months | .quarters | .years |\n|-------------------------------|:----:|:-----:|:------:|:--------:|:-----:|\n| FreePeriod                    |   X  |   X   |    X   |     X    |   X   |\n| StandardPeriod::Day           |      |       |        |          |       |\n| StandardPeriod::Week          |   X  |       |        |          |       |\n| StandardPeriod::Month         |   X  |   X   |        |          |       |\n| StandardPeriod::Quarter       |   X  |   X   |    X   |          |       |\n| StandardPeriod::Year          |   X  |   X   |    X   |     X    |       |\n\nCalled from a **FreePeriod** all overlapping **StandardPeriod** are return     \nCalled from a **StandardPeriod** only strictly included **StandardPeriod** are return     \nThese methods return an **ActivePeriod::Collection** implementing **Enumerable**\n\n#### Example\n```ruby\n# The FreePeriod from 01/01/2021 to 01/02/2021 has 5 weeks\nPeriod.new('01/01/2021'...'01/02/2021').weeks.count # 5\n\n# The StandardPeriod::Month for 01/01/2021 has 4 weeks\nPeriod.month('01/01/2021').weeks.count # 4\n\n# How many day in the current quarter\nPeriod.this_quarter.days.count\n\n# Get all the quarters overlapping a Period of time\nPeriod.new(Time.now..2.month.from_now).quarters.to_a\n```\n\n## BelongsTo\n\n**StandardPeriod** respond to `.day`, `.week`, `.month`, `.quarter` and `.year`        \nThese methods return a **StandardPeriod** who include the current period    \n**FreePeriod** does not respond to these methods\n\n| BelongTo -\u003e StandardPeriod | .day | .week | .month | .quarter | .year |\n|----------------------------|:---:|:----:|:-----:|:-------:|:----:|\n| FreePeriod                 |     |      |       |         |      |\n| StandardPeriod::Day        |     |   X  |   X   |    X    |   X  |\n| StandardPeriod::Week       |     |      |   X   |    X    |   X  |\n| StandardPeriod::Month      |     |      |       |    X    |   X  |\n| StandardPeriod::Quarter    |     |      |       |         |   X  |\n| StandardPeriod::Year       |     |      |       |         |      |\n\n#### Example with BelongTo and HasMany\n\n```ruby\n# Get the third day, of the last week, of the second month, of the current year\nPeriod.this_year.months.second.weeks.last.days.third\n```\n\n## Period Combination with `\u0026` and `|`\n\nYou can use `\u0026` to combine overlapping periods     \nAnd `|` to combine overlapping and tail to head periods     \nIf the given periods cannot combine, then `nil` will be return       \nThe period we take the ending date from, determine if the ending date is included or excluded\n\n#### Example for `\u0026`\n```ruby\n# Overlapping periods\n(Period['01/01/2021'..'20/01/2021'] \u0026 Period['10/01/2021'...'30/01/2021']).to_s\n=\u003e \"From the 10 January 2021 to the 20 January 2021 included\"\n\n# Theses period cannot combine\nPeriod.this_month \u0026 Period.next_month\n=\u003e nil\n```\n\n#### Example for `|`\n```ruby\n# Overlapping periods\n(Period['01/01/2021'..'20/01/2021'] | Period['10/01/2021'...'30/01/2021']).to_s\n=\u003e \"From the 01 January 2021 to the 30 January 2021 excluded\"\n\n# Example with tail to head\n(Period.this_month | Period.next_month).to_s\n=\u003e \"From the 01 September 2022 to the 31 October 2022 included\"\n(Period['01/01/2021'..'09/01/2021']  | Period['10/01/2021'..'20/01/2021']).to_s\n=\u003e \"From the 01 January 2021 to the 20 January 2021 included\"\n\n# Theses period cannot combine\nPeriod['01/01/2021'...'09/01/2021'] | Period['10/01/2021'..'20/01/2021']\n=\u003e nil\n```\n\n## Period clamping with `clamp_on`\n\nYou can use `clamp_on` to clamp a `Period`, a date or a range of dates, within a `Period`.\nIf you user `clamp_on` on a range of dates, the return value will also be a range of dates, this is to preserve hours/minutes/second precision\n:warning: The classic `clamp` apply the param to the caller. It's `42.clamp(1..10) -\u003e 10` **and this don't exist** `(1..10).clamp(42)`      \n`clamp_on` reverse this logic, and apply the caller to the params.     \nThis choice has been made to avoid a monkey patch on `clamp`\n:warning: If you `clamp_on` a `range` that don't overlap with the caller, the return value is `nil`\n\n```ruby\nPeriod.today.clamp_on(Period.this_week) \n=\u003e Period.today\n\nPeriod.today.clamp_on(Period.last_week)\n=\u003e nil\n\nPeriod.today.clamp_on(3.from_now)\n=\u003e # 3.from_now or today ending's time\n\n```\n\n## Boundless Period\n\nBoundless period are fully supported and work as you expect them to do    \nThe values `nil`, `''`, `Date::Infinity`, `Float::INFINITY` and `-Float::INFINITY` are supported as start and end\nYou can iterate on the `days`, `weeks`, `months`, `quarters` and `years` of an Endless period\n```ruby\n('01/01/2021'..nil).days.each { ... }\n('01/01/2021'..'').days.each { ... }\n('01/01/2021'..).days.each { ... }\n```\nYou can reverse iterate on the `days`, `weeks`, `months`, `quarters` and `years` of an Beginless period\n```ruby\n(nil..'01/01/2021').days.reverse_each { ... }\n(''..'01/01/2021').days.reverse_each { ... }\n(..'01/01/2021').days.reverse_each { ... }\n```\n\nYou can create an infinite period of time     \nObviously it's not iterable\n```ruby\nPeriod.new(nil..nil).to_s\n=\u003e \"Limitless time range\"\n```\n\nYou can specifically forbid boundless period with `allow_endless`, `allow_beginless` or with `Period.bounded`\n```ruby\nPeriod.new('01/01/2020'..'', allow_endless: false)\nPeriod.bounded('01/01/2020'..)\n=\u003e ArgumentError (The end date is invalid)\n\nPeriod.new(..'01/01/2020', allow_beginless: false)\nPeriod.bounded(..'01/01/2020')\n=\u003e ArgumentError (The start date is invalid)\n```\n\n## ActiveRecord\n\nAs **Period** inherit from **Range**, you can natively use them in **ActiveRecord** query\n\n```ruby\n# Get all book published this year\nBook.where(published_at: Period.this_year)\n\n# Get all users created after 01/01/2020\nUser.where(created_at: ('01/01/2020'..).to_period)\n```\n\n## Rails Controller\n\nIn a Controller, use the error handling to validate the date for you\n\n```ruby\nclass BookController \u003c ApplicationController\n  def between\n    begin\n      # Retrieve books from the DB\n      @books = Book.where(published: Period.bounded(params[:from]..params[:to]))\n    rescue ArgumentError =\u003e e\n      # Period will handle mis-formatted date and incoherent period\n      # I18n is supported for errors messages\n      flash[:alert] = e.message\n    end\n  end\nend\n```\n\n## I18n and to_s\n\nI18n is supported for `en` and `fr`   \n\n```ruby\nPeriod.new('01/01/2000'...'01/02/2001').to_s\n=\u003e \"From the 01 January 2000 to the 31 January 2001 included\"\n\nI18n.locale = :fr\nPeriod.new('01/01/2000'...'01/02/2001').to_s\n=\u003e \"Du 01 janvier 2000 au 31 janvier 2001 inclus\"\n```\nErrors are also supported\n```ruby\nPeriod.new 'Foo'..'Bar'\n=\u003e ArgumentError (The start date is invalid)\n\nPeriod.new '01/02/3030'..'Bar'\nPeriod.bounded '01/02/3030'..\n=\u003e ArgumentError (The end date is invalid)\n\nPeriod.new '01/02/3030'..'01/01/2020'\n=\u003e ArgumentError (The start date is greater than the end date)\n```\n\nSee `locales/en.yml` to implement your language support\n\nIf you need to change the format for a single call\n\n```ruby\n  period.to_s(format: 'Your Format')\n  # or\n  period.strftime('Your Format')\n```\nFor a FreePeriod or if you need to print the start and the end of your period differently, use `.i18n`\n```ruby\n  period.i18n do |from, to, excluded_end|\n    \"You have from #{from.strftime(...)} until #{to.strftime(...)} to deliver the money !\"\n  end\n```\n\n## The tricky case of Weeks\n\nWeeks are implemented following the [ISO 8601](https://en.wikipedia.org/wiki/ISO_week_date)      \nSo `Period.this_month.weeks.first` doesn't necessarily include the first days of the month     \nAlso a **StandardPeriod** and a **FreePeriod** covering the same range of time, may not includes the same `Weeks`\n\n## TimeZone\n\nTime zone are supported\nIf you change the global `Time.zone` of your app\nIf your Period [begin in a time zone and end in another](https://en.wikipedia.org/wiki/Daylight_saving_time), you have nothing to do\n\n## Holidays\n\n`ActivePeriod` include an optional support of the [gem holidays](https://github.com/holidays/holidays)      \nIf your project include this gem you can use the power of `.holidays` and `.holiday?`\n\n`.holiday?` and `.holidays` take the same params as `Holidays.on` except the first one        \n`Holidays.on(Date.civil(2008, 4, 25), :au)` become `Period.day('24/04/2008').holidays(:au)` or `Period.day('24/04/2008').holiday?(:au)`\n\n```ruby\nrequire 'holidays'\n# Get all worldwide holidays in the current month\nPeriod.this_month.holidays\n\n# Get all US holidays in the next week\nPeriod.next_week.holidays(:us)\n\n# Get all US and CA holidays in the prev quarter\nPeriod.prev_quarter.holidays(:us, :ca)\n\n# First up coming `FR` holiday\nholiday = (Time.now..).to_period.holidays(:fr).first\n# return the next holiday with same options as the original `.holidays` collection\nholiday.next\n# return the previous holiday with same options as the original `.holidays` collection\nholiday.prev\n```\n\n:warning:  If you call a `holidays` related method without the [gem holidays](https://github.com/holidays/holidays) in your project    \nYou will raise a `RuntimeError`\n```ruby\nPeriod.this_month.holidays\n#=\u003e RuntimeError (The gem \"holidays\" is needed for this feature to work)\n```\n\n## Planned updates \u0026 Idea\n\n- [ ] ActiveRecord Serializer (maybe)\n- [ ] Dedicated Exception for each possible error\n\n## Bug reports\n\nIf you discover any bugs, feel free to create an [issue on GitHub](https://github.com/billaul/active_period/issues)        \nPlease add as much information as possible to help us in fixing the potential bug     \nWe also encourage you to help even more by forking and sending us a pull request\n\nNo issues will be addressed outside GitHub\n\n## Maintainer\n\n* Myself (https://github.com/billaul)\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbillaul%2Factive_period","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbillaul%2Factive_period","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbillaul%2Factive_period/lists"}