Create a datetime value from separate date time columns - sql

I have individual columns for year, month, day, hour, minute, second and millisecond. I need to combine them all together into a date timestamp. I am able to make a date value, but can't seem to make a valid time one. I am working in Oracle and I have sample below. I'd like my value to look like the createtime column.
This did the trick:
to_timestamp(XT.MONTH1||'-'||XT.DAY1||'-'||XT.YEAR||' '|| xt.hour24||'.'||xt.minute||'.'||xt.second||'.'||xt.milliseconds,'MM-DD-YYYY HH24:MI:SS.FF')

That is quite easy but not pretty. You convert the columns from datatype NUMBER to VARCHAR2 padded with leading zeros, using fm to suppress any spaces for + or -:
SELECT TO_CHAR(month,'fm00') FROM mytable;
05
Next, you glue the converted columns together with the || operator to get a single, long, string:
SELECT TO_CHAR(year,'fm0000')||TO_CHAR(month,'fm00')|| ...
20200519...
This long string can now be converted to datatype DATE, or, in your case TIMESTAMP as you have milliseconds. You need to specify the date format you used, f.i. 'YYYYMMDD...'
SELECT TO_TIMESTAMP(TO_CHAR(year,'fm0000')|| ... , 'YYYYMM')
A complete example looks like:
SELECT TO_TIMESTAMP(
TO_CHAR(year,'fm0000')||TO_CHAR(month,'fm00')||
TO_CHAR(day,'fm00')||TO_CHAR(hour24,'fm00')||
TO_CHAR(minute,'fm00')||TO_CHAR(second,'fm00')||
TO_CHAR(ms,'fm000')
, 'YYYYMMDDHH24MISSFF3')
FROM (-- your table, as a mockup, I'll use DUAL
SELECT 2020 as year, 5 as month, 19 as day,
13 as hour24, 7 as minute, 10 as second,
300 as ms
FROM DUAL);
2020-05-19 13:07:10,300000000
EDIT:
The fill mode modifier fm supresses a leading space for positive numbers (to make room for the - sign for negative numbers). All parts of a date are positive, so you get a lot of spaces in your string.
SELECT TO_CHAR(x,'99'), TO_CHAR(x,'fm99')
FROM (SELECT -10 as x FROM DUAL UNION ALL SELECT 10 FROM DUAL);
| 10|10|
|-10|-10|
The documentation is a bit hidden under Format Model Modifiers.
Come to think of it, you might as well keep the spaces and adjust your format model:
SELECT TO_TIMESTAMP(
TO_CHAR(year,'0000')||TO_CHAR(month,'00')||
TO_CHAR(day,'00')||TO_CHAR(hour24,'00')||
TO_CHAR(minute,'00')||TO_CHAR(second,'00')||
TO_CHAR(ms,'000')
,' YYYY MM DD HH24 MI SS FF3')
FROm (SELECT 2020 as year, 5 as month, 19 as day,
13 as hour24, 7 as minute, 10 as second,
300 as ms FROM DUAL);

Related

Oracle sql not reading the entire string

