| René Nyffenegger's collection of things on the web | |
|
René Nyffenegger on Oracle - Most wanted - Feedback
|
April 14, 2007: printf in PL/SQL | ||
|
Note: I have started to move some scripts and source code from this website to GitHub.
This affects some or all of the scripts found on this page. They should be found under
I don't intend to maintain the scripts and or sources on this page any longer (so they might be outdated). But I will try to improve the code in the GitHub repository and accept Push Requests.
There are a few things in PL/SQL that I am really missing. One of these things is
printf. I am not aware of any built-in functionality that remotely resembles the elegance of printf. Ok,
lpad and rpad and to_char come to mind. But these functions are, IMHO, tedious to use, especially when it
comes to handling null values.
Therefore, I have hacked up my own printf. I stress that it is a hack since there would probably be many features or improvements worth implementing, but that's for another day... So, if you come up with a better printf, please
let me know, so I can put it on my website.
Anyway, there are a few differences between my printf and the standard c printf. For one, my printf handles only the format specifiers
%d (numbers) and %s (strings). Also, the (optional) width (w)
and precision (p) part in %w.pd behave differently. In c's printf, w denotes the total width of the resulting string. In my implementation, w denotes how many characters (digits) will be printed left of the
dot. So, the resulting string of %7.3d will always be 10 digits wide, the sign inclusively. However, if I use %+7.3d, the resulting string is eleven characters wide. The behaviour of my printf is probably best
demonstrated in this test case.
Since printf has a variable number of arguments, I create a nested table type to mimic this feature:
create or replace type arg_list as table of varchar2(4000) /
Now, sprintf, which returns the formatted string:
create or replace function sprintf(format in varchar2, parms in arg_list) return varchar2 is ret varchar2(4000); cur_pos number := 0; cur_format varchar2(4000); len_format number := length(format); left_aligned boolean; print_sign boolean; cur_param number := 0; begin loop -- Iterating over each character in the format. -- cur_pos points to the character 'being examined'. cur_pos := cur_pos + 1; exit when cur_pos > len_format; -- The iteration is over when cur_pos is past the last character. if substr(format, cur_pos, 1) = '%' then -- A % sign is recognized. -- I assume the default: left aligned, sign (+) not printed left_aligned := false; print_sign := false; -- Advance cur_pos so that it points to the character -- right of the % cur_pos := cur_pos + 1; -- if substr(format, cur_pos, 1) = '%' then -- If % is immediately followed by another %, a literal -- % is wanted: ret := ret || '%'; -- No need to further process the format (it is none) goto percent; end if; if substr(format, cur_pos, 1) = '-' then -- Current format will be left aligned left_aligned := true; cur_pos := cur_pos + 1; end if; if substr(format, cur_pos, 1) = '+' then -- Print plus sign explicitely (only number, %d) print_sign := true; cur_pos := cur_pos + 1; end if; -- Now, reading the rest until 'd' or 's' and -- store it in cur_format. cur_format := ''; -- cur_param points to the corresponding entry -- in parms cur_param := cur_param + 1; loop -- Make sure, iteration doesn't loop forever -- (for example if incorrect format is given) exit when cur_pos > len_format; if substr(format, cur_pos, 1) = 'd' then declare -- some 'local' variables, only used for %d chars_left_dot number; chars_rite_dot number; chars_total number; dot_pos number; to_char_format varchar2(50); buf varchar2(50); num_left_dot char(1) := '9'; begin if cur_format is null then -- Format is: %d (maybe %-d, or %+d which SHOULD be -- handled, but isn't) ret := ret || to_char(parms(cur_param)); -- current format specification finished, exit the loop exit; end if; -- does the current format contain a dot? -- dot_pos will be the position of the dot -- if it contains one, or will be 0 otherwise. dot_pos := instr(cur_format, '.'); if substr(cur_format, 1, 1) = '0' then -- Is the current format something like %0...d? num_left_dot := '0'; end if; -- determine how many digits (chars) are to be printed left -- and right of the dot. if dot_pos = 0 then -- If no dot, there won't be any characters rigth of the dot -- (and no dot will be printed, either) chars_rite_dot := 0; chars_left_dot := to_number(cur_format); chars_total := chars_left_dot; else chars_rite_dot := to_number(substr(cur_format, dot_pos + 1)); chars_left_dot := to_number(substr(cur_format, 1, dot_pos - 1)); chars_total := chars_left_dot + chars_rite_dot + 1; end if; if parms(cur_param) is null then -- null h ret := ret || lpad(' ', chars_total); exit; end if; to_char_format := lpad('9', chars_left_dot-1, '9') || num_left_dot; if dot_pos != 0 then -- There will be a dot to_char_format := to_char_format || '.' || lpad('9', chars_rite_dot, '9'); end if; if print_sign then to_char_format := 'S' || to_char_format; -- The explicit printing of the sign widens the output one character chars_total := chars_total + 1; end if; buf := to_char(to_number(parms(cur_param)), to_char_format); if not print_sign then if substr(buf, 1, 1) = '-' or substr(buf, 1, 1) != ' ' then -- print a bunch of ! if the number doesn't fit the length buf := lpad('!', chars_total, '!'); else buf := substr(buf, 2); end if; end if; if left_aligned then buf := rpad(trim(buf), chars_total); else buf := lpad(trim(buf), chars_total); end if; ret := ret || buf; exit; end; elsif substr(format, cur_pos, 1) = 's' then if cur_format is null then ret := ret || parms(cur_param); exit; end if; if left_aligned then ret := ret || rpad(nvl(parms(cur_param), ' '), to_number(cur_format)); else ret := ret || lpad(nvl(parms(cur_param), ' '), to_number(cur_format)); end if; exit; end if; cur_format := cur_format || substr(format, cur_pos, 1); cur_pos := cur_pos + 1; end loop; else ret := ret || substr(format, cur_pos, 1); end if; <<PERCENT>> null; end loop; return ret; end sprintf; /
The test case for sprintf:
set serveroutput on size 100000 format wrapped declare procedure cmp(test_no in number, format in varchar2, args in arg_list, expected in varchar2) is gotten varchar2(4000); begin gotten := sprintf(format, args); if gotten != expected then dbms_output.put_line('test ' || test_no || ' failed'); dbms_output.put_line(' gotten is: ' || gotten); dbms_output.put_line(' expected is: ' || expected); end if; end cmp; begin dbms_output.put_line('sprintf test'); -- Right aligning a string: cmp( 1, 'String: %20s', arg_list('hello world'), 'String: hello world'); -- Left aligning a string 12345678901234567890 cmp( 2, 'String: %-20s', arg_list('hello world'), 'String: hello world '); -- A left and a right aligned string ... 1234567890 1234567890 ... cmp( 3, '... %10s %-10s ...', arg_list('hello', 'world'), '... hello world ...'); -- Strings and null ... 12345 12345 12345 12345 ... cmp( 4, '... %5s %5s %-5s %-5s ...', arg_list('a', null, 'b', null), '... a b ...'); -- Numbers cmp( 5, 'Numbers: %d,%d,%d' , arg_list( 42, 0, -42), 'Numbers: 42,0,-42'); cmp( 6, 'Numbers: %d,%d,%d' , arg_list( 42, null, -42), 'Numbers: 42,,-42' ); -- Right aligning numbers: 12345 12345 12345 cmp( 7, 'Numbers: %5d,%5d,%5d' , arg_list( 42, 0, -42), 'Numbers: 42, 0, -42'); cmp( 8, 'Numbers: %5d,%5d,%5d' , arg_list( 42, null, -42), 'Numbers: 42, , -42'); -- Left aligning numbers: 12345 12345 12345 cmp( 9, 'Numbers: %-5d,%-5d,%-5d' , arg_list( 42, null, -42), 'Numbers: 42 , ,-42 '); -- Fractions 12.12345 cmp(10, '1/3: %2.5d', arg_list( 1/3), '1/3: .33333'); cmp(11, '1/3: %02.5d', arg_list( 1/3), '1/3: 0.33333'); cmp(12, '1/3: %2.5d', arg_list(-1/3), '1/3: -.33333'); cmp(13, '1/3: %02.5d', arg_list(-1/3), '1/3: -0.33333'); -- Fractions with signs S12.12345 cmp(14, '1/3: %+2.5d', arg_list( 1/3), '1/3: +.33333'); cmp(15, '1/3: %+02.5d', arg_list( 1/3), '1/3: +0.33333'); cmp(16, '1/3: %+2.5d', arg_list(-1/3), '1/3: -.33333'); cmp(17, '1/3: %+02.5d', arg_list(-1/3), '1/3: -0.33333'); -- Recognizition of the % cmp(18, '%d %% of %d is: %d', arg_list(7, 68, 68/100*7), '7 % of 68 is: 4.76'); -- Number doesn't fit the length cmp(19, '... %4d ...', arg_list(12345), '... !!!! ...'); end; /
Here's the
printf function:
create or replace procedure printf (format in varchar2, parms in arg_list) is begin dbms_output.put_line(sprintf(format, parms)); end; / More on OracleThis is an on Oracle article. The most current articles of this series can be found here.
|