From: Stephen Gildea Date: Mon, 6 Jan 2025 19:00:07 +0000 (-0800) Subject: time-stamp: lower- and title-case weekday and month names X-Git-Url: http://git.eshelyaron.com/gitweb/?a=commitdiff_plain;h=ff5e0d8f0e1f0b2e9eb792080092a84496824304;p=emacs.git time-stamp: lower- and title-case weekday and month names * lisp/time-stamp.el (time-stamp-string-preprocess): new formats for lowercase weekday name, lowercase month name, and system name * test/lisp/time-stamp.el: new tests (formatz-generate-tests): Better doc strings for generated test fns. (cherry picked from commit 3de5fcd0a60b01639276f360561037403f8d7389) --- diff --git a/lisp/time-stamp.el b/lisp/time-stamp.el index 0ef85da6374..fb3c6cb81da 100644 --- a/lisp/time-stamp.el +++ b/lisp/time-stamp.el @@ -52,7 +52,7 @@ with %, which are converted as follows: %H 24-hour clock hour %I 12-hour clock hour %m month number %M minute -%p `AM' or `PM' +%p meridian indicator: `AM', `PM' %S seconds %w day number of week, Sunday is 0 %Y 4-digit year %y 2-digit year @@ -67,16 +67,29 @@ Non-date items: %q unqualified host name %Q fully-qualified host name %h mail host name -A \"#\" after the % changes the case of letters. For example, on Mondays, -in the default locale, \"%#A\" converts to \"MONDAY\". +The % may be followed by a modifier affecting the letter case. +The modifier \"#\" changes the case of letters, usually to uppercase, +or if the word is already uppercase, to lowercase. +The modifier \"^\" converts letters to uppercase; +\"^\" may be followed by \"#\" to convert to lowercase. +The modifier \"*\" converts words to title case (capitalized). + +Here are some example conversions on Mondays, in two locales: + + English French +%A Monday lundi +%^A MONDAY LUNDI +%^#A monday lundi +%*A Monday Lundi Decimal digits before the type character specify the minimum field width. A \"0\" before the field width adds insignificant zeroes as appropriate, otherwise the padding is done with spaces. -If no padding is specified, a field that can be one or two digits is padded -with \"0\" to two digits if necessary. Follow the % with \"_\" to pad with a -space instead, or follow it with \"-\" to suppress this padding entirely. +If no padding is specified, a field that can be one or two digits is +padded with \"0\" to two digits if necessary. Follow the % with \"_\" +to pad with a space instead, or follow it with \"-\" to suppress this +padding entirely. Thus, on the 5th of the month, the day is converted as follows: \"%d\" -> \"05\" @@ -92,10 +105,11 @@ The examples here are for the default (\"C\") locale. `time-stamp-time-zone' controls the time zone used. Some of the conversions recommended here work only in Emacs 27 or later. +The title-case and lowercase modifiers work only in Emacs 31 or later. If your files might be edited by older versions of Emacs also, you should limit yourself to the formats recommended by that older version." :type 'string - :version "27.1") + :version "31.1") ;;;###autoload(put 'time-stamp-format 'safe-local-variable 'stringp) @@ -515,6 +529,7 @@ and all `time-stamp-format' compatibility." field-result (alt-form 0) (change-case nil) + (title-case nil) (upcase nil) (flag-pad-with-spaces nil) (flag-pad-with-zeros nil) @@ -526,7 +541,7 @@ and all `time-stamp-format' compatibility." (setq cur-char (if (< ind fmt-len) (aref format ind) ?\0)) - (or (eq ?. cur-char) (eq ?* cur-char) + (or (eq ?. cur-char) (eq ?~ cur-char) (eq ?* cur-char) (eq ?E cur-char) (eq ?O cur-char) (eq ?, cur-char) (eq ?: cur-char) (eq ?@ cur-char) (eq ?- cur-char) (eq ?+ cur-char) (eq ?_ cur-char) @@ -563,7 +578,9 @@ and all `time-stamp-format' compatibility." ((eq cur-char ?#) (setq change-case t)) ((eq cur-char ?^) - (setq upcase t)) + (setq upcase t title-case nil change-case nil)) + ((eq cur-char ?*) + (setq title-case t upcase nil change-case nil)) ((eq cur-char ?0) (setq flag-pad-with-zeros t)) ((eq cur-char ?-) @@ -572,18 +589,18 @@ and all `time-stamp-format' compatibility." (setq field-width "2" flag-pad-with-spaces t)))) (if (> (string-to-number field-width) 99) (setq field-width (if flag-pad-with-zeros "099" "99"))) - (setq field-result - (cond + (setq field-result + (cond ((eq cur-char ?%) "%") ((eq cur-char ?a) ;day of week - (if (> alt-form 0) - (if (string-equal field-width "") - (time-stamp--format "%A" time) - "") ;discourage "%:3a" - (if (or change-case upcase) - (time-stamp--format "%#a" time) - (time-stamp--format "%a" time)))) + (time-stamp-do-letter-case + nil upcase title-case change-case + (if (> alt-form 0) + (if (string-equal field-width "") + (time-stamp--format "%A" time) + "") ;discourage "%:3a" + (time-stamp--format "%a" time)))) ((eq cur-char ?A) (if (and (>= (string-to-number field-width) 1) (<= (string-to-number field-width) 3) @@ -592,24 +609,25 @@ and all `time-stamp-format' compatibility." (progn (time-stamp-conv-warn "%3A" "%#a") (time-stamp--format "%#a" time)) - (if (or change-case upcase) - (time-stamp--format "%#A" time) - (if (or (> alt-form 0) - flag-minimize flag-pad-with-spaces - (string-equal field-width "")) - (time-stamp--format "%A" time) - (time-stamp-conv-warn (format "%%%sA" field-width) - (format "%%#%sA" field-width) - (format "%%:%sA" field-width)) - (time-stamp--format "%#A" time))))) + (if (or (> alt-form 0) + change-case upcase title-case + flag-minimize flag-pad-with-spaces + (string-equal field-width "")) + (time-stamp-do-letter-case + nil upcase title-case change-case + (time-stamp--format "%A" time)) + (time-stamp-conv-warn (format "%%%sA" field-width) + (format "%%#%sA" field-width) + (format "%%:%sA" field-width)) + (time-stamp--format "%#A" time)))) ((eq cur-char ?b) ;month name - (if (> alt-form 0) - (if (string-equal field-width "") - (time-stamp--format "%B" time) - "") ;discourage "%:3b" - (if (or change-case upcase) - (time-stamp--format "%#b" time) - (time-stamp--format "%b" time)))) + (time-stamp-do-letter-case + nil upcase title-case change-case + (if (> alt-form 0) + (if (string-equal field-width "") + (time-stamp--format "%B" time) + "") ;discourage "%:3b" + (time-stamp--format "%b" time)))) ((eq cur-char ?B) (if (and (>= (string-to-number field-width) 1) (<= (string-to-number field-width) 3) @@ -618,16 +636,17 @@ and all `time-stamp-format' compatibility." (progn (time-stamp-conv-warn "%3B" "%#b") (time-stamp--format "%#b" time)) - (if (or change-case upcase) - (time-stamp--format "%#B" time) - (if (or (> alt-form 0) - flag-minimize flag-pad-with-spaces - (string-equal field-width "")) - (time-stamp--format "%B" time) - (time-stamp-conv-warn (format "%%%sB" field-width) - (format "%%#%sB" field-width) - (format "%%:%sB" field-width)) - (time-stamp--format "%#B" time))))) + (if (or (> alt-form 0) + change-case upcase title-case + flag-minimize flag-pad-with-spaces + (string-equal field-width "")) + (time-stamp-do-letter-case + nil upcase title-case change-case + (time-stamp--format "%B" time)) + (time-stamp-conv-warn (format "%%%sB" field-width) + (format "%%#%sB" field-width) + (format "%%:%sB" field-width)) + (time-stamp--format "%#B" time)))) ((eq cur-char ?d) ;day of month, 1-31 (time-stamp-do-number cur-char alt-form field-width time)) ((eq cur-char ?H) ;hour, 0-23 @@ -639,17 +658,15 @@ and all `time-stamp-format' compatibility." ((eq cur-char ?M) ;minute, 0-59 (time-stamp-do-number cur-char alt-form field-width time)) ((eq cur-char ?p) ;AM or PM - (if change-case - (time-stamp--format "%#p" time) - (if upcase - (time-stamp--format "%^p" time) - (time-stamp--format "%p" time)))) + (time-stamp-do-letter-case + t upcase title-case change-case + (time-stamp--format "%p" time))) ((eq cur-char ?P) ;AM or PM - (if change-case - (time-stamp--format "%#p" time) - (if upcase - "" ;discourage inconsistent "%^P" - (time-stamp--format "%p" time)))) + (if (and upcase (not change-case)) + "" ;discourage inconsistent "%^P" + (time-stamp-do-letter-case + t upcase title-case change-case + (time-stamp--format "%p" time)))) ((eq cur-char ?S) ;seconds, 00-60 (time-stamp-do-number cur-char alt-form field-width time)) ((eq cur-char ?w) ;weekday number, Sunday is 0 @@ -699,9 +716,9 @@ and all `time-stamp-format' compatibility." field-width-num offset-secs))))) ((eq cur-char ?Z) ;time zone name - (if change-case - (time-stamp--format "%#Z" time) - (time-stamp--format "%Z" time))) + (time-stamp-do-letter-case + t upcase title-case change-case + (time-stamp--format "%Z" time))) ((eq cur-char ?f) ;buffer-file-name, base name only (if buffer-file-name (file-name-nondirectory buffer-file-name) @@ -724,12 +741,14 @@ and all `time-stamp-format' compatibility." (user-full-name)) ((eq cur-char ?h) ;mail host name (or mail-host-address (system-name))) - ((eq cur-char ?q) ;unqualified host name - (let ((qualname (system-name))) - (if (string-match "\\." qualname) - (substring qualname 0 (match-beginning 0)) - qualname))) - ((eq cur-char ?Q) ;fully-qualified host name + ((or (eq cur-char ?q) ;unqualified host name + (eq cur-char ?x)) ;short system name, experimental + (let ((shortname (system-name))) + (if (string-match "\\." shortname) + (substring shortname 0 (match-beginning 0)) + shortname))) + ((or (eq cur-char ?Q) ;fully-qualified host name + (eq cur-char ?X)) ;full system name, experimental (system-name)) )) (and (numberp field-result) @@ -747,6 +766,28 @@ and all `time-stamp-format' compatibility." (setq ind (1+ ind))) result)) +(defun time-stamp-do-letter-case (change-is-downcase + upcase title-case change-case text) + "Apply upper- and lower-case conversions to TEXT according to the flags. +CHANGE-IS-DOWNCASE non-nil indicates that modifier CHANGE-CASE requests +lowercase, otherwise the modifier requests uppercase. +UPCASE is non-nil if the \"^\" modifier is active. +TITLE-CASE is non-nil if the \"*\" modifier is active. +CHANGE-CASE is non-nil if the \"#\" modifier is active. +This is an internal helper for `time-stamp-string-preprocess'." + (cond ((and upcase change-case) + (downcase text)) + ((and title-case change-case) + (upcase text)) + ((and change-is-downcase change-case) + (downcase text)) + ((or change-case upcase) + (upcase text)) + (title-case + (capitalize text)) + (t + text))) + (defun time-stamp-do-number (format-char alt-form field-width time) "Handle compatible FORMAT-CHAR where only default width/padding will change. ALT-FORM is whether `#' was specified. FIELD-WIDTH is the string diff --git a/test/lisp/time-stamp-tests.el b/test/lisp/time-stamp-tests.el index ddf17645f46..6858990b982 100644 --- a/test/lisp/time-stamp-tests.el +++ b/test/lisp/time-stamp-tests.el @@ -293,8 +293,12 @@ (with-time-stamp-test-env (let* ((Mon (format-time-string "%a" ref-time1 t)) (MON (format-time-string "%^a" ref-time1 t)) + (mon (downcase (format-time-string "%a" ref-time1 t))) + (Mon-tc (capitalize (format-time-string "%a" ref-time1 t))) (Monday (format-time-string "%A" ref-time1 t)) (MONDAY (format-time-string "%^A" ref-time1 t)) + (monday (downcase (format-time-string "%A" ref-time1 t))) + (Monday-tc (capitalize (format-time-string "%A" ref-time1 t))) (p4-Mon (string-pad Mon 4 ?\s t)) (p4-MON (string-pad MON 4 ?\s t)) (p10-Monday (string-pad Monday 10 ?\s t)) @@ -331,15 +335,27 @@ (should (equal (time-stamp-string "%A" ref-time1) Monday)) ;; warned 1997-2019, changed in 2019 (should (equal (time-stamp-string "%^a" ref-time1) MON)) - (should (equal (time-stamp-string "%^4a" ref-time1) p4-MON))))) + (should (equal (time-stamp-string "%^4a" ref-time1) p4-MON)) + ;; implemented since 2025 + (should (equal (time-stamp-string "%^#A" ref-time1) monday)) + (should (equal (time-stamp-string "%^#a" ref-time1) mon)) + (should (equal (time-stamp-string "%*A" ref-time1) Monday-tc)) + (should (equal (time-stamp-string "%*a" ref-time1) Mon-tc)) + ;; discouraged + (should (equal (time-stamp-string "%:3a" ref-time1) " ")) + ))) (ert-deftest time-stamp-format-month-name () "Test `time-stamp' formats for month name." (with-time-stamp-test-env (let* ((Jan (format-time-string "%b" ref-time1 t)) (JAN (format-time-string "%^b" ref-time1 t)) + (jan (downcase (format-time-string "%b" ref-time1 t))) + (Jan-tc (capitalize (format-time-string "%^b" ref-time1 t))) (January (format-time-string "%B" ref-time1 t)) (JANUARY (format-time-string "%^B" ref-time1 t)) + (january (downcase (format-time-string "%B" ref-time1 t))) + (January-tc (capitalize (format-time-string "%B" ref-time1 t))) (p4-Jan (string-pad Jan 4 ?\s t)) (p4-JAN (string-pad JAN 4 ?\s t)) (p10-January (string-pad January 10 ?\s t)) @@ -376,7 +392,15 @@ (should (equal (time-stamp-string "%B" ref-time1) January)) ;; warned 1997-2019, changed in 2019 (should (equal (time-stamp-string "%^b" ref-time1) JAN)) - (should (equal (time-stamp-string "%^4b" ref-time1) p4-JAN))))) + (should (equal (time-stamp-string "%^4b" ref-time1) p4-JAN)) + ;; implemented since 2025 + (should (equal (time-stamp-string "%^#B" ref-time1) january)) + (should (equal (time-stamp-string "%^#b" ref-time1) jan)) + (should (equal (time-stamp-string "%*B" ref-time1) January-tc)) + (should (equal (time-stamp-string "%*b" ref-time1) Jan-tc)) + ;; discouraged + (should (equal (time-stamp-string "%:3b" ref-time1) " ")) + ))) (ert-deftest time-stamp-format-day-of-month () "Test `time-stamp' formats for day of month." @@ -399,7 +423,10 @@ (should (equal (time-stamp-string "%_d" ref-time1) " 2")) (should (equal (time-stamp-string "%_d" ref-time2) "18")) (should (equal (time-stamp-string "%d" ref-time1) "02")) - (should (equal (time-stamp-string "%d" ref-time2) "18")))) + (should (equal (time-stamp-string "%d" ref-time2) "18")) + ;; discouraged + (should (equal (time-stamp-string "%:2d" ref-time1) " ")) + )) (ert-deftest time-stamp-format-hours-24 () "Test `time-stamp' formats for hour on a 24-hour clock." @@ -591,12 +618,25 @@ ;; changed in 2024 (should (equal (time-stamp-string "%^p" ref-time1) PM)) (should (equal (time-stamp-string "%^p" ref-time3) AM)) + (should (equal (time-stamp-string "%#^p" ref-time1) PM)) + (should (equal (time-stamp-string "%#^p" ref-time3) AM)) (should (equal (time-stamp-string "%#P" ref-time1) pm)) (should (equal (time-stamp-string "%#P" ref-time3) am)) (should (equal (time-stamp-string "%^#P" ref-time1) pm)) (should (equal (time-stamp-string "%^#P" ref-time3) am)) (should (equal (time-stamp-string "%^P" ref-time1) "")) - (should (equal (time-stamp-string "%^P" ref-time3) ""))))) + (should (equal (time-stamp-string "%^P" ref-time3) "")) + ;; reserved for possible adding or removing periods (dots) + (should (equal (time-stamp-string "%:p" ref-time1) Pm)) + (should (equal (time-stamp-string "%#:p" ref-time1) pm)) + (should (equal (time-stamp-string "%^:p" ref-time1) PM)) + (should (equal (time-stamp-string "%.p" ref-time1) Pm)) + (should (equal (time-stamp-string "%#.p" ref-time1) pm)) + (should (equal (time-stamp-string "%^.p" ref-time1) PM)) + (should (equal (time-stamp-string "%@p" ref-time1) Pm)) + (should (equal (time-stamp-string "%#@p" ref-time1) pm)) + (should (equal (time-stamp-string "%^@p" ref-time1) PM)) + ))) (ert-deftest time-stamp-format-day-number-in-week () "Test `time-stamp' formats for day number in week." @@ -674,17 +714,26 @@ ;; implemented since 2007, recommended since 2019 (should (equal (time-stamp-string "%Q" ref-time1) "test-system-name.example.org")) - (should (equal (time-stamp-string "%q" ref-time1) "test-system-name"))) + (should (equal (time-stamp-string "%q" ref-time1) "test-system-name")) + ;; implemented since 2025 + (should (equal (time-stamp-string "%X" ref-time1) + "test-system-name.example.org")) + (should (equal (time-stamp-string "%x" ref-time1) "test-system-name"))) (with-time-stamp-system-name "sysname-no-dots" + ;; implemented since 2007, recommended since 2019 (should (equal (time-stamp-string "%Q" ref-time1) "sysname-no-dots")) - (should (equal (time-stamp-string "%q" ref-time1) "sysname-no-dots"))))) + (should (equal (time-stamp-string "%q" ref-time1) "sysname-no-dots")) + ;; implemented since 2025 + (should (equal (time-stamp-string "%X" ref-time1) "sysname-no-dots")) + (should (equal (time-stamp-string "%x" ref-time1) "sysname-no-dots")) + ))) (ert-deftest time-stamp-format-ignored-modifiers () "Test additional args allowed (but ignored) to allow for future expansion." (with-time-stamp-test-env (let ((May (format-time-string "%B" ref-time3 t))) ;; allowed modifiers - (should (equal (time-stamp-string "%.,@+*EO (stuff)B" ref-time3) May)) + (should (equal (time-stamp-string "%.,@+~EO (stuff)B" ref-time3) May)) ;; parens nest (should (equal (time-stamp-string "%(st(u)ff)B" ref-time3) May)) ;; escaped parens do not change the nesting level @@ -761,6 +810,14 @@ (should (equal (time-stamp-string "%-Z" ref-time1) UTC-abbr)) (should (equal (time-stamp-string "%_Z" ref-time1) UTC-abbr))))) +(ert-deftest time-stamp-format-letter-case () + "Test `time-stamp' upcase and downcase modifiers not tested elsewhere." + (with-time-stamp-test-env + (let ((MONDAY (format-time-string "%^A" ref-time1 t))) + (should (equal (time-stamp-string "%*^A" ref-time1) MONDAY)) + (should (equal (time-stamp-string "%*#A" ref-time1) MONDAY)) + ))) + ;;; Tests of helper functions (ert-deftest time-stamp-helper-string-defaults () @@ -1008,14 +1065,19 @@ the other expected results for hours greater than 99 with non-zero seconds." ;; Generate a form to create a list of tests to define. When this ;; macro is called, the form is evaluated, thus defining the tests. ;; We will modify this list, so start with a list consed at runtime. - (let ((ert-test-list (list 'list))) + (let ((ert-test-list (list 'list)) + (common-description + (concat "\nThis test expands from a call to" + " the macro `formatz-generate-tests'.\n" + "To find the specific call, search the source file for \""))) (dolist (form-string form-strings ert-test-list) (nconc ert-test-list (list `(ert-deftest ,(intern (concat "formatz-" form-string "-hhmm")) () - ,(concat "Test time-stamp format " form-string - " with whole hours or minutes.") + ,(concat "Test `time-stamp' format " form-string + " with whole hours and whole minutes.\n" + common-description form-string "\".") (should (equal (formatz ,form-string (fz-make+zone 0)) ,(car hour-mod))) (formatz-hours-exact-helper ,form-string ',(cdr hour-mod)) @@ -1023,14 +1085,16 @@ the other expected results for hours greater than 99 with non-zero seconds." ,(car mins-mod))) (formatz-nonzero-minutes-helper ,form-string ',(cdr mins-mod))) `(ert-deftest ,(intern (concat "formatz-" form-string "-seconds")) () - ,(concat "Test time-stamp format " form-string - " with offsets that have non-zero seconds.") + ,(concat "Test `time-stamp' format " form-string + " with offsets that have non-zero seconds.\n" + common-description form-string "\".") (should (equal (formatz ,form-string (fz-make+zone 0 0 30)) ,(car secs-mod))) (formatz-nonzero-seconds-helper ,form-string ',(cdr secs-mod))) `(ert-deftest ,(intern (concat "formatz-" form-string "-threedigit")) () - ,(concat "Test time-stamp format " form-string - " with offsets that are 100 hours or greater.") + ,(concat "Test `time-stamp' format " form-string + " with offsets of 100 hours or greater.\n" + common-description form-string "\".") (should (equal (formatz ,form-string (fz-make+zone 100)) ,(car big-mod))) (formatz-hours-big-helper ,form-string ',(cdr big-mod))