6

Is there any way to check whether an integer is a member of a list of integer numbers and return a boolean?

I found this neat solution, here that does a great job. It checks whether a string exists in a list of strings:

\documentclass{article}
\usepackage{xstring}
\newcommand\IfStringInList[2]{\IfSubStr{,#2,}{,#1,}}
\begin{document}
    \IfStringInList{Paul}{George,John,Paul,Ringo}{True}{False}
\end{document}

However, it does return a string not a boolean "true" or "false" and caused me some difficulty which is posted here.

Let's call the macro I would like to have \ISMEMBER. My goal is to use \ISMEMBER and examine whether an integer is in the list and depending on whether it is or not, perform some tasks, as an example:

\fpeval{ \ISMEMBER{1}{1,2,3,4,5} ? \MACRO_FOR_MEMBERS : \MACRO_FOR_NON_MEMBERS }

Is such functionality possible?

10

You can use do a comma-separated list parser to loop through the list of items and check if the given item is present in the list. Since the comma-separated list can be read by using a delimited macro, this can be done expandably, allowing you to plug the macro into \fpeval to get the syntax you want.

Using expl3 (you are loading it with xfp anyway) something like this would solve the problem:

\ExplSyntaxOn
\prg_new_conditional:Npnn \afp_int_ismember:nn #1#2 { p, T, F, TF }
  { \__afp_ismember_loop:nw {#1} #2 , \q_recursion_tail , \q_recursion_stop }
\cs_new:Npn \__afp_ismember_loop:nw #1#2,
  {
    \quark_if_recursion_tail_stop_do:nn {#2}
      { \prg_return_false: }
    \int_compare:nNnTF {#1} = {#2}
      { \use_i_delimit_by_q_recursion_stop:nw { \prg_return_true: } }
      { \__afp_ismember_loop:nw {#1} }
  }
\ExplSyntaxOff

The code above defines a conditional \afp_int_ismember:nn(TF), whose first argument is the item to check, and the second is the comma-separated list. The macro starts by expanding \__afp_ismember_loop:nw: this macro takes the item to be tested (#1) and the first item in the list, delimited by a , (#2). The macro tests the equality of #1 and #2 using \int_compare:nNnTF and issues \prg_return_true: if they are equal or calls \__afp_ismember_loop:nw for the next item. If the end of the list is found (i.e., \q_recursion_tail is grabbed), then the function issues \prg_return_false: because no match was found for #1.

The code above can be tweaked to replace \int_compare:nNnTF by a generic equality comparison function. If you do that you can define wrappers around \__afp_ismember_loop:nw to create ismember functions for different data types. The code below does that and defines two functions: \afp_int_ismember:nn(TF) (using \int_compare:nNnTF) and \afp_str_ismember:nn(TF) (using \str_if_eq:eeTF). Doing that you can even test if I'm a member of the Beatles!

enter image description here

To use the function in \fpeval you just need the predicate form (\afp_int_ismember_p:nn) of the conditional function:

\fpeval{ \afp_int_ismember_p:nn {1} {1,2,3,4,5} ? 123 : 321 }

And the string version would also work here:

\fpeval{ \afp_str_ismember_p:nn {Phelype} {George,John,Paul,Ringo} ? 123 : 321 }

Full code:

\documentclass{article}
\usepackage{expl3}
\usepackage{xparse}
\usepackage{xfp}
\ExplSyntaxOn
% Core code for the membership test
\cs_new:Npn \__afp_ismember_loop:Nnw #1#2#3,
  {
    \quark_if_recursion_tail_stop_do:nn {#3}
      { \prg_return_false: }
    #1 {#2} {#3}
      { \use_i_delimit_by_q_recursion_stop:nw { \prg_return_true: } }
      { \__afp_ismember_loop:Nnw #1 {#2} }
  }
% Wrapper for testing integers
\prg_new_conditional:Npnn \afp_int_ismember:nn #1#2 { p, T, F, TF }
  {
    \__afp_ismember_loop:Nnw \__afp_int_isequal:nnTF {#1} #2 ,
    \q_recursion_tail , \q_recursion_stop
  }
\prg_new_conditional:Npnn \__afp_int_isequal:nn #1#2 { p, T, F, TF }
  {
    \int_compare:nNnTF {#1} = {#2}
      { \prg_return_true: }
      { \prg_return_false: }
  }
% Wrappers for testing strings
  % With expansion
\prg_new_conditional:Npnn \afp_str_ismember:ee #1#2 { p, T, F, TF }
  {
    \__afp_ismember_loop:Nnw \str_if_eq:eeTF {#1} #2 ,
    \q_recursion_tail , \q_recursion_stop
  }
  % Without expansion
\prg_new_conditional:Npnn \afp_str_ismember:nn #1#2 { p, T, F, TF }
  {
    \__afp_ismember_loop:Nnw \str_if_eq:nnTF {#1} #2 ,
    \q_recursion_tail , \q_recursion_stop
  }
% Sample commands
\NewExpandableDocumentCommand { \IntIsmember } { m m }
  {
    \afp_int_ismember:nnTF {#1} {#2}
      { #1~is~member~of~`#2' }
      { #1~is~\emph{not}~member~of~`#2' }
  }
\NewExpandableDocumentCommand { \StrIsmember } { m m }
  {
    \afp_str_ismember:nnTF {#1} {#2}
      { #1~is~member~of~`#2' }
      { #1~is~\emph{not}~member~of~`#2' }
  }
\ExplSyntaxOff
\begin{document}
\IntIsmember{1}{1,2,3}

\IntIsmember{4}{1,2,3}

\StrIsmember{Paul}{George,John,Paul,Ringo}

\textbf{\StrIsmember{Phelype}{George,John,Paul,Ringo}}

\ExplSyntaxOn
\fpeval{ \afp_int_ismember_p:nn {1} {1,2,3,4,5} ? 123 : 321 }\par
\fpeval{ \afp_str_ismember_p:nn {Phelype} {George,John,Paul,Ringo} ? 123 : 321 }
\ExplSyntaxOff
\end{document}
  • That answer makes me wonder why there isn't an expandable \clist_if_in:nn(TF) – siracusa Jul 28 '19 at 2:28
  • 1
    @siracusa There could be... Please, do ask :-) The only problem I see is that we would possibly need a \clist_str_if_in:nn(TF) and a \clist_tl_if_in:nn(TF) (and potentially others) because the only \clisty thing here is that the function looks up something in a clist: the actual data type in the clist var would need to be queried... Perhaps \str_if_in_clist:nn and \tl_if_in_clist:nn and \int_if_in_clist:nn... – Phelype Oleinik Jul 28 '19 at 2:33
  • @PhelypeOleinik Very nice! Any reason you use \cs_new:Npn instead of \NewExpandableDocumentCommand for the document-level commands? Is it that you don't want to load xparse, maybe? Or... are you a rebel? :) – frougon Jul 28 '19 at 6:28
  • @frougon A rebel, probably ;-) Actually I didn't even think of it: the \IntIsmember and \StrIsmember commands are just examples. But you're right, xparse is better here. Thanks :-) – Phelype Oleinik Jul 28 '19 at 16:09
8

Something like this? The macro name is inspired by the Mathematica command MemberQ, and the code comes from here. This solution does not require any packages.

\documentclass{article}
\newif\ifmember
\makeatletter% for \@for see e.g. https://tex.stackexchange.com/a/100684/121799
%from https://tex.stackexchange.com/a/498576/121799
\newcommand{\MemberQ}[2]{\global\memberfalse%
\@for\next:=#1\do{\ifnum\next=#2\global\membertrue\fi}}
\makeatother
\begin{document}

\MemberQ{1,2,3,4}{2}
\ifmember 2 is in list \fi

\MemberQ{1,2,3,4}{5}
\ifmember 5 is in list\else%
5 is not in the list\fi
\end{document}
  • 4
    Oh, an answer of @marmot that does not include anything from TikZ!?! I MUST READ IT. – manooooh Jul 28 '19 at 0:05
  • 1
    @manooooh This is a great solution and also compact. I wish I could select two accepted answers. Can this be used inside a \fpeval? Thanks for your time. – AFP Jul 28 '19 at 0:40
  • 3
    @AFP No, it cannot because it isn't expandable (\@for isn't expandable because it does assignments, and \membertrue and \memberfalse are also assignments, so not expandable). The expandability of l3fp allows you to use it virtually anywhere TeX expects a number (see this trick, for example). The downside of this expandability is that everything inside an expression must be expandable too. – Phelype Oleinik Jul 28 '19 at 2:19

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.