% \iffalse meta-comment
%
% Copyright (C) 2026 by Antoine Missier <antoine.missier@ac-toulouse.fr>
%
% This file may be distributed and/or modified under the conditions of
% the LaTeX Project Public License, version 1.3c or later.
%
% This file is part of the textdate package.
% \fi
%
% \iffalse
%<*driver>
\ProvidesFile{textdate.dtx}
%</driver>
%<*package>
\NeedsTeXFormat{LaTeX2e}[2005/12/01]
\ProvidesPackage{textdate}
    [2026-10-25 v1.0 Displaying dates in textual form]
%</package>
%<*driver>
\documentclass{ltxdoc}

\usepackage{hypdoc}
\usepackage{fontspec}
\setmonofont{DejaVu Sans Mono}[Scale=MatchLowercase]
\usepackage[english]{babel}
\usepackage{textdate}
\usepackage{sectsty}
\usepackage{xcolor}

\definecolor{nicebrown}{rgb}{0.5,0.1,0.1}
\allsectionsfont{\color{nicebrown}}
\setlength\marginparsep{10pt}

\DisableCrossrefs
%\CodelineIndex
%\RecordChanges

\hypersetup{%
    colorlinks, 
    linkcolor=blue,
    citecolor=red,   
    pdftitle={textdate}, 
    pdfsubject={LaTeX package}, 
    pdfauthor={Antoine Missier}
}
\GetFileInfo{textdate.sty}

\title{The \textsf{textdate} package%
    \thanks{This document corresponds to \textsf{textdate}~\fileversion, dated \filedate.
      It has been entirely written by ChatGPT under my supervision and after my critical review.
}}
\author{Antoine Missier \\ \texttt{antoine.missier@ac-toulouse.fr}}
\date{\textdate{\filedate}}

\begin{document}

  \DocInput{textdate.dtx}

\end{document}
%</driver>
% \fi
%
% \changes{v1.0}{2026/10/25}{Initial version}
%
% \maketitle
%
% \section{Introduction}
%
% When writing a LaTeX package, the release date is commonly stored in a macro such 
% as |\filedate|, while the date is also displayed in textual form in the title
% of the package documentation. 
% Maintaining both representations manually is tedious and can easily lead 
% to inconsistencies when the package is updated.
%
% Several \LaTeX\ packages provide facilities for formatting dates. 
% In particular, \textsf{datetime2} offers extensive support for dates, times, time zones 
% and regional formats, while \textsf{isodate} provides several date input and output formats.
% These packages are appropriate when date formatting is part of a broader document requirement,
% but they provide considerably more functionality than is needed for this simple use case.
%
% The \textsf{textdate} package provides a deliberately small alternative for 
% this particular purpose. It consists of a single command |\textdate| that converts 
% a numerical date into its textual form, taking the current language into account. 
% The package does not aim to provide a general-purpose date formatting interface;
% its purpose is simply to avoid having to maintain a textual date separately 
% from its numerical representation.
%
% \section{Usage}
%
% \DescribeMacro{\textdate}
% The command
% |\textdate|\marg{date}
% converts a numerical date into textual form. 
% Dates in ISO format can be written using either a hyphen or a slash as separator:
% \begin{center}
% \hfill |\textdate{2026-08-07}| \hfill
% |\textdate{2026/08/07}| \hfill\mbox{}
% \end{center}
% With English as the active language, both produce:
% ``\textdate{2026-08-07}''.
%
% \bigskip
% A numerical date can be stored in a macro, which can be then passed to |\textdate|,
% to produce the same result: ``\textdate{2026-08-07}''.
%
% \begin{verbatim}
%\def\mydate{2026-08-07}
%\textdate{\mydate}
% \end{verbatim}
%
% When the active language uses the day-month-year order, the corresponding
% local input format is also accepted:
%
% \begin{verbatim}
%\usepackage[french]{babel}
%\usepackage{textdate}
%...
%\textdate{01/08/2026} % or \textdate{01-08-2026}
% \end{verbatim}
% which produces: ``1\textsuperscript{er} août 2026''.
%
% \section{Localization and limitations}
%
% The package does not load \textsf{babel} or \textsf{polyglossia}.
% When either package is loaded, the current language is used through
% the \textsf{translations} package. 
% If no supported language is selected, English is used.
%
% The package provides translations for English, French, German, Spanish,
% Italian, Portuguese and Russian. Additional languages can be added by
% declaring the corresponding translation keys.
%
% The package accepts the ISO input order |YYYY-MM-DD| or |YYYY/MM/DD|
% independently of the current language. The local input order |DD-MM-YYYY| or |DD/MM/YYYY| 
% is accepted for all the above predefined languages except English, 
% which uses only the |YYYY-MM-DD| or |YYYY/MM/DD| format.
%
% Date validation is intentionally limited to the format and to basic ranges
% for the day and month. The package does not perform calendar calculations;
% for example, |2026-02-31| has a valid date format but is not detected as
% an invalid calendar date.
%
% \StopEventually{}
%
% \section{Implementation}
%
%    \begin{macrocode}
\RequirePackage{expl3}
\RequirePackage{translations}

