7

I'm trying to create a custom more advanced function with a structure similar to

\str_case:nn {⟨test string⟩}
{
  {⟨string case1⟩} {⟨code case1⟩}
  {⟨string case2⟩} {⟨code case2⟩}
  ...
  {⟨string casen⟩} {⟨code casen⟩}
}

In particular I'm looking for a multi-case string comparison. In particular

\multi_case:nn {input1} {input2}
{
  {foo}
  {bar}
}
{
  {case1}{foo1}{bar1}
  {case2}{foo2}{bar2}
  ...
  {caseN}{fooN}{barN}
}

It should work as follows: input1 is checked against the first set (foo, bar) and input2 is checked against the second set (case1, ..., caseN) and return the result based on input1. Example of inputs and outputs:

foo, case1 -> foo1
bar, case1 -> bar1
bar, case2 -> bar2

I know that it can be achieved by nesting \str_case:nn, but it will have too many cases repetition and more importantly, really hard to extend. \multi_case:nn should support dynamic amount of cases like so

\multi_case:nn {input1} {input2}
{
  {foo}
  {bar}
  {baz}
}
{
  {case1}{foo1}{bar1}{baz1}
  {case2}{foo2}{bar2}{baz3}
  ...
  {caseN}{fooN}{barN}{baz3}
}
1
  • 2
    Such command should be name \multi_case:nnnn. Commented May 19 at 12:31

2 Answers 2

8

