Oracle string compare in query - sql

I have two string and want to know, if in alphabetically sorted list the string2 is before string1, is the same, or comes after string1.
Something like STRCMP (string1, string2) for MySql.
Is there some kind of built in function, that isn't that popular, or should I use a non-function approach manually comparing each char (if that is even considerable)?
Thanks in advance.

Yes, it is <. You can use boolean comparators to get "alphabetic" ordering. I put that in quotes because it depends on the collation of the strings, but it is usually reasonable.
In a where clause:
where string1 < string2
If you want a value, then this works in both Oracle and MySQL:
select (case when string1 < string2 then 1 else 0 end) as string_compare_flag

You'll need to consider nulls and case.
In PL/SQL you could do something like this...
declare
BEFORE CONSTANT PLS_INTEGER := -1;
AFTER CONSTANT PLS_INTEGER := 1;
SAME CONSTANT PLS_INTEGER := 0;
NULLS_FIRST CONSTANT VARCHAR2(1) := CHR(9);
NULLS_LAST CONSTANT VARCHAR2(1) := CHR(127);
string1 varchar2(100);
string2 varchar2(100);
result pls_integer;
--If this was a function then these would be your input parameters
inputString1 varchar2(100) := 'A'; --try null here
inputString2 varchar2(100) := 'b';
--swap this over if you want nulls first
nullPos VARCHAR2(1) := NULLS_LAST; --try swapping to NULLS_FIRST
begin
string1 := upper(nvl(inputString1,nullPos));
string2 := upper(nvl(inputString2,nullPos));
result := case when string1 < string2 then BEFORE
when string1 = string2 then SAME
else AFTER end;
dbms_output.put_line(result);
end;
The values chosen for NULLS_FIRST and NULLS_LAST are just arbitrary but outside of the standard a-z A-Z so that a null value will get placed either before or after a non null one, and so two null values will return 0 (same).
If you aren't bothered about case i.e A comes after b then you can ignore the upper function code.
You could put the plsql code (or similar) in a function and call it from your SQL or you could do
SELECT case when upper(nvl(string1,CHR(127))) < upper(nvl(string2,CHR(127))) then -1
when upper(nvl(string1,CHR(127))) = upper(nvl(string2,CHR(127))) then 0
else 1
end
...

Related

Oracle. Not valid ascii value of regex result

