1

I made a macro using xparse for formatting the month with optional day and year. Except the appended optional argument doesn't work inside a description label for the description list---unless I enclose it in curly braces (e.g. {\Month{4}[8]}). This error is easily fixable via curly braces, but I would like to know WHY it occurs and perhaps also how to avoid having to fix it with curly braces.

\documentclass{article}
\usepackage{stix2}
\usepackage{etoolbox,xparse,xspace}
%\numtomonth converts a number into its corresponding month.
%The starred and unstarred versions display the long and short forms of the month.
\makeatletter
\NewDocumentCommand{\numtomonth}{ s m }{%
    \IfBooleanTF{#1}%
        {%ifstar
            \ifnumequal{#2}{1}{January}{%
            \ifnumequal{#2}{2}{February}{%
            \ifnumequal{#2}{3}{March}{%
            \ifnumequal{#2}{4}{April}{%
            \ifnumequal{#2}{5}{May}{%
            \ifnumequal{#2}{6}{June}{%
            \ifnumequal{#2}{7}{July}{%
            \ifnumequal{#2}{8}{August}{%
            \ifnumequal{#2}{9}{September}{%
            \ifnumequal{#2}{10}{October}{%
            \ifnumequal{#2}{11}{November}{%
            \ifnumequal{#2}{12}{December}{%
            \errmessage{The input must be an integer between 1 and 12}%
            }}}}}%
            }}}}}%
            }}%
        }%
        {%ifnostar
            %https://tex.stackexchange.com/questions/15009/macros-for-common-abbreviations
            \ifnumequal{#2}{1}{Jan\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{2}{Feb\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{3}{March}{%
            \ifnumequal{#2}{4}{April}{%
            \ifnumequal{#2}{5}{May}{%
            \ifnumequal{#2}{6}{June}{%
            \ifnumequal{#2}{7}{July}{%
            \ifnumequal{#2}{8}{Aug\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{9}{Sep\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{10}{Oct\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{11}{Nov\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{12}{Dec\@ifnextchar{.}{}{.\@\xspace}}{%
            \errmessage{The input must be an integer between 1 and 12}%
            }}}}}%
            }}}}}%
            }}%
        }%
}
\makeatother
%
%\Month has three arguments.
%The first and third are optional.
%The first is the year and the third is the day of the month.
%The mandatory argument is the number corresponding to the month.
%\Month displays the short form of the month and, if used, the day and the year.
\NewDocumentCommand{\Month}{ O{} m O{} }{%\month already defined
    \numtomonth{#2}%
    \ifstrempty{#3}{}{~#3}%
    \ifstrempty{#1}{}{%
        \ifstrempty{#3}{ #1}{, #1}%
    }%
}
\usepackage{enumitem}
\parindent=0pt
\begin{document}
I am trying to make a description list on \Month[2023]{4}[8].%random sentence
\begin{description}
\item[\Month{4}]%works; no appended optional argument
Alpha
\item%[\Month{4}[8]]%doesn't work with appended optional argument
Beta
\item[{\Month{4}[8]}]%works; same as above, but enclosed in curly braces
Charlie
\item%[\Month[2023]{4}[8]]%doesn't work with appended optional argument
Delta
\item[{\Month[2023]{4}[8]}]%works; same as above, but enclosed in curly braces
Epsilon
\end{description}
\end{document}
User23456234
  • 1,808
  • 1
    Only as an additional note to Henri's link: In your case the problem is, that \item is a classic command not defined using \NewDocumentCommand. So automatic nesting of commands with optional argument(s) inside the optional argument of \item is not provided. – cabohah Apr 08 '23 at 08:51

3 Answers3

2

With current LaTeX \item is a classic LaTeX command not defined using \NewDocumentCommand. So nesting of commands with optional arguments is not supported. But you can define your own \Item and use it instead of \item to fix this:

\documentclass{article}
\usepackage{stix2}
\usepackage{etoolbox,xparse,xspace}
%\numtomonth converts a number into its corresponding month.
%The starred and unstarred versions display the long and short forms of the month.
\makeatletter
\NewDocumentCommand{\numtomonth}{ s m }{%
    \IfBooleanTF{#1}%
        {%ifstar
            \ifnumequal{#2}{1}{January}{%
            \ifnumequal{#2}{2}{February}{%
            \ifnumequal{#2}{3}{March}{%
            \ifnumequal{#2}{4}{April}{%
            \ifnumequal{#2}{5}{May}{%
            \ifnumequal{#2}{6}{June}{%
            \ifnumequal{#2}{7}{July}{%
            \ifnumequal{#2}{8}{August}{%
            \ifnumequal{#2}{9}{September}{%
            \ifnumequal{#2}{10}{October}{%
            \ifnumequal{#2}{11}{November}{%
            \ifnumequal{#2}{12}{December}{%
            \errmessage{The input must be an integer between 1 and 12}%
            }}}}}%
            }}}}}%
            }}%
        }%
        {%ifnostar
            %https://tex.stackexchange.com/questions/15009/macros-for-common-abbreviations
            \ifnumequal{#2}{1}{Jan\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{2}{Feb\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{3}{March}{%
            \ifnumequal{#2}{4}{April}{%
            \ifnumequal{#2}{5}{May}{%
            \ifnumequal{#2}{6}{June}{%
            \ifnumequal{#2}{7}{July}{%
            \ifnumequal{#2}{8}{Aug\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{9}{Sep\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{10}{Oct\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{11}{Nov\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{12}{Dec\@ifnextchar{.}{}{.\@\xspace}}{%
            \errmessage{The input must be an integer between 1 and 12}%
            }}}}}%
            }}}}}%
            }}%
        }%
}
\makeatother
%
%\Month has three arguments.
%The first and third are optional.
%The first is the year and the third is the day of the month.
%The mandatory argument is the number corresponding to the month.
%\Month displays the short form of the month and, if used, the day and the year.
\NewDocumentCommand{\Month}{ O{} m O{} }{%\month already defined
    \numtomonth{#2}%
    \ifstrempty{#3}{}{~#3}%
    \ifstrempty{#1}{}{%
        \ifstrempty{#3}{ #1}{, #1}%
    }%
  }

\NewDocumentCommand{\Item}{o}{% \IfValueTF{#1}{\item[{#1}]}{\item}% }

\usepackage{enumitem} \parindent=0pt \begin{document} I am trying to make a description list on \Month[2023]{4}[8].%random sentence \begin{description} \Item[\Month{4}]%works; no appended optional argument Alpha \Item[\Month{4}[8]]%works with appended optional argument Beta \Item[{\Month{4}[8]}]%works; same as above, but with enclosed in curly braces Charlie \Item[\Month[2023]{4}[8]]%works with appended optional argument Delta \Item[{\Month[2023]{4}[8]}]%works; same as above, but enclosed in curly braces Epsilon \end{description} \end{document}

I would not recommend to redefine \item itself, because this could fail, if a package redefines \item depending on environments. But if you want to ignore this, you could also do:

\documentclass{article}
\usepackage{stix2}
\usepackage{etoolbox,xparse,xspace}
%\numtomonth converts a number into its corresponding month.
%The starred and unstarred versions display the long and short forms of the month.
\makeatletter
\NewDocumentCommand{\numtomonth}{ s m }{%
    \IfBooleanTF{#1}%
        {%ifstar
            \ifnumequal{#2}{1}{January}{%
            \ifnumequal{#2}{2}{February}{%
            \ifnumequal{#2}{3}{March}{%
            \ifnumequal{#2}{4}{April}{%
            \ifnumequal{#2}{5}{May}{%
            \ifnumequal{#2}{6}{June}{%
            \ifnumequal{#2}{7}{July}{%
            \ifnumequal{#2}{8}{August}{%
            \ifnumequal{#2}{9}{September}{%
            \ifnumequal{#2}{10}{October}{%
            \ifnumequal{#2}{11}{November}{%
            \ifnumequal{#2}{12}{December}{%
            \errmessage{The input must be an integer between 1 and 12}%
            }}}}}%
            }}}}}%
            }}%
        }%
        {%ifnostar
            %https://tex.stackexchange.com/questions/15009/macros-for-common-abbreviations
            \ifnumequal{#2}{1}{Jan\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{2}{Feb\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{3}{March}{%
            \ifnumequal{#2}{4}{April}{%
            \ifnumequal{#2}{5}{May}{%
            \ifnumequal{#2}{6}{June}{%
            \ifnumequal{#2}{7}{July}{%
            \ifnumequal{#2}{8}{Aug\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{9}{Sep\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{10}{Oct\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{11}{Nov\@ifnextchar{.}{}{.\@\xspace}}{%
            \ifnumequal{#2}{12}{Dec\@ifnextchar{.}{}{.\@\xspace}}{%
            \errmessage{The input must be an integer between 1 and 12}%
            }}}}}%
            }}}}}%
            }}%
        }%
}
\makeatother
%
%\Month has three arguments.
%The first and third are optional.
%The first is the year and the third is the day of the month.
%The mandatory argument is the number corresponding to the month.
%\Month displays the short form of the month and, if used, the day and the year.
\NewDocumentCommand{\Month}{ O{} m O{} }{%\month already defined
    \numtomonth{#2}%
    \ifstrempty{#3}{}{~#3}%
    \ifstrempty{#1}{}{%
        \ifstrempty{#3}{ #1}{, #1}%
    }%
  }

\NewCommandCopy{\iitem}{\item} \RenewDocumentCommand{\item}{o}{% \IfValueTF{#1}{\iitem[{#1}]}{\iitem}% }

\usepackage{enumitem} \parindent=0pt \begin{document} I am trying to make a description list on \Month[2023]{4}[8].%random sentence \begin{description} \item[\Month{4}]%works; no appended optional argument Alpha \item[\Month{4}[8]]%works with appended optional argument Beta \item[{\Month{4}[8]}]%works; same as above, but enclosed in curly braces Charlie \item[\Month[2023]{4}[8]]%works with appended optional argument Delta \item[{\Month[2023]{4}[8]}]%works; same as above, but enclosed in curly braces Epsilon \end{description} \end{document}

cabohah
  • 11,455
2

I suggest a different approach without so many optional arguments.

\documentclass{article}
\usepackage{stix2}
\usepackage{enumitem}

%\numtomonth converts a number into its corresponding month. %The starred and unstarred versions display the long and short forms of the month.

\ExplSyntaxOn

\NewDocumentCommand{\printdate}{sm} { \IfBooleanTF { #1 } { \egreg_date_print:Nn \egreg_date_longmonth:e { #2 } } { \egreg_date_print:Nn \egreg_date_shortmonth:e { #2 } } }

\seq_new:N \l__egreg_date_items_seq

\cs_new_protected:Nn \egreg_date_print:Nn {% #1 is either long or short month \seq_set_split:Nnn \l__egreg_date_items_seq { - } { #2 } \int_case:nnF { \seq_count:N \l__egreg_date_items_seq } { {1}{ #1 { #2 } }% just the month {2}{ __egreg_date_year_or_day:Nee #1 { \seq_item:Nn \l__egreg_date_items_seq { 1 } } { \seq_item:Nn \l__egreg_date_items_seq { 2 } } } {3}{ #1 { \seq_item:Nn \l__egreg_date_items_seq { 2 } } % month \nobreakspace \seq_item:Nn \l__egreg_date_items_seq { 3 } % day ,~ \seq_item:Nn \l__egreg_date_items_seq { 1 } % year } } {\errmessage{Invalid~date}} }

\cs_new_protected:Nn __egreg_date_year_or_day:Nnn { \int_compare:nTF { #2>1000 } {% first item is year, second item is month #1 { #3 } % month \nobreakspace #2 % year } {% first item is month, second item is day #1 { #2 } % month \nobreakspace #3 % day } } \cs_generate_variant:Nn __egreg_date_year_or_day:Nnn { Nee }

\cs_new_protected:Nn \egreg_addperiod: { \peek_charcode:NF { . } { . } }

\cs_new:Nn \egreg_date_longmonth:n { \int_case:nnF { #1 } { {1}{January} {2}{February} {3}{March} {4}{April} {5}{May} {6}{June} {7}{July} {8}{August} {9}{September} {10}{October} {11}{November} {12}{December} } {\errmessage{The~input~must~be~an~integer~between~1~and~12}} } \cs_generate_variant:Nn \egreg_date_longmonth:n { e } \cs_new:Nn \egreg_date_shortmonth:n { \int_case:nnF { #1 } { {1}{Jan\egreg_addperiod:} {2}{Feb\egreg_addperiod:} {3}{March} {4}{April} {5}{May} {6}{June} {7}{July} {8}{Aug\egreg_addperiod:} {9}{Sep\egreg_addperiod:} {10}{Oct\egreg_addperiod:} {11}{Nov\egreg_addperiod:} {12}{Dec\egreg_addperiod:} } {\errmessage{The~input~must~be~an~integer~between~1~and~12}} } \cs_generate_variant:Nn \egreg_date_shortmonth:n { e }

\ExplSyntaxOff

\begin{document}

I am trying to make a description list on \printdate{2023-4-8}

\begin{description}

\item[\printdate{4}] Alpha

\item[\printdate{4-8}] Beta

\item[\printdate{2023-4}] Charlie

\item[\printdate{2023-4-8}] Delta

\item[\printdate{2023-1-8}] Delta

\item[\printdate*{2023-1-8}] Delta

\end{description}

\end{document}

If you specify a date with just one item, it's taken as month. With two items, the first one is checked to be greater than 1000: in this case it's taken as a year and the second item is a month, otherwise the first item is the month and the second item is the day. With three items, we have year, month and day.

enter image description here

egreg
  • 1,121,712
  • Question: Is the reason that this solution does not require additional curly braces due to the ExplSyntax stuff (which I don't understand) or the lack of optional arguments? – User23456234 Apr 08 '23 at 14:15
  • 1
    @User23456234 I don't use optional arguments to \printdate. – egreg Apr 08 '23 at 14:25
1

This error is easily fixable via curly braces, but I would like to know WHY it occurs and perhaps also how to avoid having to fix it with curly braces.

\item is a classical LaTeX command.

With classical LaTeX commands optional arguments are implemented as ]-delimited macro arguments. If with such commands you nest optional arguments, then the first ] will be taken for the matching delimiter of the outermost optional argument although it should be taken for the delimiter of the innermost optional argument and a subsequent ] should be taken for the matching delimiter of the outermost optional argument. Delimiter-matching with delimited macro arguments is not the same as curly-brace-matching with undelimited macro arguments.

(Unlike with classical commands, with non-classical commands defined in terms of xparse-facilities like \NewDocumentCommand some extra effort is done for keeping track of nested [ and doing proper ]-matching.)

As you already mentioned in the question, if with traditional LaTeX commands it comes to nesting optional arguments, then the entire(!) content of outer optional arguments should be nested in curly braces for ensuring correct delimiter-matching. Curly braces "hide" ]-delimiters of inner optional arguments from being taken for a matching delimiter of outer optional arguments. As surrounding the entire argument, the curly braces will be removed/stripped off in the process of gathering the tokens belonging to the argument and thus do no harm at all.

\item[{\Month{4}[8]}]: Here the curly braces {...} ensure that the ] of \Month{4}[8] is not taken for the matching ]-delimiter of \item's optional argument. As the curly braces surround the entire content of \item's optional/]-delimited argument, they are discarded in the course of gathering the tokens that form \item's optional argument so that only the tokens \Month{4}[8], but not the surrounding curly braces, are gathered as \item's optional argument.

\item[{\Month[2023]{4}[8]}]: Here the curly braces {...} ensure that neither the ] of \Month[2023] nor the ] of [8] is taken for the matching ]-delimiter of \item's optional argument. As the curly braces surround the entire content of \item's optional/]-delimited argument, they are discarded in the course of gathering the tokens that form \item's optional argument so that only the tokens \Month[2023]{4}[8], but not the surrounding braces, are gathered as \item's optional argument.


In my answer to How does TeX look for delimited arguments? I tried to explain the differences between TeX's gathering of delimited arguments and TeX's gathering of undelimited arguments.

Ulrich Diez
  • 28,770