{"id":15581328,"url":"https://github.com/molawson/repeatable","last_synced_at":"2025-10-14T06:31:06.364Z","repository":{"id":27490277,"uuid":"30970371","full_name":"molawson/repeatable","owner":"molawson","description":"Recurring event schedules","archived":false,"fork":false,"pushed_at":"2024-08-02T01:59:06.000Z","size":1474,"stargazers_count":21,"open_issues_count":3,"forks_count":3,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-01-23T00:07:48.530Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://github.com/molawson/repeatable","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/molawson.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","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-18T15:06:02.000Z","updated_at":"2024-05-09T17:14:31.000Z","dependencies_parsed_at":"2024-05-09T00:28:59.131Z","dependency_job_id":"554e51f1-f14d-4aa4-adcc-62799a1a0f7c","html_url":"https://github.com/molawson/repeatable","commit_stats":{"total_commits":165,"total_committers":9,"mean_commits":"18.333333333333332","dds":0.509090909090909,"last_synced_commit":"3ad3da9b3823f1953e7b741353642fb62187fed1"},"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/molawson%2Frepeatable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/molawson%2Frepeatable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/molawson%2Frepeatable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/molawson%2Frepeatable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/molawson","download_url":"https://codeload.github.com/molawson/repeatable/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":236451239,"owners_count":19150826,"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":[],"created_at":"2024-10-02T19:42:30.690Z","updated_at":"2025-10-14T06:31:06.346Z","avatar_url":"https://github.com/molawson.png","language":"Ruby","readme":"# Repeatable\n\n[![CI](https://github.com/molawson/repeatable/actions/workflows/ci.yml/badge.svg)](https://github.com/molawson/repeatable/actions/workflows/ci.yml)\n[![Maintainability](https://qlty.sh/gh/molawson/projects/repeatable/maintainability.svg)](https://qlty.sh/gh/molawson/projects/repeatable)\n[![Test Coverage](https://qlty.sh/gh/molawson/projects/repeatable/coverage.svg)](https://qlty.sh/gh/molawson/projects/repeatable)\n\nRuby implementation of Martin Fowler's [Recurring Events for Calendars](http://martinfowler.com/apsupp/recurring.pdf) paper.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'repeatable'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install repeatable\n\n## Usage\n\n### Building a Schedule\n\nYou can create a schedule in one of two ways.\n\n#### Composed objects\n\nInstantiate and compose each of the `Repeatable::Expression` objects manually.\n\n```ruby\nsecond_monday = Repeatable::Expression::WeekdayInMonth.new(weekday: 1, count: 2)\noct_thru_dec = Repeatable::Expression::RangeInYear.new(start_month: 10, end_month: 12)\nintersection = Repeatable::Expression::Intersection.new(second_monday, oct_thru_dec)\n\nschedule = Repeatable::Schedule.new(intersection)\n```\n\n\n#### Hash\n\nOr describe the same structure with a `Hash`, and the gem will handle instantiating and composing the objects.\n\n```ruby\narg = {\n  intersection: [\n    { weekday_in_month: { weekday: 1, count: 2 } },\n    { range_in_year: { start_month: 10, end_month: 12 } },\n    { exact_date: { date: \"2015-08-01\" } }\n  ]\n}\n\nschedule = Repeatable::Schedule.new(arg)\n```\n\n- - -\n\n#### Time Expressions\n\nThere are a number of time expressions available which, when combined, can describe most any schedule.\n\n```ruby\n# SETS\n\n# Any conditions can be met\n{ union: [] }\nRepeatable::Expression::Union.new(expressions)\n\n# All conditions must be met\n{ intersection: [] }\nRepeatable::Expression::Intersection.new(expressions)\n\n# Date is part of the first set (`included`) but not part of the second set (`excluded`)\n{ difference: { included: expression, excluded: another_expression } }\nRepeatable::Expression::Difference.new(included: expression, excluded: another_expression)\n\n\n# DATES\n\n# Every Sunday\n{ weekday: { weekday: 0 } }\nRepeatable::Expression::Weekday.new(weekday: 0)\n\n# The 3rd Monday of every month\n{ weekday_in_month: { weekday: 1, count: 3 } }\nRepeatable::Expression::WeekdayInMonth.new(weekday: 1, count: 3)\n\n# The last Thursday of every month\n{ weekday_in_month: { weekday: 4, count: -1 } }\nRepeatable::Expression::WeekdayInMonth.new(weekday: 4, count: -1)\n\n# Every other Monday, starting from December 1, 2015\n{ biweekly: { weekday: 1, start_after: '2015-12-01' } }\nRepeatable::Expression::Biweekly.new(weekday: 1, start_after: Date.new(2015, 12, 1))\n\n# The 13th of every month\n{ day_in_month: { day: 13 } }\nRepeatable::Expression::DayInMonth.new(day: 13)\n\n# The last day of every month\n{ day_in_month: { day: -1 } }\nRepeatable::Expression::DayInMonth.new(day: -1)\n\n# All days in October\n{ range_in_year: { start_month: 10 } }\nRepeatable::Expression::RangeInYear.new(start_month: 10)\n\n# All days from October through December\n{ range_in_year: { start_month: 10, end_month: 12 } }\nRepeatable::Expression::RangeInYear.new(start_month: 10, end_month: 12)\n\n# All days from October 1 through December 20\n{ range_in_year: { start_month: 10, end_month: 12, start_day: 1, end_day: 20 } }\nRepeatable::Expression::RangeInYear.new(start_month: 10, end_month: 12, start_day: 1, end_day: 20)\n\n# only December 21, 2012\n{ exact_date: { date: '2012-12-21' } }\nRepeatable::Expression::ExactDate.new(date: Date.new(2012, 12, 21)\n```\n\n#### Schedule Errors\n\nIf something in the argument passed into `Repeatable::Schedule.new` can't be handled by the `Schedule` or `Parser` (e.g. an expression hash key that doesn't match an existing expression class), a `Repeatable::ParseError` will be raised with a (hopefully) helpful error message.\n\n### Getting information from a Schedule\n\nAsk a schedule to do a number of things.\n\n```ruby\nschedule.next_occurrence\n  # =\u003e Date of next occurrence\n\n# By default, it will find the next occurrence after Date.today.\n# You can also specify a start date.\nschedule.next_occurrence(Date.new(2015, 1, 1))\n  # =\u003e Date of next occurrence after Jan 1, 2015\n\n# You also have the option of including the start date as a possible result.\nschedule.next_occurrence(Date.new(2015, 1, 1), include_start: true)\n  # =\u003e Date of next occurrence on or after Jan 1, 2015\n\n# By default, searches for the next occurrence are limited to the next 36,525 days (about 100 years).\n# That limit can also be specified in number of days.\nschedule.next_occurrence(limit: 365)\n  # =\u003e Date of next occurrence within the next 365 days\n\nschedule.occurrences(Date.new(2015, 1, 1), Date.new(2016, 6, 30))\n  # =\u003e Dates of all occurrences between Jan 1, 2015 and June 30, 2016\n\nschedule.include?(Date.new(2015, 10, 10))\n  # =\u003e Whether the schedule has an event on the date given (true/false)\n\nschedule.to_h\n  # =\u003e Hash representation of the Schedule, which is useful for storage and\n  #    can be used to recreate an identical Schedule object at a later time\n```\n\n#### Pattern Matching\n\nBoth `Repeatable::Schedule` and all `Repeatable::Expression` classes support Ruby 2.7+ [pattern matching][ruby-pattern-matching] which is particularly useful for parsing or presenting an existing schedule.\n\n```ruby\ncase schedule\nin weekday: { weekday: }\n  \"Weekly on #{Date::DAYNAMES[weekday]}\"\nin day_in_month: { day: }\n  \"Every month on the #{day.ordinalize}\"\nin weekday_in_month: { weekday:, count: }\n  \"Every month on the #{count.ordinalize} #{Date::DAYNAMES[weekday]}\"\nend\n```\n\n#### Equivalence\n\nBoth `Repeatable::Schedule` and all `Repeatable::Expression` classes have equivalence `#==` defined according to what's appropriate for each class, so regardless of the order of arguments passed to each, you can tell whether one object is equivalent to the other in terms of whether or not, when asked the same questions, you'd receive the same results from each.\n\n```ruby\nRepeatable::Expression::DayInMonth.new(day: 1) == Repeatable::Expression::DayInMonth.new(day: 1)\n  # =\u003e true\n\nfirst = Repeatable::Expression::DayInMonth.new(day: 1)\nfifteenth = Repeatable::Expression::DayInMonth.new(day: 15)\nfirst == fifteenth\n  # =\u003e false\n\nunion = Repeatable::Expression::Union.new(first, fifteenth)\nanother_union = Repeatable::Expression::Union.new(fifteenth, first)\nunion == another_union\n  # =\u003e true (order of Union and Intersection arguments doesn't their affect output)\n\nRepeatable::Schedule.new(union) == Repeatable::Schedule.new(another_union)\n  # =\u003e true (their expressions are equivalent, so they'll produce the same results)\n\n```\n\n## Ruby version support\n\nCurrently tested and supported: \n- 3.2\n- 3.3\n- 3.4\n\nDeprecated (currently tested but have reached EOL and will be unsupported in the next major version):\n- 2.5\n- 2.6\n- 2.7\n- 3.0\n- 3.1\n\nThe supported versions will roughly track with versions that are currently maintained by the Ruby core team.\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.\n\nYou can run the tests with `bundle exec rake`.\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/molawson/repeatable. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.\n\n1. Fork it ( https://github.com/molawson/repeatable/fork )\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create a new Pull Request\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n\n## Code of Conduct\n\nEveryone interacting in the Repeatable project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/molawson/repeatable/blob/main/CODE_OF_CONDUCT.md).\n\n[ruby-pattern-matching]: https://docs.ruby-lang.org/en/3.0/syntax/pattern_matching_rdoc.html\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmolawson%2Frepeatable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmolawson%2Frepeatable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmolawson%2Frepeatable/lists"}