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 at 18:34
  • 1
    @PhelypeOleinik Ouch. Yes. Done. Thank you. ;-) Feb 9 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 at 22:24
  • @AriBrodsky Copy paste error! :-( Fixed! Thanks!
    – egreg
    Feb 9 at 22:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.