I'd like to edit a string. Get from 2 standing nearby digits digit and letter (00 -> 0a, 01 - 0b, 23-> 2c etc.)
111324 -> 1b1d2e.
Then my code:
set serveroutput on size unlimited
declare
str varchar2(128);
function convr(num varchar2) return varchar2 is
begin
return chr(ascii(num)+49);
-- return chr(ascii(num)+49)||'<-'||(ascii(num)+49)||','||ascii(num)||','||num||'|';
end;
function replace_dd(str varchar2) return varchar2 is
begin
return regexp_replace(str,'((\d)(\d))','\2'||convr('\3'));
end;
begin
str := '111324';
Dbms_Output.Put_Line(str);
Dbms_Output.Put_Line(replace_dd(str));
end;
But I get the next string: '112'.
When I checked result by commented return string I'v got:
'1<-141,92,1|1<-141,92,3|2<-141,92,4|'.
ascii(num) does not depend on num. It always works like ascii('\'). It is 92, plus 49 we got 141 and it is out of ascii table. But num by itself is printed correctly.
How can I get correct values? Or maybe another way to resolve this issue?
What is happening is that the replacement string is expanded first, and only after it is fully processed, any remaining backreferences like \2 are replaced by string fragments. So convr('\3') is processed first, and at this stage '\3' is a literal. ascii() returns the ascii code of the FIRST character of whatever string it receives as argument. So the 3 plays no role, you only get ascii('\') as you noticed. Then your user-defined function is evaluated and plugged back into the concatenation... by now there is no \3 left in the replacement string.
Exercise: Try to explain/understand why
regexp_replace('abcdef', '(b).*(e)', '\2' || upper('\1'))
is aebf and not aeBf. (Hint: what is the return from upper('\1') by itself, unrelated to anything else?)
You could split the input string into component characters, apply your transformation on those with even index and combine the string back (all in SQL, no need for loops and such). Something like this (done in plain SQL, you can rewrite it into your function if you like):
with
inputs ( str ) as (
select '111324' from dual union all
select '372' from dual
),
singletons ( str, idx, ch ) as (
select str, level, substr(str, level, 1)
from inputs
connect by level <= length(str)
and prior str = str
and prior sys_guid() is not null
)
select str,
listagg(case mod(idx, 2) when 1 then ch else chr(ascii(ch)+49) end, '')
within group (order by idx)
as modified_str
from singletons
group by str
;
STR MODIFIED_STR
------ --------------
111324 1b1d2e
372 3h2
Here code adds 5 to a single letter and resolve the isssue.
set serveroutput on size unlimited
declare
str varchar2(128);
str1 varchar2(128);
function replace_a(str varchar2) return varchar2 is
begin
return regexp_replace(str,'(\D)','5\1');
end;
function convr(str varchar2) return varchar2 is
ind number;
ret varchar2(128);
begin
Dbms_Output.Put_Line(str);
--return chr(ascii(num)+49)||'<-'||(ascii(num)+49)||','||ascii(num)||','||num||'|';
ind := 1 ;
ret :=str;
loop
ind := regexp_instr(':'||ret,'(#\d#)',ind) ;
exit when ind=0;
Dbms_Output.Put_Line(ind);
ret := substr(ret,1,ind-2)||chr(ascii(substr(ret,ind,1))+49)||substr(ret,ind+2);
SYS.Dbms_Output.Put_Line(ret);
end loop;
return ret;
end;
function replace_dd(str varchar2) return varchar2 is
begin
return convr(regexp_replace(str,'((\d)(\d))','\2#\3#'));
end;
begin
str := '11a34';
Dbms_Output.Put_Line(str);
Dbms_Output.Put_Line(replace_a(str));
Dbms_Output.Put_Line(replace_dd(replace_a(str)));
end;
result:
11a34
115a34
1#1#5a3#4#
3
1b5a3#4#
7
1b5a3e
1b5a3e

recursive permutation algorithm in plsql

I'm trying to run a recursive procedure that permutates a given string.
It's compiling on sqldeveloper but when I try to run with input its giving me ora-06502: numeric or value errors on line 13 (the prefix assignment)
create or replace
procedure print_anagrams
(pre in varchar2, str in varchar2)
is
prefix varchar2(30);
stringg varchar2(30);
strlen number;
begin
strlen := length(str);
if strlen = 0 then
dbms_output.put_line(pre);
else
for i in 1..strlen loop
prefix := pre || SUBSTR(str,i,1);
stringg := SUBSTR(str,1,i) || SUBSTR(str,i+1,strlen);
print_anagrams(prefix,stringg);
end loop;
end if;
end;
There were two problems:
Firstly, the LENGTH function returns NULL if its parameter is NULL, not 0, so the following condition in your code was never true (because strlen is NULL):
if strlen = 0 then
You were getting the ora-06502: numeric or value errors error, because, when the str argument was empty, the upper range limit of the FOR LOOP was NULL (because strlen is NULL):
for i in 1..NULL loop
And this yields:
ora-06502: numeric or value errors
Secondly, the last parameter of the substr function in Oracle has different meaning than String's substring method in Java. In Oracle, that parameter means "how many characters should be returned", whereas in Java it stands for "end index of the substring to be returned from the original string", so the following line should be changed:
stringg := SUBSTR(str,1,i) || SUBSTR(str,i+1,strlen);
to:
stringg := SUBSTR(str,1,i - 1) || SUBSTR(str,i+1,strlen);
The change had to be made, because in the Java code that you provided the link to, the loop starts from 0, and 0 is passed as the third argument, which results in an empty string being returned for the first iteration of the loop. Without the change, first iteration in PL/SQL version would return the first character from the argument.
In the end, you get a working procedure:
create or replace
procedure print_anagrams
(pre in varchar2, str in varchar2)
is
prefix varchar2(30);
stringg varchar2(30);
strlen number;
begin
strlen := length(str);
if NVL(strlen, 0) = 0 then
dbms_output.put_line(pre);
else
for i in 1..strlen loop
prefix := pre || SUBSTR(str,i,1);
stringg := SUBSTR(str,1,i - 1) || SUBSTR(str,i+1,strlen);
print_anagrams(prefix,stringg);
end loop;
end if;
end;
/
Test:
EXEC print_anagrams('', 'cat');
Output:
cat
cta
act
atc
tca
tac
Oracle Substr Function
Java String's substring method

Oracle insert() function like mysql's

Does oracle have an equivalent function like mysql's Insert() to handle strings ?
EDIT:
This is the coded answer in order to be easily understood
create or replace function fn_insert(ori_string in varchar2, in_pos in number,
p_length in number, new_string in varchar2)
return varchar2
is
resul varchar2(250) default '';
begin
if in_pos < 0 then
resul := ori_string;
else
resul := substr(ori_string, 1, in_pos-1)||new_string||substr(ori_string, in_pos+p_length, length(ori_string) - ((in_pos+p_length)-1));
end if;
return resul;
END fn_insert;
/
You can do it manually. So, insert(ori_string, in_pos, length, new_string) could be written as:
substr(ori_string, 1, in_pos)||new_string||substr(orig_string, in_pos+length, length(orig_string) - (in_pos+length))
In other databases, this function is also called stuff()

Oracle PL/SQL string compare issue

I have the following Oracle PL/SQL codes that may be rusty from you guys perspective:
DECLARE
str1 varchar2(4000);
str2 varchar2(4000);
BEGIN
str1:='';
str2:='sdd';
IF(str1<>str2) THEN
dbms_output.put_line('The two strings is not equal');
END IF;
END;
/
This is very obvious that two strings str1 and str2 are not equal, but why 'The two strings are not equal' was not printed out? Do Oracle have another common method to compare two string?
As Phil noted, the empty string is treated as a NULL, and NULL is not equal or unequal to anything. If you expect empty strings or NULLs, you'll need to handle those with NVL():
DECLARE
str1 varchar2(4000);
str2 varchar2(4000);
BEGIN
str1:='';
str2:='sdd';
-- Provide an alternate null value that does not exist in your data:
IF(NVL(str1,'X') != NVL(str2,'Y')) THEN
dbms_output.put_line('The two strings are not equal');
END IF;
END;
/
Concerning null comparisons:
According to the Oracle 12c documentation on NULLS, null comparisons using IS NULL or IS NOT NULL do evaluate to TRUE or FALSE. However, all other comparisons evaluate to UNKNOWN, not FALSE. The documentation further states:
A condition that evaluates to UNKNOWN acts almost like FALSE. For example, a SELECT statement with a condition in the WHERE clause that evaluates to UNKNOWN returns no rows. However, a condition evaluating to UNKNOWN differs from FALSE in that further operations on an UNKNOWN condition evaluation will evaluate to UNKNOWN. Thus, NOT FALSE evaluates to TRUE, but NOT UNKNOWN evaluates to UNKNOWN.
A reference table is provided by Oracle:
Condition Value of A Evaluation
----------------------------------------
a IS NULL 10 FALSE
a IS NOT NULL 10 TRUE
a IS NULL NULL TRUE
a IS NOT NULL NULL FALSE
a = NULL 10 UNKNOWN
a != NULL 10 UNKNOWN
a = NULL NULL UNKNOWN
a != NULL NULL UNKNOWN
a = 10 NULL UNKNOWN
a != 10 NULL UNKNOWN
I also learned that we should not write PL/SQL assuming empty strings will always evaluate as NULL:
Oracle Database currently treats a character value with a length of zero as null. However, this may not continue to be true in future releases, and Oracle recommends that you do not treat empty strings the same as nulls.
Let's fill in the gaps in your code, by adding the other branches in the logic, and see what happens:
SQL> DECLARE
2 str1 varchar2(4000);
3 str2 varchar2(4000);
4 BEGIN
5 str1:='';
6 str2:='sdd';
7 IF(str1<>str2) THEN
8 dbms_output.put_line('The two strings is not equal');
9 ELSIF (str1=str2) THEN
10 dbms_output.put_line('The two strings are the same');
11 ELSE
12 dbms_output.put_line('Who knows?');
13 END IF;
14 END;
15 /
Who knows?
PL/SQL procedure successfully completed.
SQL>
So the two strings are neither the same nor are they not the same? Huh?
It comes down to this. Oracle treats an empty string as a NULL. If we attempt to compare a NULL and another string the outcome is not TRUE nor FALSE, it is NULL. This remains the case even if the other string is also a NULL.
I compare strings using = and not <>. I've found out that in this context = seems to work in more reasonable fashion than <>. I have specified that two empty (or NULL) strings are equal. The real implementation returns PL/SQL boolean, but here I changed that to pls_integer (0 is false and 1 is true) to be able easily demonstrate the function.
create or replace function is_equal(a in varchar2, b in varchar2)
return pls_integer as
begin
if a is null and b is null then
return 1;
end if;
if a = b then
return 1;
end if;
return 0;
end;
/
show errors
begin
/* Prints 0 */
dbms_output.put_line(is_equal('AAA', 'BBB'));
dbms_output.put_line(is_equal('AAA', null));
dbms_output.put_line(is_equal(null, 'BBB'));
dbms_output.put_line(is_equal('AAA', ''));
dbms_output.put_line(is_equal('', 'BBB'));
/* Prints 1 */
dbms_output.put_line(is_equal(null, null));
dbms_output.put_line(is_equal(null, ''));
dbms_output.put_line(is_equal('', ''));
dbms_output.put_line(is_equal('AAA', 'AAA'));
end;
/
To fix the core question, "how should I detect that these two variables don't have the same value when one of them is null?", I don't like the approach of nvl(my_column, 'some value that will never, ever, ever appear in the data and I can be absolutely sure of that') because you can't always guarantee that a value won't appear... especially with NUMBERs.
I have used the following:
if (str1 is null) <> (str2 is null) or str1 <> str2 then
dbms_output.put_line('not equal');
end if;
Disclaimer: I am not an Oracle wizard and I came up with this one myself and have not seen it elsewhere, so there may be some subtle reason why it's a bad idea. But it does avoid the trap mentioned by APC, that comparing a null to something else gives neither TRUE nor FALSE but NULL. Because the clauses (str1 is null) will always return TRUE or FALSE, never null.
(Note that PL/SQL performs short-circuit evaluation, as noted here.)
I've created a stored function for this text comparison purpose:
CREATE OR REPLACE FUNCTION TextCompare(vOperand1 IN VARCHAR2, vOperator IN VARCHAR2, vOperand2 IN VARCHAR2) RETURN NUMBER DETERMINISTIC AS
BEGIN
IF vOperator = '=' THEN
RETURN CASE WHEN vOperand1 = vOperand2 OR vOperand1 IS NULL AND vOperand2 IS NULL THEN 1 ELSE 0 END;
ELSIF vOperator = '<>' THEN
RETURN CASE WHEN vOperand1 <> vOperand2 OR (vOperand1 IS NULL) <> (vOperand2 IS NULL) THEN 1 ELSE 0 END;
ELSIF vOperator = '<=' THEN
RETURN CASE WHEN vOperand1 <= vOperand2 OR vOperand1 IS NULL THEN 1 ELSE 0 END;
ELSIF vOperator = '>=' THEN
RETURN CASE WHEN vOperand1 >= vOperand2 OR vOperand2 IS NULL THEN 1 ELSE 0 END;
ELSIF vOperator = '<' THEN
RETURN CASE WHEN vOperand1 < vOperand2 OR vOperand1 IS NULL AND vOperand2 IS NOT NULL THEN 1 ELSE 0 END;
ELSIF vOperator = '>' THEN
RETURN CASE WHEN vOperand1 > vOperand2 OR vOperand1 IS NOT NULL AND vOperand2 IS NULL THEN 1 ELSE 0 END;
ELSIF vOperator = 'LIKE' THEN
RETURN CASE WHEN vOperand1 LIKE vOperand2 OR vOperand1 IS NULL AND vOperand2 IS NULL THEN 1 ELSE 0 END;
ELSIF vOperator = 'NOT LIKE' THEN
RETURN CASE WHEN vOperand1 NOT LIKE vOperand2 OR (vOperand1 IS NULL) <> (vOperand2 IS NULL) THEN 1 ELSE 0 END;
ELSE
RAISE VALUE_ERROR;
END IF;
END;
In example:
SELECT * FROM MyTable WHERE TextCompare(MyTable.a, '>=', MyTable.b) = 1;
Only change the line
str1:='';
to
str1:=' ';
The '' would be treated as NULL, so, both the strings need to be checked as NULL.
Function:
CREATE OR REPLACE FUNCTION str_cmpr_fnc(str_val1_in IN VARCHAR2, str_val2_in IN VARCHAR2) RETURN VARCHAR2
AS
l_result VARCHAR2(50);
BEGIN
-- string comparison
CASE
WHEN str_val1_in IS NULL AND str_val2_in IS NULL THEN
l_result := 'Both Unknown';
WHEN str_val1_in IS NULL THEN
l_result := 'Str1 Unknown';
WHEN str_val2_in IS NULL THEN
l_result := 'Str2 Unknown';
ELSE
CASE
WHEN str_val1_in = str_val2_in THEN
l_result := 'Both are equel';
ELSE
l_result := 'Both strings are not equal';
END CASE;
END CASE;
-- return result
RETURN l_result;
EXCEPTION
WHEN OTHERS THEN
-- set serveroutput on to get the error information
DBMS_OUTPUT.put_line(SQLERRM||' ,'|| DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
-- return result
RETURN l_result;
END str_cmpr_fnc;
Sql Statement:
SELECT str_cmpr_fnc('7', 'd') FROM DUAL;
To the first question:
Probably the message wasn't print out because you have the output turned off. Use these commands to turn it back on:
set serveroutput on
exec dbms_output.enable(1000000);
On the second question:
My PLSQL is quite rusty so I can't give you a full snippet, but you'll need to loop over the result set of the SQL query and CONCAT all the strings together.

How can I determine if a string is numeric in 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