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}


strvstl... As far as speed is concerned, time will tell whether optimisation is necessary or not. – projetmbc Sep 23 '23 at 21:14\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. – projetmbc Sep 23 '23 at 21:27Pythonscript. – projetmbc Sep 23 '23 at 21:41\g_mbc_month_sizeis not named according the the l3 specification. A sequence variable name should always end_seq. If it is internal, it should begin\l__or\g__. Also, I doubt whether you need a global variable here. If not, it would be better local. The same goes for all other cases of internal variables/functions etc. They all should have__at the start or immediately following the initial\if,\l,\c,\getc. Plus your conditional should includembc.\ValidateJulianDateshould preferably includembc. Also, these are NOT Julian dates. – cfr Sep 24 '23 at 02:41\prg_new_conditional:Npnnto avoid overwriting an existing defintion. You should only usesetif you've previously usednew. Note you can use\prg_generate_conditional_variant:Nnnto generate alternative argument specifications. This would be another way of dealing with theN/nconfusion. If you generate a variant ofnusingV, you can pass an unbraced variable directly. – cfr Sep 24 '23 at 03:00N/Vvariant for eachnjust in case someone wants to use\int_yearor\my_value_yearinstead of{ \int_year }? Why shouldn't I force my user (or me in case of__= to always use{ … }even if it's just one token?. Will theN/Vget expanded once and then forwarded to thenversion if the variants are generated? – Qrrbrbirlbel Sep 24 '23 at 03:24\cs_generate_variant:Nn \int_abs:n { v }but also\cs_generate_variant:Nn \__chronos_dateformat_sign:n { v }. It's not a question of shouldn't force your user to use braces. It's a question of can't. (Well, I suppose you could, but not without breaking a whole lot of stuff.) – cfr Sep 24 '23 at 03:36Vsubstitutes the value of the variable into a function specified withn. I'm not sure ifforwardis quite the right way to put it. You can't generate a variant which replacesnwithN.Nandnare base forms. All functions are defined with argument specifications which use only the base forms (N,netc.). You can't define a function withVoroin the argument specification. Those are always generated variants.N->c;n>V,v,e,oetc. – cfr Sep 24 '23 at 03:43400-2-003is a valid input because it might not be ISO but it is still unambiguous and we can extract the integers for future usage. – Qrrbrbirlbel Sep 24 '23 at 10:02L3baby coder so my concern is more about coding. ;-) – projetmbc Sep 24 '23 at 10:11