SkillAgentSearch skills...

Montrose

Recurring events library for Ruby. Enumerable recurrence objects and convenient chainable interface.

Install / Use

/learn @rossta/Montrose
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Montrose

Build Status Code Climate Coverage Status

Montrose 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 and rule definitions available in Recurrence.

Installation

Add this line to your application's Gemfile:

gem "montrose"

And then execute:

$ bundle

Or install it yourself as:

$ gem install montrose

Why

Dealing with recurring events is hard. Montrose provides a simple interface for specifying and enumerating recurring events as Time objects for Ruby applications.

More specifically, this project intends to:

  • model recurring events in Ruby
  • embrace Ruby idioms
  • support recent Rubies
  • be reasonably performant
  • serialize to yaml, hash, and ical formats
  • be suitable for integration with persistence libraries

What Montrose doesn't do:

Concepts

Montrose allows you to easily create "recurrence objects" through chaining:

# Every Monday at 10:30am
Montrose.weekly.on(:monday).at("10:30 am")
=> #<Montrose::Recurrence...>

Each 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'.

# Example 1 - building recurrence in succession
r1 = Montrose.every(:week)
r2 = r1.on([:tuesday, :thursday])
r3 = r2.at("12 pm")
r4 = r3.total(4)

# Example 2 - merging distinct recurrences
r1 = Montrose.every(:week)
r2 = Montrose.on([:tuesday, :thursday])
r3 = Montrose.at("12 pm")
r4 = r1.merge(r2).merge(r3).total(4)

Most recurrence methods accept additional options if you favor the hash-syntax:

Montrose.r(every: :week, on: :monday, at: "10:30 am")
=> #<Montrose::Recurrence...>

See the docs for Montrose::Chainable for more info on recurrence creation methods.

A Montrose recurrence responds to #events, which returns an Enumerator that can generate timestamps:

r = Montrose.hourly
=> #<Montrose::Recurrence...>

r.events
=> #<Enumerator:...>

r.events.take(10)
=> [2016-02-03 18:26:08 -0500,
2016-02-03 19:26:08 -0500,
2016-02-03 20:26:08 -0500,
2016-02-03 21:26:08 -0500,
2016-02-03 22:26:08 -0500,
2016-02-03 23:26:08 -0500,
2016-02-04 00:26:08 -0500,
2016-02-04 01:26:08 -0500,
2016-02-04 02:26:08 -0500,
2016-02-04 03:26:08 -0500]

Montrose recurrences are themselves enumerable:

# Every month starting a year from now on Friday the 13th for 5 occurrences
r = Montrose.monthly.starting(1.year.from_now).on(friday: 13).repeat(5)

r.map(&:to_date)
=> [Fri, 13 Oct 2017,
Fri, 13 Apr 2018,
Fri, 13 Jul 2018,
Fri, 13 Sep 2019,
Fri, 13 Dec 2019]

Conceptually, recurrences can represent an infinite sequence. When we say simply "every day", there is no implied ending. It's therefore possible to create a recurrence that can enumerate forever, so use your Enumerable methods wisely.

# Every day starting now
r = Montrose.daily

# this expression will never complete, Ctrl-c!
r.map(&:to_date)

# use `lazy` enumerator to avoid eager enumeration
r.lazy.map(&:to_date).select { |d| d.mday > 25 }.take(5).to_a
=> [Fri, 26 Feb 2016,
Sat, 27 Feb 2016,
Sun, 28 Feb 2016,
Mon, 29 Feb 2016,
Sat, 26 Mar 2016]

It's straightforward to convert a recurrence to a hash and back.

opts = Montrose::Recurrence.new(every: 10.minutes).to_h
=> {:every=>:minute, :interval=>10}

Montrose::Recurrence.new(opts).take(3)
=> [2016-02-03 19:06:07 -0500,
2016-02-03 19:16:07 -0500,
2016-02-03 19:26:07 -0500]

A 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.

r = Montrose.at("12pm")
=> #<Montrose::Recurrence...>
r.each
Montrose::ConfigurationError: Please specify the :every option

Usage

require "montrose"

# a new recurrence
Montrose.r
Montrose.recurrence
Montrose::Recurrence.new

# daily for 10 occurrences
Montrose.daily(total: 10)

# daily until December 23, 2015
starts = Date.new(2015, 1, 1)
ends = Date.new(2015, 12, 23)
Montrose.daily(starts: starts, until: ends)

# every other day forever
Montrose.daily(interval: 2)

# every 10 days 5 occurrences
Montrose.every(10.days, total: 5)

# everyday in January for 3 years
starts = Time.current.beginning_of_year
ends = Time.current.end_of_year + 2.years
Montrose.daily(month: :january, between: starts...ends)

# weekly for 10 occurrences
Montrose.weekly(total: 10)