I have a oracle sql query to get me the dates from 18/05/2021 to '28/05/2021'.
For some reason the the value after slash is not read, as the values from month 4 is also outputed. I dont know where i am wrong. Please give a hand if you are able, thanks a lot for your time.
NOTE: the dates have been stored in the database with a varchar datatype.
SELECT datadate
FROM mytable
WHERE trailerid= '1' and datattime>'05:00:00' and datattime<'12:00:00'
AND datadate between '18/05/2021' and '28/05/2021'
GROUP BY datadate ORDER BY datadate;
Current output
DATADATE
----------------
18/05/2021
19/04/2021
19/05/2021
20/05/2021
21/04/2021
21/05/2021
22/04/2021
22/05/2021
23/04/2021
23/05/2021
24/04/2021
24/05/2021
25/04/2021
25/05/2021
26/04/2021
26/05/2021
27/04/2021
27/05/2021
28/04/2021
28/05/2021
That's what happens when people store date values as strings.
See if this helps:
AND to_date(datadate, 'dd/mm/yyyy') between to_date('18/05/2021', 'dd/mm/yyyy')
and to_date('28/05/2021', 'dd/mm/yyyy')
You don't have to start over, just move the data to a datetime column
ALTER TABLE t ADD x DATE;
UPDATE t SET x = to_date(concat(datadate,datatime), 'dd/mm/yyyyhh24:mi:ss'))
WHERE datadate in (SELECT to_char(to_date('1999-12-31', 'yyyy-mm-dd') + level, 'dd/mm/yyyy') FROM dual CONNECT BY level <= 10000)
That WH ERE clause should generate a list of valid dates from 2000 to about 2030 - if you have dates outside this range adjust accordingly
Should now be able to find invalid dates (they weren't part of the where clause and x should hence remain null) and fix manually:
SELECT * FROM t WHERE x is null
Then drop your datadate and datatime cols and rename x to datadatetime
Now queries like BETWEEN work properly, and if you need just the date part you can do TRUNC(x). (You can even TRUNC to other date parts like hours, to cut the minutes and seconds off, or week of year to round dates down to the start of the week etc)
If you need just the time you either do x - TRUNC(x) which gives a decimal number like 0.5 for 12 noon or 0.75 for 6pm, or you can TOCHAR depending on what you want to do. It would be better to do x -TRUNC(x) BETWEEN 9.0/24.0 AND 17.0/24.0 than doing a string compare

(Oracle)Getting 30 days before

My goal is to get 30 days before.
The data are stored as varchar2 type, '20200630' and '20200613'.
The expected result is '20200531' and '20200514' for each.
I have simply subtracted 30 from my varchar2 '20200630' and '20200613' and
it shows result by casting, but the result are not expected such as '20200600','20200583' which are not form of date.
Could I know how to modify my code as below
WITH A AS
(SELECT '20200630' YEARMONTHDAY FROM DUAL
UNION ALL
SELECT '20200613' FROM DUAL)
SELECT YEARMONTHDAY - 30 FROM A;
Store your data using the appropriate type! That is, use date rather than a string.
If you are stuck with data in this format, convert to date:
select to_date(yearmonthday, 'YYYYMMDD') - interval '30' day
I would not recommend converting the value back to a string. Dates should be dates.

how to add dates with keep their formats?

I have a start_time which is already formatted as date type and have duration as number like 449. It means 449 seconds. So i need end_time. Of course i can obviously convert duration to date format and add duration on start_time using below simply queries
select to_char(to_date(USE_SEC,'sssss'),'hh24miss')
from ABA_RM_INB_USAGE;
USE_SEC column is containing integer(number in oracle) like 1167
and above query is returning date formatted result like 001927 that is okay.
This is query that add duration on start_time
select to_char(USE_STRT_DTTM, 'hh24miss') + to_char(to_date(USE_SEC, 'sssss'), 'hh24miss') as duration
from ABA_RM_INB_USAGE;
This is returning that result which is problem that convert to date format
95980.
It means 09:59:80 oops 80 seconds is absolutely wrong. Can i add dates with keep their formats. How can i ?
You can use +. This is the traditional method:
select start_time + duration / 24*60*60
You can write this now as:
select start_time + duration * interval '1' second
Your first query is converting your number-of-second value to a string. In your second query you are converting the start time to another string. Both represent HHMISS. Then you add them together, effectively:
'094053' + '001927'
For the addition operator to work they are implicitly converted to numbers, so it becomes:
94053 + 1927
which gives you your (numeric) result of 95980.
As soon as you convert to strings you are losing the ability to treat them as dates and honour the mod-60 behaviour for minutes and seconds, which is my you appear to end up with 80 seconds - but they aren't really seconds at all, it's just a number. You also lose the mod-24 behaviour for hours, so if your start time is just before midnight and the duration pushes you over midnight, your result wouldn't reflect that either.
As #GordonLinoff suggested, keep your date as a date, and add the number of seconds as a number, or a number converted to an interval:
USE_STRT_DTTM + USE_SEC / (24*60*60)
or:
USE_STRT_DTTM + USE_SEC * interval '1' second
Demo:
-- CTE for sample data
with ABA_RM_INB_USAGE (USE_STRT_DTTM, USE_SEC) as (
select to_date('09:40:53', 'HH24:MI:SS'), 1167 from dual
union all
select to_date('23:54:55', 'HH24:MI:SS'), 449 from dual
)
-- query showing working
select USE_STRT_DTTM,
USE_SEC,
to_char(to_date(USE_SEC, 'sssss'), 'hh24:mi:ss') as use_sec_hhmiss,
USE_SEC * interval '1' second as use_sec_interval,
USE_STRT_DTTM + USE_SEC / (24*60*60) as result1,
USE_STRT_DTTM + USE_SEC * interval '1' second as result2
from ABA_RM_INB_USAGE;
USE_STRT_DTTM USE_SEC USE_SEC_HHMISS USE_SEC_INTERVAL RESULT1 RESULT2
------------------- ------- -------------- ------------------- ------------------- -------------------
2019-08-01 09:40:53 1167 00:19:27 +00 00:19:27.000000 2019-08-01 10:00:20 2019-08-01 10:00:20
2019-08-01 23:54:55 449 00:07:29 +00 00:07:29.000000 2019-08-02 00:02:24 2019-08-02 00:02:24
Read more about Datetime/Interval Arithmetic.
I have a start_time which is already formatted as date type
Your column is (I hope, and seems to be the case from your query) a date. Dates do not have intrinsic human-readable formats. When you query your table your client will format the date to something readable, using either its own preferences or your session's NLS_DATE_FORMAT.
Of course i can obviously convert duration to date format and add duration on start_time
You originally converted your duration to a date data type (via to_date()), at 00:19:27 on the first day of the current month (which is what if defaults to if not day, month or year components are supplied; my CTE above is doing the same). You cannot add a date to another date. That even has its own error, "ORA-00975: date + date not allowed". So you then converted both your date values (start time and converted duration) to strings. You can't add strings together either, as that makes no sense; but if you try Oracle will implicitly try to convert both strings to numbers. In this case that implicit conversion works for both strings, but it usually won't; the superficially-similar '09:40:53' + '00:19:27' would get "ORA-01722: invalid number".
In Oracle DATE values do not have a format - you use the TO_CHAR function to format them when you need to output them.
In this case it looks like you need to use an interval. You have a field which contains a number of seconds that you want to convert to an interval - for this you can use the TO_DSINTERVAL function, although amusingly enough you have to convert the number to a string in order to use the function to convert it to an interval:
-- Version using TO_DSINTERVAL
WITH cteData AS (SELECT USE_STRT_DTTM + TO_DSINTERVAL('PT' || TO_CHAR(USE_SEC) || 'S') AS DT_TIME
FROM ABA_RM_INB_USAGE)
SELECT TO_CHAR(DT_TIME, 'YYYY-MM-DD HH24:MI:SS') FORMATTED_DATE_TIME
FROM cteData;
Docs for TO_DSINTERVAL here
dbfiddle demonstrating this in use here
EDIT
As #AlexPoole points out, the better function to use here is NUMTODSINTERVAL:
-- Version using NUMTODSINTERVAL
WITH cteData AS (SELECT USE_STRT_DTTM + NUMTODSINTERVAL(USE_SEC, 'SECOND') AS DT_TIME
FROM ABA_RM_INB_USAGE)
SELECT TO_CHAR(DT_TIME, 'YYYY-MM-DD HH24:MI:SS') FORMATTED_DATE_TIME
FROM cteData;
Docs for NUMTODSINTERVAL here
updated dbfiddle here

Number to HH24:MM conversion - SQL, Oracle

We have number pairs like 810 1015 that mean the hour and minute. We have to calculate the minute difference of the pair. The example above would give 125 (minutes).
What solution would you give? I thought about converting to string and substringing then concatenating, but can't know if it is 3 or 4 long and using IF ELSE but would be too complicated (if no other solution exist I am left with this). Also thought about somehow converting to base 60 and subtracting, but also too complicated.
Thanks in advance.
Edit: This solution is based on Plirkee's comment to lpad numbers to get 4-character strings, and on Stefano Zanini's solution modified to allow for 0 hour, and 24-hour format.
If last two digits always represent minutes, and if hours are always in 24-hour format:
with t(time1, time2) as (
select 810, 1015 from dual union all
select 20, 1530 from dual
),
conv(time1, time2) as (
select lpad(to_char(time1), 4, '0'),
lpad(to_char(time2), 4, '0')
from t
)
select time1,
time2,
24 * 60 * (to_date(time2, 'HH24MI') - to_date(time1, 'HH24MI')) diff_minutes
from conv;
How about storing the data as a DATA datetype, using an standard date portion, such as 01-10-2000. So you data would be
01-01-2000 8:10:00
01-01-2000 10:15:00
etc
Then you can just do simple date math :)
Assuming 3 digits is the minimum length of your numbers (otherwise you'd have ambiguous cases), this following query should do the trick
select (to_date(substr(t2, 1, length(t2)-2) || ':' || substr(t2, length(t2)-1, length(t2)), 'HH:MI') -
to_date(substr(t1, 1, length(t1)-2) || ':' || substr(t1, length(t1)-1, length(t1)), 'HH:MI')) * 24 * 60 cc
from (select 810 t1, 1015 t2 from dual)
The steps are:
explode the numbers in two parts each: last two digits as the minutes and the remaining digits as the hour
concatenate the two parts with a separator (in this example ':')
convert that concatenations into dates
multiply the difference between the two dates (which is in days) by 24 to get hours and by 60 to get minutes
Just an another tweak which can be used. Hope this helps.
SELECT
TO_CHAR(TO_DATE(LPAD(LPAD('1015',4,'0') - LPAD('810',4,'0'),4,'0'),'HH24MI'),'HH24')*60
+TO_CHAR(TO_DATE(lpad(lpad('1015',4,'0') - lpad('810',4,'0'),4,'0'),'HH24MI'),'MI') MINUTES
FROM dual;

Varchar2 to Date for sysdate comparison

A table I am querying has dates stored as varchar2. I need to compare the dates stored as varchar2 with sysdate using BETWEEN sysdate-1 AND sysdate-30 (to return varchar2 dates from the last month).
When specifying TO_DATE(varchar2, 'DD-MON-YYYY') I get error "literal does not match format string".
I am stuck as Oracle documentation says this is an acceptable format for TO_DATE() when converting from varchar2.
UPDATE: This is a corporate database, I did not design the DB and can only work with what I have available. The data set is enormous and is automatically updated by SCADA devices, over 10,000 devices daily.
SELECT device_name, read_date, sysdate
FROM oracle_database
------- Data Returned by query --------
device_name read_date sysdate
Device 1 5/14/2013 22-Sep-14
Device 2 5/14/2013 22-Sep-14
Device 3 5/14/2013 22-Sep-14
Device 4 5/14/2013 22-Sep-14
Device 5 5/14/2013 22-Sep-14
Device 6 5/14/2013 22-Sep-14
Device 7 5/14/2013 22-Sep-14
Results from using TO_DATE
SELECT device_name, TO_DATE(read_date, 'DD-MON-YY'), sysdate
------- Data Returned by query ----------
ORA-01861: literal does not match format string
Given your data, the proper format is very probably MM/DD/YYYY. It will match both single and double digits months or days.
MM : month from 01 to 12 (leading 0 not mandatory by default)
\ : any punctuation sign
DD : day from 01 to 31 (leading 0 not mandatory by default)
\ : any punctuation sign
YYYY : year
See Oracle's documentation about datetime Format Models for the details.
Here is example:
-- Some test data
WITH testdata AS (
SELECT '5/14/2013' as d FROM DUAL
UNION SELECT '05/14/2013' FROM DUAL
UNION SELECT '5/1/2013' FROM DUAL
UNION SELECT '5/01/2013' FROM DUAL
UNION SELECT '6-6-2013' FROM DUAL)
-- Actual query demonstrating the use of the MM/DD/YYYY format
select d, TO_DATE(d,'MM/DD/YYYY') FROM testdata
Producing:
D TO_DATE(D,'MM/DD/YYYY')
05/14/2013 05/14/2013
5/01/2013 05/01/2013
5/1/2013 05/01/2013
5/14/2013 05/14/2013
6-6-2013 06/06/2013
If you really need to enforce the fact that your date components are separated by / (and not by any other punctuation sign), you should use fxfmMM/DD/YYYY instead:
the fx flag force strict comparison, both for punctuation and number of digits in the format
the fm flag relax that comparison to allow up to the number of specified digits
SELECT device_name, TO_DATE(read_date, 'DD/MM/YYYY'), sysdate
Firstly, your design is flawed. You should never have DATE as VARCHAR2 data type.
Secondly, while comparing the dates, and converting a date literal to a DATE, always specify the same format mask on both sides.
To convert a string to date, use TO_DATE with proper format mask.
To convert a date to string, use TO_CHAR with same format mask.
Make sure, while comparing the values in an expression, you explicitly convert the data type on either sides of the comparison operator to avoid implicit data type conversion.
And you must read this excellent article bybEd Stevens, http://edstevensdba.wordpress.com/2011/04/07/nls_date_format/