Ruby DateTime: Get next 5:15pm (or similar) - ruby-on-rails

So, given a DateTime object, and a fixed time, I want to get the next occurrence of the fixed time after the given DateTime object.
For example, given the date of 14th March, 2016, 4:00pm, and the time of 5:15pm, I want to return 14th March, 2016 5:15pm.
However, given the date of 14th March, 2016, 6:00pm, and the time of 5:15pm, I want to return 15th March, 2016, 5:15pm, since that's the next occurrence.
So far, I've written this code:
# Given fixed_time and date_time
new_time = date_time
if fixed_time.utc.strftime("%H%M%S%N") >= date_time.utc.strftime("%H%M%S%N")
new_time = DateTime.new(
date_time.year,
date_time.month,
date_time.day,
fixed_time.hour,
fixed_time.min,
fixed_time.sec
)
else
next_day = date_time.beginning_of_day + 1.day
new_time = DateTime.new(
next_day.year,
next_day.month,
next_day.day,
fixed_time.hour,
fixed_time.min,
fixed_time.sec
)
end
# Return new_time
It works, but is there a better way?

I would construct the new date time just once and add 1 day if needed:
# Given fixed_time and date_time
new_date_time = DateTime.new(
date_time.year,
date_time.month,
date_time.day,
fixed_time.hour,
fixed_time.min,
fixed_time.sec
)
# add 1 day if new date prior to the given date
new_date_time += 1.day if new_date_time < date_time

Here's a little stab at refactoring it to remove some of the redundancy:
# Given fixed_time and date_time
base_date = date_time.to_date
if fixed_time.to_time.utc.strftime("%T%N") <= date_time.to_time.utc.strftime("%T%N")
base_date = base_date.next_day
end
new_time = DateTime.new(
base_date.year,
base_date.month,
base_date.day,
fixed_time.hour,
fixed_time.min,
fixed_time.sec
)
# Return new_time
The biggest changes are that the base_date is determined before the new_time is created, so that it can be used there.
I also used the next_day method on DateTime to get the next day, and used the "%T" format specifier as a shortcut for "%H:%M:%S"
Here's a little test program that to show that it works:
require "date"
def next_schedule(fixed_time, date_time)
# Given fixed_time and date_time
base_date = date_time.to_date
if fixed_time.to_time.utc.strftime("%T%N") <= date_time.to_time.utc.strftime("%T%N")
base_date = base_date.next_day
end
new_time = DateTime.new(
base_date.year,
base_date.month,
base_date.day,
fixed_time.hour,
fixed_time.min,
fixed_time.sec
)
# Return new_time
end
StartTime = DateTime.strptime("2016-02-14 17:15:00", "%F %T")
Dates = [
"2016-03-14 16:00:00",
"2016-03-14 18:00:00"
]
Dates.each do |current_date|
scheduled = next_schedule(StartTime, DateTime.strptime(current_date, "%F %T"))
puts "Scheduled: #{scheduled.strftime('%F %T')}"
end
The output of this is:
Scheduled: 2016-03-14 17:15:00
Scheduled: 2016-03-15 17:15:00
It's using the test cases described in the question, and it gets the expected answers.

Related

How to make working hours in ruby ​on rails?

