reverse string without goto

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Post Reply
Message
Author
Sponge Belly
Posts: 216
Joined: 01 Oct 2012 13:32
Location: Ireland
Contact:

reverse string without goto

#1 Post by Sponge Belly » 16 Sep 2013 15:35

Building on what I learnt from Dave Benham’s demonstration of escaping special characters inside a for /f loop’s in (…) clause, and Aacini’s use of a for /l loop called by a cmd subshell to emulate a while loop, I’ve cobbled together an alternative method for reversing a string that does not require goto or the length of the string to be reversed.

Code: Select all

[see third post for updated code]
Could someone please explain why I had to suppress the first 5 chars of rev? The cmd inside the in (…) clause behaves as if it’s receiving input from the command line. The rev var is initialised to nothing, or so I thought. On inspection, it had the value “!rev!” :twisted:
Last edited by Sponge Belly on 25 Feb 2018 06:08, edited 1 time in total.

penpen
Expert
Posts: 1988
Joined: 23 Jun 2013 06:15
Location: Germany

Re: reverse string without goto

#2 Post by penpen » 16 Sep 2013 16:07

Sponge Belly wrote:Could someone please explain why I had to suppress the first 5 chars of rev? The cmd inside the in (…) clause behaves as if it’s receiving input from the command line. The rev var is initialised to nothing, or so I thought. On inspection, it had the value “!rev!” :twisted:

The batch variable "rev" is first undefined, then the loop in the new cmd instance is started.
Then the script reaches this part:

Code: Select all

set "rev=!rev!!chr!"
And on an undefined variable in enabled delayed expansion that part is not replaced and handled as normal text:

Code: Select all

Z:\>set "rev="

Z:\>cmd /V:ON
Microsoft Windows XP [Version 5.1.2600]
(C) Copyright 1985-2001 Microsoft Corp.

Z:\>set "char=a"

Z:\>set "rev=!rev!!char!"

Z:\>set rev
rev=!rev!a


If your inspection was similar to this, it's the same reason as above:

Code: Select all

