I have a table with columns word and sentence. The idea is to replace the words in the sentence with the link(includes the word itself) if the word is found in the word column. The query below replaces perfectly but since the link is constructed from the temp.word column, the case of the words in the sentence is changed to the case of the words in the word column. Is there a way to retain the same case in the sentence itself?
Create table temp(
id NUMBER,
word VARCHAR2(1000),
sentence VARCHAR2(2000)
);
insert into temp
SELECT 1,'automation testing', 'automtestingation Testing is popular kind of testing' FROM DUAL UNION ALL
SELECT 2,'testing','manual testing' FROM DUAL UNION ALL
SELECT 3,'manual testing','this is an old method of testing' FROM DUAL UNION ALL
SELECT 4,'punctuation','automation testing,manual testing,punctuation,automanual testing-testing' FROM DUAL UNION ALL
SELECT 5,'B-number analysis','B-number analysis table' FROM DUAL UNION ALL
SELECT 6,'B-number analysis table','testing B-number analysis' FROM DUAL UNION ALL
SELECT 7,'Not Matched','testing testing testing' FROM DUAL;
with words(id, word, word_length, search1, replace1, search2, replace2) as (
select id, word, length(word),
'(^|\W)' || REGEXP_REPLACE(word, '([][)(}{|^$\.*+?])', '\\\1') || '($|\W)',
'\1{'|| id ||'}\2',
'{'|| id ||'}',
'http://localhost/' || id || '/<u>' || word || '</u>'
FROM temp
)
, joined_data as (
select w.search1, w.replace1, w.search2, w.replace2,
s.rowid s_rid, s.sentence,
row_number() over(partition by s.rowid order by word_length desc) rn
from words w
join temp s
on instr(UPPER(s.sentence), UPPER(w.word)) > 0
and regexp_like(s.sentence, w.search1)
)
, unpivoted_data as (
select S_RID, SENTENCE, PHASE, SEARCH_STRING, REPLACE_STRING,
row_number() over(partition by s_rid order by phase, rn) rn,
case when row_number() over(partition by s_rid order by phase, rn)
= count(*) over(partition by s_rid)
then 1
else 0
end is_last
from joined_data
unpivot(
(search_string, replace_string)
for phase in ( (search1, replace1) as 1, (search2, replace2) as 2 ))
)
, replaced_data(S_RID, RN, is_last, SENTENCE) as (
select S_RID, RN, is_last,
regexp_replace(SENTENCE, search_string, replace_string,1,0,'i')
from unpivoted_data
where rn = 1
union all
select n.S_RID, n.RN, n.is_last,
case when n.phase = 1
then regexp_replace(o.SENTENCE, n.search_string, n.replace_string,1,0,'i')
else replace(o.SENTENCE, n.search_string, n.replace_string)
end
from unpivoted_data n
join replaced_data o
on o.s_rid = n.s_rid and n.rn = o.rn + 1
)
select s_rid, sentence from replaced_data
where is_last = 1
order by s_rid;
For example, for id = 1, the sentence is automtestingation Testing is popular kind of testing
After replacement it will be automtestingation http://localhost/2/<u>testing</u> is popular kind of http://localhost/2/<u>testing</u>.
The word Testing is replaced with testing(from the temp.word column).
The expected outcome is
automtestingation http://localhost/2/<u>Testing</u> is popular kind of http://localhost/2/<u>testing</u>
Oracle Setup:
Create table temp(
id NUMBER,
word VARCHAR2(1000),
Sentence VARCHAR2(2000)
);
insert into temp
SELECT 1,'automation testing', 'automtestingation TeStInG TEST is popular kind of testing' FROM DUAL UNION ALL
SELECT 2,'testing','manual testing' FROM DUAL UNION ALL
select 2,'test', 'test' FROM DUAL UNION ALL
SELECT 3,'manual testing','this is an old method of testing' FROM DUAL UNION ALL
SELECT 4,'punctuation','automation testing,manual testing,punctuation,automanual testing-testing' FROM DUAL UNION ALL
SELECT 5,'B-number analysis','B-number analysis table' FROM DUAL UNION ALL
SELECT 6,'B-number analysis table','testing B-number analysis' FROM DUAL UNION ALL
SELECT 7,'Not Matched','testing testing testing' FROM DUAL UNION ALL
SELECT 8,'^[($','testing characters ^[($ that need escaping in a regular expression' FROM DUAL;
SQL Types:
CREATE TYPE stringlist IS TABLE OF VARCHAR2(4000);
/
CREATE TYPE intlist IS TABLE OF NUMBER(20,0);
/
PL/SQL Function:
CREATE FUNCTION replace_words(
word_list IN stringlist,
id_list IN intlist,
sentence IN temp.sentence%TYPE
) RETURN temp.sentence%TYPE
IS
p_sentence temp.sentence%TYPE := UPPER( sentence );
p_pos PLS_INTEGER := 1;
p_min_word_index PLS_INTEGER;
p_word_index PLS_INTEGER;
p_start PLS_INTEGER;
p_index PLS_INTEGER;
o_sentence temp.sentence%TYPE;
BEGIN
LOOP
p_min_word_index := NULL;
p_index := NULL;
FOR i IN 1 .. word_list.COUNT LOOP
p_word_index := p_pos;
LOOP
p_word_index := INSTR( p_sentence, word_list(i), p_word_index );
EXIT WHEN p_word_index = 0;
IF ( p_word_index > 1
AND REGEXP_LIKE( SUBSTR( p_sentence, p_word_index - 1, 1 ), '\w' )
)
OR REGEXP_LIKE( SUBSTR( p_sentence, p_word_index + LENGTH( word_list(i) ), 1 ), '\w' )
THEN
p_word_index := p_word_index + 1;
CONTINUE;
END IF;
IF p_min_word_index IS NULL OR p_word_index < p_min_word_index THEN
p_min_word_index := p_word_index;
p_index := i;
END IF;
EXIT;
END LOOP;
END LOOP;
IF p_index IS NULL THEN
o_sentence := o_sentence || SUBSTR( sentence, p_pos );
EXIT;
ELSE
o_sentence := o_sentence
|| SUBSTR( sentence, p_pos, p_min_word_index - p_pos )
|| 'http://localhost/'
|| id_list(p_index)
|| '/<u>'
|| SUBSTR( sentence, p_min_word_index, LENGTH( word_list( p_index ) ) )
|| '</u>';
p_pos := p_min_word_index + LENGTH( word_list( p_index ) );
END IF;
END LOOP;
RETURN o_sentence;
END;
/
Merge:
MERGE INTO temp dst
USING (
WITH lists ( word_list, id_list ) AS (
SELECT CAST(
COLLECT(
UPPER( word )
ORDER BY LENGTH( word ) DESC, UPPER( word ) ASC, ROWNUM
)
AS stringlist
),
CAST(
COLLECT(
id
ORDER BY LENGTH( word ) DESC, UPPER( word ) ASC, ROWNUM
)
AS intlist
)
FROM temp
)
SELECT t.ROWID rid,
replace_words(
word_list,
id_list,
sentence
) AS replaced_sentence
FROM temp t
CROSS JOIN lists
) src
ON ( dst.ROWID = src.RID )
WHEN MATCHED THEN
UPDATE SET sentence = src.replaced_sentence;
Output:
SELECT * FROM temp;
ID | WORD | SENTENCE
-: | :---------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1 | automation testing | automtestingation http://localhost/2/<u>TeStInG</u> http://localhost/2/<u>TEST</u> is popular kind of http://localhost/2/<u>testing</u>
2 | testing | http://localhost/3/<u>manual testing</u>
2 | test | http://localhost/2/<u>test</u>
3 | manual testing | this is an old method of http://localhost/2/<u>testing</u>
4 | punctuation | http://localhost/1/<u>automation testing</u>,http://localhost/3/<u>manual testing</u>,http://localhost/4/<u>punctuation</u>,automanual http://localhost/2/<u>testing</u>-http://localhost/2/<u>testing</u>
5 | B-number analysis | http://localhost/6/<u>B-number analysis table</u>
6 | B-number analysis table | http://localhost/2/<u>testing</u> http://localhost/5/<u>B-number analysis</u>
7 | Not Matched | http://localhost/2/<u>testing</u> http://localhost/2/<u>testing</u> http://localhost/2/<u>testing</u>
8 | ^[($ | http://localhost/2/<u>testing</u> characters http://localhost/8/<u>^[($</u> that need escaping in a regular expression
db<>fiddle here
While there's certainly a way to do this in a single SQL statement, I think this problem is better solved with a separate function:
create or replace function replace_words(p_word varchar2, p_sentence varchar2) return varchar2 is
v_match_position number;
v_match_count number := 0;
v_new_sentence varchar2(4000) := p_sentence;
begin
--Find all matches.
loop
--Find Nth case-insensitive match
v_match_count := v_match_count + 1;
v_match_position := regexp_instr(
srcstr => v_new_sentence,
pattern => p_word,
occurrence => v_match_count,
modifier => 'i');
exit when v_match_position = 0;
--Insert text, instead of replacing, to keep the original case.
v_new_sentence :=
substr(v_new_sentence, 1, v_match_position - 1) || 'http://localhost/2/<u>'||
substr(v_new_sentence, v_match_position, length(p_word)) || '</u>'||
substr(v_new_sentence, v_match_position + length(p_word));
end loop;
return v_new_sentence;
end;
/
Then the SQL query looks like this:
select id, word, sentence, replace_words(word, sentence) from temp;
Related
I need an Oracle (11) function to handle this question.
I need to counting distinct values count from comma separated string.
For example the comma separated string:
'Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text'
The result have to be = 6
Beacuse of
Lorem
Ipsum
is
simply
dummy
text
I want to use like this
select fn_dist_count_values_in_list_arr('Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text') from dual;
Can anyone help to write this ("fn_dist_count_values_in_list_arr") oracle function?
You don't need a context switch from SQL to a PL/SQL function and can do it all in SQL:
SELECT ( SELECT COUNT( DISTINCT CAST(column_value AS VARCHAR2(20)) )
FROM XMLTABLE( ('"'||REPLACE(value,',','","')||'"') ) )
AS num_distinct_values
FROM table_name
Which, for the sample data:
CREATE TABLE table_name ( value ) AS
SELECT 'Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text' FROM DUAL;
Outputs:
| NUM_DISTINCT_VALUES |
| ------------------: |
| 6 |
If you want a pure PL/SQL function (so that you do not have multiple context-switches) then:
CREATE FUNCTION fn_dist_count_values_in_list_arr (
list_value IN VARCHAR2
) RETURN NUMBER DETERMINISTIC
IS
TYPE t_words IS TABLE OF NUMBER(1,0) INDEX BY VARCHAR2(200);
v_words t_words;
v_start PLS_INTEGER := 1;
v_end PLS_INTEGER;
BEGIN
IF list_value IS NULL THEN
RETURN 0;
END IF;
LOOP
v_end := INSTR( list_value, ',', v_start );
EXIT WHEN v_end = 0;
v_words(SUBSTR(list_value, v_start, v_end - v_start ) ) := 1;
v_start := v_end + 1;
END LOOP;
v_words(SUBSTR(list_value,v_start)) := 1;
RETURN v_words.COUNT;
END;
/
and then:
SELECT fn_dist_count_values_in_list_arr( value )
FROM table_name
outputs:
| FN_DIST_COUNT_VALUES_IN_LIST_ARR(VALUE) |
| --------------------------------------: |
| 6 |
db<>fiddle here
CREATE OR REPLACE FUNCTION DIST_COUNT_VALUES_IN_STR_ARR
(STR_ARR IN VARCHAR2)
RETURN NUMBER
AS
DIST_COUNT NUMBER(38);
BEGIN
SELECT COUNT(DISTINCT COL1)
INTO DIST_COUNT FROM (
SELECT REGEXP_SUBSTR(STR_ARR,'[^,]+', 1, LEVEL) COL1
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT(STR_ARR, ',') + 1
);
RETURN DIST_COUNT;
END;
This worked for me. The inner query separates the elements into rows by using regex on the comma character. I had to rename your function as i hit the limit of the max length of an object name for my version of Oracle.
And another approach would be to use hierarchical query to split comma separated values to a set of rows (with clause) and run a simple sql query against it
with str_parsed as (SELECT REGEXP_SUBSTR('Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text', '[^,]+', 1, LEVEL) val
FROM dual
CONNECT BY REGEXP_SUBSTR('Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text,Lorem,Ipsum,is,simply,dummy,text', '[^,]+', 1, LEVEL) IS NOT NULL)
select count(distinct val) from str_parsed
What i need
i need to create dynamic sql based on param procedure receives.
Type :[A,B,C]
Code : [1,2,3]
Dynamic sql
AND ( (in_type = 'A' and in_code = '1' )
OR (in_type = 'B' and in_code = '2')
OR (in_type = 'C' and in_code = '3')
)
Solution i tried
splitting string from comma separated
CREATE OR REPLACE TYPE t_my_list AS TABLE OF VARCHAR2(100);
CREATE OR REPLACE
FUNCTION comma_to_table(p_list IN VARCHAR2)
RETURN t_my_list
AS
l_string VARCHAR2(32767) := p_list || ',';
l_comma_index PLS_INTEGER;
l_index PLS_INTEGER := 1;
l_tab t_my_list := t_my_list();
BEGIN
LOOP
l_comma_index := INSTR(l_string, ',', l_index);
EXIT
WHEN l_comma_index = 0;
l_tab.EXTEND;
l_tab(l_tab.COUNT) := TRIM(SUBSTR(l_string,l_index,l_comma_index - l_index));
l_index := l_comma_index + 1;
END LOOP;
RETURN l_tab;
END comma_to_table;
/
FOR x IN (select * from (table(comma_to_table(in_type)) ) )
LOOP
dbms_output.put_line(x.COLUMN_VALUE);
FOR y IN (select * from (table(comma_to_table(in_code )) ) )
LOOP
dbms_output.put_line(y.COLUMN_VALUE);
IF x.COLUMN_VALUE = 'A' THEN
l_where := l_where||' AND UPPER(column_name) IN (' || upper(in_code) || ')';
END IF;
END LOOP;
END LOOP;
You can define a single collection of objects and then do additional processing:
CREATE OR REPLACE TYPE t_my_item as object(
name varchar2(1),
code number
);
/
CREATE OR REPLACE TYPE t_my_list AS TABLE OF t_my_item;
declare
l_my_list t_my_list;
begin
select cast(multiset(
select t1.val, t2.val from (
select regexp_substr('A,B,C','[^,]+', 1, level ) val , level lvl
from dual
connect by regexp_substr('A,B,C', '[^,]+', 1, level) is not null) t1,
(select regexp_substr('1,2,3','[^,]+', 1, level) val , level lvl
from dual
connect by regexp_substr('1,2,3', '[^,]+', 1, level) is not null) t2
where t1.lvl = t2.lvl) as t_my_list) into l_my_list from dual;
end;
/
and here's a single select stmt:
select ' AND ( '||LISTAGG(
stmt,
' or '
) WITHIN GROUP(
ORDER BY lvl) || ' )'
from (
select t1.lvl, '(in_type = ''' || t1.val||''' and in_code = ''' || t2.val || ''' )' stmt
from (
select regexp_substr('A,B,C','[^,]+', 1, level ) val , level lvl
from dual
connect by regexp_substr('A,B,C', '[^,]+', 1, level) is not null) t1,
(select regexp_substr('1,2,3','[^,]+', 1, level) val , level lvl
from dual
connect by regexp_substr('1,2,3', '[^,]+', 1, level) is not null) t2
where t1.lvl = t2.lvl);
just copy paste what's above to your method:
create or replace FUNCTION comma_to_table(p_type IN VARCHAR2, p_code IN VARCHAR2)
RETURN t_my_list
AS
l_my_list t_my_list;
begin
select cast(multiset(
select t1.val, t2.val from (
select regexp_substr(p_type,'[^,]+', 1, level ) val , level lvl
from dual
connect by regexp_substr(p_type, '[^,]+', 1, level) is not null) t1,
(select regexp_substr(p_code,'[^,]+', 1, level) val , level lvl
from dual
connect by regexp_substr(p_code, '[^,]+', 1, level) is not null) t2
where t1.lvl = t2.lvl) as t_my_list) into l_my_list from dual;
return l_my_list;
end;
/
select comma_to_table(p_type => 'A,B,C', p_code => '1,2,3') from dual;
I have written this code
Select replace (upper(substr(div_no,1,3), ',' ','',''')
from dual
I want when user write div_no like this ('v0e200,q0e600') must return
v0e,q0e
If I have a string like 's05200 , s02700' I want to take first three characters, like 's05 ',' s02'
It seems you're looking for the translate() function :
SQL> select translate ('vwe200,qwe600', ',1234567890', ',')
2 from dual;
TRANSLA
-------
vwe,qwe
SQL>
So your revised test data clarifies the requirement. Here is a regex solution for extracting the first three characters from each segment of a string:
SQL> with testdata as (
2 select 'vwe200,qwe600' as str from dual union all
3 select 's05200 , e0300' as str from dual
4 )
5 select regexp_replace(str, '([a-z0-9]{3})([a-z0-9]*)(,?)','\1\3')
6 from testdata;
REGEXP_REPLACE(STR,'([A-Z0-9]{3})([A-Z0-9]*)(,?)','\1\3')
--------------------------------------------------------------------------------
vwe,qwe
s05 , e03
SQL>
If removing the trailing spaces is required it can be done by adjusting the regex pattern :
SQL> with testdata as (
2 select 'vwe200,qwe600' as str from dual union all
3 select 's05200 , e0300' as str from dual
4 )
5 select regexp_replace(str, '( *)([a-z0-9]{3})([a-z0-9 ]*)(,?)','\2\4')
6 from testdata;
REGEXP_REPLACE(STR,'([A-Z0-9]{3})([A-Z0-9]*)(,?)','\1\3')
--------------------------------------------------------------------------------
vwe,qwe
s05,e03
SQL>
Injecting additional quotes is a use for the replace() function:
SQL> select replace('s05 , e03', ',', ''',''') from dual;
REPLACE('S0
-----------
s05 ',' e03
SQL>
Use the output from the previous answer as the input for this one.
Oracle 12c Query:
with FUNCTION getFirst3CharsOfDelimitedList(
str IN VARCHAR2
) RETURN VARCHAR2
IS
val VARCHAR2(4000);
i INTEGER := 1;
occ INTEGER := 1;
BEGIN
val := SUBSTR( str, i, 3 );
LOOP
i := INSTR( str, ',', 1, occ );
EXIT WHEN i = 0;
LOOP
EXIT WHEN SUBSTR( str, i + 1, 1 ) <> ' ';
i := i + 1;
END LOOP;
val := val || ',' || SUBSTR( str, i + 1, 3 );
occ := occ + 1;
END LOOP;
RETURN val;
END getFirst3CharsOfDelimitedList;
testdata as (
select 'vwe200,qwe600' as str from dual union all
select 's05200 , e0300' as str from dual
)
SELECT getFirst3CharsOfDelimitedList( str ) FROM testdata;
Output:
GETFIRST3CHARSOFDELIMITEDLIST(STR)
----------------------------------
vwe,qwe
s05,e03
Need Split function which will take two parameters, string to split and delimiter to split the string and return a table with columns Id and Data.And how to call Split function which will return a table with columns Id and Data. Id column will contain sequence and data column will contain data of the string.
Eg.
SELECT*FROM Split('A,B,C,D',',')
Result Should be in below format:
|Id | Data
-- ----
|1 | A |
|2 | B |
|3 | C |
|4 | D |
Here is how you could create such a table:
SELECT LEVEL AS id, REGEXP_SUBSTR('A,B,C,D', '[^,]+', 1, LEVEL) AS data
FROM dual
CONNECT BY REGEXP_SUBSTR('A,B,C,D', '[^,]+', 1, LEVEL) IS NOT NULL;
With a little bit of tweaking (i.e., replacing the , in [^,] with a variable) you could write such a function to return a table.
There are multiple options. See Split single comma delimited string into rows in Oracle
You just need to add LEVEL in the select list as a column, to get the sequence number to each row returned. Or, ROWNUM would also suffice.
Using any of the below SQLs, you could include them into a FUNCTION.
INSTR in CONNECT BY clause:
SQL> WITH DATA AS
2 ( SELECT 'word1, word2, word3, word4, word5, word6' str FROM dual
3 )
4 SELECT trim(regexp_substr(str, '[^,]+', 1, LEVEL)) str
5 FROM DATA
6 CONNECT BY instr(str, ',', 1, LEVEL - 1) > 0
7 /
STR
----------------------------------------
word1
word2
word3
word4
word5
word6
6 rows selected.
SQL>
REGEXP_SUBSTR in CONNECT BY clause:
SQL> WITH DATA AS
2 ( SELECT 'word1, word2, word3, word4, word5, word6' str FROM dual
3 )
4 SELECT trim(regexp_substr(str, '[^,]+', 1, LEVEL)) str
5 FROM DATA
6 CONNECT BY regexp_substr(str , '[^,]+', 1, LEVEL) IS NOT NULL
7 /
STR
----------------------------------------
word1
word2
word3
word4
word5
word6
6 rows selected.
SQL>
REGEXP_COUNT in CONNECT BY clause:
SQL> WITH DATA AS
2 ( SELECT 'word1, word2, word3, word4, word5, word6' str FROM dual
3 )
4 SELECT trim(regexp_substr(str, '[^,]+', 1, LEVEL)) str
5 FROM DATA
6 CONNECT BY LEVEL
Using XMLTABLE
SQL> WITH DATA AS
2 ( SELECT 'word1, word2, word3, word4, word5, word6' str FROM dual
3 )
4 SELECT trim(COLUMN_VALUE) str
5 FROM DATA, xmltable(('"' || REPLACE(str, ',', '","') || '"'))
6 /
STR
------------------------------------------------------------------------
word1
word2
word3
word4
word5
word6
6 rows selected.
SQL>
Using MODEL clause:
SQL> WITH t AS
2 (
3 SELECT 'word1, word2, word3, word4, word5, word6' str
4 FROM dual ) ,
5 model_param AS
6 (
7 SELECT str AS orig_str ,
8 ','
9 || str
10 || ',' AS mod_str ,
11 1 AS start_pos ,
12 Length(str) AS end_pos ,
13 (Length(str) - Length(Replace(str, ','))) + 1 AS element_count ,
14 0 AS element_no ,
15 ROWNUM AS rn
16 FROM t )
17 SELECT trim(Substr(mod_str, start_pos, end_pos-start_pos)) str
18 FROM (
19 SELECT *
20 FROM model_param MODEL PARTITION BY (rn, orig_str, mod_str)
21 DIMENSION BY (element_no)
22 MEASURES (start_pos, end_pos, element_count)
23 RULES ITERATE (2000)
24 UNTIL (ITERATION_NUMBER+1 = element_count[0])
25 ( start_pos[ITERATION_NUMBER+1] = instr(cv(mod_str), ',', 1, cv(element_no)) + 1,
26 end_pos[iteration_number+1] = instr(cv(mod_str), ',', 1, cv(element_no) + 1) ) )
27 WHERE element_no != 0
28 ORDER BY mod_str ,
29 element_no
30 /
STR
------------------------------------------
word1
word2
word3
word4
word5
word6
6 rows selected.
SQL>
You could also use DBMS_UTILITY package provided by Oracle. It provides various utility subprograms. One such useful utility is COMMA_TO_TABLE procedure, which converts a comma-delimited list of names into a PL/SQL table of names.
Read DBMS_UTILITY.COMMA_TO_TABLE
Oracle Setup:
CREATE OR REPLACE FUNCTION split_String(
i_str IN VARCHAR2,
i_delim IN VARCHAR2 DEFAULT ','
) RETURN SYS.ODCIVARCHAR2LIST DETERMINISTIC
AS
p_result SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST();
p_start NUMBER(5) := 1;
p_end NUMBER(5);
c_len CONSTANT NUMBER(5) := LENGTH( i_str );
c_ld CONSTANT NUMBER(5) := LENGTH( i_delim );
BEGIN
IF c_len > 0 THEN
p_end := INSTR( i_str, i_delim, p_start );
WHILE p_end > 0 LOOP
p_result.EXTEND;
p_result( p_result.COUNT ) := SUBSTR( i_str, p_start, p_end - p_start );
p_start := p_end + c_ld;
p_end := INSTR( i_str, i_delim, p_start );
END LOOP;
IF p_start <= c_len + 1 THEN
p_result.EXTEND;
p_result( p_result.COUNT ) := SUBSTR( i_str, p_start, c_len - p_start + 1 );
END IF;
END IF;
RETURN p_result;
END;
/
Query
SELECT ROWNUM AS ID,
COLUMN_VALUE AS Data
FROM TABLE( split_String( 'A,B,C,D' ) );
Output:
ID DATA
-- ----
1 A
2 B
3 C
4 D
If you need a function try this.
First we'll create a type:
CREATE OR REPLACE TYPE T_TABLE IS OBJECT
(
Field1 int
, Field2 VARCHAR(25)
);
CREATE TYPE T_TABLE_COLL IS TABLE OF T_TABLE;
/
Then we'll create the function:
CREATE OR REPLACE FUNCTION TEST_RETURN_TABLE
RETURN T_TABLE_COLL
IS
l_res_coll T_TABLE_COLL;
l_index number;
BEGIN
l_res_coll := T_TABLE_COLL();
FOR i IN (
WITH TAB AS
(SELECT '1001' ID, 'A,B,C,D,E,F' STR FROM DUAL
UNION
SELECT '1002' ID, 'D,E,F' STR FROM DUAL
UNION
SELECT '1003' ID, 'C,E,G' STR FROM DUAL
)
SELECT id,
SUBSTR(STR, instr(STR, ',', 1, lvl) + 1, instr(STR, ',', 1, lvl + 1) - instr(STR, ',', 1, lvl) - 1) name
FROM
( SELECT ',' || STR || ',' AS STR, id FROM TAB
),
( SELECT level AS lvl FROM dual CONNECT BY level <= 100
)
WHERE lvl <= LENGTH(STR) - LENGTH(REPLACE(STR, ',')) - 1
ORDER BY ID, NAME)
LOOP
IF i.ID = 1001 THEN
l_res_coll.extend;
l_index := l_res_coll.count;
l_res_coll(l_index):= T_TABLE(i.ID, i.name);
END IF;
END LOOP;
RETURN l_res_coll;
END;
/
Now we can select from it:
select * from table(TEST_RETURN_TABLE());
Output:
SQL> select * from table(TEST_RETURN_TABLE());
FIELD1 FIELD2
---------- -------------------------
1001 A
1001 B
1001 C
1001 D
1001 E
1001 F
6 rows selected.
Obviously you'd need to replace the WITH TAB AS... bit with where you would be getting your actual data from.
Credit Credit
Use this 'Split' function:
CREATE OR REPLACE FUNCTION Split (p_str varchar2) return sys_refcursor is
v_res sys_refcursor;
begin
open v_res for
WITH TAB AS
(SELECT p_str STR FROM DUAL)
select substr(STR, instr(STR, ',', 1, lvl) + 1, instr(STR, ',', 1, lvl + 1) - instr(STR, ',', 1, lvl) - 1) name
from
( select ',' || STR || ',' as STR from TAB ),
( select level as lvl from dual connect by level <= 100 )
where lvl <= length(STR) - length(replace(STR, ',')) - 1;
return v_res;
end;
You can't use this function in select statement like you described in question, but I hope you will find it still useful.
EDIT: Here are steps you need to do.
1. Create Object: create or replace type empy_type as object(value varchar2(512))
2. Create Type: create or replace type t_empty_type as table of empy_type
3. Create Function:
CREATE OR REPLACE FUNCTION Split (p_str varchar2) return sms.t_empty_type is
v_emptype t_empty_type := t_empty_type();
v_cnt number := 0;
v_res sys_refcursor;
v_value nvarchar2(128);
begin
open v_res for
WITH TAB AS
(SELECT p_str STR FROM DUAL)
select substr(STR, instr(STR, ',', 1, lvl) + 1, instr(STR, ',', 1, lvl + 1) - instr(STR, ',', 1, lvl) - 1) name
from
( select ',' || STR || ',' as STR from TAB ),
( select level as lvl from dual connect by level <= 100 )
where lvl <= length(STR) - length(replace(STR, ',')) - 1;
loop
fetch v_res into v_value;
exit when v_res%NOTFOUND;
v_emptype.extend;
v_cnt := v_cnt + 1;
v_emptype(v_cnt) := empty_type(v_value);
end loop;
close v_res;
return v_emptype;
end;
Then just call like this:
SELECT * FROM (TABLE(split('a,b,c,d,g')))
This function returns the nth part of input string MYSTRING.
Second input parameter is separator ie., SEPARATOR_OF_SUBSTR
and the third parameter is Nth Part which is required.
Note: MYSTRING should end with the separator.
create or replace FUNCTION PK_GET_NTH_PART(MYSTRING VARCHAR2,SEPARATOR_OF_SUBSTR VARCHAR2,NTH_PART NUMBER)
RETURN VARCHAR2
IS
NTH_SUBSTR VARCHAR2(500);
POS1 NUMBER(4);
POS2 NUMBER(4);
BEGIN
IF NTH_PART=1 THEN
SELECT REGEXP_INSTR(MYSTRING,SEPARATOR_OF_SUBSTR, 1, 1) INTO POS1 FROM DUAL;
SELECT SUBSTR(MYSTRING,0,POS1-1) INTO NTH_SUBSTR FROM DUAL;
ELSE
SELECT REGEXP_INSTR(MYSTRING,SEPARATOR_OF_SUBSTR, 1, NTH_PART-1) INTO POS1 FROM DUAL;
SELECT REGEXP_INSTR(MYSTRING,SEPARATOR_OF_SUBSTR, 1, NTH_PART) INTO POS2 FROM DUAL;
SELECT SUBSTR(MYSTRING,POS1+1,(POS2-POS1-1)) INTO NTH_SUBSTR FROM DUAL;
END IF;
RETURN NTH_SUBSTR;
END;
Hope this helps some body, you can use this function like this in a loop to get all the values separated:
SELECT REGEXP_COUNT(MYSTRING, '~', 1, 'i') INTO NO_OF_RECORDS FROM DUAL;
WHILE NO_OF_RECORDS>0
LOOP
PK_RECORD :=PK_GET_NTH_PART(MYSTRING,'~',NO_OF_RECORDS);
-- do some thing
NO_OF_RECORDS :=NO_OF_RECORDS-1;
END LOOP;
Here NO_OF_RECORDS,PK_RECORD are temp variables.
Hope this helps.
Best Query For comma separated
in This Query we Convert Rows To Column ...
SELECT listagg(BL_PRODUCT_DESC, ', ') within
group( order by BL_PRODUCT_DESC) PROD
FROM GET_PRODUCT
-- WHERE BL_PRODUCT_DESC LIKE ('%WASH%')
WHERE Get_Product_Type_Id = 6000000000007
Created PL/SQL function that can split string by specified delimiter and return result as VARRAY.
CREATE OR REPLACE FUNCTION split(p_parameters VARCHAR2, p_delimiter VARCHAR2) RETURN string_varray AS
v_delimiter_position NUMBER := 0;
v_read_position NUMBER :=1;
v_list string_varray := string_varray();
v_substring VARCHAR2(4000);
FUNCTION normalize(v_substring VARCHAR2, p_delimiter VARCHAR2) RETURN VARCHAR2 AS
BEGIN
RETURN trim(TRAILING p_delimiter FROM trim(BOTH ' ' FROM v_substring));
END normalize;
BEGIN
LOOP
v_delimiter_position := instr(p_parameters, p_delimiter, v_read_position);
IF v_delimiter_position = 0 THEN
v_delimiter_position := LENGTH(p_parameters);
END IF;
v_substring := substr(p_parameters, v_read_position, v_delimiter_position-v_read_position+1);
v_list.EXTEND;
v_list(v_list.LAST) := normalize(v_substring, p_delimiter);
v_read_position := v_delimiter_position+1;
IF v_delimiter_position = LENGTH(p_parameters) THEN
EXIT;
END IF;
END LOOP;
RETURN v_list;
END split;
string_varray is VARRAY of VARCHAR2(4000) type. Function also removes whitespaces and the start and end of each value. Invocation example:
select * from table(split('zaa, dddd,ccc', ','));
Will produce three rows in output: zaa dddd ccc
begin
for rec in (select * from table(split('shfgjsdfg,242535', ',')))
loop
dbms_output.put_line(rec.COLUMN_VALUE);
end loop;
end;
-- Output
shfgjsdfg
242535
Try like below
select
split.field(column_name,1,',','"') name1,
split.field(column_name,2,',','"') name2
from table_name
I have a string which is years separated by comma.
For example 2000,2001,2002,2005,2006,2007 and 2010.
I want to group the consecutive numbers.
My output should be 2000-2003,2005-2007 and 2010. Is there any way to do this in Oracle Stored procedure?
Disclaimer - I don't recommend using this solution "as is", but it can give ideas, and it was fun writing it
I assume you have a column with the csv strings in a table.
If you're using oracle 11gR2 then you can use recursive CTEs-
Here is a sqlfiddle demo
with t as
(
select replace(replace(v, ' and ', ','), ' ','') v
from strings
),
rcte(text, token, res) as
(
select v, regexp_substr(v, '^\d*[^,]'), regexp_substr(v, '^\d*[^,]') ||'-'
from t
union all
select regexp_replace(text, '^\d*,', ''),
regexp_substr(text, '^\d*[^,]'),
case when regexp_substr(text, '^\d*[^,]') = token then
res
when regexp_substr(text, '^\d*[^,]') = token+1 then
regexp_replace(res, '-\d*$', '-'||(token+1))
else rtrim(res, '-') || ',' || regexp_substr(text, '^\d*[^,]') || '-'
end
from rcte
where text <> token
)
select rtrim(res, '-') from rcte
where text = regexp_substr(rtrim(res, '-'), '\d*$');
(This can be done without regular expressions as well)
18:42:15 SYSTEM#dwal> l
1 with p as (
2 select replace('2000,2001,2002,2005,2006,2007 and 2010',' and ',',') s, '[0-9]{4}' r from dual
3 ), ex as (
4 select regexp_substr(s,r, 1, level) as y
5 from p
6 connect by level <= regexp_count(s, r)
7 ), grp as (
8 select connect_by_root(y) s, y
9 from ( select e1.y y, e2.y p from ex e1, ex e2 where e1.y - 1 = e2.y(+) )
10 connect by prior y = p
11 start with p is null
12 ), agg as (
13 select listagg(s||decode(max(y), s, null, '-'||max(y)), ',') within group (order by s) str
14 from grp group by s
15 )
16* select regexp_replace(str, ',', ' and ', 1, regexp_count(str, ',')) result from agg
18:42:16 SYSTEM#dwal> /
RESULT
------------------------------
2000-2002,2005-2007 and 2010
Elapsed: 00:00:00.02
use mathematics
select min(seq) as range_from , max(seq) range_to , count(*) as cnt
from sometable
group by ceil(seq/3) * 3
this part ceil(seq/3) * 3 is rounding a number to the nearest multiple of three. if you want a range of 5 just use ceil(seq/5) * 5.
Cheers !
It can be done with SQL by using analytical functions.
*Update 1: * Answer updated with parser functionality, missed in previous version.
*Update 2: * Added final string composition
with p as ( -- Parameter string
select replace('2000,2001,2002,2005,2006,2007 and 2010',' and ',',') s from dual
),
ex as ( -- Parse string to sequence
select
to_number(
substr(
s,
decode( level, 1, 1, instr(s,',',1,level-1)+1 ),
decode( instr(s,',',1,level), 0, length(s)+1, instr(s,',',1,level) )
-
decode( level, 1, 1, instr(s,',',1,level-1)+1 )
)
) as y
from p
connect by instr(s,',',1,level-1) > 0
),
period_set as (
select -- Make final string for each interval start
y,
lag(y) over (order by y) prior_y,
max(y) over (partition by 1) max_y,
y || (case when is_end > 1 then null else '-' ||end_y end) as interval_string
from
( -- For each start find interval end
select
y,
is_start,
is_end,
lead(y) over (order by y) end_y
from
( -- Find if previous/next value differs more then by one.
-- If so, mark as start/end
select
y,
nvl(y - prev_y, 100) is_start,
nvl(next_y - y, 100) is_end
from
( -- Find previous/next value in sequence
select
y,
lag(y) over (order by y) prev_y,
lead(y) over (order by y) next_y
from ex
)
)
where
is_start > 1 or is_end > 1
)
where is_start > 1
)
select
replace(
substr(
sys_connect_by_path(
decode(y,max_y,'m', null) || interval_string,
','
),2
),
',m',
' and '
) result_str
from
period_set
where
connect_by_isleaf = 1
start with
prior_y is null
connect by
prior y = prior_y
SQL Fiddle can be found here.
A great QUESTION!
Please check my logic...
with test as
(
select '2000,2002,2003,2004,2006,2007' str from dual
)
,test1 as (
select
split1,
lead(split1, 1, null) over (order by split1 asc) lead_no,
level1
from
(
select to_number(regexp_substr (str, '[^,]+', 1, rownum)) split1, level as level1
from test b
connect by level <= length (regexp_replace (str, '[^,]+')) + 1
)x
)
--select * from test1
,test2 (split1, lead_no, level1, op, op1) as(
select
split1,
lead_no,
level1,
(case when split1+1=lead_no then to_char(split1) else NULL end),
(case when split1+1=lead_no then NULL else to_char(split1) end)
from test1
where level1=1
union all
select
a.split1,
a.lead_no,
b.level1+1,
(case when a.split1+1=a.lead_no and to_char(b.op) is not null then to_char(b.op)
when a.split1+1=a.lead_no and to_char(b.op) is null then to_char(a.split1)
else null end),
(case when (a.split1+1<>a.lead_no and to_char(b.op)<>to_char(a.split1)) OR
(a.lead_no is null and to_char(b.op) is not null) then to_char(b.op) ||'-'||to_char(a.split1)
when a.lead_no is null then to_char(a.split1)
else null end)
from test1 a inner join test2 b on a.level1 = b.level1+1
)
select op1 from test2
where op1 is not null
Assuming your data will be comma seperated and input you can mentioned any years seperated by comma.
DECLARE
v_str VARCHAR(100) := '&n';
v_instr NUMBER;
v_instr1 NUMBER;
v_g VARCHAR(50);
v_F VARCHAR(50);
v_OUT VARCHAR(50);
v_OUT1 VARCHAR(50);
v_TEMP VARCHAR(50);
v_TMP VARCHAR(50) := ' ';
v_cnt NUMBER :=0;
V_FLAG NUMBER :=1;
BEGIN
FOR i IN 1..Length(v_str)-Length(REPLACE(v_str,',',''))+1 LOOP
IF i = 1 THEN
v_g := SubStr(v_str,1,InStr(v_str,',',1,i)-1);
V_F := V_G;
ELSE
v_instr := InStr(v_str,',',1,i-1);
v_instr1 := InStr(v_str,',',1,i);
IF(v_cnt+1 <= Length(v_str)-Length(REPLACE(v_str,',',''))) then
v_g := SubStr(v_str,v_instr+1,v_instr1-v_instr-1);
IF V_FLAG = 0 THEN V_F := V_G; V_FLAG :=1; END IF;
ELSE
v_g := SubStr(v_str,v_instr+1);
IF V_FLAG = 0 THEN V_F := V_G; V_FLAG :=1; END IF;
END IF;
END IF;
v_cnt := v_cnt+1;
--IF(I>1) THEN
IF(V_TEMP+1 = V_G) THEN
IF(V_OUT IS not NULL) THEN
V_OUT := V_OUT||'-'||V_G;
ELSE
V_OUT := V_F||'-'||V_G;
END IF;
ELSE
V_OUT1 := SubStr(V_OUT,1,5)||SubStr(V_OUT,-4);
V_OUT := NULL;
v_out := v_g;
V_FLAG := 0;
END IF;
--END IF;
V_TEMP := To_Number(V_G);
--Dbms_Output.put_line(v_g);
IF(v_tmp <> v_out1) THEN
SELECT Decode(instr(v_OUT1,'-'),0,subStr(V_OUT1,1,4),v_out1) INTO v_out1 FROM dual;
Dbms_Output.put_line('Year span : '||v_OUT1);
v_tmp := v_out1;
END IF;
END LOOP;
SELECT Decode(Length(v_OUT),4,v_out,subStr(V_OUT,1,5)||SubStr(V_OUT,-4)) INTO v_out1 FROM dual;
Dbms_Output.put_line('Year span : '||v_out1);
END;
You have to use cursor, to loop throught the years, and to do somethink like this :
CREATE OR REPLACE Function concatYears
RETURN varchar;
DECLARE
oldYear number;
concat varchar(300);
newGroup boolean;
cursor cur1 is
SELECT year
FROM dates
ORDER BY year ASC;
BEGIN
oldYear:=0;
newGroup:=true;
concat := '';
FOR row in c1
IF oldYear == 0 THEN
oldYear := row.year;
END IF;
IF newGroup == true THEN
concat := concat || CAST(oldYear AS varchar(4));
newGroup:= false;
ELSE
IF row.year > oldYear+1 THEN
concat:= concat || '-' || CAST(oldYear AS varchar(4)) || ' , ';
newGroup:=true;
END IF;
END IF;
oldYear:=row.year;
END LOOP;
RETURN concat;
END;