Rails DateTime Conversion and Time Comparison - ruby-on-rails

I am having a bit of trouble with comparing times in Rails. I want to check to see if an event lies within a window, if it does, then find which, the event or window starts last and which ends first. My code is as follows.
startingTime = 0
endingTime = 0
time = 0
eventTimeStart = Time.parse(event.start.to_s) #Need to convert DateTime to just Time
windowTimeStart = Time.parse(application.reportStart.to_s)
eventTimeEnd = Time.parse(event.end.to_s) #Need to convert DateTime to just Time
windowTimeEnd = Time.parse(application.reportEnd.to_s)
days = 0
if((windowTimeStart > eventTimeStart) || !(eventTimeStart < windowTimeEnd))
startingTime = windowTimeStart
if((eventTimeStart > windowTimeEnd))
days -= 1
end
else
startingTime = eventTimeStart
end
if((windowTimeEnd > eventTimeEnd) && (eventTimeEnd > windowTimeStart))
endingTime = eventTimeEnd
else
if((eventTimeEnd < windowTimeStart))
days -= 1
end
endingTime = windowTimeEnd
end
I have handwritten out each case, however at runtime it seems to run different from expected. It seems as if I always get into the windowed times. Does Rails use a different approach to Times than what I'm thinking? Can you even compare times in this manner?

If you are trying to see whether intervals overlap or not, this simple check will do:
overlaps = interval_1_start < interval_2_end && interval_1_end > interval_2_start
I don't understand the rest of the question, but I just hope that you don't have two big loops for event and application around the code you have pasted above.

Thanks for your suggestion Mladen.
In order to get this working, I passed all of the time objects into a method I made that converted them into minutes.
def self.getMin(time)
return((time.hour*60) + time.min)
end
I called that on all of my checks and now it seems to work.
if((getMin(windowTimeStart) > getMin(eventTimeStart)) || !(getMin(eventTimeStart) < getMin(windowTimeEnd)))
startingTime = windowTimeStart
if((getMin(eventTimeStart) > getMin(windowTimeEnd)))
days -= 1
end
else
startingTime = eventTimeStart
end
I'm sure there is a better way to check, however it does work now.

Related

How do I convert a date into a time when parsing an .xls doc using Rails?