echo("!rev!"
The !rev! just is not replaced, so it seems to contain !rev! (5 chars long)

penpen

Sponge Belly
Posts: 216
Joined: 01 Oct 2012 13:32
Location: Ireland
Contact:

Re: reverse string without goto

#3 Post by Sponge Belly » 19 Feb 2018 09:06

Belated thanks to Penpen for his informative response to my previous effort.

Please find below my revised code for reversing a string:

Code: Select all

@echo off & setLocal enableExtensions disableDelayedExpansion
(call;) %= sets errorLevel to 0 =%
(set lf=^
%= BLANK LINE REQUIRED =%
)
set "cr=" & if not defined cr for /f "skip=1" %%C in (
    'echo(^|replace ? . /w /u'
) do set "cr=%%C"

set ^"orig=^<!-- !^^!^^^^! %%etad%% ^| ^"^^^&^^ --^>^"
call :reverseStr res1 orig || goto end
setLocal enableDelayedExpansion
call :reverseStr res2 orig || (endLocal & goto end)
echo(orig: [!orig!]
echo(res1: [!res1!]
echo(res2: [!res2!]
endLocal

:end - exit program with appropriate errorLevel
endLocal & goto :EOF

:reverseStr result= original=
:: reverses a string
setLocal
set "ddx=!" %= is delayed expansion enabled or disabled? =%
setLocal disableDelayedExpansion
setLocal enableDelayedExpansion
set "die=" & if not defined %2 (
    >&2 echo(  ERROR: var "%2" not defined & set "die=1"
) else set "str=!%2!" %= if =%

if not defined die for %%L in ("!lf!") ^
do if "!str!" neq "!str:%%~L=!" (
    >&2 echo(  ERROR: var "%2" contains linefeeds & set "die=1"
) %= if =%

if not defined die for %%C in ("!cr!") ^
do if "!str!" neq "!str:%%~C=!" (
    >&2 echo(  ERROR: var "%2" contains carriage returns
    set "die=1"
) %= if =%

if defined die (
    endLocal & endLocal & endLocal & set "%1=" & exit /b 1
) %= if =%
endLocal

:: reverse string
for /f delims^=^ eol^= %%R in ('
    cmd /von /q /c set "rev=!%2:~-1!" ^^^& ^
    set "str=!%2:~0,-1!" ^^^& for /l %%I in (^) do ^
    if defined str (set "rev=!rev!!str:~-1!" ^^^& ^
    set "str=!str:~0,-1!"^) else (echo(^!rev^!^^^& exit 0^)
') do set "str=%%R"

setLocal enableDelayedExpansion
:: double carets if returning to enabled delayed expansion
if not defined ddx set "str=!str:^=^^^^!"
:: double quotes
set "str=!str:"=""!"
:: escape exclaims if returning to enabled delayed expansion
if not defined ddx set "str=%str:!=^^^!%" !
:: restore quotes
set "str=!str:""="!"

:: use for /f to pass string back over endLocal boundary
for /f delims^=^ eol^= %%A in ("!str!") do (
    endLocal & endLocal & endLocal & set "%1=%%A" !
) %= for /f =%
exit /b 0 %= reverseStr =%
The subroutine now works regardless of whether it is called with delayed expansion enabled or disabled. CRs and LFs in the string to be reversed are not supported.

Special thanks to Jeb for his for-endLocal technique and kudos to Carlos for his superior method for capturing CR.

npocmaka_
Posts: 512
Joined: 24 Jun 2013 17:10
Location: Bulgaria
Contact:

Re: reverse string without goto

#4 Post by npocmaka_ » 20 Feb 2018 06:10

Here's my attempt (it uses strlen macro):

Code: Select all

:reverse [%1 - string to reverse ; %2 - if defined will store the result in variable with same name]
@echo off
setlocal disableDelayedExpansion
set "str=%~1"
set LF=^


rem ** Two empty lines are required
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"
set $strLen=for /L %%n in (1 1 2) do if %%n==2 (%\n%
      for /F "tokens=1,2 delims=, " %%1 in ("!argv!") do (%\n%
         set "str=A!%%~2!"%\n%
           set "len=0"%\n%
           for /l %%A in (12,-1,0) do (%\n%
             set /a "len|=1<<%%A"%\n%
             for %%B in (!len!) do if "!str:~%%B,1!"=="" set /a "len&=~1<<%%A"%\n%
           )%\n%
           for %%v in (!len!) do endlocal^&if "%%~b" neq "" (set "%%~1=%%v") else echo %%v%\n%
      ) %\n%
) ELSE setlocal enableDelayedExpansion ^& set argv=,

%$strLen% len,str
setlocal enableDelayedExpansion

set /a half=len/2
set "res=!str:~%half%,-%half%!"

for /L %%C in (%half%,-1,0) do (
	set /a len=%len%-1-%%C
	if %%C neq %half% (
		for  %%c in (!len!) do (			
			set "res=!str:~%%c,1!!res!!str:~%%C,1!"
		)
	)

)
endlocal & endlocal & if "%~2" NEQ "" (set %~2=%res%) else echo %res%
Though probably not the fastest possible.if calculating the length and swapping characters is done in one iteration it will be faster.

pieh-ejdsch
Posts: 239
Joined: 04 Mar 2014 11:14
Location: germany

Re: reverse string without goto

#5 Post by pieh-ejdsch » 24 Feb 2018 17:18

Instead of just cutting the backward string into just 2 parts, the number of loops into for can be reduced to a certain mediocre level.
With only two characters converted, there are a maximum of about 4000 times in the loop.
With the maximum line length approx 700 characters can be shifted at once.
But that would be 700 times in a for loop.
With the correct calculation, this can be reduced to a maximum of about 200 times recurring forloops.
So a maximum of 90 conversions at once within a line and 90 times the string in total swap.
can this also be calculated by logarithm?
but maybe that's enough. I have no idea how I can calculate otherwise.

Code: Select all

@echo off
setlocal disableDelayedExpansion
if NOT defined stringVar set "stringVar=%~1"
if NOT defined stringVar echo no stringVar defined! & exit /b
call :setAllMacros

setlocal enabledelayedexpansion
%strLen(var):var=!stringVar!%
set /a loop=3
if %len% gtr 150 set /a loop=len/10
if %len% gtr 600 set /a loop=len/25
if %len% gtr 1500 set /a loop=len/90
endlocal & set /a loop=%loop%, len=%len%
 rem Reverse var
set "rev=!R:X,1!"
set "revers="
set "reverStr="
setlocal enabledelayedexpansion
for /l %%L in (1 1 %loop%) do set "revers=!Rev:X=~%%L!!revers!"
for /l %%L in (0 %loop% %len%) do (
  set "R= !stringVar:~%%L!"
  set "ReverStr=%revers%!ReverStr!"
)
if "%~2" equ "" echo !ReverStr!
for /f delims^=^ eol^= %%i in ("!ReverStr!") do (
  endlocal
  endlocal
  if "%~2" neq "" set "%~2=%%i"
)
exit /b 

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:setAllMacros
:: define LF as a Line Feed (newline) character
set ^"LF=^

^" Above empty line is required - do not remove
:: define a newline with line continuation
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:strLen.var
:: create a macro with reduced For loops
:: Parenthesis are included
:: usage: %strLen(var):var=!stringVar!% 
:: 
@for %%T in ("%temp%\%~n0.tmp.cmd") do @(
 @ >%%T (
  echo( @set strLen(var^)=(%%\n%%
  echo( set "str=Avar"%%\n%%
  echo( set "len=0"%%\n%%
  for /l %%i in (12 -1 0) do @(
   echo( set /a "len|=1<<%%i"%%\n%%
   echo( for %%%%# in (!len!^) do if .!str:~%%%%#^^^^^^,1!==. set /a "len&=~1<<%%i"%%\n%%
  )
  echo(^)
 )
 call %%T
 del %%T
)

set "LF="
set "\n="
exit /B
edit
Ok I have now combined the calculation of the number of substitutions per line with a square check.
Can these be calculated faster as a root?
Creation of the macro with reduced loop (saves read and execute) I pushed without temporary file - for it partially within delayedexpansion.
Since the assembly for the variable of the substitutions for line eh must start from 1, the calculation of the quatrate is carried out in the same loop.
Thus, the number of replacements and the assembly of the string are approximately balanced and increased evenly.
the coarse maximum number for the quadratic calculation is calculated when determining the string length.
Thus, the code has become a bit shorter.
Overall, in total, in all loops there are between 25 and 200 repetitions for the whole batch.

Code: Select all

@echo off
setlocal disableDelayedExpansion
if NOT defined stringVar set "stringVar=%~1"
if NOT defined stringVar echo no stringVar defined! & exit /b
set "rev=!R:~X,1!"
set "revMax="
set "revers="
set "reverStr="
set ^"LF=^

^" Above empty line is required - do not remove
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"
set forI= set /a "len|=1<<FORi"%\n%
 for %%# in (!len!^) do if .!str:~%%#^^^,1!==. ( set /a "len&=~1<<FORi"%\n%
  ^) else if not defined revMax set /a "revMax=(FORi-3)*(FORi-3)+10"
@set strLen(var^)=(%\n%
 set "str=Avar"%\n%
 set "len=0"
setlocal enabledelayedexpansion
for /l %%i in (12 -1 0) do @ set "strLen(var)=!strLen(var)!!lf!!forI:FORi=%%i!"
set strLen(var)=!strLen(var)!!lf!)

%strLen(var):var=!stringVar!%
set /a L=loop=1
for /l %%L in (1 1 %revMax%) do if !L! lss %len% ( set /a L=%%L*%%L, loop=%%L
 set "revers=!Rev:X=%%L!!revers!"
)
if not defined revers ( set "ReverStr=!stringVar!"
 ) else for /l %%L in (0 %loop% %len%) do ( set "R= !stringVar:~%%L!"
  set "ReverStr=%revers%!ReverStr!"
)
if "%~2" equ "" echo !ReverStr!
for /f delims^=^ eol^= %%i in ("!ReverStr!") do (
  endlocal
  endlocal
  if "%~2" neq "" set "%~2=%%i"
)
exit /b 

aschipfl
Posts: 9
Joined: 13 Feb 2019 03:33

Re: reverse string without goto

#6 Post by aschipfl » 26 Dec 2021 17:29

Here is a method without first determining the length of the string. It is designed as a function that accepts two arguments:
  • the name of the variabe that receives the resulting string;
  • the name of the variable that contains the original string;

Code: Select all

@echo off
setlocal EnableExtensions DisableDelayedExpansion
set "ORIGINAL=<some^sample!string%%incorporating&special|characters=and%%even(unbalanced)quotation"marks^>^"
call :REVERSE_STRING REVERSED ORIGINAL
set "REVERSED"
endlocal
exit /B
    
    
:REVERSE_STRING
    setlocal DisableDelayedExpansion
    set "#RTN=%~1"
    set "#STR=%~2"
    setlocal EnableDelayedExpansion
    set "%#RTN%="
    if defined %#STR% (
        set /A "LIM=(1<<13)-1"
        for /L %%I in (12,-1,0) do (
            set "%#RTN%=" & set /A "CNT=1<<%%I, DBL=CNT<<1"
            for %%J in (!CNT!) do (
                for /L %%K in (0,!DBL!,!LIM!) do (
                    for %%L in (!DBL!) do (
                        set "PRT=!%#STR%:~%%K,%%L!"
                        if "!PRT:~,-%%J!"=="" set /A "LIM=%%K+%%J-2"
                        set "%#RTN%=!%#RTN%!!PRT:~-%%J!!PRT:~,-%%J!"
                    )
                )
            )
            set "%#STR%=!%#RTN%!"
        )
    )
    for /F "delims=" %%J in (^""!%#RTN%!"^") do (
        endlocal & endlocal & set "%#RTN%=%%~J"
    )
    exit /B
The string may contain all special characters except line-breaks (that is carriage-return and line-feed characters).

Sponge Belly
Posts: 216
Joined: 01 Oct 2012 13:32
Location: Ireland
Contact:

Re: reverse string without goto

#7 Post by Sponge Belly » 19 Feb 2022 17:08

Hello All! :)

Thanks again to @penpen for his informative reply. And belated thanks to @npocmaka_, @pieh-ejdsch, and @aschipfl for their awesome versions of how to reverse a string. I must confess I can’t completely follow their approaches. Something about repeatedly halving the string and substituting the last character of the first half with the first character of the second half?

Anyway, I now realise that my own attempt was naive and inefficient. But that’s what I love about this forum. The quality of contributions here is top notch. It encourages me to continually try harder and do more, even if I never reach the same dizzying levels as the forum members mentioned above.

Keep up the good work! 8)

- SB

Aacini
Expert
Posts: 1885
Joined: 06 Dec 2011 22:15
Location: México City, México
Contact:

Re: reverse string without goto

#8 Post by Aacini » 20 Feb 2022 21:34

I am very late to this party but I like this problem, so here it is my answer. It use a recursive subroutine to reverse substrings that have a lenght that is a power of 2. Each string is recursively split in two parts by half until a substring have just two characters, that are directly reversed.

Code: Select all

@echo off
setlocal EnableDelayedExpansion

:nextString
echo/
set /P "string=String: "
if errorlevel 1 goto :EOF

echo "!string!"
set "result="
for /L %%b in (12,-1,1) do if defined string (
   set /A "len=1<<%%b, pos=len-1"
   for /F "tokens=1,2" %%i in ("!len! !pos!") do (
      if "!string:~%%j!" neq "" (
         call :RevPow2Substr "!string:~-%%i!" %%i
         set "string=!string:~0,-%%i!"
      )
   )
)
echo "!result!!string!"
goto nextString


:RevPow2Substr str len

set "str=%~1"
if %2 equ 2 set "result=%result%%str:~1%%str:~0,1%" & exit /B
set /A "len=%2/2"
set "left=!str:~0,%len%!" & set "right=!str:~%len%!"
(
call :RevPow2Substr "%right%" %len%
call :RevPow2Substr "%left%" %len%
)
exit /B
Antonio

T3RRY
Posts: 243
Joined: 06 May 2020 10:14

Re: reverse string without goto

#9 Post by T3RRY » 21 Feb 2022 06:27

Aacini wrote:
20 Feb 2022 21:34
I am very late to this party but I like this problem
Antonio
Same -

My take, doesn't bother getting string length - splits string by character and rebuilds string in reverse order.

Usage: %reverse% StringVar ReturnVar
DE need not be enabled prior to expansion.

Code: Select all

@echo off

	Set "$Example=/?! ^&f><oo%%.^|bar ^ ""` &^^ & | *((:~=!)^^ ^^^ " -"

REM requires calling environment to NOT have EnableDelayedExpanion active.
	If "!!" == "" Exit /B 1

(Set \n=^^^

%= Newline var \n for multi-line macro definition - Do not modify. =%)

Set Reverse=For %%n in (1 2)Do if %%n==2 (%\n%
	For /F "tokens=1,2 Delims=, " %%E in ("!Reverse_Args!")Do (%\n: Prep string for Parsing =%
		Set "TempString=!%%~E!"%\n%
		If Defined TempString (%\n%
			Set ^^^"TempString=!TempString:"=``'``!"%\n%
			Set "TempString=!TempString:^=^^!"%\n%
			Set "TempString=!TempString:&=^&!"%\n%
			Set "TempString=!TempString:|=^|!"%\n%
			Set "TempString=!TempString:<=^<!"%\n%
			Set "TempString=!TempString:>=^>!"%\n%
			Set "TempString=!TempString:^=^^^!"%\n%
			Set "NewString="%\n%
			Set "Pos[#]=0"%\n: Split string by character and rebuild; handle detection and escaping of poison characters '!' '^' and '"' =%
			For /f "usebackq Delims=" %%G in (`%Systemroot%\System32\cmd.exe /u /c ^"Echo(!TempString!^"^^^|%Systemroot%\System32\find.exe /v "[false_match_%~n0]"^^^|%Systemroot%\System32\findstr.exe "^^"`)Do (%\n%
				Set /A "Pos[#]+=1"%\n%
				Set "Char="%\n%
				If "^%%~G" == "^^" (%\n: Character is a Caret =%
					Set "Char=^"%\n%
				)Else (%\n%
					Set "Char="%\n%
					If "^^^%%~G" == "^^^!" (%\n: Character is an Exclamation mark =%
						Set "Char=^^^%%~G"%\n%
					) Else (%\n%
						Set Char=^^%%~G%\n%
					)%\n%
				)%\n%
				Set "NewString=!Char!!NewString!"%\n%
			)%\n%
		)%\n: end string rebuild; Enact Escaping restorations =%
		Set "NewString=!NewString:^^^=^^!"%\n%
		Set "NewString=!NewString:^^={TwoCarets}!"%\n%
		Set "NewString=!NewString:&^^=&^!"%\n%
		Set "NewString=!NewString:|^^=|^!"%\n%
		Set "NewString=!NewString:<^^=<^!"%\n%
		Set "%%~F=!NewString:>^^=>^!"%\n%
		Set "NewString=!NewString:^=!"%\n%
		Set "NewString=!NewString:{TwoCarets}=^!"%\n%
		Set ^^^"NewString=!NewString:``'``="!"%\n%
		Set "Return=$Return"%\n%
		If Defined TempString (%\n%
			Set "Return=%%F"%\n%
		)Else Set "NewString="%\n%
	)%\n: Rebuild complete; return across endlocal =%
	For /f "tokens=1* Delims=`" %%R in ("!Return!`!NewString!")Do (%\n%
		Endlocal ^& Set "%%R=%%S"%\n%
	)%\n%
)Else Setlocal EnableDelayedExpansion ^& Set Reverse_Args=
CLS
Rem Examples

	%Reverse% $Example $Result1
	%Reverse% $Result1 $Result2
	Set $

Goto:Eof

Latest Edit: Updated to reverse literal string without requiring string be escaped prior. Preserves any carets present in string.
Last edited by T3RRY on 01 Dec 2022 08:20, edited 5 times in total.

Sponge Belly
Posts: 216
Joined: 01 Oct 2012 13:32
Location: Ireland
Contact:

Re: reverse string without goto

#10 Post by Sponge Belly » 30 Oct 2022 06:31

Hi Again! :)

Belated thanks to Aacini and T3RRY for their excellent contributions.

Below is my final word on the topic—a Batch/JScript hybrid:

Code: Select all

@if (@X==@Y) @then
:: Batch
@echo off & setLocal enableExtensions disableDelayedExpansion

set ^"strFwd=short ^" ^< %% ^| ^^ ! string^"

set "strRev=" & for /f "delims="eol^= %%R in ('
    cscript //nologo //e:jscript "%~dpf0" strFwd
') do set "strRev=%%R"

set str

endLocal & goto :EOF

@end // JScript

WScript.StdOut.WriteLine(
    WSH.CreateObject('WScript.Shell').
    ExpandEnvironmentStrings('%'+WScript.Arguments(0)+'%').
    split('').reverse().join('')
);
There might be some code page issues with this approach, but that's a whole other box of crayons! :lol:

Happy Halloween!

- SB

T3RRY
Posts: 243
Joined: 06 May 2020 10:14

Re: reverse string without goto

#11 Post by T3RRY » 26 Nov 2022 02:11

Sponge Belly wrote:
30 Oct 2022 06:31
Hi Again! :)

Belated thanks to Aacini and T3RRY for their excellent contributions.

Below is my final word on the topic—a Batch/JScript hybrid:

Code: Select all

@if (@X==@Y) @then
:: Batch
@echo off & setLocal enableExtensions disableDelayedExpansion

set ^"strFwd=short ^" ^< %% ^| ^^ ! string^"

set "strRev=" & for /f "delims="eol^= %%R in ('
    cscript //nologo //e:jscript "%~dpf0" strFwd
') do set "strRev=%%R"

set str

endLocal & goto :EOF

@end // JScript

WScript.StdOut.WriteLine(
    WSH.CreateObject('WScript.Shell').
    ExpandEnvironmentStrings('%'+WScript.Arguments(0)+'%').
    split('').reverse().join('')
);
There might be some code page issues with this approach, but that's a whole other box of crayons! :lol:

Happy Halloween!

- SB
Nice work. triggered me to review mine - now updated with a macro to reverse any literal string comprised of ascii printable characters.

Post Reply