2

I'm facing a strange issue

I'm trying to convert some numbers into "words" , simply by doing this:

TO_CHAR(TO_TIMESTAMP(LPAD( nvl(trunc(99999999),0) , 9, '0'), 'FF9'),'FFSP')      AS amt_in_words

that works perfectly ,the output is "NINETY-NINE MILLION NINE HUNDRED NINETY-NINE THOUSAND NINE HUNDRED NINETY-NINE"

but what happens when we try a number like 99999998?

TO_CHAR(TO_TIMESTAMP(LPAD( nvl(trunc(99999998),0) , 9, '0'), 'FF9'),'FFSP')      AS amt_in_words

it throws an error "ORA-01877: string is too long for internal buffer"

well,it's a smaller number , it should work ,but it doesn't my theory is : it failed because the length of the resulted text would have been 79 characters , and it fails for some reason

suggestion 1: using cast (EXPR as VARCHAR(100) ) but it gave the same error

any ideas?

Clarification: unfortunately i'm working on Oracle Fusion BI buplisher, where appearantly i can't use any Pl/sql procedures , just normal select queries

user2219385
  • 45
  • 1
  • 10
  • 1
    The nvl/lpad/trim is irrelevant and a bit distracting; you can just do `TO_CHAR(TO_TIMESTAMP('099999998', 'FF9'), 'FFSP')`. Oddly using SPTH works - for a bit longer anyway. Anyway... this has been raised before as bug 18408384 but still happens in 11.2.0.4; don't have 12c to check on. – Alex Poole Mar 17 '16 at 14:36

2 Answers2

1

Split the number into groups of thousands and just process each of those:

Oracle Setup:

CREATE OR REPLACE FUNCTION TO_WORDS(
  in_value IN INT
) RETURN VARCHAR2 DETERMINISTIC
AS
  p_value INT := ABS( in_value );
  p_words VARCHAR2(4000);
  t_array CONSTANT SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST(
    null,
    ' thousand ',
    ' million ',
    ' billion ',
    ' trillion ',
    ' quadrillion ',
    ' quintillion ',
    ' sextillion ',
    ' septillion ',
    ' octillion ',
    ' decillion ',
    ' undecillion ',
    ' duodecillion '
  );
  p_counter INT := 0;
BEGIN
  
  WHILE p_value > 0 LOOP
    p_counter := p_counter + 1;
    IF MOD( p_value, 1000 ) > 0 THEN
      p_words := TO_CHAR( TO_DATE( MOD( p_value, 1000 ), 'j' ), 'jsp' ) || t_array( p_counter ) || p_words;
    END IF;
    p_value := FLOOR( p_value / 1000 );
  END LOOP;
  RETURN CASE WHEN in_value < 0 THEN RTRIM( 'minus ' || p_words )
              WHEN in_value = 0 THEN 'Zero'
                                ELSE RTRIM( p_words ) END;
END;
/

Query:

SELECT TO_WORDS( 999999999999998 ) FROM DUAL;

Output:

TO_WORDS(999999999999998)                                                      
--------------------------------------------------------------------------------
nine hundred ninety-nine trillion nine hundred ninety-nine billion nine hundred 
ninety-nine million nine hundred ninety-nine thousand nine hundred ninety-eight

Update - Using only SQL

