21. Januar 2017

String-Operationen leicht gemacht mit dem neuen APEX_STRING

Seit kurzem ist Application Express 5.1 verfügbar. Es enthält, wie frühere Releases, neue PL/SQL-Packages, die auch außerhalb von APEX verwendet werden können. In diesem Posting möchte ich APEX_STRING vorstellen; das ist sicherlich für fast jeden Entwickler interessant.

APEX_STRING enthält einige Funktionen und Prozeduren zum einfachen Umgang mit Zeichenketten. So ist mit der Funktion SPLIT ein String Tokenizer enthalten ...

select apex_string.split( 'a,b,c,d,e,f,g,h', ',' ) from dual;

COLUMN_VALUE                                                                                                            
---------------------------
a                                                                                                                       
b                                                                                                                       
c                                                                                                                       
d                                                                                                                       
e                                                                                                                       
f        

Auch in PL/SQL kann SPLIT verwendet werden.

declare
  l_array wwv_flow_t_varchar2;
begin
  l_array := apex_string.split( 'a,b,c,d,e,f,g,h', ',' );
  for i in 1 .. l_array.count loop
    dbms_output.put_line( apex_string.format( 'element at index %s: %s', i, l_array(i) ) );
  end loop;
end;
/

element at index 1: a
element at index 2: b
element at index 3: c
element at index 4: d
element at index 5: e
:

Ist man nur an den ersten Werten eines größeren Strings (oder CLOBs) interessiert, so ist der Parameter P_LIMIT bedeutsam: Ist das Limit erreicht, bricht die Funktion ab und spart so Rechenaufwand. Das letzte Element im zurückgegebenen Array enthält den Rest des Strings.

Neben VARCHAR2 unterstützt SPLIT auch den CLOB-Datentypen, SPLIT_NUMBERS liefert außerdem ein Array von NUMBER-Datentypen zurück. Aufmerksame Leser dürften bemerken, dass in obigem Block noch eine weitere Funktion aus dem APEX_STRING-Paket verwendet wurde: FORMAT.

FORMAT vereinfacht das Zusammensetzen von Strings aus festen Literalen und variablen Inhalten: anstelle vieler Konkatenationen mit || setzt man %s oder %{N} in den String ein. APEX_STRING ersetzt das n-te %s durch das n-the Argument p{N} (also das erste %s durch das Argument p0, das zweite durch p1 und so fort). Die Argumente p{N} können mit %{N} auch direkt angesprochen und so mehrfach verwendet werden: So spricht man p0 mit %0, p1 mit %1 an und so fort.

select apex_string.format( 
  p_message => 'Text mit Platzhaltern: USER: %s, SYSDATE: %s, RANDOM VALUE: %s', 
  p0 =>       user, 
  p1 =>       sysdate, 
  p2 =>       dbms_random.value 
) as formatted_text from dual;

FORMATTED_TEXT
------------------------------------------------------------------------------
Text mit Platzhaltern: USER: SYS, SYSDATE: 21.01.2017 06:39:47, 
RANDOM VALUE: ,52835625740274072229710302735665090347   

select apex_string.format( 
  p_message => 'Text mit Platzhaltern: USER: %0, SYSDATE: %1, nochmal USER: %0',
  p0 =>       user, 
  p1 =>       sysdate 
) as formatted_text from dual;

FORMATTED_TEXT                                                                                                          
------------------------------------------------------------------------------
Text mit Platzhaltern: USER: SYS, SYSDATE: 21.01.2017 06:42:34, 
nochmal USER: SYS

Mit den PLIST Prozeduren können einfache Property Listen verwaltet werden. Ein Eintrag in einer solchen Liste, die in einem Array vom Typ WWV_FLOW_T_VARCHAR2 abgelegt wird, ist ein Key-Value-Paar. Folgerichtig gibt es die Funktionen PLIST_GET, PLIST_PUT und PLIST_DELETE. Der folgende Code zeigt, wie's geht:

declare
  l_props wwv_flow_t_varchar2 := wwv_flow_t_varchar2();
begin
  apex_string.plist_put( l_props, 'username', 'SCOTT' ); 
  apex_string.plist_put( l_props, 'password', 'tiger' ); 
  apex_string.plist_put( l_props, 'connection', 'localhost:1521/orcl' );

  for i in 1 .. l_props.count loop
    dbms_output.put_line( apex_string.format( '%0: %1', i, l_props( i ) ) );
  end loop;

  dbms_output.put_line( apex_string.format( 
     p_message => '*** %0/%1@%2', 
     p0 => apex_string.plist_get(l_props, 'username'), 
     p1 => apex_string.plist_get(l_props, 'password'), 
     p2 => apex_string.plist_get(l_props, 'connection')
  ));

  apex_string.plist_delete( l_props, 'password' );

  for i in 1 .. l_props.count loop
    dbms_output.put_line( apex_string.format( '%0: %1', i, l_props( i ) ) );
  end loop;
end;
/

1: username
2: SCOTT
3: password
4: tiger
5: connection
6: localhost:1521/orcl

