Postgres to_timestamp function - sql

I have a python script that insert hourly readings into a postgres db. It is failing in 2010-03-28. How is postgres interpreting both 01:00:00 and 02:00:00 as 02:00:00. what am I doing wrong (ps: works for other dates prior to this)
select to_timestamp('28/03/2010 01:00:00','DD/MM/YYYY HH24:MI:SS');
to_timestamp
------------------------
2010-03-28 02:00:00+01
(1 row)
select to_timestamp('28/03/2010 02:00:00','DD/MM/YYYY HH24:MI:SS');
to_timestamp
------------------------
2010-03-28 02:00:00+01
(1 row)

what am I doing wrong?
Nothing wrong.
As commented by a_horse_with_no_name, what you are seeing is the effect of Daylight Saving Time. On 2 AM CET on March 28th, the clock skips one hour and goes directly to 3 AM. I assume that your timezone is UTC+01, so you are seeing this on 1 AM > 2 AM.
That particular day only has 23 hours: both dates do represent the same point in time, which is what you are seeing in the results generated by to_timestamp().
Extracting the corresponding epochs, you can also see that the results are identical:
select
to_timestamp('28/03/2010 01:00:00','DD/MM/YYYY HH24:MI:SS') "1 AM",
to_timestamp('28/03/2010 02:00:00','DD/MM/YYYY HH24:MI:SS') "2 AM",
extract(epoch from to_timestamp('28/03/2010 01:00:00','DD/MM/YYYY HH24:MI:SS')) "1 AM epoch",
extract(epoch from to_timestamp('28/03/2010 02:00:00','DD/MM/YYYY HH24:MI:SS')) "2 AM epoch"
1 AM | 2 AM | 1 AM epoch | 2 AM epoch
:--------------------- | :--------------------- | :--------- | :---------
2010-03-28 02:00:00+01 | 2010-03-28 02:00:00+01 | 1269738000 | 1269738000
This is just one of the many tricks involved when working with dates. There is not much you can do about that from Postgres' perspective: it is on your application to handle that.

I'm afraid that I disagree with the statement that you're doing nothing wrong.
If your database timezone is set to a timezone thats changes to daylight savings at 01:00 on 2010-03-28, e.g., 'Europe/London', then you shouldn't be attempting to create records for both 01:00:00 and 02:00:00 on 2010-03-28, as 01:00:00 does not exist (the time jumps from 00:59:59 to 02:00:00).
If you're not meant to be following daylight savings, then you need to change the DB server timezone to one that doesn't, e.g., 'GMT' or 'UTC'.

Related

Add 1 day to timezone aware timestamp with regards to daylight savings

