7

I'd like to implement an easy foreach-loop, which takes two arguments — a string and an expected formatting for every character (optionally except last), as follows:

\spell{example}{\x-}, which produces an output: e-x-a-m-p-l-e.

I find myself as a newcomer to TeX, and not advanced or near familiar to TeX specifics, hence searching for a MWE did not result in a success. The following example made me struggle with an error (edited):

Update: I've updated the code, which is working now. However, various answers below also provide better ways to do so.

\documentclass{article}

\makeatletter
\newcommand{\spell}[2]{
  \@tfor\x:=#1\do{\x#2}
}
\makeatletter

\begin{document}
\spell{example}{-}
\end{document}
1
  • Can spaces be part of the argument? What should happen to spaces if they are? Ignored, forwarded as is, or being looped as well?
    – Skillmon
    Jan 14 at 14:59

3 Answers 3

7

I see only Expl3 examples here, but this task is very suitable for using TeX primitives only. We define the \spell macro which gets a single word and prints the first letter by the format given by \spellFirst and the next letters by the format given by \spellNext. We need only five very clear lines. On the other hand, the Expl3 examples are not clear, IMHO.

\def\spell #1{\spellA#1{}}
\def\spellA #1{\spellFirst{#1}\spellB}
\def\spellB #1{\ifx^#1^\else\spellNext{#1}\expandafter\spellB\fi}

% format is given by:
\def\spellFirst #1{#1}  % only copy to the output
\def\spellNext  #1{-#1} % add `-` before the character

\spell{example}.

\bye
6
  • @chzzh This code doesn’t do what you ask in your question.
    – egreg
    Jan 15 at 8:49
  • @chzzh also, if your input contains any non-ASCII-characters the only stable solution working across all major engines that doesn't require a lot of work on your end or specifically crafted input is using a \text_map_...-function from expl3.
    – Skillmon
    Jan 15 at 14:41
  • @chzzh Sorry, no: this code doesn't accomplish what you ask. It just interleaves the characters with hyphens and you explicitly asked for expected formatting to be specified with an additional argument.
    – egreg
    Jan 15 at 15:21
  • Formatting is configurable by various values of \spellFirst and \spallNext macros. The hyphens is only example.
    – wipet
    Jan 15 at 16:04
  • 2
    at Unicode characters: We are writing year 2023, there is no sense to support non-Unicode engines by answers and do answers more complicated due this fact. Of course, LaTeX team decided to support old engines and they create an obscure Expl3 language. If we are using Unicode engine then we need not the "functions" (in fact, macros) which solve Unicode characters on non-Unicode engines. Moreover, csplain supports Unicode input on non-Unicode engine pdfTeX since 2000 without problems, only LaTeX has problems with it because it chose a bad solution via active characters.
    – wipet
    Jan 15 at 16:08
5

The following implements both. The variant including the last character is straight forward using the expl3 function \text_map_inline:nn, the variant ignoring the last character needs to loop over the text twice (first to count the length, then to apply the user function).

\documentclass{article}

