Oracle Custom IsNumber Function with Precision and Scale

℡╲_俬逩灬. 提交于 2019-12-11 08:15:52

问题


Is is possible to write an Oracle function that tests to see if a string conforms to a numeric precision and scale while providing precision and scale at run-time, and not using execute immediate?

Function signature would be this:

FUNCTION IsNumber(pVALUE VARCHAR2, pPRECISION NUMBER, pSCALE NUMBER) RETURN NUMBER

It's not valid to do something like this:

DECLARE aNUMBER NUMBER(pPRECISION, pSCALE);

Any ideas on how to get something like this working?


回答1:


I don't think there's any simple built in way; and doing a dynamic check is relatively easy (see example below). But as a rather convoluted approach you could convert the string to a number and back to a string using a format model constructed from your precision and scale:

CREATE OR REPLACE FUNCTION IsNumber(pVALUE VARCHAR2, pPRECISION NUMBER,
  pSCALE NUMBER) RETURN NUMBER
IS
  lFORMAT VARCHAR2(80);
  lNUMBER NUMBER;
  lSTRING NUMBER;

  FUNCTION GetFormat(p NUMBER, s NUMBER) RETURN VARCHAR2 AS
  BEGIN
    RETURN
      CASE WHEN p >= s THEN LPAD('9', p - s, '9') END
        || CASE WHEN s > 0 THEN '.' || CASE WHEN s > p THEN
            LPAD('0', s - p, '0') || RPAD('9', p, '9')
          ELSE RPAD('9', s, '9') END
      END;
  END GetFormat;
BEGIN
  -- sanity-check values; other checks needed (precision <= 38?)
  IF pPRECISION = 0 THEN
    RETURN NULL;
  END IF;

  -- check it's actually a number
  lNUMBER := TO_NUMBER(pVALUE);

  -- get it into the expected format; this will error if the precision is
  -- exceeded, but scale is rounded so doesn't error
  lFORMAT := GetFormat(pPRECISION, pSCALE);
  lSTRING := to_char(lNUMBER, lFORMAT, 'NLS_NUMERIC_CHARACTERS='',.''');

  -- to catch scale rounding, check against a greater scale
  -- note: this means we reject numbers that CAST will allow but round
  lFORMAT := GetFormat(pPRECISION + 1, pSCALE + 1);

  IF lSTRING != to_char(lNUMBER, lFORMAT, 'NLS_NUMERIC_CHARACTERS='',.''') THEN
    RETURN NULL;  -- scale too large
  END IF;
  RETURN lNUMBER;
EXCEPTION
  WHEN OTHERS THEN
    RETURN NULL;  -- not a number, precision too large, etc.
END IsNumber;
/

Only tested with a few values but seems to work so far:

with t as (
  select '0.123' as value, 3 as precision, 3 as scale from dual
  union all select '.123', 2, 2 from dual
  union all select '.123', 1, 3 from dual
  union all select '.123', 2, 2 from dual
  union all select '1234', 4, 0 from dual
  union all select '1234', 3, 1 from dual
  union all select '123', 2, 0 from dual
  union all select '.123', 0, 3 from dual
  union all select '-123.3', 4, 1 from dual
  union all select '123456.789', 6, 3 from dual
  union all select '123456.789', 7, 3 from dual
  union all select '101.23253232', 3, 8 from dual
  union all select '101.23253232', 11, 8 from dual
)
select value, precision, scale,
  isNumber(value, precision, scale) isNum,
  isNumber2(value, precision, scale) isNum2
from t;

VALUE         PRECISION      SCALE      ISNUM     ISNUM2
------------ ---------- ---------- ---------- ----------
0.123                 3          3       .123       .123 
.123                  2          2                   .12 
.123                  1          3       .123            
.123                  2          2                   .12 
1234                  4          0       1234       1234 
1234                  3          1                       
123                   2          0                       
.123                  0          3                       
-123.3                4          1     -123.3     -123.3 
123456.789            6          3                       
123456.789            7          3                       
101.23253232          3          8                       
101.23253232         11          8 101.232532 101.232532 

Using WHEN OTHERS isn't ideal and you could replace that with specific exception handlers. I've assumed you want this to return null if the number isn't valid, but of course you could return anything, or throw your own exception.

The isNum2 column is from a second, much simpler function, which is just doing the cast dynamically - which I know you don't want to do, this is just for comparison:

CREATE OR REPLACE FUNCTION IsNumber2(pVALUE VARCHAR2, pPRECISION NUMBER,
  pSCALE NUMBER) RETURN NUMBER
IS
  str VARCHAR2(80);
  num NUMBER;
BEGIN
  str := 'SELECT CAST(:v AS NUMBER(' || pPRECISION ||','|| pSCALE ||')) FROM DUAL';
  EXECUTE IMMEDIATE str INTO num USING pVALUE;
  RETURN num;
EXCEPTION
  WHEN OTHERS THEN
    RETURN NULL;
END IsNumber2;
/

But note that cast rounds if the specified scale is too small for the value; I may have interpreted "conforms to" too strongly in the question as I'm erroring in that case. If you want something like '.123', 2, 2 to be allowed (giving .12) then the second GetFormat call and the 'scale too large' check can be removed from my IsNumber. There may be other nuances I've missed or misinterpreted as well.

Also worth noting that the initial to_number() relies on NLS settings for the data and the session matching - the decimal separator particularly; and it won't allow a group separator.

It might be simpler to deconstruct the passed numeric value into its internal representation and see if that compares with the precision and scale... though the dynamic route saves a lot of time and effort.



来源:https://stackoverflow.com/questions/25895594/oracle-custom-isnumber-function-with-precision-and-scale

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!