I'm using Rails 5. I want to parse an .xls (not to be confused with .xlsx doc) using the code below
book = Roo::Spreadsheet.open(file_location)
sheet = book.sheet(0)
text = sheet.to_csv
csv = CSV.parse(text)
arr_of_arrs = csv
text_content = ""
arr_of_arrs.each do |arr|
arr.map!{|v| v && v.to_f < 1 && v.to_f > 0 ? TimeFormattingHelper.time_as_str(v.to_f * 24 * 3600 * 1000) : v}
text_content = "#{text_content}\n#{arr.join("\t")}"
end
Here is the method I reference above
def time_as_str(time_in_ms)
regex = /^(0*:?)*0*/
Time.at(time_in_ms.to_f/1000).utc.strftime("%H:%M:%S.%1N").sub!(regex, '')
end
One area I'm having trouble is that a cell that appears in my .xls doc as
24:08:00
is processed as
1904-01-02T00:08:00+00:00
with the code above. How do I parse the value I see on the screen? That is, how do I convert the date value into a time value?
As an example from another Excel doc, the cell that appears as
24:02:00
is getting parsed by my code above as
1899-12-31T00:02:00+00:00
It seems your .xls is in the 1904 date system, and Roo is not able to distinguish between what is a Duration and what is a DateTime, so you'll need to subtract the base date 1904-01-01 to the cell value. Weirdly enough, in case of the 1900 date system, you need to subtract the base date 1899-12-30, due to a bug in Lotus 1-2-3 that Microsoft replicated in Excel for compatibility.
Here is a method that converts the DateTime read from the spreadsheet into the duration according to the base date:
def duration_as_str(datetime, base_date)
total_seconds = DateTime.parse(datetime).to_i - base_date.to_i
hours = total_seconds / (60 * 60)
minutes = (total_seconds / 60) % 60
seconds = total_seconds % 60
"%d:%02d:%02d" % [hours, minutes, seconds]
end
Let's test it:
irb(main):019:0> duration_as_str("1904-01-02T00:08:00+00:00", DateTime.new(1904, 1, 1))
=> "24:08:00"
irb(main):020:0> duration_as_str("1899-12-31T00:02:00+00:00", DateTime.new(1899, 12, 30))
=> "24:02:00"
You can use book.workbook.date_base.year to determine the spreadsheet's date system, and then just add another map inside your each loop:
book = Roo::Spreadsheet.open(file_location)
sheet = book.sheet(0)
text = sheet.to_csv
csv = CSV.parse(text)
base_date = book.workbook.date_base.year == 1904 ? DateTime.new(1904, 1, 1) : DateTime.new(1899, 12, 30)
arr_of_arrs = csv
text_content = ""
arr_of_arrs.each do |arr|
arr.map!{|v| v && v.to_f < 1 && v.to_f > 0 ? TimeFormattingHelper.time_as_str(v.to_f * 24 * 3600 * 1000) : v}
arr.map!{|v| v =~ /^(1904|1899)-/ ? duration_as_str(v, base_date) : v}
text_content = "#{text_content}\n#{arr.join("\t")}"
end
You could use something like the below and write a custom parser for that string.
duration = 0
"24:08:01".split(":").each_with_index do |value, i|
if i == 0
duration += value.to_i.hours
elsif i == 1
duration += value.to_i.minutes
else
duration += value.to_i.seconds
end
end
duration.value => 86881 (duration in seconds)
This parser will assume a format of hours:minutes:seconds and return an instance of ActiveSupport::Duration. Then, duration.value will give you the number of seconds.
You need to read the internal value of cell instead of formatted value.
Formatted value gets written to csv when you use to_csv
To read internal value, you would have to use either sheet objects excelx_value method or row object's cell_value method.
These methods return value in float (days). Here is an example using cell_value by iterating over rows, assuming no header and first column with value to be converted.
Using Roo 2.7.1 (similar methods exist in older version)
book = Roo::Spreadsheet.open(file_location)
sheet = book.sheet(0)
formatted_times = []
time_column_index = 0
sheet.each_row_streaming do |row|
time_in_days = row[time_column_index].cell_value
formatted_times << time_as_str(time_in_days.to_f * 24 * 3600)
end
def time_as_str(t)
minutes, seconds = t.divmod(60)
hours, minutes = minutes.divmod(60)
"%02d:%02d:%02d" % [hours, minutes, seconds]
end
# eg: time_in_days = 1.0169444444444444
# formatted_time = "24:24:24"
First, I will try rephrasing what you want to accomplish.
You want to “parse the value you see on the screen”, but I am not sure whether that is 24:08:00 or 1904-01-02T00:08:00+00:00. I assume it is the first.
You want to convert the date value into a time value. I am not sure you actually want the output var to be a Time, a Date, a DateTime, or simply a String. I assume it is ok for you to have it simply as a String, but this is a minor issue.
With this, I assume that what you in general see as HH:MM:SS in Excel, you want to get as “HH:MM:SS” in Rails, regardless of HH being > 23. As an example, 24:08:00 in Excel would turn into “24:08:00” in Rails.
The two seemingly discordant cases you report most likely stem from the two .xls files having different date systems.
To get the desired result you have two options:
Use to_csv, whose result is affected by the date system of the Excel file. In this case, you have to subtract the base_date, as done by Helder Pereira.
Directly get the numeric value from Excel, which is not affected by the date system. In this case, code is simpler, since you only need one conversion (function days2str below).
Code is (modulo minor adjustments)
def days2str(days)
days_int = int(days)
hours = ( days - days_int ) * 24
hours_int = int(hours)
seconds = ( hours - hours_int ) * 3600
seconds_int = int(seconds)
hours_int = hours_int + 24 * days_int
format("%d:%02d:%02d", hours_int, minutes_int, seconds_int)
end
def is_date(v)
# Define the checking function
end
require 'spreadsheet'
Spreadsheet.open('MyTestSheet.xls') do |book|
book.worksheet('Sheet1').each do |row|
break if row[0].nil?
puts row.join(',')
row.map!{|v| is_date(v) ? days2str(v) : v }
text_content = "#{text_content}\n#{arr.join("\t")}"
end
end

Find Gaps in range of dates

Hi Folks I used google calendars to pull free/busy schedule for users. I want to able to take those range of dates and then output all the gaps into a new array of some sort?
Any direction pointing would be greatly appreciated.
Step 1: Get all your dates (as integers) into a sorted (by time) two-dimensional array, noting whether each date is the beginning or end of the timeframe. For example:
array = [[1484715564, 'start'], [1484715565, 'start'], [1484715569, 'end'], [1484715587, 'end'], ...]
Then, all you need to do is keep track of whether you've gone through as many ends as starts, and if you have, make note of it!
num_starts = 0
gap_start = 0
gaps = []
array.each do |date, which_end|
if which_end == 'start'
num_starts += 1
if num_starts == 1
gaps << [gap_start, date]
end
else
num_starts -= 1
if num_starts == 0
gap_start = date
end
end
end

Removing nested [quote] tags in ruby

