Splitting time into hour intervals in oracle (CTE) - sql

So, my aim is to be able to count time spent on certain activities in hour ranges.
My data contains: start of the certain activity and end of that activity,
for example I know that someone had break from '2019-01-09 17:04:34' to '2019-01-09 19:55:03'.
My aim is to calculate that this person spent 55 minutes on break in interval '17-18', 60 minutes on '18-19' and 55 minutes on '19-20'.
My idea was to always split the source so for the row containing start and and of the activity I would receive as many rows as my time range split in the hour ranges (for this sample data I would receive 3: rows with '2019-01-09 17:04:34' to '2019-01-09 17:59:59', '2019-01-09 18:00:00' to '2019-01-09 18:59:59' and '2019-01-09 19:00:00' to '2019-01-09 19:55:03')
If I could obtain something like that I could manage to count all things I need to. I predict that to obtain this result I should use CTE (as we don't know in how many ranges we need to split time interval), but I have no experience in it.
Hopefully I managed to explain my problem clearly. I work on oracle sql developer.
I'd be very grateful for your help on at least some tips.

Since you mentioned recursion, this uses recursive subquery factoring:
-- CTE for sample data
with your_table (id, start_time, end_time) as (
select 1, timestamp '2019-01-09 17:04:34', timestamp '2019-01-09 19:55:03' from dual
union all
select 2, timestamp '2019-01-09 23:47:01', timestamp '2019-01-10 02:05:03' from dual
union all
select 3, timestamp '2019-01-09 18:01:01', timestamp '2019-01-09 18:02:07' from dual
union all
select 4, timestamp '2019-01-09 13:00:00', timestamp '2019-01-09 14:00:01' from dual
),
-- recursive CTE
rcte (id, hour_period, minutes, period_start_time, end_time, hour_num) as (
select id,
-- first period is the original start hour
extract(hour from start_time),
-- minutes in first period, which can end at the end of that hour, or at original
-- end time if earlier
case when extract(minute from end_time) = 0
and end_time >= cast(trunc(start_time, 'HH') as timestamp) + interval '1' hour
then 60
else extract(minute from
least(cast(trunc(start_time, 'HH') as timestamp) + interval '1' hour, end_time)
- start_time
)
end,
-- calculate next period start
cast(trunc(start_time, 'HH') as timestamp) + interval '1' hour,
-- original end time
end_time,
-- first hour period (for later ordering)
1
from your_table
union all
select id,
-- this period's hour value
extract(hour from period_start_time),
-- minutes in this period - either 60 if we haven't reach the end time yet;
-- or if we have then the number of minutes from the end time
case when end_time < period_start_time + interval '1' hour
then extract(minute from end_time)
else 60
end,
-- calculate next period start
period_start_time + interval '1' hour,
-- original end time
end_time,
-- increment hour period (for later ordering)
hour_num + 1
from rcte
where period_start_time < end_time
)
select id, hour_period, minutes
from rcte
order by id, hour_num;
ID HOUR_PERIOD MINUTES
---------- ----------- ----------
1 17 55
1 18 60
1 19 55
2 23 12
2 0 60
2 1 60
2 2 5
3 18 1
4 13 60
4 14 0
It find finds the amount of time spent in the first hour of the period in the anchor member, then recursively looks at subsequent hours until the end time is reached, increasing the passed-on period end time each time; and in the recursive member it checks whether to use a fixed 60 minutes (if it knows the end time hasn't been reached) or use the actual minutes from the end time.
My example periods include ones that span midnight, cover less than an hour, and that start in the first minute of an hour - and which end in the first minute of an hour, which (in my calculation anyway) ends up with a row for that hour anyway and the number of minutes as zero. You can easily filter that out if you don't want to see it.

It is not entirely clear from your post how you want to handle non-zero seconds components (what combination of rounding and/or truncation). In any case, that can be coded easily, once a complete set of non-contradictory rules is agreed upon.
Other than that, your question consists of two parts: identify the proper hours for each id (each activity or event), and the duration of the part of that event during that hour. In the query below, using the CONNECT BY hierarchical technique, I generate the hours and the duration as an interval day to second. As I said, that can be converted to minutes (between 0 and 60) once you clarify the rounding rules.
with
your_table (id, start_time, end_time) as (
select 1, timestamp '2019-01-09 17:04:34', timestamp '2019-01-09 19:55:03'
from dual union all
select 2, timestamp '2019-01-09 23:47:01', timestamp '2019-01-10 02:05:03'
from dual union all
select 3, timestamp '2019-01-09 18:01:01', timestamp '2019-01-09 18:02:07'
from dual union all
select 4, timestamp '2019-01-09 13:00:00', timestamp '2019-01-09 14:00:01'
from dual
)
select id,
trunc(start_time, 'hh') + interval '1' hour * (level - 1) as hr,
case when level = 1 and connect_by_isleaf = 1
then end_time - start_time
when level = 1
then trunc(start_time, 'hh') + interval '1' hour - start_time
when connect_by_isleaf = 1
then end_time - trunc(end_time, 'hh')
else interval '1' hour
end as duration
from your_table
connect by trunc(start_time, 'hh') + interval '1' hour * (level - 1) < end_time
and prior id = id
and prior sys_guid() is not null
;
Output:
ID HR DURATION
---------- ------------------- -------------------
1 2019-01-09 17:00:00 +00 00:55:26.000000
1 2019-01-09 18:00:00 +00 01:00:00.000000
1 2019-01-09 19:00:00 +00 00:55:03.000000
2 2019-01-09 23:00:00 +00 00:12:59.000000
2 2019-01-10 00:00:00 +00 01:00:00.000000
2 2019-01-10 01:00:00 +00 01:00:00.000000
2 2019-01-10 02:00:00 +00 00:05:03.000000
3 2019-01-09 18:00:00 +00 00:01:06.000000
4 2019-01-09 13:00:00 +00 01:00:00.000000
4 2019-01-09 14:00:00 +00 00:00:01.000000

Related

Split row data base on timestamp SQL Oracle

Good day everyone. I have a table as below. Duration is the time from current state to next state.
Timestamp
State
Duration(minutes)
10/9/2022 8:50:00 AM
A
35
10/9/2022 9:25:00 AM
B
10
10/9/2022 9:35:00 AM
C
...
How do I split data at 9:00 AM of each day like below:
Timestamp
State
Duration(minutes)
10/9/2022 8:50:00 AM
A
10
10/9/2022 9:00:00 AM
A
25
10/9/2022 9:25:00 AM
B
10
10/9/2022 9:35:00 AM
C
...
Thank you.
Use a row-generator function to generate extra rows when the timestamp is before 09:00 and the next timestamp is after 09:00 (and calculate the diff value rather than storing it in the table):
SELECT l.ts AS timestamp,
t.state,
ROUND((l.next_ts - l.ts) * 24 * 60, 2) As diff
FROM (
SELECT timestamp,
LEAD(timestamp) OVER (ORDER BY timestamp) AS next_timestamp,
state
FROM table_name
) t
CROSS APPLY (
SELECT GREATEST(
t.timestamp,
TRUNC(t.timestamp - INTERVAL '9' HOUR) + INTERVAL '9' HOUR + LEVEL - 1
) AS ts,
LEAST(
t.next_timestamp,
TRUNC(t.timestamp - INTERVAL '9' HOUR) + INTERVAL '9' HOUR + LEVEL
) AS next_ts
FROM DUAL
CONNECT BY
TRUNC(t.timestamp - INTERVAL '9' HOUR) + INTERVAL '9' HOUR + LEVEL - 1 < t.next_timestamp
) l;
Which, for your sample data:
CREATE TABLE table_name (Timestamp, State) AS
SELECT DATE '2022-10-09' + INTERVAL '08:50' HOUR TO MINUTE, 'A' FROM DUAL UNION ALL
SELECT DATE '2022-10-09' + INTERVAL '09:25' HOUR TO MINUTE, 'B' FROM DUAL UNION ALL
SELECT DATE '2022-10-09' + INTERVAL '09:35' HOUR TO MINUTE, 'C' FROM DUAL UNION ALL
SELECT DATE '2022-10-12' + INTERVAL '09:35' HOUR TO MINUTE, 'D' FROM DUAL;
Outputs:
TIMESTAMP
STATE
DIFF
2022-10-09 08:50:00
A
10
2022-10-09 09:00:00
A
25
2022-10-09 09:25:00
B
10
2022-10-09 09:35:00
C
1405
2022-10-10 09:00:00
C
1440
2022-10-11 09:00:00
C
1440
2022-10-12 09:00:00
C
35
2022-10-12 09:35:00
D
null
fiddle

Selecting Dates from Monday to Saturday and Hours Between Morning 8:00:00 AM To Next Day Morning 7:00:00 AM

I am trying to work on a query where there is date selection in the where clause i.e. if sysdate is Monday I have to get the dates from Monday to Saturday and Hours Between Morning 08:00:00 AM to Next Day Morning 07:00:00 AM. I am hardcoding the dates and Hours in the where clause, When I run the query data does not show.
Query:
SELECT TO_CHAR(sysdate, 'HH24:MI:SS'), REPLACE(TO_CHAR(sysdate, 'DAY'), ' ')
FROM dual
WHERE TO_CHAR(sysdate, 'HH24:MI:SS') BETWEEN '08:01:00' AND '08:00:00'
AND TO_CHAR(sysdate, 'DAY') >= 'MONDAY'
AND TO_CHAR(sysdate, 'DAY') <= 'SATURDAY';
You need to filter the wider range first (from 8 AM at Monday till the end of a Saturday, if I understood correctly) and then exclude time from 7 AM till 8 AM.
In the below code iw format element stands for ISO week, that starts on the Monday.
with a as (
select
date '2022-04-17'
+ interval '01:30:00' hour to second
+ interval '2' hour * level
as dt
from dual
connect by level < 90
)
select
to_char(dt, 'yyyymmdd') as day_
, listagg(to_char(dt, 'hh24:mi'), ',')
within group (order by dt asc) as hours
from a
where 1 = 1
/*From Mon 08 AM*/
and dt > trunc(dt, 'iw') +
interval '8' hour
/*Till Sat end of the day*/
and dt < trunc(dt, 'iw') + 6
/*and except minutes between 7 and 8 AM*/
and not (
to_char(dt, 'hh24mi') < '0800'
and to_char(dt, 'hh24mi') > '0700'
)
group by to_char(dt, 'yyyymmdd')
DAY_ | HOURS
:------- | :----------------------------------------------------------------
20220418 | 09:30,11:30,13:30,15:30,17:30,19:30,21:30,23:30
20220419 | 01:30,03:30,05:30,09:30,11:30,13:30,15:30,17:30,19:30,21:30,23:30
20220420 | 01:30,03:30,05:30,09:30,11:30,13:30,15:30,17:30,19:30,21:30,23:30
20220421 | 01:30,03:30,05:30,09:30,11:30,13:30,15:30,17:30,19:30,21:30,23:30
20220422 | 01:30,03:30,05:30,09:30,11:30,13:30,15:30,17:30,19:30,21:30,23:30
20220423 | 01:30,03:30,05:30,09:30,11:30,13:30,15:30,17:30,19:30,21:30,23:30
db<>fiddle here
(And what if sysdate isn't Monday?)
Therefore, could you explain a little bit better what is the input (dates? One date? SYSDATE?) and what is desired output (related to that input).
Basically, I don't understand what you want. Meanwhile, errors you made (if it'll help).
Format model is wrong; this is what you did:
SQL> select to_char(sysdate, 'DAY') day, length(to_char(sysdate, 'DAY')) len from dual;
DAY LEN
--------- ----------
FRIDAY 9
"FRIDAY" doesn't have 9 characters; it has 6 of them --> use the fm format modifier (it'll truncate trailing spaces):
SQL> select to_char(sysdate, 'fmDAY') day, length(to_char(sysdate, 'fmDAY')) len from dual;
DAY LEN
--------- ----------
FRIDAY 6
SQL>
Today (22.04.2022) is Friday. Your query searches for data whose day is between "MONDAY" and "SATURDAY". As you're comparing strings and alphabet goes as [A, B, ..., F, G, ..., M, N, ..., S, T], "F(riday)" is NEVER between M(onday) and S(aturday) so there's zero chance that it'll work.
As of hours: which time exactly is between 08:01 and 08:00? Time doesn't go backwards (unless you meant "08:01 today and 08:00 tomorrow").
if sysdate is Monday I have to get the dates from Monday to Saturday and Hours Between Morning 08:00:00 AM to Next Day Morning 07:00:00 AM.
You can find whether SYSDATE is Monday by comparing the day to the start of the ISO week (which will always be midnight on Monday):
SELECT *
FROM DUAL
WHERE SYSDATE - TRUNC(SYSDATE, 'IW') < 1
You can find out whether the hours are between 08:00 and 07:00 the next day by subtracting 8 hours and finding out whether the time is between 00:00 and 23:00:
SELECT *
FROM DUAL
WHERE (SYSDATE - INTERVAL '8' HOUR) - TRUNC(SYSDATE - INTERVAL '8' HOUR)) DAY TO SECOND
<= INTERVAL '23' HOUR;
You can combine the two to find out if the day is between Monday and Saturday and the time is between 08:00 and 07:00 on the next day (so for Saturday, it would include 7 hours of Sunday) using:
SELECT *
FROM DUAL
WHERE (SYSDATE - INTERVAL '8' HOUR) - TRUNC(SYSDATE - INTERVAL '8' HOUR), 'IW') < 6
AND (SYSDATE - INTERVAL '8' HOUR) - TRUNC(SYSDATE - INTERVAL '8' HOUR)) DAY TO SECOND
<= INTERVAL '23' HOUR;
Note: This does not use TO_CHAR so it is unaffected by any changes to the NLS_TERRITORY or NLS_DATE_LANGUAGE session parameters so it will always give the same answer (independent of the settings of the user who runs the query).
You can use such a combination
SELECT TO_CHAR(dt,'HH24:MI:SS','NLS_DATE_LANGUAGE=English') AS Hour,
TO_CHAR(dt,'Day','NLS_DATE_LANGUAGE=English') AS day
FROM t
WHERE TO_CHAR(dt,'Dy','NLS_DATE_LANGUAGE=English') IN ('Tue','Wed','Thu','Fri')
AND TO_CHAR(dt, 'HH24:MI:SS') NOT BETWEEN '07:00:01' AND '08:00:00'
OR TO_CHAR(dt,'Dy','NLS_DATE_LANGUAGE=English') = 'Mon'
AND TO_CHAR(dt, 'HH24:MI:SS')>= '07:00:00'
OR TO_CHAR(dt,'Dy','NLS_DATE_LANGUAGE=English') = 'Sat'
AND TO_CHAR(dt, 'HH24:MI:SS')<= '08:00:00'
where needs to consider restricting the periods for the bound dates individually
Demo

