Rails. How to store time of day (for schedule)? - ruby-on-rails

I'm writing an app that keeps track of school classes.
I need to store the schedule. For example: Monday-Friday from 8:am-11am.
I was thinking about using a simple string column but I'm going to need to make time calculations later.
For example, I need to store a representation of 8am, such as start_at:8am end_at:11am
So how should I store the time? What datatype should I use? Should I store start time and number of seconds or minutes and then calculate from there? or is there an easier way?
I use MySQL for production and SQLite for development.

I made an app recently that had to tackle this problem. I decided to store open_at and closed_at in seconds from midnight in a simple business hour model. ActiveSupport includes this handy helper for finding out the time in seconds since midnight:
Time.now.seconds_since_midnight
This way I can do a simple query to find out if a venue is open:
BusinessHour.where("open_at > ? and close_at < ?", Time.now.seconds_since_midnight, Time.now.seconds_since_midnight)
Any tips for making this better would be appreciated =)

If you're using Postgresql you can use a time column type which is just the time of day and no date. You can then query
Event.where("start_time > '10:00:00' and end_time < '12:00:00'")
Maybe MySQL has something similar

Check out the gem 'tod' for Rails 4 or Time_of_Day for Rails 3. They both solve the problem of storing time in a database while using an an Active Record model.
SQL has a time data type but Ruby does not. Active Record addresses this difference by representing time attributes using Ruby’s Time class on the canonical date 2000-01-01. All Time attributes are arbitrarily assigned the same dates. While the attributes can be compared with one another without an issue, (the dates are the same), errors arise when you attempt to compare them with other Time instances. Simply using Time.parse on a string like ”10:05” adds today’s date to the output.
Lailson Bandeira created a created solution for this problem, the Time_of_Day gem for Rails 3. Unfortunately the gem is no longer maintained. Use Jack Christensen’s ‘tod’ gem instead. It works like a charm.

This ruby gem converts time of day to seconds since midnight and back. The seconds value is stored in the database and can be used for calculations and validations.
Define the time of day attributes:
class BusinessHour < ActiveRecord::Base
time_of_day_attr :opening, :closing
end
Converts time of day to seconds since midnight when a string was set:
business_hour = BusinessHour.new(opening: '9:00', closing: '17:00')
business_hour.opening
=> 32400
business_hour.closing
=> 61200
To convert back to time of day:
TimeOfDayAttr.l(business_hour.opening)
=> '9:00'
TimeOfDayAttr.l(business_hour.closing)
=> '17:00'
You could also omit minutes at full hour:
TimeOfDayAttr.l(business_hour.opening, omit_minutes_at_full_hour: true)
=> '9'

I would store the starting hour and the duration within the database, using two integer columns.
By retrieving both values, you could convert the starting hour as in (assuming that you know the day already:
# assuming date is the date of the day, datetime will hold the start time
datetime = date.change({:hour => your_stored_hour_value , :min => 0 , :sec => 0 })
# calculating the end time
end_time = datetime + your_stored_duration.seconds
Otherwise, hava a look at Chronic. The gem makes handling time a little bit easier. Note that the changemethod is part of rails, and not available in plain Ruby.
The documentation on DateTime for plain Ruby can be found here.
Also, whatever you do, don't start storing your dates/time in 12-hour format, you can use I18nin Rails to convert the time:
I18n.l Time.now, :format => "%I.%m %p", :locale => :"en"
I18n.l Time.now + 12.hours, :format => "%I.%m %p", :locale => :"en"
You can also get from this notation, that you can store you duration in hours, if you want, you can then convert them rather easily by:
your_stored_value.hours
if stored as an integer, that is.

Suggestion:
Don’t worry about a specific datatype for that. A simple solution would be:
In the database, add an integer type column for start_time and another for end_time. Each will store the number of minutes since midnight.
Ex: 8:30am would be stored as 510 (8*60+30)
In the form, create a select field (dropdown) that displays all available times in time format:Ex.: 10am, 10:30am and so on.
But the actual field values that get saved in the database are their integer equivalents:
Ex: 600, 630 and so on (following the example above)

I assume you are using some kind of database for this. If you are using MySQL or Postgresql, you can use the datetime column type, which Ruby/Rails will automatically convert to/from a Time object when reading/writing to the database. I'm not sure if sqlite has something similar, but I imagine it probably does.

From the SQLite 3 website,
"SQLite does not have a storage class set aside for storing dates and/or times. Instead, the built-in Date And Time Functions of SQLite are capable of storing dates and times as TEXT, REAL, or INTEGER values:
TEXT as ISO8601 strings ("YYYY-MM-DD HH:MM:SS.SSS").
REAL as Julian day numbers, the number of days since noon in Greenwich on November 24, 4714 B.C. according to the proleptic Gregorian calendar.
INTEGER as Unix Time, the number of seconds since 1970-01-01 00:00:00 UTC.
Applications can chose to store dates and times in any of these formats and freely convert between formats using the built-in date and time functions."
You can then manipulate the values using the Date and Time functions outlined here.

Related

Save only time in rails

I have two time fields in the table i.e. start_time and end_time .
When I execute MyModel.save(start_time: '12:34'), it gets saved with appending a date(Sat, 01 Jan 2000 07:25:00 UTC +00:00).
I want to save time only. I am using Rails5
When we execute, a = MyModel.save(start_time: '12:34'), it saves only time in the database. But when we display the value in the console(a.start_time), we gets time with date. So we have to retrieve time from it by the following:
a.start_time.strftime("%H:%M")
Data gets correctly saved in the database
Rails will always append date with time even its type is "time".
You can achieve storing only time using sql query.
But i would suggest you to use one of the following options:
1)store time in column of type fixnum or decimal in 24hrs format like "22.44"
2) Store number of seconds in column of type integer & then write a helper method to confirm it into time.
3)Use column type :time and ignore the date whole querying.

