From 64dd2b1a2a0a65a571c2bef5a004fd59cd61bb1e Mon Sep 17 00:00:00 2001 From: Stephen Gildea Date: Mon, 21 Jun 2021 21:28:20 -0700 Subject: [PATCH] time-stamp: add principled, expressive %z * lisp/time-stamp.el (time-stamp-formatz-from-parsed-options): New function for time zone offset formatting ("%z" variants). * test/lisp/time-stamp-tests.el (formatz*): New unit tests to cover the new implementation of %5z. --- lisp/time-stamp.el | 241 +++++++++++++++++--- test/lisp/time-stamp-tests.el | 414 +++++++++++++++++++++++++++++++++- 2 files changed, 624 insertions(+), 31 deletions(-) diff --git a/lisp/time-stamp.el b/lisp/time-stamp.el index 0cc566f0d8c..ae911717151 100644 --- a/lisp/time-stamp.el +++ b/lisp/time-stamp.el @@ -25,7 +25,7 @@ ;; A template in a file can be updated with a new time stamp when ;; you save the file. For example: -;; static char *ts = "sdmain.c Time-stamp: <2001-08-13 10:20:51 gildea>"; +;; static char *ts = "sdmain.c Time-stamp: <2020-04-18 14:10:21 gildea>"; ;; To use time-stamping, add this line to your init file: ;; (add-hook 'before-save-hook 'time-stamp) @@ -278,7 +278,7 @@ look like one of the following: Time-stamp: <> Time-stamp: \" \" The time stamp is written between the brackets or quotes: - Time-stamp: <2001-02-18 10:20:51 gildea> + Time-stamp: <2020-08-07 17:10:21 gildea> The time stamp is updated only if the variable `time-stamp-active' is non-nil. @@ -422,7 +422,7 @@ Returns the end point, which is where `time-stamp' begins the next search." ;;;###autoload (defun time-stamp-toggle-active (&optional arg) "Toggle `time-stamp-active', setting whether \\[time-stamp] updates a buffer. -With ARG, turn time stamping on if and only if arg is positive." +With ARG, turn time stamping on if and only if ARG is positive." (interactive "P") (setq time-stamp-active (if (null arg) @@ -457,7 +457,7 @@ normally the current time is used." (defun time-stamp-string-preprocess (format &optional time) "Use a FORMAT to format date, time, file, and user information. Optional second argument TIME is only for testing. -Implements non-time extensions to `format-time-string' +Implements extensions to `format-time-string' and all `time-stamp-format' compatibility." (let ((fmt-len (length format)) (ind 0) @@ -477,6 +477,9 @@ and all `time-stamp-format' compatibility." (alt-form 0) (change-case nil) (upcase nil) + (flag-pad-with-spaces nil) + (flag-pad-with-zeros nil) + (flag-minimize nil) (paren-level 0)) ;; eat any additional args to allow for future expansion (while (progn @@ -521,10 +524,12 @@ and all `time-stamp-format' compatibility." (setq change-case t)) ((eq cur-char ?^) (setq upcase t)) + ((eq cur-char ?0) + (setq flag-pad-with-zeros t)) ((eq cur-char ?-) - (setq field-width "1")) + (setq field-width "1" flag-minimize t)) ((eq cur-char ?_) - (setq field-width "2")))) + (setq field-width "2" flag-pad-with-spaces t)))) (setq field-result (cond ((eq cur-char ?%) @@ -586,26 +591,37 @@ and all `time-stamp-format' compatibility." ((eq cur-char ?Y) ;4-digit year (string-to-number (time-stamp--format "%Y" time))) ((eq cur-char ?z) ;time zone offset - (if change-case - "" ;discourage %z variations - (cond ((= alt-form 0) - (if (string-equal field-width "") - (progn - (time-stamp-conv-warn "%z" "%#Z") - (time-stamp--format "%#Z" time)) - (cond ((string-equal field-width "1") - (setq field-width "3")) ;%-z -> "+00" - ((string-equal field-width "2") - (setq field-width "5")) ;%_z -> "+0000" - ((string-equal field-width "4") - (setq field-width "0"))) ;discourage %4z - (time-stamp--format "%z" time))) - ((= alt-form 1) - (time-stamp--format "%:z" time)) - ((= alt-form 2) - (time-stamp--format "%::z" time)) - ((= alt-form 3) - (time-stamp--format "%:::z" time))))) + (let ((field-width-num (string-to-number field-width)) + ;; Handle numeric time zone ourselves, because + ;; current-time-zone cannot handle offsets + ;; greater than 24 hours. + (offset-secs + (cond ((numberp time-stamp-time-zone) + time-stamp-time-zone) + ((and (consp time-stamp-time-zone) + (numberp (car time-stamp-time-zone))) + (car time-stamp-time-zone)) + ;; interpret text time zone + (t (car (current-time-zone + time time-stamp-time-zone)))))) + ;; we do our own padding; do not let it be updated further + (setq field-width "") + (cond (change-case + "") ;discourage %z variations + ((and (= alt-form 0) + (not flag-minimize) + (not flag-pad-with-spaces) + (not flag-pad-with-zeros) + (= field-width-num 0)) + (time-stamp-conv-warn "%z" "%#Z") + (time-stamp--format "%#Z" time)) + (t (time-stamp-formatz-from-parsed-options + flag-minimize + flag-pad-with-spaces + flag-pad-with-zeros + alt-form + field-width-num + offset-secs))))) ((eq cur-char ?Z) ;time zone name (if change-case (time-stamp--format "%#Z" time) @@ -653,7 +669,8 @@ and all `time-stamp-format' compatibility." (string-to-number field-width)))) (if (> initial-length desired-length) ;; truncate strings on right - (if (stringp field-result) + (if (and (stringp field-result) + (not (eq cur-char ?z))) ;offset does not truncate (substring padded-result 0 desired-length) padded-result) ;numbers don't truncate padded-result))))) @@ -698,6 +715,176 @@ Suggests replacing OLD-FORM with NEW-FORM." (insert "\"" old-form "\" -- use " new-form "\n")) (display-buffer "*Time-stamp-compatibility*")))) +;;; A principled, expressive implementation of time zone offset +;;; formatting ("%z" and variants). + +;;; * Overarching principle for %z + +;; The output should be clear and complete. +;; +;; That is, +;; a) it should be unambiguous what offset is represented, and +;; b) it should be possible to exactly recreate the offset. + +;;; * Principles for %z + +;; - The numeric fields are HHMMSS. +;; - The fixed point is at the left. The first 2 digits are always +;; hours, the next 2 (if they exist) minutes, and next 2 (if they +;; exist) seconds. "+11" is 11 hours (not 11 minutes, not 11 seconds). +;; "+1015" is 10 hours 15 minutes (not 10 minutes 15 seconds). +;; - Each of the three numeric fields is two digits. +;; "+1" and "+100" are illegal. (Is that 1 hour? 10 hours? 100 hours?) +;; - The MMSS fields may be omitted only if both are 00. Thus, the width +;; of the field depends on the data. (This is similar to how +;; %B is always long enough to spell the entire month name.) +;; - The SS field may be omitted only if it is 00. +;; - Colons between the numeric fields are an option, unless the hours +;; field is greater than 99, when colons are needed to prevent ambiguity. +;; - If padding with zeros, we must pad on the right, because the +;; fixed point is at the left. (This is similar to how %N, +;; fractional seconds, must add its zeros on the right.) +;; - After zero-padding has filled out minutes and seconds with zeros, +;; further padding can be blanks only. +;; Any additional zeros would be confusing. + +;;; * Padding for %z + +;; Padding is under-specified, so we had to make choices. +;; +;; Principles guiding our choices: +;; +;; - The syntax should be easy to remember and the effect predictable. +;; - It should be possible to produces as many useful effects as possible. +;; +;; Padding choices: +;; +;; - By default, pad with spaces, as other formats with non-digits do. +;; The "0" flag pads first with zeros, until seconds are filled out. +;; - If padding with spaces, pad on the right. This is consistent with +;; how zero-padding works. Padding on the right also keeps the fixed +;; point in the same place, as other formats do for any given width. +;; - The %_z format always outputs seconds, allowing all added padding +;; to be spaces. Without this rule, there would be no way to +;; request seconds that worked for both 2- and 3-digit hours. +;; - Conflicting options are rejected, lest users depend +;; on incidental behavior. +;; +;; Padding combos that make no sense and are thus disallowed: +;; +;; %-:z - minus minimizes to hours, : expands to minutes +;; %-::z - minus minimizes to hours, :: expands to seconds +;; %_:z - underscore requires seconds, : displays minutes +;; %_:::z - underscore requires seconds, ::: minimizes to hours +;; +;; Example padding effects (with offsets of 99 and 100 hours): +;; +;; %-7z "+99 " "+100:00" +;; %7z "+9900 " "+100:00" +;; %07z "+990000" "+100:00" +;; %_7z "+990000" "+100:00:00" +;; +;; %7:::z "+99 " "+100:00" +;; %7:z "+99:00 " "+100:00" +;; %07:z "+99:00:00" "+100:00" +;; %7::z "+99:00:00" "+100:00:00" + +;;; * BNF syntax of the offset string produced by %z + +;; ::= [[]] | +;; [[]] | +;; [] +;; ::= "+"|"-" +;; ::= <2digits> +;; ::= <2digits> +;; ::= <2digits> +;; ::= ":" +;; ::= ":" +;; <2digits> ::= +;; ::= "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9" +;; ::= *<2digits> +;; ::= " "* + +(defun time-stamp-formatz-from-parsed-options (flag-minimize + flag-pad-spaces-only + flag-pad-zeros-first + colon-count + field-width + offset-secs) + "Formats a time offset according to a %z variation. +The caller of this function must have already parsed the %z format +string; this function accepts just the parts of the format. + +With no flags, the output includes hours and minutes: +-HHMM +unless there is a non-zero seconds part, in which case the seconds +are included: +-HHMMSS + +FLAG-MINIMIZE is whether \"-\" was specified. If non-nil, the +output may be limited to hours if minutes and seconds are zero. + +FLAG-PAD-SPACES-ONLY is whether \"_\" was specified. If non-nil, +seconds must be output, so that any padding can be spaces only. + +FLAG-PAD-ZEROS-FIRST is whether \"0\" was specified. If non-nil, +padding to the requested FIELD-WIDTH (if any) is done by adding +00 seconds before padding with spaces. + +COLON-COUNT is the number of colons preceding the \"z\" (0-3). One or +two colons put that many colons in the output (+-HH:MM or +-HH:MM:SS). +Three colons outputs only hours if minutes and seconds are zero and +includes colon separators if minutes and seconds are output. + +FIELD-WIDTH is a whole number giving the minimum number of characters +in the output; 0 specifies no minimum. Additional characters will be +added on the right if necessary. The added characters will be spaces +unless FLAG-PAD-ZEROS-FIRST is non-nil. + +OFFSET-SECS is the time zone offset (in seconds east of UTC) to be +formatted according to the preceding parameters." + (let ((hrs (/ (abs offset-secs) 3600)) + (mins (/ (% (abs offset-secs) 3600) 60)) + (secs (% (abs offset-secs) 60)) + (result "")) + ;; valid option combo? + (cond + ((not (or (and flag-minimize (> colon-count 0)) + (and flag-pad-spaces-only (> colon-count 0)) + (and flag-pad-spaces-only flag-minimize) + (and flag-pad-spaces-only flag-pad-zeros-first) + (and flag-pad-zeros-first flag-minimize))) + (setq result (concat result (if (>= offset-secs 0) "+" "-"))) + (setq result (concat result (format "%02d" hrs))) + ;; Need minutes? + (cond + ((or (> hrs 99) + (> mins 0) + (> secs 0) + (not (or flag-minimize (= colon-count 3))) + (and (> field-width (length result)) + flag-pad-zeros-first)) + ;; Need colon before minutes? + (if (or (> colon-count 0) + (> hrs 99)) + (setq result (concat result ":"))) + (setq result (concat result (format "%02d" mins))) + ;; Need seconds, too? + (cond + ((or (> secs 0) + (= colon-count 2) + flag-pad-spaces-only + (and (> field-width (length result)) + flag-pad-zeros-first)) + ;; Need colon before seconds? + (if (or (> colon-count 0) + (> hrs 99)) + (setq result (concat result ":"))) + (setq result (concat result (format "%02d" secs))))))) + ;; Need padding? + (let ((needed-padding (- field-width (length result)))) + (if (> needed-padding 0) + (setq result (concat result (make-string needed-padding ?\s))))))) + result)) + (provide 'time-stamp) ;;; time-stamp.el ends here diff --git a/test/lisp/time-stamp-tests.el b/test/lisp/time-stamp-tests.el index b42271e4e51..e42a58a1685 100644 --- a/test/lisp/time-stamp-tests.el +++ b/test/lisp/time-stamp-tests.el @@ -525,7 +525,7 @@ (should (equal (time-stamp-string "%#Z" ref-time1) utc-abbr))))) (ert-deftest time-stamp-format-time-zone-offset () - "Tests time-stamp legacy format %z and new offset format %5z." + "Tests time-stamp legacy format %z and spot tests of new offset format %5z." (with-time-stamp-test-env (let ((utc-abbr (format-time-string "%#Z" ref-time1 t))) ;; documented 1995-2019, warned since 2019, will change @@ -540,8 +540,9 @@ (let ((time-stamp-time-zone "CET-1")) (should (equal (time-stamp-string "%5z" ref-time1) "+0100"))) ;; implemented since 2019, verify that these don't warn + ;; See also the "formatz" tests below, which since 2021 test more + ;; variants with more offsets. (should (equal (time-stamp-string "%-z" ref-time1) "+00")) - (should (equal (time-stamp-string "%_z" ref-time1) "+0000")) (should (equal (time-stamp-string "%:z" ref-time1) "+00:00")) (should (equal (time-stamp-string "%::z" ref-time1) "+00:00:00")) (should (equal (time-stamp-string "%9::z" ref-time1) "+00:00:00")) @@ -615,16 +616,24 @@ (concat Mon "." MON "." Mon))) ;; underscore flag is independent (should (equal (time-stamp-string "%_d.%d.%_d" ref-time1) " 2.02. 2")) - ;; minus flag is independendent + (should (equal (time-stamp-string "%_7z.%7z.%_7z" ref-time1) + "+000000.+0000 .+000000")) + ;; minus flag is independent (should (equal (time-stamp-string "%d.%-d.%d" ref-time1) "02.2.02")) - ;; 0 flag is independendent + (should (equal (time-stamp-string "%3z.%-3z.%3z" ref-time1) + "+0000.+00.+0000")) + ;; 0 flag is independent (should (equal (time-stamp-string "%2d.%02d.%2d" ref-time1) " 2.02. 2")) + (should (equal (time-stamp-string "%6:::z.%06:::z.%6:::z" ref-time1) + "+00 .+00:00.+00 ")) ;; field width is independent (should (equal (time-stamp-string "%6Y.%Y.%6Y" ref-time1) " 2006.2006. 2006")) ;; colon modifier is independent (should (equal (time-stamp-string "%a.%:a.%a" ref-time1) (concat Mon "." Monday "." Mon))) + (should (equal (time-stamp-string "%5z.%5::z.%5z" ref-time1) + "+0000.+00:00:00.+0000")) ;; format letter is independent (should (equal (time-stamp-string "%H:%M" ref-time1) "15:04"))))) @@ -691,4 +700,401 @@ (should (safe-local-variable-p 'time-stamp-pattern "a string")) (should-not (safe-local-variable-p 'time-stamp-pattern 17))) +;;;; Setup for tests of time offset formatting with %z + +(defun formatz (format zone) + "Uses time FORMAT string to format the offset of ZONE, returning the result. +FORMAT is \"%z\" or a variation. +ZONE is as the ZONE argument of the `format-time-string' function." + (with-time-stamp-test-env + (let ((time-stamp-time-zone zone)) + ;; Call your favorite time formatter here. + ;; For narrower-scope unit testing, + ;; instead of calling time-stamp-string here, + ;; we could directly call (format-time-offset format zone) + (time-stamp-string format) + ))) + +(defun format-time-offset (format offset-secs) + "Uses FORMAT to format the time zone represented by OFFSET-SECS. +FORMAT must be \"%z\", possibly with a flag and padding. +This function is a wrapper around `time-stamp-formatz-from-parsed-options' +and is used for testing." + ;; This wrapper adds a simple regexp-based parser that handles only + ;; %z and variants. In normal use, time-stamp-formatz-from-parsed-options + ;; is called from a parser that handles all time string formats. + (string-match + "\\`\\([^%]*\\)%\\([-_]?\\)\\(0?\\)\\([1-9][0-9]*\\)?\\([EO]?\\)\\(:*\\)\\([^a-zA-Z]+\\)?z\\(.*\\)" + format) + (let ((leading-string (match-string 1 format)) + (flag-minimize (seq-find (lambda (x) (eq x ?-)) + (match-string 2 format))) + (flag-pad-with-spaces (seq-find (lambda (x) (eq x ?_)) + (match-string 2 format))) + (flag-pad-with-zeros (equal (match-string 3 format) "0")) + (field-width (string-to-number (or (match-string 4 format) ""))) + (colon-count (length (match-string 6 format))) + (garbage (match-string 7 format)) + (trailing-string (match-string 8 format))) + (concat leading-string + (if garbage + "" + (time-stamp-formatz-from-parsed-options flag-minimize + flag-pad-with-spaces + flag-pad-with-zeros + colon-count + field-width + offset-secs)) + trailing-string))) + +(defun fz-make+zone (h &optional m s) + "Creates a non-negative offset." + (let ((m (or m 0)) + (s (or s 0))) + (+ (* 3600 h) (* 60 m) s))) + +(defun fz-make-zone (h &optional m s) + "Creates a negative offset. The arguments are all non-negative." + (- (fz-make+zone h m s))) + +(defmacro formatz-should-equal (zone expect) + "Formats ZONE and compares it to EXPECT. +Uses the free variables `form-string' and `pattern-mod'. +The functions in `pattern-mod' are composed left to right." + `(let ((result ,expect)) + (dolist (fn pattern-mod) + (setq result (funcall fn result))) + (should (equal (formatz form-string ,zone) result)))) + +;; These test cases have zeros in all places (first, last, none, both) +;; for hours, minutes, and seconds. + +(defun formatz-hours-exact-helper (form-string pattern-mod) + "Tests format %z with whole hours." + (formatz-should-equal (fz-make+zone 0) "+00") ;0 sign always +, both digits + (formatz-should-equal (fz-make+zone 10) "+10") + (formatz-should-equal (fz-make-zone 10) "-10") + (formatz-should-equal (fz-make+zone 2) "+02") + (formatz-should-equal (fz-make-zone 2) "-02") + (formatz-should-equal (fz-make+zone 13) "+13") + (formatz-should-equal (fz-make-zone 13) "-13") + ) + +(defun formatz-nonzero-minutes-helper (form-string pattern-mod) + "Tests format %z with whole minutes." + (formatz-should-equal (fz-make+zone 0 30) "+00:30") ;has hours even though 0 + (formatz-should-equal (fz-make-zone 0 30) "-00:30") + (formatz-should-equal (fz-make+zone 0 4) "+00:04") + (formatz-should-equal (fz-make-zone 0 4) "-00:04") + (formatz-should-equal (fz-make+zone 8 40) "+08:40") + (formatz-should-equal (fz-make-zone 8 40) "-08:40") + (formatz-should-equal (fz-make+zone 0 15) "+00:15") + (formatz-should-equal (fz-make-zone 0 15) "-00:15") + (formatz-should-equal (fz-make+zone 11 30) "+11:30") + (formatz-should-equal (fz-make-zone 11 30) "-11:30") + (formatz-should-equal (fz-make+zone 3 17) "+03:17") + (formatz-should-equal (fz-make-zone 3 17) "-03:17") + (formatz-should-equal (fz-make+zone 12 45) "+12:45") + (formatz-should-equal (fz-make-zone 12 45) "-12:45") + ) + +(defun formatz-nonzero-seconds-helper (form-string pattern-mod) + "Tests format %z with non-0 seconds." + ;; non-0 seconds are always included + (formatz-should-equal (fz-make+zone 0 0 50) "+00:00:50") + (formatz-should-equal (fz-make-zone 0 0 50) "-00:00:50") + (formatz-should-equal (fz-make+zone 0 0 06) "+00:00:06") + (formatz-should-equal (fz-make-zone 0 0 06) "-00:00:06") + (formatz-should-equal (fz-make+zone 0 7 50) "+00:07:50") + (formatz-should-equal (fz-make-zone 0 7 50) "-00:07:50") + (formatz-should-equal (fz-make+zone 0 0 16) "+00:00:16") + (formatz-should-equal (fz-make-zone 0 0 16) "-00:00:16") + (formatz-should-equal (fz-make+zone 0 12 36) "+00:12:36") + (formatz-should-equal (fz-make-zone 0 12 36) "-00:12:36") + (formatz-should-equal (fz-make+zone 0 3 45) "+00:03:45") + (formatz-should-equal (fz-make-zone 0 3 45) "-00:03:45") + (formatz-should-equal (fz-make+zone 8 45 30) "+08:45:30") + (formatz-should-equal (fz-make-zone 8 45 30) "-08:45:30") + (formatz-should-equal (fz-make+zone 0 11 45) "+00:11:45") + (formatz-should-equal (fz-make-zone 0 11 45) "-00:11:45") + (formatz-should-equal (fz-make+zone 3 20 15) "+03:20:15") + (formatz-should-equal (fz-make-zone 3 20 15) "-03:20:15") + (formatz-should-equal (fz-make+zone 11 14 30) "+11:14:30") + (formatz-should-equal (fz-make-zone 11 14 30) "-11:14:30") + (formatz-should-equal (fz-make+zone 12 30 49) "+12:30:49") + (formatz-should-equal (fz-make-zone 12 30 49) "-12:30:49") + (formatz-should-equal (fz-make+zone 12 0 34) "+12:00:34") + (formatz-should-equal (fz-make-zone 12 0 34) "-12:00:34") + ) + +(defun formatz-hours-big-helper (form-string pattern-mod) + "Tests format %z with hours that don't fit in two digits." + (formatz-should-equal (fz-make+zone 101) "+101:00") + (formatz-should-equal (fz-make+zone 123 10) "+123:10") + (formatz-should-equal (fz-make-zone 123 10) "-123:10") + (formatz-should-equal (fz-make+zone 123 2) "+123:02") + (formatz-should-equal (fz-make-zone 123 2) "-123:02") + ) + +(defun formatz-seconds-big-helper (form-string pattern-mod) + "Tests format %z with hours greater than 99 and non-zero seconds." + (formatz-should-equal (fz-make+zone 123 0 30) "+123:00:30") + (formatz-should-equal (fz-make-zone 123 0 30) "-123:00:30") + (formatz-should-equal (fz-make+zone 120 0 4) "+120:00:04") + (formatz-should-equal (fz-make-zone 120 0 4) "-120:00:04") + ) + +;; Functions that modify the expected output string, so that we can +;; use the above test cases for multiple formats. + +(defun formatz-mod-del-colons (string) + "Returns STRING with any colons removed." + (replace-regexp-in-string ":" "" string)) + +(defun formatz-mod-add-00 (string) + "Returns STRING with \"00\" appended." + (concat string "00")) + +(defun formatz-mod-add-colon00 (string) + "Returns STRING with \":00\" appended." + (concat string ":00")) + +(defun formatz-mod-pad-r10 (string) + "Returns STRING padded on the right to 10 characters." + (concat string (make-string (- 10 (length string)) ?\s))) + +(defun formatz-mod-pad-r12 (string) + "Returns STRING padded on the right to 12 characters." + (concat string (make-string (- 12 (length string)) ?\s))) + +;; Convenience macro for generating groups of test cases. + +(defmacro formatz-generate-tests + (form-strings hour-mod mins-mod secs-mod big-mod secbig-mod) + "Defines ert-deftest tests for time formats FORM-STRINGS. +FORM-STRINGS is a list of formats, each \"%z\" or some variation thereof. + +Each of the remaining arguments is an unquoted list of the form +(SAMPLE-OUTPUT . MODIFIERS). SAMPLE-OUTPUT is the result of the +FORM-STRINGS for a particular offset, detailed below for each argument. +The remaining elements of the list, the MODIFIERS, are the names of +functions to modify the expected results for sets of tests. +The MODIFIERS do not modify the SAMPLE-OUTPUT. + +The one, literal sample output is given in the call to this macro +to provide a visual check at the call site that the format +behaves as expected. + +HOUR-MOD is the result for offset 0 and modifiers for the other +expected results for whole hours. +MINS-MOD is the result for offset +30 minutes and modifiers for the +other expected results for whole minutes. +SECS-MOD is the result for offset +30 seconds and modifiers for the +other expected results for offsets with non-zero seconds. +BIG-MOD is the result for offset +100 hours and modifiers for the other +expected results for hours greater than 99 with a whole number of minutes. +SECBIG-MOD is the result for offset +100 hours 30 seconds and modifiers for +the other expected results for hours greater than 99 with non-zero seconds." + (declare (indent 1)) + ;; Generate a form to create a list of tests to define. When this + ;; macro is called, the form is evaluated, thus defining the tests. + (let ((ert-test-list '(list))) + (dolist (form-string form-strings ert-test-list) + (nconc + ert-test-list + (list + `(ert-deftest ,(intern (concat "formatz-" form-string "-hhmm")) () + (should (equal (formatz ,form-string (fz-make+zone 0)) + ,(car hour-mod))) + (formatz-hours-exact-helper ,form-string ',(cdr hour-mod)) + (should (equal (formatz ,form-string (fz-make+zone 0 30)) + ,(car mins-mod))) + (formatz-nonzero-minutes-helper ,form-string ',(cdr mins-mod))) + `(ert-deftest ,(intern (concat "formatz-" form-string "-secs")) () + (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 "-big")) () + (should (equal (formatz ,form-string (fz-make+zone 100)) + ,(car big-mod))) + (formatz-hours-big-helper ,form-string ',(cdr big-mod)) + (should (equal (formatz ,form-string (fz-make+zone 100 0 30)) + ,(car secbig-mod))) + (formatz-seconds-big-helper ,form-string ',(cdr secbig-mod))) + ))))) + +;;;; The actual test cases for %z + +;;; %z formats without colons. + +;; Option character "-" (minus) minimizes; it removes "00" minutes. +(formatz-generate-tests ("%-z" "%-3z") + ("+00") + ("+0030" formatz-mod-del-colons) + ("+000030" formatz-mod-del-colons) + ("+100:00") + ("+100:00:30")) +;; Tests that minus with padding pads with spaces. +(formatz-generate-tests ("%-12z") + ("+00 " formatz-mod-pad-r12) + ("+0030 " formatz-mod-del-colons formatz-mod-pad-r12) + ("+000030 " formatz-mod-del-colons formatz-mod-pad-r12) + ("+100:00 " formatz-mod-pad-r12) + ("+100:00:30 " formatz-mod-pad-r12)) +;; Tests that 0 after other digits becomes padding of ten, not zero flag. +(formatz-generate-tests ("%-10z") + ("+00 " formatz-mod-pad-r10) + ("+0030 " formatz-mod-del-colons formatz-mod-pad-r10) + ("+000030 " formatz-mod-del-colons formatz-mod-pad-r10) + ("+100:00 " formatz-mod-pad-r10) + ("+100:00:30")) + +;; Although time-stamp doesn't call us for %z, we do want to spot-check +;; it here, to verify the implementation we will eventually use. +;; The legacy exception for %z in time-stamp will need to remain +;; through at least 2024 and Emacs 28. +(ert-deftest formatz-%z-spotcheck () + (should (equal (format-time-offset "%z" (fz-make+zone 0)) "+0000")) + (should (equal (format-time-offset "%z" (fz-make+zone 0 30)) "+0030")) + (should (equal (format-time-offset "%z" (fz-make+zone 0 0 30)) "+000030")) + (should (equal (format-time-offset "%z" (fz-make+zone 100)) "+100:00")) + (should (equal (format-time-offset "%z" (fz-make+zone 100 0 30)) "+100:00:30")) + ) + +;; Basic %z outputs 4 digits. +;; Small padding values do not extend the result. +(formatz-generate-tests (;; We don't check %z here because time-stamp + ;; has a legacy behavior for it. + ;;"%z" + "%5z" "%0z" "%05z") + ("+0000" formatz-mod-add-00) + ("+0030" formatz-mod-del-colons) + ("+000030" formatz-mod-del-colons) + ("+100:00") + ("+100:00:30")) + +;; Tests that padding adds spaces. +(formatz-generate-tests ("%12z") + ("+0000 " formatz-mod-add-00 formatz-mod-pad-r12) + ("+0030 " formatz-mod-del-colons formatz-mod-pad-r12) + ("+000030 " formatz-mod-del-colons formatz-mod-pad-r12) + ("+100:00 " formatz-mod-pad-r12) + ("+100:00:30 " formatz-mod-pad-r12)) + +;; Requiring 0-padding to 6 adds seconds (only) as needed. +(formatz-generate-tests ("%06z") + ("+000000" formatz-mod-add-00 formatz-mod-add-00) + ("+003000" formatz-mod-del-colons formatz-mod-add-00) + ("+000030" formatz-mod-del-colons) + ("+100:00") + ("+100:00:30")) + +;; Option character "_" always adds seconds. +(formatz-generate-tests ("%_z" "%_7z") + ("+000000" formatz-mod-add-00 formatz-mod-add-00) + ("+003000" formatz-mod-del-colons formatz-mod-add-00) + ("+000030" formatz-mod-del-colons) + ("+100:00:00" formatz-mod-add-colon00) + ("+100:00:30")) + +;; Enough 0-padding adds seconds, then adds spaces. +(formatz-generate-tests ("%012z" "%_12z") + ("+000000 " formatz-mod-add-00 formatz-mod-add-00 formatz-mod-pad-r12) + ("+003000 " formatz-mod-del-colons formatz-mod-add-00 formatz-mod-pad-r12) + ("+000030 " formatz-mod-del-colons formatz-mod-pad-r12) + ("+100:00:00 " formatz-mod-add-colon00 formatz-mod-pad-r12) + ("+100:00:30 " formatz-mod-pad-r12)) + +;;; %z formats with colons + +;; Three colons can output hours only, +;; like %-z, but uses colons with non-zero minutes and seconds. +(formatz-generate-tests ("%:::z" "%0:::z" + "%3:::z" "%03:::z") + ("+00") + ("+00:30") + ("+00:00:30") + ("+100:00") + ("+100:00:30")) + +;; Padding with three colons adds spaces +(formatz-generate-tests ("%12:::z") + ("+00 " formatz-mod-pad-r12) + ("+00:30 " formatz-mod-pad-r12) + ("+00:00:30 " formatz-mod-pad-r12) + ("+100:00 " formatz-mod-pad-r12) + ("+100:00:30 " formatz-mod-pad-r12)) +;; Tests that 0 after other digits becomes padding of ten, not zero flag. +(formatz-generate-tests ("%10:::z") + ("+00 " formatz-mod-pad-r10) + ("+00:30 " formatz-mod-pad-r10) + ("+00:00:30 " formatz-mod-pad-r10) + ("+100:00 " formatz-mod-pad-r10) + ("+100:00:30")) + +;; One colon outputs minutes, like %z but with colon. +(formatz-generate-tests ("%:z" "%6:z" "%0:z" "%06:z" "%06:::z") + ("+00:00" formatz-mod-add-colon00) + ("+00:30") + ("+00:00:30") + ("+100:00") + ("+100:00:30")) + +;; Padding with one colon adds spaces +(formatz-generate-tests ("%12:z") + ("+00:00 " formatz-mod-add-colon00 formatz-mod-pad-r12) + ("+00:30 " formatz-mod-pad-r12) + ("+00:00:30 " formatz-mod-pad-r12) + ("+100:00 " formatz-mod-pad-r12) + ("+100:00:30 " formatz-mod-pad-r12)) + +;; Requiring 0-padding to 7 adds seconds (only) as needed. +(formatz-generate-tests ("%07:z" "%07:::z") + ("+00:00:00" formatz-mod-add-colon00 formatz-mod-add-colon00) + ("+00:30:00" formatz-mod-add-colon00) + ("+00:00:30") + ("+100:00") + ("+100:00:30")) + +;; Two colons outputs HH:MM:SS, like %_z but with colons. +(formatz-generate-tests ("%::z" "%9::z" "%0::z" "%09::z") + ("+00:00:00" formatz-mod-add-colon00 formatz-mod-add-colon00) + ("+00:30:00" formatz-mod-add-colon00) + ("+00:00:30") + ("+100:00:00" formatz-mod-add-colon00) + ("+100:00:30")) + +;; Enough padding adds minutes and seconds, then adds spaces. +(formatz-generate-tests ("%012:z" "%012::z" "%12::z" "%012:::z") + ("+00:00:00 " formatz-mod-add-colon00 formatz-mod-add-colon00 + formatz-mod-pad-r12) + ("+00:30:00 " formatz-mod-add-colon00 formatz-mod-pad-r12) + ("+00:00:30 " formatz-mod-pad-r12) + ("+100:00:00 " formatz-mod-add-colon00 formatz-mod-pad-r12) + ("+100:00:30 " formatz-mod-pad-r12)) + +;;; Illegal %z formats + +(ert-deftest formatz-illegal-options () + "Tests that illegal/nonsensical/ambiguous %z formats don't produce output." + ;; multiple options + (should (equal "" (formatz "%_-z" 0))) + (should (equal "" (formatz "%-_z" 0))) + (should (equal "" (formatz "%_0z" 0))) + (should (equal "" (formatz "%0_z" 0))) + (should (equal "" (formatz "%0-z" 0))) + (should (equal "" (formatz "%-0z" 0))) + ;; inconsistent to both minimize and require mins or secs + (should (equal "" (formatz "%-:z" 0))) + (should (equal "" (formatz "%-::z" 0))) + ;; consistent, but redundant + (should (equal "" (formatz "%-:::z" 0))) + (should (equal "" (formatz "%_::z" 0))) + ;; inconsistent to both pre-expand and default to hours or mins + (should (equal "" (formatz "%_:::z" 0))) + (should (equal "" (formatz "%_:z" 0))) + ;; options that don't make sense with %z + (should (equal "" (formatz "%#z" 0))) + ) + ;;; time-stamp-tests.el ends here -- 2.39.2