4

I am trying to do a non-case-sensitive check on two strings that may or may not have spaces in them. I have googled this and there is quite a lot of literature on either of these problems, but I can't seem to find much on doing both at once and I would ideally like a tex or latex solution that doesn't require external packages to be loaded (but this isn't required). I have something that seems like it should work using packages, and yet I can't determine why it doesn't. Here is the MWE.

\documentclass{article}
\usepackage{ifthenx}

%%% Support command %%%
\makeatletter
\newcommand\compareStrings[4]{%
  \edef\tempA{\expandafter\zap@space \lowercase{#1} \@empty}
  \edef\tempB{\expandafter\zap@space \lowercase{#2} \@empty}
  \ifthenelse{\equal{\tempA}{\tempB}}{True}{False}

  \tempA

  \tempB
}
\makeatother
%%%%% End support commands %%%%

\begin{document}
\compareStrings{test}{tesT}{1}{2}
\end{document}

When you compile it, the \tempA and \tempB both display the exact same string, but the comparison itself returns False (the 3rd and 4th parameters in \compareStrings are suppose to be the true and false conditions, but I replaced them with words for testing purposes).

Any suggestions or explanations as to why it appears that the two strings are the same but aren't yielding "True" would be much appreciated. My first thought was that expansion would be to blame, but I would have expected edef to solve this problem, and clearly it is not.

  • \lowercase is not expandable. If you do \edef\tempa{\lowercase{A}}\show\tempa it will show \tempa=macro: ->\lowercase {A}.. – Phelype Oleinik Sep 7 '18 at 15:54
  • Ah, that explains a lot... is there some expandable version? Or another way to do a case-insensitive comparison? – Jason Sep 7 '18 at 16:12
  • if the strings are always presented as strings of characters with no macros you can use \lowercase{\edef\tempA{\zap@space #1 \@empty}} or in fact simply \lowercase{\ifthenelse{\equal{#1}{#2}}{True}{False} with no edef – David Carlisle Sep 7 '18 at 16:16
  • 1
    note that \lowercase is a very simplistic lowercase for ascii strings, it does not do lowercasing of natural language strings for accented letters etc, so this test is Ok for things like option names where you have control, but not for natural language user input. – David Carlisle Sep 7 '18 at 16:20
  • I will always be in the situation where #1 will be a macro containing only a string (and the one that potentially has spaces and upper or lowercase inconsistencies) whereas #2 is a completely controlled parameter if I wanted to settle for a non-generalized macro to do what I want. In this case then I guess it would be a good idea to edef to zap the spaces and then use lowercase within the ifthenelse command? – Jason Sep 7 '18 at 16:22
7

You need to apply lowercase to the strings before you compare, so it needs to be outside any \edef here I have used \pdfstrcmp to avoid having to define additional temp macros, this primitive is also available in other engines than pdftex, as \strcmp.

This finds all but the last as equal

enter image description here

\documentclass{article}


%%% Support command %%%
\makeatletter
\newcommand\compareStrings[2]{%
\edef\tempA{\lowercase{\noexpand\ifnum0=\noexpand\pdfstrcmp
    {\noexpand\zap@space#1 \noexpand\@empty}%
    {\noexpand\zap@space#2 \noexpand\@empty}%
}\relax}%
\tempA
    \expandafter\@firstoftwo
  \else
    \expandafter\@secondoftwo
  \fi}

\makeatother
%%%%% End support commands %%%%


\begin{document}

\def\zz{T e sT}
0: \compareStrings{tesT}{tesT}{1}{2}

1: \compareStrings{test}{tesT}{1}{2}

2: \compareStrings{\zz}{tesT}{1}{2}

3: \compareStrings{testj}{tesT}{1}{2}

\end{document}
  • Awesome, this is perfect. Out of curiosity, is there a reason you opted to do a 2 argument command and then use the \@firstoftwo \@secondoftwo to pull in the 3rd and 4th argument, instead of just using 4 arguments? – Jason Sep 7 '18 at 21:50
  • 1
    @Jason force of habit, if you look in latex.ltx you will see the \expandafter\@firstoftwo\else idiom everywhere, it has advantages in that it avoids re-parsing the true and false branches and it gets the trailing \fi out of the way, here the true branch is just 1 so it makes no difference but if the true branch was a macro taking an argument and the false branch was a different macro (say \@gobble) then the argument can follow the construction and be eaten by the true or false code without the trailing \fi getting in the way. – David Carlisle Sep 7 '18 at 21:59
  • In case people are not aware that \uppercase and \lowercase are not expandable, it is probably worth mentioning that nesting \uppercase/\lowercase will probably not work out as expected, i.e., \compareStrings{test}{\lowercase{test}}{1}{2} will not work out as expected while \compareStrings{\lowercase{TEST}}{\lowercase{test}}{1}{2} will work out as expected... ;-) – Ulrich Diez Sep 8 '18 at 9:38
8

An expl3 solution. Here, I use the fact that \tl_map_function:nN will 'eat' spaces, and do proper case folding rather than lower casing (see the Unicode docs for the reasons that this is important):

\documentclass{article}
\usepackage{expl3,xparse}
\ExplSyntaxOn
\NewExpandableDocumentCommand \compareStrings { m m +m +m }
  {
    \str_if_eq:eeTF % \str_if_eq_x:nnTF in older code
      { \tl_map_function:fN { \str_fold_case:n {#1} } \use:n }
      { \tl_map_function:fN { \str_fold_case:n {#2} } \use:n }
      {#3} {#4}
  }
\cs_generate_variant:Nn \tl_map_function:nN { f }
\ExplSyntaxOff


\begin{document}
\compareStrings{test}{tesT}{1}{2}
\end{document}
5

Be aware that \lowercase neither is expandable nor does trigger expansion of its argument.

Thus you need to ensure that those tokens that expand to characters whose cases are to be changed are already fully expanded when \lowercase or \uppercase comes to action. (Same for \uppercase.)

Be aware that \zap@space does not trigger expansion of its argument.

Thus you need to ensure that those tokens that expand to characters where spaces shall be removed are already fully expanded when \zap@space comes to action.

\documentclass{article}
\usepackage{ifthenx}

\makeatletter
\DeclareRobustCommand\compareStrings[2]{%
  % Make sure each instance of \zap@space gets its argument 
  % expanded - do this by edef-fing while via \noexpand preventing 
  % expansion both of \zap@space and of \@empty. The latter is a
  % sentinel-token for \zap@space and therefore must be let in place
  % untouched also:
  \protected@edef\tempA{%
    {\noexpand\zap@space#1 \noexpand\@empty}%
    {\noexpand\zap@space#2 \noexpand\@empty}%
  }%
  % Now that the arguments for the \zap@space-instances are expanded,
  % via another \protected@edef have carried out the \zap@space-instances:
  \protected@edef\tempA{%
    % \lowercase is not expandable, thus does not get expanded
    % /does not get carried out at "e-def-fing-time". 
    % Expansion of anything else but \tempA is prevented via 
    % \noexpand. Within \tempA anything but the \zap@space-
    % instances is already expanded due to the previous \protected@edef.
    % Thus the only effect of this \protected@edef is carrying
    % out \zap@space-instances on arguments that were expanded by 
    % the previous \protected@edef.
    \lowercase{\noexpand\ifthenelse{\noexpand\equal\tempA}}%
  }%
  % Now in \tempA the arguments are expanded and space tokens
  % are removed. Thus \tempA expands to a call to \lowercase
  % where the arguments are expanded and thus you don't have
  % control-sequence-tokens any more (whereon \lowercase would have
  % no effect) but character-tokens (whereon \lowwercase does have
  % an effect).
  \tempA{\@firstoftwo}{\@secondoftwo}%
}
\makeatother

\parindent=0ex
\parskip=\baselineskip

\begin{document}

\verb|\compareStrings{test}{tesT}{equal}{different}| yields:
\compareStrings{test}{tesT}{equal}{different}

\verb|\compareStrings{test}{t e sT}{equal}{different}| yields:
\compareStrings{test}{t e sT}{equal}{different}

\verb|\compareStrings{test}{t E   sT}{equal}{different}| yields:
\compareStrings{test}{t e sT}{equal}{different}

\verb|\compareStrings{test}{tset}{equal}{different}| yields:
\compareStrings{test}{tset}{equal}{different}

\def\test{test}%
\def\tset{TsEt}%
\verb|\def\test{test}|\\    
\verb|\def\tset{TsEt}|

\verb|\compareStrings{\test}{te s T}{equal}{different}| yields:
\compareStrings{\test}{te s T}{equal}{different}

\verb|\compareStrings{\test}{tSeT}{equal}{different}| yields:
\compareStrings{\test}{tSeT}{equal}{different}

\verb|\compareStrings{\tset}{te s T}{equal}{different}| yields:
\compareStrings{\tset}{te s T}{equal}{different}

\verb|\compareStrings{\tset}{tSeT}{equal}{different}| yields:
\compareStrings{\tset}{tSeT}{equal}{different}

\verb|\compareStrings{\tset}{\test}{equal}{different}| yields:
\compareStrings{\tset}{\test}{equal}{different}

\verb|\compareStrings{\test}{\tset}{equal}{different}| yields:
\compareStrings{\test}{\tset}{equal}{different}

Be aware that \verb|\uppercase| and \verb|\lowercase| are not expandable, thus:

\verb|\compareStrings{\uppercase{test}}{TEST}{equal}{different}| yields:
\compareStrings{\uppercase{test}}{TEST}{equal}{different}

\verb|\compareStrings{\uppercase{test}}{test}{equal}{different}| yields:
\compareStrings{\uppercase{test}}{test}{equal}{different}

\verb|\compareStrings{\lowercase{TEST}}{test}{equal}{different}| yields:
\compareStrings{\lowercase{TEST}}{test}{equal}{different}

\verb|\compareStrings{\lowercase{test}}{test}{equal}{different}| yields:
\compareStrings{\lowercase{test}}{test}{equal}{different}

\end{document}

enter image description here


In case the removal of space tokens needs to be applied in situations where the argument itself may contain braces, I can offer an expandable routine \UD@removeallspace for recursively removing all explicit space tokens from a token sequence even when the argument contains braces, without the need of sentinel-tokens that may not occur within the argument.

As a side-effect the routine does replace pairs of matching explicit character tokens of category code 1/2 by matching braces.

Usually braces are the only characters with catcode 1/2, thus usually this should not be a problem. Usually.

(In case somebody knows a method that can be used in expansion-contexts on old 8bit-engines without e-TeX- or odfTeX-extensions also, and where matching explicit character tokens of category 1/2 will be left in place untouched, I'll be glad to learn abut it. ;-) )

\documentclass{article}
\usepackage{ifthenx}

\makeatletter
%% Code for expandable recursive space-remove-routine:
%%
%%=============================================================================
%% Paraphernalia:
%%    \UD@firstoftwo, \UD@secondoftwo,
%%    \UD@PassFirstToSecond, \UD@Exchange, \UD@removespace
%%    \UD@CheckWhetherNull, \UD@CheckWhetherBrace,
%%    \UD@CheckWhetherLeadingSpace, \UD@ExtractFirstArg
%%=============================================================================
\newcommand\UD@firstoftwo[2]{#1}%
\newcommand\UD@secondoftwo[2]{#2}%
\newcommand\UD@PassFirstToSecond[2]{#2{#1}}%
\newcommand\UD@Exchange[2]{#2#1}%
\newcommand\UD@removespace{}\UD@firstoftwo{\def\UD@removespace}{} {}%
%%-----------------------------------------------------------------------------
%% 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]{%
  \romannumeral0\expandafter\UD@secondoftwo\string{\expandafter
  \UD@secondoftwo\expandafter{\expandafter{\string#1}\expandafter
  \UD@secondoftwo\string}\expandafter\UD@firstoftwo\expandafter{\expandafter
  \UD@secondoftwo\string}\expandafter\expandafter\UD@firstoftwo{ }{}%
  \UD@secondoftwo}{\expandafter\expandafter\UD@firstoftwo{ }{}\UD@firstoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether argument's first token is a catcode-1-character
%%.............................................................................
%% \UD@CheckWhetherBrace{<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>}%
\newcommand\UD@CheckWhetherBrace[1]{%
  \romannumeral0\expandafter\UD@secondoftwo\expandafter{\expandafter{%
  \string#1.}\expandafter\UD@firstoftwo\expandafter{\expandafter
  \UD@secondoftwo\string}\expandafter\expandafter\UD@firstoftwo{ }{}%
  \UD@firstoftwo}{\expandafter\expandafter\UD@firstoftwo{ }{}\UD@secondoftwo}%
}%
%%-----------------------------------------------------------------------------
%% Check whether brace-balanced argument starts with a space-token
%%.............................................................................
%% \UD@CheckWhetherLeadingSpace{<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>}%
\newcommand\UD@CheckWhetherLeadingSpace[1]{%
  \romannumeral0\UD@CheckWhetherNull{#1}%
  {\expandafter\expandafter\UD@firstoftwo{ }{}\UD@secondoftwo}%
  {\expandafter\UD@secondoftwo\string{\UD@CheckWhetherLeadingSpaceB.#1 }{}}%
}%
\newcommand\UD@CheckWhetherLeadingSpaceB{}%
\long\def\UD@CheckWhetherLeadingSpaceB#1 {%
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@secondoftwo#1{}}%
  {\UD@Exchange{\UD@firstoftwo}}{\UD@Exchange{\UD@secondoftwo}}%
  {\UD@Exchange{ }{\expandafter\expandafter\expandafter\expandafter
   \expandafter\expandafter\expandafter}\expandafter\expandafter
   \expandafter}\expandafter\UD@secondoftwo\expandafter{\string}%
}%
%%-----------------------------------------------------------------------------
%% Extract first inner undelimited argument:
%%
%%   \UD@ExtractFirstArg{ABCDE} yields {A}
%%
%%   \UD@ExtractFirstArg{{AB}CDE} yields {AB}
%%
%% Be aware that (La)TeX does discard preceding space tokens when
%% gathering an undelimited argument. Thus:
%%
%%   \UD@ExtractFirstArg{  ABCDE} also yields {A}
%%
%%   \UD@ExtractFirstArg{  {AB}CDE} also yields {AB}
%%
%% This routine only works when the argument of \UD@ExtractFirstArg
%% is not empty/when the argument of \UD@ExtractFirstArg does have a
%% first inner undelimited argument. Thus use this routine only in 
%% situations where non-emptiness of \UD@ExtractFirstArg's argument is
%% ensured.
%%.............................................................................
\newcommand\UD@RemoveTillUD@SelDOm{}%
\long\def\UD@RemoveTillUD@SelDOm#1#2\UD@SelDOm{{#1}}%
\newcommand\UD@ExtractFirstArg[1]{%
  \romannumeral0%
  \UD@ExtractFirstArgLoop{#1\UD@SelDOm}%
}%
\newcommand\UD@ExtractFirstArgLoop[1]{%
  \expandafter\UD@CheckWhetherNull\expandafter{\UD@firstoftwo{}#1}%
  { #1}%
  {\expandafter\UD@ExtractFirstArgLoop\expandafter{\UD@RemoveTillUD@SelDOm#1}}%
}%
%%=============================================================================
%%  \UD@removeallspace{<argument probably with space tokens>}
%%
%%  after two expansion-steps delivers <argument without space tokens>
%%  
%%  (!!! \UD@removeallspace does also replace all pairs of matching 
%%       explicit character tokens of catcode 1/2 by matching braces!!!)
%%-----------------------------------------------------------------------------
\newcommand\UD@removeallspace[1]{%
  \romannumeral0\UD@RemoveAllSpaceLoop{#1}{}%
}%
\newcommand\UD@RemoveAllSpaceLoop[2]{%
  \UD@CheckWhetherNull{#1}{ #2}{%
    \UD@CheckWhetherLeadingSpace{#1}{%
       \expandafter\UD@RemoveAllSpaceLoop
       \expandafter{\UD@removespace#1}{#2}%
    }{%
      \UD@CheckWhetherBrace{#1}{%
        \expandafter\expandafter\expandafter\UD@PassFirstToSecond
        \expandafter\expandafter\expandafter{%
        \expandafter\UD@PassFirstToSecond\expandafter{%
            \romannumeral0\expandafter\UD@RemoveAllSpaceLoop
            \romannumeral0\UD@ExtractFirstArgLoop{#1\UD@SelDOm}{}%
        }{#2}}%
        {\expandafter\UD@RemoveAllSpaceLoop\expandafter{\UD@firstoftwo{}#1}}%
      }{%
       \expandafter\UD@RemoveAllSpaceLoopPushFirstArgument
       \romannumeral0\UD@ExtractFirstArgLoop{#1\UD@SelDOm}{#1}{#2}%
      }%
    }%
  }%
}%
\newcommand\UD@RemoveAllSpaceLoopPushFirstArgument[3]{%
    \expandafter\UD@RemoveAllSpaceLoop
    \expandafter{\UD@firstoftwo{}#2}{#3#1}%
}%

%% End of code for expandable recursive space-remove-routine.
\makeatother


\makeatletter
\DeclareRobustCommand\compareStrings[2]{%
  % Make sure each instance of \UD@removeallspace gets its argument 
  % expanded - do this by edef-fing while via \noexpand preventing 
  % expansion of \UD@removeallspace:
  \protected@edef\tempA{%
    {\noexpand\UD@removeallspace{#1}}%
    {\noexpand\UD@removeallspace{#2}}%
  }%
  % Now that the arguments for the \UD@removeallspace-instances are expanded,
  % via another \protected@edef have carried out the \UD@removeallspace-instances:
  \protected@edef\tempA{%
    % \lowercase is not expandable, thus does not get expanded
    % /does not get carried out at "e-def-fing-time". 
    % Expansion of anything else but \tempA is prevented via 
    % \noexpand. Within \tempA anything but the \UD@removeallspace-
    % instances is already expanded due to the previous \protected@edef.
    % Thus the only effect of this \protected@edef is carrying
    % out \UD@removeallspace-instances on arguments that were expanded by 
    % the previous \protected@edef.
    \lowercase{\noexpand\ifthenelse{\noexpand\equal\tempA}}%
  }%
  % Now in \tempA the arguments are expanded and space tokens
  % are removed. Thus \tempA expands to a call to \lowercase
  % where the arguments are expanded and thus you don't have
  % control-sequence-tokens any more (whereon \lowercase would have
  % no effect) but character-tokens (whereon \lowwercase does have
  % an effect).
  \tempA{\UD@firstoftwo}{\UD@secondoftwo}%
}
\makeatother

\parindent=0ex
\parskip=\baselineskip

\begin{document}

\verb|\compareStrings{te{s}t}{te { S } T}{equal}{different}| yields:
\compareStrings{te{s}t}{te { S } T}{equal}{different}

\verb|\compareStrings{test}{t E   sT}{equal}{different}| yields:
\compareStrings{test}{t E   sT}{equal}{different}

\verb|\compareStrings{t{es}t}{t{se}t}{equal}{different}| yields:
\compareStrings{t{es}t}{t{se}t}{equal}{different}

\def\test{te{ s} t}%
\def\tset{Ts{ E }t}%
\verb|\def\test{te{ s} t}|\\    
\verb|\def\tset{Ts{ E }t}|

\verb|\compareStrings{\test}{te {s} T}{equal}{different}| yields:
\compareStrings{\test}{te {s} T}{equal}{different}

\verb|\compareStrings{\test}{{tS}eT}{equal}{different}| yields:
\compareStrings{\test}{{tS}eT}{equal}{different}

\verb|\compareStrings{\tset}{t{e} s T}{equal}{different}| yields:
\compareStrings{\tset}{t{e} s T}{equal}{different}

\verb|\compareStrings{\tset}{tS{e}T}{equal}{different}| yields:
\compareStrings{\tset}{tS{e}T}{equal}{different}

\verb|\compareStrings{\tset}{\test}{equal}{different}| yields:
\compareStrings{\tset}{\test}{equal}{different}

\verb|\compareStrings{\test}{\tset}{equal}{different}| yields:
\compareStrings{\test}{\tset}{equal}{different}

Be aware that braces are taken into account, thus:

\verb|\compareStrings{t{es}t}{test}{equal}{different}| yields:
\compareStrings{t{es}t}{test}{equal}{different}

Be aware that \verb|\uppercase| and \verb|\lowercase| are not expandable, thus:

\verb|\compareStrings{\uppercase{test}}{TEST}{equal}{different}| yields:
\compareStrings{\uppercase{test}}{TEST}{equal}{different}

\verb|\compareStrings{\uppercase{test}}{test}{equal}{different}| yields:
\compareStrings{\uppercase{test}}{test}{equal}{different}

\verb|\compareStrings{\lowercase{TEST}}{test}{equal}{different}| yields:
\compareStrings{\lowercase{TEST}}{test}{equal}{different}

\verb|\compareStrings{\lowercase{test}}{test}{equal}{different}| yields:
\compareStrings{\lowercase{test}}{test}{equal}{different}

\end{document}

enter image description here

  • Joseph has an expandable upper-lower case code in expl3 for all tex variants, sensible or not:-) – David Carlisle Sep 8 '18 at 9:57
  • @DavidCarlisle Thanks for pointing this out. One more interesting thing I need to investigate... ;-) – Ulrich Diez Sep 8 '18 at 10:04
  • LaTeX's \zap@space gives unexpected results when the input has braces somewhere... – user4686 Sep 8 '18 at 12:10
  • @UlrichDiez I've posted an answer: as this is a string test (from the description), expandability is easy (simply detokenize first). – Joseph Wright Sep 8 '18 at 12:23
  • @jfbu Sure. Besides this, \zap@space relies on the sentinel-token \@empty. Thus one might also get unexpected results when \@empty occurs in the argument. There are situations where I don't like recursive iteration with sentinel-tokens for terminating the loop. Often I work with within each iteration checking for emptiness and examining and extracting and acting on the first undelimited argument/component contained in an argument... But I did not yet find an expandable method where matching pairs of explicit catcode1/2-character-tokens-pairs do not get replaced by braces... – Ulrich Diez Sep 8 '18 at 12:46

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.