Adapt regex with lookahead to oracle db format - sql

After the useful answers on my previous question (see How do I create a regex to avoid a repeated number with optional hyphen?) we reached a solution that matched my needings.
The final result was:
^(?!(\d)(?:-?\1)*$)\d{2}-?\d{7}$
The above regex excludes these data:
00-0000000 and 000000000
11-1111111 and 111111111
22-2222222 and 222222222
...
99-9999999 and 999999999
Note that 22-2222221 is valid.
Note also that the position of the hyphen can be anywhere after the first digit and before the last one
Now that everything seemd to work fine we noticed that this pattern is not compatible with the oracle database REGEXP LIKE command.
Any suggestion on how to adapt it?
Thanks in advance.
I read here Oracle regular expression replacement for negative lookahead/lookbehind and the solution provided doesn't seem to work for me.

Given you comment:
the hyphen can be anywhere after the first digit and before the last one
You can do it all without regular expressions using:
SELECT *
FROM table_name
WHERE -- Check that the value as the correct length
LENGTH(value) IN (9, 10)
-- Check that the value has the correct length without hyphens
AND LENGTH(REPLACE(value, '-')) = 9
-- Check that the value has only digits or hyphens
AND TRANSLATE(value, 'a-0123456789', 'a') IS NULL
-- Check that all the characters are not either hyphens or the same as the
-- first character
AND TRANSLATE(value, 'a-' || SUBSTR(value, 1, 1), 'a') IS NOT NULL;
If the hyphen will always be the 3rd character (if it is present) then:
SELECT *
FROM table_name
WHERE -- Check that the value has the correct format
( value LIKE '_________' OR value LIKE '__-_______' )
-- Check that the other characters are digits
AND TRANSLATE(
SUBSTR(value, 1, 2) || SUBSTR(value, -7),
'a0123456789',
'a'
) IS NULL
-- Check that all the characters are not either hyphens or the same as the
-- first character
AND TRANSLATE(value, 'a-' || SUBSTR(value, 1, 1), 'a') IS NOT NULL;
If you want to use regular expressions then you will need two regular expressions:
SELECT *
FROM table_name
WHERE REGEXP_LIKE(value, '^\d{2}-?\d{7}$')
AND NOT REGEXP_LIKE(value, '^(\d)\1-?\1{7}$');
or for hyphens anywhere:
SELECT *
FROM table_name
WHERE REGEXP_LIKE(value, '^\d+-?\d+$')
AND REGEXP_LIKE(value, '^[0-9-]{9,10}$')
AND NOT REGEXP_LIKE(value, '^(\d)(-?\1){8}$');
Alternatively, you can enable Java inside the database and use look-ahead via a Java method and the regular expression:
^(?!(\d)(-?\1){8}$)(?=(\d{9}|[0-9-]{10})$)\d+-?\d+$
fiddle

Related

Regexp substr until underscore + digit

I have the this string: 'STRING_EXA2MP_3LE'. I want to obtain all the characters until there is an underscore followed by a digit, which in this case would output 'STRING_EXA2MP'. How could I obtain this?
This is what I have tried so far.
SELECT
regexp_substr('STRING_EXA2MP_3LE', '[^(_0-9]+', 1, 1)
FROM
dual
Select regexp_replace('STRING_EXA2MP_3LE', '_[0-9].*') from dual
I suggest
SELECT regexp_substr('STRING_EXA2MP_3LE', '(.+)_[0-9]+', 1, 1, NULL, 1)
FROM dual
The subexpression in parentheses ((.+)), which is subexpression #1 (important later) says "Match 1 or more of any character". The rest of the expression (_[0-9]+) says "Match an underscore followed by one or more numeric digits". The last argument to REGEXP_SUBSTR says "Return the value of subexpression #1". So using the subexpression here is important as it allows you to extract a portion of the matched string.
db<>fiddle here
Try this:
SELECT regexp_substr('STRING_EXA2MP_3LE', '(.+_?)_[0-9]?', 1, 1,'i',1)
FROM dual;

why output is null at select translate(' #',' ','') from dual; and why resulit is # at select replace(' #',' ','') from dual;

