Oracle
 sql >> Base de Dados >  >> RDS >> Oracle

Função IsNumber personalizada da Oracle com precisão e escala


Eu não acho que exista uma maneira simples de construir; e fazer uma verificação dinâmica é relativamente fácil (veja o exemplo abaixo). Mas como uma abordagem bastante complicada, você poderia converta a string para um número e de volta para uma string usando um modelo de formato construído a partir de sua precisão e escala:
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;
/

Apenas testado com alguns valores, mas parece funcionar até agora:
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 

Usando WHEN OTHERS não é o ideal e você pode substituí-lo por manipuladores de exceção específicos. Presumi que você deseja que isso retorne null se o número não for válido, mas é claro que você pode retornar qualquer coisa ou lançar sua própria exceção.

O isNum2 column é de uma segunda função muito mais simples, que está apenas fazendo o cast dinamicamente - o que eu sei que você não quer fazer, isso é apenas para comparação:
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;
/

Mas observe que cast arredonda se a escala especificada for muito pequena para o valor; Eu posso ter interpretado "conforme" com muita força na pergunta, pois estou errando nesse caso. Se você quiser algo como '.123', 2, 2 ser permitido (dando .12 ) então o segundo GetFormat chamada e a verificação 'escala muito grande' pode ser removida do meu IsNumber . Pode haver outras nuances que eu perdi ou interpretei mal também.

Também vale a pena notar que o to_number() inicial depende das configurações de NLS para os dados e a correspondência de sessão - particularmente o separador decimal; e não permitirá um separador de grupo.

Pode ser mais simples desconstruir o valor numérico passado em sua representação interna e ver se isso se compara com a precisão e a escala... embora a rota dinâmica economize muito tempo e esforço.