Coder Social home page Coder Social logo

repeatable's Introduction

Repeatable

CI Maintainability Test Coverage

Ruby implementation of Martin Fowler's Recurring Events for Calendars paper.

Installation

Add this line to your application's Gemfile:

gem 'repeatable'

And then execute:

$ bundle

Or install it yourself as:

$ gem install repeatable

Usage

Building a Schedule

You can create a schedule in one of two ways.

Composed objects

Instantiate and compose each of the Repeatable::Expression objects manually.

second_monday = Repeatable::Expression::WeekdayInMonth.new(weekday: 1, count: 2)
oct_thru_dec = Repeatable::Expression::RangeInYear.new(start_month: 10, end_month: 12)
intersection = Repeatable::Expression::Intersection.new(second_monday, oct_thru_dec)

schedule = Repeatable::Schedule.new(intersection)

Hash

Or describe the same structure with a Hash, and the gem will handle instantiating and composing the objects.

arg = {
  intersection: [
    { weekday_in_month: { weekday: 1, count: 2 } },
    { range_in_year: { start_month: 10, end_month: 12 } },
    { exact_date: { date: "2015-08-01" } }
  ]
}

schedule = Repeatable::Schedule.new(arg)

Time Expressions

There are a number of time expressions available which, when combined, can describe most any schedule.

# SETS

# Any conditions can be met
{ union: [] }
Repeatable::Expression::Union.new(expressions)

# All conditions must be met
{ intersection: [] }
Repeatable::Expression::Intersection.new(expressions)

# Date is part of the first set (`included`) but not part of the second set (`excluded`)
{ difference: { included: expression, excluded: another_expression } }
Repeatable::Expression::Difference.new(included: expression, excluded: another_expression)


# DATES

# Every Sunday
{ weekday: { weekday: 0 } }
Repeatable::Expression::Weekday.new(weekday: 0)

# The 3rd Monday of every month
{ weekday_in_month: { weekday: 1, count: 3 } }
Repeatable::Expression::WeekdayInMonth.new(weekday: 1, count: 3)

# The last Thursday of every month
{ weekday_in_month: { weekday: 4, count: -1 } }
Repeatable::Expression::WeekdayInMonth.new(weekday: 4, count: -1)

# Every other Monday, starting from December 1, 2015
{ biweekly: { weekday: 1, start_after: '2015-12-01' } }
Repeatable::Expression::Biweekly.new(weekday: 1, start_after: Date.new(2015, 12, 1))

# The 13th of every month
{ day_in_month: { day: 13 } }
Repeatable::Expression::DayInMonth.new(day: 13)

# The last day of every month
{ day_in_month: { day: -1 } }
Repeatable::Expression::DayInMonth.new(day: -1)

# All days in October
{ range_in_year: { start_month: 10 } }
Repeatable::Expression::RangeInYear.new(start_month: 10)

# All days from October through December
{ range_in_year: { start_month: 10, end_month: 12 } }
Repeatable::Expression::RangeInYear.new(start_month: 10, end_month: 12)

# All days from October 1 through December 20
{ range_in_year: { start_month: 10, end_month: 12, start_day: 1, end_day: 20 } }
Repeatable::Expression::RangeInYear.new(start_month: 10, end_month: 12, start_day: 1, end_day: 20)

