How can I determine if a string is numeric in SQL? - sql

In a SQL query on Oracle 10g, I need to determine whether a string is numeric or not. How can I do this?

You can use REGEXP_LIKE:
SELECT 1 FROM DUAL
WHERE REGEXP_LIKE('23.9', '^\d+(\.\d+)?$', '')

You ca try this:
SELECT LENGTH(TRIM(TRANSLATE(string1, ' +-.0123456789', ' '))) FROM DUAL
where string1 is what you're evaluating. It will return null if numeric. Look here for further clarification

I don't have access to a 10G instance for testing, but this works in 9i:
CREATE OR REPLACE FUNCTION is_numeric (p_val VARCHAR2)
RETURN NUMBER
IS
v_val NUMBER;
BEGIN
BEGIN
IF p_val IS NULL OR TRIM (p_val) = ''
THEN
RETURN 0;
END IF;
SELECT TO_NUMBER (p_val)
INTO v_val
FROM DUAL;
RETURN 1;
EXCEPTION
WHEN OTHERS
THEN
RETURN 0;
END;
END;
SELECT is_numeric ('333.5') is_numeric
FROM DUAL;
I have assumed you want nulls/empties treated as FALSE.

As pointed out by Tom Kyte in http://asktom.oracle.com/pls/apex/f?p=100:11:0::::P11_QUESTION_ID:7466996200346537833, if you're using the built-in TO_NUMBER in a user defined function, you may need a bit of extra trickery to make it work.
FUNCTION is_number(x IN VARCHAR2)
RETURN NUMBER
IS
PROCEDURE check_number (y IN NUMBER)
IS
BEGIN
NULL;
END;
BEGIN
PRAGMA INLINE(check_number, 'No');
check_number(TO_NUMBER(x);
RETURN 1;
EXCEPTION
WHEN INVALID_NUMBER
THEN RETURN 0;
END is_number;
The problem is that the optimizing compiler may recognize that the result of the TO_NUMBER is not used anywhere and optimize it away.
Says Tom (his example was about dates rather then numbers):
the disabling of function inlining will make it do the call to
check_date HAS to be made as a function call - making it so that the
DATE has to be pushed onto the call stack. There is no chance for the
optimizing compiler to remove the call to to_date in this case. If the
call to to_date needed for the call to check_date fails for any
reason, we know that the string input was not convertible by that date
format.

Here is a method to determine numeric that can be part of a simple query, without creating a function. Accounts for embedded spaces, +- not the first character, or a second decimal point.
var v_test varchar2(20);
EXEC :v_test := ' -24.9 ';
select
(case when trim(:v_test) is null then 'N' ELSE -- only banks, or null
(case when instr(trim(:v_test),'+',2,1) > 0 then 'N' ELSE -- + sign not first char
(case when instr(trim(:v_test),'-',2,1) > 0 then 'N' ELSE -- - sign not first char
(case when instr(trim(:v_test),' ',1,1) > 0 then 'N' ELSE -- internal spaces
(case when instr(trim(:v_test),'.',1,2) > 0 then 'N' ELSE -- second decimal point
(case when LENGTH(TRIM(TRANSLATE(:v_test, ' +-.0123456789',' '))) is not null then 'N' ELSE -- only valid numeric charcters.
'Y'
END)END)END)END)END)END) as is_numeric
from dual;

I found that the solution
LENGTH(TRIM(TRANSLATE(string1, ' +-.0123456789', ' '))) is null
allows embedded blanks ... it accepts "123 45 6789" which for my purpose is not a number.
Another level of trim/translate corrects this. The following will detect a string field containing consecutive digits with leading or trailing blanks such that to_number(trim(string1)) will not fail
LENGTH(TRIM(TRANSLATE(translate(trim(string1),' ','X'), '0123456789', ' '))) is null

For integers you can use the below. The first translate changes spaces to be a character and the second changes numbers to be spaces. The Trim will then return null if only numbers exist.
TRIM(TRANSLATE(TRANSLATE(TRIM('1 2 3d 4'), ' ','#'),'0123456789',' ')) is null

Related

PL/SQL LOOP - Return a row with mixed capital letters

I know this question probably has an easy answer, but I can't get my head around it.
I'm trying to, inside a loop, return a string (in the SQL output) with mixed capital and non-capital letters.
Example: If a name in the row is John Doe, the output will print JoHn DoE, or MiXeD CaPiTaL.
This is my code (which I know is poor written but I need to use the cursor!):
declare
aa_ VARCHAR2(2000);
bb_ NUMBER:=0;
cc_ NUMBER:=0;
CURSOR cur_ IS
SELECT first_name namn, last_name efternamn FROM person_info
;
begin
FOR rec_ IN cur_ LOOP
dbms_output.put_line(rec_.namn);
FOR bb_ IN 1.. LENGTH(rec_.namn) LOOP
dbms_output.put(UPPER(SUBSTR(rec_.namn,bb_,1)));
cc_ := MOD(bb_,2);
IF cc_ = 0 THEN
dbms_output.put(UPPER(SUBSTR(rec_.namn,cc_,1)));
ELSE
dbms_output.put(LOWER(SUBSTR(rec_.namn,2)));
END IF;
end loop;
dbms_output.new_line;
end loop;
end;
Again, I know the code is really bad but yeah, trying to learn!
Thanks in advance :)
You may use plain SQL for this purpose, without any loop:
Split input text by pairs separated with some special character (that doesn't appear in the text).
Use initcap SQL function to turn each first letter to upper case.
Remove the special separator.
with a as (
select 'John Doe' as a
from dual
union all
select 'mixed capital and non-capital letters'
from dual
)
select
replace(
initcap(
/*Convert case*/
regexp_replace(a, '([a-zA-Z]{2})',
/*Add ASCII nul after each two letters*/
'\1' || chr(0)
)
),
/*Remove ASCII nul to revert changes*/
chr(0)
) as mixed_case
from a
| MIXED_CASE |
| :------------------------------------ |
| JoHn DoE |
| MiXeD CaPiTaL AnD NoN-CaPiTaL LeTtErS |
db<>fiddle here
I'd put the text transformation into a function, rather than including all the logic in the body of the loop.
declare
cursor c_people is
select 'John' as first_name, 'Doe' as last_name from dual union all
select 'Mixed', 'Capitals'
from dual;
function mixCaps(inText varchar2) return varchar2
is
letter varchar2(1);
outText varchar2(4000);
begin
for i in 1..length(inText) loop
letter := substr(inText,i,1);
outText := outText ||
case mod(i,2)
when 0 then lower(letter)
else upper(letter)
end;
end loop;
return outText;
end mixCaps;
begin
for person in c_people loop
dbms_output.put_line(mixCaps(person.first_name|| ' ' || person.last_name));
end loop;
end;
If performance was critical and you had large numbers of values, you might consider inlining the function using pragma inline (but then you wouldn't be using dbms_output anyway).
For learning purpose you can use code below (it is not efficient it is for learning of oracle features)
Steps :
split word on letters using connect by level
get Nth (level) occurence of one letter ('.?') from word using reg exp
convert to upper case every 2nd letter
concatenate back using list agg and sorting by letter number
used here function in with so you can apply it to any sql table
with
function mixed(iv_name varchar2) return varchar2 as
l_result varchar2(4000);
begin
with src_letters as
(select REGEXP_SUBSTR(iv_name, '.?', level) as letter
,level lvl
from dual
connect by level <= length(iv_name)),
mixed_letters as
(select case
when mod(lvl, 2) = 0 then
letter
else
upper(letter)
end as letter
,lvl
from src_letters
order by lvl)
select listagg(letter) within group(order by lvl)
into l_result
from mixed_letters;
return l_result;
end;
select mixed('text') from dual

Postgresql: CASE statement inside STRING_AGG() loops forever

I am trying to execute a (postgresql) query the auto-generates the INSERT statement for a table dynamically, for test data. I am using a SWITCH CASE statement to decide which value to return based on the column's type.
Anyone know why the following SQL query seems to execute indefinitely (or at least, for a very, very long time).
select STRING_AGG(
CASE
WHEN (udt_name = 'timestamptz' OR udt_name = 'timestamp' OR udt_name = 'date') THEN
'1990-02-02'
WHEN (udt_name = 'text') THEN
--text value as long as length of column
schema_a.make_text_value(character_maximum_length) -- returns value instantly
WHEN (udt_name = 'numeric') THEN
null
WHEN (udt_name = 'bit') THEN
null
WHEN (udt_name = 'bool') THEN
null
WHEN (udt_name = 'int4') THEN
null
ELSE null
END
, ','
) cols
FROM INFORMATION_SCHEMA.COLUMNS col2
WHERE TABLE_SCHEMA='schema_a'
AND col2.TABLE_NAME='big_event' -- big_event has 4 columns
LIMIT 1
The stored procedure called is as follows:
CREATE OR REPLACE FUNCTION schema_a.make_text_value(loopMax integer)
RETURNS TEXT AS $$
DECLARE
loopCounter INTEGER := 1;
val TEXT := '';
BEGIN
LOOP
EXIT WHEN loopCounter = loopMax ;
val := val || 'A';
loopCounter := loopCounter + 1;
END LOOP;
-- CONCAT('version_id' , CAST(idCounter AS TEXT)),
-- '2020-08-28 13:25:00.123456'
RETURN val;
END;
$$
LANGUAGE plpgsql VOLATILE
If loopMax is NULL, it can never be equal to loopCounter. So now let's look at the definition for INFORMATION_SCHEMA.Columns.character_maximum_length:
If data_type identifies a character or bit string type, the declared maximum length; null for all other data types or if no maximum length was declared.
(emphasis mine)
It certainly seems possible for this value to be NULL, based on that definition. Of course, you know your own schema, so you might be able to show this never happens... but I'd triple check to be sure, and maybe add a well-considered guard around the loopMax value anyway; you probably don't want to concatenate two billion strings in that situation.

varchar number starting with +

I am trying below to find out if the column (dis_num) value is numeric or not which is working fine.
REGEXP_LIKE(dis_num, '^[[:digit:]]+$')
Now dis_num column can starts with + and then numbers like +8143434344. How to modify above regex which is consider starting with + as well ? Means if column has number starting with + then also we need to consider as numeric.
Thanks
If you want to check for a literal + sign you can escape it; and can make it optional with ?
REGEXP_LIKE(dis_num, '^\+?[[:digit:]]+$')
Very quick demo:
with t (dis_num) as (
select '1234' from dual
union all select 'abc' from dual
union all select '+8143434344' from dual
)
select dis_num,
case when REGEXP_LIKE(dis_num, '^[[:digit:]]+$') then 'Yes' else 'No' end as check1,
case when REGEXP_LIKE(dis_num, '^\+?[[:digit:]]+$') then 'Yes' else 'No' end as check2
from t;
DIS_NUM CHE CHE
----------- --- ---
1234 Yes Yes
abc No No
+8143434344 No Yes
AS Littlefoot says you need to replace + then evaluate the string.
You can also uses TRANSLATE, I think it's pretty useful to create a function in any oracle DB that you can call anywhere like this:
FUNCTION only_numbers(p_value VARCHAR2) RETURN VARCHAR2 IS
BEGIN
RETURN(TRANSLATE(p_value , '1' || TRANSLATE(p_value , 'a1234567890', 'a'), '1'));
END only_numbers;
SELECT only_numbers(dis_num) FROM your_table should work too.
Remove + first, then check it:
REGEXP_LIKE(replace(dis_num, '+', ''), '^[[:digit:]]+$')
-------------------------

Procedure to apply formatting to all rows in a table

I had a SQL procedure that increments through each row and and pads some trailing zeros on values depending on the length of the value after a decimal point. Trying to carry this over to a PSQL environment I realized there was a lot of syntax differences between SQL and PSQL. I managed to make the conversion over time but I am still getting a syntax error and cant figure out why. Can someone help me figure out why this wont run? I am currently running it in PGadmin if that makes any difference.
DO $$
DECLARE
counter integer;
before decimal;
after decimal;
BEGIN
counter := 1;
WHILE counter <> 2 LOOP
before = (select code from table where ID = counter);
after = (SELECT SUBSTRING(code, CHARINDEX('.', code) + 1, LEN(code)) as Afterward from table where ID = counter);
IF before = after
THEN
update table set code = before + '.0000' where ID = counter;
ELSE
IF length(after) = 1 THEN
update table set code = before + '000' where ID = counter;
ELSE IF length(after) = 2 THEN
update table set code = before + '00' where ID = counter;
ELSE IF length(after) = 3 THEN
update table set code = before + '0' where ID = counter;
ELSE
select before;
END IF;
END IF;
counter := counter + 1;
END LOOP
END $$;
Some examples of the input/output of the intended result:
Input 55.5 > Output 55.5000
Input 55 > Output 55.0000
Thanks for your help,
Justin
There is no need for a function or even an update on the table to format values when displaying them.
Assuming the values are in fact numbers stored in a decimal or float column, all you need to do is to apply the to_char() function when retrieving them:
select to_char(code, 'FM999999990.0000')
from data;
This will output 55.5000 or 55.0000
The drawback of the to_char() function is that you need to anticipate the maximum number of digits of that can occur. If you have not enough 9 in the format mask, the output will be something like #.###. But as too many digits in the format mask don't hurt, I usually throw a lot into the format mask.
For more information on formatting functions, please see the manual: https://www.postgresql.org/docs/current/static/functions-formatting.html#FUNCTIONS-FORMATTING-NUMERIC-TABLE
If you insist on storing formatted data, you can use to_char() to update the table:
update the_table
set code = to_char(code::numeric, 'FM999999990.0000');
Casting the value to a number will of course fail if there a non-numeric values in the column.
But again: I strong recommend to store numbers as numbers, not as strings.
If you want to compare this to a user input, it's better to convert the user input to a proper number and compare that to the (number) values stored in the database.
The string matching that you are after doesn't actually require a function either. Using substring() with a regex will do that:
update the_table
set code = code || case length(coalesce(substring(code from '\.[0-9]*$'), ''))
when 4 then '0'
when 3 then '00'
when 2 then '000'
when 1 then '0000'
when 0 then '.0000'
else ''
end
where length(coalesce(substring(code from '\.[0-9]*$'), '')) < 5;
substring(code from '\.[0-9]*$') extracts everything the . followed by numbers that is at the end of the string. So for 55.0 it returns .0 for 55.50 it returns .50 if there is no . in the value, then it returns null that's why the coalesce is needed.
The length of that substring tells us how many digits are present. Depending on that we can then append the necessary number of zeros. The case can be shortened so that not all possible length have to be listed (but it's not simpler):
update the_table
set code = code || case length(coalesce(substring(code from '\.[0-9]*$'), ''))
when 0 then '.0000'
else lpad('0', 5- length(coalesce(substring(code from '\.[0-9]*$'), '')), '0')
end
where length(coalesce(substring(code from '\.[0-9]*$'), '')) < 5;
Another option is to use the position of the . inside the string to calculate the number of 0 that need to be added:
update the_table
set code =
code || case
when strpos(code, '.') = 0 then '0000'
else rpad('0', 4 - (length(code) - strpos(code, '.')), '0')
end
where length(code) - strpos(code, '.') < 4;
Regular expressions are quite expensive not using them will make this faster. The above will however only work if there is always at most one . in the value.
But if you can be sure that every value can be cast to a number, the to_char() method with a cast is definitely the most robust one.
To only process rows where the code columns contains correct numbers, you can use a where clause in the SQL statement:
where code ~ '^[0-9]+(\.[0-9][0-9]?)?$'
To change the column type to numeric:
alter table t alter column code type numeric

Using between operator for string which stores numbers

I have a column in which numbers are stored as string because of the nature of the column where any kind of data type is expected like date, numbers, alpha numeric,
etc.
Now i need to check if the values in that column is in defined range or not here is sample data for testing
create table test (val varchar2(10));
insert into test values ('0');
insert into test values ('67');
insert into test values ('129');
insert into test values ('200');
insert into test values ('1');
Here expected range in which value should be is 0-128 if values are not in range then i need to filter them out for further processing.
For this i have written some queries but none of then is giving requires output.
select *
from test
where val not between '0' and '128';
select *
from test
to_number(val, '9') not between to_number('0', '9') and to_number('128', '9999');
select * from test where
to_number(val, '9') < TO_NUMBER('0', '9')
or
to_number(val, '999') > TO_NUMBER('128', '999')
;
These above queries are producing desired output !! :(
I ma using DB version --
Oracle Database 12c Enterprise Edition Release 12.1.0.2.0 - 64bit Production
Just leave the format out of to_number():
select *
from test
where to_number(val) not between to_number('0') and to_number('128');
The numeric format is needed for conversion to a character. If you pass it in to to_number(), then it expects a number of that format -- and you might get the number of digits wrong.
Or, better yet:
select *
from test
where to_number(val) not between 0 and 128;
Or, even better yet, change the column to contain a number rather than a string.
EDIT:
If the problem is that your value is not a number (which is quite different from your original question), then test for that. This is one situation where case is appropriate in the where clause (because case guarantees the order of evaluation of its arguments:
where (case when regexp_like(val, '[^-0-9]') then 'bad'
when cast(val as number) < 0 then 'bad'
when cast(val as number) > 128 then 'bad'
else 'good'
end) = 'bad'
#GordonLinoff's answer works with the sample data you've shown, but it will error with ORA-01722 "invalid number" if you have any values which do no represent numbers. Your sample data only has good values, but you said that for your real field "any kind of data type is expected like date, numbers, alpha numeric, etc."
You can get around that with a function that attempts to convert the stored string value to a number, and returns null if it gets that exception. A simple example:
create function safe_to_number (p_str varchar2) return number is
begin
return to_number(p_str);
exception
when value_error then
return null;
end;
/
You can then do
select *
from test
where safe_to_number(val) not between 0 and 128;
VAL
----------
129
200
Anything that can't be converted and causes an ORA-06502 value-error exception will be seen as null, which is neither between nor not between any values you supply.
If you need to check date ranges you can do something similar, but there are more errors possible, and you may have dates in multiple formats; you would need to declare exceptions and initialise them to known error numbersto catch the ones you expect to see. This isn't complete, but you could start with something like:
create function safe_to_date (p_str varchar2) return date is
l_formats sys.odcivarchar2list;
format_ex_1 exception;
format_ex_2 exception;
format_ex_3 exception;
format_ex_4 exception;
format_ex_5 exception;
pragma exception_init(format_ex_1, -1840);
pragma exception_init(format_ex_2, -1841);
pragma exception_init(format_ex_3, -1847);
pragma exception_init(format_ex_4, -1858);
pragma exception_init(format_ex_5, -1861);
-- add any others you might get
begin
-- define all expected formats
l_formats := sys.odcivarchar2list('YYYY-MM-DD', 'DD/MM/YYYY', 'DD-MON-RRRR'); -- add others
for i in 1..l_formats.count loop
begin
return to_date(p_str, l_formats(i));
exception
when format_ex_1 or format_ex_2 or format_ex_3 or format_ex_4 or format_ex_5 then
-- ignore the exception; carry on and try the next format
null;
end;
end loop;
-- did not match any expected formats
return null;
end;
/
select *
from test
where safe_to_date(val) not between date '2016-02-01' and date '2016-02-29';
Although I wouldn't normally use between for dates; if you don't have any with times specified then you'd get away with it here.
You could use when others to catch any exception without having to declare them all, but even for this that's potentially dangerous - if something is breaking in a way you don't expect you want to know about it, not hide it.
Of course, this is an object lesson in why you should store numeric data in NUMBER columns and dates in DATE or TIMESTAMP fields - trying to extract useful information when everything is stored as strings is messy, painful and inefficient.
I think the best approach you can try in this condition is use
TRANSLATE function to eliminate the alphanumeric characters. Once its
done all now is OLD school technique to check the data by using NOT
BETWEEN function Hope this helps.
SELECT B.NM
FROM
(SELECT a.nm
FROM
(SELECT '0' AS nm FROM dual
UNION
SELECT '1' AS nm FROM dual
UNION
SELECT '68' AS nm FROM dual
UNION
SELECT '129' AS nm FROM dual
UNION
SELECT '200' AS nm FROM dual
UNION
SELECT '125a' AS nm FROM dual
)a
WHERE TRANSLATE(a.nm, ' +-.0123456789', ' ') IS NULL
)b
WHERE b.nm NOT BETWEEN 1 AND 128;