Rails - Query time shifts - ruby-on-rails

currently I'm developing a system that manages work shifts.
The system contains an endpoint that returns the current shift based on server time. I created a query that works fine for regular shifts like 08:00 AM to 04:00 PM and so on but the challenge is when there is a dawn shift that starts at 10:00 PM until 06:00 AM from the other day.
My model contains the following fields:
name (String)
start_at (Time)
end_at (Time)
Ps.: I'm using Postgres as the database.
My code:
class Shift < ApplicationRecord
def self.current
current_time = Time.now()
current_time = current_time.change(year: 2000, month: 1, day: 1)
#current ||= Shift
.where('start_time <= ?', current_time)
.where('end_time >= ?', current_time)
.first()
end
end
Ps.: I guess, since I'm using the Time type in the database, I have to normalize the Time.now to use the 2000 - 01 - 01 date.
So, is there an easy/best way to do that?
Thanks for the help!

Interesting problem! So there's two cases: (1) a normal shift (where start_time <= end_time), and (2) a shift that overlaps midnight (where start_time > end_time).
You are already handling the first case by checking if the current time is between the start time and the end time.
I believe the second case can be handled by checking if the current time is either between the start time and midnight, or between midnight and the end time. Which translates to start_time <= ? OR end_time >= ?.
I haven't used Rails in a while, but I think you could do something like this:
#current ||= Shift
.where('start_time <= end_time')
.where('start_time <= ?', current_time)
.where('end_time >= ?', current_time)
.or(Shift
.where('start_time > end_time')
.or(Shift
.where('start_time <= ?', current_time)
.where('end_time >= ?', current_time)))
.first()
If you go with this, consider splitting the two cases into separate scopes, so you can write something more readable like this in this method:
#current ||= current_normal_shifts.or(current_dawn_shifts).first

You might be interested in the tod gem that provides a TimeOfDay class and a Shift class that takes two TimeOfDay objects to represent the start and end of the shift. It handles dawn shifts as expected.
An implementation might look like this:
require 'tod'
class Shift < ApplicationRecord
def self.current
find(&:current?)
end
def current?
schedule.include?(Tod::TimeOfDay(Time.now))
end
def schedule
Tod::Shift.new(Tod::TimeOfDay(start_time), Tod::TimeOfDay(end_time))
end
end

Related

Can I replace (Date.current - created_at.to_date).to_i <= number_of_days with number_of_days.days.ago.to_date? Is it a good idea?

In my models/book.rb I have this
def created_since?(number_of_days)
(Date.current - created_at.to_date).to_i <= number_of_days
end
That is used in view files like
<% if #book.created_since?(30) %>
Can I optimize it editing with something like this?
def created_since?(number_of_days)
number_of_days.days.ago.to_date
end
Is it a good idea? will it work the same?
To subtract days from a date, it's easiest to work with dates. I would try something like this, from here:
def created_since?(number_of_days)
created_at.to_date >= Date.now - number_of_days
end
The only downside of this is it will not be precise to the minute, if you did want this accuracy you'll need to edit the Time rather than the Date:
def created_since?(number_of_days)
created_at >= Time.now - (number_of_days * 24 * 60 * 60)
end
This takes the number of days, converts it into seconds, and then uses that to find the correct cut off point.

Fetching Data based date and time

I am trying to find results from today onwards but also want to include the yesterdays plans if the time is between 12:00am-5:00am
Right now i have the following
def self.current
where(
"plan_date >= :today",
today: Date.current,
)
end
Is there a way i can know the time of the day based on the users timezone which am setting as bellow in the app controller and make sure that if its before 6:am the next day i want to include the previous days results as well.
def set_time_zone(&block)
if current_user
Time.use_zone(current_user.time_zone_name, &block)
else
yield
end
end
Try this:
def self.current
where(
"plan_date >= :today",
today: (Time.zone.now.in_time_zone(get_user_time_zone) - 6.hours).beginning_of_day,
)
end
...where get_user_time_zone returns the time zone for the user (E.G.: America/New_York). I'm using - 6.hours because you wanted it to be "before 6am" local time.

ActiveRecord where method datetime passed

Thanks for your continuing support in my latest venture in a Rails app. It's been a while since I've made something in Rails so here is my latest question. I appreciate the help you've all given in the past.
I have a Model called Event.rb which contains a date:date and time:time fields for the Event.
I also have a method inside of the model which is..
def begins
DateTime.new(date.year, date.month, date.day, time.hour, time.min, time.sec)
end
As I can't see if something has truly passed because I only have Date and Time separate so I need them together.
My question is...
I want to be able to add in the DateTime :begins into the following other method in my Model...
def self.past
where("date <= ?", TIME_NOW)
end
Just like I have a method which is...
def upcoming?
self.date >= Time.now
end
Which I could easily change self.date to begins and would past I would imagine?
Thanks!
Perhaps something like this will work for querying the database for past events using your existing date and time columns:
scope :past, lambda {
where("date <= ? and time <= ?",
Time.now.strftime("%Y-%d-%m"),
Time.now.strftime("%H:%M:%S")
)
}
past_events = Event.past
For checking the current instance, you could continue to use your begins method:
def past?
begins < DateTime.now
end
#event = Event.first
#event.past?