(Note, this isn't simple)

SQL Fiddle

Oracle 11g R2 Schema Setup:

CREATE TABLE NUMBERS ( value ) AS
SELECT 123456789012345678901 FROM DUAL UNION ALL
SELECT 999000 FROM DUAL;

Query 1:

WITH words ( id, word ) AS (
  SELECT 0, null FROM DUAL UNION ALL
  SELECT 1, ' thousand' FROM DUAL UNION ALL
  SELECT 2, ' million' FROM DUAL UNION ALL
  SELECT 3, ' billion' FROM DUAL UNION ALL
  SELECT 4, ' trillion' FROM DUAL UNION ALL
  SELECT 5, ' quadrillion' FROM DUAL UNION ALL
  SELECT 6, ' quintillion' FROM DUAL UNION ALL
  SELECT 7, ' sextillion' FROM DUAL UNION ALL
  SELECT 8, ' septillion' FROM DUAL UNION ALL
  SELECT 9, ' octillion' FROM DUAL UNION ALL
  SELECT 10, ' decillion' FROM DUAL UNION ALL
  SELECT 11, ' undecillion' FROM DUAL UNION ALL
  SELECT 12, ' duodecillion' FROM DUAL
)
SELECT MAX( n.value ) AS value,
       LISTAGG(
         CASE
           WHEN MOD( FLOOR( n.value / POWER( 1000, t.column_value ) ), 1000 ) = 0
           THEN NULL
           ELSE TO_CHAR(
                  TO_DATE(
                    MOD( FLOOR( n.value / POWER( 1000, t.column_value ) ), 1000 ),
                    'j'
                  ),
                  'jsp'
                ) || w.word
         END,
         ' '
       ) WITHIN GROUP ( ORDER BY t.COLUMN_VALUE DESC )
         AS to_words
FROM   numbers n,
       TABLE(
         CAST(
           MULTISET(
             SELECT LEVEL - 1
             FROM   DUAL
             CONNECT BY POWER( 1000, LEVEL - 1 ) < n.value
           ) AS SYS.ODCINUMBERLIST
         )
       ) t,
       words w
WHERE  t.column_value = w.id
GROUP BY n.ROWID

Results:

|                 VALUE | TO_WORDS                             |
|-----------------------|--------------------------------------|
|  12345678901234568901 | one hundred twenty-three quintillion |
|                       | four hundred fifty-six quadrillion   |
|                       | seven hundred eighty-nine trillion   |
|                       | twelve billion                       |
|                       | three hundred forty-five million     |
|                       | six hundred seventy-eight thousand   |
|                       | nine hundred one                     |
|                       |                                      |
|                999000 | nine hundred ninety-nine thousand    |

An alternative SQL method is:

SELECT *
FROM   table_name t
       CROSS JOIN LATERAL (
         SELECT LISTAGG(
                  CASE
                  WHEN depth = 1 AND part = '0'
                  THEN 'ZERO'
                  WHEN part = '000'
                  THEN NULL
                  ELSE CASE
                       WHEN is_last = 0
                       THEN ' '
                       END || 
                       TO_CHAR(TO_DATE(part, 'J'), 'JSP') ||
                       CASE depth
                       WHEN 1 THEN NULL
                       WHEN 2 THEN ' THOUSAND'
                       WHEN 3 THEN ' MILLION'
                       WHEN 4 THEN ' BILLION'
                       WHEN 5 THEN ' TRILLION'
                       WHEN 6 THEN ' QAUDRILLION'
                       WHEN 7 THEN ' QUINTILLION'
                       WHEN 8 THEN ' SEXTILLION'
                       WHEN 9 THEN ' SEPTILLION'
                       WHEN 10 THEN ' OCTILLION'
                       WHEN 11 THEN ' NONILLION'
                       WHEN 12 THEN ' DECILLION'
                       WHEN 13 THEN ' UNDECILLION'
                       WHEN 14 THEN ' DUODECILLION'
                       END
                  END,
                  NULL
                ) WITHIN GROUP (ORDER BY depth DESC) AS words
         FROM   (
           SELECT CASE
                  WHEN 3*LEVEL > LENGTH(value)
                  THEN SUBSTR(value, 1, MOD(LENGTH(value), 3))
                  ELSE SUBSTR(value, -3*LEVEL, 3)
                  END AS part,
                  LEVEL AS depth,
                  CONNECT_BY_ISLEAF AS is_last
           FROM   DUAL
           CONNECT BY 3 * LEVEL - 2 <= LENGTH(value)
         )
       );

fiddle

MT0
  • 143,790
  • 11
  • 59
  • 117
  • That's great , but unfortunately i'm working on Oracle Fusion BI publisher, where apparently i can't use any procedures , just normal select queries – user2219385 Mar 20 '16 at 11:41
  • 1
    Its not a procedure - it is a function that can be used in SQL queries. If your DBA can create the function then you may be able to use it. – MT0 Mar 20 '16 at 18:15
  • unfortunately he can't , we don't have such privileges :/ – user2219385 Mar 22 '16 at 12:39
  • 1
    @user2219385 Updated with a pure SQL query to do exactly the same thing as the function... but its not simple. – MT0 Mar 22 '16 at 13:54
1

PL/SQL doesn't seem to trip over the same issue, so you could write your own function to do the conversion:

create or replace function spell_number(p_number number) return varchar2 is
  l_str varchar2(200);
begin
  l_str := to_char(to_timestamp(lpad(nvl(trunc(p_number), 0) , 9, '0'), 'FF9'), 'FFSP');
  return l_str;
end;
/

Then even longer values work:

select spell_number(99999999) from dual;

AMT_IN_WORDS                                                                                       
----------------------------------------------------------------------------------------------------
NINETY-NINE MILLION NINE HUNDRED NINETY-NINE THOUSAND NINE HUNDRED NINETY-NINE                      


select spell_number(99999998) from dual;

AMT_IN_WORDS                                                                                       
----------------------------------------------------------------------------------------------------
NINETY-NINE MILLION NINE HUNDRED NINETY-NINE THOUSAND NINE HUNDRED NINETY-EIGHT                     

select spell_number(999999999) from dual;

AMT_IN_WORDS                                                                                       
----------------------------------------------------------------------------------------------------
NINE HUNDRED NINETY-NINE MILLION NINE HUNDRED NINETY-NINE THOUSAND NINE HUNDRED NINETY-NINE         

Anything longer than 9 digits ignores the least significant digits but that seems to be a limitation of the SP format:

with t (num) as (
  select 99999998 from dual
  union all select 99999999 from dual
  union all select 999999999 from dual
  union all select 1000000000 from dual
  union all select 1000000001 from dual
  union all select 1000000010 from dual
  union all select 9999999999 from dual
  union all select 10000000000 from dual
  union all select 99999999999 from dual
  union all select 100000000000 from dual
)
select num, spell_number(num) as amt_in_words from t;

          NUM AMT_IN_WORDS                                                                                       
------------- ----------------------------------------------------------------------------------------------------
     99999998 NINETY-NINE MILLION NINE HUNDRED NINETY-NINE THOUSAND NINE HUNDRED NINETY-EIGHT                     
     99999999 NINETY-NINE MILLION NINE HUNDRED NINETY-NINE THOUSAND NINE HUNDRED NINETY-NINE                      
    999999999 NINE HUNDRED NINETY-NINE MILLION NINE HUNDRED NINETY-NINE THOUSAND NINE HUNDRED NINETY-NINE         
   1000000000 ONE HUNDRED  MILLION                                                                                
   1000000001 ONE HUNDRED  MILLION                                                                                
   1000000010 ONE HUNDRED  MILLION ONE                                                                            
   9999999999 NINE HUNDRED NINETY-NINE MILLION NINE HUNDRED NINETY-NINE THOUSAND NINE HUNDRED NINETY-NINE         
  10000000000 ONE HUNDRED  MILLION                                                                                
  99999999999 NINE HUNDRED NINETY-NINE MILLION NINE HUNDRED NINETY-NINE THOUSAND NINE HUNDRED NINETY-NINE         
 100000000000 ONE HUNDRED  MILLION                                                                                
Alex Poole
  • 183,384
  • 11
  • 179
  • 318