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.
This commit is contained in:
Stephen Gildea 2021-06-21 21:28:20 -07:00
parent 3b1d69efc3
commit 64dd2b1a2a
2 changed files with 624 additions and 31 deletions

View file

@ -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
;; <offset> ::= <sign><hours>[<minutes>[<seconds>]]<padding> |
;; <sign><hours>[<colonminutes>[<colonseconds>]]<padding> |
;; <sign><bighours><colonminutes>[<colonseconds>]<padding>
;; <sign> ::= "+"|"-"
;; <hours> ::= <2digits>
;; <minutes> ::= <2digits>
;; <seconds> ::= <2digits>
;; <colonminutes> ::= ":"<minutes>
;; <colonseconds> ::= ":"<seconds>
;; <2digits> ::= <digit><digit>
;; <digit> ::= "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"
;; <bighours> ::= <digit>*<digit><2digits>
;; <padding> ::= " "*
(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

View file

@ -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