sql >> Base de Datos >  >> RDS >> Oracle

Función Oracle Custom IsNumber con precisión y escala

No creo que haya una manera simple de construir; y hacer una verificación dinámica es relativamente fácil (vea el ejemplo a continuación). Pero como un enfoque bastante enrevesado, podría convierta la cadena en un número y vuelva a ser una cadena usando un modelo de formato construido a partir de su precisión y 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;
/

Solo se probó con algunos valores, pero parece funcionar hasta ahora:

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 no es ideal y podría reemplazarlo con controladores de excepción específicos. Supuse que desea que esto devuelva nulo si el número no es válido, pero, por supuesto, puede devolver cualquier cosa o lanzar su propia excepción.

El isNum2 column es de una segunda función mucho más simple, que solo está haciendo el lanzamiento dinámicamente, lo que sé que no quieres hacer, esto es solo para comparar:

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;
/

Pero tenga en cuenta que cast redondea si la escala especificada es demasiado pequeña para el valor; Es posible que haya interpretado "se ajusta a" con demasiada fuerza en la pregunta, ya que me estoy equivocando en ese caso. Si quieres algo como '.123', 2, 2 para ser permitido (dando .12 ) luego el segundo GetFormat la llamada y la verificación de "escala demasiado grande" se pueden eliminar de mi IsNumber . Puede haber otros matices que también me he perdido o malinterpretado.

También vale la pena señalar que el to_number() inicial se basa en la configuración de NLS para los datos y la coincidencia de sesión, en particular el separador decimal; y no permitirá un separador de grupo.

Podría ser más sencillo deconstruir el valor numérico pasado en su representación interna y ver si eso se compara con la precisión y la escala... aunque la ruta dinámica ahorra mucho tiempo y esfuerzo.