How to efficiently convert text to number in Oracle PL/SQL with non-default NLS_NUMERIC_CHARACTERS? - oracle

I'm trying to find an efficient, generic way to convert from string to a number in PL/SQL, where the local setting for NLS_NUMERIC_CHARACTERS settings is inpredictable -- and preferable I won't touch it. The input format is the programming standard "123.456789", but with an unknown number of digits on each side of the decimal point.
select to_number('123.456789') from dual;
-- only works if nls_numeric_characters is '.,'
select to_number('123.456789', '99999.9999999999') from dual;
-- only works if the number of digits in the format is large enough
-- but I don't want to guess...
to_number accepts a 3rd parameter but in that case you to specify a second parameter too, and there is no format spec for "default"...
select to_number('123.456789', null, 'nls_numeric_characters=''.,''') from dual;
-- returns null
select to_number('123.456789', '99999D9999999999', 'nls_numeric_characters=''.,''') from dual;
-- "works" with the same caveat as (2), so it's rather pointless...
There is another way using PL/SQL:
CREATE OR REPLACE
FUNCTION STRING2NUMBER (p_string varchar2) RETURN NUMBER
IS
v_decimal char;
BEGIN
SELECT substr(VALUE, 1, 1)
INTO v_decimal
FROM NLS_SESSION_PARAMETERS
WHERE PARAMETER = 'NLS_NUMERIC_CHARACTERS';
return to_number(replace(p_string, '.', v_decimal));
END;
/
select string2number('123.456789') from dual;
which does exactly what I want, but it doesn't seem efficient if you do it many, many times in a query. You cannot cache the value of v_decimal (fetch once and store in a package variable) because it doesn't know if you change your session value for NLS_NUMERIC_CHARACTERS, and then it would break, again.
Am I overlooking something? Or am I worrying too much, and Oracle does this a lot more efficient then I'd give it credit for?

The following should work:
SELECT to_number(:x,
translate(:x, '012345678-+', '999999999SS'),
'nls_numeric_characters=''.,''')
FROM dual;
It will build the correct second argument 999.999999 with the efficient translate so you don't have to know how many digits there are beforehand. It will work with all supported Oracle number format (up to 62 significant digits apparently in 10.2.0.3).
Interestingly, if you have a really big string the simple to_number(:x) will work whereas this method will fail.
Edit: support for negative numbers thanks to sOliver.

If you are doing a lot of work per session, an option may be to use
ALTER SESSION SET NLS_NUMERIC_CHARACTERS = '.,'
at the beginning of your task.
Of course, if lots of other code is executed in the same session, you may get funky results :-)
However we are able to use this method in our data load procedures, since we have dedicated programs with their own connection pools for loading the data.

Sorry, I noticed later that your question was for the other way round. Nevertheless it's noteworthy that for the opposite direction there is an easy solution:
A bit late, but today I noticed the special format masks 'TM9' and 'TME' which are described as "the text minimum number format model returns (in decimal output) the smallest number of characters possible." on https://docs.oracle.com/cloud/latest/db112/SQLRF/sql_elements004.htm#SQLRF00210.
It seems as if TM9 was invented just to solve this particular problem:
select to_char(1234.5678, 'TM9', 'NLS_NUMERIC_CHARACTERS=''.,''') from dual;
The result is '1234.5678' with no leading or trailing blanks, and a decimal POINT despite my environ containing NLS_LANG=GERMAN_GERMANY.WE8MSWIN1252, which would normally cause a decimal COMMA.

select to_number(replace(:X,'.',to_char(0,'fmd'))) from dual;
btw
select to_number(replace('1.2345e-6','.',to_char(0,'fmd'))) from dual;
and if you want more strict
select to_number(translate(:X,to_char(0,'fmd')||'.','.'||to_char(0,'fmd'))) from dual;

