Which engines can compare strings expandably?

The pdfTeX engine introduces the primitive \pdfstrcmp for expandable string comparison, with the input itself fully expanded, for example

\edef\foo{\detokenize{foo}}
\ifnum\pdfstrcmp{\foo}{foo}=0 %
\TRUE
\else
\FALSE
\fi


As detailed in \pdfstrcmp or \strcmp in pure TeX, this (almost certainly) cannot be emulated completely in TeX90. The functionality therefore has to be provided by the engine itself. Which engines provide the equivalent of \pdfstrcmp?

• @jfbu No problem :) There is a place for asking and answering questions 'for the public good' - I was surprised that this had not been covered before (but can't find it). – Joseph Wright Mar 23 '18 at 9:08
• I have a spin-off question: I did some quick benchmarking but for this used time in a bash shell, hence needed to do many reps to make negligible engine, format, and packages loading time; I did this because I recall luatex does not support \pdfelapsedtime. Do you have a canonical answer also for \pdfresettimer/\pdfelapsedtime? (I find them useful to get rough order of magnitudes). – user4686 Mar 23 '18 at 9:50
• @jfbu I'll write something up later: in LuaTeX one can use Lua to emulate the timer primitives, whilst in XeTeX and (u)pTeX we are out-of-luck, at least without shell escape – Joseph Wright Mar 23 '18 at 10:13
• @jfbu BTW, looking again here: the timer primitives will be in TL'19 pTeX/upTeX, and I have done the work for XeTeX, just waiting for my merge to be accepted – Joseph Wright Aug 24 '18 at 16:40

In addition to pdfTeX itself, e-pTeX and e-upTeX provide the \pdfstrcmp primitive under that name. In XeTeX, the same idea is implemented as \strcmp: the lack of pdf reflects the fact this is not PDF-related. In LuaTeX, one can emulate the outcomes in Lua:

\directlua{

  local write = tex.write
function strcmp(A,B)
if A == B then
write("0")
elseif A < B then
write("-1")
else
write("1")
end
end

}
\long\def\luaescapeexpandedstring#1{%
\luaescapestring{\detokenize\expandafter{\expanded{#1}}}%
}
\long\def\luastrcmp#1#2{%
\directlua{%
strcmp("\luaescapeexpandedstring{#1}","\luaescapeexpandedstring{#2}")
}%
}


Notice that we have to cover a couple of things here:

• Handling # tokens correctly requires a bit of work with expansion
• As well as the 'obvious' 'they are equal' outcome, one also needs to cover the 'one is bigger than the other' outcome (useful for example if comparing large integer values of the same number of digits!)

We might therefore set up something like

\begingroup\expandafter\expandafter\expandafter\endgroup
\expandafter\ifx\csname ifdefined\endcsname\relax
\ERROR
\expandafter\endinput
\fi
\ifdefined\directlua
\directlua{

    local write = tex.write
function strcmp(A,B)
if A == B then
write("0")
elseif A < B then
write("-1")
else
write("1")
end
end

  }
\long\def\luaescapeexpandedstring#1{%
\luaescapestring{\detokenize\expandafter{\expanded{#1}}}%
}
\long\def\stringcompare#1#2{%
\directlua{%
strcmp("\luaescapeexpandedstring{#1}","\luaescapeexpandedstring{#2}")
}%
}
\else
\ifdefined\pdfstrcmp
\long\def\stringcompare#1#2{\pdfstrcmp{#1}{#2}}
\else
\ifdefined\strcmp
\long\def\stringcompare#1#2{\strcmp{#1}{#2}}
\else
\ERROR
\fi
\fi
\fi


to define \stringcompare in an engine-neutral way (note I've avoided the primitive name as the number of expansions required is different from a macro wrapper).

• +1. :-) If the luacode package is loaded, the \luastring macro -- which fully expands its argument -- may be employed. The main instruction in the \stringcompare macro may then be simplified to \directlua{strcmp(\luastring{#1},\luastring{#2})}, and the \luaescapeexpandedstring macro needn't be defined. In order to make this approach engine-neutral, one would also have to write \usepackage{luacode} immediately after \ifdefined\directlua... – Mico Mar 23 '18 at 9:12
• @Mico I don't usually use the luacode package, as other than toy applications it's better to put Lua in a separate file. Here, though, the issue would be that \luastring doesn't handle# tokens in the same way as \pdfstrcmp (\luastring is just a wrapper for \luaescapestring): you do need to do the more careful manipulation involving \expanded and \detokenize to get the same result in all cases. – Joseph Wright Mar 23 '18 at 9:16
• @Mico Try \ifnum\stringcompare{#}{\detokenize{#}}=0 \TRUE\else\FALSE\fi using \luastring ;) – Joseph Wright Mar 23 '18 at 9:18
• Many thanks -- I wasn't aware until now of these shortcomings of the \luastring macro. – Mico Mar 23 '18 at 9:21
• @Mico We (the LaTeX team) spotted this one a while ago when picking up the code from pdftexcmds: it showed up in the test suite. We fixed in it the expl3 version, which is what I've written in 'normal' names my answer! – Joseph Wright Mar 23 '18 at 9:26