8

I would like to build a TeX macro that would transform its argument by expanding every number into a sequence of letter (always the same letter). For example 3(2)2(1) should produce AAA(AA)AA(A).

I already found a way to produce an arbitrary number of A from a number:

\newcommand\expandtoA[1]{%
  \foreach \index in {1, ..., #1} {A}
}

Thus, \expandtoA{10} will be expanded into AAAAAAAAAA.

But, I am looking at a way to find all the numbers in the argument and substitute each of them by the number of A's it represents.

8
  • What about 2(11)? Should it read "one and one" (AA(AA)) or "eleven" (AA(AAAAAAAAAAA))? – Phelype Oleinik Jan 23 at 18:28
  • It should be eleven AA(AAAAAAAAAAA). But, If you have a solution for any of them, I'm taking it. :-) – perror Jan 23 at 18:30
  • Nesting is out of use... There should be only one level of parenthesis at the time. – perror Jan 23 at 18:31
  • If you want to handle with nesting, maybe you should include more examples of what you need. – Jairo A. del Rio Jan 23 at 18:33
  • 2
    note that \foreach does not work by expansion so the name you suggest here is misleading. – David Carlisle Jan 23 at 18:55
8

This does work via expansion so as shown works in \typeout as well as the typeset paragraph.



\documentclass{article}

\def\zz#1{\zzz#1(\relax)}
\def\zzz#1(#2){\zzA{#1}\ifx\relax#2\else(\zzA{#2})\expandafter\zzz\fi}
\def\zzA#1{\ifnum\numexpr0#1\relax>0 A\expandafter\zzA\expandafter{\the\numexpr#1-1\relax}\fi}

\begin{document}

\zz{3(2)2(1)}

\typeout{\zz{3(2)2(1)}}

\end{document}

puts

AAA(AA)AA(A)

on the terminal.

1
  • Nice answer, it has very few dependencies, but the other answers are quite interesting as well! – perror Jan 23 at 19:26
8

Here's an expandable approach using expl3. \Replicate[<tokens>]{<token list>} It will loop over your input <token list> looking for any sequence of digits and replacing them by that amount of repetitions of the input <tokens> (by default A). Everything else that is not a digit will be just forwarded to the output, so you can have things like

\Replicate{3(2)2(1)}
\Replicate[Z]{3(2)2(1)}
\Replicate[(Z)]{3(2)2(1)}
\Replicate[Z]{3 \textbf{(2) 2}( 1 )}
\edef\zzzzzzzzzzzz{\Replicate[Z]{3\textbf{(2)2}(1)}}
\texttt{\meaning\zzzzzzzzzzzz}

produce:

enter image description here


Here's the code:

\documentclass{article}
\usepackage{xparse}
\pagestyle{empty}
\ExplSyntaxOn
\NewExpandableDocumentCommand \Replicate { O{A} m }
  { \perror_replicate:nn {#1} {#2} }
\cs_new:Npn \perror_replicate:nn #1 #2
  {
    \__perror_replicate_loop:w #2
      \q_recursion_tail \q_recursion_stop {#1} { }
  }
\cs_new:Npn \__perror_replicate_loop:w #1 \q_recursion_stop
  {
    \tl_if_head_is_N_type:nTF {#1}
      { \__perror_replicate_parse_token:N }
      {
        \tl_if_head_is_group:nTF {#1}
          { \__perror_replicate_nested:n }
          { \__perror_replicate_output_space:w }
      }
    #1 \q_recursion_stop
  }
\use:nn { \cs_new:Npn \__perror_replicate_output_space:w } { ~ }
  { \__perror_replicate_output:nw { ~ } }
\cs_new:Npn \__perror_replicate_output:nw #1 #2 \q_recursion_stop #3 #4
  { \__perror_replicate_loop:w #2 \q_recursion_stop {#3} { #4 #1 } }
\cs_new:Npn \__perror_replicate_nested:n #1 #2 \q_recursion_stop #3
  {
    \exp_args:Ne \__perror_replicate_output:nw
      { { \perror_replicate:nn {#3} {#1} } }
    #2 \q_recursion_stop {#3}
  }
\cs_new:Npn \__perror_replicate_end:nn #1 #2 { \exp_not:n {#2} }
\cs_new:Npn \__perror_replicate_parse_token:N #1
  {
    \quark_if_recursion_tail_stop_do:Nn #1
      { \__perror_replicate_end:nn }
    \__perror_replicate_if_digit:NTF #1
      { \__perror_replicate_collect_number:nw }
      { \__perror_replicate_output:nw }
    {#1}
  }
\prg_new_conditional:Npnn \__perror_replicate_if_digit:N #1 { TF }
  {
    \if_int_compare:w 10 < 9 \token_to_str:N #1 \exp_stop_f:
      \prg_return_true:
    \else:
      \prg_return_false:
    \fi:
  }
\cs_new:Npn \__perror_replicate_collect_number:nw #1 #2 \q_recursion_stop
  {
    \tl_if_head_is_N_type:nTF {#2}
      { \__perror_replicate_collect_number:nN }
      { \__perror_replicate_finish_number:nw }
    {#1} #2 \q_recursion_stop
  }
\cs_new:Npn \__perror_replicate_collect_number:nN #1 #2
  {
    \quark_if_recursion_tail_stop_do:Nn #2
      {
        \__perror_replicate_finish_number:nw {#1}
        \q_recursion_tail \q_recursion_stop
      }
    \__perror_replicate_if_digit:NTF #2
      { \__perror_replicate_collect_number:nw { #1 #2 } }
      { \__perror_replicate_finish_number:nw {#1} #2 }
  }
\cs_new:Npn \__perror_replicate_finish_number:nw #1 #2 \q_recursion_stop #3
  {
    \exp_args:Ne \__perror_replicate_output:nw
      { \prg_replicate:nn {#1} {#3} }
    #2 \q_recursion_stop {#3}
  }
\ExplSyntaxOff

\begin{document}
\Replicate{3(2)2(1)}

\Replicate[Z]{3(2)2(1)}

\Replicate[(Z)]{3(2)2(1)}

\Replicate[Z]{3 \textbf{(2) 2}( 1 )}

\edef\zzzzzzzzzzzz{\Replicate[Z]{3\textbf{(2)2}(1)}}

\texttt{\meaning\zzzzzzzzzzzz}

\end{document}
5

A piece of cake using LuaLaTeX (but yes, only works with LuaLaTeX):

\documentclass{standalone}
\usepackage{luacode}
\newcommand\myexpand[2]{%
\directlua{local function myexpand(x,s) return (x:gsub("(\csstring\%d+)", function(u) return s:rep(math.floor(u)) end)) end tex.sprint(myexpand(\luastring{#1},\luastring{#2}))}}
\begin{document}
\myexpand{(3)2(2)1}{A}
\end{document}

enter image description here

3
  • Ah, this is extremely nice to have access to a full programming language. But, I am stuck with the good old TeX macros! But, nice answer. Thanks! – perror Jan 23 at 18:49
  • 1
    @perror No problem. When you have the opportunity (or the will) to change, there's a solution for you :) – Jairo A. del Rio Jan 23 at 18:50
  • 1
    I am looking at LuaTeX and Xetex more and more seriously these days! :-) – perror Jan 23 at 18:52
4

A recursive implementation using etoolbox:

enter image description here

\documentclass{article}

\usepackage{etoolbox}

\makeatletter
\newcommand{\myexpand@}[2]{%
  \xdef\@sequence{#2}%
  \patchcmd{\@sequence}{1}{#1}{\myexpand@{#1}{\@sequence}}{}%
  \patchcmd{\@sequence}{2}{#1#1}{\myexpand@{#1}{\@sequence}}{}%
  \patchcmd{\@sequence}{3}{#1#1#1}{\myexpand@{#1}{\@sequence}}{}%
  \patchcmd{\@sequence}{4}{#1#1#1#1}{\myexpand@{#1}{\@sequence}}{}%
  \patchcmd{\@sequence}{5}{#1#1#1#1#1}{\myexpand@{#1}{\@sequence}}{}%
  \patchcmd{\@sequence}{6}{#1#1#1#1#1#1}{\myexpand@{#1}{\@sequence}}{}%
  \patchcmd{\@sequence}{7}{#1#1#1#1#1#1#1}{\myexpand@{#1}{\@sequence}}{}%
  \patchcmd{\@sequence}{8}{#1#1#1#1#1#1#1#1}{\myexpand@{#1}{\@sequence}}{}%
  \patchcmd{\@sequence}{9}{#1#1#1#1#1#1#1#1#1}{\myexpand@{#1}{\@sequence}}{}%
}
\newcommand{\myexpand}[2]{%
  \myexpand@{#1}{#2}% Replace numbers with copies of #1
  \@sequence% Print sequence
}
\makeatother

\begin{document}

\myexpand{A}{(3)2(2)1}

\end{document}
2

In case you are interested in some sort of syntax-error-management—which I would probably not be as the code of David Carlisle is considerably shorter and faster as long as the user obeys the rules;-)—I can offer a rather slow \romannumeral-expansion-based routine

\expandtoA{⟨sequence of numbers denoted by digits and separated from each other by properly matched not nested parentheses⟩}{⟨tokens in case of error⟩}

which does without whatsoever TeX-extensions and delivers the result after two expansion-steps/after two "hits" by \expandafter.

Digits and parentheses in the 1st argument must be explicit character tokens of category code 12(other).

Empty pairs of matched unnested parentheses are allowed.

Spaces are not allowed.

In case the 1st argument is not of the described pattern, ⟨tokens in case of error⟩ will be delivered instead of a "conversion" to "A".
You can use ⟨tokens in case of error⟩ for triggering a LaTeX-error-message or whatever.

There definitely is room for improvement/shortcuts.

\documentclass{article}

\makeatletter    
%%=============================================================================
%% Paraphernalia:
%%    \UD@firstoftwo, \UD@secondoftwo, \UD@Exchange, \UD@PassFirstToSecond, 
%%    \UD@stopromannumeral, \UD@CheckWhetherNull, 
%%    \UD@CheckWhetherLeadingTokens, \UD@replicate
%%=============================================================================
\newcommand\UD@firstoftwo[2]{#1}%
\newcommand\UD@secondoftwo[2]{#2}%
\newcommand\UD@Exchange[2]{#2#1}%
\newcommand\UD@PassFirstToSecond[2]{#2{#1}}%
\@ifdefinable\UD@stopromannumeral{\chardef\UD@stopromannumeral=`\^^00}%
%%-----------------------------------------------------------------------------
%% Check whether argument is empty:
%%.............................................................................
%% \UD@CheckWhetherNull{<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>}%
%%
%% The gist of this macro comes from Robert R. Schneck's \ifempty-macro:
%% <https://groups.google.com/forum/#!original/comp.text.tex/kuOEIQIrElc/lUg37FmhA74J>
\newcommand\UD@CheckWhetherNull[1]{%
  \romannumeral\expandafter\UD@secondoftwo\string{\expandafter
  \UD@secondoftwo\expandafter{\expandafter{\string#1}\expandafter
  \UD@secondoftwo\string}\expandafter\UD@firstoftwo\expandafter{\expandafter
  \UD@secondoftwo\string}\expandafter\UD@stopromannumeral\UD@secondoftwo}{%
  \expandafter\UD@stopromannumeral\UD@firstoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether argument's leading tokens form a specific 
%% token-sequence that does neither contain explicit character tokens of 
%% category code 1 or 2 nor contain tokens of category code 6:
%%.............................................................................
%% \UD@CheckWhetherLeadingTokens{<argument which is to be checked>}%
%%                              {<a <token sequence> without explicit 
%%                                character tokens of category code
%%                                1 or 2 and without tokens of
%%                                category code 6>}%
%%                              {<internal token-check-macro>}%
%%                              {<tokens to be delivered in case
%%                                <argument which is to be checked> has
%%                                <token sequence> as leading tokens>}%
%%                              {<tokens to be delivered in case 
%%                                <argument which is to be checked>
%%                                does not have <token sequence> as
%%                                leading tokens>}%
\newcommand\UD@CheckWhetherLeadingTokens[3]{%
  \romannumeral\UD@CheckWhetherNull{#1}%
  {\expandafter\UD@stopromannumeral\UD@secondoftwo}%
  {%
    % Let's nest things into \UD@firstoftwo{...}{} to make sure they are nested in braces
    % and thus do not disturb when the test is carried out within \halign/\valign:
    \expandafter\UD@firstoftwo\expandafter{%
      \expandafter\expandafter\expandafter\UD@stopromannumeral
      \romannumeral
      \expandafter\UD@secondoftwo\string{\expandafter\UD@@CheckWhetherLeadingTokens#3{\relax}#1#2}{}}{}%
  }%
}%
\newcommand\UD@@CheckWhetherLeadingTokens[1]{%
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
  {\UD@Exchange{\UD@firstoftwo}}{\UD@Exchange{\UD@secondoftwo}}%
  {\expandafter\expandafter\expandafter\UD@stopromannumeral
   \expandafter\expandafter\expandafter}%
  \expandafter\UD@secondoftwo\expandafter{\string}%
}%
%%-----------------------------------------------------------------------------
%% \UD@internaltokencheckdefiner{<internal token-check-macro>}%
%%                              {<token sequence>}%
%% Defines <internal token-check-macro> to snap everything 
%% until reaching <token sequence>-sequence and spit that out
%% nested in braces.
%%-----------------------------------------------------------------------------
\newcommand\UD@internaltokencheckdefiner[2]{%
  \@ifdefinable#1{\long\def#1##1#2{{##1}}}%
}%
%------------------------------------------------------------------------------
% \UD@replicate{<number>}{<tokens>}
%------------------------------------------------------------------------------
\newcommand\UD@replicateloop[3]{%
  \if m#3\expandafter\UD@firstoftwo\else\expandafter\UD@secondoftwo\fi
  {\UD@replicateloop{#1}{#2#1}}{\UD@stopromannumeral#2}%
}%
\newcommand\UD@replicate[2]{%
  \romannumeral
  \expandafter\UD@Exchange\expandafter{\romannumeral\number\number#1 000}%
                                      {\UD@replicateloop{#2}{}}\relax
}%
%------------------------------------------------------------------------------
\UD@internaltokencheckdefiner{\UD@CheckLeftParen}{(}%
\UD@internaltokencheckdefiner{\UD@CheckRightParen}{)}%
\UD@internaltokencheckdefiner{\UD@CheckZero}{0}%
\UD@internaltokencheckdefiner{\UD@CheckOne}{1}%
\UD@internaltokencheckdefiner{\UD@CheckTwo}{2}%
\UD@internaltokencheckdefiner{\UD@CheckThree}{3}%
\UD@internaltokencheckdefiner{\UD@CheckFour}{4}%
\UD@internaltokencheckdefiner{\UD@CheckFive}{5}%
\UD@internaltokencheckdefiner{\UD@CheckSix}{6}%
\UD@internaltokencheckdefiner{\UD@CheckSeven}{7}%
\UD@internaltokencheckdefiner{\UD@CheckEight}{8}%
\UD@internaltokencheckdefiner{\UD@CheckNine}{9}%
%------------------------------------------------------------------------------
\newcommand\expandtoA[2]{%
  \romannumeral\expandtoALoop{#1}{#2}{}{}{}%
}%
%------------------------------------------------------------------------------
\newcommand\expandtoALoop[5]{%
  % #1 Remaining argument to examine
  % #2 Tokens in case of error
  % #3 Flag denoting how next number must terminate: 
  %    Empty -> next number must terminate due to opening parenthesis or emptiness of remaining argument to examine.
  %    Not empty -> next number must terminate due to closing parenthesis.
  % #4 Digits of next number gathered so far.
  % #5 Result gathered so far.
  \UD@CheckWhetherNull{#1}{%
    \UD@CheckWhetherNull{#3}{%
       \expandafter\expandafter\expandafter\UD@Exchange
       \expandafter\expandafter\expandafter{\UD@replicate{#4}{A}}%
       {\UD@stopromannumeral#5}%
    }{\UD@stopromannumeral#2}%
  }{%
    \UD@CheckWhetherLeadingTokens{#1}{(}{\UD@CheckLeftParen}{%
      \UD@CheckWhetherNull{#3}{%
        \expandafter\UD@PassFirstToSecond\expandafter{%
           \romannumeral
           \expandafter\expandafter\expandafter\UD@Exchange
           \expandafter\expandafter\expandafter{\UD@replicate{#4}{A}}{\UD@stopromannumeral#5}%
        }%
        {\expandafter\expandtoALoop\expandafter{\UD@firstoftwo{}#1}{#2}{\relax}{}}%
      }{\UD@stopromannumeral#2}%
    }{%
      \UD@CheckWhetherLeadingTokens{#1}{)}{\UD@CheckRightParen}{%
        \UD@CheckWhetherNull{#3}{\UD@stopromannumeral#2}{%
          \expandafter\UD@PassFirstToSecond\expandafter{%
             \romannumeral
             \expandafter\expandafter\expandafter\UD@Exchange
             \expandafter\expandafter\expandafter{\UD@replicate{#4}{A}}{\UD@stopromannumeral#5(})%
          }%
          {\expandafter\expandtoALoop\expandafter{\UD@firstoftwo{}#1}{#2}{}{}}%
        }%
      }{%
        \UD@CheckWhetherLeadingTokens{#1}{0}{\UD@CheckZero}{%
          \expandafter\expandtoALoop\expandafter{\UD@firstoftwo{}#1}{#2}{#3}{#40}{#5}%
        }{%
          \UD@CheckWhetherLeadingTokens{#1}{1}{\UD@CheckOne}{%
            \expandafter\expandtoALoop\expandafter{\UD@firstoftwo{}#1}{#2}{#3}{#41}{#5}%
          }{%
            \UD@CheckWhetherLeadingTokens{#1}{2}{\UD@CheckTwo}{%
              \expandafter\expandtoALoop\expandafter{\UD@firstoftwo{}#1}{#2}{#3}{#42}{#5}%
            }{%
              \UD@CheckWhetherLeadingTokens{#1}{3}{\UD@CheckThree}{%
                \expandafter\expandtoALoop\expandafter{\UD@firstoftwo{}#1}{#2}{#3}{#43}{#5}%
              }{%
                \UD@CheckWhetherLeadingTokens{#1}{4}{\UD@CheckFour}{%
                  \expandafter\expandtoALoop\expandafter{\UD@firstoftwo{}#1}{#2}{#3}{#44}{#5}%
                }{%
                  \UD@CheckWhetherLeadingTokens{#1}{5}{\UD@CheckFive}{%
                    \expandafter\expandtoALoop\expandafter{\UD@firstoftwo{}#1}{#2}{#3}{#45}{#5}%
                  }{%
                    \UD@CheckWhetherLeadingTokens{#1}{6}{\UD@CheckSix}{%
                      \expandafter\expandtoALoop\expandafter{\UD@firstoftwo{}#1}{#2}{#3}{#46}{#5}%
                    }{%
                      \UD@CheckWhetherLeadingTokens{#1}{7}{\UD@CheckSeven}{%
                        \expandafter\expandtoALoop\expandafter{\UD@firstoftwo{}#1}{#2}{#3}{#47}{#5}%
                      }{%
                        \UD@CheckWhetherLeadingTokens{#1}{8}{\UD@CheckEight}{%
                          \expandafter\expandtoALoop\expandafter{\UD@firstoftwo{}#1}{#2}{#3}{#48}{#5}%
                        }{%
                          \UD@CheckWhetherLeadingTokens{#1}{9}{\UD@CheckNine}{%
                            \expandafter\expandtoALoop\expandafter{\UD@firstoftwo{}#1}{#2}{#3}{#49}{#5}%
                          }{%
                            \UD@stopromannumeral#2%
                          }%
                        }%
                      }%
                    }%
                  }%
                }%
              }%
            }%
          }%
        }%
      }%
    }%
  }%
}%
%------------------------------------------------------------------------------
\makeatother

\begin{document}

\message{\detokenize\expandafter\expandafter\expandafter{\expandtoA{1(2)3(4)5(6)7(8)9(10)()(11)}{ErrorText}}}%

\message{\detokenize\expandafter\expandafter\expandafter{\expandtoA{1(2)3(4)5(6)7(89(10)()(11)}{ErrorText}}}%

\end{document}

The above code delivers the messages

A(AA)AAA(AAAA)AAAAA(AAAAAA)AAAAAAA(AAAAAAAA)AAAAAAAAA(AAAAAAAAAA)()(AAAAAAAAAAA)

and

ErrorText

to the terminal. (ErrorText with the second call as here parentheses are not properly matched.)

3
  • 1
    users who input syntax errors deserve no sympathy!! :-) – David Carlisle Jan 23 at 21:39
  • Interesting code, I really appreciate it (and, user are making mistakes, this is why they are still users and not developers! :-) ). – perror Jan 23 at 21:44
  • @DavidCarlisle I fully agree. But which users do? ;-> – Ulrich Diez Jan 24 at 13:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.