Is it realistic that the number of digits is unlimited?
If we assume it is then isn't it a good reason to look into the requirements more carefully?
If we have that fantastic situation when the initial string is super long, then the following does the trick:
select
to_number(
'11111111.2222'
, 'FM' || lpad('9', 32, '9') || 'D' || lpad('9', 30, '9')
, 'NLS_NUMERIC_CHARACTERS=''.,'''
)
from
dual

Related

Is LPAD allowed in PLSQL?

I have a PL/SQL script with a variable called v_credit_hours that is a number data type. I fetch my fields from a cursor and insert them into a table where the credit_hours field is also a number. I need to pad v_credit_hours with leading zeros and end up with four digits before inserting it. So 50 hours should like like 0050. I have this line just before my insert statement.
v_credit_hours := LPAD(ROUND(v_credit_hours), 4, 0);
The problem is that it does not change anything. 50 still comes out as 50. Nothing happens if I change 4 to 10 or if I remove LPAD completely. I tried changing v_credit_hours to v_credit_hours * 8 inside the ROUND, and that altered the result. It is as if Oracle is just ignoring LPAD. It comes out fine in this query, but not when I use PL/SQL. I also tried adding TO_CHAR between LPAD and ROUND, but that did nothing.
SELECT LPAD(ROUND(50), 4, 0) FROM dual;
Can I not use LPAD in this way? I can do it up in my original cursor, but I really only wanted to see the leading zeros in the final output.
Its sorta hard to tell if v_credit_hours is typed as a Number or a Varchar. However, if you do it this way you will get what you want.
DECLARE
v_formatted_credit_hours varchar2(60);
BEGIN
SELECT lpad(round(50),4,0)
INTO v_formatted_credit_hours
FROM dual ;
SYS.dbms_output.put_line( v_formatted_credit_hours) ;
END;
hopefully that helps to shine a little light on your issue.

Oracle: Why does to_date() accept 2 digit year when I have 4 digits in format string? - and how to enforce 4 digits?