I am trying to add 1 day to a timezone aware timestamp.
In this example I expected + interval '1' day to add 23 hours because DST starts on 2021-03-28 02:00:00 in Europe/Berlin, but it behaves the same as + interval '24' hour:
select timestamp '2021-03-28 00:00:00 Europe/Berlin' as before_dst,
timestamp '2021-03-28 00:00:00 Europe/Berlin' + interval '1' day as plus_1_day,
timestamp '2021-03-28 00:00:00 Europe/Berlin' + interval '24' hour as plus_24_hour
from dual;
BEFORE_DST
PLUS_1_DAY
PLUS_24_HOUR
2021-03-28 00:00:00.000000000 +01:00
2021-03-29 01:00:00.000000000 +02:00
2021-03-29 01:00:00.000000000 +02:00
Is there a way to add a day to a timestamp so that the beginnings or ends of daylight saving times are respected? For the example above that means a way to have oracle automatically recognize that the day 2021-03-28 only has 23 hours in Europe/Berlin.
I attempted to solve this by converting the timestamp to a local timestamp using at local before adding a day, but that does not work because at local converts the timestamp to the local time zone and not to something like a LocalDateTime in java, resulting in the exact same outcome: + interval '1' day always adding exactly 24 hours.
You could cast the timestamp with time zone value to a plain timestamp, which discards the time zone information; then add the 1-day interval, and declare the result to be in the required time zone:
from_tz(cast(timestamp '2021-03-28 00:00:00 Europe/Berlin' as timestamp) + interval '1' day, 'Europe/Berlin') as plus_1_day
or cast to a date (which could be implicit), add a day, and cast back:
from_tz(cast(cast(timestamp '2021-03-28 00:00:00 Europe/Berlin' as date) + 1 as timestamp), 'Europe/Berlin')
Adapting your example and showing the intermediate values:
select timestamp '2021-03-28 00:00:00 Europe/Berlin' as before_dst,
cast(timestamp '2021-03-28 00:00:00 Europe/Berlin' as timestamp) as as_ts,
cast(timestamp '2021-03-28 00:00:00 Europe/Berlin' as timestamp) + 1 as plus_1_day_ts,
from_tz(cast(timestamp '2021-03-28 00:00:00 Europe/Berlin' as timestamp) + interval '1' day, 'Europe/Berlin') as plus_1_day
from dual;
BEFORE_DST
AS_TS
PLUS_1_DAY_TS
PLUS_1_DAY
2021-03-28 00:00:00 +01:00
2021-03-28 00:00:00
2021-03-29 00:00:00
2021-03-29 00:00:00 +02:00
db<>fiddle
This assumes that you're always dealing with a fixed known time zone region; if you actually have a variable or column value with an unknown time zone then you can extract the region from that and use that as the from_tz() argument.
You should also be aware that this will work for your example at midnight, but won't work for all times. For example if your starting value was timestamp '2021-03-27 02:30:00 Europe/Berlin' then it would fail with "ORA-01878: specified field not found in datetime or interval", because it would end up try to declare 2021-03-28 02:30:00 to be in zone Europe/Berlin - and there is no such time, as that falls into the 'lost' hour of 02:00-03:00. Simply adding a day interval handles that - but then doesn't work as you expect in your example...
And this is because of this line in the documentation:
Oracle performs all timestamp arithmetic in UTC time.
2021-03-28 00:00:00 Europe/Berlin is 2021-03-27 23:00:00 UTC; adding a day to that is 2021-03-28 23:00:00 UTC; which is 2021-03-29 02:00:00 Europe/Berlin

Series Generating Functions and intervals?

In paragraph 9.24. "Set Returning Functions" of the PostgreSQL 9.5 manual is an example with "generate_series" with which I disagree.
SELECT * FROM generate_series('2008-03-01 00:00'::timestamp,
'2008-03-04 12:00', '10 hours');
generate_series
---------------------
2008-03-01 00:00:00
2008-03-01 10:00:00
2008-03-01 20:00:00
2008-03-02 06:00:00
2008-03-02 16:00:00
2008-03-03 02:00:00
2008-03-03 12:00:00
2008-03-03 22:00:00
2008-03-04 08:00:00
(9 rows)
I explain why, if we talk about intervals of 10 hours in length, then the line: "2008-03-04 08:00:00" should not be displayed, since there are only 4 hours left.
The last line should be 2008-03-03 22:00:00.
With the problem of outputting the intervals entirely, I recently encountered how to solve this problem?
No, you are interpreting it wrong.
generate_series() starts with the first value and continues adding the interval until the value would exceed the end value.
That is exactly how it is defined and exactly what your example is doing. The boundaries are the start and end.
A simpler example uses numbers. This query:
SELECT *
FROM generate_series(0, 22, 5);
returns a series of multiples of 5 up to 22. That is, the last value is 20.

Reset the date portion to the last of the month while preserving the time

Is there any way to reset the date portion to the last day of the month while preserving the time? For example:
2018-01-02 23:00:00 -> 2018-01-31 23:00:00
2018-04-04 10:00:00 -> 2018-04-30 10:00:00
The Oracle function last_day() does exactly this. Try:
select last_day(sysdate), sysdate
from dual
to see how it works.
Ironically, I usually find the preservation of the date to be counterintuitive, so my usual usage is more like:
select last_day(trunc(sysdate))
from dual

oracle sql Time to Resolve Calculation