# only December 21, 2012
{ exact_date: { date: '2012-12-21' } }
Repeatable::Expression::ExactDate.new(date: Date.new(2012, 12, 21)

Schedule Errors

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

Getting information from a Schedule

Ask a schedule to do a number of things.

schedule.next_occurrence
  # => Date of next occurrence

# By default, it will find the next occurrence after Date.today.
# You can also specify a start date.
schedule.next_occurrence(Date.new(2015, 1, 1))
  # => Date of next occurrence after Jan 1, 2015

# You also have the option of including the start date as a possible result.
schedule.next_occurrence(Date.new(2015, 1, 1), include_start: true)
  # => Date of next occurrence on or after Jan 1, 2015

# By default, searches for the next occurrence are limited to the next 36,525 days (about 100 years).
# That limit can also be specified in number of days.
schedule.next_occurrence(limit: 365)
  # => Date of next occurrence within the next 365 days

schedule.occurrences(Date.new(2015, 1, 1), Date.new(2016, 6, 30))
  # => Dates of all occurrences between Jan 1, 2015 and June 30, 2016

schedule.include?(Date.new(2015, 10, 10))
  # => Whether the schedule has an event on the date given (true/false)

schedule.to_h
  # => Hash representation of the Schedule, which is useful for storage and
  #    can be used to recreate an identical Schedule object at a later time

Pattern Matching

Both Repeatable::Schedule and all Repeatable::Expression classes support Ruby 2.7+ pattern matching which is particularly useful for parsing or presenting an existing schedule.

case schedule
in weekday: { weekday: }
  "Weekly on #{Date::DAYNAMES[weekday]}"
in day_in_month: { day: }
  "Every month on the #{day.ordinalize}"
in weekday_in_month: { weekday:, count: }
  "Every month on the #{count.ordinalize} #{Date::DAYNAMES[weekday]}"
end

Equivalence

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

Repeatable::Expression::DayInMonth.new(day: 1) == Repeatable::Expression::DayInMonth.new(day: 1)
  # => true

first = Repeatable::Expression::DayInMonth.new(day: 1)
fifteenth = Repeatable::Expression::DayInMonth.new(day: 15)
first == fifteenth
  # => false

union = Repeatable::Expression::Union.new(first, fifteenth)
another_union = Repeatable::Expression::Union.new(fifteenth, first)
union == another_union
  # => true (order of Union and Intersection arguments doesn't their affect output)

Repeatable::Schedule.new(union) == Repeatable::Schedule.new(another_union)
  # => true (their expressions are equivalent, so they'll produce the same results)

Ruby version support

Currently tested and supported:

  • 3.1
  • 3.2
  • 3.3

Deprecated (currently tested but have reached EOL and will be unsupported in the next major version):

  • 2.5
  • 2.6
  • 2.7
  • 3.0

The supported versions will roughly track with versions that are currently maintained by the Ruby core team.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run bin/console for an interactive prompt that will allow you to experiment.

You can run the tests with bundle exec rake.

Contributing

Bug 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 code of conduct.

  1. Fork it ( https://github.com/molawson/repeatable/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Repeatable project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

repeatable's People

Contributors

cmoel avatar danielma avatar danott avatar dependabot[bot] avatar molawson avatar patricklerner avatar rmosolgo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

repeatable's Issues

Create a CHANGELOG

Setup a simple changelog with features, fixes, etc. for each release, and fill it in with info from previous versions.

Human readable, natural language representation of a Schedule

It would be handy to have Schedule#to_s return a text description of the Schedule.

schedule = Repeatable::Schedule.new(weekday_in_month: { weekday: 2, count: 1 })
schedule.to_s # => "The first Tuesday of every month"

Obviously, that's a straightforward case and things get more difficult with Unions and Intersections.

The first step would probably involve coming up with how you'd naturally describe more complex expressions and then devise a plan for a #to_s for each Expression type.

Limit Schedule#next_occurrence to a reasonable time frame

Currently, Schedule#next_occurrence will loop infinitely until it finds an occurrence. With even a simple schedule, it would be pretty easy to turn that into an infinite loop.

arg = {
  intersection: [
    { weekday: { weekday: 0 } },  # Every Sunday
    { weekday: { weekday: 1 } }   # Every Monday
  ]
}
schedule = Repeatable::Schedule.new(arg)
schedule.next_occurrence 
  # => infinite loop because a day will never be both a Sunday and a Monday

I'm thinking we set a limit (in terms of time) to how far forward we'll look for the next occurrence. We don't want it to be so low that it would cause problems for more complex schedules that occur very infrequently, but we want to prevent infinite loops.

Perhaps we can set an arbitrarily reasonable time limit, and if we encounter problems with a few outlier schedules, we could expose an argument for the user to set that limit themselves.

Create custom error classes

Replace generic errors here and here with some sort of Repeatable::InvalidSchedule error that apps using the gem can rescue from and handle however they see fit.

Use with datetimes?

Oh hi :)

I'm trying to use this gem with repeating events that happen at certain times of day, for example:

  • Union:
    • Every Tuesday from 7pm to 8:30pm
    • Every Thursday from 6:30pm to 8pm

In that case, applying times to the output of this gem is straightforward: if it's a Tuesday, it's 7-8:30, and if it's a Thursday, it's 6:30-8. But in some cases, it's a bit harder:

  • Union:
    • Every first and third Tuesday from 5pm to 6:30pm
    • Every Tuesday from 7pm to 8:30pm
    • Exact date: mm-dd-yyyy (imagine it's a specific first Tuesday) from 3:30pm to 5pm

Now, given a stream of dates, it's a bit harder to match them with times from my own data. I'd have to reimplement a bit of recursion logic to check which Tuesdays are which.

Are you interested in supporting this use case? I can imagine a few things that might work for me (although I didn't read the paper, so forgive me if I'm going off the rails) :

  • Accept starts-at and ends-at times along with various expression, then return Occurrence objects instead of Dates. (Occurrence could duck-type like a Date, which would minimize the impact on current users.)

    {
      weekday_in_month: {
        weekday: 2, 
        count: 1, 
        start_time: [17, 00], # [hours, minutes] ? Or some other object here?
        end_time: [18, 30], 
    }
    # ...
    schedule.next_occurrence # => #<Occurrence @starts_at=#<DateTime ...> @ends_at=#<DateTime ...> >
  • Accept user-defined metadata with expressions, and include that metadata along with occurrence results

    {
      weekday_in_month: {
        weekday: 2, 
        count: 1, 
        metadata: my_application_object,
      }
    }
    # ...
    schedule.next_occurrence # => #<Occurrence @date=#<Date ...>, @metadata=my_application_object >

Perhaps there are other ways to support this use case?

If you're interested in supporting it, I'd be happy to take a crack at some approach that seems good to you, Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.