function that take two parameters, the first to be a string and the second is the order (Asc or Desc) and the returned output to be ordering the first string as per the second parameter.
IN : dgtak
OUT: adgkt
Tried this but doesn't seem to work
CREATE OR REPLACE FUNCTION order_string(my_string IN VARCHAR2)
RETURN VARCHAR2 IS
ret_string VARCHAR2(4000);
BEGIN
SELECT LISTAGG(regexp_substr(my_string, '\w', 1, level), '') WITHIN
GROUP(
ORDER BY 1)
INTO ret_string
FROM dual
CONNECT BY regexp_substr(my_string, '\w', 1, level) IS NOT NULL;
RETURN ret_string;
END;
select order_string('dgtak') as RESULT from dual;
Here's one option:
SQL> create or replace function order_string (par_string in varchar2, par_order in varchar2)
2 return varchar2
3 is
4 retval varchar2(100);
5 begin
6 with temp (val) as
7 -- split PAR_STRING to rows
8 (select substr(par_string, level, 1)
9 from dual
10 connect by level <= length(par_string)
11 )
12 -- aggregate characters back in ascending or descending order
13 select case when par_order = 'Asc' then listagg(val, '') within group (order by val asc)
14 when par_order = 'Desc' then listagg(val, '') within group (order by val desc)
15 else null
16 end
17 into retval
18 from temp;
19
20 return retval;
21 end;
22 /
Function created.
Testing:
SQL> select order_string('dfag', 'Asc') result_asc,
2 order_string('dfag', 'Desc') result_desc
3 from dual;
RESULT_ASC RESULT_DESC
-------------------- --------------------
adfg gfda
SQL>
Just for fun, here's a procedural version. It has more lines of code than the SQL version but in my tests it's slightly faster.
create or replace function order_string
( p_string varchar2
, p_reverse varchar2 default 'N' )
return varchar2
as
pragma udf;
type letter_tt is table of number index by varchar2(1);
letters letter_tt := letter_tt();
letter varchar2(1);
sorted_string long;
string_length integer := length(p_string);
begin
-- Store all characters of p_string as indices of array:
for i in 1..string_length loop
letter := substr(p_string,i,1);
if letters.exists(letter) then
letters(letter) := letters(letter) +1;
else
letters(letter) := 1;
end if;
end loop;
-- Loop through array appending each array index to sorted_string
for i in indices of letters loop
for r in 1..letters(i) loop
sorted_string := sorted_string || i;
end loop;
end loop;
if p_reverse = 'Y' then
select reverse(sorted_string) into sorted_string from dual;
end if;
return sorted_string;
end order_string;
I've used the 21c indices of loop iterator, but you can write a conventional loop in earlier versions. You might also use two alternative loops for ascending and descending order in place of my hack.
Related
I have a table called 'config' and when I query it in following manner:
SELECT value FROM config WHERE property = 'SPECIAL_STORE_ID'
its response will be: 59216;131205;76707;167206 //... (1)
I want to tokenize the above values using semicolon as the delimiter and then use them in a user-defined Function's IF statement to compare, something like this:
IF in_store_id exists in (<delimited response from (1) above>)//...(2)
THEN do some stuff
where in_store_id is the parameter passed-in to the function
Is this possible to do as one-liner in (2) above ?
I'm on Oracle 12c
One-liner? I don't think so, but - if you're satisfied with something like this, fine.
SQL> select * From config;
VALUE PROPERTY
-------------- ----------------
7369;7499;7521 SPECIAL_STORE_ID
SQL> declare
2 in_store_id varchar2(20) := 7369;
3 l_exists number;
4 begin
5 select instr(value, ';' || in_store_id || ';')
6 into l_exists
7 from config
8 where property = 'SPECIAL_STORE_ID';
9
10 if l_exists > 0 then
11 dbms_output.put_line('that STORE_ID exists in the value');
12 else
13 dbms_output.put_line('that STORE_ID does not exist in the value');
14 end if;
15 end;
16 /
that STORE_ID exists in the value
PL/SQL procedure successfully completed.
SQL>
If the delimited response is a collection then you can use member of to check if the collection contains the ID or not like
create or replace procedure test_procedure2(p_property in varchar2, p_id in varchar2) is
type test_t is table of varchar2(20);
l_ids test_t;
begin
select regexp_substr(value, '[^;]+', 1, level) bulk collect into l_ids
from (select value from config where property = p_property)
connect by level <= regexp_count(value, ';')+1;
if(p_id member of (l_ids)) then
dbms_output.put_line('Do stuff for '||p_property||' '||p_id);
end if;
end;
/
or do it without the collection with intermediate select like
create or replace procedure test_procedure1(p_property in varchar2, p_id in varchar2) is
l_flag number(3);
begin
select count(1) into l_flag from dual where p_id in (
select regexp_substr(value, '[^;]+', 1, level)
from (select value from config where property = p_property)
connect by level <= regexp_count(value, ';')+1
);
if(l_flag > 0) then
dbms_output.put_line('Do stuff for '||p_property||' '||p_id);
end if;
end;
/
See fiddle
If a column value if too long, then listagg gives NULL output in Oracle 12c. What is the cause of this and what is the solution?
SQL> select length(algo_desc) from r2_temp where DECL = '305';
32759
ALGO_DESC is very long, like 1045,2339,2389.......37364,58922,2389392
Now if I use LISTAGG then it gives NULL as follows:
SQL> select dump(listagg(algo_desc, ',') within group (order by algo_desc)) as algo_desc from r2_temp where DECL = '305';
NULL
max size of column ALGO_DESC is 32767, it's extended varchar2 in 12c
As a workaround you can use this function:
CREATE OR REPLACE TYPE VARCHAR_TABLE_TYPE AS TABLE OF VARCHAR2(32767);
-- or CREATE OR REPLACE TYPE VARCHAR_TABLE_TYPE AS TABLE OF NUMBER;
CREATE OR REPLACE FUNCTION JoinTable(TAB IN VARCHAR_TABLE_TYPE, Joiner IN VARCHAR2) RETURN CLOB IS
res CLOB;
BEGIN
IF TAB IS NULL THEN
RETURN NULL;
END IF;
IF TAB.COUNT = 0 THEN
RETURN NULL;
END IF;
DBMS_LOB.CREATETEMPORARY(res, FALSE, DBMS_LOB.CALL);
IF TAB(TAB.FIRST) IS NOT NULL THEN
DBMS_LOB.APPEND(res, TAB(TAB.FIRST));
END IF;
IF TAB.COUNT > 1 THEN
FOR i IN TAB.FIRST+1..TAB.LAST LOOP
DBMS_LOB.APPEND(res, Joiner||TAB(i));
END LOOP;
END IF;
RETURN res;
END JoinTable;
select JoinTable(CAST(COLLECT(algo_desc order by algo_desc) AS VARCHAR_TABLE_TYPE), ',') as algo_desc
from r2_temp
where DECL = '305';
I have 2 strings str1: 'abc,def,ghi' and str2: 'tyu,abc,fgh'.
I want to compare these two strings using the delimiter ,. Now since the 2 strings have abc it should return true. I want a function in Oracle SQL which can perform this operation.
Looks complicated but it is just a couple of helper functions to split a list into separate values (to be contained into a table type) and then a very simple function to test the intersection of two collections.
Oracle Setup:
CREATE TYPE VARCHAR2s_Table AS TABLE OF VARCHAR2(4000);
CREATE FUNCTION regexp_escape(
expression VARCHAR2
) RETURN VARCHAR2 DETERMINISTIC
AS
BEGIN
RETURN REGEXP_REPLACE( expression, '([$^[()+*?{\|])', '\\\1', 1, 0, 'c' );
END;
/
CREATE FUNCTION splitList(
list VARCHAR2,
delim VARCHAR2 := ','
) RETURN VARCHAR2s_Table DETERMINISTIC
AS
pattern VARCHAR2(256);
len BINARY_INTEGER;
t_items VARCHAR2s_Table := VARCHAR2s_Table();
BEGIN
IF list IS NULL THEN
NULL;
ELSIF delim IS NULL THEN
t_items.EXTEND( LENGTH( list ) );
FOR i IN 1 .. LENGTH( list ) LOOP
t_items(i) := SUBSTR( list, i, 1 );
END LOOP;
ELSE
pattern := '(.*?)($|' || REGEXP_ESCAPE( delim ) || ')';
len := REGEXP_COUNT( list, pattern ) - 1;
t_items.EXTEND( len );
IF len = 1 THEN
t_items(1) := list;
ELSE
FOR i IN 1 .. len LOOP
t_items(i) := REGEXP_SUBSTR( list, pattern, 1, i, NULL, 1 );
END LOOP;
END IF;
END IF;
RETURN t_items;
END;
/
CREATE FUNCTION check_list_intersect(
list1 VARCHAR2,
list2 VARCHAR2
) RETURN NUMBER DETERMINISTIC
AS
BEGIN
IF splitList( list1 ) MULTISET INTERSECT splitList( list2 ) IS EMPTY THEN
RETURN 0;
ELSE
RETURN 1;
END IF;
END;
/
Query 1:
SELECT check_list_intersect( 'abc,def,ghi', 'abc' ) AS matches
FROM DUAL;
Results:
MATCHES
---------
1
Query 2:
SELECT check_list_intersect( 'abc,def,ghi', 'abcd' ) AS matches
FROM DUAL;
Results:
MATCHES
---------
0
The below will make the trick.
with temp as (
select 1 strid, 'abc,def,ghi' Error from dual
union all
select 2, 'tyu,abc,fgh' from dual
)
select str
from (
SELECT strid, trim(regexp_substr(str, '[^,]+', 1, level)) str
FROM (SELECT strid, Error str FROM temp) t
CONNECT BY instr(str, ',', 1, level - 1) > 0
)
group by str
having count(distinct strid) > 1;
Warning: This answer is not fully correct. (See the comments.)
Starting from dcieslak answer(csv split with regexp), a variation of the subject would be:
create or replace function check_string_intersec(str1 varchar2, str2 varchar2) return number
is
begin
for k in (SELECT trim(regexp_substr(str1, '[^,]+', 1, level)) item
FROM dual
CONNECT BY instr(str1, ',', 1, level - 1) > 0
)
loop
if instr(str2, k.item,1) > 0 then return 1; end if;
end loop;
return 0;
end;
This splits first string and search for every item in the second string.
I want to use this array in 'select from..where..in(YYY)' statement.
I don't want to iterate through array values, I want to use it whole in my select statement.
Unfortunately, I found only how to iterate it:
1 declare
2 type array is table of varchar2(30) index by binary_integer;
3 a array;
4 procedure p( array_in array )
5 is
6 begin
7 for i in 1..array_in.count loop
8 dbms_output.put_line( array_in(i) );
9 end loop;
10 end;
11 begin
12 a(1) := 'Apple';
13 a(2) := 'Banana';
14 a(3) := 'Pear';
15 p( a );
16 end;
17 /
You can do this by creating a function returning your array. Then you can use it into a select:
Create external types and function
create or replace type t_array is table of varchar2(30);
create or replace function list_of_fruits
return t_array
is
l_ t_array:=t_array();
begin
l_.extend(); l_(l_.COUNT) := 'Apple';
l_.extend(); l_(l_.COUNT) := 'Banana';
l_.extend(); l_(l_.COUNT) := 'Pear';
return l_;
end list_of_fruits;
/
And here is how to use it:
select * from (
select 'Peter' this_and_that from dual
union all select 'Joy' from dual
union all select 'God' from dual
union all select 'Pear' from dual
union all select 'Man' from dual
)
where this_and_that in (
select column_value from (table( list_of_fruits() ))
);
The trick here is to use the table() function to make a SQL usable list for your select; also difficult for me was to discover the name of that column_value... which is some built-in constant from Oracle: how do you guess that?
You can use oracle defined collection to achieve this as well. Please see below and example.
declare
a sys.odcivarchar2list;
begin
a := sys.odcivarchar2list('Apple','Banana','Pear');
for r in ( SELECT m.column_value m_value
FROM table(a) m )
loop
dbms_output.put_line (r.m_value);
end loop;
end;
I have a table with this values:
ID VALUE
-----------------------
23559 200
23562 -1 & {14376}#-1
and I want to do to a select that if I cannot convert to number set NULL.
I generally use translate for this because it is such an odd corner case:
SELECT
CASE
WHEN NOT TRIM(TRANSLATE(COLUMN_NAME, '1234567890', ' ')) IS NULL THEN NULL
ELSE COLUMN_NAME
END AS "NUMERIC_COLUMN"
FROM
TABLE_NAME;
If necessary, that can be turned into a procedure, but I'm not sure that there would be terribly much benefit performance-wise.
You can create a function that tries to convert the string to a number and catches the exception. Something like
CREATE OR REPLACE FUNCTION my_to_number( p_str IN VARCHAR2 )
RETURN NUMBER
IS
l_num NUMBER;
BEGIN
BEGIN
l_num := to_number( p_str );
EXCEPTION
WHEN others THEN
l_num := null;
END;
RETURN l_num;
END;
Then you can
SELECT id, my_to_number( value )
FROM your_table
You could also use REGEXP_LIKE:
SELECT id
, CASE WHEN regexp_like(value,'^[0-9]+$') THEN TO_NUMBER(value)
ELSE NULL
END value
FROM your_table;
For example:
SQL> WITH q AS (
2 SELECT 1 ID, '200' col FROM dual
3 UNION
4 SELECT 2, '-1 & {14376}#-1' FROM dual
5 )
6 SELECT id, CASE WHEN regexp_like(col,'^[0-9]+$') THEN TO_NUMBER(col) ELSE NULL END TEST FROM q;
ID TEST
---------- ----------
1 200
2
With Oracle 12.2 this can be done a bit easier using the on conversion error option:
select id, cast(value as number default null on conversion error) as value
from the_table;
Optionally you can also specify a format mask, similar to the to_number() function.
I assume this would be faster than using a PL/SQL function, not sure about the performance compared to a case with a regex. But it is definitely a lot shorter.
CREATE OR REPLACE FUNCTION asnumber(p_val IN VARCHAR2) RETURN NUMBER IS
l_val NUMBER;
BEGIN
l_val := TO_NUMBER(p_val);
RETURN l_val;
EXCEPTION WHEN VALUE_ERROR THEN
RETURN null;
END;