\ExplSyntaxOn

% ---------------------------------------------------------------
% Translations
% ---------------------------------------------------------------

% Month names

\DeclareTranslationFallback{textdate-january}{January}
\DeclareTranslationFallback{textdate-february}{February}
\DeclareTranslationFallback{textdate-march}{March}
\DeclareTranslationFallback{textdate-april}{April}
\DeclareTranslationFallback{textdate-may}{May}
\DeclareTranslationFallback{textdate-june}{June}
\DeclareTranslationFallback{textdate-july}{July}
\DeclareTranslationFallback{textdate-august}{August}
\DeclareTranslationFallback{textdate-september}{September}
\DeclareTranslationFallback{textdate-october}{October}
\DeclareTranslationFallback{textdate-november}{November}
\DeclareTranslationFallback{textdate-december}{December}

% French

\DeclareTranslation{French}{textdate-january}{janvier}
\DeclareTranslation{French}{textdate-february}{février}
\DeclareTranslation{French}{textdate-march}{mars}
\DeclareTranslation{French}{textdate-april}{avril}
\DeclareTranslation{French}{textdate-may}{mai}
\DeclareTranslation{French}{textdate-june}{juin}
\DeclareTranslation{French}{textdate-july}{juillet}
\DeclareTranslation{French}{textdate-august}{août}
\DeclareTranslation{French}{textdate-september}{septembre}
\DeclareTranslation{French}{textdate-october}{octobre}
\DeclareTranslation{French}{textdate-november}{novembre}
\DeclareTranslation{French}{textdate-december}{décembre}

% German

\DeclareTranslation{German}{textdate-january}{Januar}
\DeclareTranslation{German}{textdate-february}{Februar}
\DeclareTranslation{German}{textdate-march}{März}
\DeclareTranslation{German}{textdate-april}{April}
\DeclareTranslation{German}{textdate-may}{Mai}
\DeclareTranslation{German}{textdate-june}{Juni}
\DeclareTranslation{German}{textdate-july}{Juli}
\DeclareTranslation{German}{textdate-august}{August}
\DeclareTranslation{German}{textdate-september}{September}
\DeclareTranslation{German}{textdate-october}{Oktober}
\DeclareTranslation{German}{textdate-november}{November}
\DeclareTranslation{German}{textdate-december}{Dezember}

% Spanish

