how to calculate time difference in rails - ruby-on-rails-3

I have a field that is a timestamp. I want to calculate the time difference between that timestamp and the current time and show the time as something humanly readable like
2 days remaining #don't show hours when > 1 day is remaining
once less than 1 day is remaining I'll have a javascript countdown ticker.

I've built the dotiw library to do exactly this: http://github.com/radar/dotiw.
This is based off the distance_of_time_in_words method in Rails which is not quite accurate enough, and so I've made it more accurate with dotiw.

Try this:
if end_date < Time.now # ended already
return 'Ended'
elsif end_date > (Time.now + 1.day) # more than 1 day away
diff_in_days = ((end_date - Time.now).to_i / 1.day)
days_string = diff_in_days.to_s
days_string += (diff_in_days > 1) ? ' Days' : ' Day'
return days_string
else # ending today
diff_in_HMS = Time.at(end_date - Time.now).gmtime.strftime('%R:%S')
return diff_in_HMS
end
It prints "X Days" if end_date is > 1 day away, HH:MM:SS if ending today, and "Ended" if end_date was in the past.

Related

Postgres: Calculate start date of period so that interval remains the same

Using PostgresQL 9.6, for a certain period of time, I have three input values:
"endDate": any date, being the end date
"months": a number of months between 0 and ca. 30
"days": a number of days between 0 and 29
The task is: Find the start date of that period. Requirement is: The result of age("end","start") (*) must always be the same as the input interval (month + days). In other words: Make the period storable with just start and end without explicitly saving the given interval, but making sure that the interval remains exactly the same.
My first simple attempt was
SELECT "endDate" - ("months" || ' mons ' || "days" ' days')::interval
However, that doesn't work for input
"endDate": 2017-06-29
"months": 4
"days": 19
The start date calculated by this approach will be 2017-02-09. And age('2017-06-29','2017-02-10') will return 4 mons 20 days which is one day too much.
The probable reason is that the minus operator most likely will first subtract the month, then if it lands on an "impossible date" like 2017-02-29, go to the previous "possible" date (here: 2017-02-28) and then subtracts the days, landing on 2017-02-09 - which is wrong here.
So, I came up with this idea:
WITH prep AS
( SELECT ("months"||' mons')::interval AS moninterval,
("days" || ' days')::interval AS dayinterval )
SELECT
CASE WHEN date_part('day',"endDate") > "days"
THEN (("endDate" - dayinterval) - moninterval)::date
ELSE ("endDate" - ( moninterval + dayinterval) )::date
END AS simstart
FROM prep
Basically, the idea is: If the number of days in the end date is bigger than the number of days in the interval, then first subtract the days, then the months. Otherwise, do as before.
That works for many cases. However, I still found an edge case where it doesn't:
"endDate": 2018-03-02
"months": 0
"days": 28
Regardless of the method used: The start date will always be calculated as 2018-02-02. And if you do SELECT age_forward('2018-03-02','2018-02-02') you will always end up with 1 mon - which is technically correct. However, it's wrong here, since the original input was 28 days.
(*) To be more precise: age_forward. See my answer to my own question here https://stackoverflow.com/a/51173709/2710714 However, I think it doesn't matter with the edge case problems described above.

can i count the next day as previous day?

I am trying to count 6am-12:30am(next day) as one date.
For some reason I cannot pull this data from the next day in for the previous day.
Is this possible?
(CASE WHEN TO_CHAR(ITD.TRAN_DATE,'HH24MI')>='0600' THEN TO_CHAR(ITD.TRAN_DATE,'HHAM')
WHEN TO_CHAR(TRUNC(ITD.TRAN_DATE+1),'HH24MI')<='0030' THEN TO_CHAR(ITD.TRAN_DATE,'HHAM')
END)
I am using this case statement to have everything until 12:30am the next day to count as the previous day. It will not work when I set a date parameter.
The answer to your question is yes.
The pseudo-code is:
IF (TRAN_TIME <= 00:30 AND TRAN_DATE = TODAY + 1) OR
(TRAN_TIME >= 06:00 AND TRAN_DATE = TODAY)
THEN ...
What you are currently doing, is taking the existing date, and adding one to it, before comparing the times, and that won't return what you are expecting.
Use an INTERVAL data type to add an offset to a date:
SELECT *
FROM itd
WHERE ITD.TRAN_DATE
BETWEEN TRUNC( :date_to_match ) + INTERVAL '00 06:00' DAY TO MINUTE
AND TRUNC( :date_to_match ) + INTERVAL '01 12:30' DAY TO MINUTE;

How to pull previous month data for in January

