Find invalid numbers - Oracle SQL - sql

I have a table:
table1
column_name value
Test1 80933Y355220
Test1 8093363X5220
Test1 809336355220
Test1 80.98
Test1 -80.98
Test2 1234
Test2 abc
Test2 12-23-09
I need to identify all the invalid number values present in the column value
Output should be:
column_name value
Test1 80933Y355220
Test1 8093363X5220
Test2 abc
Test2 12-23-09
Is there a way to get this list?

The common way to solve this is with a user-defined function:
create or replace function is_number (p_str in varchar2) return varchar2 is
n number;
rv varchar2(1);
begin
begin
n := to_number(p_str);
rv := 'Y';
exception
when others then
rv := 'N';
end;
return rv;
end;
/
select column_name, value, is_number(value) as is_number
from table1
where is_number(value) = 'N'
/
In Oracle 12c R2 we acquired a built-in function to do it: validate_conversion() which handles lots of data types. Find out more.
select column_name
, value
from table1
where validate_conversion(value as number) = 0
/
Here is the inevitable demo on db<>fiddle.
Regex solutions are tricky to get right. It's easy to formulate expressions which exclude valid numbers (such as -80.98) or include non-numbers (such as (12-34-56). Even we do get that bit right (as Gordon's solution does) what happens if we want to allow decimal separators or currency symbols? Or scientific notation? That's a messy regex. It's easy to extend a UDF to handle a format mask and validate_conversion() does it natively:
SELECT VALIDATE_CONVERSION('$100,00' AS NUMBER,
'$999D99', 'NLS_NUMERIC_CHARACTERS = '',.''')
FROM DUAL;

I would use regexp_like() with this pattern:
SELECT column_name, value
FROM table1
WHERE NOT REGEXP_LIKE(value, '^[-]?[0-9]*[.]?[0-9]*$')

Just to give an Oracle 12.1 answer, it doesn't have validate_conversion or the on conversion error clause of to_date, but it does support PL/SQL functions in a WITH clause, allowing something like this:
with function is_number(str varchar2) return varchar2
as
n number;
begin
n := str; return 'Y';
exception
when others then return 'N';
end;
select d.column_name
, d.value
from demo d
where is_number(d.value) = 'N'

Related

How can I declare constant in PL SQL without writing procedure in "begin"?

I would like to do something like that:
DECLARE MY_DATE CONSTANT DATE := DATE'2022-07-01';
BEGIN null; end;
SELECT MY_DATE FROM dual
or
DECLARE MY_DATE CONSTANT DATE := DATE'2022-07-01';
BEGIN null; end;
SELECT CASE WHEN sysdate > MY_DATE THEN 1 ELSE 0 END FROM dual
But I am getting error. How can I fix it?
NEW:
From the comment I see you're declaring a constant within a package and your aim is to use it in an SQL statement outside of package.
You need a function to return that constant.
NOTE: I wouldn't recommend to store constants in a package and use it in SQL. I'd prefer to store them in a special table because of overhead you'll get when adding or removing constants. And performance issues are to be expected as well because of context switching while using PLSQL constants in SQL statements.
However, answering your question, here's the example for a package having multiple constants:
CREATE OR REPLACE PACKAGE MYCONSTANTS
IS
MY_DATE CONSTANT DATE := DATE'2022-07-01';
MY_CHAR CONSTANT VARCHAR2(10) := 'ABCD';
function get_my_date return date;
function get_my_char return varchar2;
END;
/
create or replace PACKAGE body MYCONSTANTS IS
function get_my_date return date
is
begin
return my_date;
end;
function get_my_char return varchar2
is
begin
return MY_CHAR;
end;
END;
/
And the you can say:
select MYCONSTANTS.get_my_date from dual;
select MYCONSTANTS.get_my_char from dual;
OLD:
As far as I can see you don't need PL/SQL but just SQL.
The PLSQL is what you would have between "begin" and "end". But in your example, you have just null there.
So, all you need is "define"
def my_date = date '2022-07-01';
begin
null;
end;
/ -- you need this to start execution of an pl/sql block
select &my_date from dual; -- be aware you need to use an "&" before var name
If you're on a recent version you can declare a local function in a CTE:
WITH
FUNCTION MY_DATE RETURN DATE IS
BEGIN
RETURN DATE '2022-07-01';
END;
SELECT MY_DATE FROM dual
Or you can use a normal CTE:
WITH cte (MY_DATE) AS (
SELECT DATE '2022-07-01' FROM DUAL
)
SELECT MY_DATE FROM cte
db<>fiddle

