9

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):

Algorithm for leap years, © 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.
<to be read again> 
                   \__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}

3 Answers 3

16

\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}
3

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}

enter image description here

From mathematics, instead of
( \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.

2
  • 1
    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, 2022 at 18:34
  • 1
    @PhelypeOleinik Ouch. Yes. Done. Thank you. ;-) Feb 9, 2022 at 19:24
2

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}

enter image description here

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

2
  • 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, 2022 at 22:24
  • @AriBrodsky Copy paste error! :-( Fixed! Thanks!
    – egreg
    Feb 9, 2022 at 22:25

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .