以下代码是一个 POC,展示了一种以“纯”方式验证一个儒略日期的方法LaTeX3
:它与这个问题。
毫无疑问,此代码有些笨拙。欢迎提出任何建议,但通过消息处理错误除外,我知道如何做到这一点。
例如,\str_new:N \l_mbc_date_year_str
和\l_mbc_date_year_int
都是必要的吗?
\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}
答案1
你处理的是 ISO 格式的日期,而不是儒略日期:儒略日只是一个整数,儒略日期是十进制数。
ISO 允许日期采用yyyy-mm-dd
或 的形式yyyymmdd
,但我们假设您只允许带连字符的形式。
从正则表达式比较开始似乎是一个好策略:要匹配的正则表达式是
\A \d{4} \- \d{2} \- \d{2} \Z
也就是说,输入应该由四位数字、一个连字符、两位数字、一个连字符、两位数字组成。任何其他输入都会导致无效的 ISO 日期消息。
这是我的建议,您可以参考一下。
\documentclass{article}
\ExplSyntaxOn
\NewDocumentCommand{\validateISOdate}{m}
{
\projetmbc_isodate_validate:n { #1 }
}
\cs_new_protected:Nn \projetmbc_isodate_validate:n
{
\regex_match:nnTF { \A \d{4} \- \d{2} \- \d{2} \Z } { #1 }
{
\__projetmbc_isodate_validate:n { #1 }
}
{
Invalid~date~'#1'~(format)
}
}
\cs_new:Nn \__projetmbc_isodate_validate:n
{
\__projetmbc_isodate_process:w #1 \q_stop
}
\cs_new:Npn \__projetmbc_isodate_process:w #1 - #2 - #3 \q_stop
{
\__projetmbc_isodate_check:nnn { #1 } { #2 } { #3 }
}
\cs_new:Nn \__projetmbc_isodate_check:nnn
{
\bool_lazy_or:nnTF { \int_compare_p:nNn { #2 } < 1 } { \int_compare_p:nNn { #2 } > { 12 } }
{
Invalid~date~'#1-#2-#3'~(month)
}
{
\__projetmbc_isodate_day:nnn { #1 } { #2 } { #3 }
}
}
\cs_new:Nn \__projetmbc_isodate_day:nnn
{
\int_compare:nNnTF { #3 } = { 0 }
{
Invalid~date~'#1-#2-#3'~(day)
}
{
\__projetmbc_isodate_day_aux:nnn { #1 } { #2 } { #3 }
}
}
\cs_new:Nn \__projetmbc_isodate_day_aux:nnn
{
\int_compare:nNnTF { #3 } > { \__projetmbc_isodate_checkday:nn { #1 } { #2 } }
{
Invalid~date~'#1-#2-#3'~(day)
}
{
Valid~date~'#1-#2-#3'
}
}
\cs_new:Nn \__projetmbc_isodate_checkday:nn
{
\int_case:nn { #2 }
{
{1}{31}
{2}{\__projetmbc_isodate_february:n{#1}}
{3}{31}
{4}{30}
{5}{31}
{6}{30}
{7}{31}
{8}{31}
{9}{30}
{10}{31}
{11}{30}
{12}{31}
}
}
\cs_new:Nn \__projetmbc_isodate_february:n
{
\bool_lazy_and:nnTF
{% year divisible by 4
\int_compare_p:nNn { \int_mod:nn { #1 } { 4 } } = { 0 }
} % AND
{% year not divisible by 100 or divisible by 400
\bool_lazy_or_p:nn
{% not divisible by 100
\int_compare_p:nNn { \int_mod:nn { #1 } { 100 } } > { 0 }
}
{% divisible by 400
\int_compare_p:nNn { \int_mod:nn { #1 } { 400 } } = { 0 }
}
}
{29} % year is leap
{28} % year is not leap
}
\ExplSyntaxOff
\begin{document}
\ExplSyntaxOn
\NewDocumentCommand{\test}{m}
{
\clist_map_inline:nn { #1 } { \projetmbc_isodate_validate:n { ##1 } \par }
}
\ExplSyntaxOff
\test{
2023-06-14,
2023-09-24,
2023-02-28,
2024-02-29,
400-02-29,
300-02-29,
2023-02-29,
2024-02-30,
2023-09-00,
2023-09-32,
2023-19-32,
2023-00-29,
2023-06-XX,
2023-09-19,
2023-09-20,
-0001-12-24,
2000-02-29,
2100-02-29
}
\end{document}
答案2
让我总结一下我的发现和一些观点:
第 19.3 节以“序列左侧的操作比右侧的操作更快”开头,这
\seq_pop_right:NN
似乎是一个糟糕的开头。我会将其替换为\seq_pop_left:NN
。在回答您链接的问题时,我
\seq_item:Nn
在语句中使用了 expandableint_compare
。遗憾的是,手册中没有提到这种看似直接的访问的速度或效率。至少我们不必为这些事情使用额外的宏(“标记列表”)。
如果我们允许在 LaTeX3 方法中使用分隔宏,我也会恢复到我的第一个版本我的其他答案并使用这些。对我来说,使用它们比处理序列要自然得多。
#n
也就是说,手册还说第二个
N
from\seq_pop_…:NN
是一个标记列表,但你使用的是“字符串”变量(当然,两者都只是宏)。n
of\int_set:Nn
是一个“int expr”,没有进一步的说明(但不是N
)。在第一个解决方案(分隔宏)中,我不需要访问任何序列,而在第二个解决方案(重正则表达式)中,我将
\seq_item:Nn
再次使用。除非您稍后需要再次使用这些值,否则我认为没有必要将它们存储在某个宏/标记列表/计数中。int 变量是真正的 TeX 计数,
\int_set:Nn
基本上可以做到#1=\numexpr#2\relax
。因此问题变成了:当其中一个是计数时,比较数字是否更快?如果你每个日期只使用它们一次或两次,这有关系吗?
\numexpr
无论如何,它首先需要被编辑一次来存储计数。我认为更重要的是 20.5 节中的效率说明:
\int_compare(_p):nNn(TF)
比 – 快五倍\int_compare(_p):n(TF)
,我假设是因为它只使用 TeX 基础=
,<
和,>
并且不必解析和转换!=
,>=
和<=
。解析
&&
和应该||
比()
直接访问函数 And 或 Or 慢。但这将在速度、自然可读性和逻辑之间做出妥协。在我的代码中,我使用了简单的二进制 Ors —— 对于
&&
与||
不是的惰性求值(第 9.3 节)。如果
\if_int_compare:w
“允许”纯的我将使用 LaTeX3 解决方案\if_case:w
来获取月份的最后一天,而不是按序列查找。0
如果月份数字超出 1-12 的范围,则可以使用最后一天作为月份检查您把
N
s 和n
s 混淆了。N
代表单个标记,n
代表“括号中给出的标记集”(第 1.1 节),但手册还指出“如果您使用单个标记作为参数n
,一切都会很好”。然而,我会这样做并支撑之后就不会破裂。
\if_leap_year:n
#1
\int_mod:nn
\if_leap_year_p:nTF { 2023 }
2
的硬编码\seq_set_item:Nnn
应该没问题。(顺便说一下,您将 30/31 列表的第二项设置为第一个二月,然后它将始终是该值,而特殊值将永远消失。)我还采纳了@cfr 在评论中提到的一些内容,但闰年测试器是否必须是内部宏还有待商榷。
我们还可以创建一个正则表达式来捕获非法的月份和日期数字:
\regex_new:N \g__mbc_datex_format_rgx
\regex_set:Nn\g__mbc_datex_format_rgx { \A (\d+) \-
(0* (?:[1-9]|1[012])) \- (0* (?:[1-9]|[12][0-9]|3[01])) \Z }
那么唯一要做的测试是:该年月份的最后一天。同样,在这个例子中,我使用整个 l3regex 系统,并用它来提取值。
测试l3benchmark
似乎表明,这比仅使用原始检查的分隔宏方法花费的时间大约多一倍\if_case:w
,这并不奇怪。
代码
\documentclass{article}
\usepackage{pgffor, l3benchmark}
\ExplSyntaxOn
\cs_new:Nn \__mbc_lastday_of_month_in_year:nn {
\if_case:w #1~
0 \or: 31 \or: \__mbc_if_leap_year:nTF {#2}{29}{28} \or: 31
\or: 30 \or: 31 \or: 30
\or: 31 \or: 31 \or: 30
\or: 31 \or: 30 \or: 31
\else: 0 \fi:
}
\prg_new_conditional:Npnn \__mbc_if_leap_year:n #1 { 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: % multiple of 400 → leap year
}{ \prg_return_false: } % multiple of 100 → no leap year
}{ \prg_return_true: } % multiple of 4 → leap year
}{ \prg_return_false: } % no multiple of 4 → no leap year
}
\regex_new:N \g__mbc_date_format_rgx
\regex_set:Nn\g__mbc_date_format_rgx { \A \d+ \- \d+ \- \d+ \Z }
\prg_new_conditional:Npnn \__mbc_if_validate_date:n #1 { TF }{
\regex_match:NnTF \g__mbc_date_format_rgx { #1 } {
\__mbc_datetest_parse:w #1 \q_stop
}{ \prg_return_false: }
}
\cs_new:Npn \__mbc_datetest_parse:w #1 - #2 - #3 \q_stop {
\bool_lazy_or:nnTF % case 0 and case else fail this for invalid months
{ \int_compare_p:nNn { #3 } < { 1 } }
{ \int_compare_p:nNn { #3 } >
{ \__mbc_lastday_of_month_in_year:nn { #2 } { #1 } } }
{ \prg_return_false: }
{ \prg_return_true: }
}
%%% heavy regex solution
\regex_new:N \g__mbc_datex_format_rgx
\regex_set:Nn\g__mbc_datex_format_rgx { \A (\d+) \-
(0* (?:[1-9]|1[012])) \- (0* (?:[1-9]|[12][0-9]|3[01])) \Z }
\prg_new_conditional:Npnn \__mbc_if_validate_datex:n #1 { TF }{
\regex_extract_once:NnNTF \g__mbc_datex_format_rgx { #1 } \l_tmpa_seq {
\int_compare:nNnTF { \seq_item:Nn \l_tmpa_seq {4} }
>
{ \__mbc_lastday_of_month_in_year:nn
{ \seq_item:Nn\l_tmpa_seq {3} }
{ \seq_item:Nn\l_tmpa_seq {2} } }
{ \prg_return_false: }
{ \prg_return_true: }
}{ \prg_return_false: }
}
\NewDocumentCommand { \ValidateJulianDate }{ m m m }{
\__mbc_if_validate_date:nTF { #1 }{ #2 }{ #3 }
}
\NewDocumentCommand { \ValidateJulianDateX }{ m m m }{
\__mbc_if_validate_datex:nTF { #1 }{ #2 }{ #3 }
}
\ExplSyntaxOff
\setlength\parindent{0pt}
\pgfkeys{
tester/.code = \ValidateJulianDate {#1}{OK}{Not OK} and
\ValidateJulianDateX{#1}{OK}{Not OK}: #1\par\medskip}
\newcommand*\test[1]{\pgfkeys{tester/.list={#1}}}
\begin{document}
\section{OK}
\test{2023-06-14, 2023-09-24, 2023-02-28, 2024-02-29, 400-02-29}
\section{KO -- Invalid day}
\test{300-02-29, 2023-02-29, 2024-02-30, 2023-09-00, 2023-09-32}
\section{KO -- Invalid month}
\test{2023-19-32, 2023-00-29}
\section{KO -- Syntax error}
\test{2023-06-XX, 2023-09-19 2023-09-20, -0001-12-24}
\clearpage
\section{Table}
\ExplSyntaxOn\ttfamily
\foreach \v in {\ValidateJulianDate, \ValidateJulianDateX}{
\expandafter \string \v : \par
\benchmark_once:n {
\foreach \m in {0, ..., 13}{
\ifnum\int_mod:nn{\m-1}{3}=0\medskip\fi
\foreach \d in {0, ..., 32}{
\ifnum\int_mod:nn{\d+1}{10}=0\relax\space\fi
\exp_args:Nx\v{2023-0\m-0\d}{1}{0}
}
\par
}
}
\bigskip
}
\ExplSyntaxOff
\end{document}