"Safe" TO_NUMBER() - oracle

SELECT TO_NUMBER('*') FROM DUAL
This obviously gives me an exception:
ORA-01722: invalid number
Is there a way to "skip" it and get 0 or NULL instead?
The whole issue: I have NVARCHAR2 field, which contains numbers and not almost ;-) (like *) and I need to select the biggest number from the column.
Yes, I know it is a terrible design, but this is what I need now... :-S
UPD:
For myself I've solved this issue with
COALESCE(TO_NUMBER(REGEXP_SUBSTR(field, '^\d+')), 0)

From Oracle Database 12c Release 2 you could use TO_NUMBER with DEFAULT ... ON CONVERSION ERROR:
SELECT TO_NUMBER('*' DEFAULT 0 ON CONVERSION ERROR) AS "Value"
FROM DUAL;
Or CAST:
SELECT CAST('*' AS NUMBER DEFAULT 0 ON CONVERSION ERROR) AS "Value"
FROM DUAL;
db<>fiddle demo

COALESCE(TO_NUMBER(REGEXP_SUBSTR(field, '^\d+(\.\d+)?')), 0)
will also get numbers with scale > 0 (digits to the right of the decimal point).

I couldn't find anything better than this:
function safe_to_number(p varchar2) return number is
v number;
begin
v := to_number(p);
return v;
exception when others then return 0;
end;

select COALESCE(TO_NUMBER(REGEXP_SUBSTR( field, '^(-|+)?\d+(\.|,)?(\d+)?$')), 0) from dual;
It will convert 123 to 123, but 123a or 12a3 to 0.

Fitting the original question and rather old skool
select a, decode(trim(translate(b,'0123456789.',' ')),null,to_number(b),0) from
(
select '1' a, 'not a number' b from dual
union
select '2' a, '1234' b from dual
)

It's probably a bit messy rolling your own regexp to test for a number, but the code below might work. I think the other solution by Gabe involving a user defined function is more robust since you are using the built in Oracle functionality (and my regexp is probably not 100% correct) but it might be worth a go:
with my_sample_data as (
select '12345' as mynum from dual union all
select '54-3' as mynum from dual union all
select '123.4567' as mynum from dual union all
select '.34567' as mynum from dual union all
select '-0.3462' as mynum from dual union all
select '0.34.62' as mynum from dual union all
select '1243.64' as mynum from dual
)
select
mynum,
case when regexp_like(mynum, '^-?\d+(\.\d+)?$')
then to_number(mynum) end as is_num
from my_sample_data
This will then give the following output:
MYNUM IS_NUM
-------- ----------
12345 12345
54-3
123.4567 123.4567
.34567
-0.3462 -0.3462
0.34.62
1243.64 1243.64

select DECODE(trim(TRANSLATE(replace(replace(A, ' '), ',', '.'), '0123456789.-', ' ')),
null,
DECODE(INSTR(replace(replace(A, ' '), ',', '.'), '.', INSTR(replace(replace(A, ' '), ',', '.'), '.') + 1),
0,
DECODE(INSTR(replace(replace(A, ' '), ',', '.'), '-', 2),
0,
TO_NUMBER(replace(replace(A, ' '), ',', '.'))))) A
from (select '-1.1' A from DUAL union all select '-1-1' A from DUAL union all select ',1' A from DUAL union all select '1..1' A from DUAL) A;
This code excludes such strings as: -1-1, 1..1, 12-2 and so on. And I haven't used regular expressions here.

A combination of previous solutions (from #sOliver and #Mike Meyers) and trying to grab as much numbers as possible by removing the last '$' from REGEXP.
It can be used to filter the actual number from a configuration table, and have a "kind-of" comment next to the number as '12 Days'.
with my_sample_data as (
select '12345' as mynum from dual union all
select '123.4567' as mynum from dual union all
select '-0.3462' as mynum from dual union all
select '.34567' as mynum from dual union all
select '-.1234' as mynum from dual union all
select '**' as mynum from dual union all
select '0.34.62' as mynum from dual union all
select '24Days' as mynum from dual union all
select '42ab' as mynum from dual union all
select '54-3' as mynum from dual
)
SELECT mynum,
COALESCE( TO_NUMBER( REGEXP_SUBSTR( mynum, '^(-|+)?\d*(.|,)?(\d+)?') ) , 0) is_num
FROM my_sample_data;
would give
MYNUM IS_NUM
-------- ----------
12345 12345
123.4567 123.4567
-0.3462 -0.3462
.34567 0.34567
-.1234 -0.1234
** 0
0.34.62 0.34
24Days 24
42ab 42
54-3 54

Best method seems to be the function solution but if you don't have necessary privileges in the environment you are struggling (like me), then you can try this one:
SELECT
CASE
WHEN
INSTR(TRANSLATE('123O0',
' qwertyuıopğüasdfghjklşizxcvbnmöçQWERTYUIOPĞÜASDFGHJKLŞİZXCVBNMÖÇ~*\/(){}&%^#$<>;#€|:_=',
'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
),
'X') > 0
THEN 'Y'
ELSE 'N'
END is_nonnumeric
FROM DUAL
By the way: In my case the problem was due to "," and "." :) So take that into consider. Inspired from this one. Also this one seems more concise.
By the way 2: Dear Oracle, can you please create some built-in functions for such small but invaluable needs?

