[Rexx] einfacher String Replacer

(DE) Projekte für OS/2, eCS und ArcaOS
(EN) OS/2, eCS and ArcaOS related projects
User avatar
Frank Wochatz
Posts: 1067
Joined: Sun 22. Dec 2013, 22:04
Location: Berlin

[Rexx] einfacher String Replacer

Post by Frank Wochatz »

Meine alte Stringreplaceroutine aus irgendeinem OS/2 Buch - eine Routine, die sich selber immer wieder aufruft - hatte das Problem, dass bei häufigen Ersetzungen (Loops) einer der beiden Rexx Interpreter ausgestiegen ist. Jedenfalls habe ich mal was neues gebaut, und bin dabei auf ein unerwartetes Verhalten von PARSE VAR gestossen.

Dieses Testprogramm baut einen Testring ruft die Routine auf:

Code: Select all

/* testaufruf repl.cmd */

string="xbc"

do i=1 to 15
string=string||"xbc"
end

rc=repl(string, "x", "a")

say "original:" string
say "new:     " rc
exit
und das hier ist der eigentliche String Replacer, als repl.cmd abspeichern:

Code: Select all

/* replace string */
parse arg string, old_s, new_s

string="+"||string||"+"

new_string=""

do until string=""

	parse var string sub_string (old_s) string

	if new_string="" then new_string=(sub_string)
		else new_string=new_string||new_s||sub_string

end

new_string=substr(new_string,2,length(new_string)-2)

return new_string


Zum Replacer:

Damit auch das erste und letzte Zeichen (Zeichenkette geht auch) bei Bedarf ausgetauscht werden kann, trickse ich in Zeile 3: es wird vorne und hinten jeweils vorübergehend ein Zeichen an den Quellstring gefügt. Hier ist es ein "+", ist aber egal. Später in Zeile 16 wird einfach das erste und letzte Zeichen wieder entfernt. Zuerst hatte ich das mit Strip gemacht, aber so ist es sicherer.

Soweit so gut.

Was mich aber wundert ist, das es auch geht, wenn mehrere austauschende Zeichen direkt hintereinander auftreten.

Beispiel:

Wenn der Quellstring "aaaaa" lautet, und ich alle "a" gegen "b" austauschen möchte.
Mein Code macht daraus zunächst "+aaaaa+".

Beim ersten PARSE Durchlauf wird dann zerlegt in "+" und den Reststring "aaaa+". Das erste "a" im Quelsstring ist ja ich sag mal der "Separator", und nun nicht mehr dabei. Okay.

Aber nun im zweiten Durchlauf wird ja der Reststring aaaa+ geparsed. Und nun haben wir ja an erster Stelle ein "a". Und trotzdem funktioniert das.

Also entweder sehe ich den Wald vor lauter Bäumen nicht. Oder PARSE VAR arbeitet hier nicht völlig stringent. Beim ersten Aufruf darf als erstes Zeichen nicht das zu ersetzende Zeichen (der PARSE Separator) stehen, bei weiteren Durchläufen dann aber schon. Wobei die Routine auch nur deshalb so einfach funktioniert.

Versteht ihr, was ich meine?

User avatar
aschn
Posts: 1363
Joined: Wed 25. Dec 2013, 22:47

Post by aschn »

Ich hab mir das noch nicht angesehen, aber Du kannst auch einfach changestr verwenden. Falls Du unedingt Kompatibilität mit uralten REXX-Versionen haben willst, kannst Du das auch einfach mit delstr und insert nachbilden. Darauf würd' ich aber verzichten.

Code: Select all

start view orexx "built-in functions"
und dann die Seite CHANGESTR.

Das funktioniert auch mit Classic REXX, ist aber in der ungenügenden REXX.INF nicht dokumentiert.

Algemein zu Deiner Frage: Wenn es irgendwie geht, dann würd ich auf rekursive Funktionen verzichten. Die sind viel langsamer und es besteht leichter die Gefahr von endlosen Schleifen, weil das, was dann wann ausgeführt nicht mehr so einfach nachvollzogen werden kann.

Hier die Funktion, die ich in E geschrieben hab, nach REXX "portiert", ungetestet:

Code: Select all

ChangeStr2:
   PARSE ARG Search, SourceString, Replace
   OutString = SourceString
   pStart = 1
   DO FOREVER
      pSearch = POS( Search, OutString, pStart)
      IF pSearch > 0 THEN
      DO 
         OutString = DELSTR( OutString, pSearch, LENGTH( Search))
         OutString = INSERT( Replace, OutString, pSearch - 1)
         pStart = pSearch + LENGTH( Replace)
      END
      ELSE
         LEAVE
   END
   RETURN OutString
Andreas Schnellbacher

User avatar
Frank Wochatz
Posts: 1067
Joined: Sun 22. Dec 2013, 22:04
Location: Berlin

Post by Frank Wochatz »

