%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
%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\"
`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)
field-result
(alt-form 0)
(change-case nil)
+ (title-case nil)
(upcase nil)
(flag-pad-with-spaces nil)
(flag-pad-with-zeros nil)
(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)
((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 ?-)
(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)
(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)
(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
((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
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)
(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)
(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
(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))
(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))
(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."
(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."
;; 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."
;; 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
(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 ()
;; 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))
,(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))