Rails 5 how to save difference in hour and minutes between two datatime in postgres?

my table has 3 columns: data type timestamp,
|created_At | final_time| duracion(difference between created at and final_time)
| | |
the column difference should save the difference in hours and minutes, in this format HH:MM
this is my controller:
def horario
horario.update(duracion: params[:duracion]) // this params is "00:59"
end
but in the table Horarios, in column duracion i have this:
2017-12-24 03:59:00
so i want to save 00:59 (59 minutes) but postgres save all current date and add 3 hours more.
i want to save so in the future i will be able tu sum column duracion. Or should i change data type for this column? In this case which datatype you recomend me for rails to save HH:MM??
thanks.
Rails 5 supports PostgreSQL's interval type to some extent. You can create interval columns in the usual way and they will be properly represented in db/schema.rb. You can also assign them values in the usual way so you can say things like:
model.some_interval = '6 hours'
and get 06:00:00 inside the database. However, there is nothing in Ruby or Rails that properly represents a time interval (we only have various timestamp and date classes) so when that interval comes out of the database, you'll have a string on your hands, i.e:
> model = Model.find(some_id)
> model.some_interval.class
=> String
so you might end up having to manually parse some strings in Ruby. For simple intervals like '6 hours', this will be easy but it won't be so easy with more complicated intervals like '6 years 23 days 11 hours'.
If you'll only be working with your time intervals inside the database then interval would be natural and easy, you can say things like:
select some_timestamp + some_interval
and
where some_timestamp + some_interval < some_other_timestamp
and everything will work nicely.
However, if you need to work with the intervals back in Ruby then you'd probably be better off storing the interval as a number of seconds in an integer column (or whatever resolution you need). Then you could say things like:
where some_timestamp + (some_interval_in_seconds || 'seconds')::interval < some_other_timestamp
inside the database and
some_time + model.some_interval_in_seconds
back in Ruby.
In any case, strings are probably the wrong approach unless you really like parsing strings everywhere all the time.
As others already pointed out, Rails handles the Postgres Interval type as a string. A string that, unfortunately, is not easy to parse.
If you do this:
u = Users.select('edited_at - created_at as time_dif')
puts u.first['time_dif']
You can get something like 168 days 12:51:20.851115. Ugly right?
Well, using Ruby to convert this string into an useful number is not easy, but you can use Postgres to do the job for you. You will need to do a plain SQL query though, but it's the best method I've found so far:
query = ActiveRecord::Base.connection.execute("
SELECT EXTRACT(epoch FROM time_dif)/3600 as hours_dif
FROM
(
SELECT (edited_at - created_at) as time_dif
FROM users
) AS MainQuery
")
In this example, Postgres' EXTRACT function will convert the Interval type into a number which represents the total seconds of the interval. If you divide this number by 3600 you will get the different in hours as in the example above.
Then, if you want to iterate over the results:
query.each do |r|
puts r['hours_dif']
end
You could save duracion as a float type, where duracion would equal something like final_time - created_at and this value would be the difference in seconds. You can then perform arithmetic with these values and always convert back to minutes, hours, or whatever you need.

Time datatype in Rails

I created a database using rails with stime attribute (referring to the starting time). Its type is Time. When I added a record with:
stime = "10:10"
it has been added as:
"stime":"2000-0101T10:10:00.000Z"
I only want the time, not the date. I thought it's the difference between Time and DateTime. Could anyone tell me what the problem is?
As the documentation states, it is not.
Time is an abstraction of dates and times. Time is stored internally
as the number of seconds with fraction since the Epoch, January 1,
1970 00:00 UTC.
There are several things you can do:
Store it as a string and do regex validation.
If you are using Postgres and don't mind depending on it, you can use time type. This would allow you do things like: YourModel.where("stime > '13:00:00'").
Store the time of the day in seconds since midnight. This will require you to add some helper methods for creating and scopes for querying data. You might find ActiveSupport's extension on numeric values helpful.
One way of handling this issue, to exemplify Uzbekjon's third point, is storing the time as seconds since midnight in an integer column:
stime = Time.now
# => 2016-04-13 10:58:13 -0700
seconds = stime.seconds_since_midnight.to_i
# => 39493
You can then retrieve this value and parse it into the time of day when needed:
time = Time.at(seconds).utc
# => 1970-01-01 10:58:13 UTC
result = time.strftime("%I:%M")
# => "10:58"
Hope it helps!

ruby timezone conversion issues

I have a scenario in which i get a timestamp and i need to search for all bookings for that date in that timestamp. The timestamp is in users respective timezone and all the records in the database are stored in UTC. so naturally i need to convert that timestamp back to UTC and then search.
Here's something that i'm doing:
Booking.where("date_time >= '#{DateTime.parse(timestamp).in_time_zone('UTC').beginning_of_day}' and date_time <= '#{DateTime.parse(timestamp).in_time_zone('UTC').end_of_day}'")
which basically means to fetch all bookings from the beginning of day till the end
However, when i use the following query it gives me a different result:
Booking.where("date_time >= '#{DateTime.parse(timestamp).beginning_of_day.in_time_zone('UTC')}' and date_time <= '#{DateTime.parse(timestamp).end_of_day.in_time_zone('UTC')}'")
I'm wondering which one is actually the correct statement to use in my use case and i would appreciate some input here.
I wouldn't use either one.
This one:
DateTime.parse(timestamp).in_time_zone('UTC').beginning_of_day
gives you the beginning of the UTC day, not the beginning of the local-time-zone-day offset to UTC. In short, it is incorrect and won't give you what you're looking for.
This one:
DateTime.parse(timestamp).beginning_of_day.in_time_zone('UTC')
is correct as it changes the time to the beginning of the day in the local time zone and then converts the timestamp to UTC.
If you let ActiveRecord deal with the quoting using a placeholder, then it will apply the UTC adjustment itself.
I'd also use < t.tomorrow.beginning_of_day rather than <= t.end_of_day to avoid timestamp truncation and precision issues; the end of the day is considered to be at 23:59:59.999... and that could leave a little tiny window for errors to creep in. I'm being pretty pedantic here, you might not care about this.
I'd probably do it more like this:
t = DateTime.parse(timestamp)
Booking.where('date_time >= :start and date_time < :end',
:start => t.beginning_of_day,
:end => t.tomorrow.beginning_of_day
)

How does MongoDB compares the date only and ignores the time, such as date <= '2010-09-10'?

For some reason:
Analytic.where({:ga_date.gte => '2010-09-01'}).count() # greater than or equal to
gives back 0, but
Analytic.where({:ga_date.gte => Time.parse('2010-09-01')}).count()
gives back 230, which is the number of records (documents).
Actually, the first line on the top works in another case, so it is quite strange.
Can only the date be compared, because if it is
Analytic.where({:ga_date.lte => Time.parse('2010-09-10')}).count() # less than or equal to
then all the records with date 2010-09-10 will not be counted because Time.parse('2010-09-10') will give 2010-09-10 00:00:00, so the records will all have to be 2010-09-09 before the midnight. In other words, 2010-09-10 2am won't be included because 2am is not "less than or equal to" 00:00:00. It can be hacked by using
Analytic.where({:ga_date.lte => Time.parse('2010-09-10 23:59:59')}).count()
but it is kind of ugly. If there is a way to compare by date only like the first line of code in this post?
I think that you have two separate issues here.
Different data types
The following two lines are not equivalent. The first is a string comparison. The second is a comparison with a date object.
Analytic.where({:ga_date.gte => '2010-09-01'}).count()
Analytic.where({:ga_date.gte => Time.parse('2010-09-01')}).count()
I think you have figured this out, but it's important to be clear here. If you are storing date objects in the DB, you need to perform comparisons with date objects.
MongoDB will compare types and data.
Mismatch date storage
You are storing dates that have information for hours, minutes and seconds. However, you don't like the following notation:
:ga_date.lte => Time.parse('2010-09-10 23:59:59')
The workaround here is to use $lt and the day after.
:ga_date.lt => (Time.parse('2010-09-10') + 1.day) # or (60 * 60 * 24)
to add,
it is not strangely works, its coincidentally works when it just happens the string representation of the date happens to also lexicographically be 'greater than' the other date
other issue,
try to use only as much data fields as needed
if you meant it to be "within the calendar day",
what I usually like is to call beginning_of_day in both cases to equalize
this has the effect of neutralizing the minutes
else if you really meant within a 24h strike zone,
use ActiveSupport's '+ 1.day'

Resources