I have a question and hopefully someone can help, because i have been stuck on this for a long time.
I have a column with remaining minutes for a task to expire and i want to calculate when this task will expire within the business days timeframe starting from the current sysdate day lets say weekdays from 09:00 to 17:00.
| Task No | Minutes Remaining | Expiration date |
| Task1 | 1800 | 27-10-16 9:45 AM |
| Task2 | 3400 | 28-10-16 9:45 AM |
| Task3 | 400 | 29-10-16 9:45 AM |
| Task4 | 180 | 30-10-16 9:45 AM |
| Task5 | 8400 | 31-10-16 9:45 AM |
| Task6 | 5000 | 1-11-16 9:45 AM |
OK, this was a fun problem. To summarize: You are given a date (which in Oracle always includes the time-of-day) from which you start measurement, and an initial duration in minutes. You need to find the expiration date (meaning date and time-of-day as always), which is calculated by adding the duration in minutes to the "clock-starting" date, but the clock should only run during business hours - 9 to 17, Monday to Friday only (not on weekends).
I assume if the "minutes remaining" is 0, then the expiration should be the same as the "clock-starting" date if it falls within work hours, or 9 am on the next work day otherwise.
To understand the solution, let's break it down in two parts. First let's consider a very special case: the "clock starts" on a Monday at 9 am. Then break down minutes remaining into an integer multiple of 2400 (5*8*60 = 2400 minutes in a full work week), plus an integer multiple of 480 from what's left (480 minutes to a work day), plus whatever is left, if anything. Then: the expiration date is the "clock-starting" date, plus however many weeks, plus however many whole days (between 0 and 4), plus the remaining minutes. One exceptional case here: if the "minutes remaining" is an exact multiple of 480 minutes, then expiration is at 5 pm on a certain work day, and not 9 am on the next work day. This requires special handling in the formula. All this is done in the outer query (at the bottom of the solution below).
Then we need to reduce the general case to this special case. This is done in the subquery prep in the solution. I simply increase the "minutes remaining" by the work minutes elapsed from 9 am on Monday at the beginning of the week. This is a relatively simple computation. Note that if the "clock starting" date is after 5 pm on a Friday (or any time on Saturday or Sunday), I must add exactly 2400 minutes, a full work week.
In the solution, I show a variety of "clock starting" dates, dt, and minutes remaining, rm. I tested a variety of situations, and I think the solution is correct, but you may want to test on more data (other situations I didn't include in the tests).
with
inputs ( task, min_rem, dt ) as (
select 'Task1', 1800, to_date('27-10-16 9:45 AM', 'dd-mm-yy hh:mi AM') from dual union all
select 'Task2', 3400, to_date('28-10-16 9:45 AM', 'dd-mm-yy hh:mi AM') from dual union all
select 'Task3', 400, to_date('29-10-16 3:45 AM', 'dd-mm-yy hh:mi AM') from dual union all
select 'Task4', 180, to_date('30-10-16 9:45 AM', 'dd-mm-yy hh:mi AM') from dual union all
select 'Task5', 8400, to_date('31-10-16 9:45 PM', 'dd-mm-yy hh:mi AM') from dual union all
select 'Task6', 5000, to_date('01-11-16 5:00 PM', 'dd-mm-yy hh:mi AM') from dual union all
select 'Task7', 0, to_date('01-12-16 5:00 PM', 'dd-mm-yy hh:mi PM') from dual
),
prep ( task, min_rem, dt, adj_min, adj_dt ) as (
select task, min_rem, dt,
min_rem + case when dt > trunc(dt, 'iw') + 5 + 17/24 then 2400
else (trunc(dt) - trunc(dt, 'iw')) * 480 +
least(480, greatest(0, 1440 * (dt - trunc(dt) - 9/24)))
end,
trunc(dt, 'iw') + 9/24
from inputs
)
select task, min_rem, dt,
adj_dt + 7 * trunc(adj_min / 2400)
+ case when adj_min/480 = trunc(adj_min/480)
then mod(adj_min, 2400) / 480 - 1 + 8/24
else trunc(mod(adj_min, 2400) / 480) + mod(adj_min, 480) / 1440
end as expiration
from prep
order by task
;
Output:
TASK MIN_REM DT EXPIRATION
----- ---------- ----------------- -----------------
Task1 1800 27-10-16 09:45 AM 01-11-16 03:45 PM
Task2 3400 28-10-16 09:45 AM 08-11-16 10:25 AM
Task3 400 29-10-16 03:45 AM 31-10-16 03:40 PM
Task4 180 30-10-16 09:45 AM 31-10-16 12:00 PM
Task5 8400 31-10-16 09:45 PM 24-11-16 01:00 PM
Task6 5000 01-11-16 05:00 PM 16-11-16 12:20 PM
Task7 0 01-12-16 05:00 PM 01-12-16 05:00 PM
7 rows selected

Add nchar field to DateTime in Informix

I have a datetime field that stores times in UTC format. There's another nchar field that stores the time zone difference based on a location. I'm trying to combine the two for a report so that the time displayed matches the appropriate time zone.
time_stamp | time_zone
---------------------------------
2015-11-24 21:00:00 | -0500
2015-11-23 15:00:00 | -0600
Expected output:
2015-11-24 16:00:00
2015-11-23 09:00:00
I was able to get this to work by using:
extend(time_stamp, year to minute) + (CAST(LEFT(time_zone,3) as int)) units hour
While this technically works for the current situation, I really don't like using the CAST and LEFT functions on the time_zone field since it breaks if the value is not negative. Seems like there's a much better solution, possible something with TO_CHAR. In an informix database, what is the proper way to combine the dateime and nchar fields so that the output time is correct? Ideally I would like to output in non 24 hr format (4:00 PM, etc...) but at this point I'm mainly focused on getting the correct time.
Ideally, your time zone column would be an INTERVAL HOUR TO MINUTE type; you'd then simply add the two columns to get the desired result. Since it is a character type, substringing in some form will be necessary. Using LEFT is one option; SUBSTRING is another; using the Informix subscripting notation is another. The CAST isn't crucial; Informix is pretty good about coercing things.
Unless you actually want only hours and minutes in the result (which is a legitimate choice), your EXTEND operation is unnecessary and undesirable; it means your result won't include the seconds value from your data.
Note that some time zones include minutes values. Newfoundland is on UTC-04:30; India is on UTC+05:30; Nepal is on UTC+05:45. (See World Time Zone for more information.) Getting the minutes accurate is harder because the sign has to be carried through.
As to formatting in AM/PM notation, apart from the question 'why', the answer is to use the TO_CHAR() function and a ghastligram expressing the time format that you want.
TO_CHAR()
GL_DATETIME
GL_DATE
Demonstration:
create table zone_char(time_stamp datetime year to second, time_zone nchar(5));
insert into zone_char values('2015-11-24 21:00:00', '-0500');
insert into zone_char values('2015-11-23 15:00:00', '-0600');
insert into zone_char values('2015-11-22 17:19:21', '+0515');
insert into zone_char values('2015-11-21 02:56:31', '-0430');
Various ways to select the data:
select extend(time_stamp, year to minute) + LEFT(time_zone,3) units hour,
time_stamp + LEFT(time_zone,3) units hour,
time_stamp + time_zone[1,3] units hour,
time_stamp + time_zone[1,3] units hour + (time_zone[1] || time_zone[4,5]) units minute,
TO_CHAR(time_stamp + time_zone[1,3] units hour + (time_zone[1] || time_zone[4,5]) units minute,
'%A %e %B %Y %I.%M.%S %p')
from zone_char;
Sample output:
2015-11-24 16:00 2015-11-24 16:00:00 2015-11-24 16:00:00 2015-11-24 16:00:00 Tuesday 24 November 2015 04.00.00 PM
2015-11-23 09:00 2015-11-23 09:00:00 2015-11-23 09:00:00 2015-11-23 09:00:00 Monday 23 November 2015 09.00.00 AM
2015-11-22 22:19 2015-11-22 22:19:21 2015-11-22 22:19:21 2015-11-22 22:34:21 Sunday 22 November 2015 10.34.21 PM
2015-11-20 22:56 2015-11-20 22:56:31 2015-11-20 22:56:31 2015-11-20 22:26:31 Friday 20 November 2015 10.26.31 PM
And note how much easier it is when the time zone is represented as an INTERVAL HOUR TO MINUTE:
alter table zone_char add hhmm interval hour to minute;
update zone_char set hhmm = time_zone[1,3] || ':' || time_zone[4,5];
SELECT:
select time_stamp, hhmm, extend(time_stamp + hhmm, year to minute),
time_stamp + hhmm,
TO_CHAR(time_stamp + hhmm, '%A %e %B %Y %I.%M.%S %p')
from zone_char;
Result:
2015-11-24 21:00:00 -5:00 2015-11-24 16:00 2015-11-24 16:00:00 Tuesday 24 November 2015 04.00.00 PM
2015-11-23 15:00:00 -6:00 2015-11-23 09:00 2015-11-23 09:00:00 Monday 23 November 2015 09.00.00 AM
2015-11-22 17:19:21 5:15 2015-11-22 22:34 2015-11-22 22:34:21 Sunday 22 November 2015 10.34.21 PM
2015-11-21 02:56:31 -4:30 2015-11-20 22:26 2015-11-20 22:26:31 Friday 20 November 2015 10.26.31 PM