ByStar (by_*) allows you easily and reliably query ActiveRecord and Mongoid objects based on time.
Post.by_year(2013) # all posts in 2013 Post.before(Date.today) # all posts for before today Post.yesterday # all posts for yesterday Post.between_times(Time.zone.now - 3.hours, # all posts in last 3 hours Time.zone.now) @post.next # next post after a given post
Install this gem by adding this to your Gemfile:
gem 'by_star', git: "git://github.com/radar/by_star"
If you are using ActiveRecord, you're done!
Mongoid users, please include the Mongoid::ByStar module for each model you wish to use the functionality. This is the convention among Mongoid plugins.
class MyModel include Mongoid::Document include Mongoid::ByStar
ByStar adds the following finder scopes (class methods) to your model to query time ranges.
These accept a
DateTime object as an argument, which defaults to
Time.zone.now if not specified:
between_times(start_time, end_time)- Finds all records occurring between the two given times.
before(end_time)- Finds all records occurring before the given time
after(start_time)- Finds all records occurring after the given time
between_times supports alternate argument forms:
between_times(start_time, nil)- same as
between_times(nil, end_time)- same as
Time Range Scopes
ByStar adds additional shortcut scopes based on commonly used time ranges. See sections below for detailed argument usage of each:
by_weekAllows zero-based week value from 0 to 52
by_cweekAllows one-based week value from 1 to 53
by_weekendSaturday and Sunday only of the given week
by_fortnightA two-week period, with the first fortnight of the year beginning on 1st January
by_calendar_monthMonth as it appears on a calendar; days form previous/following months which are part of the first/last weeks of the given month
by_quarter3-month intervals of the year
ByStar also adds scopes which are relative to the current time.
next_* methods represent a time distance from current time (
and do not strictly end/begin evenly on a calendar week/month/year (unlike
by_* methods which do.)
todayFinds all occurrences on today's date
yesterdayFinds all occurrences on yesterday's date
tomorrowFinds all occurrences on tomorrow's date
past_dayPrior 24-hour period from current time
past_weekPrior 7-day period from current time
past_fortnightPrior 14-day period from current time
past_monthPrior 30-day period from current time
past_yearPrior 365-day period from current time
next_daySubsequent 24-hour period from current time
next_weekSubsequent 7-day period from current time
next_fortnightSubsequent 14-day period from current time
next_monthSubsequent 30-day period from current time
next_yearSubsequent 365-day period from current time
Find the oldest or newest records. Returns an object instance (not a relation):
In addition, ByStar adds instance methods to return the next / previous record in the timewise sequence. Returns an object instance (not a relation):
ByStar extends the kernel
DateTime objects with the following instance methods,
which mirror the ActiveSupport methods
Lastly, ByStar aliases Rails 3
Date#to_time_in_current_zone to the Rails 4 syntax
#in_time_zone, if it has not already been defined.
Setting the Query Field
By default, ByStar assumes you will use the
created_at field to query objects by time.
You may specify an alternate field on all query methods as follows:
Post.by_month("January", field: :updated_at)
Alternatively, you may set a default in your model using the
class Post < ActiveRecord::Base by_star_field :updated_at end
Scoping the Query
All ByStar methods (except
Mongoid::Criteria objects, which can be daisy-chained with other scopes/finder methods:
Post.by_month.your_scope Post.by_month(1).include(:tags).where("tags.name" => "ruby")
Want to count records? Simple:
All ByStar finders support an
:offset option which is applied to time period of the query condition.
This is useful in cases where the daily cycle occurs at a time other than midnight.
For example, if you'd like to find all Posts from 9:00 on 2014-03-05 until 8:59:59.999 on 2014-03-06, you can do:
Post.by_day('2014-03-05', offset: 9.hours)
You may also set a offset scope in the
class Post < ActiveRecord::Base by_star_field offset: 9.hours end
If your object has both a start and end time, you may pass both params to
by_star_field :start_time, :end_time
By default, ByStar queries will return all objects whose range has any overlap within the desired period (permissive):
MultiDayEvent.by_month("January") #=> returns MultiDayEvents that overlap in January, even if they start in December and/or end in February
If you'd like to confine results to only those both starting and ending within the given range, use the
MultiDayEvent.by_month("January", :strict => true) #=> returns MultiDayEvents that both start AND end in January
Timespan Objects: Database Indexing and
In order to ensure query performance on large dataset, you must add an index to the query field (e.g. "created_at") be indexed. ByStar does not define indexes automatically.
Database indexes require querying a range query on a single field, i.e.
start_time >= X and start_time <= Y.
If we use a single-sided query, the database will iterate through all items either from the beginning or until the end of time.
This poses a challenge for timespan-type objects which have two fields, i.e.
There are two cases to consider:
- Timespan with
:strictoption, e.g. "start_time >= X and end_time <= Y".
Given that this gem requires start_time >= end_time, we add the converse constraint "start_time <= Y and end_time >= X" to ensure both fields are double-sided, i.e. an index can be used on either field.
- Timespan without
:strictoption, e.g. "start_time < Y and end_time > X".
This is not yet supported but will be soon.
If Chronic gem is present, it will be used to parse natural-language date/time
strings in all ByStar finder methods. Otherwise, the Ruby
Time.parse kernel method will be used as a fallback.
As of ByStar 2.2.0, you must explicitly include
gem 'chronic' into your Gemfile in order to use Chronic.
To find records between two times:
You use a Range like so:
Also works with dates - WARNING: there are currently some caveats see Issue #49:
before and after
To find all posts before / after the current time:
To find all posts before certain time or date:
Post.before(Date.today + 2) Post.after(Time.now + 5.days)
You can also pass a string:
For Time-Range type objects, only the start time is considered for
previous and next
To find the prior/subsequent record to a model instance,
next on it:
You can specify a field also:
Post.last.previous(field: "published_at") Post.first.next(field: "published_at")
For Time-Range type objects, only the start time is considered for
To find records from the current year, simply call the method without any arguments:
To find records based on a year you can pass it a two or four digit number:
This will return all posts in 2009, whereas:
will return all the posts in the year 1999.
You can also specify the full year:
If you know the number of the month you want:
This will return all posts in the first month (January) of the current year.
If you like being verbose:
This will return all posts created in January of the current year.
If you want to find all posts in January of last year just do
Post.by_month(1, year: 2007)
Post.by_month("January", year: 2007)
This will perform a find using the column you've specified.
If you have a Time object you can use it to find the posts:
Post.by_month(Time.local(2012, 11, 24))
This will find all the posts in November 2012.
Finds records for a given month as shown on a calendar. Includes all the results of
by_month, plus any results which fall in the same week as the first and last of the month. Useful for working with UI calendars which show rows of weeks.
Parameter behavior is otherwise the same as
:start_day option is supported to specify the start day of the week (
Fortnight numbering starts at 0. The beginning of a fortnight is Monday, 12am.
To find records from the current fortnight:
To find records based on a fortnight, you can pass in a number (representing the fortnight number) or a time object:
This will return all posts in the 18th fortnight of the current year.
Post.by_fortnight(18, year: 2012)
This will return all posts in the 18th fortnight week of 2012.
This will return all posts from the first fortnight of 2012.
by_week and by_cweek
Week numbering starts at 0, and cweek numbering starts at 1 (same as
Date#cweek). The beginning of a week is as defined in
ActiveSupport#beginning_of_week, which can be configured.
To find records from the current week:
Post.by_week Post.by_cweek # same result
This will return all posts in the 37th week of the current year (remember week numbering starts at 0):
Post.by_week(36) Post.by_cweek(37) # same result
This will return all posts in the 37th week of 2012:
Post.by_week(36, year: 2012) Post.by_cweek(37, year: 2012) # same result
This will return all posts in the week which contains Jan 1, 2012:
Post.by_week(Time.local(2012,1,1)) Post.by_cweek(Time.local(2012,1,1)) # same result
You may pass in a
:start_day option (
:tuesday, etc.) to specify the starting day of the week. This may also be configured in Rails.
If the time passed in (or the time now is a weekend) it will return posts from 0:00 Saturday to 23:59:59 Sunday. If the time is a week day, it will show all posts for the coming weekend.
by_day and today
To find records for today:
To find records for a certain day:
Post.by_day(Time.local(2012, 1, 1))
You can also pass a string:
This will return all posts for the given day.
Finds records by 3-month quarterly period of year. Quarter numbering starts at 1. The four quarters of the year begin on Jan 1, Apr 1, Jul 1, and Oct 1 respectively.
To find records from the current quarter:
To find records based on a quarter, you can pass in a number (representing the quarter number) or a time object:
This will return all posts in the 4th quarter of the current year.
Post.by_quarter(2, year: 2012)
This will return all posts in the 2nd quarter of 2012.
This will return all posts from the first quarter of 2012.
ByStar is tested against the following versions:
- Ruby 1.9.3+
- Rails/ActiveRecord 3.0+
- Mongoid 3.0+
Note that ByStar automatically adds the following version compatibility shims:
- ActiveSupport 3.x:
Date#to_time_in_current_zoneis aliased to
Date#in_time_zonefrom version 4+
- Mongoid 3.x: Adds support for
Criteria#reordermethod from version 4+
Specify a database by supplying a
DB environmental variable:
bundle exec rake spec DB=sqlite
You can also take an ORM-specific test task for a ride:
bundle exec rake spec:active_record
Have an Active Record or Mongoid version in mind? Set the environment variables
MONGOID_VERSION to a version of your choice. A
version number provided will translate to
~> VERSION, and the string
will grab the latest from Github.
# Update your bundle appropriately... ACTIVE_RECORD_VERSION=4.0.0 MONGOID_VERSION=master bundle update # ...then run the specs ACTIVE_RECORD_VERSION=4.0.0 MONGOID_VERSION=master bundle exec rpsec spec
ByStar tests use TimeCop to lock the system
Time.now at Jan 01, 2014, and seed
objects with fixed dates according to
Note that the timezone is randomized on each run to shake-out timezone related quirks.
ByStar is actively maintained by Ryan Bigg (radar) and Johnny Shields (johnnyshields)
Thank you to the following people:
- Thomas Sinclair for the original bump for implementing ByStar
- Ruby on Rails for their support
- Mislav Marohnic
- August Lilleas (leethal)
- Sam Elliott (lenary)
- The creators of the Chronic gem
- Erik Fonselius
- Johnny Shields (johnnyshields)