# weekly until December 23, 2015
ends_on = Date.new(2015, 12, 23)
starts_on = ends_on - 15.weeks
Montrose.every(:week, until: ends_on, starts: starts_on)

# every other week forever
Montrose.every(2.weeks)

# weekly on Tuesday and Thursday for five weeks
# from September 1, 2015 until October 5, 2015
Montrose.weekly(on: [:tuesday, :thursday],
  between: Date.new(2015, 9, 1)..Date.new(2015, 10, 5))

# every other week on Monday, Wednesday and Friday until December 23 2015,
# but starting on Tuesday, September 1, 2015
Montrose.every(2.weeks,
  on: [:monday, :wednesday, :friday],
  starts: Date.new(2015, 9, 1))

# every other week on Tuesday and Thursday, for 8 occurrences
Montrose.weekly(on: [:tuesday, :thursday], total: 8, interval: 2)

# monthly on the first Friday for ten occurrences
Montrose.monthly(day: { friday: [1] }, total: 10)

# monthly on the first Friday until December 23, 2015
Montrose.every(:month, day: { friday: [1] }, until: Date.new(2016, 12, 23))

# every other month on the first and last Sunday of the month for 10 occurrences
Montrose.every(:month, day: { sunday: [1, -1] }, interval: 2, total: 10)

# monthly on the second-to-last Monday of the month for 6 months
Montrose.every(:month, day: { monday: [-2] }, total: 6)

# monthly on the third-to-the-last day of the month, forever
Montrose.every(:month, mday: [-3])

# monthly on the 2nd and 15th of the month for 10 occurrences
Montrose.every(:month, mday: [2, 15], total: 10)

# monthly on the first and last day of the month for 10 occurrences
Montrose.monthly(mday: [1, -1], total: 10)

# every 18 months on the 10th thru 15th of the month for 10 occurrences
Montrose.every(18.months, total: 10, mday: 10..15)

# every Tuesday, every other month
Montrose.every(2.months, on: :tuesday)

# yearly in June and July for 10 occurrences
Montrose.yearly(month: [:june, :july], total: 10)

# every other year on January, February, and March for 10 occurrences
Montrose.every(2.years, month: [:january, :february, :march], total: 10)

# every third year on the 1st, 100th and 200th day for 10 occurrences
Montrose.yearly(yday: [1, 100, 200], total: 10)

# every 20th Monday of the year, forever
Montrose.yearly(day: { monday: [20] })

# Monday of week number 20 forever
Montrose.yearly(week: [20], on: :monday)

# every Thursday in March, forever
Montrose.monthly(month: :march, on: :thursday, at: "12 pm")

# every Thursday, but only during June, July, and August, forever" do
Montrose.monthly(month: 6..8, on: :thursday)

# every Friday 13th, forever
Montrose.monthly(on: { friday: 13 })

# first Saturday that follows the first Sunday of the month, forever
Montrose.monthly(on: { saturday: 7..13 })

# every four years, the first Tuesday after a Monday in November, forever (U.S. Presidential Election day)
Montrose.every(4.years, month: :november, on: { tuesday: 2..8 })

# every 3 hours from 9:00 AM to 5:00 PM on a specific day
date = Date.new(2016, 9, 1)
Montrose.hourly(between: date..(date+1), hour: 9..17, interval: 3)

# every hour and a half for four occurrences
Montrose.every(90.minutes, total: 4)

# every 20 minutes from 9:00 AM to 4:40 PM every day
Montrose.every(20.minutes, hour: 9..16)

# every 20 minutes from 9:00 AM to 4:40 PM every day with time-of-day precision
r = Montrose.every(20.minutes)
r.during("9am-4:40pm")                                        # as semantic time-of-day range OR
r.during(time.change(hour: 9)..time.change(hour: 4: min: 40)) # as ruby time range OR
r.during([9, 0, 0], [16, 40, 0])                              # as hour, min, sec tuple pairs for start, end

# every 20 minutes during multiple time-of-day ranges
Montrose.every(20.minutes).during("9am-12pm", "1pm-5pm")

# Minutely
Montrose.minutely
Montrose.r(every: :minute)

Montrose.every(10.minutes)
Montrose.r(every: 10.minutes)
Montrose.r(every: :minute, interval: 10) # every 10 minutes

Montrose.minutely(until: "9:00 PM")
Montrose.r(every: :minute, until: "9:00 PM")

# Daily
Montrose.daily
Montrose.every(:day)
Montrose.r(every: :day)

Montrose.every(9.days)
Montrose.r(every: 9.days)
Montrose.r(every: :day, interval: 9)

Montrose.daily(at: "9:00 AM")
Montrose.

Related Skills

View on GitHub
GitHub Stars856
CategoryDevelopment
Updated2mo ago
Forks53

Languages

Ruby

Security Score

100/100

Audited on Jan 9, 2026

No findings