{"id":13412039,"url":"https://github.com/rossta/montrose","last_synced_at":"2025-05-13T22:12:14.284Z","repository":{"id":37580102,"uuid":"48853728","full_name":"rossta/montrose","owner":"rossta","description":"Recurring events library for Ruby. Enumerable recurrence objects and convenient chainable interface.","archived":false,"fork":false,"pushed_at":"2025-01-25T22:04:58.000Z","size":885,"stargazers_count":854,"open_issues_count":17,"forks_count":55,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-05-13T20:07:57.561Z","etag":null,"topics":["recurrence","recurring-events","ruby"],"latest_commit_sha":null,"homepage":"https://rossta.net/montrose/","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/rossta.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-12-31T15:28:42.000Z","updated_at":"2025-04-28T09:32:02.000Z","dependencies_parsed_at":"2024-01-08T17:13:30.349Z","dependency_job_id":"df480cf6-8ac4-49ef-879a-053a5a8274dd","html_url":"https://github.com/rossta/montrose","commit_stats":{"total_commits":459,"total_committers":18,"mean_commits":25.5,"dds":"0.061002178649237515","last_synced_commit":"d144d58632b5e08bd760186673e376dc1576fc10"},"previous_names":[],"tags_count":29,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rossta%2Fmontrose","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rossta%2Fmontrose/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rossta%2Fmontrose/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rossta%2Fmontrose/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rossta","download_url":"https://codeload.github.com/rossta/montrose/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254036843,"owners_count":22003654,"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":["recurrence","recurring-events","ruby"],"created_at":"2024-07-30T20:01:20.385Z","updated_at":"2025-05-13T22:12:14.086Z","avatar_url":"https://github.com/rossta.png","language":"Ruby","funding_links":[],"categories":["Ruby","Date and Time Processing"],"sub_categories":[],"readme":"# Montrose\n\n[![Build Status](https://dl.circleci.com/status-badge/img/gh/rossta/montrose/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/rossta/montrose/tree/main)\n[![Code Climate](https://api.codeclimate.com/v1/badges/305689119fb4ddcbaec1/maintainability)](https://codeclimate.com/github/rossta/montrose/maintainability)\n[![Coverage Status](https://coveralls.io/repos/rossta/montrose/badge.svg?branch=master\u0026service=github)](https://coveralls.io/github/rossta/montrose?branch=master)\n\nMontrose is an easy-to-use library for defining recurring events in Ruby. It uses a simple chaining system for building enumerable recurrences, inspired heavily by the design principles of [HTTP.rb](https://github.com/httprb/http) and rule definitions available in [Recurrence](https://github.com/fnando/recurrence).\n\n- [Introductory blog post](http://bit.ly/1PA68Zb)\n- [NYC.rb\n  presentation](https://speakerdeck.com/rossta/recurring-events-with-montrose)\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem \"montrose\"\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install montrose\n\n## Why\n\nDealing with recurring events is hard. `Montrose` provides a simple interface for specifying and enumerating recurring events as `Time` objects for Ruby applications.\n\nMore specifically, this project intends to:\n\n- model recurring events in Ruby\n- embrace Ruby idioms\n- support recent Rubies\n- be reasonably performant\n- serialize to yaml, hash, and [ical](http://www.kanzaki.com/docs/ical/rrule.html#basic) formats\n- be suitable for integration with persistence libraries\n\nWhat `Montrose` doesn't do:\n\n- support all calendaring use cases under the sun\n- schedule recurring jobs for your Rails app. Use one of these instead: [cron](https://en.wikipedia.org/wiki/Cron), [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler), [sidekiq-cron](https://github.com/ondrejbartas/sidekiq-cron), [sidetiq](https://github.com/tobiassvn/sidetiq), [whenever](https://github.com/javan/whenever)\n\n## Concepts\n\nMontrose allows you to easily create \"recurrence objects\" through chaining:\n\n```ruby\n# Every Monday at 10:30am\nMontrose.weekly.on(:monday).at(\"10:30 am\")\n=\u003e #\u003cMontrose::Recurrence...\u003e\n```\n\nEach chained recurrence returns a **new object** so they can be composed and merged. In both examples below, recurrence `r4` represents 'every week on Tuesday and Thursday at noon for four occurrences'.\n\n```ruby\n# Example 1 - building recurrence in succession\nr1 = Montrose.every(:week)\nr2 = r1.on([:tuesday, :thursday])\nr3 = r2.at(\"12 pm\")\nr4 = r3.total(4)\n\n# Example 2 - merging distinct recurrences\nr1 = Montrose.every(:week)\nr2 = Montrose.on([:tuesday, :thursday])\nr3 = Montrose.at(\"12 pm\")\nr4 = r1.merge(r2).merge(r3).total(4)\n```\n\nMost recurrence methods accept additional options if you favor the hash-syntax:\n\n```ruby\nMontrose.r(every: :week, on: :monday, at: \"10:30 am\")\n=\u003e #\u003cMontrose::Recurrence...\u003e\n```\n\nSee [the docs for `Montrose::Chainable`](https://rossta.net/montrose/Montrose/Chainable.html) for more info on recurrence creation methods.\n\nA Montrose recurrence responds to `#events`, which returns an [`Enumerator`](/blog/what-is-enumerator.html) that can generate timestamps:\n\n```ruby\nr = Montrose.hourly\n=\u003e #\u003cMontrose::Recurrence...\u003e\n\nr.events\n=\u003e #\u003cEnumerator:...\u003e\n\nr.events.take(10)\n=\u003e [2016-02-03 18:26:08 -0500,\n2016-02-03 19:26:08 -0500,\n2016-02-03 20:26:08 -0500,\n2016-02-03 21:26:08 -0500,\n2016-02-03 22:26:08 -0500,\n2016-02-03 23:26:08 -0500,\n2016-02-04 00:26:08 -0500,\n2016-02-04 01:26:08 -0500,\n2016-02-04 02:26:08 -0500,\n2016-02-04 03:26:08 -0500]\n```\n\nMontrose recurrences are themselves enumerable:\n\n```ruby\n# Every month starting a year from now on Friday the 13th for 5 occurrences\nr = Montrose.monthly.starting(1.year.from_now).on(friday: 13).repeat(5)\n\nr.map(\u0026:to_date)\n=\u003e [Fri, 13 Oct 2017,\nFri, 13 Apr 2018,\nFri, 13 Jul 2018,\nFri, 13 Sep 2019,\nFri, 13 Dec 2019]\n```\n\nConceptually, recurrences can represent an infinite sequence. When we say\nsimply \"every day\", there is no implied ending. It's therefore possible to\ncreate a recurrence that can enumerate forever, so use your `Enumerable` methods wisely.\n\n```ruby\n# Every day starting now\nr = Montrose.daily\n\n# this expression will never complete, Ctrl-c!\nr.map(\u0026:to_date)\n\n# use `lazy` enumerator to avoid eager enumeration\nr.lazy.map(\u0026:to_date).select { |d| d.mday \u003e 25 }.take(5).to_a\n=\u003e [Fri, 26 Feb 2016,\nSat, 27 Feb 2016,\nSun, 28 Feb 2016,\nMon, 29 Feb 2016,\nSat, 26 Mar 2016]\n```\n\nIt's straightforward to convert a recurrence to a hash and back.\n\n```ruby\nopts = Montrose::Recurrence.new(every: 10.minutes).to_h\n=\u003e {:every=\u003e:minute, :interval=\u003e10}\n\nMontrose::Recurrence.new(opts).take(3)\n=\u003e [2016-02-03 19:06:07 -0500,\n2016-02-03 19:16:07 -0500,\n2016-02-03 19:26:07 -0500]\n```\n\nA recurrence object must minimally specify a frequency, e.g. `:minute`, `:hour`, `:day`, `:week`, `:month`, or, `:year`, to be viable. Otherwise, you'll see an informative error message when attempting to enumerate the recurrence.\n\n```ruby\nr = Montrose.at(\"12pm\")\n=\u003e #\u003cMontrose::Recurrence...\u003e\nr.each\nMontrose::ConfigurationError: Please specify the :every option\n```\n\n## Usage\n\n```ruby\nrequire \"montrose\"\n\n# a new recurrence\nMontrose.r\nMontrose.recurrence\nMontrose::Recurrence.new\n\n# daily for 10 occurrences\nMontrose.daily(total: 10)\n\n# daily until December 23, 2015\nstarts = Date.new(2015, 1, 1)\nends = Date.new(2015, 12, 23)\nMontrose.daily(starts: starts, until: ends)\n\n# every other day forever\nMontrose.daily(interval: 2)\n\n# every 10 days 5 occurrences\nMontrose.every(10.days, total: 5)\n\n# everyday in January for 3 years\nstarts = Time.current.beginning_of_year\nends = Time.current.end_of_year + 2.years\nMontrose.daily(month: :january, between: starts...ends)\n\n# weekly for 10 occurrences\nMontrose.weekly(total: 10)\n\n# weekly until December 23, 2015\nends_on = Date.new(2015, 12, 23)\nstarts_on = ends_on - 15.weeks\nMontrose.every(:week, until: ends_on, starts: starts_on)\n\n# every other week forever\nMontrose.every(2.weeks)\n\n# weekly on Tuesday and Thursday for five weeks\n# from September 1, 2015 until October 5, 2015\nMontrose.weekly(on: [:tuesday, :thursday],\n  between: Date.new(2015, 9, 1)..Date.new(2015, 10, 5))\n\n# every other week on Monday, Wednesday and Friday until December 23 2015,\n# but starting on Tuesday, September 1, 2015\nMontrose.every(2.weeks,\n  on: [:monday, :wednesday, :friday],\n  starts: Date.new(2015, 9, 1))\n\n# every other week on Tuesday and Thursday, for 8 occurrences\nMontrose.weekly(on: [:tuesday, :thursday], total: 8, interval: 2)\n\n# monthly on the first Friday for ten occurrences\nMontrose.monthly(day: { friday: [1] }, total: 10)\n\n# monthly on the first Friday until December 23, 2015\nMontrose.every(:month, day: { friday: [1] }, until: Date.new(2016, 12, 23))\n\n# every other month on the first and last Sunday of the month for 10 occurrences\nMontrose.every(:month, day: { sunday: [1, -1] }, interval: 2, total: 10)\n\n# monthly on the second-to-last Monday of the month for 6 months\nMontrose.every(:month, day: { monday: [-2] }, total: 6)\n\n# monthly on the third-to-the-last day of the month, forever\nMontrose.every(:month, mday: [-3])\n\n# monthly on the 2nd and 15th of the month for 10 occurrences\nMontrose.every(:month, mday: [2, 15], total: 10)\n\n# monthly on the first and last day of the month for 10 occurrences\nMontrose.monthly(mday: [1, -1], total: 10)\n\n# every 18 months on the 10th thru 15th of the month for 10 occurrences\nMontrose.every(18.months, total: 10, mday: 10..15)\n\n# every Tuesday, every other month\nMontrose.every(2.months, on: :tuesday)\n\n# yearly in June and July for 10 occurrences\nMontrose.yearly(month: [:june, :july], total: 10)\n\n# every other year on January, February, and March for 10 occurrences\nMontrose.every(2.years, month: [:january, :february, :march], total: 10)\n\n# every third year on the 1st, 100th and 200th day for 10 occurrences\nMontrose.yearly(yday: [1, 100, 200], total: 10)\n\n# every 20th Monday of the year, forever\nMontrose.yearly(day: { monday: [20] })\n\n# Monday of week number 20 forever\nMontrose.yearly(week: [20], on: :monday)\n\n# every Thursday in March, forever\nMontrose.monthly(month: :march, on: :thursday, at: \"12 pm\")\n\n# every Thursday, but only during June, July, and August, forever\" do\nMontrose.monthly(month: 6..8, on: :thursday)\n\n# every Friday 13th, forever\nMontrose.monthly(on: { friday: 13 })\n\n# first Saturday that follows the first Sunday of the month, forever\nMontrose.monthly(on: { saturday: 7..13 })\n\n# every four years, the first Tuesday after a Monday in November, forever (U.S. Presidential Election day)\nMontrose.every(4.years, month: :november, on: { tuesday: 2..8 })\n\n# every 3 hours from 9:00 AM to 5:00 PM on a specific day\ndate = Date.new(2016, 9, 1)\nMontrose.hourly(between: date..(date+1), hour: 9..17, interval: 3)\n\n# every hour and a half for four occurrences\nMontrose.every(90.minutes, total: 4)\n\n# every 20 minutes from 9:00 AM to 4:40 PM every day\nMontrose.every(20.minutes, hour: 9..16)\n\n# every 20 minutes from 9:00 AM to 4:40 PM every day with time-of-day precision\nr = Montrose.every(20.minutes)\nr.during(\"9am-4:40pm\")                                        # as semantic time-of-day range OR\nr.during(time.change(hour: 9)..time.change(hour: 4: min: 40)) # as ruby time range OR\nr.during([9, 0, 0], [16, 40, 0])                              # as hour, min, sec tuple pairs for start, end\n\n# every 20 minutes during multiple time-of-day ranges\nMontrose.every(20.minutes).during(\"9am-12pm\", \"1pm-5pm\")\n\n# Minutely\nMontrose.minutely\nMontrose.r(every: :minute)\n\nMontrose.every(10.minutes)\nMontrose.r(every: 10.minutes)\nMontrose.r(every: :minute, interval: 10) # every 10 minutes\n\nMontrose.minutely(until: \"9:00 PM\")\nMontrose.r(every: :minute, until: \"9:00 PM\")\n\n# Daily\nMontrose.daily\nMontrose.every(:day)\nMontrose.r(every: :day)\n\nMontrose.every(9.days)\nMontrose.r(every: 9.days)\nMontrose.r(every: :day, interval: 9)\n\nMontrose.daily(at: \"9:00 AM\")\nMontrose.every(:day, at: \"9:00 AM\")\nMontrose.r(every: :day, at: \"9:00 AM\")\n\nMontrose.daily(total: 7)\nMontrose.every(:day, total: 7)\nMontrose.r(every: :day, total: 7)\n\n# Weekly\nMontrose.weekly\nMontrose.every(:week)\nMontrose.r(every: :week)\n\nMontrose.every(:week, on: :monday)\nMontrose.every(:week, on: [:monday, :wednesday, :friday])\nMontrose.every(2.weeks, on: :friday)\nMontrose.every(:week, on: :friday, at: \"3:41 PM\")\nMontrose.weekly(on: :thursday)\n\n# Monthly by month day\nMontrose.monthly(mday: 1) # first of the month\nMontrose.every(:month, mday: 1)\nMontrose.r(every: :month, mday: 1)\n\nMontrose.monthly(mday: [2, 15]) # 2nd and 15th of the month\nMontrose.monthly(mday: -3) # third-to-last day of the month\nMontrose.monthly(mday: 10..15) # 10th through the 15th day of the month\n\n# Monthly by week day\nMontrose.monthly(day: :friday, interval: 2) # every Friday every other month\nMontrose.every(:month, day: :friday, interval: 2)\nMontrose.r(every: :month, day: :friday, interval: 2)\n\nMontrose.monthly(day: { friday: [1] }) # 1st Friday of the month\nMontrose.monthly(day: { sunday: [1, -1] }) # first and last Sunday of the month\n\nMontrose.monthly(mday: 7..13, day: :saturday) # first Saturday that follow the first Sunday of the month\n\n# Yearly\nMontrose.yearly\nMontrose.every(:year)\nMontrose.r(every: :year)\n\nMontrose.yearly(month: [:june, :july]) # yearly in June and July\nMontrose.yearly(month: 6..8, day: :thursday) # yearly in June, July, August on Thursday\nMontrose.yearly(yday: [1, 100]) # yearly on the 1st and 100th day of year\n\nMontrose.yearly(on: { january: 31 })\nMontrose.r(every: :year, on: { 10 =\u003e 31 }, interval: 3)\n\n# Chaining\nMontrose.weekly.starting(3.weeks.from_now).on(:friday)\nMontrose.every(:day).at(\"4:05pm\")\nMontrose.yearly.between(Time.current..10.years.from_now)\n\n# Enumerating events\nr = Montrose.every(:month, mday: 31, until: \"January 1, 2017\")\nr.each { |time| puts time.to_s }\nr.take(10).to_a\n\n# Merging rules\nr.merge(starts: \"2017-01-01\").each { |time| puts time.to_s }\n\n# Using #events Enumerator\nr.events # =\u003e #\u003cEnumerator: ...\u003e\nr.events.take(10).each { |date| puts date.to_s }\nr.events.lazy.select { |time| time \u003e 1.month.from_now }.take(3).each { |date| puts date.to_s }\n```\n\nMontrose relies on `ActiveSupport` for `DateTime`, `Date`, and `Time` calculations. As such, configuring ActiveSupport settings should work for Montrose recurrences.\n\nFor example, your application can configure the `Date` \"beginning of the week\" ([docs](https://www.rubydoc.info/docs/rails/Date.beginning_of_week)):\n\n```ruby\nDate.beginning_of_the_week = :sunday\n# OR\nDate.beginning_of_the_week = :monday\n```\n\nSimilarly in Rails ([docs](https://guides.rubyonrails.org/configuring.html#config-beginning-of-week)):\n\n```ruby\nconfig.beginning_of_week = :sunday\n# OR\nconfig.beginning_of_week = :monday\n```\n\nChanging these settings may affect the behavior of Montrose weekly recurrences.\n\n### Combining recurrences\n\nIt may be necessary to combine several recurrence rules into a single\nenumeration of events. For this purpose, there is `Montrose::Schedule`. To create a schedule of multiple recurrences:\n\n```ruby\nrecurrence_1 = Montrose.monthly(day: { friday: [1] })\nrecurrence_2 = Montrose.weekly(on: :tuesday)\n\nschedule = Montrose::Schedule.build do |s|\n  s \u003c\u003c recurrence_1\n  s \u003c\u003c recurrence_2\nend\n\n# add after building\ns \u003c\u003c Montrose.yearly\n```\n\nThe `Schedule#\u003c\u003c` method also accepts valid recurrence options as hashes:\n\n```ruby\nschedule = Montrose::Schedule.build do |s|\n  s \u003c\u003c { day: { friday: [1] } }\n  s \u003c\u003c { on: :tuesday }\nend\n```\n\nA schedule acts like a collection of recurrence rules that also behaves as a single\nstream of events:\n\n```ruby\nschedule.events # =\u003e #\u003cEnumerator: ...\u003e\nschedule.each do |event|\n  puts event\nend\n```\n\n### Ruby on Rails\n\nInstances of `Montrose::Recurrence` support the ActiveRecord serialization API so recurrence objects can be marshalled to and from a single database column:\n\n```ruby\nclass RecurringEvent \u003c ApplicationRecord\n  serialize :recurrence, Montrose::Recurrence\n\nend\n```\n\n`Montrose::Schedule` can also be serialized:\n\n```ruby\nclass RecurringEvent \u003c ApplicationRecord\n  serialize :recurrence, Montrose::Schedule\n\nend\n```\n\n## Inspiration\n\nMontrose is named after the beautifully diverse and artistic [neighborhood in Houston, Texas](https://en.wikipedia.org/wiki/Montrose,_Houston).\n\n### Related Projects\n\nCheck out following related projects, all of which have provided inspiration for `Montrose`.\n\n- [ice_cube](https://github.com/seejohnrun/ice_cube)\n- [recurrence](https://github.com/fnando/recurrence)\n- [runt](https://github.com/mlipper/runt)\n- [http.rb](https://github.com/httprb/http) - not a recurrence project, but inspirational to design, implementation, and interface of `Montrose`\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. `bin/setup` will install gems for each gemfile in `gemfiles/` against the current Ruby version.\n\nTo run tests against all gemfiles for current Ruby:\n\n```\nbin/spec\n```\n\nTo update installed gems for gemfiles:\n\n```\nbin/update\n```\n\nTo fix lint errors:\n\n```\nbin/standardrb --fix\n```\n\nWhen adding a new gemfile to `gemfiles/`, run `bin/setup` and commit the generated lock file.\n\nYou can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/rossta/montrose. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frossta%2Fmontrose","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frossta%2Fmontrose","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frossta%2Fmontrose/lists"}