Rails calculate Date duration from start_date and end_date - ruby-on-rails

In my Rails 6 app I've got a model with fields:
create_table "events", force: :cascade do |t|
t.date "start_date", null: false
t.date "end_date"
t.integer "duration"
end
When duration is not provided I need to have mechanism which calculates the missing value if start_date and end_date are given. The solution is pretty simple I guess:
def duration_calculator(start_date, end_date)
end_date - start_date
end
2.7.1 :049 > start_date
=> Mon, 15 Feb 2021
2.7.1 :050 > end_date
=> Mon, 15 Feb 2021
2.7.1 :051 > end_date - start_date
=> (0/1)
But it won't worked - I was expecting to get 1 since the event lasted all day. How to achieve that behaviour?

To avoid the rational number, you could use a inclusive range and count the number of days via:
(start_date..end_date).count
Unfortunately, count actually traverses the range instead of just calculating the difference.
You could calculate the difference yourself using jd – the Julian day number:
end_date.jd - start_date.jd + 1
Or combine both of the above into:
(start_date.jd..end_date.jd).count
Here, count calculates the difference, because it is an integer range.
Note that the range-based solutions don't allow negative results because ranges are always ascending in Ruby.

In your case, start_date is equal to end_date as both are Mon, 15 Feb 2021, so the result in console is correct. The returned fraction can be cast into an integer format by (end_date - start_date).to_i, but of course this will still return 0.
You should use DateTime instead of Date if you want to be more exact on timing of events, or you could adjust your helper to the following to ensure the smallest duration is 1:
def duration_calculator(start_date, end_date)
if end_date == start_date
1
else
end_date - start_date
end
end
Note, you would have to consider here about an event with a start_date of today, and an end_date of tomorrow, as that would also return 1.
EDIT
Updating the method above so the method will return the number of days spanned:
def duration_calculator(start_date, end_date)
duration_in_seconds = end_date.at_end_of_day - start_date.at_beginning_of_day
duration_in_days = duration_in_seconds/86400 # seconds in one day
duration_in_days.to_i # round to nearest day
end

Related

Migrating existing Time to DateTime in Rails while keeping data intact

Forgive me if I'm asking a silly question, but I've run into some trouble trying to update a time column to datetime.
Here's what the relevant part of my schema.rb looks like:
create_table "shop_hours", id: :serial, force: :cascade do |t|
t.time "from_hours"
t.time "to_hours"
t.string "day"
t.integer "repair_shop_id"
t.boolean "is_shop_open"
t.integer "chain_id"
t.integer "regions", default: [], array: true
t.index ["repair_shop_id"], name: "index_shop_hours_on_repair_shop_id"
end
Here's an example of random ShopHour object:
[67] pry(main)> ShopHour.find(439)
#<ShopHour:0x00007ff05462d3a0
id: 439,
from_hours: Sat, 01 Jan 2000 15:00:00 UTC +00:00,
to_hours: Sat, 01 Jan 2000 00:00:00 UTC +00:00,
day: "Friday",
repair_shop_id: 468,
is_shop_open: true,
chain_id: nil,
regions: []>
Ultimately, I want to migrate the attributes from_hours and to_hours on all of my ShopHour tables so that they're of type datetime.
I'd also like to update the date on each from_hours and to_hours to be current.
I tried this migration, but ran into an error:
class ChangeShopHoursToDateTime < ActiveRecord::Migration[6.0]
def change
change_column :shop_hours, :from_hours, 'timestamp USING CAST(from_hours AS timestamp)'
change_column :shop_hours, :to_hours, 'timestamp USING CAST(to_hours AS timestamp)'
end
end
Here's the error I'm encountering:
== 20201021083719 ChangeShopHoursToDateTime: migrating ========================
-- change_column(:shop_hours, :from_hours, "timestamp USING CAST(from_hours AS timestamp)")
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:
PG::CannotCoerce: ERROR: cannot cast type time without time zone to timestamp without time zone
LINE 1: ...s" ALTER COLUMN "from_hours" TYPE timestamp USING CAST(from_...
Please let me know if I can provide any more information. Thanks in advance!
You can't automatically actually cast a time column to a timestamp as a time has no date component. Postgres actually correctly prevents you from doing this as the result would be ambiguous - which date should it really cast 12:45 to:
0 BC?
the beginning of epoc time?
todays date?
Ruby doesn't actually have a class to represent a time without a date component. The major difference is that Time is simple wrapper written in C that wraps a UNIX timestamp and DateTime is better at historical times. The fact that Rails just casts a time database column to a Time starting at 2000-01-01 is really just a strange yet pragmatic solution to the problem instead of creating something like a TimeWithoutDate class.
If you want to migrate a database column from time to timestamp / timestampz you need to tell the database which date you expect the time to be at:
class AddDatetimeColumnsToHours < ActiveRecord::Migration[6.0]
def up
add_column :shop_hours, :opens_at, :datetime
add_column :shop_hours, :closes_at, :datetime
ShopHour.update_all(
[ "closes_at = (timestamp '2000-01-01') + to_hour, opens_at = (timestamp '2000-01-01') + from_hour" ]
)
end
def down
remove_column :shop_hours, :opens_at
remove_column :shop_hours, :closes_at
end
end
This adds two new columns and you should really consider just dropping the existing column and going with this naming scheme as methods that start with to_ are by convention casting methods in Ruby (for example to_s, to_a, to_h) - to_hour is thus a really bad name.

