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"
Then run bundle install
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 Date
, Time
, or 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 timeafter(start_time)
- Finds all records occurring after the given time
between_times
supports alternate argument forms:
between_times(Range)
between_times(Array)
between_times(start_time, nil)
- same asafter(start_time)
between_times(nil, end_time)
- same asbefore(end_time)
ByStar adds additional shortcut scopes based on commonly used time ranges. See sections below for detailed argument usage of each:
by_day
by_week
Allows zero-based week value from 0 to 52by_cweek
Allows one-based week value from 1 to 53by_weekend
Saturday and Sunday only of the given weekby_fortnight
A two-week period, with the first fortnight of the year beginning on 1st Januaryby_month
by_calendar_month
Month as it appears on a calendar; days form previous/following months which are part of the first/last weeks of the given monthby_quarter
3-month intervals of the yearby_year
ByStar also adds scopes which are relative to the current time.
Note the past_*
and next_*
methods represent a time distance from current time (Time.zone.now
),
and do not strictly end/begin evenly on a calendar week/month/year (unlike by_*
methods which do.)
today
Finds all occurrences on today's dateyesterday
Finds all occurrences on yesterday's datetomorrow
Finds all occurrences on tomorrow's datepast_day
Prior 24-hour period from current timepast_week
Prior 7-day period from current timepast_fortnight
Prior 14-day period from current timepast_month
Prior 30-day period from current timepast_year
Prior 365-day period from current timenext_day
Subsequent 24-hour period from current timenext_week
Subsequent 7-day period from current timenext_fortnight
Subsequent 14-day period from current timenext_month
Subsequent 30-day period from current timenext_year
Subsequent 365-day period from current time
Find the oldest or newest records. Returns an object instance (not a relation):
newest
oldest
In addition, ByStar adds instance methods to return the next / previous record in the timewise sequence. Returns an object instance (not a relation):
object.next
object.previous
ByStar extends the kernel Date
, Time
, and DateTime
objects with the following instance methods,
which mirror the ActiveSupport methods beginning_of_day
, end_of_week
, etc:
beginning_of_weekend
end_of_weekend
beginning_of_fortnight
end_of_fortnight
beginning_of_calendar_month
end_of_calendar_month
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.
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 by_star_field
macro:
class Post < ActiveRecord::Base
by_star_field :updated_at
end
All ByStar methods (except oldest
, newest
, previous
, next
) return ActiveRecord::Relation
and/or
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:
Post.by_month.count
You may pass a :scope
option as a Relation or Proc to all ByStar methods like so:
@post.next(scope: Post.where(category: @post.category))
@post.next(scope: ->{ where(category: 'blog') })
This is particularly useful for oldest
, newest
, previous
, next
which return a model instance rather than
a Relation and hence cannot be daisy-chained with other scopes.
previous
and next
support a special feature that the :scope
option may take the subject record as an argument:
@post.next(scope: ->(record){ where(category: record.category) })
You may also set a default scope in the by_star_field
macro. (It is recommended this be a Proc):
class Post < ActiveRecord::Base
by_star_field scope: ->{ where(category: 'blog') }
end
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 by_star_field
macro:
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
:
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 :strict
option:
MultiDayEvent.by_month("January", :strict => true) #=> returns MultiDayEvents that both start AND end in January
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. start_time
and end_time
.
There are two cases to consider:
- Timespan with
:strict
option, 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
:strict
option, 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:
Post.between_times(time1, time2)
You use a Range like so:
Post.between_times(time1..time2)
Also works with dates - WARNING: there are currently some caveats see Issue #49:
Post.between_times(date1, date2)
To find all posts before / after the current time:
Post.before
Post.after
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:
Post.before("next tuesday")
For Time-Range type objects, only the start time is considered for before
and after
.
To find the prior/subsequent record to a model instance, previous
/next
on it:
Post.last.previous
Post.first.next
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 previous
and next
.
To find records from the current year, simply call the method without any arguments:
Post.by_year
To find records based on a year you can pass it a two or four digit number:
Post.by_year(09)
This will return all posts in 2009, whereas:
Post.by_year(99)
will return all the posts in the year 1999.
You can also specify the full year:
Post.by_year(2009)
Post.by_year(1999)
If you know the number of the month you want:
Post.by_month(1)
This will return all posts in the first month (January) of the current year.
If you like being verbose:
Post.by_month("January")
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)
or
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.
Post.by_calendar_month
Parameter behavior is otherwise the same as by_month
. Also, :start_day
option is supported to specify the start day of the week (:monday
, :tuesday
, etc.)
Fortnight numbering starts at 0. The beginning of a fortnight is Monday, 12am.
To find records from the current fortnight:
Post.by_fortnight
To find records based on a fortnight, you can pass in a number (representing the fortnight number) or a time object:
Post.by_fortnight(18)
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.
Post.by_fortnight(Time.local(2012,1,1))
This will return all posts from the first fortnight of 2012.
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 (:monday
, :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.
Post.by_weekend(Time.now)
To find records for today:
Post.by_day
Post.today
To find records for a certain day:
Post.by_day(Time.local(2012, 1, 1))
You can also pass a string:
Post.by_day("next tuesday")
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:
Post.by_quarter
To find records based on a quarter, you can pass in a number (representing the quarter number) or a time object:
Post.by_quarter(4)
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.
Post.by_week(Time.local(2012,1,1))
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_zone
is aliased toDate#in_time_zone
from version 4+ - Mongoid 3.x: Adds support for
Criteria#reorder
method 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
ACTIVE_RECORD_VERSION
and MONGOID_VERSION
to a version of your choice. A
version number provided will translate to ~> VERSION
, and the string master
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 spec/fixtures/shared/seeds.rb
.
Note that the timezone is randomized on each run to shake-out timezone related quirks.
ByStar is actively maintained by Ryan Biggs (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)
- gte351s
- Sam Elliott (lenary)
- The creators of the Chronic gem
- Erik Fonselius
- Johnny Shields (johnnyshields)