I\'m creating a Rails app that will store the opening and closing hours for a business. Originally, I thought of simply using a text data type and letting it be free-form:
I'm currently setting up a directory listing for a client and we want give the user more flexibility. So: Let the user set up blocks for days:
We have a day (integer 1-7), opens (time), closes (time) and some shops or sights have summer and winter opening times, or they make vacations. According to schema.org you have to add a valid_from (datetime) and a valid_through (datetime).
With this setup the user can create whatever he wants:
# migration
class CreateOpeningHours < ActiveRecord::Migration
def change
create_table :opening_hours do |t|
t.integer :entry_id # your model reference
t.integer :day
t.time :closes
t.time :opens
t.datetime :valid_from
t.datetime :valid_through
end
end
end
Example for the model:
class OpeningHour < ActiveRecord::Base
belongs_to :entry
validates_presence_of :day, :closes, :opens, :entry_id
validates_inclusion_of :day, :in => 1..7
validate :opens_before_closes
validate :valid_from_before_valid_through
# sample validation for better user feedback
validates_uniqueness_of :opens, scope: [:entry_id, :day]
validates_uniqueness_of :closes, scope: [:entry_id, :day]
protected
def opens_before_closes
errors.add(:closes, I18n.t('errors.opens_before_closes')) if opens && closes && opens >= closes
end
def valid_from_before_valid_through
errors.add(:valid_through, I18n.t('errors.valid_from_before_valid_through')) if valid_from && valid_through && valid_from >= valid_through
end
end
With that setup you can create easily a is_open? method in your model. Currently I did not setup the is_open? method, but if somebody needs, give me a hit! I think I will finish it in the next days.
I would say that Operating Hours belong to a Party Address, and should be represented by an RFC 5455 RRULE and optionally an EXRULE or EXDATEs.
Let's say you have a Party:
"Acme, Inc."
They have one PhysicalAddress:
"123 Main Street, Vancouver"
Their address plays two roles:
"Sales" and "Service"
Sales is open Mo-Sa 10-8 and Service is open Mo-Fr 9-5
These are the RRULES:
(Sales)
startTime:'10:00:00'
endTime:'20:00:00'
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA
(Service)
startTime:'09:00:00'
endTime:'17:00:00'
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
In this case, I would probably do something relational, perhaps with STI if you wanted to have certain days where the business is closed (e.g. non-recurring closings). Here's a basic STI example:
class Business < ActiveRecord::Base
has_many :open_time_blocks
has_many :closed_time_blocks
def open?(time)
# false if time is "inside" any of the closed_time_blocks
# else is true if inside any of the open_time_blocks
# else is false
end
def closed?(time)
!open?
end
end
# == Schema Information
#
# Table name: time_blocks
#
# id :integer not null, primary key
# business_id :integer
# type :string(255)
# start_at :datetime
# end_at :datetime
# created_at :datetime
# updated_at :datetime
class TimeBlock < ActiveRecord::Base
belongs_to :business
end
class OpenTimeBlock < TimeBlock; end
class ClosedTimeBlock < TimeBlock; end
Checkout these libraries for handling recurring dates.
Once the recurrence is setup in an object say office_hours
, in ice_cube
you can query it as:
office_hours.occurring_at?(Time.now)
So I just found out ice_cube
can handle recurring durations:
> schedule = IceCube::Schedule.new(Time.now, :duration => 3600)
> schedule.add_recurrence_rule Rule.daily
> schedule.occurs_at?(Time.now + 1000)
true
> schedule.occurs_at?(Time.now + 1.day + 1000)
true
> schedule.occurs_at?(Time.now + 4000)
false
I suspect that you can handle a lot of different situations using ice_cube
in this manner.
Incidentally, I'm implementing a similar open/closed business schedule. I'd be interested to know how you've done it, and if you have a github repo we can collaborate on together.