How to add a date attribute in rails models

Hi i'm a beginner in RoR and was having a little bit trouble in generating a particular model.
I want to create 2 models - List and Item. The List has_many Items and Item belongs_to List.
I want the Item model to have 3 attributes. rails g model Item name:string desc:string date:????
1.What data type to add for date:???
2.What format will the date attribute be in? (mm/dd/yy)?
3.And what kind of form input should it have?
f.date_field :date?
Thanks in advance!
1. What data type to add for date:???
In your migrations, you can use the following types for columns:
:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean
(extracted from the add_column transformation here)
In your case, if you don't need to store the time, you can use date:name_of_your_field.
2. What format will the date attribute be in? (mm/dd/yy)?
The attribute will be stored as a ActiveSupport::TimeWithZone, and you will have to format it when displaying it. You can use Time#strftime to do so.
your_attribute.strftime("%m/%d/%Y") #=> "11/19/2007"
3. And what kind of form input should it have?
Yes, you can perfectly use:
f.date_field :date?
It will return a text_field of type “date”. Depending on the browser support, a date picker will show up in the input field.
I hope that helps! Happy coding!
You can either have date or datetime according to the documentation. Therefore:
rails g model Item name:string desc:string date:datetime
or
rails g model Item name:string desc:string date:date
But the best practice is to use DateTime as a general purpose representation of time.
Although I would probably call it something more descriptive than date. (And just fyi created_at and updated_at columns are already created for you.)
The type is pretty much format agnostic. You can format it with strftime:
%Y%m%d => 20071119 Calendar date (basic)
%F => 2007-11-19 Calendar date (extended)
%Y-%m => 2007-11 Calendar date, reduced accuracy, specific month
%Y => 2007 Calendar date, reduced accuracy, specific year
%C => 20 Calendar date, reduced accuracy, specific century
%Y%j => 2007323 Ordinal date (basic)
%Y-%j => 2007-323 Ordinal date (extended)
%GW%V%u => 2007W471 Week date (basic)
%G-W%V-%u => 2007-W47-1 Week date (extended)
%GW%V => 2007W47 Week date, reduced accuracy, specific week (basic)
%G-W%V => 2007-W47 Week date, reduced accuracy, specific week (extended)
%H%M%S => 083748 Local time (basic)
%T => 08:37:48 Local time (extended)
%H%M => 0837 Local time, reduced accuracy, specific minute (basic)
%H:%M => 08:37 Local time, reduced accuracy, specific minute (extended)
%H => 08 Local time, reduced accuracy, specific hour
%H%M%S,%L => 083748,000 Local time with decimal fraction, comma as decimal sign (basic)
%T,%L => 08:37:48,000 Local time with decimal fraction, comma as decimal sign (extended)
%H%M%S.%L => 083748.000 Local time with decimal fraction, full stop as decimal sign (basic)
%T.%L => 08:37:48.000 Local time with decimal fraction, full stop as decimal sign (extended)
%H%M%S%z => 083748-0600 Local time and the difference from UTC (basic)
%T%:z => 08:37:48-06:00 Local time and the difference from UTC (extended)
%Y%m%dT%H%M%S%z => 20071119T083748-0600 Date and time of day for calendar date (basic)
%FT%T%:z => 2007-11-19T08:37:48-06:00 Date and time of day for calendar date (extended)
%Y%jT%H%M%S%z => 2007323T083748-0600 Date and time of day for ordinal date (basic)
%Y-%jT%T%:z => 2007-323T08:37:48-06:00 Date and time of day for ordinal date (extended)
%GW%V%uT%H%M%S%z => 2007W471T083748-0600 Date and time of day for week date (basic)
%G-W%V-%uT%T%:z => 2007-W47-1T08:37:48-06:00 Date and time of day for week date (extended)
%Y%m%dT%H%M => 20071119T0837 Calendar date and local time (basic)
%FT%R => 2007-11-19T08:37 Calendar date and local time (extended)
%Y%jT%H%MZ => 2007323T0837Z Ordinal date and UTC of day (basic)
%Y-%jT%RZ => 2007-323T08:37Z Ordinal date and UTC of day (extended)
%GW%V%uT%H%M%z => 2007W471T0837-0600 Week date and local time and difference from UTC (basic)
%G-W%V-%uT%R%:z => 2007-W47-1T08:37-06:00 Week date and local time and difference from UTC (extended)
Thanks to #BWStearns for that quote
Finally as far as input field goes: Have a look at these Form helpers.
<%= date_field(:user, :born_on) %>
<%= datetime_field(:user, :meeting_time) %>
<%= datetime_local_field(:user, :graduation_day) %>