Group by with Unix time stamps

I am trying write a query where time stamps are in Unix format.
The objective of the query is group by these time stamps in five minute segments and to count each unique Id in those segments.
Is there a simple way of doing this?
The result looking for this
Time_utc Id count
25/07/2019 1600 1 3
25/07/2019 1600 2 1
25/07/2019 1605 1 4
You haven't shown data, so as a starting point you can group the Unix timestamps by dividing by 300 (for 5 minutes worth of seconds):
select 300 * floor(unix_ts/300) as unix_five_minute,
timestamp '1970-01-01 00:00:00 UTC'
+ (300*floor(unix_ts/300)) * interval '1' second as oracle_timestamp,
count(*)
from cte2
group by floor(unix_ts/300);
or if you have millisecond precision adjust by a factor of 1000:
select 300000 * floor(unix_ts/300000) as unix_five_minute,
timestamp '1970-01-01 00:00:00 UTC'
+ (300*floor(unix_ts/300000)) * interval '1' second as oracle_timestamp,
count(*)
from cte2
group by floor(unix_ts/300000);
Demo using made-up data generated from current time:
-- CTEs to generate some sample data
with cte1 (oracle_interval) as (
select systimestamp - level * interval '42' second
- timestamp '1970-01-01 00:00:00.0 UTC'
from dual
connect by level <= 30
),
cte2 (unix_ts) as (
select trunc(
extract(day from oracle_interval) * 86400000
+ extract(hour from oracle_interval) * 3600000
+ extract(minute from oracle_interval) * 60000
+ extract(second from oracle_interval) * 1000
)
from cte1
)
-- actual query
select 300000 * floor(unix_ts/300000) as unix_five_minute,
timestamp '1970-01-01 00:00:00 UTC'
+ (300*floor(unix_ts/300000)) * interval '1' second as oracle_timestamp,
count(*)
from cte2
group by floor(unix_ts/300000);
UNIX_FIVE_MINUTE ORACLE_TIMESTAMP COUNT(*)
---------------- ------------------------- ----------------
1564072500000 2019-07-25 16:35:00.0 UTC 7
1564072200000 2019-07-25 16:30:00.0 UTC 7
1564071600000 2019-07-25 16:20:00.0 UTC 4
1564071900000 2019-07-25 16:25:00.0 UTC 8
1564072800000 2019-07-25 16:40:00.0 UTC 4
Unix time stamps such as 155639.600 or 155639.637
Those are unusual values; Unix/epoch times are usually 10-digit numbers, or 13 digits for millisecond precision. Assuming (or rather, guessing) that they are tenths of a second for some reason:
-- CTE for sample data
with cte (unix_ts) as (
select 155639.600 from dual
union all
select 155639.637 from dual
)
-- actual query
select 300 * floor(unix_ts*10000/300) as unix_five_minute,
timestamp '1970-01-01 00:00:00 UTC'
+ (300*floor(unix_ts*10000/300)) * interval '1' second as oracle_timestamp,
count(*)
from cte
group by floor(unix_ts*10000/300);
UNIX_FIVE_MINUTE ORACLE_TIMESTAMP COUNT(*)
---------------- ------------------------- ----------------
1556396100 2019-04-27 20:15:00.0 UTC 1
1556395800 2019-04-27 20:10:00.0 UTC 1
The 10000/300 could be simplified to 100/3, but I think it's clearer left as it is.