*** SCOTT/tiger@localhost:1521/orcl

1: username
2: SCOTT
3: 
4: 
5: connection
6: localhost:1521/orcl

Die Prozedur PUSH ist ein bequemer Weg, einen einfachen Wert an das Array anzuhängen. Der folgende Code zeigt sie im Zusammenspiel mit der Funktion SHUFFLE, welche das Array "durcheinanderwürfelt".

declare
  l_array wwv_flow_t_varchar2;
begin
  apex_string.push( l_array, 'Value 1' );
  apex_string.push( l_array, 'Value 2' );
  apex_string.push( l_array, 'Value 3' );
  apex_string.push( l_array, 'Value 4' );
  apex_string.shuffle( l_array );

  for i in 1 .. l_array.count loop
    dbms_output.put_line( apex_string.format( '%0: %1', i, l_array( i ) ) );
  end loop;
end;
/

1: Value 3
2: Value 2
3: Value 1
4: Value 4

Die coolste Funktion ist für mich GREP, diese funktioniert wie die REGEXP_* SQL-Funktionen, liefert mehrere Ergebnisse aber als Array zurück. Das folgende Beispiel zeigt das: Aus einem Text sollen alle Email-Adressen extrahiert werden (die Regexp für Emailadressen ist sicherlich nicht die beste, für dieses Beispiel soll es aber reichen).

select * from table( apex_string.grep(
  'This is a text with some mail adresses. The first one is a.a@oracle.com, we have ' || 
  'noname@company.com and thesupport@firma.de',
  '[A-Z0-9._%+-]*@[A-Z0-9.-]*\.[A-Z]*',
  'i'
));

COLUMN_VALUE
--------------------------
a.a@oracle.com
noname@company.com
thesupport@firma.de

Die "normale" Funktion REGEXP_SUBSTR liefert stets genau einen String zurück; APEX_STRING.GREP dagegen ein Array mit einem Element für jeden Match. Genau das macht die Arbeit mit den extrahierten Werten massiv einfacher.

Zum Schluß stellen wir mit JOIN das Gegenstück zur eingangs vorgestellten Funktion SPLIT vor und schließen so den Kreis.

declare
  l_array wwv_flow_t_varchar2;
begin
  apex_string.push( l_array, 'Value 1' );
  apex_string.push( l_array, 'Value 2' );
  apex_string.push( l_array, 'Value 3' );
  apex_string.push( l_array, 'Value 4' );
  apex_string.shuffle( l_array );

  dbms_output.put_line( apex_string.join( l_array, '#' ) );
end;
/

Value 4#Value 2#Value 1#Value 3

Viel Spaß beim Ausprobieren und beim Nutzen dieses neuen Pakets; ich bin mir recht sicher, dass es vielen Entwicklern eine Menge Arbeit abnehmen wird.

Application Express 5.1 has been released recently - and as earlier versions, it contains new PL/SQL packages, which are also usable outside of an APEX context. In this blog posting I'd like to introduce the APEX_STRING package, which is very interesting for all SQL and PL/SQL developers.

APEX_STRING contains functions and procedures to work with - as the name indicates, strings. Let's start with the SPLIT function, which is a String Tokenizer (which we need that all the time, right?). SPLIT can be used in a SQL context ...

select apex_string.split( 'a,b,c,d,e,f,g,h', ',' ) from dual;

COLUMN_VALUE       
---------------------------                                                                                                     
a                                                                                                                       
b                                                                                                                       
c                                                                                                                       
d                                                                                                                       
e                                                                                                                       
f        

... as well as within PL/SQL:

declare
  l_array wwv_flow_t_varchar2;
begin
  l_array := apex_string.split( 'a,b,c,d,e,f,g,h', ',' );
  for i in 1 .. l_array.count loop
    dbms_output.put_line( apex_string.format( 'element at index %s: %s', i, l_array(i) ) );
  end loop;
end;
/

element at index 1: a
element at index 2: b
element at index 3: c
element at index 4: d
element at index 5: e
:

If you are interested only in the first few values of a larger string (or a larger CLOB), the P_LIMIT argument gets important: The function will stop when the limit has been reached, thus saving CPU cycles. The last element of the returned array will contain the rest of the input string.

SPLIT also supports the CLOB datatype - for cases, where CLOBs are to be tokenized. SPLIT_NUMBERS returns an array of NUMBER elements.

Careful readers might have noticed that the above PL/SQL block contains another APEX_STRING function: FORMAT. FORMAT allows building a string, containing fixed literals and variable contents, by replacing placeholders. Code using APEX_STRING is much better readable and maintainable than code using classic || concatenations.

As the following code example illustrates, you can use the %s and %{N} placeholders. FORMAT then replaces the n-th %s with the value of the n-th p{N} function argument. Each p{N} argument can also be referenced directly with %{N} i.e. %0 references the p0 argument, %1 will reference p1 and so on. That syntax allows to use p{N} arguments more than once.