Create endtime from collection_select

Good Afternoon,
Feels like a newbie question but I have the following:
:start_datetime t.date
:end_datetime t.date
:length t.integer
On create my end_datetime is nil but I want to get it by adding the length to the start_datetime in order to generate the endtime.
Currently my integer is stored as '30 Mins', 30 and '1 Hour', 60.
Drawn a blank on where I should do this. I'm guessing I need to create it in the model when the booking is created.
If you're going to be using increments of minutes, I think you should start by redefining your datetimes as datetime rather than date.
then do something like this:
#controller
def create
...
end_datetime = params[:start_datetime] + params[:length].minutes
#save this
...
end

How to scope last week by Date object [duplicate]

This question already has an answer here:
Closed 11 years ago.
Possible Duplicate:
Scoping date attribute for this week?
I am trying to scope all of my Products for this week, so it should show all the products leading up to whichever day of the week.
class Product < ActiveRecord::Base
attr_accessible :purchase_date
def self.last_week # All prices of last week.
where(:purchase_date => 1.week.ago)
end
create_table :products do |t|
t.date :purchase_date
end
end
This code renders nothing in the view though so what do I need to correct?
ANSWER
For some reason I had to add advance(:days => -1) to in order to also retrieve Monday as well. You may not have to do this though.
def self.last_week
where(:purchase_date => 1.week.ago.beginning_of_week.advance(:days => -1)..1.week.ago.end_of_week).order("purchase_date desc")
end
UPDATED ANSWER
I had to do the advance(:days => -1) because of the Time zone I am in. I got rid of this by making sure I'm in my own Time zone. So now it can be normal as it should be:
def self.last_week
where(:purchase_date => 1.week.ago.beginning_of_week..1.week.ago.end_of_week)
end
And it should work correctly ONLY if you go by the default Rails Time zone or you config your own:
app/config/environment/development.rb
config.time_zone = "Eastern Time (US & Canada)"
Good luck.
This should do the trick:
scope :last_week, lambda { where("purchase_date >= :date", :date => 1.week.ago) }
scope :past_week, lambda { where("purchase_date >= :start_date AND purchase_date <= :end_date", {:start_date => 1.week.ago, :end_date => 1.day.ago }) }

Compare a DateTime to the current date

I am trying to use a condition on events when the start_at DateTime is equal to or greater than Today's date.
I want to list upcoming events, but clearly they are not upcoming if they have already passed.
I have:
#appointments = Event.find(:all, :conditions => ['move_id = ? AND start_at = ?', #move.id, Date.today])
I think I may be comparing apples and oranges here. It doesn't throw and error, just doesn't do what it is supposed to.
Help! Thanks in advance.
Try:
#appointments = Event.find(:all, :conditions => ['move_id = ? AND start_at >= ?', #move.id, DateTime.now])
Weird is that I can't find DateTime#now documentation.
I'm using Postgres for this test.
>> Event.last.created_at.class
=> Time
>> Event.find(:first, :conditions => ['created_at >= ?', DateTime.now]).valid?
=> true
Migration field code
t.datetime "created_at", :null => false
as you said, start_at is equal or greater than today's date, perhaps it was a problem with the operator that was checking your event's start_at with today's date
Evento.find(:all, :conditions => ['start_at >= ?', DateTime.now])
if you're using mysql you could use "start_at >= CURRENT_DATETIME" or something like this also

Resources