I have an embedded query that I use to pull previous month data. Everything has been working fine until this month (January). My code looks like this:
(MONTH(CURRENT DATE)-1) = MONTH(TSTAMP)
I have it setup this way because I have a timestamp in my data that I base the query off of. This usually works like a charm, but it's not working this month and I think it's because of the new year. How does this function work when dealing with a different year? Is there a way to write it into the query so I don't have to worry about a change in year?
You can do this by using the year, like this:
YEAR(CURRENT DATE) * 12 + MONTH(CURRENT DATE) - 1 = YEAR(TSTAMP) * 12 + MONTH(TSTAMP)
This, in essence, converts the dates into months since time 0 -- so the -1 makes sense.
The proper way to do this is with a range query (one with an exclusive upper-bound, <, too), so that the db is free to us an index if one is available.
The first of the month can be retrieved pretty easily via:
CURRENT_DATE - (DAY(CURRENT_DATE) - 1) DAYS
(Subtract the difference in days between the current date and the start of the month)
This gives a wonderful upper-bound condition for the query:
WHERE tStamp < CURRENT_DATE - (DAY(CURRENT_DATE) - 1) DAYS
(Get everything before the start of the current month).
However, since we're really only interested in the previous month, we also need to limit the lower bound. Well that's everything since, or on, the start of that month... and since we can already get the start of the current month:
WHERE tStamp >= CURRENT_DATE - (DAY(CURRENT_DATE) - 1) DAYS + 1 MONTH
AND tStamp < CURRENT_DATE - (DAY(CURRENT_DATE) - 1) DAYS
There's a related way to do this, supposing you have a calendar table with appropriate indices. If you have a minimal table with these columns:
calendarDate - DATE
year - INTEGER
month - INTEGER
dayOfMonth - INTEGER
... you can use this table to get the relevant values instead:
WHERE tStamp >= (SELECT calendarDate
FROM calendarTable
WHERE year = YEAR(CURRENT_DATE - 1 MONTH)
AND month = MONTH(CURRENT_DATE - 1 MONTH)
AND dayOfMonth = 1)
AND tStamp < (SELECT calendarDate
FROM calendarTable
WHERE year = YEAR(CURRENT_DATE)
AND month = MONTH(CURRENT_DATE)
AND dayOfMonth = 1)
(there's a couple of different forms of this, but this one looks pretty simple)

Find and count consecutive records in ruby on rails

I have an app that tracks employee times. Employees are required to have at least 2 days off every 12 days... An employee should enter a record with boolean true for day_off...but, in case they don't I also want to find breaks in days that could also days off. I am trying to simply count records whos date decrements by one day, starting from the boolean day_off Or break in consecutive dates...and ending on a given date.
This is the helper I am working on
def consecutive_days_on(user, dutylog)
# dutylog will be supplied via a loop
last_day_off = user.dutylogs.where("entry_date < ?", dutylog.entry_date).where(day_off: true).last
start_date = dutylog.entry_date
if last_day_off.present?
end_date = last_day_off.entry_date
# if the user logged their days off
else
end_date = user.dutylogs.where("entry_date < ?", dutylog.entry_date).last.entry_date
# as of now this just finds the last record...it needs to iterate and increment date to find a break in days on
# to find break in consecutive dates...if user did not log days off
end
user.dutylogs.where("entry_date >= ? AND entry_date <= ?", start_date, end_date).count
# count the records between the two dates...to find consecutive days on
end
I refactored my helper...and it works. I decided to rely on the user entering a day as off in their log...and not assuming that a break in entered days could be considered a day off.
def consecutive_days_on(user, dutylog)
last_day_off = user.dutylogs.where(day_off: true).where("entry_date < ?", dutylog.entry_date).select(:entry_date).last
start_date = dutylog.entry_date
if last_day_off.present?
end_date = last_day_off.entry_date
else
end_date = dutylog.entry_date
end
a = user.dutylogs.where("entry_date >= ? AND entry_date <= ?", end_date, start_date).count
a-1
end

How to determine largest resolution of an INTERVAL?

How can I determine the largest resolution of an INTERVAL value? For example:
INTERVAL '100 days and 3 seconds' => day
TIME '20:05' - TIME '12:01:01' => hour
AGE(NOW(), NOW() - INTERVAL '1 MONTH') => month
The question isn't 100% clear so the answer may or may not be exactly what you're looking for, but...
There is a justify_interval() function, which you might want to look into.
test=# select justify_interval(INTERVAL '100 days 3 seconds');
justify_interval
-------------------------
3 mons 10 days 00:00:03
(1 row)
test=# select justify_interval(TIME '20:05' - TIME '12:01:01');
justify_interval
------------------
08:03:59
(1 row)
test=# select justify_interval(AGE(NOW(), NOW() - INTERVAL '1 MONTH'));
justify_interval
------------------
1 mon
(1 row)
For there extract the year, then month, then day, etc. until you come up with a non-zero answer:
test=# select extract('mon' from interval '3 mons 10 days 00:00:03');
date_part
-----------
3
Re your other question in comments:
create function max_res(interval) returns interval as $$
select case
when extract('year' from justify_interval($1)) > 0 or
extract('mon' from justify_interval($1)) > 0 or
extract('day' from justify_interval($1)) > 0
then '1 day'
when extract('hour' from justify_interval($1)) > 0
then '1 hour'
when ...
end;
$$ language sql immutable strict;
INTERVAL is 12 bytes and is a struct containing months, days and microseconds and has a range of +/- 178000000 years. It always has a fixed max size of 178000000 years due to the way that it stores this information.
Be careful with your understanding of "a month" because the Julian month is not a constant in the same way that an hour or a minute is (e.g. how many days are in the month of February? Or how many days are there in a year? It's not always 30 or 365 in reality and PostgreSQL updates things correctly. per an interesting conversation on IRC, Adding 1 month::INTERVAL to January 30th will result in whatever the last day of February because it increments the tm_mon member of struct tm (and in this case, rolls back to the previous valid date).
Ah ha! I get the question now (or at least I think so). You're looking to determine the largest "non-zero integer unit" for a given INTERVAL.
PostgreSQL doesn't have a built-in function that returns that information. I think you're going to have to chain a conditional and return type. Some example PL code:
t := EXTRACT(EPOCH FROM my_time_input);
IF t >= 31104000 THEN
RETURN 'year';
ELSIF t >= 2592000 THEN
RETURN 'month';
ELSIF t >= 604800 THEN
RETURN 'week';
ELSIF t >= 86400 THEN
RETURN 'day';
ELSIF t >= 3600 THEN
RETURN 'hour';
ELSIF t >= 60 THEN
RETURN 'minute'
ELSIF t > 1 THEN
RETURN 'seconds';
ELSIF t == 1 THEN
RETURN 'second';
ELSE
RETURN resolve_largest_sub_second_unit(my_time);
END IF;