\DeclareTranslation{Spanish}{textdate-january}{enero}
\DeclareTranslation{Spanish}{textdate-february}{febrero}
\DeclareTranslation{Spanish}{textdate-march}{marzo}
\DeclareTranslation{Spanish}{textdate-april}{abril}
\DeclareTranslation{Spanish}{textdate-may}{mayo}
\DeclareTranslation{Spanish}{textdate-june}{junio}
\DeclareTranslation{Spanish}{textdate-july}{julio}
\DeclareTranslation{Spanish}{textdate-august}{agosto}
\DeclareTranslation{Spanish}{textdate-september}{septiembre}
\DeclareTranslation{Spanish}{textdate-october}{octubre}
\DeclareTranslation{Spanish}{textdate-november}{noviembre}
\DeclareTranslation{Spanish}{textdate-december}{diciembre}

% Italian

\DeclareTranslation{Italian}{textdate-january}{gennaio}
\DeclareTranslation{Italian}{textdate-february}{febbraio}
\DeclareTranslation{Italian}{textdate-march}{marzo}
\DeclareTranslation{Italian}{textdate-april}{aprile}
\DeclareTranslation{Italian}{textdate-may}{maggio}
\DeclareTranslation{Italian}{textdate-june}{giugno}
\DeclareTranslation{Italian}{textdate-july}{luglio}
\DeclareTranslation{Italian}{textdate-august}{agosto}
\DeclareTranslation{Italian}{textdate-september}{settembre}
\DeclareTranslation{Italian}{textdate-october}{ottobre}
\DeclareTranslation{Italian}{textdate-november}{novembre}
\DeclareTranslation{Italian}{textdate-december}{dicembre}

% Portuguese

\DeclareTranslation{Portuguese}{textdate-january}{janeiro}
\DeclareTranslation{Portuguese}{textdate-february}{fevereiro}
\DeclareTranslation{Portuguese}{textdate-march}{março}
\DeclareTranslation{Portuguese}{textdate-april}{abril}
\DeclareTranslation{Portuguese}{textdate-may}{maio}
\DeclareTranslation{Portuguese}{textdate-june}{junho}
\DeclareTranslation{Portuguese}{textdate-july}{julho}
\DeclareTranslation{Portuguese}{textdate-august}{agosto}
\DeclareTranslation{Portuguese}{textdate-september}{setembro}
\DeclareTranslation{Portuguese}{textdate-october}{outubro}
\DeclareTranslation{Portuguese}{textdate-november}{novembro}
\DeclareTranslation{Portuguese}{textdate-december}{dezembro}

% Russian

\DeclareTranslation{Russian}{textdate-january}{января}
\DeclareTranslation{Russian}{textdate-february}{февраля}
\DeclareTranslation{Russian}{textdate-march}{марта}
\DeclareTranslation{Russian}{textdate-april}{апреля}
\DeclareTranslation{Russian}{textdate-may}{мая}
\DeclareTranslation{Russian}{textdate-june}{июня}
\DeclareTranslation{Russian}{textdate-july}{июля}
\DeclareTranslation{Russian}{textdate-august}{августа}
\DeclareTranslation{Russian}{textdate-september}{сентября}
\DeclareTranslation{Russian}{textdate-october}{октября}
\DeclareTranslation{Russian}{textdate-november}{ноября}
\DeclareTranslation{Russian}{textdate-december}{декабря}

% Date formats

\DeclareTranslationFallback
  {textdate-format}
  {\textdate@month\space\textdate@day,\space\textdate@year}

\DeclareTranslation{French}
  {textdate-format}
  {\textdate@day\space\textdate@month\space\textdate@year}

\DeclareTranslation{German}
  {textdate-format}
  {\textdate@day.\space\textdate@month\space\textdate@year}

\DeclareTranslation{Spanish}
  {textdate-format}
   {\textdate@day\space de\space\textdate@month\space de\space\textdate@year}

\DeclareTranslation{Italian}
  {textdate-format}
  {\textdate@day\space\textdate@month\space\textdate@year}

\DeclareTranslation{Portuguese}
  {textdate-format}
  {\textdate@day\space de\space\textdate@month\space de\space\textdate@year}

\DeclareTranslation{Russian}
  {textdate-format}
  {\textdate@day\space\textdate@month\space\textdate@year\space г.}

