10

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}
1
  • Do you need complete generality? If not, between 1900 and 2100 (open interval), leap year is the same as multiple of 4.
    – lhf
    Commented Feb 17 at 14:43

6 Answers 6

17

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

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

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

We can solve this task without expl3, but using Lua in LuaTeX:

\edef\nbb{\string\\} \edef\%{\csstring\%}
\def\isleap#1\iftrue{\directlua{
   if #1\%4 > 0 then tex.print("\nbb iffalse")
   else if #1\%100 > 0 then tex.print("\nbb iftrue")
        else if #1\%400 > 0 then tex.print("\nbb iffalse")
             else tex.print("\nbb iftrue")
   end  end  end
}}
2022: \isleap 2022 \iftrue Leap \else Common \fi year.\par 
2024: \isleap 2024 \iftrue Leap \else Common \fi year.\par 
2000: \isleap 2000 \iftrue Leap \else Common \fi year.\par  
1900: \isleap 1900 \iftrue Leap \else Common \fi year.\par 

\bye

Or, we can use \xcasesof and \expr macros from OpTeX:

\def\isleap#1\iftrue{%
   \xcasesof
   {\ifnum \expr[0]{#1\%4} > 0 }   {\iffalse}
   {\ifnum \expr[0]{#1\%100} > 0 } {\iftrue}
   {\ifnum \expr[0]{#1\%400} > 0 } {\iffalse}
   \_finc {\iftrue}%
}

2022: \isleap 2022 \iftrue Leap \else Common \fi year.\par 
2024: \isleap 2024 \iftrue Leap \else Common \fi year.\par 
2000: \isleap 2000 \iftrue Leap \else Common \fi year.\par  
1900: \isleap 1900 \iftrue Leap \else Common \fi year.\par 

\bye
1

Here is a solution that uses the \int_case: function. In my opinion, this allows for a more condensed, more readable code structure.

\documentclass{article}

\ExplSyntaxOn

\prg_new_conditional:Npnn \__my_int_if_leap_year:n #1 { p , TF , T , F }
  {
    \int_case:nnF {#1}
      {
        { \int_div_truncate:nn {#1} {400} * 400 } { \prg_return_true:  }
        { \int_div_truncate:nn {#1} {100} * 100 } { \prg_return_false: }
        { \int_div_truncate:nn {#1} {  4} *   4 } { \prg_return_true:  }
      }
      { \prg_return_false: }
  }
% example user command
\NewExpandableDocumentCommand \IfLeapYearTF { m m m }
  { \__my_int_if_leap_year:nTF {#1} {#2} {#3} }

\ExplSyntaxOff

\begin{document}

\IfLeapYearTF{1999}{TRUE}{FALSE} % Expands to "FALSE"

\IfLeapYearTF{2000}{TRUE}{FALSE} % Expands to "TRUE"

\IfLeapYearTF{1900}{TRUE}{FALSE} % Expands to "FALSE"

\IfLeapYearTF{2024}{TRUE}{FALSE} % Expands to "TRUE"

\end{document}
0
1

Just for comparison. What we can do with classical plain TeX (without Lua):

\newcount\modresult
\newif\ifleap

\def\modulo #1#2 {\modresult=#1\relax \divide\modresult by#2 
    \multiply\modresult by-#2 \advance\modresult by#1\relax
}
\def\isleap#1\iftrue {%
   \modulo{#1}4
   \ifnum\modresult>0 \leapfalse
   \else \modulo{#1}100
         \ifnum\modresult>0 \leaptrue 
         \else \modulo{#1}400 
               \ifnum\modresult>0 \leapfalse
               \else \leaptrue
               \fi
         \fi
   \fi
   \ifleap
}

2022: \isleap 2022 \iftrue Leap \else Common \fi year.\par 
2024: \isleap 2024 \iftrue Leap \else Common \fi year.\par 
2000: \isleap 2000 \iftrue Leap \else Common \fi year.\par  
1900: \isleap 1900 \iftrue Leap \else Common \fi year.\par 

\bye

The \isleap macro isn't expandable in this case.

2
  • You can do it with any programming language. The question wasn't to have a leap year algorithm, but to understand what went wrong in the provided code.
    – egreg
    Commented Feb 17 at 14:56
  • 2
    @egreg I mean that this is good and sufficiently simple example to show the coding principles used with TeX and to compare them. I have a course "Typesetting and TeX" at our university and I've found to be usable to show especially this thread to my students when they start to decide how they will do programming tasks in TeX. There are typical examples. And your comment is typical too. It shows that egerg has seen all wisdom and he insert such comments that teach someone in this site.
    – wipet
    Commented Feb 18 at 6:34

You must log in to answer this question.

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