Here is a code. The main arguments (#3 and #4) are swept only once.

\documentclass{article}

\begin{document}

Test.

\ExplSyntaxOn

\cs_new_protected:Nn \antshar_multi_case:nnnn
  {
    \tl_set:Nn \l_tmpa_tl { #2 }
    \seq_set_split:Nnn \l_tmpa_seq { } { #3 }
    \int_set:Nn \l_tmpa_int { \seq_count:N \l_tmpa_seq }
    \int_zero:N \l_tmpb_int 
    \seq_map_indexed_inline:Nn \l_tmpa_seq
      { 
        \str_if_eq:nnT { ##2 } { #1 } 
          {
            \int_set:Nn \l_tmpb_int { ##1 }
            \seq_map_break:
          }
      }
    \int_if_zero:nF \l_tmpb_int 
      { \__antshar_multi_case_i:n #4 \q_stop }
  }

\cs_new_protected:Nn \__antshar_multi_case_i:n
  {
    \str_if_eq:nnF { #1 } \q_stop 
      {
        \str_if_eq:nVTF { #1 } \l_tmpa_tl 
          { \exp_args:Ne \__antshar_gobble_and_return:n { \l_tmpb_int - 1 } }
          { \exp_args:NV \__antshar_gobble_and_go_on:n \l_tmpa_int }
      }
  }

\cs_new_protected:Npn \__antshar_gobble_all:w #1 \q_stop 
  { }

\cs_new_protected:Nn \__antshar_gobble_and_go_on:n 
  {
    \int_if_zero:nTF { #1 } 
      { \__antshar_multi_case_i:n }
      { \exp_args:Ne \__antshar_gobble_and_go_on:nn { \int_eval:n { #1 - 1 } } }
  }

\cs_new_protected:Nn \__antshar_gobble_and_go_on:nn 
  {
    \int_if_zero:nTF { #1 } 
      { \__antshar_multi_case_i:n }
      { \exp_args:Ne \__antshar_gobble_and_go_on:nn { \int_eval:n { #1 - 1 } } }
  }


\cs_new_protected:Nn \__antshar_gobble_and_return:n
  {
    \int_if_zero:nTF { #1 } 
      { \__antshar_return:n }
      { \exp_args:Ne \__antshar_gobble_and_return:nn { \int_eval:n { #1 - 1 } } }
  }

\cs_new_protected:Nn \__antshar_gobble_and_return:nn
  {
    \int_if_zero:nTF { #1 } 
      { \__antshar_return:n }
      { \exp_args:Ne \__antshar_gobble_and_return:nn { \int_eval:n { #1 - 1 } } }
  }


\cs_new_protected:Nn \__antshar_return:n 
  { 
    \tl_set:Nn \l_tmpb_tl { #1 } 
    \exp_after:wN \l_tmpb_tl \__antshar_gobble_all:w 
  }


\antshar_multi_case:nnnn {bar} {case5}
{
  {foo}
  {bar}
  {baz}
}
{
  {case1}{foo1}{bar1}{baz1}
  {case2}{foo2}{bar2}{baz2}
  {case3}{foo3}{bar3}{baz3}
  {case4}{foo4}{bar4}{baz4}
  {case5}{foo5}{bar5}{baz5}
  {caseN}{fooN}{barN}{bazN}
}

\ExplSyntaxOff


\end{document}

The command \__antshar_gobble_and_go_on:n will take in as argument a number of items to gobble (an item is an element like {foo5} or {barN}). If its argument is non-zero, it will use recursively \__antshar_gobble_and_go_on:nn. The first argument of this command is the number of items to gobble, but it has also a second argument. That second argument is an item which is actually gobbled (it's discarded from the TeX flow and not used by \__antshar_gobble_and_go_on:nn (there is no #2 in the body of that control sequence)). Eventually, the command \__antshar_gobble_and_go_on:nn will be used as many times as needed to gobble the items. There is go_on in its name because, after gobbling several items, it will leave in the TeX flow the command \__antshar_multi_case_i:n which will try to find an item matching #2 in the following items which are in the TeX flow (until the quark q_stop). The use of \__antshar_gobble_and_go_on:nn will, if I can say, branch on \__antshar_multi_case_i:n at the end of the recursion.

Maybe it would have been possible to use \prg_replicate:nn and \use:n to gobble the items...

6
  • Can you explain why n and nn variants are needed for \__antshar_gobble_and_go_on. They seem the same, yet nn doesn't even have the second argument.
    – antshar
    Commented May 19 at 15:01
  • I have added explanations in my answer... Commented May 19 at 15:17
  • Ok, I see now. Thank you
    – antshar
    Commented May 19 at 16:39
  • nice answer but shouldn't \__antshar_multi_case:nnnn be \antshar_multi_case:nnnn the others are internal auxiliary functions so can have an \__ local prefix but the main command deserves a public interface, I think? Commented May 19 at 18:00
  • @DavidCarlisle: You're right. I will modify my answer. Commented May 19 at 18:01
6

If you like to have it expandable, you can do s.th. like this — pseudocode:

\my_module_multi_case:nnnn 
  {<case column to find in case table>}
  {<case row to find in case table>}
  {
    % <list of case columns>
    {foo} % name of case column 1
    {bar} % name of case column 2
    %...
  }
  {
    % <case table>
    % Name of row | column 1 | column 2 | ...
    {case1}{foo1}{bar1}...
    {case2}{foo2}{bar2}...
    ...
    {caseN}{fooN}{barN}...
  }

yields

\exp:w 
\my_module_multi_case_loop:nooffnnn
     {<\str_case:nnF-rows construcred so far>}; initialized empty
     {<name of case table row whose column-entries currently are gathered for constructing \str_case:nnF-call>; initialized to head of #4 of \my_module_multi_case:nnnn
     {<\str_case:nnF-row in construction>}; initialized empty
     {<list of case columns still to process until \str_case:nnF-row is constructed>}; initialized #3 of \my_module_multi_case:nnnn
     {<case table>}; initialized to tail of #4 of \my_module_multi_case:nnnn
     {<list of case columns>}; initialized #3 of \my_module_multi_case:nnnn
     {<case row to find in case table>; initialized #2 of \my_module_multi_case:nnnn
     {<case column to find in case table>}; initialized #1 of \my_module_multi_case:nnnn

, with \my_module_multi_case_loop:nooffnnn recursively calling itself, creating arguments for next iteration from arguments of the current iteration as follows—at the left of ":=" you find the descriptive name of the argument, at the right of ":=" you find how to construct that argument for the next iteration from the arguments of the current iteration:

If <case table> is blank: 
  - Terminate the loop and do:
      \str_case:nnF{<case row to find in case table>}%
                   {
                      <\str_case:nnF-rows constructed so far>
                     {<name of case table row whose column-entries currently are gathered for constructing \str_case:nnF-call>}%
                     {\str_case:nnF{<case column to find in case table>}{<\str_case:nnF-row in construction>}{\exp_end:}}
                   }
                   {\exp_end:}
Else:
  If <list of case columns still to process until \str_case:nnF-row is constructed> is blank:
    - <\str_case:nnF-rows construcred so far>
      :=
      <\str_case:nnF-rows construcred so far>
      {<name of case table row whose column-entries currently are gathered for constructing \str_case:nnF-call>}
      {\str_case:nnF{<case column to find in case table>}{<\str_case:nnF-row in construction>}{\exp_end:}}
    - <name of case table row whose column-entries currently are gathered for constructing \str_case:nnF-call> 
      :=
      \tl_head:w <case table> { }\q_stop
    - <\str_case:nnF-row in construction> := \use:n{}
    - <list of case columns still to process until \str_case:nnF-row is constructed> 
      :=
      \exp_stop_f: <list of case columns>
    - <case table> := \tl_if_blank:nTF {<case table>}{\exp_stop_f:}{ \tl_tail:n {<case table>} }
    - <list of case columns> := <list of case columns>
    - <case row to find in case table> := <case row to find in case table>
    - <case column to find in case table> := <case column to find in case table>
  Else: 
    - <\str_case:nnF-rows construcred so far> := <\str_case:nnF-rows construcred so far>
    - <name of case table row whose column-entries currently are gathered for constructing \str_case:nnF-call>
      := 
      \use:n{}<name of case table row whose column-entries currently are gathered for constructing \str_case:nnF-call>
    - <\str_case:nnF-row in construction> 
      :=  \exp:w \exp_args:Nooo
                   \use:n
                   {\use:n{} \exp_end: <\str_case:nnF-row in construction>} 
                   {\tl_head:w <list of case columns still to process until \str_case:nnF-row is constructed> { }\q_stop}
                   {\exp_after:wN \exp_end: \tl_head:w <case table> { }\q_stop}
    - <list of case columns still to process until \str_case:nnF-row is constructed>
      :=
      \tl_if_blank:nTF {<list of case columns still to process until \str_case:nnF-row is constructed>}
                       {\exp_stop_f:}
                       { \tl_tail:n {<list of case columns still to process until \str_case:nnF-row is constructed>} }
    - <case table> := \tl_if_blank:nTF {<case table>}{\exp_stop_f:}{\tl_tail:n {<case table>}}
    - <list of case columns> := <list of case columns>
    - <case row to find in case table> := <case row to find in case table>
    - <case column to find in case table> := <case column to find in case table>

The following might be a starting-point for implementing this:

\ExplSyntaxOn
\cs_new:Nn \my_module_multi_case:nnnn {
  \exp:w 
  \__my_module_multi_case_loop:nooffnnn
    {}
    {\tl_head:w #4 { }\q_stop}
    {\use:n{}}
    {\exp_stop_f: #3}
    {\tl_if_blank:nTF {#4}{\exp_stop_f:}{\tl_tail:n {#4}}}
    {#3}{#2}{#1}
}
\cs_new:Nn \__my_module_multi_case_loop:nnnnnnnn {
  \tl_if_blank:nTF {#5} {
    \str_case:nnF {#7}{#1{#2}{\str_case:nnF{#8}{#3}{\exp_end:}}}{\exp_end:}
  }{
    \tl_if_blank:nTF {#4} {
       \__my_module_multi_case_loop:nooffnnn
         {#1{#2}{\str_case:nnF{#8}{#3}{\exp_end:}}}
         {\tl_head:w #5{ }\q_stop}
         {\use:n{}}
         {\exp_stop_f: #6}
    }{
       \__my_module_multi_case_loop:nooffnnn
         {#1}
         {\use:n{}#2}
         {
           \exp:w \exp_args:Nooo
                    \use:n
                    {\use:n{} \exp_end: #3} 
                    {\tl_head:w #4{ }\q_stop}
                    {\exp_after:wN \exp_end: \tl_head:w #5{ }\q_stop}
         }
         {\tl_if_blank:nTF {#4}{\exp_stop_f:}{\tl_tail:n {#4}}}
    }
    {\tl_if_blank:nTF {#5}{\exp_stop_f:}{\tl_tail:n {#5}}}
    {#6}{#7}{#8}
  }
}
\cs_generate_variant:Nn \__my_module_multi_case_loop:nnnnnnnn { nooffnnn }
\cs_new_eq:NN \Multicase \my_module_multi_case:nnnn
\ExplSyntaxOff

\documentclass{article}

\begin{document}

\Multicase{foo}{case1}%
          {{foo}}%
          {%
            {case1}{foo1}%
          }

\Multicase{foo}{case1}%
          {{foo}}%
          {%
            {case1}{foo1}%
            {case2}{foo2}%
            {caseN}{fooN}%
          }

\Multicase{foo}{case1}%
          {{foo}{bar}}%
          {%
            {case1}{foo1}{bar1}%
            {case2}{foo2}{bar2}%
            {caseN}{fooN}{barN}%
          }

\Multicase{foo}{case2}%
          {{foo}{bar}}%
          {%
            {case1}{foo1}{bar1}%
            {case2}{foo2}{bar2}%
            {caseN}{fooN}{barN}%
          }

\Multicase{foo}{caseN}%
          {{foo}{bar}}%
          {%
            {case1}{foo1}{bar1}%
            {case2}{foo2}{bar2}%
            {caseN}{fooN}{barN}%
          }

\Multicase{bar}{case1}%
          {{foo}{bar}}%
          {%
            {case1}{foo1}{bar1}%
            {case2}{foo2}{bar2}%
            {caseN}{fooN}{barN}%
          }

\Multicase{bar}{case2}%
          {{foo}{bar}}%
          {%
            {case1}{foo1}{bar1}%
            {case2}{foo2}{bar2}%
            {caseN}{fooN}{barN}%
          }

\Multicase{bar}{caseN}%
          {{foo}{bar}}%
          {%
            {case1}{foo1}{bar1}%
            {case2}{foo2}{bar2}%
            {caseN}{fooN}{barN}%
          }

\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\test
\expandafter\expandafter\expandafter{%
\Multicase{bar}{case2}%
          {{foo}{bar}}%
          {%
            {case1}{foo1}{bar1}%
            {case2}{foo2}{bar2}%
            {caseN}{fooN}{barN}%
          }%
}

\texttt{\frenchspacing[\string\test: \meaning\test]}
\end{document}

enter image description here


On the one hand with the code of the example above expl3's
\tl_head:w ⟨token list⟩{ }\q_stop
is used. This fails in the edge case of the ⟨token list⟩ itself for some obscure reason containing the token \q_stop not surrounded by curly braces.

On the other hand with expl3's \tl_head:n there is no proper expansion control other than with x- and e-expansion while x-expansion itself is not expandable and in interface3.pdf one is adviced to avoid e-expansion with TeX-engines where the primitive \expanded needs to be emulated by some fallback-code.

If you don't like these things, you can define your own routine for extracting the head of a token-list/for obtaining the 1st undelimited argument of a list of undelimited arguments:

\my_module_tl_head:nn {⟨tokens from which to extract the 1st undelimited argument⟩}%
                      {⟨tokens to prepend to the extracted 1st undelimited argument⟩}

⟨tokens to prepend to the extracted 1st undelimited argument⟩ can consist of tokens for expansion-control.

E.g., in case expansion of \my_module_tl_head:nn was triggered via f-expansion, this argument could hold the token \exp_stop_f: for stopping f-expansion.

E.g., in case expansion of \my_module_tl_head:nn was triggered via \exp:w-expansion, this argument could hold the token \exp_end: for stopping \exp:w-expansion.

In case expansion-control is not needed, this argument can be left empty.

Implementing \my_module_tl_head:nn could look like this:

\ExplSyntaxOn
\cs_new:Nn \__my_module_tl_head_loop:nn 
  {
    \tl_if_empty:oTF {\use_none:n #1}
      { \use_ii_i:nn #1{#2} }
      { \exp_args:No \__my_module_tl_head_loop:nn 
                     {\__my_module_tl_head_RemoveTillFrozenrelax:nw #1}
                     {#2}
      }
  }
\group_begin:
\cs_set:Npn \__my_module_tl_head_loop:nn #1 {
  \group_end:
  \cs_new:Npn \__my_module_tl_head_RemoveTillFrozenrelax:nw ##1##2#1{{##1}}
  \cs_new:Npn \my_module_tl_head:nn ##1 { \__my_module_tl_head_loop:nn {##1{}#1} }
}
\exp_args:NNo\exp_args:No\__my_module_tl_head_loop:nn
{\exp_after:wN\exp_after:wN\tex_ifnum:D0=0\tex_fi:D}
\ExplSyntaxOff

The gist of the routine is recursively calling a macro which takes an undelimited argument and an argument delimited by frozen-\relax and returns only the undelimited argument until only a single undelimited argument is left. I decided to use frozen-\relax because this neither can be (re)defined to be \outer nor is affected by \uppercase/\lowercase and because its occurrence in user-provided input is unlikely so that in most cases a single iteration will provide the result. However, frozen-\relax is not forbidden to occur in the user-input; the only effect is that obtaining the result may take more iterations which can be compensated by means of the second argument which for the sake of making expansion-control possible holds the ⟨tokens to prepend to the extracted 1st undelimited argument⟩.

Combining this routine and the previous example might look like this:

\ExplSyntaxOn
%--------------------------------------------------------------------------------
% Get the head of a token list/get the 1st undelimited argument of a list of
% undelimited arguments::
% \my_module_tl_head:nn {<tokens from which to extract the 1st undelimited argument>}%
%                       {<tokens to prepend to the extracted 1st undelimited argument>}%
%--------------------------------------------------------------------------------
\cs_new:Nn \__my_module_tl_head_loop:nn 
  {
    \tl_if_empty:oTF {\use_none:n #1}
      { \use_ii_i:nn #1{#2} }
      { \exp_args:No \__my_module_tl_head_loop:nn 
                     {\__my_module_tl_head_RemoveTillFrozenrelax:nw #1}
                     {#2}
      }
  }
\group_begin:
\cs_set:Npn \__my_module_tl_head_loop:nn #1 {
  \group_end:
  \cs_new:Npn \__my_module_tl_head_RemoveTillFrozenrelax:nw ##1##2#1{{##1}}
  \cs_new:Npn \my_module_tl_head:nn ##1 { \__my_module_tl_head_loop:nn {##1{}#1} }
}
\exp_args:NNo\exp_args:No\__my_module_tl_head_loop:nn
{\exp_after:wN\exp_after:wN\tex_ifnum:D0=0\tex_fi:D}
%--------------------------------------------------------------------------------
%
\cs_new:Nn \my_module_multi_case:nnnn {
  \exp:w 
  \__my_module_multi_case_loop:nooffnnn
    {}
    {\exp:w \my_module_tl_head:nn {#4}{\exp_end:}}
    {\use:n{}}
    {\exp_stop_f: #3}
    {\tl_if_blank:nTF {#4}{\exp_stop_f:}{\tl_tail:n {#4}}}
    {#3}{#2}{#1}
}
\cs_new:Nn \__my_module_multi_case_loop:nnnnnnnn {
  \tl_if_blank:nTF {#5} {
    \str_case:nnF {#7}{#1{#2}{\str_case:nnF{#8}{#3}{\exp_end:}}}{\exp_end:}
  }{
    \tl_if_blank:nTF {#4} {
       \__my_module_multi_case_loop:nooffnnn
         {#1{#2}{\str_case:nnF{#8}{#3}{\exp_end:}}}
         {\exp:w \my_module_tl_head:nn {#5}{\exp_end:}}
         {\use:n{}}
         {\exp_stop_f: #6}
    }{
       \__my_module_multi_case_loop:nooffnnn
         {#1}
         {\use:n{}#2}
         {
           \exp:w \exp_args:Nooo
                    \use:n
                    {\use:n{} \exp_end: #3} 
                    {\exp:w \my_module_tl_head:nn {#4}{\exp_end:}}
                    {\exp:w \my_module_tl_head:nn {#5}{\exp_end:\exp_end:}}
         }
         {\tl_if_blank:nTF {#4}{\exp_stop_f:}{\tl_tail:n {#4}}}
    }
    {\tl_if_blank:nTF {#5}{\exp_stop_f:}{\tl_tail:n {#5}}}
    {#6}{#7}{#8}
  }
}
\cs_generate_variant:Nn \__my_module_multi_case_loop:nnnnnnnn { nooffnnn }
\cs_new_eq:NN \Multicase \my_module_multi_case:nnnn
\ExplSyntaxOff

\documentclass{article}

\begin{document}

\Multicase{foo}{case1}%
          {{foo}}%
          {%
            {case1}{foo1}%
          }

\Multicase{foo}{case1}%
          {{foo}}%
          {%
            {case1}{foo1}%
            {case2}{foo2}%
            {caseN}{fooN}%
          }

\Multicase{foo}{case1}%
          {{foo}{bar}}%
          {%
            {case1}{foo1}{bar1}%
            {case2}{foo2}{bar2}%
            {caseN}{fooN}{barN}%
          }

\Multicase{foo}{case2}%
          {{foo}{bar}}%
          {%
            {case1}{foo1}{bar1}%
            {case2}{foo2}{bar2}%
            {caseN}{fooN}{barN}%
          }

\Multicase{foo}{caseN}%
          {{foo}{bar}}%
          {%
            {case1}{foo1}{bar1}%
            {case2}{foo2}{bar2}%
            {caseN}{fooN}{barN}%
          }

\Multicase{bar}{case1}%
          {{foo}{bar}}%
          {%
            {case1}{foo1}{bar1}%
            {case2}{foo2}{bar2}%
            {caseN}{fooN}{barN}%
          }

\Multicase{bar}{case2}%
          {{foo}{bar}}%
          {%
            {case1}{foo1}{bar1}%
            {case2}{foo2}{bar2}%
            {caseN}{fooN}{barN}%
          }

\Multicase{bar}{caseN}%
          {{foo}{bar}}%
          {%
            {case1}{foo1}{bar1}%
            {case2}{foo2}{bar2}%
            {caseN}{fooN}{barN}%
          }

\expandafter\expandafter\expandafter\def
\expandafter\expandafter\expandafter\test
\expandafter\expandafter\expandafter{%
\Multicase{bar}{case2}%
          {{foo}{bar}}%
          {%
            {case1}{foo1}{bar1}%
            {case2}{foo2}{bar2}%
            {caseN}{fooN}{barN}%
          }%
}

\texttt{\frenchspacing[\string\test: \meaning\test]}
\end{document}

enter image description here

1
  • Thank you for such a detailed explanation, yet a neat solution.
    – antshar
    Commented May 20 at 12:48

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .