Compatibility updates
release 1.2l (2017/07/26)
removed an internal macro (\xint_gob_til_xint_relax
) which I had used here. So I added the definition to make the code compile again.
(2015/03/07) since release 1.1 (2014/10/28)
of xint the original code of this answer was broken. The overhaul of \xintNewExpr
was mentioned in the user manual, but it should have been included among the list of "breaking changes" in CHANGES.html
. Actually it is only a very esoteric feature of \xintNew(Bool)Expr
whose change impacted the code here, I had forgotten that there existed some use of this in the world !
For people with xint < 1.1
the old code is there, but commented out.
In passing I mention that the logic operator &&
and ||
are now preferred by xint
rather than &
and |
, but the latter are still accepted although perhaps they will be used in the future for other things: bitwise operations perhaps.
This is a (modest) alternative to the LaTeX3+lualatex
approach. It uses xintexpr. It requires the user to employ \AND
, \OR
, \NOT
in the logical expressions. Other keywords are \XOR
, \ALL
, \ANY
.
latest update: the number of variables is anyhow limited to 9
, because each expression will be converted into a macro with that many parameters. Hence the first answer with nested \xintFor
, which was spelled out up to 5
variables had the potential of full generality in this context. But in my second answer (bottom of this answer) I generated the rows in a way analogous to the method I see in the LaTeX3+lualatex
code, via binary expansion of the integers 0
, 1
... up to 2^n-1
, with n
the number of variables. This makes for a more compact coding (also less efficient, but that doesn't matter much).
This second answer additionally adds an optional parameter allowing the user to specify directly how the expressions will appear in the header row.
Also, it allows the final expression too to be ended by a ;
.
And, a starred variant uses \halign
rather than an array
environment in order to allow pagebreaks. With 9
variables, there are 512
generated rows, hence this is necessary. The image is only the first page of a document having 12
of them.
update: use of ;
(as in OP) rather than ,
to separate expressions.
The boolean expressions are typed in using \AND
, \OR
, \NOT
and also there is \XOR
, \ALL
, \ANY
(which have multiple arguments separated by ,
). One can then customize in the macro how they are supposed to be typeset.
The boolean expressions are separated (as in the OP) by semi-colons: my initial answer used commas, because the \xintexpr
parser handles this natively, also when commas are used elsewhere to separate arguments to function.
However, for typesetting the first row, one had to hide such commas with braces for example \XOR({a,b,c})
, or \XOR(a\SEP b\SEP c)
with \def\SEP{,}
, not very convenient. Thus, expressions in the user input are now separated using ;
.
As one can see, the code here is quite short. I ran a bit out of steam at the end and possibly there could be a way to shorten it somewhat more and avoid the big \ifcase
. As it stands, the code allow to construct a table with up to 5
indeterminates represented as letters (enlarging the \ifcase
one may go up to 9
; with 5
letters we already have 32
rows) and arbitrarily many boolean expressions separated by commas (actually as it stands the nb of expressions plus the nb of indeterminates should be kept at most 10
, see the *{10}c
array preamble) [I didn't have time to check if one can replace 10
by something dynamical, in that case there is no problem computing it from the two arguments to \truthtable
]

Two more truth tables:

\documentclass{article}
\usepackage{xintexpr}
\catcode`_ 11
\newcount \tt_varcnt
% this internal macro was suppressed in xint 1.2l (2017/07/26)
% but is used in this answer
\long\def\xint_gob_til_xint_relax #1\xint_relax {}%
\let\xint_relax\relax
% auxiliary macro to handle things separated by semi-colons
\def\truthtable_scanexprs #1;{%
\xint_gob_til_xint_relax #1\truthtable_scan_done\xint_relax
\odef\tt_exprlist {\tt_exprlist{#1}}%
\truthtable_scanexprs
}%
\def\truthtable_scan_done #1\truthtable_scanexprs {}
\newcommand\truthtable [2]{%
% example of use :
% \truthtable{a,b,c}{a\OR b; b\AND\NOT(c)}
%
% The first argument lists the used indeterminates, they must be lowercase or
% uppercase Latin letters.
%
% The second argument is a ; separated list of logical expressions.
\begingroup
\endlinechar-1 \everyeof{\noexpand }% for use of \scantokens
% initializing
\tt_varcnt 0
\def\tt_exprlist {}% will be {exprA}{exprB}{exprC}...
% scan #2 for ; separated expressions and fill \tt_exprlist:
\truthtable_scanexprs #2;\xint_relax;%
%
% PREPARATION FOR DEFINING THE BOOLEAN EXPRESSIONS
% MODIFIED FOR COMPATIBILITY WITH xint 1.1 OR LATER
%%%% | and & are still accepted but the manual recommend || and &&
%%%% (2015/03/07)
\def\OR{||}\def\AND{&&}\def\NOT{!}% needed for \xintNewBoolExpr
% original was
% \def\OR{|}\def\AND{&}\def\NOT{!}% needed for \xintNewBoolExpr
% END
% the negation ! must be applied to a parenthesized expression
%
% extra strange set-up before \edef and then \scantokens
% the idea is too allow, X, O, R, A, L, L, A, N, Y as indeterminates...
% ... and for this as we will need to make them active, thus
% we must first replace \XOR for example by \1.
\def\XOR{\1}\def\ALL{\2}\def\ANY{\3}%
\let\1\relax\let\2\relax\let\3\relax
%
\edef\tt_x {\tt_exprlist}% replaces \XOR by \1, \ALL by \2, \ANY by \3
%
% ORIGINAL VERSION:
% % we now make active the indeterminates to replace them by {$1}, {$2}, ..
% % place holders (cf. xintexpr manual)
% \xintFor ##1 in {#1} \do
% {\catcode`##1\active
% \advance\tt_varcnt 1
% \lccode`~=`##1\relax
% \lowercase{\edef~}{{$\the\tt_varcnt}}%$1, $2, $3, as in xintexpr manual
% }%
% NEW VERSION FOR COMPATIBILITY WITH xint 1.1 OR LATER (2015/03/07)
% we now make active the indeterminates to replace them by {#1}, {#2}, ..
% place holders (with a catcode 12 # as it simplifies things here, and
% the \xintNewExpr scanner does not mind)
\xintFor ##1 in {#1} \do%
% I should have defined a sub-macro for this, code would be clearer
{\catcode`##1\active
\advance\tt_varcnt 1
\lccode`~=`##1\relax
\lowercase{\edef~}{{\string####\the\tt_varcnt}}}%
% END OF MODIFICATION
%
% Now \scantokens to replace the now active letters by the braced
% macro parameters (I don't recall exactly why I wanted them braced,
% these braces will get removed during \xintNewExpr scan)
%
\edef\tt_x {\scantokens\expandafter{\tt_x}}%
%
% For use of \xintNewBoolExpr (which again does some \scantokens),
% the letters *must* be reset to their standard catcodes.
%
\xintFor ##1 in {#1} \do {\catcode`##1 11 }%
%
% Definition of the boolean expressions. Using ; as separator for
% user input was only in order to ease up
% NOTE: (2015/03/07)
% for simplicity I stick here to the originally used ; as expression separator
% on input, but xint 1.1 has macros which can be used, out of \xintexpr
% context to identify (expandably) comma separated things, even themselves
% containing commas if they are in parentheses
% finding the correct
% expressions for typesetting the head-row of the table, we need to
% re-install here the , as separator, this allows to do only one
% \xintNewBoolExpr, as it knows how to identify the various
% comma separated sub-expressions.
%
\def\1{xor}\def\2{all}\def\3{any}%
\xintNewBoolExpr\tt_y[\tt_varcnt]{\xintListWithSep{,}{\tt_x}}%
%
% CUSTOMIZE HERE:
% For typesetting the head row, customize as desired:
\def\OR{+}\def\AND{\cdot}\def\NOT{\mathord{\sim}}%
\def\XOR{\mbox{\texttt{xor}}}%
\def\ALL{\mbox{\texttt{all}}}%
\def\ANY{\mbox{\texttt{any}}}%
%
% the table. Up to five variables, extensible up to use of nine variables
\begin{array}{*{10}c}
\xintListWithSep{&}{\xintCSVtoList{#1}}&%
\xintListWithSep{&}{\tt_exprlist}\\
\hline
\ifcase\tt_varcnt
\or
\xintFor* ##1 in {01}\do
{##1 & \xintListWithSep{&}{\xintCSVtoList{\tt_y {##1}}}\\ }
\or
\xintFor* ##1 in {01}\do
{\xintFor* ##2 in {01}\do
{##1 & ##2&
\xintListWithSep{&}{\xintCSVtoList{\tt_y {##1}{##2}}}\\ }}
\or
\xintFor* ##1 in {01}\do
{\xintFor* ##2 in {01}\do
{\xintFor* ##3 in {01}\do
{##1 & ##2 & ##3& \xintListWithSep{&}%
{\xintCSVtoList{\tt_y {##1}{##2}{##3}}}\\ }}}
\or
\xintFor* ##1 in {01}\do
{\xintFor* ##2 in {01}\do
{\xintFor* ##3 in {01}\do
{\xintFor* ##4 in {01}\do
{##1 & ##2 & ##3& ##4&\xintListWithSep{&}%
{\xintCSVtoList{\tt_y {##1}{##2}{##3}{##4}}}\\ }}}}
\or
\xintFor* ##1 in {01}\do
{\xintFor* ##2 in {01}\do
{\xintFor* ##3 in {01}\do
{\xintFor* ##4 in {01}\do
{\xintFor* ##5 in {01}\do
{##1 & ##2 & ##3& ##4& ##5&\xintListWithSep{&}%
{\xintCSVtoList{\tt_y {##1}{##2}{##3}{##4}{##5}}}\\ }}}}}
\fi
\hline
\end{array}
\endgroup
}
\catcode`_ 8
\begin{document}
\[
\truthtable{a,b,c}{a\OR b; b\AND\NOT(c); \NOT (a\OR b)\OR (b\AND\NOT(c))}
\]
\[
\truthtable{X,Y,Z}{Z\AND X \OR Y \AND X}
\]
\[
\truthtable{X,Y,Z,T}{Z\AND X \OR Y \AND X \AND T}
\]
\[
\truthtable{a, b}{\XOR (a,b)}
\]
\[
\truthtable {p, q, r, s, t}{\ANY(p,q,r,s,t); \XOR(p,q,r,s,t); \ALL(p,q,r,s,t)}
\]
\[
\truthtable{a, b, c, d}{\XOR (a,b,c,d); \XOR(a,\XOR(b,c,d)); \XOR(\XOR(a,b),\XOR(c,d))}
\]
\[
\truthtable{A,D,N}{A\AND D \OR N \AND D; A\OR D \AND N \OR D}
\]
\end{document}
Here is the code of the second answer:
\documentclass[a4paper]{article}
\usepackage{geometry}
\usepackage{xintexpr}
% in order to convert from decimal to binary using \xintDecBin
\usepackage{xintbinhex}
\catcode`_ 11
\makeatletter
% to count the number of variables:
\newcount \tt_varcnt
% The number of variables is at most 9 (leading to 512 rows...)
% to count the number of expressions (in order to choose dynamically
% a large enough number of columns for the array environment)
\newcount \tt_exprcnt
% to handle things separated by semi-colons
% we add some extra to allow empty expressions to be skipped, and also to allow
% the user to terminate the last expression by a ;
% [if the optional parameter is used, use \space to get something empty in the
% header row cell]
% this internal macro was suppressed in xint 1.2l (2017/07/26)
% but is used in this answer
\long\def\xint_gob_til_xint_relax #1\xint_relax {}%
\let\xint_relax\relax
\def\truthtable_scanexprs #1;{%
\if\relax\detokenize{#1}\relax\expandafter\truthtable_scan_skip\fi
\xint_gob_til_xint_relax #1\truthtable_scan_done\xint_relax
\odef\tt_exprlist {\tt_exprlist{#1}}%
\advance \tt_exprcnt 1
\truthtable_scanexprs
}%
\def\truthtable_scan_done #1\truthtable_scanexprs {}%
\def\truthtable_scan_skip #1\truthtable_scanexprs {\truthtable_scanexprs}%
% we now have an optional parameter to provide a custom typesetting
% of the expressions in the head row
% and we have furthermore a star variant to use \halign rather than array
\newcommand*\truthtable {%
\begingroup
\endlinechar-1 \everyeof{\noexpand }% for use of \scantokens
\tt_varcnt 0
\tt_exprcnt 0
\def\tt_exprlist {}%
\def\tt_headexprlist {\tt_exprlist}%
\def\tt_usearray {1}%
\@ifstar{\def\tt_usearray {0}\truthtable@chkopt}\truthtable@chkopt
}%
\def\truthtable@chkopt {\@ifnextchar[{\truthtable@opt}{\truthtable@}%]
}
\def\truthtable@opt [#1]{%
% we first scan the optional argument for the header row.
% it will be printed as is, only need to transform the ;'s into &'s
\truthtable_scanexprs #1;\xint_relax;%
\let\tt_headexprlist\tt_exprlist
\tt_exprcnt 0
\def\tt_exprlist {}%
\truthtable@
% *NO* check is done that #1 defines the same number of expressions
% as the last mandatory argument to \truthtable !
}
\def\truthtable@ #1#2{%
% convert the comma separated indeterminates into a list
\oodef\tt_varlist{\xintCSVtoListNoExpand {#1}}%
% scan #2 for ; separated expressions and fill \tt_exprlist:
\truthtable_scanexprs #2;\xint_relax;%
% PREPARATION FOR DEFINING THE BOOLEAN EXPRESSIONS
% MODIFIED FOR COMPATIBILITY WITH xint 1.1 OR LATER
%%%% | and & are still accepted but the manual recommend || and &&
%%%% (2015/03/07)
\def\OR{||}\def\AND{&&}\def\NOT{!}% needed for \xintNewBoolExpr
% original was
% \def\OR{|}\def\AND{&}\def\NOT{!}% needed for \xintNewBoolExpr
% END
% the negation ! must be applied to a parenthesized expression
%
% extra strange set-up before \edef and then \scantokens
% the idea is too allow, X, O, R, A, L, L, A, N, Y as indeterminates...
% ... and for this as we will need to make them active, thus
% we must first replace \XOR for example by \1.
\def\XOR{\1}\def\ALL{\2}\def\ANY{\3}%
\let\1\relax\let\2\relax\let\3\relax
%
\edef\tt_x {\tt_exprlist}% replaces \XOR by \1, \ALL by \2, \ANY by \3
%
% ORIGINAL VERSION:
% % we now make active the indeterminates to replace them by {$1}, {$2}, ..
% % place holders (cf. xintexpr manual)
% \xintFor ##1 in {#1} \do
% {\catcode`##1\active
% \advance\tt_varcnt 1
% \lccode`~=`##1\relax
% \lowercase{\edef~}{{$\the\tt_varcnt}}%$1, $2, $3, as in xintexpr manual
% }%
% NEW VERSION FOR COMPATIBILITY WITH xint 1.1 OR LATER (2015/03/07)
% we now make active the indeterminates to replace them by {#1}, {#2}, ..
% place holders (with a catcode 12 # as it simplifies things here, and
% the \xintNewExpr scanner does not mind)
\xintFor ##1 in {#1} \do%
% I should have defined a sub-macro for this, code would be clearer
{\catcode`##1\active
\advance\tt_varcnt 1
\lccode`~=`##1\relax
\lowercase{\edef~}{{\string####\the\tt_varcnt}}}%
% END OF MODIFICATION
% Now the \scantokens with active letters.
\edef\tt_x {\scantokens\expandafter{\tt_x}}%
%
% For use of \xintNewBoolExpr (which again does some \scantokens),
% the letters *must* recover their standard catcodes
%
\xintFor* ##1 in \tt_varlist \do {\catcode`##1 11 }%
%
% Definition of the boolean expressions. Using ; as separator for
% user input was only in order to ease up finding the correct
% expressions for typesetting the head-row of the table, we need to
% re-install here the , as separator, this allows to do only one
% \xintNewBoolExpr, it knows how to identify the various sub-expressions.
\def\1{xor}\def\2{all}\def\3{any}%
\xintNewBoolExpr\tt_y[\tt_varcnt]{\xintListWithSep{,}{\tt_x}}%
%
% CUSTOMIZE HERE:
% For typesetting the head row, customize as desired:
% (*not* relevant in case of use of the optional parameter)
\def\OR{+}\def\AND{\cdot}\def\NOT{\mathord{\sim}}%
\def\XOR{\mbox{\texttt{xor}}}%
\def\ALL{\mbox{\texttt{all}}}%
\def\ANY{\mbox{\texttt{any}}}%
%
\if\tt_usearray 1\expandafter\truthtable_array\else
\expandafter\truthtable_halign
\fi
\endgroup
}
\def\truthtable_array {%
% (we have the count of variables in \tt_varcnt and the count of
% expressions in \tt_exprcnt)
\begin{array}{*{\numexpr\tt_varcnt+\tt_exprcnt\relax}c}
% generate the header-row,
\xintListWithSep{&}{\tt_varlist}
&
\xintListWithSep{&}{\tt_headexprlist}\\
\hline
% calculate 2^n, n is nb of variables
\tt_varcnt \xintiiPow {2}{\tt_varcnt}
% generate integers from 2^n to 2*2^n -1, then binary notation
% will always start by a 1 which we can gobble
\xintFor* ##1 in {\xintSeq {\tt_varcnt}{2*\tt_varcnt-1}}
\do
% (the problem is that \xintDecToBin always trims leading zeros...
% hence we resort to this to always have a leading 1)
{\edef\tt_temp{\expandafter\expandafter\expandafter
\xint_gobble_i\xintDecToBin {##1}}%
\edef\tt_temp{\tt_temp\xintCSVtoList{\expandafter\tt_y\tt_temp}}%
\xintListWithSep {&}{\tt_temp}\\}
\hline
\end{array}
}
\newtoks\tt_toks
% The \halign variant (should *not* be put inside \[..\])
\def\truthtable_halign {%
% This is variant using \halign to allow break accross pages.
% I use some tricks to center it (whow! incredibly it works)
% (I initially used a repeatable preamble, but following TeX by Topic
% 25.5, to center the alignment, I need to insert some \tabskip
% at the LAST column, hence here I follow a more complicated approach
% constructing the preamble first.)
%
% but then I don't know how to have horizontal rules limited to
% the width covered by the actual contents...
%
\tt_toks {\hfil$\mathstrut##$\hfil\tabskip 2\arraycolsep}%
\xintiloop [{\tt_varcnt+\tt_exprcnt-1}+-1]
\ifnum\xintiloopindex>0
\tt_toks \expandafter{\the\tt_toks &\hfil$##$\hfil}%
\repeat
%\showthe\tt_toks
\tabskip 0pt plus 1000pt minus 1000pt
\halign to \hsize
{\span\the\tt_toks\tabskip 0pt plus 1000pt minus 1000pt \cr
% first row
\xintListWithSep{&}{\tt_varlist}&\xintListWithSep{&}{\tt_headexprlist}\cr
% however the rule extends across the full page
% \hline
\tt_varcnt \xintiiPow {2}{\tt_varcnt}
\xintFor* ##1 in {\xintSeq {\tt_varcnt}{2*\tt_varcnt-1}}
\do
{\edef\tt_temp{\expandafter\expandafter\expandafter
\xint_gobble_i\xintDecToBin {##1}}%
\edef\tt_temp{\tt_temp\xintCSVtoList{\expandafter\tt_y\tt_temp}}%
\xintListWithSep {&}{\tt_temp}\cr }%
% \hline
}%
}
\catcode`_ 8
\makeatother
\pagestyle{empty}
\begin{document}\thispagestyle{empty}
\[
\truthtable{a,b,c}{a\OR b; b\AND\NOT(c); \NOT (a\OR b)\OR (b\AND\NOT(c));}
\]
\textbf{Exercise:~} fill in the header row:
\[% in the optional argument, \AND, \NOT, are not mandatory but optional
% on can use whatever one wants.
\truthtable [a?b; ?\cdot\NOT(c); \NOT(a+?)?(b\AND\NOT(?));]% trailing ; allowed
{a, b, c}
{a\OR b; b\AND\NOT(c); \NOT (a\OR b)\OR (b\AND\NOT(c))}
\]
% maximal number of variables is 9
% \truthtable* {a, b, c}{\XOR(a, b, c)}
\bigskip
\hrule
\medskip
An example with the maximal number of variables (9, hence 512 rows) using
\verb|\halign| to allow breaking accross pages:\medskip
\truthtable* [\XOR(a,\cdots,i); ?(a,\cdots,d)\AND\NOT(?(e,\cdots,i));]
{a, b , c, d, e, f, g, h, i}
{\XOR(a, b , c, d, e, f, g, h, i);
\ALL(a,b,c,d)\AND\NOT(\ANY(e,f,g,h,i))
}
\end{document}

\mathord{\sim}
?\mathord
do? It's the first time I see this command.\mathord
makes the following character or subformula function as anord
inary object; as such, the spacing is that of an ordinary object instead of the default spacing of\sim
as arel
ational symbol (\sim
is defined using\mathrel
).