-- Various PL/SQL utilities. Help yourself, but credit me occasionally and don't sue me if they break. -- Requires Oracle 9i. (See utilities80 for an 8.0 compatible version.) -- UTILITIES is a bit of a non-name. For production use, these procedures and functions might be better -- moved into more tightly defined packages e.g. STRING_PKG for string manipulation functions. -- -- 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 oddly-named 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 := TRIM(TRIM(CHR(10) FROM SUBSTR(v_text, v_chop_position +1))); 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 || '