Oracle SQL: How to replace within all members of a list

Using Oracle SQL...
I'd like to use the replace function on all values of a hardcoded list. Something like this...
WHERE (
(field IN replace(('PREFIX-ITEM0','PREFIX-ITEM1','PREFIX-ITEM2','PREFIX-ITEM3','PREFIX-ITEM4','PREFIX-ITEM5','PREFIX-ITEM6','PREFIX-ITEM7','PREFIX-ITEM8','PREFIX-ITEM9'),'PREFIX-','NEWBIE-'))
)
Is there any way to do this?
If you have a fixed list of values - so they aren't coming from a table - then it would be much simpler to modify them externally before putting them into a query.
But if for some reason you can't do that, you could treat the list as a varray of strings, and use a function to replace them individually. In recent versions of Oracle you can do that with a local function defined in a with clause:
with
function myfunc (p_in_list sys.odcivarchar2list, p_old varchar2, p_new varchar2)
return sys.odcivarchar2list is
l_out_list sys.odcivarchar2list;
begin
l_out_list := new sys.odcivarchar2list();
l_out_list.extend(p_in_list.count);
for i in p_in_list.first..p_in_list.last loop
l_out_list(i) := replace(p_in_list(i), p_old, p_new);
end loop;
return l_out_list;
end;
select field
from t
where field in (
select * from myfunc(
sys.odcivarchar2list('PREFIX-ITEM0','PREFIX-ITEM1','PREFIX-ITEM2','PREFIX-ITEM3','PREFIX-ITEM4','PREFIX-ITEM5','PREFIX-ITEM6','PREFIX-ITEM7','PREFIX-ITEM8','PREFIX-ITEM9'),
'PREFIX-',
'NEWBIE-')
)
/
which with some dummy data with a mix of PREFIX and NEWBIE values produces:
FIELD
------------
NEWBIE-ITEM2
NEWBIE-ITEM3
NEWBIE-ITEM4
db<>fiddle

How to use listagg parameter in Oracle procedure where clause

I group a series of IDs using LISTAGG.
Here is an example of a returned value-
A 1,2,3,4,5 PROC.ABC
B 6,7,8 PROC.ABC
C 2,3,4 PROC.DEF
I then try to use a cursor and pass each value into the following procedure:
PROCEDURE abc(id_list IN VARCHAR2) IS
BEGIN
UPDATE table_a SET flag = 1 WHERE id IN (id_list);
END;
This errors out ("Invalid Number" error) because id_list is being inserted as
'1,2,3,4,5'
, not
1,2,3,4,5
. How can I get this to work? I would prefer not to use dynamic SQL if possible.
If you have APEX installed in your database, you can use the APEX_STRING.SPLIT function to split your string by commas. If you know id_list is always going to be numbers, you can use APEX_STRING.SPLIT_NUMBERS as well.
PROCEDURE abc (id_list IN VARCHAR2)
IS
BEGIN
UPDATE table_a
SET flag = 1
WHERE id IN (SELECT * FROM TABLE (apex_string.split (id_list, ',')));
END;
Firstly you need to create a function that returns a list of values.
FUNCTION TVF_SPLIT (expression CLOB, delimiter CHAR) RETURN sys.ODCIVARCHAR2LIST
AS
v_TYPE_TABLE_SEPRATOR sys.ODCIVARCHAR2LIST :=sys.ODCIVARCHAR2LIST();
v_xml_data CLOB;
BEGIN
v_xml_data := '<r><n>' || replace(expression,delimiter,'</n><n>') || '</n></r>';
SELECT * BULK COLLECT INTO v_TYPE_TABLE_SEPRATOR FROM
(
SELECT * FROM
XMLTABLE ( '/r/*'
PASSING xmltype(v_xml_data)
COLUMNS
r VARCHAR2(4000) PATH '/n'
) xmlt
);
RETURN v_TYPE_TABLE_SEPRATOR;
END;
Select * from table(TVF_SPLIT('1,2,3,4,5',','));
Don't use listagg; create a collection type.
create or replace type t_vchar_tab as table of integer
Then replace listagg in the query-
CAST(COLLECT(id) AS t_vchar_tab) as id_list
Then the procedure should reference the collection w/use of MEMBER
PROCEDURE abc(id_list t_vchar_tab) IS
BEGIN
UPDATE table_a SET flag = 1 WHERE id MEMBER OF id_list;
END;