I want to check a date for correctness. (Let's not talk about the fact, that the date is stored in a varchar please ...)
Dates are stored like DDMMYYYY so for instance 24031950 is a correct date. 240319 is not.
So I do this, when the call works, it's a correct date:
select to_date('24031950','DDMMYYYY') from dual;
But unfortunately this also does not return an error:
select to_date('240319','DDMMYYYY') from dual (why?);
But it's interesting, that this one does not work:
select to_date('190324','YYYYMMDD') from dual;
So, how to enforce a check o 4 digit year with the given format mask?
Thanks!
Quoting the docs:
Oracle Database converts strings to dates with some flexibility. [...]
And I believe that flexibility is what you are seeing. You can turn it off with the fx modifier:
FX: Requires exact matching between the character data and the format model
select to_date('240319','fxDDMMYYYY') from dual;
Gives an ORA-01862 error.
Just an additional note to Mat's answer. fx acts like a switch in the string.
For example TO_DATE('2019-11-5','fxYYYY-MM-DD') gives an ORA-01862 error because exact matching applies for the entire string. If you need exact match only for parts of the string, then use for example
TO_DATE('2019-11-5','fxYYYY-MM-fxDD')
In this case YYYY-MM- has to match exactly, whereas DD applies flexible (or lazy) match.
To check whether it is in correct format without exceptions you can also use regex functions.
One possible way would to be check if the string contains 8 digits:
select REGEXP_INSTR ('24031950', '[0-9]{8}') from dual;
1
select REGEXP_INSTR ('240350', '[0-9]{8}') from dual;
0

Oracle determinism requirements and idiosyncrasies

I've been troubled by my lack of understanding about an issue that periodically emerges: Function-Determinicity.
From the docs, it seems fairly clear:
A DETERMINISTIC function may not have side effects.
A DETERMINISTIC function may not raise an unhandled exception.
As these are important core concepts with robust, central implementations in standard packages, I don't think there is a bug or anything (the fault lies in my assumptions and understanding, not Oracle). That being said, both of these requirements sometimes appear to have some idiosyncratic uses within the standard package and the DBMS_ and UTL_ packages.
I hoped to post a couple of examples of Oracle functions that raise some doubts for me in my use of DETERMINISTIC and the nuances in these restrictions, and see if anyone can explain how things fit together. I apologize this is something of a "why" question and it can be migrated if needed, but the response to this question: (Is it ok to ask a question where you've found a solution but don't know why something was behaving the way it was?) made me think it might be appropriate for SO.
Periodically in my coding, I face uncertainty whether my own UDFs qualify as pure, and at other times, I use Oracle functions that surprise me greatly to learn they are impure. If anyone can take a look and advise, I would be grateful.
As a first example, TO_NUMBER. This function seems pure, but it also throws exceptions. In this example I'll use TO_NUMBER in a virtual column (DETERMINISTIC should be required here)
CREATE TABLE TO_NUMBER_IS_PURE_BUT_THROWS (
SOURCE_TEXT CHARACTER VARYING(5 CHAR) ,
NUMERICIZATION NUMBER(5 , 0) GENERATED ALWAYS AS (TO_NUMBER(SOURCE_TEXT , '99999')) ,
CONSTRAINT POSITIVE_NUMBER CHECK (NUMERICIZATION >= 0)
);
Table TO_NUMBER_IS_PURE_BUT_THROWS created.
INSERT INTO TO_NUMBER_IS_PURE_BUT_THROWS VALUES ('0',DEFAULT);
INSERT INTO TO_NUMBER_IS_PURE_BUT_THROWS VALUES ('88088',DEFAULT);
INSERT INTO TO_NUMBER_IS_PURE_BUT_THROWS VALUES ('UH-OH',DEFAULT);
1 row inserted.
1 row inserted.
ORA-01722: invalid number
The ORA-01722 would seem to violate the unhandled-exception requirement. Presumably any function I create that casts via TO_NUMBER should handle this possibility to remain pure. But throwing the exception here seems appropriate, and reliable. It seems there is some debate about whether exceptions violate referential-transparency (Why is the raising of an exception a side effect?)
The second situation I encounter is System functions that seem like they should-be DETERMINISTIC but arent't. There must be some reason they are considered impure. In some cases, it seems unfathomable that the internals would be generating side-effects.
An extreme example of this could be DBMS_ASSERT.NOOP though there are many others. The function returns its input unmodified. How can it be nondeterministic?
CREATE TABLE HOW_IS_NOOP_IMPURE (
SOURCE_TEXT VARCHAR2(256 BYTE),
COPY_TEXT VARCHAR2(256 BYTE) GENERATED ALWAYS AS (DBMS_ASSERT.NOOP(SOURCE_TEXT)),
CONSTRAINT COPY_IS_NOT_NULL CHECK(COPY_TEXT IS NOT NULL)
);
Yields:
ORA-30553: The function is not deterministic
Presumably it violates the requirements for determinicity, but that is hard to imagine. I wondered what I'm missing in my presumption that functions like this would be deterministic.
EDIT In response to Lukasz's comment about session settings:
I can accept it if cross-session repeatability is the root cause of functions like NOOPnot being DETERMINISTIC, but TO_CHAR is deterministic/eligible for use in virtual columns et al. but appears to have sensitivity to session settings in its format masks:
ALTER SESSION SET NLS_NUMERIC_CHARACTERS = '._';
Session altered.
CREATE TABLE TO_CHAR_NLS(
INPUT_NUMBER NUMBER(6,0),
OUTPUT_TEXT CHARACTER VARYING(64 CHAR) GENERATED ALWAYS AS (TO_CHAR(INPUT_NUMBER,'999G999'))
);
Table TO_CHAR_NLS created.
INSERT INTO TO_CHAR_NLS VALUES (123456,DEFAULT);
INSERT INTO TO_CHAR_NLS VALUES (111222,DEFAULT);
SELECT INPUT_NUMBER, OUTPUT_TEXT FROM TO_CHAR_NLS ORDER BY 1 ASC;
1 row inserted.
1 row inserted.
INPUT_NUMBER OUTPUT_TEXT
111222 111_222
123456 123_456
The ORA-01722 would seem to violate the unhandled-exception
requirement. Presumably any function I create that casts via TO_NUMBER
should handle this possibility to remain pure.
Firstly, i must appreciate you for asking such a good question. Now, when you say you used TO_NUMBER, it should convert all the text inputted to the function but you should know that TO_NUMBER has some restrictions.
As per TO_NUMBER definition:
The TO_NUMBER function converts a formatted TEXT or NTEXT expression
to a number. This function is typically used to convert the
formatted numerical output of one application (which includes currency symbols, decimal markers, thousands group markers, and so
forth) so that it can be used as input to another application.
It clearly says,it used to cast the formatted numerical output of one application, that means TO_NUMBER itself expect a numerical input and when you write as below:
INSERT INTO TO_NUMBER_IS_PURE_BUT_THROWS VALUES ('UH-OH',DEFAULT);
You completely passed the unexpected input to TO_NUMBER function and hence it throws the error ORA-01722: invalid number as expected behavior.
Read more about TO_NUMBER.
Secondly,
An extreme example of this could be DBMS_ASSERT.NOOP though there are
many others. The function returns its input unmodified. How can it be
nondeterministic?
DBMS_ASSERT.NOOP function is can be used where someone passing actual piece of code through a variable and don't want it to be checked for SQL injection attacks.
This has to be nondeterministic as it just return what we input to the function.
I show you a example to demonstrate why this has to be non-deterministic.
Let's say i create a function years_from_today as deterministic.
CREATE OR REPLACE FUNCTION years_from_today
( p_date IN DATE )
RETURN NUMBER DETERMINISTIC IS
BEGIN
RETURN ABS(MONTHS_BETWEEN(SYSDATE, p_date) / 12);
END years_from_today;
/
Now i create a table and use this function in a query as below:
CREATE TABLE det_test AS
SELECT TO_DATE('01-JUL-2009', 'DD-MON-YYYY') AS date_value
FROM dual;
SELECT date_value, SYSDATE, years_from_today(date_value)
FROM det_test
WHERE years_from_today(date_value) < 2;
Output
DATE_VALU SYSDATE YEARS_FROM_TODAY(DATE_VALUE)
--------- --------- ----------------------------
01-JUL-09 20-SEP-10 1.21861774
Then i create a function-based index on the new table.
CREATE INDEX det_test_fbi ON det_test (years_from_today(date_value));
Now, to see the implications of our DETERMINISTIC choice, change the date on the server (in a test environment of course) to move ahead a full year. Even though the date has changed, running the query again will still return the same value as before from YEARS_FROM_TODAY, along with the same row, because the index is used instead of executing the function.
SELECT date_value, SYSDATE, years_from_today(date_value)
FROM det_test
WHERE years_from_today(date_value) < 2;
Output:
DATE_VALU SYSDATE YEARS_FROM_TODAY(DATE_VALUE)
--------- --------- ----------------------------
01-JUL-09 20-SEP-11 1.2186201
Without the WHERE clause, the query should return the following:
DATE_VALU SYSDATE YEARS_FROM_TODAY(DATE_VALUE)
--------- --------- ----------------------------
01-JUL-09 20-SEP-11 2.21867063
As is evident from the erroneous output, a function should never be created as deterministic unless it will ALWAYS return the same value given the same parameters.
And hence your assumption to make DBMS_ASSERT.NOOP doesnot stands true in all the cases.

How to compare Clob column for multiple values?

Can anyone tell me how to compare column which has clob datatype in oracle for multiple values?
For one value we are comparing like
dbms_lob.compare(attr_value,'A')=0
Similarly if I want to know whether attr_value is in ('A','B','C','D'). I tried this:
dbms_lob.compare(attr_value,'A')=0 or dbms_lob.compare(attr_value,'B')=0 or ...
This is not giving me proper result. Is there any other way?
OR should work fine. Also you may try this:
SELECT * FROM your_tab WHERE CAST(s as VARCHAR2(2)) IN ('A', 'B', 'C', 'D');
but I'm not sure about the performance.
Since it seems you don't really want to compare CLOBS of massive size with a bunch of other massive CLOBS, the fastest way would be to just compare a Substring of the CLOB:
WHERE DBMS_LOB.SUBSTR( attr_value, 4000, 1 ) IN ('A','B','C')
Here 4000 can be replaced by the maximum length of all you comparison values.
If you really want to compare massive CLOBS I don't think a select is the right approach, you
should probably rework your application logic...
DBMS_LOB.COMPARE does an exact comparison between two LOB objects. The documentation says:
COMPARE returns zero if the data exactly matches over the range
specified by the offset and amount parameters. Otherwise, a nonzero
INTEGER is returned.
On Oracle 11g, you could use REGEXP_INSTR function:
SELECT REGEXP_INTR(attr_value,'A|B|C|D|E') from dual;
I hope it helps.

Confusion using to_date function to confirm date format

select to_date('07/09/14','yyyy-mm-dd') from dual;
is returning 14-SEP-07
I was expecting it to thrown an exception as the date and the format requested are not the same. Secondly we have slashes in the input date and hypen in the format.
Can someone tell me how to confirm if the input value is of the provided format.
to_date is relatively liberal in attempting to convert the input string using the provided format mask. It generally doesn't concern itself with the specific separator character in the string or in the format mask-- your string could use dashes or slashes or, heck, asterixes if you wanted. Of course, that can mean that you get unexpected results. In this case, for example, the date that is created is in the year 7 (i.e. 2007 years ago). That is a valid date in Oracle but it is highly unlikely to be the date you expect unless you're storing data about ancient Rome.
What, exactly, does it mean to you to "confirm if the input value is of the provided format"? Depending on what you are looking for, you may want to use regular expressions.
REGEXP_LIKE can somewhat do this. Note, this is basic, since it would accept values like 2014-0-32. However, those insane values would fail in whatever you do next in your code, such as to_date().
SELECT 'Yes, valid boss.' is_valid FROM DUAL
WHERE regexp_like('07/09/14','^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$');
no rows selected
.
SELECT 'Yes, valid boss.' is_valid FROM DUAL
WHERE regexp_like('2014-07-09','^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$');
IS_VALID
----------------
Yes, valid boss.
.
SELECT 'Yes, valid boss.' is_valid FROM DUAL
WHERE regexp_like('2014-7-9','^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$');
IS_VALID
----------------
Yes, valid boss.
EDIT: and if you're into PL/SQL, you can do a regex match and throw your own exception ...
DECLARE
v_is_valid INTEGER;
BEGIN
SELECT count(*) INTO v_is_valid FROM DUAL
WHERE regexp_like('07/09/14','^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$');
IF v_is_valid = 0 THEN
raise_application_error (-20400, 'Exception: date was given in the wrong format.');
END IF;
END;
/
*
ORA-20400: Exception: date was given in the wrong format.
TO_DATE() only takes a string and convert it to a date/time in respect to the given format your provided. If it can't find the exact format, it will do its best to determine what could it be. Given your 'yyyy-mm-dd' format, Oracle deduces that the 07 is the year, and the 14 is the day.
You have to write your own function to throw an expected exception or the like to handle the issue, if there is any based on your functional requirements.
So when you select the resulting date, the format that is displayed is actually based on your NLS_DATE_FORMAT system parameter, which gives your resulting date.

Resources