% Date input order

\DeclareTranslationFallback
  {textdate-date-order}
  {YMD}

\DeclareTranslation{French}
  {textdate-date-order}
  {DMY}

\DeclareTranslation{German}
  {textdate-date-order}
  {DMY}

\DeclareTranslation{Spanish}
  {textdate-date-order}
  {DMY}

\DeclareTranslation{Italian}
  {textdate-date-order}
  {DMY}

\DeclareTranslation{Portuguese}
  {textdate-date-order}
  {DMY}

\DeclareTranslation{Russian}
  {textdate-date-order}
  {DMY}

% ---------------------------------------------------------------
% Internal implementation
% ---------------------------------------------------------------

\msg_new:nnn { textdate } { invalid-date-format }
  {
    Invalid~date~format~'#1'.
  }

\msg_new:nnn { textdate } { invalid-date-value }
  {
    Invalid~date~value~'#1'.
  }

\tl_new:N \l_textdate_input_tl
\tl_new:N \l_textdate_year_tl
\tl_new:N \l_textdate_month_tl
\tl_new:N \l_textdate_day_tl
\tl_new:N \l_textdate_month_key_tl

\seq_new:N \l_textdate_date_seq

\int_new:N \l_textdate_month_int
\int_new:N \l_textdate_day_int