Danke, Andreas!

Also meine Funktion oben funktioniert ja auch (nur sollte sie eigentlich nicht funktionieren). :)

Ich schaue mir deine Variante mal an und werde das auch mal vergleichen.

Und: genau, rekursiv war die alte Funktion, die dann manchmal bei zu großer Vernestung abgestürzt ist.

User avatar
DonLucio
Posts: 789
Joined: Sun 29. Dec 2013, 01:14
Location: Hamburg

Post by DonLucio »

aschn wrote:
Fri 28. Jan 2022, 13:54
und dann die Seite CHANGESTR.

Das funktioniert auch mit Classic REXX, ist aber in der ungenügenden REXX.INF nicht dokumentiert.
Äh ... bei mir nicht:

Code: Select all

     8 +++   Say ChangeStr(x, y, 'a');
REX0043: Error 43 running E:\rexxe\TEST.CMD, line 8: Routine not found
Was ist bei mir falsch?

Gruß,
Lutz W.

User avatar
Frank Wochatz
Posts: 1067
Joined: Sun 22. Dec 2013, 22:04
Location: Berlin

Post by Frank Wochatz »

CHANGESTR funktioniert hier auch nur unter OREXX.


Ich habe nun spaßeshalber einige Performancetests gemacht. Dazu habe ich einen String mit 100.000 gleichen Zeichen erstellt, und alle Zeichen durch ein anderes ersetzt. Und dann dazu die Zeit mit SysElapsedTime gemessen.

Die Variante von Andreas mit DELSTRING und INSERT und meine Variante mit PARSE VAR sind gleich schnell. CHANGESTR ist hier in dem Test allerdings um den Faktor 700 schneller.

Ich würde sagen bei kleineren Datenmengen bzw. wenigen Ersetzungen ist es egal, bei größeren spürt man doch deutliche Wartezeiten mit den selbstgebauten Routinen.

Die alte rekursive Routine habe ich nicht getestet, die habe ich jetzt mal aussortiert.

User avatar
Frank Wochatz
Posts: 1067
Joined: Sun 22. Dec 2013, 22:04
Location: Berlin

Post by Frank Wochatz »

Ergänzung: mit dem Classic Rexx Interpreter laufen die Routinen dann nochmal rund 2 bis 3x langsamer als unter ORexx...

User avatar
aschn
Posts: 1363
Joined: Wed 25. Dec 2013, 22:47

Post by aschn »

Frank Wochatz wrote:
Fri 28. Jan 2022, 15:56
CHANGESTR funktioniert hier auch nur unter OREXX.
Kann nicht sein. Die wird allgemein benutzt, z.B. von cube.cmd, das für die ArcaOS-und RPM-Installation (wahrscheinlich auch schon für eCS) benutzt wird um die config.sys zu editieren. Dort wird changestr viel benutzt. Das läuft auf jeden Fall mit Classic REXX. Ich hab's natürlich auch schon häufiger benutzt.

Zu PARSE: Das hat an einer Stelle Probleme mit Leerzeichen, war wahrscheinlich am Ende. Ausnahme ist Positional Parsing, also z.B. mit +2 var1 +1 ... Ich vermeide deshalb PARSE für solche Zwecke.
Frank Wochatz wrote:
Fri 28. Jan 2022, 15:56
Die Variante von Andreas mit DELSTRING und INSERT und meine Variante mit PARSE VAR sind gleich schnell. CHANGESTR ist hier in dem Test allerdings um den Faktor 700 schneller.
Da bin ich überrascht. Dass die C-Variante viel schneller ist, ist ganz normal. Dort gibt es auch keine Strings. Statt Zeichenketten im Speicher umzukopieren werden dort nur Zeiger geändert. Ich hätte aber erwartet, dass PARSE wegen der Komplexität etwas langsamer ist. REXX ist aber grundsätzlich völlig langsam. Wenn es um Geschwindigkeit geht, dann muss man eine andere Sprache nehmen. Ein Grund bei REXX ist sicherlich das Suchen von Variablen in einer (globalen) Tabelle.
Andreas Schnellbacher

User avatar
Frank Wochatz
Posts: 1067
Joined: Sun 22. Dec 2013, 22:04
Location: Berlin

Post by Frank Wochatz »

aschn wrote:
Fri 28. Jan 2022, 16:35
Frank Wochatz wrote:
Fri 28. Jan 2022, 15:56
CHANGESTR funktioniert hier auch nur unter OREXX.
Kann nicht sein. Die wird allgemein benutzt, z.B. von cube.cmd, das für die ArcaOS-und RPM-Installation (wahrscheinlich auch schon für eCS) benutzt wird um die config.sys zu editieren. Dort wird changestr viel benutzt. Das läuft auf jeden Fall mit Classic REXX. Ich hab's natürlich auch schon häufiger benutzt.
Ich bekomme ein "REX0043: Error 43 running C:\dev\repl\test.cmd, line 9: Routine not found". Vorletzte Arca Version. Ev. wird die Funktion unter Classic Rexx über eine DLL geladen?