select apex_string.format( 
  p_message => 'Text with placeholders: USER: %s, SYSDATE: %s, RANDOM VALUE: %s', 
  p0 =>       user, 
  p1 =>       sysdate, 
  p2 =>       dbms_random.value 
) as formatted_text from dual;

FORMATTED_TEXT
-------------------------------------------------------------------------------
Text with placeholders: USER: SYS, SYSDATE: 21.01.2017 06:39:47, 
RANDOM VALUE: ,52835625740274072229710302735665090347   

select apex_string.format( 
  p_message => 'Text with placeholders: USER: %0, SYSDATE: %1, USER (once more): %0',
  p0 =>       user, 
  p1 =>       sysdate 
) as formatted_text from dual;

FORMATTED_TEXT                                                                                                          
-------------------------------------------------------------------------------
Text with placeholders: USER: SYS, SYSDATE: 21.01.2017 06:42:34, 
USER (once more): SYS                                                       

The PLIST_* procedures allow to maintain simple property lists, which consist of key-value pairs. These lists are stored in an array of the WWV_FLOW_T_VARCHAR2 type and are maintained with PLIST_GET, PLIST_PUT and PLIST_DELETE. The following sample code shows how it works.

declare
  l_props wwv_flow_t_varchar2 := wwv_flow_t_varchar2();
begin
  apex_string.plist_put( l_props, 'username', 'SCOTT' ); 
  apex_string.plist_put( l_props, 'password', 'tiger' ); 
  apex_string.plist_put( l_props, 'connection', 'localhost:1521/orcl' );

  for i in 1 .. l_props.count loop
    dbms_output.put_line( apex_string.format( '%0: %1', i, l_props( i ) ) );
  end loop;

  dbms_output.put_line( apex_string.format( 
     p_message => '*** %0/%1@%2', 
     p0 => apex_string.plist_get(l_props, 'username'),
     p1 => apex_string.plist_get(l_props, 'password'), 
     p2 => apex_string.plist_get(l_props, 'connection')
  ));

  apex_string.plist_delete( l_props, 'password' );

  for i in 1 .. l_props.count loop
    dbms_output.put_line( apex_string.format( '%0: %1', i, l_props( i ) ) );
  end loop;
end;
/

1: username
2: SCOTT
3: password
4: tiger
5: connection
6: localhost:1521/orcl

*** SCOTT/tiger@localhost:1521/orcl

1: username
2: SCOTT
3: 
4: 
5: connection
6: localhost:1521/orcl

PUSH is an easy way to add a single scalar value to an array; the array is treated like a stack. The following PL/SQL block adds a few values with PUSH, then uses the SHUFFLE function to randomize element order and it finally prints the array out.

declare
  l_array wwv_flow_t_varchar2;
begin
  apex_string.push( l_array, 'Value 1' );
  apex_string.push( l_array, 'Value 2' );
  apex_string.push( l_array, 'Value 3' );
  apex_string.push( l_array, 'Value 4' );
  apex_string.shuffle( l_array );

  for i in 1 .. l_array.count loop
    dbms_output.put_line( apex_string.format( '%0: %1', i, l_array( i ) ) );
  end loop;
end;
/

1: Value 3
2: Value 2
3: Value 1
4: Value 4

For me, the coolest function within APEX_STRING is GREP, which extracts substrings based on regular expressions, but which returns an array instead of a single string. Here's an example, which extracts all email addresses from a string (the regular expression to match email addresses is a very simple one):

select * from table( apex_string.grep(
  'This is a text with some mail adresses. The first one is a.a@oracle.com, we have ' || 
  'noname@company.com and thesupport@firma.de',
  '[A-Z0-9._%+-]*@[A-Z0-9.-]*\.[A-Z]*',
  'i'
));

COLUMN_VALUE
--------------------------
a.a@oracle.com
noname@company.com
thesupport@firma.de

Having all matches in an array instead of a string makes post-processing a lot easier.

Finally, we'll show the JOIN function as the counterpart to SPLIT, which we introduced at the beginning of the posting. So we came full circle.

declare
  l_array wwv_flow_t_varchar2;
begin
  apex_string.push( l_array, 'Value 1' );
  apex_string.push( l_array, 'Value 2' );
  apex_string.push( l_array, 'Value 3' );
  apex_string.push( l_array, 'Value 4' );
  apex_string.shuffle( l_array );

  dbms_output.put_line( apex_string.join( l_array, '#' ) );
end;
/

Value 4#Value 2#Value 1#Value 3

Have fun trying this (and other use cases) out. I'm pretty sure that this new package provides a lot of value to all SQL and PL/SQL developers; whether they are working with APEX or not.

Kommentare:

Unknown hat gesagt…

Hello

My output from the "select apex_string.split( 'a,b,c,d,e,f,g,h', ',' ) from dual"is not the 8 lines, but 1 line with another datatype ?

How did you get the 8 lines +

regards
Mette

Carsten Czarski hat gesagt…

Hi Mette,

just do a "select * from table(apex_string.split(...))" - that should return what you want ...

Best regards

-Carsten

Beliebte Postings