I'm writing a forum application in Rails and I'm stuck on limiting nested quotes.
I'm try to use regex and recursion, going down to each matching tag, counting the levels and if the current level is > max, deleting everything inside of it. Problem is that my regex is only matching the first [ quote ] with the first seen [ /quote ], and not the last as intended.
The regex is just a slight tweak of what was given in the docs of the custom bbcode library I'm using (I know very little about regex, I've tried to learn as much as I can in the past couple days but I'm still stuck). I changed it so it'd include [quote], [quote=name] and [quote=name;222] . Could someone examine my code and let me know what the problem could be? I'd appreciate it lots.
def remove_nested_quotes(post_string, max_quotes, count)
result = post_string.match(/\[quote(:.*)?(?:)?(.*?)(?:)?\](.*?)\[\/quote\1?\]/mi)
if result.nil?
return false
elsif (count = count+1) > max_quotes
full_str = result[0]
offset_beg = result.begin(3)
offset_end = result.end(3)
excess_quotes = full_str[offset_beg ..offset_end ]
new_string = full_str.slice(excess_quotes )
return new_string
else
offset_beg = result.begin(3)
offset_end = result.end(3)
full_str = result[0]
inner_string = full_str[offset_beg..offset_end]
return remove_nested_quotes(inner_string , max, count)
end
end
I mean something like
counter = 0
max = 5
loop do
matched = false
string.match /endquote|quote/ do |match|
matched = true
if endquote matched
counter -= 1
else # quote matched
counter += 1
end
if counter > max
# Do something, break or return
else
string = match.post_match
end
end
break unless matched
end

Issue with Time and ranges

I'm currently working on an Appointment system and building it with Ruby on Rails. I have an Appointment model and appointments controller where on the index, I want to show a list of appointments for that day, separated by 30 minute chunks.
I have a basic working version and I've got a ruby method that adds a class on the table row which shows the if the current 30 minute chunk is the current time or not.
The issue is, it sets the row class as "current_time" when the time is anywhere between the start and end of the hour which isn't what I want.
def date_class(time)
now = DateTime.now.utc
if (now.beginning_of_hour..(now.end_of_hour - 0.5.hours)).cover?(time)
"current_time"
elsif ((now.beginning_of_hour + 0.5.hours)..now.end_of_hour).cover?(time)
"current_time"
elsif (now.beginning_of_day..now.end_of_hour).cover?(time)
"past"
else
"future"
end
end
Any ideas?
The screenshot below and shows that the code works fine and shows true or false correctly.
http://s.deanpcmad.com/2014/uifGf.png
Although it has only been tested with instances of class Time for now, the time_frame gem could be an alternative solution for this kind of problem:
require 'time_frame'
def date_class(time)
now = Time.now.utc
frame = TimeFrame.new(min: now.beginning_of_hour, duration: 29.minutes + 59.seconds)
frame = frame.shift_by(30.minutes) if now.min >= 30
return 'past' if frame.deviation_of(time) < 0.minutes
return 'current_time' if frame.cover?(time)
'future'
end
# Demo: Building 30.minutes interval blocks and print out the date class used by each block:
frame = TimeFrame.new(
min: (Time.now.utc - 2.hours).beginning_of_hour,
max: (Time.now.utc + 2.hours).beginning_of_hour
)
frame.split_by_interval(30.minutes).each do |interval|
puts "#{interval.min} -> #{date_class(interval.min)}"
end
Wouldn't this work for you?
def date_class(time)
now = DateTime.now.utc
return "past" if time < now.beginning_of_hour
return "current_time" if now.hour == time.hour && now.min < 30 && time.min < 30
return "current_time" if now.hour == time.hour && now.min >= 30 && time.min >= 30
return "future"
end
I am sure there is a better way, but this would also work I think

Can using Chronic impair your sense of time?

Haha..
I'm using Chronic to parse the time users add in the Calendar. Where the code works and implements the right time, the end result is that, IF a user adds a time, then it has no date, and because it has no date, it will not show in results. Any ideas?
def set_dates
unless self.natural_date.blank? || Chronic.parse(self.natural_date).blank?
# check if we are dealing with a date or a date + time
if time_provided?(self.natural_date)
self.date = nil
self.time = Chronic.parse(self.natural_date)
else
self.date = Chronic.parse(self.natural_date).to_date
self.time = nil
end
end
unless self.natural_end_date.blank? || Chronic.parse(self.natural_end_date).blank?
# check if we are dealing with a date or a date + time
if time_provided?(self.natural_end_date)
self.end_date = nil
self.end_time = Chronic.parse(self.natural_end_date)
else
self.end_date = Chronic.parse(self.natural_end_date).to_date
self.end_time = nil
end
end
end
Edit:
Here is the time_provided? method:
def time_provided?(natural_date_string)
date_span = Chronic.parse(natural_date_string, :guess => false)
(date_span.last - date_span.first).to_i == 1
end
First, I'm not really sure what are you asking about, because it looks like the code intentionally does what you describe... When there's time provided, the date fields are assigned nil. And I don't think that is Chronic is to blame because that's how your code works.
Not knowing your design (why there are separate date & time fields), the types of fields etc., I would suggest starting with a little kludge like this:
if time_provided?(self.natural_date)
self.time = Chronic.parse(self.natural_date)
self.date = self.time.to_date
or:
self.end_date = Chronic.parse(self.natural_date).to_date
if time_provided?(self.natural_date)
self.time = Chronic.parse(self.natural_date)
end
Or maybe the problem is outside the code you provided: in the part that is responsible for the "because it has no date, it will not show in results" behavior? Maybe you should make the conditions more flexible?

Resources