\ExplSyntaxOn
\int_new:N \l__chzzh_grapheme_count_int
\int_new:N \l__chzzh_current_int
\NewDocumentCommand \chzzhLoop { s m m }
  {
    \IfBooleanTF {#1}
      { \__chzzh_loop_without_last:en { \text_expand:n {#2} } {#3} }
      { \text_map_inline:nn {#2} {#3} }
  }
\cs_new:Npn \__chzzh_loop_without_last:nn #1#2
  {
    \int_zero:N \l__chzzh_grapheme_count_int
    \int_zero:N \l__chzzh_current_int
    \text_map_inline:nn {#1} { \int_incr:N \l__chzzh_grapheme_count_int }
    \text_map_inline:nn {#1}
      {
        \int_incr:N \l__chzzh_current_int
        \int_compare:nNnTF \l__chzzh_current_int < \l__chzzh_grapheme_count_int
          {#2}
          {##1}
      }
  }
\cs_generate_variant:Nn \__chzzh_loop_without_last:nn { e }
\ExplSyntaxOff

\begin{document}
\chzzhLoop{example}{#1-}

\chzzhLoop*{example}{#1-}
\end{document}

enter image description here

1
  • 1
    @chzzh sure: \newcommand\spell[1]{\chzzhLoop*{#1}{##1-}} (note that you need to double the # in the second argument).
    – Skillmon
    Jan 14 at 15:47
5

tl;dr

If you want to just insert a hyphen between graphemes, this works.

\documentclass{article}
\usepackage{iftex}

\iftutex
  \usepackage{fontsetup}
\fi

\ExplSyntaxOn

\NewDocumentCommand{\unicodespell}{m}
 {
  \seq_clear:N \l_tmpa_seq
  \text_map_inline:nn { #1 } { \seq_put_right:Nn \l_tmpa_seq { ##1 } }
  \seq_pop_left:NN \l_tmpa_seq \l_tmpa_tl
  \tl_use:N \l_tmpa_tl
  \seq_map_inline:Nn \l_tmpa_seq { -##1 }
 }

\ExplSyntaxOff

\begin{document}

\iftutex
  \unicodespell{éxãm̃ple}% can't work correctly in pdflatex
\else
  \unicodespell{éxãmple}% but this works in pdflatex
\fi

\end{document}

Output with pdflatex

enter image description here

Note. The word éxãmple is used here (pdflatex is not able to cope with combining characters).

Output with xelatex or lualatex

enter image description here

Note. Here you see that even , that is, the combination U+006D LATIN SMALL LETTER M with U+0303 COMBINING TILDE, is treated correctly.

Comparison with wipet's solution, that claims to work with Unicode. Compile with optex.

\fontfam[Termes]

\def\spell #1{\spellA#1{}}
\def\spellA #1{\spellFirst{#1}\spellB}
\def\spellB #1{\ifx^#1^\else\spellNext{#1}\expandafter\spellB\fi}

% format is given by:
\def\spellFirst #1{#1}  % only copy to the output
\def\spellNext  #1{-#1} % add `-` before the character

\spell{éxãm̃ple}% this doesn't work

\bye

enter image description here

Original answer with more details

Inserting some tokens between each item is easy: detach the first item in the given string and then process the rest by adding the second argument in front of each item.

At the end, optionally add the second argument. If you want to swap the behavior of the *-variant, just change \IfBooleanT into \IfBooleanF.

The examples below show that this works also with one-letter words or even with empty first argument.

\documentclass{article}

\ExplSyntaxOn

\NewDocumentCommand{\spell}{smm}
 {
  \tl_head:n { #2 }
  \tl_map_inline:en { \tl_tail:n { #2 } } { #3 ##1 }
  \IfBooleanF{#1}{#3}
 }
\cs_generate_variant:Nn \tl_map_inline:nn { e }

\ExplSyntaxOff

\begin{document}

\spell*{example}{-}

\spell{example}{-}

\spell*{example}{=}

\spell*{e}{-} \spell{e}{-}

X\spell{}{-}X

\end{document}

enter image description here

If however you want to have more general formatting of the items, say \fboxing each of them, you need to do something more.

\documentclass{article}

\ExplSyntaxOn

\NewDocumentCommand{\spell}{smm}
 {
  \chzzh_spell:nnn { #1 } { #2 } { #3 }
 }

\cs_generate_variant:Nn \tl_map_function:nN { e }

\cs_new_protected:Nn \chzzh_spell:nnn
 {
  \cs_set_protected:Nn \__chzzh_spell_item:n { #3 }
  \tl_map_function:eN { \tl_range:nnn { #2 } { 1 } { -2 } } \__chzzh_spell_item:n
  \bool_if:nTF { #1 }
   {
    \tl_item:nn { #2 } { -1 }
   }
   {
    \__chzzh_spell_item:e { \tl_item:nn { #2 } { -1 } }
   }
 }
\cs_new_protected:Nn \__chzzh_spell_item:n {} % initialize
\cs_generate_variant:Nn \__chzzh_spell_item:n { e }

\ExplSyntaxOff

\begin{document}

\spell{example}{#1-}

\spell*{example}{#1-}

\spell{example}{\fbox{#1}}

\end{document}

Here the detached item is the last one.

enter image description here

However, if your text is expected to have non ASCII characters, you need something more; with the help of \text_map_inline:nn we can populate a sequence and act on it.

\documentclass{article}

\ExplSyntaxOn

\NewDocumentCommand{\spell}{smm}
 {
  \chzzh_spell:nnn { #1 } { #2 } { #3 }
 }

\cs_generate_variant:Nn \tl_map_function:nN { e }

\seq_new:N \l__chzzh_spell_seq
\tl_new:N \l__chzzh_spell_last_tl

\cs_new_protected:Nn \chzzh_spell:nnn
 {
  % populate the sequence with the items in #2
  \seq_clear:N \l__chzzh_spell_seq
  \text_map_inline:nn { #2 }
   {
    \seq_put_right:Nn \l__chzzh_spell_seq { ##1 }
   }
  % detach the last item
  \seq_pop_right:NN \l__chzzh_spell_seq \l__chzzh_spell_last_tl
  % define the aux function like specified by the last argument
  \cs_set_protected:Nn \__chzzh_spell_item:n { #3 }
  % map the sequence
  \seq_map_function:NN \l__chzzh_spell_seq  \__chzzh_spell_item:n
  % deal with the last item
  \bool_if:nTF { #1 }
   {
    \tl_use:N \l__chzzh_spell_last_tl
   }
   {
    \__chzzh_spell_item:V \l__chzzh_spell_last_tl
   }
 }
\cs_new_protected:Nn \__chzzh_spell_item:n {} % initialize
\cs_generate_variant:Nn \__chzzh_spell_item:n { V }

\ExplSyntaxOff

\begin{document}

\spell{example}{#1-}

\spell*{example}{#1-}

\spell{éxãmple}{\fbox{#1}}

\end{document}

enter image description here

3
  • Shouldn't the \text_map... functions be preferred over \tl_map_... for typeset text?
    – Skillmon
    Jan 14 at 15:48
  • @Skillmon It mostly depends on what kind of text is involved.
    – egreg
    Jan 14 at 15:49
  • @Skillmon Anyway, I added a version also for this case that's simpler than yours and requires just one \text_map_inline:nn and no counting.
    – egreg
    Jan 14 at 16:59

You must log in to answer this question.