String Pattern Matching in Sql / Pl/sql

I need to match a string to a pattern to validate the given string.
The given string could be like this 1234/5678.
I should validate the string in such a way that the first four and the last four characters will have to be numbers and they must be seperated by a slash.
How can I do this in SQL or PL/SQL?
I tried different functions such as REGEXP_LIKE, REGEXP_REPLACE,REGEXP_SUBSTR.
Can anyone please help me on this?
If this needs to be done in PL/SQL (e.g. you're validating user input, rather than data in a table), you can create a function to do the validation, e.g.:
DECLARE
v_str VARCHAR2(10);
FUNCTION validate_string (in_str VARCHAR2) RETURN BOOLEAN
IS
BEGIN
RETURN regexp_like(in_str, '\d{4}/\d{4}');
END validate_string;
PROCEDURE validation_output (in_str VARCHAR2)
IS
BEGIN
IF validate_string (in_str => in_str) THEN
dbms_output.put_line(in_str||': validated');
ELSE
dbms_output.put_line(in_str||': not validated');
END IF;
END validation_output;
BEGIN
v_str := '1234/5678';
validation_output (v_str);
v_str := '12/5678';
validation_output (v_str);
v_str := NULL;
validation_output (v_str);
END;
/
1234/5678: validated
12/5678: not validated
: not validated
if you are using oracle you can user regexp_like
https://www.techonthenet.com/oracle/regexp_like.php
if you are using mysql regexp or rlike
https://dev.mysql.com/doc/refman/5.5/en/regexp.html
for sqlserver IsMatch()
https://github.com/zzzprojects/Eval-SQL.NET/wiki/SQL-Server-Regex-%7C-Use-regular-expression-to-search,-replace-and-split-text-in-SQL#sql-regex---ismatch
ORACLE
SELECT * FROM T WHERE COL REGEXP_LIKE REGULAREXP
MYSQL
SELECT * FROM T WHERE COL RLIKE REGULAREXP
SELECT * FROM T WHERE COL REGEXP REGULAREXP
sample table:
SELECT * FROM ns_98;
4321/4567
43/45
43898/4521
4388/4521
43885/45215
4388///4521
SELECT a
FROM ns_98
WHERE REGEXP_LIKE (a,'^[0-9]{4}/{1}[0-9]{4}$');
output:
4321/4567
4388/4521

How can I know which values are numeric in oracle 9i

I have this database which contains a varchar.
I want to know which records holds numeric values. I tried REGEXP_COUNT and other but I'm running on 9i and I think this is for 10g >
How can I achieve this?
I tried:
select to_number( my_column ) from my_table
But it doesn't work, because well not all of them are numeric.
EDIT
Background.
This table contains employee id's, all of which are numeric ( read 1234 or 24523 or 6655 )
The in the initial database load, when the employee id was unknown instead of using something like -1 they entered texts like:
NA, N/A, NONE, UNK, UNKNOW, TEST, EXTERNAL, WITHOUT_ID
Really the main fault is, that column is varchar and not number as it should.
Now, what I try to do, is to get ll the records that are not numeric ( that don't contain an employee id ) but since that db is 9i, I could not use RegExp
I am afraid you'll have to write your own isnumber function, and then use it, something like this (untested) found in this thread, should work.
DECLARE FUNCTION isNumber(p_text IN VARCHAR2) RETURN NUMBER IS
v_dummy NUMBER;
not_number EXCEPTION;
PRAGMA EXCEPTION_INIT(-, not_number);
BEGIN
v_dummy := TO_NUMBER(p_text);
RETURN 1;
EXCEPTION
WHEN not_number THEN RETURN 0;
END is_number;
After that you could use a decode function combined with your isnumber function to get the results you need.
Just another pure SQL workaround:
select my_column
from my_table
where translate(my_column,'x0123456789','x') is null;
Depends on what you count as 'numeric'. Do you allow negative numbers, decimals or just integers, or scientific notation (eg '1e3'). Are leading zeroes allowed ?
If you just want positive integer values, try
where translate(col,' 1234567890','0') is null
Try this
CREATE OR REPLACE PACKAGE value_tests
AS
FUNCTION get_number( pv_value IN VARCHAR2 ) RETURN NUMBER;
END;
/
CREATE OR REPLACE PACKAGE BODY value_tests
AS
FUNCTION get_number( pv_value IN VARCHAR2 ) RETURN NUMBER
IS
converted_number NUMBER;
invalid_number EXCEPTION;
PRAGMA EXCEPTION_INIT( invalid_number, -01722 );
value_error EXCEPTION;
PRAGMA EXCEPTION_INIT( value_error, -06502 );
BEGIN
<<try_conversion>>
BEGIN
converted_number := TO_NUMBER( pv_value );
EXCEPTION
WHEN invalid_number OR value_error
THEN
converted_number := NULL;
END try_conversion;
RETURN converted_number;
END get_number;
END;
/
Running it on this...
select my_column
, value_tests.get_number( my_column ) my_column_num
from ( select 'mydoghas3legs' my_column from dual
union all select '27.5' my_column from dual
union all select '27.50.5' my_column from dual
)
returns
MY_COLUMN MY_COLUMN_NUM
------------- -------------
mydoghas3legs
27.5 27.5
27.50.5
I do not like using exceptions in normal code but this seems to be the best and safest aproach:
CREATE OR REPLACE FUNCTION "IS_NUMBER" (pX in varchar2) return integer is
n number;
begin
n:=to_number(pX);
return 1;
exception
when others then
return 0;
end;
I manage to work around like this:
select my_column
from my_table
where my_column not like '%1%'
and my_column not like '%2%'
and my_column not like '%3%'
and my_column not like '%4%'
and my_column not like '%5%'
and my_column not like '%6%'
and my_column not like '%7%'
and my_column not like '%8%'
and my_column not like '%9%'
and my_column not like '%0%'
Dirty, but it works. ;)
Yet another approach, here's a function I wrote some time ago:
CREATE OR REPLACE function string_is_numeric
(p_string_in in varchar2)
return boolean is
begin
for i in 1..length(p_string_in) loop
if substr(p_string_in, i, 1) not in ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9') then
return false;
end if;
end loop;
return true;
end;
/
As pointed out in pedromarce's answer, you might need to change that from a boolean return to number or varchar2 to better suit your needs.
Here's my version of the routine, similar to that posted by pedromarce. Note that the posted example doesn't compile due to the "-" exception number. This example compiles and works:
create or replace function IsNumber(
a_Text varchar2
) return char is
t_Test number;
begin
begin
t_Test := to_number(a_Text);
return 'Y';
exception when value_error then
return 'N';
end;
end;
Example usage:
select IsNumber('zzz') from dual;
Result: N
select IsNumber('123.45') from dual;
Result: Y