LaTeX3 - 有关验证 ISO 日期的代码的建议

LaTeX3 - 有关验证 ISO 日期的代码的建议

以下代码是一个 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

让我总结一下我的发现和一些观点:

  1. 第 19.3 节以“序列左侧的操作比右侧的操作更快”开头,这\seq_pop_right:NN似乎是一个糟糕的开头。我会将其替换为\seq_pop_left:NN

    在回答您链接的问题时,我\seq_item:Nn在语句中使用了 expandable int_compare。遗憾的是,手册中没有提到这种看似直接的访问的速度或效率。

    至少我们不必为这些事情使用额外的宏(“标记列表”)。

    如果我们允许在 LaTeX3 方法中使用分隔宏,我也会恢复到我的第一个版本我的其他答案并使用这些。对我来说,使用它们比处理序列要自然得多。#n

  2. 也就是说,手册还说第二个Nfrom\seq_pop_…:NN是一个标记列表,但你使用的是“字符串”变量(当然,两者都只是宏)。nof\int_set:Nn是一个“int expr”,没有进一步的说明(但不是N)。

    在第一个解决方案(分隔宏)中,我不需要访问任何序列,而在第二个解决方案(重正则表达式)中,我将\seq_item:Nn再次使用。除非您稍后需要再次使用这些值,否则我认为没有必要将它们存储在某个宏/标记列表/计数中。

  3. int 变量是真正的 TeX 计数,\int_set:Nn基本上可以做到#1=\numexpr#2\relax

    因此问题变成了:当其中一个是计数时,比较数字是否更快?如果你每个日期只使用它们一次或两次,这有关系吗?\numexpr无论如何,它首先需要被编辑一次来存储计数。

  4. 我认为更重要的是 20.5 节中的效率说明: \int_compare(_p):nNn(TF)比 – 快五倍\int_compare(_p):n(TF),我假设是因为它只使用 TeX 基础=<和,>并且不必解析和转换!=>=<=

  5. 解析&&和应该||()直接访问函数 And 或 Or 慢。但这将在速度、自然可读性和逻辑之间做出妥协。

    在我的代码中,我使用了简单的二进制 Ors —— 对于&&||不是的惰性求值(第 9.3 节)。

  6. 如果\if_int_compare:w“允许”纯的我将使用 LaTeX3 解决方案\if_case:w来获取月份的最后一天,而不是按序列查找。

    0如果月份数字超出 1-12 的范围,则可以使用最后一天作为月份检查

  7. 您把Ns 和ns 混淆了。

    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 列表的第二项设置为第一个二月,然后它将始终是该值,而特殊值将永远消失。)

  8. 我还采纳了@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}

输出

在此处输入图片描述

相关内容