The following code is a POC showing a way to validate one Julian date in "pure" LaTeX3
: it is related to this question.
There is no doubt that this code contains some clumsiness. Any advice would be welcome, except for error handling via messages, which is something I know how to do.
For example, do \str_new:N \l_mbc_date_year_str
and \l_mbc_date_year_int
are both necessary?
\documentclass{article}
% Source for easy testing via pgffor:
% + https://tex.stackexchange.com/a/696444/6880
\usepackage{pgffor}
\ExplSyntaxOn
\seq_new:N \g_mbc_month_size
\seq_set_from_clist:Nn \g_mbc_month_size {%
0, % Not used.
31, % January
0, % February: this special value will help us to find bugs...
31, % March
30, % April
31, % May
30, % June
31, % July
31, % August
30, % September
31, % October
30, % November
31 % December
}
% The rule defining a leap year A is as follows:
%
% + If A % 4 != 0, the year is not a leap year.
%
% + If A % 4 = 0 , the year is a leap year unless
% A % 100 = 0 and A % 400 != 0.
%
% This leads to the following one-line validating test.
%
% + (A % 4 = 0) AND (A % 100 != 0 OR A % 400 = 0)
\prg_set_conditional:Npnn \if_leap_year:N #1 { p , T , TF } {
\bool_if:nTF {
\int_compare_p:n { \int_mod:nn #1 { 4 } = 0 }
&& (
\int_compare_p:n { \int_mod:nn #1 { 100 } != 0 }
||
\int_compare_p:n { \int_mod:nn #1 { 400 } = 0 }
)
}{
\prg_return_true:
}{
\prg_return_false:
}
}
\regex_new:N \g_mbc_date_format_rgx
\regex_set:Nn\g_mbc_date_format_rgx { \A (\d+) \- (\d+) \- (\d+) \Z }
\tl_new:N \l_mbc_date_year_tl
\tl_new:N \l_mbc_date_month_tl
\tl_new:N \l_mbc_date_day_tl
\int_new:N \l_mbc_date_year_int
\int_new:N \l_mbc_date_month_int
\int_new:N \l_mbc_date_day_int
\NewDocumentCommand { \ValidateISODate }{ m }{
\regex_extract_once:NnNTF \g_mbc_date_format_rgx { #1 } \l_tmpa_seq {
% Integer values found.
\seq_pop_right:NN \l_tmpa_seq \l_mbc_date_day_tl
\seq_pop_right:NN \l_tmpa_seq \l_mbc_date_month_tl
\seq_pop_right:NN \l_tmpa_seq \l_mbc_date_year_tl
\int_set:Nn \l_mbc_date_day_int \l_mbc_date_day_tl
\int_set:Nn \l_mbc_date_month_int \l_mbc_date_month_tl
\int_set:Nn \l_mbc_date_year_int \l_mbc_date_year_tl
% 1 <= month <= 12
\int_compare:nTF { 1 <= \l_mbc_date_month_int <= 12 }{
% February special setting.
\int_compare:nT { \l_mbc_date_month_int = 2 }{
\if_leap_year:NTF \l_mbc_date_year_int {
\seq_set_item:Nnn \g_mbc_month_size 2 { 29 }
}{
\seq_set_item:Nnn \g_mbc_month_size 2 { 28 }
}
}
% Good day.
\int_compare:nTF {
1 <= \l_mbc_date_day_int
<= \seq_item:Nn \g_mbc_month_size
{ \int_use:N \l_mbc_date_month_int }
}{
OK
% Bad day.
}{
KO (day)
}
% NOT(1 <= month <= 12).
}{
KO (month)
}
% Syntax error
}{
KO (syntax)
}
}
\ExplSyntaxOff
\begin{document}
\section{OK}
\pgfkeys{
tester/.code=\ValidateISODate{#1}{:} #1\par\medskip,
tester/.list = {
2023-06-14,
2023-09-24,
2023-02-28,
2024-02-29,
400-02-29
}
}
\section{KO -- Invalid day}
\pgfkeys{
tester/.code=\ValidateISODate{#1}{:} #1\par\medskip,
tester/.list = {
300-02-29,
2023-02-29,
2024-02-30,
2023-09-00,
2023-09-32
}
}
\section{KO -- Invalid month}
\pgfkeys{
tester/.code=\ValidateISODate{#1}{:} #1\par\medskip,
tester/.list = {
2023-19-32,
2023-00-29
}
}
\section{KO -- Syntax error}
\pgfkeys{
tester/.code=\ValidateISODate{#1}{:} #1\par\medskip,
tester/.list = {
2023-06-XX,
2023-09-19 2023-09-20,
-0001-12-24
}
}
\end{document}
str
vstl
... As far as speed is concerned, time will tell whether optimisation is necessary or not.\int_compare:nTF
, but I really think that the first concern is readability, and if optimization becomes a need, then you can "dirty" the code.\prg_new_conditional:Npnn
to avoid overwriting an existing defintion. You should only useset
if you've previously usednew
. Note you can use\prg_generate_conditional_variant:Nnn
to generate alternative argument specifications. This would be another way of dealing with theN
/n
confusion. If you generate a variant ofn
usingV
, you can pass an unbraced variable directly.400-2-003
is a valid input because it might not be ISO but it is still unambiguous and we can extract the integers for future usage.