Basically translate will change character to character and Replace string to string , and here i have tried to remove spaces using translate to count the number words .
select translate(' #',' ','') from dual;
select replace(' #',' ','') from dual;
select ename , nvl(length(replace(TRANSLATE(upper(trim(ename)),'ABCDEFGHIJKLMNOPQRSTUVWXYZ'' ',' # '),' ',''))+1,1) NOOFWORDs
from emp;
Unfortunately Oracle has made many bizarre choices around null vs. empty string.
One of those has to do with TRANSLATE. TRANSLATE will return NULL if any of its arguments (including the last one) is NULL, no matter what the logical behavior should be.
So, to remove spaces (say) with TRANSLATE, you must add a character you do NOT want to be removed to both the second and the third argument. I added the lower-case letter z, but you could add anything (a dot, the digit 0, whatever - just make sure you add the same character at the beginning of both arguments)
... translate (input_string, 'z ', 'z') ....
For example:
select translate(' #','z ','z') from dual;
TRANSLATE('#','Z','Z')
------------------------
#
select translate(' #',' ','') from dual;
Returns NULL because in Oracle empty strings unfortunately yield NULLs. Therefore it's equivalent to
SELECT translate(' #', ' ', NULL)
FROM dual;
and translate() returns NULL when an argument is null. Actually this is well documented in "TRANSLATE":
(...)
You cannot use an empty string for to_string to remove all characters in from_string from the return value. Oracle Database interprets the empty string as null, and if this function has a null argument, then it returns null.
If you want to replace one character, use replace() as you already did. For a few but more than one characters you can nest the replace()s.
This however gets unhandy, when you want to replace quite a lot of characters. In such a situation, if the replacement character is only one character or the empty string regexp_replace() using a character class or alternates may come in handy.
For example
SELECT regexp_replace('a12b478c01', '[0-9]', '')
FROM dual;
replaces all the digits so just 'abc' remains and
SELECT regexp_replace('ABcc1233', 'c|3', '')
FROM dual;
removes any '3' or 'c' and results in 'AB12'. In your very example
SELECT regexp_replace(' #', ' ', '')
FROM dual;
would also work and give you '#'. Though in the simple case of your example a simple replace() is enough.

regexp_substr strip text between first forward slash and second one

