# Problems testing for a leap year using expl3

For a recent project I tried to become acquainted with LaTeX3 but unfortunately, I have some difficulties and cannot find the issue, which is why I'm asking for a helpful hint.

In the project, I want to check whether a given year of a date is a leap year. That's why I defined an \isLeapYear macro calling a LaTeX3 function under the hood.

The common algorithm to check whether a given year is a leap year is the following (from Wikipedia):

Taking this, I wrote the following code. Remember, that I'm just starting to learn LaTeX3, and to be honest, it's quite challenging even though I am more or less proficient in several programming languages.

\prg_new_conditional:Npnn \is_leapyear:n #1 { p, T, F, TF }
{
\int_set:Nn \l_tmpa_int { \curriculum_modulo:nn {#1} {4} }
\tl_if_eq:nnTF {\l_tmpa_int} {0} {
\int_set:Nn \l_tmpa_int { \curriculum_modulo:nn {#1} {100} }
\tl_if_eq:nnTF {\l_tmpa_int} {0} {
\int_set:Nn \l_tmpa_int { \curriculum_modulo:nn {#1} {400} }
\tl_if_eq:nnTF {\l_tmpa_int} {0} {\prg_return_true}{\prg_return_false}
}{\prg_return_true}
} {\prg_return_false}
}

\newcommand{\isLeapYear}[1]
{
\bool_if:nTF { \is_leapyear_p:n {#1} } {leap~year} {normal~year}
}


When running this code \isLeapYear{2024} a Missing number, treated as zero. error occurs.

Here is the exact error message.

! Missing number, treated as zero.
\__bool_=_0:
l.25     \isLeapYear{2024}

A number should have been here; I inserted 0'.
(If you can't figure out why I needed to see a number,
look up weird error' in the index to The TeXbook.)


Is there a major issue in my approach or am I missing something? I look forward to any helpful hints that increase my knowledge of LaTeX3 and take me further.

### MWE

\documentclass{article}

\ExplSyntaxOn

% https://www.alanshawn.com/latex3-tutorial/#implementing-modulo-operation
\cs_set:Npn \curriculum_modulo:nn #1#2 {
\int_set:Nn \l_tmpa_int { \int_div_truncate:nn {#1}{#2} }
\int_eval:n { (#1) - \l_tmpa_int * (#2) }
}

\prg_new_conditional:Npnn \is_leapyear:n #1 { p, T, F, TF }
{
\int_set:Nn \l_tmpa_int { \curriculum_modulo:nn {#1} {4} }
\tl_if_eq:nnTF {\l_tmpa_int} {0} {
\int_set:Nn \l_tmpa_int { \curriculum_modulo:nn {#1} {100} }
\tl_if_eq:nnTF {\l_tmpa_int} {0} {
\int_set:Nn \l_tmpa_int { \curriculum_modulo:nn {#1} {400} }
\tl_if_eq:nnTF {\l_tmpa_int} {0} {\prg_return_true}{\prg_return_false}
}{\prg_return_true}
} {\prg_return_false}
}

\newcommand{\isLeapYear}[1]
{
\bool_if:nTF { \is_leapyear_p:n {#1} } {leap~year} {normal~year}
}

\ExplSyntaxOff

\begin{document}
\isLeapYear{2024}
\end{document}


\bool_if:n(TF) requires that all operations in its first (n) argument be expandable (marked with ★ in interface3). You were using \int_set:Nn (assignments are never expandable) and \tl_if_eq:nnTF, which are not expandable. You can easily rewrite that code using \int_mod:nn instead of \curriculum_modulo:nn, and \int_compare:nNnTF instead of \int_set:Nn+\tl_if_eq:nnTF.

\int_compare:nNnTF expects <integer expressions> as (1st and 3rd) arguments, so instead of feeding it an int variable, you can feed it a (mandatorily) expandable expression that evaluates to an integer. If you look at the documentation of \int_mod:nn in interface3 (currently in section 20.1 Integer expressions), you'll see it is expandable, and evaluates to an integer, so you can safely use it anywhere LaTeX expects an integer.

\documentclass{article}

\ExplSyntaxOn

\prg_new_conditional:Npnn \is_leapyear:n #1 { p, T, F, TF }
{
\int_compare:nNnTF { \int_mod:nn {#1} { 4 } } = { 0 }
{
\int_compare:nNnTF { \int_mod:nn {#1} { 100 } } = { 0 }
{
\int_compare:nNnTF { \int_mod:nn {#1} { 400 } } = { 0 }
{ \prg_return_true: }
{ \prg_return_false: }
}
{ \prg_return_true: }
}
{ \prg_return_false: }
}

\newcommand{\isLeapYear}[1]
{
\bool_if:nTF { \is_leapyear_p:n {#1} } {leap~year} {normal~year}
}

\ExplSyntaxOff

\begin{document}
\isLeapYear{2024}
\end{document}


Within \int_if_odd:nTF you can use \fp_eval:n.

Within \fp_eval:n you can use boolean expressions.

You can use these things for delivering either the control-sequence-token \prg_return_true: or the control-squence-token \prg_return_false: depending on whether the number in question denotes a leap-year or not.

\documentclass{article}

\ExplSyntaxOn
\prg_new_conditional:Npnn \is_leapyear:n #1 { p, T, F, TF } {
\int_if_odd:nTF{
% the \fp_eval-expression yields 1 for leap-years, 0 otherwise.
\fp_eval:n {    ( \int_mod:nn {#1}{4}=0 && \int_mod:nn{#1}{25}!=0 )
|| \int_mod:nn {#1}{400}=0   }
}{\prg_return_true:}{\prg_return_false:}
}
\cs_new:Npn {\isLeapYear} #1 {
#1~is~a~\bool_if:nTF { \is_leapyear_p:n {#1} } {leap} {normal}~year
}
\ExplSyntaxOff

\begin{document}
\isLeapYear{2024}

\isLeapYear{2000}

\isLeapYear{1600}

\isLeapYear{1700}

\isLeapYear{2023}
\end{document}


( \int_mod:nn {#1}{4}=0 && \int_mod:nn{#1}{25}!=0 )
you could as well use
( \int_mod:nn {#1}{4}=0 && \int_mod:nn{#1}{100}!=0 ).
I don't know which one is computed faster in LaTeX/expl3.

• Also instead of constructing \use_<thing>:nn to select \prg_return_[true|false]:, you can just skip one indirection and construct the latter Feb 9 at 18:34
• @PhelypeOleinik Ouch. Yes. Done. Thank you. ;-) Feb 9 at 19:24

There are two good answers, but they're incomplete. For instance, the given code would return that 1500 has not been a leap year, which is wrong. Similarly, in the United Kingdom (and colonies) 1700 has been a leap year.

Of course this is also a small joke, but the code below shows other features of the language, so I think it's a good addition.

\documentclass{article}

\ExplSyntaxOn

\prg_new_conditional:Nnn \sam_is_leapyear_newstyle:n { p, T, F, TF }
{
\bool_lazy_or:nnTF
{% #1 is divisible by 4 but not by 100
\bool_lazy_and_p:nn
{ \int_compare_p:n { \int_mod:nn { #1 } { 4 } = 0 } }
{ \int_compare_p:n { \int_mod:nn { #1 } { 100 } > 0 } }
}
{% #1 is divisible by 400
\int_compare_p:n { \int_mod:nn { #1 } { 400 } = 0 }
}
{ \prg_return_true: }
{ \prg_return_false: }
}
\prg_new_conditional:Nnn \sam_is_leapyear_oldstyle:n { p, T, F, TF }
{
\int_compare:nTF { \int_mod:nn { #1 } { 4 } = 0 }
{ \prg_return_true: }
{ \prg_return_false: }
}
\prg_new_conditional:Nnn \sam_is_leapyear:nn { p, T, F, TF }
{
\int_compare:nTF { #2 >= #1 }
{
\sam_is_leapyear_newstyle:nTF { #2 } { \prg_return_true: } { \prg_return_false: }
}
{
\sam_is_leapyear_oldstyle:nTF { #2 } { \prg_return_true: } { \prg_return_false: }
}
}

\NewExpandableDocumentCommand{\isleapyearTF}{O{1582}mmm}
{
\sam_is_leapyear:nnTF { #1 } { #2 } { #3 } { #4 }
}

\ExplSyntaxOff

\begin{document}

2022: \isleapyearTF{2022}{leap year}{not leap year}

2024: \isleapyearTF{2024}{leap year}{not leap year}

2000: \isleapyearTF{2000}{leap year}{not leap year}

1600: \isleapyearTF{1600}{leap year}{not leap year}

1700: \isleapyearTF{1700}{leap year}{not leap year}

1800: \isleapyearTF{2023}{leap year}{not leap year}

1500: \isleapyearTF{1500}{leap year}{not leap year}

1700 (UK): \isleapyearTF[1752]{1700}{leap year}{not leap year}

1800 (UK): \isleapyearTF[1752]{1800}{leap year}{not leap year}

1700 (RU): \isleapyearTF[1918]{1700}{leap year}{not leap year}

1800 (RU): \isleapyearTF[1918]{1800}{leap year}{not leap year}

1900 (RU): \isleapyearTF[1918]{1900}{leap year}{not leap year}

\end{document}


The optional argument is the year of adoption of the Gregorian calendar we want to check with (default 1582).

• There's a bug in the first line, where it reports that 2022 is a leap year, because it's actually checking 2024. Feb 9 at 22:24
• @AriBrodsky Copy paste error! :-( Fixed! Thanks! Feb 9 at 22:25