PS In meiner cube.cmd kommt changestr nicht vor.

User avatar
aschn
Posts: 1363
Joined: Wed 25. Dec 2013, 22:47

Post by aschn »

Da lag ich völlig falsch: Bei mir existiert ein changestr.cmd. cube.cmd definiert auch eine eigene Funktion mit gleichem Namen (sollte man lieber nicht machen). Mir ist das jetzt aber zu mühsam wegen der scheußlichen Formatierung. Hier aus der Version 2.9 von cube.cmd:

Code: Select all

/* ChangeStr */
ChangeStr:  Procedure   ;

PARSE UPPER ARG    .   ,  .  ,  .    ,  flags =1  . 'I' +0 ignorecase ,  .  ;
ignorecase  =  '' << ignorecase  ;   /*  Booleanate the value */

PARSE  ARG  oldneedle, haystack, newneedle,   .   ;

IF  '' = oldneedle  THEN
  RETURN haystack  ;
/*  Alternate interpretations would be for putting newneedle
    inbetween each character of haystack (where zero length strings
    exist in the interstices :-) of the string.)
    Or one newneedle at the end of haystack,
    where parse interprets '' to be.
*/

/*  First, try to handle a cheap case:  */
IF  1 = Length( oldneedle )  THEN
  IF  1 = Length( newneedle )  THEN
    DO
    IF  ignorecase  THEN
      DO
      lowers  =  XRange( 'a', 'z' )  ;
      IF  0 < Pos( oldneedle, lowers )  THEN
        DO
        newneedle  =  newneedle || newneedle  ;
        oldneedle  =  oldneedle || Translate( oldneedle )  ;
        END
      ELSE      /*  not a lower case letter  */
        DO
        upper  =  Pos( oldneedle, XRange( 'A', 'Z' ) )  ;
        IF  0 < upper  THEN
          DO
          newneedle  =  newneedle || newneedle  ;
          oldneedle  =  oldneedle || SubStr( lowers, upper, 1 )  ;
          END  ;  /*  is upper case  */
        END  ;  /*  non-lower case  */
      END  ;   /*  ignore case  */

    RETURN  Translate( haystack, newneedle, oldneedle )  ;
    END  ;  /*  both needles a single character  */

/* First let's count how many instances are going to be replaced  */
/*  This could be inlined here if neccessary:  */
total  =  CountStr( oldneedle, haystack, flags )  ;

IF  ignorecase  THEN
  DO
  lneedle  =  Length( oldneedle )  ;
  PARSE UPPER ARG   uneedle  ;
  PARSE UPPER VAR  haystack  outstring (uneedle) .  ;
  spot  =  Length( outstring'#' )   ;
  /*  -  append another character rather than add 1 after
         calculating length
   */
  PARSE VAR haystack  . =(spot) pneedle +(lneedle) haystack  ,
                  =1  outstring (pneedle) .  ;
  DO  total
    outstring  =  outstring || newneedle  ;
    PARSE UPPER VAR haystack  increment (uneedle) .  ;
    spot  =  Length( increment'#' )   ;
    PARSE VAR haystack  . =(spot) pneedle +(lneedle) haystack  ,
                    =1  increment (pneedle) .  ;
    outstring  =  outstring || increment  ;
  END    /*  WHILE  */  ;
  END    /*  IF  */  ;
ELSE              /* use case  */
  DO              /* this branch is simpler to understand, but still
                     mirrors the steps in the branch (case insensitive)
                     above
                   */
  PARSE VAR haystack  outstring (oldneedle) haystack  ;
  DO  total
    outstring  =  outstring || newneedle  ;
    PARSE VAR haystack  increment (oldneedle) haystack  ;
    outstring  =  outstring || increment  ;
    /*  - Need a seperate string to store eventual output,
        incase old needle is a substring of new needle,
        which would cause an infinite loop.
     */
  END    /*  WHILE  */  ;
  END    /*  ELSE  */  ;

/* To some extent, haystack and outstring function as a stacks,
   allowing the shift of the character data from the structure for
   input to the structure for output.
   From this point of view, this demonstrates the ability to
   use data stacks to replace recursive program flow.
*/

RETURN  outstring  ;
Andreas Schnellbacher

User avatar
aschn
Posts: 1363
Joined: Wed 25. Dec 2013, 22:47

Post by aschn »

Nebenbei: Hier hab ich die Funktion ja doch schon mal als ChangeString (Zeile 734, ein Parameter mehr) eingebaut.

Falls jemand noch weitere Funktionen suchen sollte (Werbung): Für die DOSBox-Installation hab ich jede Menge Funktionen geschrieben. Die Erstellung der .wpi-Datei ist bisher unveröffentlicht und der größte Brocken.
Andreas Schnellbacher