Related

REGEXP_SUBSTR not able to process only current row

(SELECT LISTAGG(EVENT_DESC, ',') WITHIN GROUP (ORDER BY EVENT_DESC) FROM EVENT_REF WHERE EVENT_ID IN
( SELECT REGEXP_SUBSTR(AFTER_VALUE,'[^,]+', 1, level) FROM DUAL
CONNECT BY REGEXP_SUBSTR(AFTER_VALUE, '[^,]+', 1, level) IS NOT NULL
)
)
A table from which I am fetching AFTER_VALUE has values of integer which is comma seperated like
AFTER_VALUE data
Expected output
1
Event1
1,2
Event1,Event2
1,12,2,5
Event1,Event12,Event2,Event5
15,13
Event15,Event13
these are Ids in EVENT_REF table which have some description. I am trying to basically present
ex. 1,2 as Event1, Event2 and send back from query. There are multiple events so using REPLACE would be very tedious.
When using above query I'm getting error as “ORA-01722: invalid number” whenever there is more than one value in AFTER_VALUE column Ex. if there exists only one id , then the query works but for values like 1,2 or 1,13 etc it throws invalid number error.
PS: The event names are not Event1,Event2 etc , I have just put for reference.
You don't even need regular expressions for this assignment. Standard string function replace() can do the same thing, and faster. You only need an extra 'Event' at the beginning of the string, since that one doesn't "replace" anything.
Like this: (note that you don't need the with clause; I included it only for quick testing)
with
event_ref (after_value) as (
select '1' from dual union all
select '1,2' from dual union all
select '1,12,2,5' from dual union all
select '15,13' from dual
)
select after_value,
'Event' || replace(after_value, ',', ',Event') as desired_output
from event_ref
;
AFTER_VALUE DESIRED_OUTPUT
----------- -----------------------------
1 Event1
1,2 Event1,Event2
1,12,2,5 Event1,Event12,Event2,Event5
15,13 Event15,Event13
Ah,ok, looks, like you have other characters in your comma-separated list, so you can use this query:
with EVENT_REF(EVENT_ID,EVENT_DESC) as (
select 1, 'Desc 1' from dual union all
select 2, 'Desc 2' from dual union all
select 3, 'Desc 3' from dual union all
select 4, 'Desc 4' from dual union all
select 5, 'Desc 5' from dual union all
select 12, 'Desc12' from dual union all
select 13, 'Desc13' from dual union all
select 15, 'Desc15' from dual
)
select
(SELECT LISTAGG(EVENT_DESC, ',')
WITHIN GROUP (ORDER BY EVENT_DESC)
FROM EVENT_REF
WHERE EVENT_ID IN
( SELECT to_number(REGEXP_SUBSTR(AFTER_VALUE,'\d+', 1, level))
FROM DUAL
CONNECT BY level<=REGEXP_COUNT(AFTER_VALUE, '\d+')
)
)
from (
select '1' AFTER_VALUE from dual union all
select '1,2' AFTER_VALUE from dual union all
select '1,12,2,5' AFTER_VALUE from dual union all
select '15,13' AFTER_VALUE from dual
);
PS. And do not forget that to_number has 'default on conversion error' now: https://docs.oracle.com/en/database/oracle/oracle-database/12.2/sqlrf/TO_NUMBER.html
There is no need to split and concatenate substrings, just use regexp_replace:
with EVENT_REF (AFTER_VALUE) as (
select '1' from dual union all
select '1,2' from dual union all
select '1,12,2,5' from dual union all
select '15,13' from dual
)
select regexp_replace(AFTER_VALUE,'(\d+)','Event\1') from EVENT_REF;
REGEXP_REPLACE(AFTER_VALUE,'(\D+)','EVENT\1')
-----------------------------------------------
Event1
Event1,Event2
Event1,Event12,Event2,Event5
Event15,Event13

IN , NOT IN for null value in oracle

In oracle why "Not in" doesn't work on null values but "IN" works
For eg
with temp(n,p) as (
select 1,2 from dual union all
select 3,2 from dual union all
select 4,6 from dual union all
select 5,6 from dual union all
select 2,8 from dual union all
select 6,8 from dual union all
select 8,null from dual
)
1. Select * from temp where n in (2,6,8,null);
2. Select * from temp where n not in (2,6,8,null);
First Statement will give the output = 2,6,8
Second statement will not give any output
Can someone please explain why?
NOT IN essentially works like this:
col NOT IN (value_a, value_b, value_c)
-- is the same as
col != value_a && col != value_b && col != value_c
If one of the values is null, the whole expression evaluates to null, not true (which you probably expect).
You can read more about it here: https://jonathanlewis.wordpress.com/2007/02/25/not-in/

regex function using only a single quote

I want to select with regex if the column has one single quote only.
Example..
Column1: who's responsible of this
Column2: who''s responsible of this
With this query
Select regexp_substr(column1,'''') from ex_tab
will always consider colum2 that have one single quote but actually it has 2
I want to select only one single quote not the doubles
I cannot use instr function because i might have who's responsible of the's
Um, you can certainly use INSTR to do this:
with str as (select 'who''s in charge?' col1 from dual union all
select 'who''''s in charge?' col1 from dual union all
select 'who''s in charge? I''m in charge!' col1 from dual union all
select 'who is in charge?' col1 from dual union all
select 'who''''s in charge? I''''m in charge!' from dual)
select col1,
case when instr(col1, '''''', 1) != 0 then 'no' else 'yes' end is_ok
from str;
COL1 IS_OK
--------------------------------- -----
who's in charge? yes
who''s in charge? no
who's in charge? I'm in charge! yes
who is in charge? yes
who''s in charge? I''m in charge! no
It'll most likely be faster than the regexp way.
You can negate characters in set using [^].
Some manual. I just noticed that there is no info there that ^ sign as first character in square brackets means "not in this set".
So regexp like:
^('?([^']+'[^']+'?)?)*$
should match everything that do not have two quotes one after another in it. And match single quote. And match strings that have more than one quote, separated by another string.
So, to put it as complete example, what Your want wloud be:
Select
column1,
REGEXP_COUNT(column1,'^(''?([^'']+''[^'']+''?)?)*$')
from
(
select 'who''''s responsible of this' as column1 FROM dual
union
select 'who''s responsible of this' as column1 FROM dual
union
select 'who''s responsible ''of'' this' as column1 FROM dual
union
select '''who''s responsible ''of'' this' as column1 FROM dual
union
select '''' as column1 FROM dual
)
Where is_ok column indicates wether the value contains two quotes, or not.
EDIT:
I just realized that there is much simpler solution. Just check if there are two or more occurances of quotes one after another. So, regexp would be just:
'{2,}
and here is working example, be aware of changing name of column to is_not_ok:
Select
column1,
REGEXP_COUNT(column1,'''{2,}') as is_not_ok
from
(
select 'who''''s responsible of this' as column1 FROM dual
union
select 'who''s responsible of this' as column1 FROM dual
union
select 'who''s responsible ''of'' this' as column1 FROM dual
union
select '''who''s responsible ''of'' this' as column1 FROM dual
union
select '''' as column1 FROM dual
union
select 'this''' as column1 FROM dual
union
select '''''' as column1 FROM dual
)

Oracle: elegant way to parse a number to 9,99 format

With Oracle 11g I want to parse a number to strip decimals if their value is 0 and keep two decimal figure after the decimal separator ',' if the value of decimals is different from 0
Example:
1,00 -> 1
1,001 -> 1
0,203 -> 0,20
And so on.
I've obtained something like that in a very unelegant way
select replace(trim(to_char (trunc ('0,2345',2),'9999999990.99')), '.', ',')
from dual
Do you know more elegant way? The output should be a char (not number).
Not sure it's much more elegant, but assuming your replace is to deal with different locales, this might work for you:
with t as (
select 1.00 as n from dual
union all select 1.001 from dual
union all select 0.203 from dual
union all select 0.2345 from dual
union all select 112.999 from dual
)
select n, regexp_replace(to_char(trunc(n, 2), '9999999990D00',
'NLS_NUMERIC_CHARACTERS='',.'''), '[,.]00$', null) as new_n
from t;
N NEW_N
---------- --------------
1 1
1.001 1
0.203 0,20
0.2345 0,23
112.999 112,99
The nls_param argument to to_char let's you dictate whether it used a comma or a period as the decimal separator. If you can set that at session level then the query looks a bit simpler. The regexp_replace strips ,00 (or .00, which come to think of it is overkill) from the end of th string.
As ThinkJet noted the regexp_replace is a bit excessive, and since the decimal seperator is defined in the column clase (and the format has no group separators anyway) it can be done with a plan replace:
with t as (
select 1.00 as n from dual
union all select 1.001 from dual
union all select 0.203 from dual
union all select 0.2345 from dual
union all select 112.999 from dual
union all select 13.08 from dual
)
select n, replace(trim(
to_char(trunc(n, 2), '9999999990D00', 'NLS_NUMERIC_CHARACTERS='',.''')),
',00', null) as new_n
from t;
N NEW_N
---------- --------------
1 1
1.001 1
0.203 0,20
0.2345 0,23
112.999 112,99
13.08 13,08
Still not sure this can be described as 'elegant' though.
To achieve correct results you must deal with numbers, not strings:
with t as (
select 1.00 as n from dual
union all select 1.001 from dual
union all select 0.203 from dual
union all select 0.2345 from dual
union all select 112.999 from dual
union all select 112.105 from dual
union all select 0 from dual
union all select -12.307 from dual
)
select
n,
decode( trunc(n,2) - trunc(n) ,
0, to_char(trunc(n), 'TM9', 'NLS_NUMERIC_CHARACTERS = '', '''),
to_char(trunc(n,2),'9999999990D00', 'NLS_NUMERIC_CHARACTERS = '', ''')
)
string_val
from t
SQLFiddle
P.S. Updated to get incorrect truncation instead of round, as in OP request.
Another variant
SELECT n,
to_char( trunc( n, 2 ),
case when mod( trunc( n, 2 ), 1 ) = 0
then '9990'
else '9990D00'
end, 'NLS_NUMERIC_CHARACTERS='',.''' ) val
from t
;
SQLFiddle demo

ORACLE SQL | Modifying data in ORDER BY

following structure in a ORACLE table:
FILE_NAME
-----------
12345_l.tif
12345_m.tif
12345_r.tif
12345_x.tif
12345_y.tif
Need the following result:
First *_m*
Then *_l*
Then *_r*
Then * (everything else)
Trying with:
SELECT FILE_NAME FROM TABLE
WHERE FILE_NAME LIKE '12345%'
ORDER BY regexp_replace(FILE_NAME, '_m', '_1'),
regexp_replace(FILE_NAME, '_l', '_2'),
regexp_replace(FILE_NAME, '_r', '_3')
But this gives me a wrong result.
Anybody with a hint?
TIA Matt
Change your ORDER BY to order it by a numeric:
ORDER BY regexp_replace(FILE_NAME, '_m', 1),
regexp_replace(FILE_NAME, '_l', 2),
regexp_replace(FILE_NAME, '_r', 3);
e.g.
WITH t
AS (SELECT '12345_l.tif' AS file_name FROM dual
UNION
SELECT '12345_m.tif' FROM dual
UNION
SELECT '12345_r.tif' FROM dual
UNION
SELECT '12345_x.tif' FROM dual
UNION
SELECT '12345_y.tif' FROM dual)
SELECT file_name
FROM t
ORDER BY regexp_replace(FILE_NAME, '_m', 1),
regexp_replace(FILE_NAME, '_l', 2),
regexp_replace(FILE_NAME, '_r', 3);
Gives:
==============
12345_m.tif
12345_l.tif
12345_r.tif
12345_x.tif
12345_y.tif
Hope it helps...
Alternatively you could use:
ORDER BY (CASE SUBSTR(file_name, INSTR(file_name, '_')+1, 1)
WHEN 'm' THEN 1
WHEN 'l' THEN 2
WHEN 'r' THEN 3
ELSE 4
END) ASC;
E.G.:
WITH t
AS (SELECT '12345_l.tif' AS file_name FROM dual
UNION
SELECT '12345_y.tif' FROM dual
UNION
SELECT '12345_r.tif' FROM dual
UNION
SELECT '12345_x.tif' FROM dual
UNION
SELECT '12345_m.tif' FROM dual)
SELECT file_name
FROM t
ORDER BY (CASE SUBSTR(file_name, INSTR(file_name, '_')+1, 1)
WHEN 'm' THEN 1
WHEN 'l' THEN 2
WHEN 'r' THEN 3
ELSE 4
END) ASC;
Gives:
12345_m.tif
12345_l.tif
12345_r.tif
12345_x.tif
12345_y.tif

Resources