Utility assortment

ECHO (word-wrapper for DBMS_OUTPUT), TO_DECIMAL, TO_BASE (number conversion functions), SEND_MAIL (HTML formatted email), LIST_ELEMENT (string tokeniser), SPLIT, TO_STRING etc, all in one rather arbitrary package.

-- Various PL/SQL utilities. Help yourself, but credit me occasionally and don't sue me if they break.
-- Written for Oracle 9i. (How time flies.)
-- UTILITIES is a bit of a non-name. For production use, these procedures and functions might be better
-- moved into more tightly defined packages.
--
-- ECHO:         Word-wrapper for DBMS_OUTPUT.PUT_LINE, avoiding 255-char limitation.
--               Also standalone procedure which just calls package version.
-- TO_DECIMAL:   Convert number in another base e.g. hex to deciimal.
-- TO_BASE:      (1): Convert any number from decimal to base specified (default 10)
--               (2): Convert between any two specified bases, e.g. octal to base 26
-- SEND_MAIL:    Uses UTL_SMTP in standard way, except it includes HTML option.
--               I copied HTML headers blindly out of an existing e-mail, so may need adjusting
--               or making smarter for use in other environments.
-- LIST_ELEMENT: Extract element m from a character(n)-separated list:
--               e.g. if LIST is 'x,y,z', LIST_ELEMENT(list,2) returns 'y'.
--               Less efficient than the equivalent SUBSTR(INSTR(...)) equivalent but a lot easier to use.
-- SPLIT:        Convert character-separated list to SQL-defined 'nested table' array (VARCHAR2_TT).
-- TO_STRING     Convert collection to character-separated list (accepts VARCHAR2_TT, NUMBER_TT and INTEGER_TT).
--               Originally I called this 'JOIN', and it still worked despite being an SQL keyword.
--
--               You could CAST a MULTISET subquery as a VARCHAR2_TT collection and then use TO_STRING
--               to convert that collection to a list.
--
--               Note that user-defined aggregates e.g. Tom Kyte's STRAGG do this kind of thing more elegantly
--               in 9i and also come with a free analytic version, although there are limitations such as they
--               must be standalone and they can only take one argument. (James Padfield's
--               CONCAT_ALL(CONCAT_EXPR(colname,delimiter)) address the latter.)
--               In 10g, the SQL aggregate function COLLECT builds a collection from a set of values
--               returned by a query, which simplifies this task still further.
--
-- William Robertson 2004 - www.williamrobertson.net

set serverout on size 10000

create type varchar2_tt as table of varchar2(4000);
/

create type date_tt as table of date;
/

create type number_tt as table of number;
/

create type integer_tt as table of integer;
/

create or replace package utilities
-- William Robertson 2004 - www.williamrobertson.net
as
   k_error_code constant pls_integer := -20500;

   k_crlf constant varchar2(2) := chr(13)||chr(10);

   -- Use 9i INTERVAL datatype to define dateless TIME 0-24 hours:
   -- e.g:
   -- v_interval TIME := TO_DSINTERVAL('0 12:34:56');
   subtype time is interval day (0) to second (0);

   function boolean_to_char
      ( p_true boolean )
      return varchar2
      deterministic;

   -- Wrapper for DBMS_OUTPUT.PUT_LINE, avoiding 255-char-per-line limit by wrapping lines:
   procedure echo
      ( p_text    varchar2
      , p_wrap_length  pls_integer default 120 );

   -- Alternative ECHO accepts array and processes each line using ECHO(line,wraplength):
   procedure echo
      ( p_text_collection varchar2_tt
      , p_wrap_length  pls_integer default 120 );

   -- Alternative ECHO accepts Boolean and returns TRUE/FALSE (sorry, no NLS translation yet)
   procedure echo
      ( p_boolean boolean
      , p_wrap_length  pls_integer default 5 );  -- Standard parameter for compatibility

   -- Convert from any numeric base 2-36 to decimal, e.g. TO_DECIMAL('A',36) = 10
   function to_decimal
      ( p_source  varchar2
      , p_base    pls_integer )
      return pls_integer
      deterministic;

   -- Convert from decimal to any numeric base 2-36, e.g. TO_BASE(502574,36) = 'ARSE'
   function to_base
      ( p_decimal pls_integer
      , p_base    pls_integer default 10 )
      return varchar2
      deterministic;

   -- Convert p_source between any two numeric bases 2-36:
   function to_base
      ( p_source  varchar2
      , p_from    pls_integer
      , p_to      pls_integer )
      return varchar2
      deterministic;

   function to_number_safe
      ( p_candidate_number varchar2 )
      return number
      deterministic
      parallel_enable;

   procedure start_timer;

   procedure show_timer;

   procedure send_mail
      ( p_sender     varchar2
      , p_recipient  varchar2
      , p_subject    varchar2
      , p_message    varchar2
      , p_html       boolean := false );

   -- Return element [n] of a character[m]-separated list.
   function list_element
      ( p_string varchar2
      , p_element integer
      , p_separator varchar2 default ',' )
      return varchar2
      deterministic;

   -- Split a string into its elements and return the results as a collection:
   function split
      ( p_text varchar2
      , p_delimiter varchar2 default ','
      , p_encloser varchar2  default null )
      return varchar2_tt
      deterministic
      parallel_enable;

   -- Equivalent to 'JOIN' in other languages (opposite of SPLIT):
   -- Write the contents of a collection to single string:
   function to_string
      ( p_table varchar2_tt
      , p_delimiter varchar2 default ','
      , p_encloser varchar2  default null )
      return varchar2
      deterministic
      parallel_enable;

   -- NUMBER_TT version of above:
   function to_string
      ( p_table number_tt
      , p_delimiter varchar2 default ','
      , p_encloser varchar2  default null )
      return varchar2
      deterministic
      parallel_enable;

   -- INTEGER_TT version of above:
   function to_string
      ( p_table integer_tt
      , p_delimiter varchar2 default ','
      , p_encloser varchar2  default null )
      return varchar2
      deterministic
      parallel_enable;