I'm creating a chat panel with a bot. Every message sent by the bot will depend on working hours. For example, during business hours the customer sends a message to the bot and the bot will reply to the text message with the sentence: Hello, you sent a message during business hours
and for example when outside working hours the customer sends a text message to the bot and the bot will reply to the message with the sentence: Hello, you sent a message outside of working hours. Where working hours apply every Monday-Saturday at 08: 30-17: 00.
I made a configuration using an array like this:
start_on = [
"Monday, 08:30:00",
"Tuesday, 08:30:00",
"Wednesday, 08:30:00",
"Thursday, 08:30:00",
"Friday, 08:30:00",
"Saturday, 08:30:00"
]
end_on = [
"Monday, 17:00:00",
"Tuesday, 17:00:00",
"Wednesday, 17:00:00",
"Thursday, 17:00:00",
"Friday, 17:00:00",
"Saturday, 17:00:00"
]
And what I want to ask is how to set the current time current_time = (Time.now.to_time) by configuring the working hours in the array start_on and end_on using ruby ​​on rails?
This function returns true if the day is not sunday and the time is between 08:30 and 17:00
def is_working_hour?(time)
!time.sunday? && time.to_s(:time).between?('08:30','17:00')
end
is_working_hour?(Time.now)
I would personally structure the start and end times like this using wday as the day of the week (0 is sunday so 1 == monday):
times = [
{ day: 1, start: "08:30:00", end: "17:00:00" },
{ day: 2, start: "08:30:00", end: "17:00:00" },
...
]
Once you've got a structure like this (you can work it work it out from the above too of course), you can use the following to get today's times.
today = times.detect { |time| time[:day] == Time.now.wday }
Then start and end times are just:
start_time = Time.parse(today[:start])
end_time = Time.parse(today[:end])
# start_time = Time.parse("08:30:00")
# end_time = Time.parse("17:00:00")
Then it's simply a matter of seeing if the current time is inside that range:
Time.now > start && Time.now < end_time
=> true
To convert the values in DateTime, you have to strptime the weekday for Date, and the time for the Time. Then combine the resulting date and time to obtain the DateTime.
curr_index=1 #loop index if necessary
#get start datetime
arr=start_on[curr_index].split(",")
d = Date.strptime(arr[0].strip, '%A')
t = Time.strptime(arr[1].strip, ' %T')
startime = DateTime.new(d.year, d.month, d.day, t.hour, t.min, t.sec, t.zone)
#get end datetime
arr=end_on[curr_index].split(",")
d = Date.strptime(arr[0].strip, '%A')
t = Time.strptime(arr[1].strip, ' %T')
endtime = DateTime.new(d.year, d.month, d.day, t.hour, t.min, t.sec, t.zone)
return (startime..endtime).cover? Time.now #check if within the range

Find out if current time is between two times

I have two time columns stored in a Postgresql database: open_time and close_time. I'm trying to find out if the current time, ignoring the date, is between the two times, ignoring the dates.
This code compares the dates as well as the time:
current_time = Time.now
if current_time.between?(store.open_time, store.close_time)
puts "IN BETWEEN"
end
It doesn't work, for example, when current_time # => 2018-06-06 23:59:49 -0600 and open_time # => 2000-01-01 22:59:00 UTC.
How do I get it to not include the dates, and just compare the times?
require 'time'
TIME_FMT = "%H%M%S"
def store_open_now?(open_time, close_time)
nt = Time.now.strftime(TIME_FMT)
ot = open_time.strftime(TIME_FMT)
ct = close_time.strftime(TIME_FMT)
ot <= ct ? (nt >= ot && nt <= ct) : (nt >= ot || nt <= ct)
end
As I write, the time is now about 32 minutes past midnight.
Time.now.strftime(TIME_FMT)
#=> "003252"
Suppose
open_time = DateTime.parse("09:00")
#=> #<DateTime: 2018-06-07T09:00:00+00:00 ((2458277j,32400s,0n),
# +0s,2299161j)>
close_time = DateTime.parse("17:00")
#=> #<DateTime: 2018-06-07T17:00:00+00:00 ((2458277j,61200s,0n),
# +0s,2299161j)>
Then
open_time.strftime(TIME_FMT)
#=> "090000"
close_time.strftime(TIME_FMT)
#=> "170000"
store_open_now?(open_time, close_time)
#=> false
Now suppose the open time is the same, but the close time is later.
close_time = DateTime.parse("01:00")
#=> #<DateTime: 2018-06-07T01:00:00+00:00 ((2458277j,3600s,0n),
# +0s,2299161j)>
Then
close_time.strftime(TIME_FMT)
#=> "010000"
store_open_now?(open_time, close_time)
#=> true
Perhaps you want something like this:
current_time = Time.now
open_time = store.open_time
close_time = store.close_time
current_time -= current_time.beginning_of_day
open_time -= open_time.beginning_of_day
close_time -= close_time.beginning_of_day
if current_time.between?(open_time, close_time)
puts "IN BETWEEN"
end
or
current_time = Time.now
open_time = store.open_time
close_time = store.close_time
current_time = [current_time.hour, current_time.min, current_time.sec]
open_time = [open_time.hour, open_time.min, open_time.sec]
close_time = [close_time.hour, close_time.min, close_time.sec]
if open_time <=> current_time == -1 and current_time <=> close_time == -1
puts "IN BETWEEN"
end
You could CAST() your datetime to time by using,
cast(tbl_store.open_time as time) as SomeVariable
cast(tbl_store.close_time as time) as SomeOtherVariable
That would give you the time only instead of the full datetime value that you had to begin with, which is what you wanted.
You can then use the same logic with your curtime() between to the get value that you were looking for.
Example:
SELECT
CAST(tbl_store.open_time as TIME) as open_time,
CAST(tbl_store.close_time as TIME) as close_time,
CURTIME() BETWEEN (cast(tbl_store.open_time as TIME)) AND (cast(tbl_store.close_time as TIME)) as time_between
FROM
tbl_store
Working SQL Fiddle
You can change the schema build in the fiddle to test the datetime values you desire.
Note that if you ever have a logic that will include midnight time, you will have to make a CASE WHEN logic against that, else it will fail and return 0, whereas it should return 1.
You can take advantage of ranges and how numeric strings are compared
r = Range.new('09:00', '18:00')
r.include?('08:59') # => false
r.include?('09:01') # => true
r.include?('18:01') # => false
Then we could use
open_hours_range = Range.new(open_time.strftime('%R'), close_time.strftime('%R'))
shop_open? = open_hours_range.include?(Time.now.strftime('%R'))