cast two separate columns which has hour ( datatype number) and minutes ( datatype number) to time datatype and subtract 90 minutes in oracle

I have two separate columns for hours and minutes in my table and I have a report where i should be subtracting 90 minutes from total time put together or ( 1 hour from hour field) and 30 minutes from minutes field. The output can be in minutes or hours.
I tried "to_char ( hours_column -1,'00' ) || ':' || to_char ( minutes_column -30,'00' ) AS "MAX_TIME" " - this fails when I have time like 9:00 I get 8:-30 as the output when I need to get 7:30.
I came up with some sql code with DATEADD and cast functions which worked but it fails when I implement it in Oracle.
Select Substring(Cast(DATEADD(minute, -90, Cast(hourscolumn + ':' + minutes column as Time)) as varchar(20)),1,5) as max_time
Can someone help me to implement the above code in Oracle? I'm just trying to deduct 90 minutes by putting the hours and minutes columns together.
Something like this?
test CTE represents your data. How come you got that (bad) idea? Who/what prevents you from storing 32 hours and 87 minutes into those columns?
query itself contains
time: the way you create a valid date value. It'll fail if hours and/or minutes are invalid (such as previously mentioned 32:87)
subtracted: subtract 90 minutes from time; (24 * 60) represents 24 hours in a day, 60 minutes in an hour. It'll contain both date and time component
the final result is achieved by applying to_char with appropriate format mask (hh24:mi) to the subtracted value
SQL> alter session set nls_Date_format = 'dd.mm.yyyy hh24:mi';
Session altered.
SQL> with test (hours, minutes) as
2 (select '09', '00' from dual union all
3 select '23', '30' from dual union all
4 select '00', '20' from dual
5 )
6 select hours,
7 minutes,
8 to_date(hours||minutes, 'hh24mi') time,
9 --
10 to_date(hours||minutes, 'hh24mi') - 90 / (24 * 60) subtracted,
11 --
12 to_char(to_date(hours||minutes, 'hh24mi') - 90 / (24 * 60), 'hh24:mi') result
13 from test;
HO MI TIME SUBTRACTED RESUL
-- -- ---------------- ---------------- -----
09 00 01.07.2019 09:00 01.07.2019 07:30 07:30
23 30 01.07.2019 23:30 01.07.2019 22:00 22:00
00 20 01.07.2019 00:20 30.06.2019 22:50 22:50
SQL>
Use NUMTODSINTERVAL to convert the hours and minutes to INTERVAL data types and then you can subtract INTERVAL '90' MINUTE and EXTRACT the resulting hour and minute components.
Oracle Setup:
CREATE TABLE table_name ( hours_column, minutes_column ) AS
SELECT 0, 0 FROM DUAL UNION ALL
SELECT 1, 30 FROM DUAL UNION ALL
SELECT 2, 45 FROM DUAL UNION ALL
SELECT 3, 0 FROM DUAL UNION ALL
SELECT 27, 59 FROM DUAL
Query:
SELECT EXTRACT( HOUR FROM time ) + EXTRACT( DAY FROM time ) * 24 AS hours,
EXTRACT( MINUTE FROM time ) AS minutes,
time,
TO_CHAR( EXTRACT( HOUR FROM time ) + EXTRACT( DAY FROM time ) * 24, '00' )
|| ':' || TO_CHAR( ABS( EXTRACT( MINUTE FROM time ) ), 'FM00' ) AS as_string
FROM (
SELECT NUMTODSINTERVAL( hours_column, 'HOUR' )
+ NUMTODSINTERVAL( minutes_column, 'MINUTE' )
- INTERVAL '90' MINUTE AS time
FROM table_name
)
Output:
HOURS | MINUTES | TIME | AS_STRING
----: | ------: | :---------------------------- | :--------
-1 | -30 | -000000000 01:30:00.000000000 | -01:30
0 | 0 | +000000000 00:00:00.000000000 | 00:00
1 | 15 | +000000000 01:15:00.000000000 | 01:15
1 | 30 | +000000000 01:30:00.000000000 | 01:30
26 | 29 | +000000001 02:29:00.000000000 | 26:29
db<>fiddle here