\NewDocumentCommand \textdate { m }
  {
    \textdate_parse:n { #1 }
  }

% ---------------------------------------------------------------
% Parsing
% ---------------------------------------------------------------

\cs_new_protected:Npn \textdate_parse:n #1
  {
    \tl_set:Nx \l_textdate_input_tl { #1 }

    \regex_replace_all:nnN
      { / }
      { - }
      \l_textdate_input_tl

    \seq_set_split:NnV
      \l_textdate_date_seq
      { - }
      \l_textdate_input_tl

    \textdate_extract:
  }

\cs_new_protected:Npn \textdate_extract:
  {
    \int_compare:nNnTF
      { \seq_count:N \l_textdate_date_seq }
      =
      { 3 }
      {
        \textdate_extract_check_first:
      }
      {
        \msg_error:nnx
          { textdate }
          { invalid-date-format }
          { \l_textdate_input_tl }
      }
  }

\cs_new_protected:Npn \textdate_extract_check_first:
  {
    \tl_set:Nx \l_tmpa_tl
      {
        \seq_item:Nn \l_textdate_date_seq { 1 }
      }

	\regex_match:nVTF
	  { \A\d{4}\Z }
	  \l_tmpa_tl
	  {
	    \textdate_extract_ymd:
	  }
	  {
	    \textdate_extract_dmy:
	  }
  }

% ---------------------------------------------------------------
% Date extraction
% ---------------------------------------------------------------

\cs_new_protected:Npn \textdate_extract_ymd:
  {
    \tl_set:Nx \l_textdate_year_tl
      {
        \seq_item:Nn \l_textdate_date_seq { 1 }
      }

    \tl_set:Nx \l_textdate_month_tl
      {
        \seq_item:Nn \l_textdate_date_seq { 2 }
      }

    \tl_set:Nx \l_textdate_day_tl
      {
        \seq_item:Nn \l_textdate_date_seq { 3 }
      }

    \textdate_validate:
  }

\cs_new_protected:Npn \textdate_extract_dmy:
  {
    \tl_set:Nx \l_tmpa_tl
      {
        \GetTranslation{textdate-date-order}
      }

    \tl_if_eq:VnTF
      \l_tmpa_tl
      { DMY }
      {
        \tl_set:Nx \l_textdate_day_tl
          {
            \seq_item:Nn \l_textdate_date_seq { 1 }
          }

        \tl_set:Nx \l_textdate_month_tl
          {
            \seq_item:Nn \l_textdate_date_seq { 2 }
          }

        \tl_set:Nx \l_textdate_year_tl
          {
            \seq_item:Nn \l_textdate_date_seq { 3 }
          }

        \textdate_validate:
      }
      {
        \msg_error:nnx
          { textdate }
          { invalid-date-format }
          { \l_textdate_input_tl }
      }
  }

% ---------------------------------------------------------------
% Validation
% ---------------------------------------------------------------

\cs_new_protected:Npn \textdate_validate:
  {
    \regex_match:nVTF
      { \A\d+\Z }
      \l_textdate_year_tl
      {
        \regex_match:nVTF
          { \A\d+\Z }
          \l_textdate_month_tl
          {
            \regex_match:nVTF
              { \A\d+\Z }
              \l_textdate_day_tl
              {
                \int_set:Nn \l_textdate_month_int
                  { \l_textdate_month_tl }

                \int_set:Nn \l_textdate_day_int
                  { \l_textdate_day_tl }

                \int_compare:nNnTF
                  { \l_textdate_month_int }
                  <
                  { 1 }
                  {
                    \msg_error:nnx
                      { textdate }
                      { invalid-date-value }
                      { \l_textdate_input_tl }
                  }
                  {
                    \int_compare:nNnTF
                      { \l_textdate_month_int }
                      >
                      { 12 }
                      {
                        \msg_error:nnx
                          { textdate }
                          { invalid-date-value }
                          { \l_textdate_input_tl }
                      }
                      {
                        \int_compare:nNnTF
                          { \l_textdate_day_int }
                          <
                          { 1 }
                          {
                            \msg_error:nnx
                              { textdate }
                              { invalid-date-value }
                              { \l_textdate_input_tl }
                          }
                          {
                            \int_compare:nNnTF
                              { \l_textdate_day_int }
                              >
                              { 31 }
                              {
                                \msg_error:nnx
                                  { textdate }
                                  { invalid-date-value }
                                  { \l_textdate_input_tl }
                              }
                              {
                                \textdate_print:
                              }
                          }
                      }
                  }
              }
              {
                \msg_error:nnx
                  { textdate }
                  { invalid-date-format }
                  { \l_textdate_input_tl }
              }
          }
          {
            \msg_error:nnx
              { textdate }
              { invalid-date-format }
              { \l_textdate_input_tl }
          }
      }
      {
        \msg_error:nnx
          { textdate }
          { invalid-date-format }
          { \l_textdate_input_tl }
      }
  }

% ---------------------------------------------------------------
% Output
% ---------------------------------------------------------------

\cs_new:Npn \textdate_month_key:n #1
  {
    \int_case:nnF { #1 }
      {
        { 1 }  { january }
        { 2 }  { february }
        { 3 }  { march }
        { 4 }  { april }
        { 5 }  { may }
        { 6 }  { june }
        { 7 }  { july }
        { 8 }  { august }
        { 9 }  { september }
        { 10 } { october }
        { 11 } { november }
        { 12 } { december }
      }
      { }
  }

\cs_new_protected:Npn \textdate_print:
  {
    \tl_set:Nx \l_textdate_month_key_tl
      {
        textdate-
        \textdate_month_key:n
          { \l_textdate_month_int }
      }

    \group_begin:

      \cs_set:Npx \textdate@day
        {
          \int_eval:n { \l_textdate_day_tl }
          \int_compare:nNnT { \l_textdate_day_tl } = { 1 }
            {
              \str_case_e:nn { \languagename }
                {
                  {french} {\exp_not:N \textsuperscript{er}}
                  {italian} {°}
                }
            }
        }

      \cs_set:Npx \textdate@year
        {
          \l_textdate_year_tl
        }

      \cs_set:Npx \textdate@month
        {
          \exp_args:NV \GetTranslation
            \l_textdate_month_key_tl
        }

      \GetTranslation{textdate-format}

    \group_end:
  }

\ExplSyntaxOff
%    \end{macrocode}
%
% \Finale
\endinput