For a given period, getting the smallest list of dates, using jokers

I use Elasticsearch where I have one index per day, and I want my Ruby on Rails application to query documents in a given period by specifying the smallest and most precise list of indices.
I can't find the code to get that list of indices. Let me explain it:
Consider a date formatted in YYYY-MM-DD.
You can use the joker * at the end of the date string. E.g. 2016-07-2* describes all the dates from 2016-07-20 to 2016-07-29.
Now, consider a period represented by a start date and an end date.
The code must return the smallest possible array of dates representing the period.
Let's use an example. For the following period:
start date: 2014-11-29
end date: 2016-10-13
The code must return an array containing the following strings:
2014-11-29
2014-11-30
2014-12-*
2015-*
2016-0*
2016-10-0*
2016-10-10
2016-10-11
2016-10-12
2016-10-13
It's better (but I'll still take a unoptimized code rather than nothing) if:
The code returns the most precise list of dates (i.e. doesn't return dates with a joker that describes a period starting before the start date, or ending after the end date)
The code returns the smallest list possible (i.e. ["2016-09-*"] is better than ["2016-09-0*", "2016-09-1*", "2016-09-2*", "2016-09-30"]
Any idea?
Okay, after more thinking and the help of a coworker, I may have a solution. Probably not totally optimized, but still...
def get_indices_from_period(start_date_str, end_date_str)
dates = {}
dates_strings = []
start_date = Date.parse(start_date_str)
end_date = Date.parse(end_date_str)
# Create a hash with, for each year and each month of the period: {:YYYY => {:MMMM => [DD1, DD2, DD3...]}}
(start_date..end_date).collect do |date|
year, month, day = date.year, date.month, date.day
dates[year] ||= {}
dates[year][month] ||= []
dates[year][month] << day
end
dates.each do |year, days_in_year|
start_of_year = Date.new(year, 1, 1)
max_number_of_days_in_year = (start_of_year.end_of_year - start_of_year).to_i + 1
number_of_days_in_year = days_in_year.collect{|month, days_in_month| days_in_month}.flatten.size
if max_number_of_days_in_year == number_of_days_in_year
# Return index formatted as YYYY-* if full year
dates_strings << "#{year}-*"
else
days_in_year.each do |month, days_in_month|
formatted_month = format('%02d', month)
if Time.days_in_month(month, year) == days_in_month.size
# Return index formatted as YYYY-MM-* if full month
dates_strings << "#{year}-#{formatted_month}-*"
else
decades_in_month = {}
days_in_month.each do |day|
decade = day / 10
decades_in_month[decade] ||= []
decades_in_month[decade] << day
end
decades_in_month.each do |decade, days_in_decade|
if (decade == 0 && days_in_decade.size == 9) ||
((decade == 1 || decade == 2) && days_in_decade.size == 10)
# Return index formatted as YYYY-MM-D* if full decade
dates_strings << "#{year}-#{formatted_month}-#{decade}*"
else
# Return index formatted as YYYY-MM-DD
dates_strings += days_in_decade.collect{|day| "#{year}-#{formatted_month}-#{format('%02d', day)}"}
end
end
end
end
end
end
return dates_strings
end
Test call:
get_indices_from_period('2014-11-29', '2016-10-13')
=> ["2014-11-29", "2014-11-30", "2014-12-*", "2015-*", "2016-01-*", "2016-02-*", "2016-03-*", "2016-04-*", "2016-05-*", "2016-06-*", "2016-07-*", "2016-08-*", "2016-09-*", "2016-10-0*", "2016-10-10", "2016-10-11", "2016-10-12", "2016-10-13"]

How to create multiple records for each day in given quarter?

I have Shift model.
--- !ruby/object:Shift
attributes:
id:
starts_at:
ends_at:
I want to add singelton method to create shifts for each day in given quarter.
class Shift
def self.open_quarter(number, year)
starts_at = "08:00"
ends_at = "08:00"
...
end
end
How to implement that in best way? I want that each shifts starts_at 8.00 am and finish 8.00 am on next day.
def self.open_quarter(number, year)
start_time = Time.new(year, number*3 - 2, 1, 8)
while start_time.month <= number*3 && start_time.year == year
Shift.create{starts_at: start_time, ends_at: start_time += 24.hours}
end
end
make sure to set the correct timezone when using Time.new. Default is current timezone (see docs). You can also use Time.utc.
def self.open_quarter(number, year)
starts_at = "08:00 am"
ends_at = "08:00 pm"
quarter_start = Date.new(year, (number * 3)).beginning_of_quarter
quarter_end = Date.new(year, (number * 3)).end_of_quarter
(quarter_end - quarter_start).to_i.times do |n|
start_shift = "#{(quarter_start + n).to_s} #{starts_at}".to_datetime
end_shift = "#{(quarter_start + n).to_s} #{ends_at}".to_datetime
Shift.create(starts_at: start_shift, ends_at: end_shift)
end
end

How can I find records from today, yesterday and so on with Ruby on Rails?

I want to find all records, say Posts, created today with Ruby on Rails, then all Posts created yesterday, and so on… how should I do?
Thank you,
Kevin
Try this:
#Today
Posts.find(:all, conditions: { :created_at => Date.today...Date.today + 1 })
#Yesterday
Posts.find(:all, conditions: { :created_at => Date.today - 1...Date.today })
Or this (preferable, in my opinion):
#Today
Posts.find(:all, conditions: ["DATE(created_at) = ?", Date.today] )
#Yesterday
Posts.find(:all, conditions: ["DATE(created_at) = ?", Date.today - 1] )
As a rule I store all dates on my server in UTC timezone and let the UI handle any timezone conversion.
To get the sort of query you are after to work correctly I had to massage the incoming date into a
UTC specific time range first.
require 'date'
class Post < ActiveRecord::Base
def self.created(a_date)
return Post.where(created_at: to_timerange(a_date))
end
private
def self.to_timerange(a_date)
raise ArgumentError, "expected 'a_date' to be a Date" unless a_date.is_a? Date
dts = Time.new(a_date.year, a_date.month, a_date.day, 0, 0, 0).utc
dte = dts + (24 * 60 * 60) - 1
return (dts...dte)
end
end
This then allows you to call
# today
posts = Post.created(Date.today)
# yesterday
posts = Post.created(Date.today - 1)
To query using a range I prefer the following:
yesterday = Date.yesterday
start = yesterday.beginning_of_day
#Fri, 27 Nov 2020 00:00:00 UTC +00:00
end = yesterday.end_of_day
# Fri, 27 Nov 2020 23:59:59 UTC +00:00 - the value here is one second before midnight
# meaning we should use an inclusive range using two dots:
range = start..end
Post.where(created_at: range)

Resources