end utilities;
/

show errors

create or replace package body utilities
as
   k_nosuchdate constant date := date '0000-01-01';

   type integer_to_alpha_table  is table of varchar2(1) index by pls_integer;
   type alpha_to_integer_table is table of pls_integer index by varchar2(1);
   g_number_to_alpha integer_to_alpha_table;
   g_alpha_to_number alpha_to_integer_table;

   g_time_value number;

   function nosuchdate
      return date
   is
   begin
      return k_nosuchdate;
   end nosuchdate;


   function boolean_to_char
      ( p_true boolean )
      return varchar2
   is
      v_return_text varchar2(5);
   begin
      if p_true then
         v_return_text := 'TRUE';
      else
         v_return_text := 'FALSE';
      end if;

      return v_return_text;
   end boolean_to_char;


   -- Safety filter for DBMS_OUTPUT.PUT_LINE:
   procedure echo
      ( p_text    varchar2
      , p_wrap_length  pls_integer default 120 )
   is
      v_text           varchar2(10000) := trim(ltrim(p_text,chr(9)||chr(32)));
      k_wrap_length    constant pls_integer := least(p_wrap_length,length(v_text));
      v_line           varchar2(255);
      v_chop_position  pls_integer;
      v_done           boolean := false;
   begin
      v_text := rtrim(ltrim(v_text,chr(9)),' '||chr(9));

      loop
         v_line := substr(v_text, 1, k_wrap_length);
         v_chop_position := least(k_wrap_length,nvl(length(v_line),0));

         if v_line = v_text
         or length(v_text) <= k_wrap_length
         or v_line is null
         or v_text is null
         then
            -- No more chopping required
            v_done := true;

         elsif v_line like '%' || CHR(10) || '%' THEN
            -- Retain existing linefeeds:
            v_chop_position := instr(v_line, chr(10)) -1;  -- first linefeed in v_line

         elsif v_line like '% %' then
            v_chop_position := instr(v_line, ' ', -1);  -- last space in v_line
         end if;

         -- Trim down to end of last whole word:
         v_line := trim(substr(v_line,1,v_chop_position));

         -- Chop [length of v_line] off start of v_message
         v_text := ltrim(rtrim(substr(v_text, v_chop_position +1),chr(10)),chr(10));

         dbms_output.put_line(v_line);

         exit when v_done;
      end loop;
   end echo;


   procedure echo
      ( p_text_collection varchar2_tt
      , p_wrap_length  pls_integer default 120 )
   is
      i pls_integer := p_text_collection.first;
   begin
      while i is not null
      loop
         echo(p_text_collection(i), p_wrap_length);
         i := p_text_collection.next(i);
      end loop;
   end echo;


   procedure echo
      ( p_boolean boolean
      , p_wrap_length  pls_integer default 5 )
   is
   begin
      echo(boolean_to_char(p_boolean), p_wrap_length);
   end echo;


   function to_base
      ( p_decimal pls_integer
      , p_base    pls_integer default 10 )
      return varchar2
   is
      v_digit pls_integer;
      v_decimal_remaining pls_integer := p_decimal;
      v_result varchar2(100);
   begin

      if p_base not between 2 and 36 then
         raise_application_error
         ( k_error_code
         , 'TO_BASE: Invalid base ' || p_base || ': must be in range 1 to 36' );
      end if;

      while v_decimal_remaining > 0
      loop
         v_digit := mod(v_decimal_remaining,p_base);
         v_result := g_number_to_alpha(v_digit) || v_result;
         v_decimal_remaining := floor(v_decimal_remaining / p_base);
      end loop;

      return v_result;
   end to_base;


   function to_decimal
      ( p_source varchar2
      , p_base   pls_integer )
      return pls_integer
   is
      v_source varchar2(130) := upper(p_source);
      v_result pls_integer := 0;
      v_position pls_integer := length(p_source);
   begin
      for i in 0..length(v_source) -1 loop
         v_position := length(v_source) -i;

         dbms_output.put_line
         ( 'i = ' || i || ': '
         || g_alpha_to_number(substr(v_source,v_position,1)) || ' * '
         || power(p_base,i) || ' = '
         || g_alpha_to_number(substr(v_source,v_position,1)) * power(p_base,i) );
         v_result := v_result + g_alpha_to_number(substr(v_source,v_position,1)) * power(p_base,i);
      end loop;

      return v_result;
   end to_decimal;


   function to_base
      ( p_source  varchar2
      , p_from    pls_integer
      , p_to      pls_integer )
      return varchar2
   is
   begin
      return to_base(to_decimal(p_source,p_from),p_to);
   end to_base;


   function to_number_safe
      ( p_candidate_number varchar2 )
      return number
      deterministic
      parallel_enable
   is
   begin
      return to_number(p_candidate_number);
   exception
      when value_error then
         return to_number(null);
   end to_number_safe;


   procedure start_timer
   is
   begin
      g_time_value := dbms_utility.get_time;
   end start_timer;


   procedure show_timer
   is
      v_elapsed g_time_value%type := dbms_utility.get_time - g_time_value;
   begin
      dbms_output.put_line(rtrim(to_char(round(v_elapsed/100,2),'99990.09'),'0') || ' seconds');
   end show_timer;


   procedure send_mail
      ( p_sender     varchar2
      , p_recipient  varchar2
      , p_subject    varchar2
      , p_message    varchar2
      , p_html       boolean := false )
   is
      k_mailhost constant varchar2(30) := 'smtp.blueyonder.co.uk';
      v_mail_conn utl_smtp.connection := utl_smtp.open_connection(k_mailhost, 25);

      v_message varchar2(2000);
   begin
      if p_html then
         v_message :=
         'Subject: '  || p_subject || k_crlf
         || 'Content-Type: text/html; charset=us-ascii' || k_crlf
         || 'Content-Transfer-Encoding: 7bit' || k_crlf
         || '' || k_crlf
         || ''  || k_crlf
         || ''  || k_crlf
         || '' || k_crlf
         || '' || p_subject || '' || k_crlf
         || '' || k_crlf
         || ''  || k_crlf
         || p_message || k_crlf
         || '';
      else
         v_message := 'Subject: ' || p_subject || k_crlf || p_message;
      end if;

      utl_smtp.helo(v_mail_conn, k_mailhost);
      utl_smtp.mail(v_mail_conn, p_sender);
      utl_smtp.rcpt(v_mail_conn, p_recipient);
      utl_smtp.data(v_mail_conn, v_message);
      utl_smtp.quit(v_mail_conn);
   exception
      when others then
         raise_application_error
         ( k_error_code
         , 'Could not send e-mail message: ''' || p_message || ''''
         , TRUE );
   end send_mail;


   function list_element
      ( p_string varchar2
      , p_element integer
      , p_separator varchar2 default ',' )
      return varchar2
      deterministic
   as
      k_separator constant varchar2(50) := nvl(p_separator,',');
      k_string constant varchar2(4000) := k_separator || p_string;
      v_result varchar2(4000);
      v_startpos integer := instr(k_string,k_separator,1,p_element) + length(k_separator);
   begin
      if not (p_string is null or p_element is null or v_startpos = 1)
      then
         v_result := trim(substr(k_string,v_startpos));
      end if;

      return substr(v_result, 1, instr(v_result || k_separator, k_separator) -1);
   end list_element;


   function split
      ( p_text varchar2
      , p_delimiter varchar2 default ','
      , p_encloser varchar2  default null )
      return varchar2_tt
   is
      i pls_integer := 1;
      v_next_position pls_integer;
      v_enclosed boolean;
      v_text varchar2(2000) := ltrim(rtrim(p_text,p_delimiter),p_delimiter);
      v_return_array varchar2_tt := varchar2_tt();
   begin
      while v_text is not null loop
         v_return_array.extend;

         -- Check whether (remaining) text starts with an encloser (e.g. doublequote):
         v_enclosed := substr(v_text ,1,1) = p_encloser;

         if v_enclosed then
            v_text := trim(substr(v_text,2));
            v_next_position := instr(v_text,p_encloser);
         else
            v_next_position := instr(v_text,p_delimiter);
         end if;

         if v_next_position = 0 then
            -- No more delimiters found, so return remaining text
            -- (first strip off any spaces and enclosing quotes)
            v_return_array(i) := rtrim(v_text, p_encloser||' ');
            exit;
         else
            -- Use the portion of the text up to the next delimiter or encloser:
            -- ('v_next_position' has already been worked out)
            v_return_array(i) := trim(substr(v_text,1,v_next_position -1));
            v_text := trim(substr(v_text,v_next_position +1));

            if v_enclosed then
               -- Deal with closing quote by stripping one more character:
               v_text := trim(substr(v_text,2));
            end if;
         end if;

         i := i +1;
      end loop;

      return v_return_array;
   end split;


   function to_string
      ( p_table varchar2_tt
      , p_delimiter varchar2 default ','
      , p_encloser varchar2  default null )
      return varchar2
   is
      i pls_integer := p_table.first;
      v_return_string varchar2(32000);
   begin
      while i is not null loop
         v_return_string := v_return_string ||
         case
            when instr(p_table(i),p_delimiter) > 0 then p_encloser || p_table(i) || p_encloser
            else p_table(i)
         end ||
         p_delimiter;

         i := p_table.next(i);
      end loop;

      return rtrim(v_return_string, p_delimiter);
   end to_string;


   -- Exact copy of VARCHAR2_TT version except for type of first argument.
   function to_string
      ( p_table number_tt
      , p_delimiter varchar2 default ','
      , p_encloser varchar2  default null )
      return varchar2
   is
      i pls_integer := p_table.first;
      v_return_string varchar2(32000);
   begin
      while i is not null loop
         v_return_string := v_return_string ||
         case
            when instr(p_table(i),p_delimiter) > 0 then p_encloser || p_table(i) || p_encloser
            else p_table(i)
         end ||
         p_delimiter;

         i := p_table.next(i);
      end loop;

      return rtrim(v_return_string, p_delimiter);
   end to_string;


   -- Exact copy of VARCHAR2_TT version except for type of first argument.
   function to_string
      ( p_table integer_tt
      , p_delimiter varchar2 default ','
      , p_encloser varchar2  default null )
      return varchar2
   is
      -- Can cast INTEGER_TT as NUMBER_TT using SQL only.
      -- PL/SQL CAST won't work here (9.2), and overloading does not recognise them as equvalent.
      -- Must try this in 10g.
      v_table number_tt;
   begin
      select cast(p_table as number_tt) into v_table from dual;
      return to_string(v_table,p_delimiter,p_encloser);
   end to_string;

begin
   -- Pre-load conversion tables to simplify TO_BASE and TO_DECIMAL functions:
   for i in 0..35 loop
      if i < 10 then
         g_number_to_alpha(i) := i;
         g_alpha_to_number(to_char(i)) := i;
      else
         g_number_to_alpha(i) := chr(i + 55);
         g_alpha_to_number(chr(i + 55)) := i;
      end if;
   end loop;
end utilities;
/

show errors


create or replace procedure echo
   ( p_text    varchar2
   , p_wrap_length  pls_integer default 120 )
as
begin
   utilities.echo(p_text, p_wrap_length);
end echo;
/

show errors

create or replace function list_element
   ( p_string varchar2
   , p_element integer
   , p_separator varchar2 default ',' )
   return varchar2
   deterministic
as
begin
   return utilities.list_element(p_string, p_element, p_separator);
end list_element;
/

show errors

create or replace function split
   ( p_text varchar2
   , p_delimiter varchar2 default ','
   , p_encloser varchar2  default null )
   return varchar2_tt
as
begin
   return utilities.split(p_text, p_delimiter, p_encloser);
end split;
/

show errors

-- Standalone version of VARCHAR2_TT version of TO_STRING:
-- (note that overloading is not possible with standalone functions.
-- For other versions, use UTILITIES.TO_STRING().
create or replace function to_string
   ( p_table varchar2_tt
   , p_delimiter varchar2 default ','
   , p_encloser varchar2  default null )
   return varchar2
as
begin
   return utilities.to_string(p_table, p_delimiter,p_encloser);
end to_string;
/

show errors