The reason why your routine does not even replace s
by S
and i
by I
is in the lines:
\ifx#1s\zCapSwitch1\fi
\ifx#1i\zCapSwitch1\fi
In case TeX gathers a number digit-character-token by digit-character-token, it will keep expanding tokens. \fi
is expandable, thus TeX does not stop gathering digits when having encountered \fi
.
Thus TeX is still gathering digits of the number of the \zCapSwitch
-assignment when carrying out the subsequent \ifnum
-comparison.
Thus the new value is not yet assigned to \zCapSwitch
when the subsequent \ifnum
-comparison is carried out.
Just make sure to terminate the \zCapSwitch
-assignment by having the digit-sequence that forms the number trailed by a space or by \relax
:
\ifx#1s\zCapSwitch1 \fi
\ifx#1i\zCapSwitch1 \fi
TeX will take the spaces behind 1
for something that terminates the digit-sequence and therefore will discard them instead of keeping them and producing horizontal glue.
When you do that, the code looks almost the same, but replacing s
and i
works to some extent:
\documentclass[margin=5mm,varwidth]{standalone}
\begin{document}
\newcount\zCapSwitch % UPPERCASE SWITCH
\zCapSwitch0 % SET TO FALSE (NO UPPERCASE CONVERSION NEEDED)
\def\zEnd{\zEnd}
\def\zzIterator#1{%
\ifx#1\zEnd
\else
% ------------ %
% OUTPUT CHUNK %
% ------------ %
% CAPITALIZE "s" AND "i"
%
% YOU NEED SPACES TO TERMINATE DIGIT-SEQUENCES.
%
\ifx#1s\zCapSwitch1 \fi
\ifx#1i\zCapSwitch1 \fi
\ifnum\zCapSwitch=1
\uppercase{#1}%
\zCapSwitch0 %
\else
#1%
\fi
% ------------ %
\expandafter\zzIterator
\fi
}
\def\zIterator#1{%
\zzIterator#1\zEnd
}
\zIterator{Keep spaces intact!}
\end{document}

But spaces are still not preserved.
The reason is that \zzIterator
does process a non-delimited macro-argument.
While gathering the tokens that belong to a non-delimited macro-argument, TeX always drops/removes/discards explicit space-tokens that precede the non-delimited macro-argument.
Besides this the case of the argument of \zIterator
containing brace-groups is not handled.
Here is an approach where \futurelet
is used for "looking ahead" at the meaning of the next token and \afterassignment
-\let
is used for removing tokens whose meaning equals the meaning of the space-token.
This approach sort of preserves spaces.
But it does still not handle the case of the argument of \zIterator
containing brace-groups:
\documentclass[margin=5mm,varwidth]{standalone}
\begin{document}
\newcount\zCapSwitch % UPPERCASE SWITCH
\zCapSwitch0 % SET TO FALSE (NO UPPERCASE CONVERSION NEEDED)
\def\zEnd{\zEnd}%
\long\def\foo#1{#1}%
\long\def\fot#1#2{#1}%
\long\def\sot#1#2{#2}%
\foo{\let\zzSpace= } %
\def\zzIterator{\futurelet\zzNext\zzSpacefork}%
\def\zzSpacefork{%
\ifx\zzNext\zzSpace
\expandafter\fot
\else
\expandafter\sot
\fi
{ \afterassignment\zzIterator\let\zzNext= }%
{\zzIteratorA}%
}%
\def\zzIteratorA#1{%
\ifx#1\zEnd
\else
% ------------ %
% OUTPUT CHUNK %
% ------------ %
% CAPITALIZE "s" AND "i"
%
% YOU NEED SPACES TO TERMINATE DIGIT-SEQUENCES.
%
\ifx#1s\zCapSwitch1 \fi
\ifx#1i\zCapSwitch1 \fi
\ifnum\zCapSwitch=1
\uppercase{#1}%
\zCapSwitch0 %
\else
#1%
\fi
% ------------ %
\expandafter\zzIterator
\fi
}
\def\zIterator#1{%
\zzIterator#1\zEnd
}
\zIterator{Keep spaces intact!}
\end{document}

