Page 1 of 1

reverse string without goto

Posted: 16 Sep 2013 15:35
by Sponge Belly
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:

Re: reverse string without goto

Posted: 16 Sep 2013 16:07
by penpen
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

Re: reverse string without goto

Posted: 19 Feb 2018 09:06
by Sponge Belly
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.

Re: reverse string without goto

Posted: 20 Feb 2018 06:10
by npocmaka_
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.

Re: reverse string without goto

Posted: 24 Feb 2018 17:18
by pieh-ejdsch
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