/abc/required_string/2/ should return abc with regexp_substr
SELECT REGEXP_SUBSTR ('/abc/blah/blah/', '/([a-zA-Z0-9]+)/', 1, 1, NULL, 1) first_val
from dual;
You might try the following:
SELECT TRIM('/' FROM REGEXP_SUBSTR(mycolumn, '^\/([^\/]+)'))
FROM mytable;
This regular expression will match the first occurrence of a pattern starting with / (I habitually escape /s in regular expressions, hence \/ which won't hurt anything) and including any non-/ characters that follow. If there are no such characters then it will return NULL.
Hope this helps.
You can search for /([^/]+)/, which says:
/ forward slash
( start of subexpression (usually called "group" in other languages)
[^/] any character other than forward slash
+ match the preceding expression one or more times
) end of subexpression
/ forward slash
You can use the 6th argument to regexp_substr to select a subexpression.
Here we pass 1 to match only the characters between the /s:
select regexp_substr(txt, '/([^/]+)/', 1, 1, null, 1)
from t1
See it working at SQL Fiddle.
Classic SUBSTR + INSTR offer a simple solution; I know you specified regular expressions, but - consider this too, might work better for a large data volume.
SQL> with test (col) as
2 (select '/abc/required_string/2/' from dual)
3 select substr(col, 2, instr(col, '/', 1, 2) - 2) result
4 from test;
RES
---
abc
SQL>
Here's another way to get the 2nd occurrence of a string of characters followed by a forward slash. It handles the problem if that element happens to be NULL as well. Always expect the unexpected!
Note: If you use the regex form of [^/]+, and that element is NULL it will return "required string" which is NOT what you expect! That form does NOT handle NULL elements. See here for more info: [https://stackoverflow.com/a/31464699/2543416]
with tbl(str) as (
select '/abc/required_string/2/' from dual union all
select '//required_string1/3/' from dual
)
select regexp_substr(str, '(.*?)(/)', 1, 2, null, 1)
from tbl;

How to use regexp_substr() with group of delimiter characters?

I have a string something like this 'SERO02~~~NA_#ERO5'. I need to sub string it using delimiter ~~~. So can get SERO02 and NA_#ERO5 as result.
I create an regex experession like this:
select regexp_substr('SERO02~~~NA_#ERO5' ,'[^~~~]+',1,2) from dual;
It worked fine and returns : NA_#ERO5
But if I change the string to ERO02~NA_#ERO5 the result is still same.
But I expect the expression to return nothing since delimiter ~~~ is not found in that string. Can someone help me out to create correct expression?
[^~~~] matches a single character that is not one of the characters following the caret in the square brackets. Since all those characters are identical then [^~~~] is the same as [^~].
You can match it using:
SELECT REGEXP_SUBSTR(
'SERO02~~~NA_#ERO5',
'~~~(.*?)(~~~|$)',
1,
1,
NULL,
1
)
FROM DUAL;
Which will match ~~~ then store zero-or-more characters in a capture group (the round brackets () indicates a capture group) until it finds either ~~~ or the end-of-string. It will then return the first capture group.
You can do it without regular expressions, with a bit of logics:
with test(text) as ( select 'SERO02~~~NA_#ERO5' from dual)
select case
when instr(text, '~~~') != 0 then
substr(text, instr(text, '~~~') + 3)
else
null
end
from test
This will give the part of the string after '~~~', if it exists, null otherwise.
You can edit the ELSE part to get what you need when the input string does not contain '~~~'.
Even using regexp,to match the string '~~~', you need to write it exactly, without []; the [] is used to list a set of characters, so [aaaaa] is exactly the same than [a],while [abc] means 'a' OR 'b' OR 'c'.
With regexp, even if not necessary, one way could be the following:
substr(regexp_substr(text, '~~~.*'), 4)
In case you want all elements. Handles NULL elements too:
SQL> with tbl(str) as (
select 'SERO02~~~NA_#ERO5' from dual
)
select regexp_substr(str, '(.*?)(~~~|$)', 1, level, null, 1) element
from tbl
connect by level <= regexp_count(str, '~~~') + 1;
ELEMENT
-----------------
SERO02
NA_#ERO5
SQL>

PLSQL show digits from end of the string

I have the following problem.
There is a String:
There is something 2015.06.06. in the air 1234567 242424 2015.06.07. 12125235
I need to show only just the last date from this string: 2015.06.07.
I tried with regexp_substr with insrt but it doesn't work.
So this is just test, and if I can solve this after it with this solution I should use it for a CLOB query where there are multiple date, and I need only the last one. I know there is regexp_count, and it is help to solve this, but the database what I use is Oracle 10g so it wont work.
Can somebody help me?
The key to find the solution of this problem is the idea of reversing the words in the string presented in this answer.
Here is the possible solution:
WITH words AS
(
SELECT regexp_substr(str, '[^[:space:]]+', 1, LEVEL) word,
rownum rn
FROM (SELECT 'There is something 2015.06.06. in the air 1234567 242424 2015.06.07. 2015.06.08 2015.06.17. 2015.07.01. 12345678999 12125235' str
FROM dual) tab
CONNECT BY LEVEL <= LENGTH(str) - LENGTH(REPLACE(str, ' ')) + 1
)
, words_reversed AS
(
SELECT *
FROM words
ORDER BY rn DESC
)
SELECT regexp_substr(word, '\d{4}\.\d{2}\.\d{2}', 1, 1)
FROM words_reversed
WHERE regexp_like(word, '\d{4}\.\d{2}\.\d{2}')
AND rownum = 1;
From the documentation on regexp_substr, I see one problem immediately:
The . (period) matches any character. You need to escape those with a backslash: \. in order to match only a period character.
For reference, I am linking this post which appears to be the approach you are taking with substr and instr.
Relevant documentation from Oracle:
INSTR(string , substring [, position [, occurrence]])
When position is negative, then INSTR counts and searches backward from the end of string. The default value of position is 1, which means that the function begins searching at the beginning of string.
The problem here is that your regular expression only returns a single value, as explained here, so you will be giving the instr function the appropriate match in the case of multiple dates.
Now, because of this limitation, I recommend using the approach that was proposed in this question, namely reverse the entire string (and your regular expression, i.e. \d{2}\.\d{2}\.\d{4}) and then the first match will be the 'last match'. Then, perform another string reversal to get the original date format.
Maybe this isn't the best solution, but it should work.
There are three different PL/SQL functions that will get you there.
The INSTR function will identify where the first "period" in the date string appears.
SUBSTR applied to the entire string using the value from (1) as the start point
TO_DATE for a specific date mask: YYYY.MM.DD will convert the result from (2) into a Oracle date time type.
To make this work in procedural code, the standard blocks apply:
DECLARE
v_position pls_integer;
... other variables
BEGIN
sql code and function calls;
END
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE finddate
(column1 varchar2(11), column2 varchar2(39))
;
INSERT ALL
INTO finddate (column1, column2)
VALUES ('row1', '1234567 242424 2015.06.07. 12125235')
INTO finddate (column1, column2)
VALUES ('string2', '1234567 242424 2015.06.07. 12125235')
SELECT * FROM dual
;
Query 1:
select instr(column2,'.',1) from finddate
where column1 = 'string2'
select substr(column2,(20-4),10) from finddate
select to_date('2015.06.07','YYYY.MM.DD') from finddate
Results:
| TO_DATE('2015.06.07','YYYY.MM.DD') |
|------------------------------------|
| June, 07 2015 00:00:00 |
| June, 07 2015 00:00:00 |
Here's a way using regexp_replace() that should work with 10g, assuming the format of the lines will be the same:
with tbl(col_string) as
(
select 'There is something 2015.06.06. in the air 1234567 242424 2015.06.07. 12125235'
from dual
)
select regexp_replace(col_string, '^.*(\d{4}\.\d{2}\.\d{2})\. \d*$', '\1')
from tbl;
The regex can be read as:
^ - Match the start of the line
. - followed by any character
* - followed by 0 or more of the previous character (which is any character)
( - Start a remembered group
\d{4}\.\d{2}\.\d{2} - 4 digits followed by a literal period followed by 2 digits, etc
) - End the first remembered group
\. - followed by a literal period
- followed by a space
\d* - followed by any number of digits
$ - followed by the end of the line
regexp_replace then replaces all that with the first remembered group (\1).
Basically describe the whole line as a regular expression, group around what you want to return. You will most likely need to tweak the regex for the end of the line if it could be other characters than digits but this should give you an idea.
For the sake of argument this works too ONLY IF there are 2 occurrences of the date pattern:
with tbl(col_string) as
(
select 'There is something 2015.06.06. in the air 1234567 242424 2015.06.07. 12125235' from dual
)
select regexp_substr(col_string, '\d{4}\.\d{2}\.\d{2}', 1, 2)
from tbl;
returns the second occurrence of the pattern. I expect the above regexp_replace more accurately describes the solution.