% \iffalse meta-comment % % Copyright (C) 2026 by Antoine Missier % % 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} % %<*package> \NeedsTeXFormat{LaTeX2e}[2005/12/01] \ProvidesPackage{textdate} [2026-10-25 v1.0 Displaying dates in textual form] % %<*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} % % \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