how to convert date format in sql

if i have dates/time like these
8/5/2014 12:00:01 AM
8/5/2014 12:00:16 AM
8/5/2014 12:00:18 AM
8/5/2014 12:17:18 AM
8/5/2014 12:19:18 AM
i want these date/times
if the minutes less than 15 and greater than 00 i want the time for minutes to be 00
if the minutes less than 30 and greater than 15 i want the minutes to be 15
if the minutes less than 45 and greater than 30 i want the minutes to be 30
if the minutes less than 00 and greater than 45 i want the minutes to be 45
8/5/2014 12:00:00 AM
...
...
8/5/2014 12:15:00AM
...
...
8/5/2014 12:30:00AM
...
...
8/5/2014 12:45:00AM
i need to do that for my report
how can i apply these in oracle
Here's another method, which is really a variation of Gordon Linoff's approach:
trunc(dt, 'HH24') + ((15/1440) * (floor(to_number(to_char(dt, 'MI'))/15)))
The trunc(dt, 'HH24') gives you the value truncated to hour precision, so for your sample that's always midnight. Then floor(to_number(to_char(dt, 'MI'))/15) gives you the number of complete 15-minute periods represented by the minute value; with your data that's either zero or 1. As Gordon mentioned when you add a numeric value to a date it's treated as fractions of a day, so that needs to be multiplied by '15 minutes' (15/1400).
with t as (
select to_date('8/5/2014 12:00:01 AM') as dt from dual
union all select to_date('8/5/2014 12:00:16 AM') as dt from dual
union all select to_date('8/5/2014 12:00:18 AM') as dt from dual
union all select to_date('8/5/2014 12:17:18 AM') as dt from dual
union all select to_date('8/5/2014 12:19:18 AM') as dt from dual
union all select to_date('8/5/2014 12:37:37 AM') as dt from dual
union all select to_date('8/5/2014 12:51:51 AM') as dt from dual
)
select dt, trunc(dt, 'HH24')
+ ((15/1440) * (floor(to_number(to_char(dt, 'MI'))/15))) as new_dt
from t;
DT NEW_DT
---------------------- ----------------------
08/05/2014 12:00:01 AM 08/05/2014 12:00:00 AM
08/05/2014 12:00:16 AM 08/05/2014 12:00:00 AM
08/05/2014 12:00:18 AM 08/05/2014 12:00:00 AM
08/05/2014 12:17:18 AM 08/05/2014 12:15:00 AM
08/05/2014 12:19:18 AM 08/05/2014 12:15:00 AM
08/05/2014 12:37:37 AM 08/05/2014 12:30:00 AM
08/05/2014 12:51:51 AM 08/05/2014 12:45:00 AM
To add yet another method this looks like a situation where the function NUMTODSINTERVAL() could be useful - it makes it slightly more obvious what's happening:
select trunc(dt)
+ numtodsinterval(trunc(to_char(dt, 'sssss') / 900) * 15, 'MINUTE')
from ...
TRUNC() truncates the date to the beginning of that day. The format model sssss calculates the number of seconds since midnight. The number of complete quarter-hours since midnight is the number of seconds divided by 900 (as there are 900 quarter-hours in the day). This is truncated again to remove any part-completed quarter-hours, multipled by 15 to give the number of minutes (there are 15 minutes in a quarter hour). Lastly, convert this to an INTERVAL DAY TO SECOND and add to the original date.
SQL> alter session set nls_date_format = 'dd/mm/yyyy hh24:mi:ss';
Session altered.
SQL> with t as (
2 select to_date('05/08/2014 12:00:01') as dt from dual union all
3 select to_date('05/08/2014 12:00:16') as dt from dual union all
4 select to_date('05/08/2014 12:00:18') as dt from dual union all
5 select to_date('05/08/2014 12:17:18') as dt from dual union all
6 select to_date('05/08/2014 12:19:18') as dt from dual union all
7 select to_date('05/08/2014 12:37:37') as dt from dual union all
8 select to_date('05/08/2014 12:51:51') as dt from dual
9 )
10 select trunc(dt)
11 + numtodsinterval(trunc(to_char(dt, 'sssss') / 900) * 15, 'MINUTE')
12 from t
13 ;
TRUNC(DT)+NUMTODSIN
-------------------
05/08/2014 12:00:00
05/08/2014 12:00:00
05/08/2014 12:00:00
05/08/2014 12:15:00
05/08/2014 12:15:00
05/08/2014 12:30:00
05/08/2014 12:45:00
7 rows selected.
I've explicitly set my NLS_DATE_FORMAT so I can rely on implicit conversion in TO_DATE() so that it fits on the page without scrolling. It is not recommended to use implicit conversion normally.
Here is one method. Extract the date and then add in what you want as hours and minutes:
select trunc(dt) + extract(hour from dt) / 24.0 +
(trunc(extract(minute from dt) / 15) * 15) / (24.0 * 60);
This uses the fact that + for dates adds a number of days. The three terms are the original date at midnight, the number of hours converted to days (hence the / 24) and the third is the number of minutes, suitably rounded.
Here is an example for ORACLE with actual date:
select trunc(sysdate,'HH')+trunc(to_number(to_char(sysdate,'MI'))/15)*15/24/60
from dual;