Rails/Postgres query rows grouped by day with time zone

I'm trying to display a count of impressions per day for the last 30 days in the specific users time zone. The trouble is that depending on the time zone, the counts are not always the same, and I'm having trouble reflecting that in a query.
For example, take two impressions that happen at 11:00pm in CDT (-5) on day one, and one impression that happens at 1:00am CDT. If you query using UTC (+0) you'll get all 3 impressions occurring on day two, instead of two the first day and one the second. Both CDT times land on the day two in UTC.
This is what I'm doing now, I know I must be missing something simple here:
start = 30.days.ago
finish = Time.now
# if the users time zone offset is less than 0 we need to make sure
# that we make it all the way to the newest data
if Time.now.in_time_zone(current_user.timezone) < 0
start += 1.day
finish += 1.day
end
(start.to_date...finish.to_date).map do |date|
# get the start of the day in the user's timezone in utc so we can properly
# query the database
day = date.to_time.in_time_zone(current_user.timezone).beginning_of_day.utc
[ (day.to_i * 1000), Impression.total_on(day) ]
end
Impressions model:
class Impression < ActiveRecord::Base
def self.total_on(day)
count(conditions: [ "created_at >= ? AND created_at < ?", day, day + 24.hours ])
end
end
I've been looking at other posts and it seems like I can let the database handle a lot of the heavy lifting for me, but I wasn't successful with using anything like AT TIME ZONE or INTERVAL.
What I have no seems really dirty, I know I must missing something obvious. Any help is appreciated.
Ok, with a little help from this awesome article I think I've figured it out. My problem stemmed from not knowing the difference between the system Ruby time methods and the time zone aware Rails methods. Once I set the correct time zone for the user using an around_filter like this I was able to use the built in Rails methods to simplify the code quite a bit:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
around_filter :set_time_zone
def set_time_zone
if logged_in?
Time.use_zone(current_user.time_zone) { yield }
else
yield
end
end
end
# app/controllers/charts_controller.rb
start = 30.days.ago
finish = Time.current
(start.to_date...finish.to_date).map do |date|
# Rails method that uses Time.zone set in application_controller.rb
# It's then converted to the proper time in utc
time = date.beginning_of_day.utc
[ (time.to_i * 1000), Impression.total_on(time) ]
end
# app/models/impression.rb
class Impression < ActiveRecord::Base
def self.total_on(time)
# time.tomorrow returns the time 24 hours after the instance time. so it stays UTC
count(conditions: [ "created_at >= ? AND created_at < ?", time, time.tomorrow ])
end
end
There might be some more that I can do, but I'm feeling much better about this now.
Presuming the around_filter correctly works and sets the Time.zone in the block, you should be able to refactor your query into this:
class Impression < ActiveRecord::Base
def self.days_ago(n, zone = Time.zone)
Impression.where("created_at >= ?", n.days.ago.in_time_zone(zone))
end
end

Using Timecop gem For Scopes

I'm spec'ing a scope in a Rails 3.0 app as follows:
class DrawingList < ActiveRecord::Base
scope :active_drawings, where('start_date <= ? AND end_date >= ?', Date.today, Date.today)
end
In my spec, I want to do:
before do
#list = DrawingList.create #things that include begin and end dates
end
it "doesn't find an active drawing if they are out of range" do
pending "really need to figure out how to work timecop in the presence of scopes"
Timecop.travel(2.days)
puts Date.today.to_s
DrawingList.active_drawings.first.should be_nil
end
As you might imagine, the puts really shows that Date.today is two days hence. However, the scope is evaluated in a different context, so it uses the old "today". How does one get today evaluated in a context that Timecop can affect.
Thanks!
This is a really common mistake. As you've written in the date used by the scope is the date as it was when the code was loaded. Were you to run this in production where code is only reloaded if you restart the app (unlike development where it is reloaded on each request), you'd get the right results on the day you restarted the app, but the next day the results would be out by one day, the day after by 2 days etc.
The correct way of defining a scope like that is
scope :active_drawings, lambda { where('start_date <= ? AND end_date >= ?', Date.today, Date.today)}
The lambda ensures that those dates are evaluated each time the scope is used.

Resources