Handling brace-groups is an interesting subject because having TeX "look ahead" at the meaning of the next token via \futurelet
or \let
actually is not sufficient:
This way you can find out whether the meaning of the next token equals, e.g., the meaning of the character-token {1
, i.e., the meaning of that character-token whose character-code is 123—123 is the number of the code-point of the {
-character in the TeX-engine's internal character representation scheme—and whose category-code is 1(begin group). But you cannot find out whether that next token is explicit, i.e., whether that next token is the explicit {1(begin group)
-token, or is implicit, i.e., is something like the \bgroupcontrol word
-token, with \let\bgroup={
.
"Explicitness/implicitness" of character-tokens of category-code 1(begin group) respectively category-code 2(end group) does matter because non-delimited macro-arguments being empty or having a leading explicit space-token or consisting of more than one token and delimited macro-arguments containing the argument-delimiter are to be nested into a pair of explicit character-tokens of category-code 1(begin group) respectively category-code 2(end group) while implicit character-tokens of category-code 1(begin group) respectively category-code 2(end group) will by TeX not be taken for markers for the begin or the end of a macro-argument.
Besides this: Some day somebody might—in addition to the character {
respectively }
—assign category-code 1(begin group) respectively category-code 2(end group) to some other characters, which then will, e.g., in matters of beginning/ending a local scope and in matters of gathering macro-arguments and in matters of gathering ⟨balanced text⟩, be treated by TeX like {
respectively }
, but whose meaning will not equal the meaning of {1(begin group)
respectively }2(end group)
as the character-codes are different.
I can offer a \romannumeral0
-expansion-based tail-recursive routine
for replacing the explicit catcode-11(letter)-character-tokens s
and i
by explicit catcode-11(letter)-character-tokens S
respectively I
where
- the result is delivered after two "hits" by
\expandafter
.
- no temporay assignments and the like take place. E.g., things like
\afterassignment
/\let
/\futurelet
are not used. Therefore the routine can also be used in expansion-contexts, e.g., within \csname..\endcsname.
- (unmatched)
\if..
/\else
/\fi
in the argument do not disturb the routine as the routine is based on delimited arguments and therefore does not use \if..
-tests at all.
- although the token
\UDSelDOm
is used as "sentinel-token" in some places, that token can occur within the argument, thus there are no forbidden tokens for the argument. (Except that you generally cannot use \outer
tokens in macro-arguments.)
- the mechanism can cope with things being nested in curly braces.
- you don't need to think about the question of distinguishing explicit brace- and/or space-tokens from their implicit pendants. (When "looking ahead" at the next token via
\let
or \futurelet
, this question probably might cause headache because \let
and \futurelet
let you cope just with the meanings of tokens while the meaning of a character-token does not include information regarding whether that character-token is an explicit or an implicit character-token...)
A side-effect of the routine is that it replaces
- all explicit-catcode 1(begin group)-character-tokens by explicit curly-opening-brace-character-tokens (
{
) of catcode 1(begin group).
- all explicit-catcode 2(end group)-character-tokens by explicit curly-closing-brace-character-tokens (
}
) of catcode 2(end group).
Usually {
is the only character whose catcode is 1(begin group).
Usually }
is the only character whose catcode is 2(end group).
Therefore this usually should not be a problem.
The routine is just an example. You can enhance it to replace characters other than s
and i
by modifying the macros \UDsiSelect
and \UDsiFork
. If you do that, you also need to adjust the call to \UDsiFork
within \zIteratorTailRecursiveLoop
.
(This is a bunch of code. If you were not insisting in plain TeX, things could be shortened, e.g., using expl3.)
%%
%% As you wished plain TeX, don't use latex/pdflatex but use tex/pdftex for compiling.
%%
%%=============================================================================
%% Paraphernalia:
%% \UDfirstoftwo, \UDsecondoftwo,
%% \UDExchange, \UDPassFirstBehindThirdToSecond
%% \UDremovespace, \UDCheckWhetherNull, \UCheckWhetherBrace,
%% \UDCheckWhetherLeadingSpace, \UDExtractFirstArg
%%=============================================================================
\long\def\UDfirstoftwo#1#2{#1}%
\long\def\UDsecondoftwo#1#2{#2}%
\long\def\UDExchange#1#2{#2#1}%
\long\def\UDPassFirstBehindThirdToSecond#1#2#3{#2{#3}{#1}}%
\UDfirstoftwo{\def\UDremovespace}{} {}%
%%=============================================================================
%% Check whether argument is empty:
%%=============================================================================
%% \UDCheckWhetherNull{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is empty>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is not empty>}%
%%
%% Due to \romannumeral0-expansion the result is delivered after two
%% expansion-steps/after two "hits" by \expandafter.
%%
%% The gist of this macro comes from Robert R. Schneck's \ifempty-macro:
%% <https://groups.google.com/forum/#!original/comp.text.tex/kuOEIQIrElc/lUg37FmhA74J>
%%
\long\def\UDCheckWhetherNull#1{%
\romannumeral0\expandafter\UDsecondoftwo\string{\expandafter
\UDsecondoftwo\expandafter{\expandafter{\string#1}\expandafter
\UDsecondoftwo\string}\expandafter\UDfirstoftwo\expandafter{\expandafter
\UDsecondoftwo\string}\UDfirstoftwo\expandafter{} \UDsecondoftwo}%
{\UDfirstoftwo\expandafter{} \UDfirstoftwo}%
}%
%%=============================================================================
%% Check whether argument's first token is a catcode-1-character
%%=============================================================================
%% \UDCheckWhetherBrace{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked has leading
%% catcode-1-token>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked has no leading
%% catcode-1-token>}%
%%
%% Due to \romannumeral0-expansion the result is delivered after two
%% expansion-steps/after two "hits" by \expandafter.
%%
\long\def\UDCheckWhetherBrace#1{%
\romannumeral0\expandafter\UDsecondoftwo\expandafter{\expandafter{%
\string#1.}\expandafter\UDfirstoftwo\expandafter{\expandafter
\UDsecondoftwo\string}\UDfirstoftwo\expandafter{} \UDfirstoftwo}%
{\UDfirstoftwo\expandafter{} \UDsecondoftwo}%
}%
%%=============================================================================
%% Check whether brace-balanced argument's first token is an explicit
%% space token
%%=============================================================================
%% \UDCheckWhetherLeadingSpace{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case <argument
%% which is to be checked>'s 1st token is a
%% space-token>}%
%% {<Tokens to be delivered in case <argument
%% which is to be checked>'s 1st token is not
%% a space-token>}%
%%
%% Due to \romannumeral0-expansion the result is delivered after two
%% expansion-steps/after two "hits" by \expandafter.
%%
\long\def\UDCheckWhetherLeadingSpace#1{%
\romannumeral0\UDCheckWhetherNull{#1}%
{\UDfirstoftwo\expandafter{} \UDsecondoftwo}%
{\expandafter\UDsecondoftwo\string{\UDInnerCheckWhetherLeadingSpace.#1 }{}}%
}%
\long\def\UDInnerCheckWhetherLeadingSpace#1 {%
\expandafter\UDCheckWhetherNull\expandafter{\UDsecondoftwo#1{}}%
{\UDExchange{\UDfirstoftwo}}{\UDExchange{\UDsecondoftwo}}%
{\UDExchange{ }{\expandafter\expandafter\expandafter\expandafter
\expandafter\expandafter\expandafter}\expandafter\expandafter
\expandafter}\expandafter\UDsecondoftwo\expandafter{\string}%
}%
%%=============================================================================
%% Extract first inner undelimited argument:
%%=============================================================================
%% \UDExtractFirstArg{ABCDE} yields {A}
%% \UDExtractFirstArg{{AB}CDE} yields {AB}
%%
%% Due to \romannumeral0-expansion the result is delivered after two
%% expansion-steps/after two "hits" by \expandafter.
%%
\long\def\UDRemoveTillUDSelDOm#1#2\UDSelDOm{{#1}}%
\long\def\UDExtractFirstArg#1{%
\romannumeral0%
\UDExtractFirstArgLoop{#1\UDSelDOm}%
}%
\long\def\UDExtractFirstArgLoop#1{%
\expandafter\UDCheckWhetherNull\expandafter{\UDfirstoftwo{}#1}%
{ #1}%
{\expandafter\UDExtractFirstArgLoop\expandafter{\UDRemoveTillUDSelDOm#1}}%
}%
%%=============================================================================
%% Extract K-th inner undelimited argument:
%%=============================================================================
%% \UDExtractKthArg{<integer K>}{<list of undelimited args>}
%%
%% In case there is no K-th argument in <list of indelimited args> :
%% Does not deliver any token.
%% In case there is a K-th argument in <list of indelimited args> :
%% Does deliver that K-th argument with one level of braces removed.
%%
%% Examples:
%%
%% \UDExtractKthArg{0}{ABCDE} yields: <nothing>
%%
%% \UDExtractKthArg{3}{ABCDE} yields: C
%%
%% \UDExtractKthArg{3}{AB{CD}E} yields: CD
%%
%% \UDExtractKthArg{4}{{001}{002}{003}{004}{005}} yields: 004
%%
%% \UDExtractKthArg{6}{{001}{002}{003}} yields: <nothing>
%%
%% Due to \romannumeral0-expansion the result is delivered after two
%% expansion-steps/after two "hits" by \expandafter.
%%
\long\def\UDExtractKthArg#1{%
\romannumeral0%
% #1: <integer number K>
\expandafter\UDExtractKthArgCheck
\expandafter{\romannumeral\number\number#1 000}%
}%
\long\def\UDExtractKthArgCheck#1#2{%
\UDCheckWhetherNull{#1}{ }{%
\expandafter\UDExtractKthArgLoop\expandafter{\UDfirstoftwo{}#1}{#2}%
}%
}%
\long\def\UDExtractKthArgLoop#1#2{%
\expandafter\UDCheckWhetherNull\expandafter{\UDfirstoftwo#2{}.}{ }{%
\UDCheckWhetherNull{#1}{%
\expandafter\UDExchange
\romannumeral0\UDExtractFirstArgLoop{#2\UDSelDOm}{ }%
}{%
\expandafter\UDExchange\expandafter{\expandafter{\UDfirstoftwo{}#2}}%
{\expandafter\UDExtractKthArgLoop\expandafter{\UDfirstoftwo{}#1}}%
}%
}%
}%
%%=============================================================================
%% Fork whether argument either is an _explicit_
%% catcode 11(letter)-character-token of the set {s, i}
%% or is something else.
%%=============================================================================
%% \UDsiFork{<Argument to check>}{%
%% {<tokens to deliver in case <Argument to check> is s>}%
%% {<tokens to deliver in case <Argument to check> is i>}%
%% {<tokens to deliver in case <Argument to check> is empty or something else>}%
%% }%
%%
%% Due to \romannumeral0-expansion the result is delivered after two
%% expansion-steps/after two "hits" by \expandafter.
%%
\long\def\UDGobbleToExclam#1!{}%
\long\def\UDCheckWhetherNoExclam#1{%
\expandafter\UDCheckWhetherNull\expandafter{\UDGobbleToExclam#1!}%
}%
\long\def\UDsiSelect#1!!s!i!#2#3!!!!{#2}%
\long\def\UDsiFork#1#2{%
\romannumeral
\UDCheckWhetherNoExclam{#1}{%
\UDsiSelect
!#1!s1!i!{\expandafter\UDsecondoftwo\UDExtractKthArg{3}{#2}}% empty
!!#1!i!{\expandafter\UDsecondoftwo\UDExtractKthArg{1}{#2}}% s
!!s!#1!{\expandafter\UDsecondoftwo\UDExtractKthArg{2}{#2}}% i
!!s!i!{\expandafter\UDsecondoftwo\UDExtractKthArg{3}{#2}}% something else without !
!!!!%
}{\expandafter\UDsecondoftwo\UDExtractKthArg{3}{#2}}% something else with !
}%
%%=============================================================================
%% The main routine which calls the main loop:
%%=============================================================================
%% \zIterator{<Argument where s respectively s to be replaced by S respectively I>}
%%
%% Due to \romannumeral0-expansion the result is delivered after two
%% expansion-steps/after two "hits" by \expandafter.
%%
\long\def\zIterator{%
\romannumeral0\zIteratorTailRecursiveLoop{}%
}%
%%=============================================================================
%% The main loop:
%%=============================================================================
%% \zIteratorTailRecursiveLoop{<list of tokens where replacement
%% is already done>}%
%% {<remaining list of tokens where replacement of
%% s/i by S/I must still be performed>}%
%%
%% In case the <remaining list of tokens where replacement of s/i by S/I must
%% still be performed> is empty, you are done, thus deliver the <list of tokens
%% where replacement is already done>.
%% Otherwise:
%% Check if the <remaining list of tokens where replacement of s/i
%% by S/I must still be performed> has a leading space.
%% If so: Add a space-token to the <list of tokens where replacement is
%% already done>.
%% Remove the leading space token from the <remaining list of tokens
%% where replacement of s/i by S/I must still be performed>
%% Otherwise:
%% Check if the <remaining list of tokens where replacement of s/i
%% by S/I must still be performed> has a leading brace.
%% If so: Extract its first component/its first undelimited argument
%% and apply this routine to that extraction and add the
%% result (nested in braces) to the <list of tokens where
%% replacement is already done> .
%% Otherwise:
%% Check if the <remaining list of tokens where replacement
%% of s/i by S/I must still be performed>'s first component
%% is s or i.
%% If so: add "S" respectively "I" to the <list of tokens
%% where replacement is already done> .
%% Otherwise:
%% Add the <remaining list of tokens where replacement
%% of s/i by S/I must still be performed>'s first
%% component to the <list of tokens where replacement
%% is already done> .
%% Remove the first compoinent/the first undelimited argument from
%% the <remaining list of tokens where replacement of s/i by S/I
%% must still be performed>.
\long\def\zIteratorTailRecursiveLoop#1#2{%
% #1 - list of tokens where replacement is already done
% #2 - remaining list of tokens where replacement of s/i by S/I must
% still be performed
\UDCheckWhetherNull{#2}{ #1}{%
\UDCheckWhetherLeadingSpace{#2}{%
\expandafter\UDPassFirstBehindThirdToSecond\expandafter{%
\UDremovespace#2%
}{%
\UDPassFirstBehindThirdToSecond{#1 }{\UDsecondoftwo{}}%
}%
}{%
\expandafter\UDPassFirstBehindThirdToSecond\expandafter{%
\UDfirstoftwo{}#2%
}{%
\UDCheckWhetherBrace{#2}{%
\expandafter\UDPassFirstBehindThirdToSecond\expandafter{%
\romannumeral0\expandafter
\UDExchange\expandafter{\expandafter{%
\romannumeral0\expandafter\zIteratorTailRecursiveLoop
\expandafter{\expandafter}%
\romannumeral0\UDExtractFirstArgLoop{#2\UDSelDOm}%
}}{ #1}%
}{\UDsecondoftwo{}}%
}{%
\expandafter\UDsiFork
\romannumeral0\UDExtractFirstArgLoop{#2\UDSelDOm}{%
{\UDPassFirstBehindThirdToSecond{#1S}{\UDsecondoftwo{}}}%
{\UDPassFirstBehindThirdToSecond{#1I}{\UDsecondoftwo{}}}%
{%
\expandafter\UDPassFirstBehindThirdToSecond\expandafter{%
\romannumeral0\expandafter\UDExchange
\romannumeral0\UDExtractFirstArgLoop{#2\UDSelDOm}{ #1}%
}{\UDsecondoftwo{}}%
}%
}%
}%
}%
}%
{\zIteratorTailRecursiveLoop}%
}%
}%
%%=============================================================================
%% Usage-Examples of \zIterator which show that
%% - the result is delivered after two "hits" by \expandafter
%% - the mechanism is based on expansion only. No temporay assignments take
%% place. Therefore \zIterator can also be used in expansion-contexts,
%% e.g., within \csname..\endcsname,
%% - (unmatched) \if.. /\else/\fi in the argument do not disturb the mechanism.
%% - although the token \UDSelDOm is used as "sentinel-token" in some places,
%% that token can occur within the argument, thus there are no forbidden
%% tokens. (Except that you generally cannot use \outer tokens in
%% macro-arguments.)
%% - the mechanism can cope with things being nested in curly braces.
%%=============================================================================
\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\test
\expandafter\expandafter\expandafter{%
\zIterator{A \TeX \is {\funny } {s sssi}i i \else \UDSelDOm {\fi } do ## not disturb me.}%
}
{\tt\meaning\test}%
\def\aSSbISSIIIISz{Yeah, that's it!}
\csname\zIterator{assbissiiiisz}\endcsname
\bye
\let\zTest=a
in the first place and what all this would be used for. – egreg Nov 18 '19 at 11:07