18

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:

"Monday to Friday 9am to 5pm
Saturday 11am to 4pm
Closed Sundays"

But, requirements have changed and I need to check the hours against the current date & time and display an "Open" or "Closed" in the view. Something like:

class Business < ActiveRecord::Base

  def open?
    # Something like ... 
    Time.now > open_time && Time.now < close_time
  end

end

So what would be the best way to tackle this in terms of storing the hours for each day of the week? Should the Business simply has_many :open_blocks (or whatever) that have open and close times? Should I just store the day as a string?

Callmeed
  • 4,972
  • 5
  • 34
  • 49
  • 1
    Possible duplicate of [Storing Business Hours in a Database](http://stackoverflow.com/questions/1036603/storing-business-hours-in-a-database) – David Aldridge Nov 29 '16 at 19:52

5 Answers5

7

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.

Simon Franzen
  • 2,628
  • 25
  • 34
  • We also allow the user a freetext field in our entry-model, like you did until now. – Simon Franzen Nov 29 '16 at 19:45
  • Thats an interesting and smart solution! But I have a few questions: 1. How can I add special opening hours like christmas (with an user readable name)? 2. What happens, when I define two entries that overlap each other? E.g. I have 'standard opening hours' and vacation. I still want my standard opening hours to be shown. How do I know which opening hours are currently valid? 3. When defining an entry for vacation, how can I tell the user that this is vacation time and not permanently closed? – Michael L. Aug 06 '18 at 08:28
  • 1
    1. I would add special holiday with valid_from / valid_through attributes. In your output of the opening hours or in your is_open? method you have to watch after these valid_from/valid_through. 2. I would disallow adding overlapping entries with a valid_from/valid_through attribtute. In your output you always have to watch after: Is there any valid_from/valid_through entry? If not, look after a standard opening hour defined by day of the week. – Simon Franzen Aug 06 '18 at 12:00
  • regarding 3: Maybe you could add a flag holiday in the opening hour model and if set to true you can look after these types in your frontend output or in your is_open? method – Simon Franzen Aug 06 '18 at 12:02
  • + 3: or maybe a generell handling of holidays. They could also come from an external API, like special hoilidays in your country. I would look after those in your is_open? or render method for frontend – Simon Franzen Aug 06 '18 at 12:05
2

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
Michelle Tilley
  • 157,729
  • 40
  • 374
  • 311
1

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
Neil McGuigan
  • 46,580
  • 12
  • 123
  • 152
0

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)
Anurag
  • 140,337
  • 36
  • 221
  • 257
  • Ok, ice_cube looked good but then I realized it only stores recurring "start times" of events. It's not really designed for recurring blocks of start and end blocks of time. Still searching ... – Callmeed Jul 28 '10 at 22:00
0

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.

thekingoftruth
  • 1,711
  • 1
  • 25
  • 24
  • 1
    `ice_cube` is a good alternative as long as you **don't** need to have specific durations per recurrence rule (which you likely do). For more information check [this answer](http://stackoverflow.com/a/13539593/2192331). – Gabriel Osorio Nov 03 '15 at 17:12