aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStephen Gildea2021-06-21 21:28:20 -0700
committerStephen Gildea2021-06-21 21:30:19 -0700
commit64dd2b1a2a0a65a571c2bef5a004fd59cd61bb1e (patch)
tree506e5d745e616e3b71ff316c63fbaba6a7ff63d1
parent3b1d69efc32c8929281f38d55cef773e4680f2ad (diff)
downloademacs-64dd2b1a2a0a65a571c2bef5a004fd59cd61bb1e.tar.gz
emacs-64dd2b1a2a0a65a571c2bef5a004fd59cd61bb1e.zip
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.
-rw-r--r--lisp/time-stamp.el241
-rw-r--r--test/lisp/time-stamp-tests.el414
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 @@
25 25
26;; A template in a file can be updated with a new time stamp when 26;; A template in a file can be updated with a new time stamp when
27;; you save the file. For example: 27;; you save the file. For example:
28;; static char *ts = "sdmain.c Time-stamp: <2001-08-13 10:20:51 gildea>"; 28;; static char *ts = "sdmain.c Time-stamp: <2020-04-18 14:10:21 gildea>";
29 29
30;; To use time-stamping, add this line to your init file: 30;; To use time-stamping, add this line to your init file:
31;; (add-hook 'before-save-hook 'time-stamp) 31;; (add-hook 'before-save-hook 'time-stamp)
@@ -278,7 +278,7 @@ look like one of the following:
278 Time-stamp: <> 278 Time-stamp: <>
279 Time-stamp: \" \" 279 Time-stamp: \" \"
280The time stamp is written between the brackets or quotes: 280The time stamp is written between the brackets or quotes:
281 Time-stamp: <2001-02-18 10:20:51 gildea> 281 Time-stamp: <2020-08-07 17:10:21 gildea>
282 282
283The time stamp is updated only if the variable 283The time stamp is updated only if the variable
284`time-stamp-active' is non-nil. 284`time-stamp-active' is non-nil.
@@ -422,7 +422,7 @@ Returns the end point, which is where `time-stamp' begins the next search."
422;;;###autoload 422;;;###autoload
423(defun time-stamp-toggle-active (&optional arg) 423(defun time-stamp-toggle-active (&optional arg)
424 "Toggle `time-stamp-active', setting whether \\[time-stamp] updates a buffer. 424 "Toggle `time-stamp-active', setting whether \\[time-stamp] updates a buffer.
425With ARG, turn time stamping on if and only if arg is positive." 425With ARG, turn time stamping on if and only if ARG is positive."
426 (interactive "P") 426 (interactive "P")
427 (setq time-stamp-active 427 (setq time-stamp-active
428 (if (null arg) 428 (if (null arg)
@@ -457,7 +457,7 @@ normally the current time is used."
457(defun time-stamp-string-preprocess (format &optional time) 457(defun time-stamp-string-preprocess (format &optional time)
458 "Use a FORMAT to format date, time, file, and user information. 458 "Use a FORMAT to format date, time, file, and user information.
459Optional second argument TIME is only for testing. 459Optional second argument TIME is only for testing.
460Implements non-time extensions to `format-time-string' 460Implements extensions to `format-time-string'
461and all `time-stamp-format' compatibility." 461and all `time-stamp-format' compatibility."
462 (let ((fmt-len (length format)) 462 (let ((fmt-len (length format))
463 (ind 0) 463 (ind 0)
@@ -477,6 +477,9 @@ and all `time-stamp-format' compatibility."
477 (alt-form 0) 477 (alt-form 0)
478 (change-case nil) 478 (change-case nil)
479 (upcase nil) 479 (upcase nil)
480 (flag-pad-with-spaces nil)
481 (flag-pad-with-zeros nil)
482 (flag-minimize nil)
480 (paren-level 0)) 483 (paren-level 0))
481 ;; eat any additional args to allow for future expansion 484 ;; eat any additional args to allow for future expansion
482 (while (progn 485 (while (progn
@@ -521,10 +524,12 @@ and all `time-stamp-format' compatibility."
521 (setq change-case t)) 524 (setq change-case t))
522 ((eq cur-char ?^) 525 ((eq cur-char ?^)
523 (setq upcase t)) 526 (setq upcase t))
527 ((eq cur-char ?0)
528 (setq flag-pad-with-zeros t))
524 ((eq cur-char ?-) 529 ((eq cur-char ?-)
525 (setq field-width "1")) 530 (setq field-width "1" flag-minimize t))
526 ((eq cur-char ?_) 531 ((eq cur-char ?_)
527 (setq field-width "2")))) 532 (setq field-width "2" flag-pad-with-spaces t))))
528 (setq field-result 533 (setq field-result
529 (cond 534 (cond
530 ((eq cur-char ?%) 535 ((eq cur-char ?%)
@@ -586,26 +591,37 @@ and all `time-stamp-format' compatibility."
586 ((eq cur-char ?Y) ;4-digit year 591 ((eq cur-char ?Y) ;4-digit year
587 (string-to-number (time-stamp--format "%Y" time))) 592 (string-to-number (time-stamp--format "%Y" time)))
588 ((eq cur-char ?z) ;time zone offset 593 ((eq cur-char ?z) ;time zone offset
589 (if change-case 594 (let ((field-width-num (string-to-number field-width))
590 "" ;discourage %z variations 595 ;; Handle numeric time zone ourselves, because
591 (cond ((= alt-form 0) 596 ;; current-time-zone cannot handle offsets
592 (if (string-equal field-width "") 597 ;; greater than 24 hours.
593 (progn 598 (offset-secs
594 (time-stamp-conv-warn "%z" "%#Z") 599 (cond ((numberp time-stamp-time-zone)
595 (time-stamp--format "%#Z" time)) 600 time-stamp-time-zone)
596 (cond ((string-equal field-width "1") 601 ((and (consp time-stamp-time-zone)
597 (setq field-width "3")) ;%-z -> "+00" 602 (numberp (car time-stamp-time-zone)))
598 ((string-equal field-width "2") 603 (car time-stamp-time-zone))
599 (setq field-width "5")) ;%_z -> "+0000" 604 ;; interpret text time zone
600 ((string-equal field-width "4") 605 (t (car (current-time-zone
601 (setq field-width "0"))) ;discourage %4z 606 time time-stamp-time-zone))))))
602 (time-stamp--format "%z" time))) 607 ;; we do our own padding; do not let it be updated further
603 ((= alt-form 1) 608 (setq field-width "")
604 (time-stamp--format "%:z" time)) 609 (cond (change-case
605 ((= alt-form 2) 610 "") ;discourage %z variations
606 (time-stamp--format "%::z" time)) 611 ((and (= alt-form 0)
607 ((= alt-form 3) 612 (not flag-minimize)
608 (time-stamp--format "%:::z" time))))) 613 (not flag-pad-with-spaces)
614 (not flag-pad-with-zeros)
615 (= field-width-num 0))
616 (time-stamp-conv-warn "%z" "%#Z")
617 (time-stamp--format "%#Z" time))
618 (t (time-stamp-formatz-from-parsed-options
619 flag-minimize
620 flag-pad-with-spaces
621 flag-pad-with-zeros
622 alt-form
623 field-width-num
624 offset-secs)))))
609 ((eq cur-char ?Z) ;time zone name 625 ((eq cur-char ?Z) ;time zone name
610 (if change-case 626 (if change-case
611 (time-stamp--format "%#Z" time) 627 (time-stamp--format "%#Z" time)
@@ -653,7 +669,8 @@ and all `time-stamp-format' compatibility."
653 (string-to-number field-width)))) 669 (string-to-number field-width))))
654 (if (> initial-length desired-length) 670 (if (> initial-length desired-length)
655 ;; truncate strings on right 671 ;; truncate strings on right
656 (if (stringp field-result) 672 (if (and (stringp field-result)
673 (not (eq cur-char ?z))) ;offset does not truncate
657 (substring padded-result 0 desired-length) 674 (substring padded-result 0 desired-length)
658 padded-result) ;numbers don't truncate 675 padded-result) ;numbers don't truncate
659 padded-result))))) 676 padded-result)))))
@@ -698,6 +715,176 @@ Suggests replacing OLD-FORM with NEW-FORM."
698 (insert "\"" old-form "\" -- use " new-form "\n")) 715 (insert "\"" old-form "\" -- use " new-form "\n"))
699 (display-buffer "*Time-stamp-compatibility*")))) 716 (display-buffer "*Time-stamp-compatibility*"))))
700 717
718;;; A principled, expressive implementation of time zone offset
719;;; formatting ("%z" and variants).
720
721;;; * Overarching principle for %z
722
723;; The output should be clear and complete.
724;;
725;; That is,
726;; a) it should be unambiguous what offset is represented, and
727;; b) it should be possible to exactly recreate the offset.
728
729;;; * Principles for %z
730
731;; - The numeric fields are HHMMSS.
732;; - The fixed point is at the left. The first 2 digits are always
733;; hours, the next 2 (if they exist) minutes, and next 2 (if they
734;; exist) seconds. "+11" is 11 hours (not 11 minutes, not 11 seconds).
735;; "+1015" is 10 hours 15 minutes (not 10 minutes 15 seconds).
736;; - Each of the three numeric fields is two digits.
737;; "+1" and "+100" are illegal. (Is that 1 hour? 10 hours? 100 hours?)
738;; - The MMSS fields may be omitted only if both are 00. Thus, the width
739;; of the field depends on the data. (This is similar to how
740;; %B is always long enough to spell the entire month name.)
741;; - The SS field may be omitted only if it is 00.
742;; - Colons between the numeric fields are an option, unless the hours
743;; field is greater than 99, when colons are needed to prevent ambiguity.
744;; - If padding with zeros, we must pad on the right, because the
745;; fixed point is at the left. (This is similar to how %N,
746;; fractional seconds, must add its zeros on the right.)
747;; - After zero-padding has filled out minutes and seconds with zeros,
748;; further padding can be blanks only.
749;; Any additional zeros would be confusing.
750
751;;; * Padding for %z
752
753;; Padding is under-specified, so we had to make choices.
754;;
755;; Principles guiding our choices:
756;;
757;; - The syntax should be easy to remember and the effect predictable.
758;; - It should be possible to produces as many useful effects as possible.
759;;
760;; Padding choices:
761;;
762;; - By default, pad with spaces, as other formats with non-digits do.
763;; The "0" flag pads first with zeros, until seconds are filled out.
764;; - If padding with spaces, pad on the right. This is consistent with
765;; how zero-padding works. Padding on the right also keeps the fixed
766;; point in the same place, as other formats do for any given width.
767;; - The %_z format always outputs seconds, allowing all added padding
768;; to be spaces. Without this rule, there would be no way to
769;; request seconds that worked for both 2- and 3-digit hours.
770;; - Conflicting options are rejected, lest users depend
771;; on incidental behavior.
772;;
773;; Padding combos that make no sense and are thus disallowed:
774;;
775;; %-:z - minus minimizes to hours, : expands to minutes
776;; %-::z - minus minimizes to hours, :: expands to seconds
777;; %_:z - underscore requires seconds, : displays minutes
778;; %_:::z - underscore requires seconds, ::: minimizes to hours
779;;
780;; Example padding effects (with offsets of 99 and 100 hours):
781;;
782;; %-7z "+99 " "+100:00"
783;; %7z "+9900 " "+100:00"
784;; %07z "+990000" "+100:00"
785;; %_7z "+990000" "+100:00:00"
786;;
787;; %7:::z "+99 " "+100:00"
788;; %7:z "+99:00 " "+100:00"
789;; %07:z "+99:00:00" "+100:00"
790;; %7::z "+99:00:00" "+100:00:00"
791
792;;; * BNF syntax of the offset string produced by %z
793
794;; <offset> ::= <sign><hours>[<minutes>[<seconds>]]<padding> |
795;; <sign><hours>[<colonminutes>[<colonseconds>]]<padding> |
796;; <sign><bighours><colonminutes>[<colonseconds>]<padding>
797;; <sign> ::= "+"|"-"
798;; <hours> ::= <2digits>
799;; <minutes> ::= <2digits>
800;; <seconds> ::= <2digits>
801;; <colonminutes> ::= ":"<minutes>
802;; <colonseconds> ::= ":"<seconds>
803;; <2digits> ::= <digit><digit>
804;; <digit> ::= "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"
805;; <bighours> ::= <digit>*<digit><2digits>
806;; <padding> ::= " "*
807
808(defun time-stamp-formatz-from-parsed-options (flag-minimize
809 flag-pad-spaces-only
810 flag-pad-zeros-first
811 colon-count
812 field-width
813 offset-secs)
814 "Formats a time offset according to a %z variation.
815The caller of this function must have already parsed the %z format
816string; this function accepts just the parts of the format.
817
818With no flags, the output includes hours and minutes: +-HHMM
819unless there is a non-zero seconds part, in which case the seconds
820are included: +-HHMMSS
821
822FLAG-MINIMIZE is whether \"-\" was specified. If non-nil, the
823output may be limited to hours if minutes and seconds are zero.
824
825FLAG-PAD-SPACES-ONLY is whether \"_\" was specified. If non-nil,
826seconds must be output, so that any padding can be spaces only.
827
828FLAG-PAD-ZEROS-FIRST is whether \"0\" was specified. If non-nil,
829padding to the requested FIELD-WIDTH (if any) is done by adding
83000 seconds before padding with spaces.
831
832COLON-COUNT is the number of colons preceding the \"z\" (0-3). One or
833two colons put that many colons in the output (+-HH:MM or +-HH:MM:SS).
834Three colons outputs only hours if minutes and seconds are zero and
835includes colon separators if minutes and seconds are output.
836
837FIELD-WIDTH is a whole number giving the minimum number of characters
838in the output; 0 specifies no minimum. Additional characters will be
839added on the right if necessary. The added characters will be spaces
840unless FLAG-PAD-ZEROS-FIRST is non-nil.
841
842OFFSET-SECS is the time zone offset (in seconds east of UTC) to be
843formatted according to the preceding parameters."
844 (let ((hrs (/ (abs offset-secs) 3600))
845 (mins (/ (% (abs offset-secs) 3600) 60))
846 (secs (% (abs offset-secs) 60))
847 (result ""))
848 ;; valid option combo?
849 (cond
850 ((not (or (and flag-minimize (> colon-count 0))
851 (and flag-pad-spaces-only (> colon-count 0))
852 (and flag-pad-spaces-only flag-minimize)
853 (and flag-pad-spaces-only flag-pad-zeros-first)
854 (and flag-pad-zeros-first flag-minimize)))
855 (setq result (concat result (if (>= offset-secs 0) "+" "-")))
856 (setq result (concat result (format "%02d" hrs)))
857 ;; Need minutes?
858 (cond
859 ((or (> hrs 99)
860 (> mins 0)
861 (> secs 0)
862 (not (or flag-minimize (= colon-count 3)))
863 (and (> field-width (length result))
864 flag-pad-zeros-first))
865 ;; Need colon before minutes?
866 (if (or (> colon-count 0)
867 (> hrs 99))
868 (setq result (concat result ":")))
869 (setq result (concat result (format "%02d" mins)))
870 ;; Need seconds, too?
871 (cond
872 ((or (> secs 0)
873 (= colon-count 2)
874 flag-pad-spaces-only
875 (and (> field-width (length result))
876 flag-pad-zeros-first))
877 ;; Need colon before seconds?
878 (if (or (> colon-count 0)
879 (> hrs 99))
880 (setq result (concat result ":")))
881 (setq result (concat result (format "%02d" secs)))))))
882 ;; Need padding?
883 (let ((needed-padding (- field-width (length result))))
884 (if (> needed-padding 0)
885 (setq result (concat result (make-string needed-padding ?\s)))))))
886 result))
887
701(provide 'time-stamp) 888(provide 'time-stamp)
702 889
703;;; time-stamp.el ends here 890;;; 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 @@
525 (should (equal (time-stamp-string "%#Z" ref-time1) utc-abbr))))) 525 (should (equal (time-stamp-string "%#Z" ref-time1) utc-abbr)))))
526 526
527(ert-deftest time-stamp-format-time-zone-offset () 527(ert-deftest time-stamp-format-time-zone-offset ()
528 "Tests time-stamp legacy format %z and new offset format %5z." 528 "Tests time-stamp legacy format %z and spot tests of new offset format %5z."
529 (with-time-stamp-test-env 529 (with-time-stamp-test-env
530 (let ((utc-abbr (format-time-string "%#Z" ref-time1 t))) 530 (let ((utc-abbr (format-time-string "%#Z" ref-time1 t)))
531 ;; documented 1995-2019, warned since 2019, will change 531 ;; documented 1995-2019, warned since 2019, will change
@@ -540,8 +540,9 @@
540 (let ((time-stamp-time-zone "CET-1")) 540 (let ((time-stamp-time-zone "CET-1"))
541 (should (equal (time-stamp-string "%5z" ref-time1) "+0100"))) 541 (should (equal (time-stamp-string "%5z" ref-time1) "+0100")))
542 ;; implemented since 2019, verify that these don't warn 542 ;; implemented since 2019, verify that these don't warn
543 ;; See also the "formatz" tests below, which since 2021 test more
544 ;; variants with more offsets.
543 (should (equal (time-stamp-string "%-z" ref-time1) "+00")) 545 (should (equal (time-stamp-string "%-z" ref-time1) "+00"))
544 (should (equal (time-stamp-string "%_z" ref-time1) "+0000"))
545 (should (equal (time-stamp-string "%:z" ref-time1) "+00:00")) 546 (should (equal (time-stamp-string "%:z" ref-time1) "+00:00"))
546 (should (equal (time-stamp-string "%::z" ref-time1) "+00:00:00")) 547 (should (equal (time-stamp-string "%::z" ref-time1) "+00:00:00"))
547 (should (equal (time-stamp-string "%9::z" ref-time1) "+00:00:00")) 548 (should (equal (time-stamp-string "%9::z" ref-time1) "+00:00:00"))
@@ -615,16 +616,24 @@
615 (concat Mon "." MON "." Mon))) 616 (concat Mon "." MON "." Mon)))
616 ;; underscore flag is independent 617 ;; underscore flag is independent
617 (should (equal (time-stamp-string "%_d.%d.%_d" ref-time1) " 2.02. 2")) 618 (should (equal (time-stamp-string "%_d.%d.%_d" ref-time1) " 2.02. 2"))
618 ;; minus flag is independendent 619 (should (equal (time-stamp-string "%_7z.%7z.%_7z" ref-time1)
620 "+000000.+0000 .+000000"))
621 ;; minus flag is independent
619 (should (equal (time-stamp-string "%d.%-d.%d" ref-time1) "02.2.02")) 622 (should (equal (time-stamp-string "%d.%-d.%d" ref-time1) "02.2.02"))
620 ;; 0 flag is independendent 623 (should (equal (time-stamp-string "%3z.%-3z.%3z" ref-time1)
624 "+0000.+00.+0000"))
625 ;; 0 flag is independent
621 (should (equal (time-stamp-string "%2d.%02d.%2d" ref-time1) " 2.02. 2")) 626 (should (equal (time-stamp-string "%2d.%02d.%2d" ref-time1) " 2.02. 2"))
627 (should (equal (time-stamp-string "%6:::z.%06:::z.%6:::z" ref-time1)
628 "+00 .+00:00.+00 "))
622 ;; field width is independent 629 ;; field width is independent
623 (should (equal 630 (should (equal
624 (time-stamp-string "%6Y.%Y.%6Y" ref-time1) " 2006.2006. 2006")) 631 (time-stamp-string "%6Y.%Y.%6Y" ref-time1) " 2006.2006. 2006"))
625 ;; colon modifier is independent 632 ;; colon modifier is independent
626 (should (equal (time-stamp-string "%a.%:a.%a" ref-time1) 633 (should (equal (time-stamp-string "%a.%:a.%a" ref-time1)
627 (concat Mon "." Monday "." Mon))) 634 (concat Mon "." Monday "." Mon)))
635 (should (equal (time-stamp-string "%5z.%5::z.%5z" ref-time1)
636 "+0000.+00:00:00.+0000"))
628 ;; format letter is independent 637 ;; format letter is independent
629 (should (equal (time-stamp-string "%H:%M" ref-time1) "15:04"))))) 638 (should (equal (time-stamp-string "%H:%M" ref-time1) "15:04")))))
630 639
@@ -691,4 +700,401 @@
691 (should (safe-local-variable-p 'time-stamp-pattern "a string")) 700 (should (safe-local-variable-p 'time-stamp-pattern "a string"))
692 (should-not (safe-local-variable-p 'time-stamp-pattern 17))) 701 (should-not (safe-local-variable-p 'time-stamp-pattern 17)))
693 702
703;;;; Setup for tests of time offset formatting with %z
704
705(defun formatz (format zone)
706 "Uses time FORMAT string to format the offset of ZONE, returning the result.
707FORMAT is \"%z\" or a variation.
708ZONE is as the ZONE argument of the `format-time-string' function."
709 (with-time-stamp-test-env
710 (let ((time-stamp-time-zone zone))
711 ;; Call your favorite time formatter here.
712 ;; For narrower-scope unit testing,
713 ;; instead of calling time-stamp-string here,
714 ;; we could directly call (format-time-offset format zone)
715 (time-stamp-string format)
716 )))
717
718(defun format-time-offset (format offset-secs)
719 "Uses FORMAT to format the time zone represented by OFFSET-SECS.
720FORMAT must be \"%z\", possibly with a flag and padding.
721This function is a wrapper around `time-stamp-formatz-from-parsed-options'
722and is used for testing."
723 ;; This wrapper adds a simple regexp-based parser that handles only
724 ;; %z and variants. In normal use, time-stamp-formatz-from-parsed-options
725 ;; is called from a parser that handles all time string formats.
726 (string-match
727 "\\`\\([^%]*\\)%\\([-_]?\\)\\(0?\\)\\([1-9][0-9]*\\)?\\([EO]?\\)\\(:*\\)\\([^a-zA-Z]+\\)?z\\(.*\\)"
728 format)
729 (let ((leading-string (match-string 1 format))
730 (flag-minimize (seq-find (lambda (x) (eq x ?-))
731 (match-string 2 format)))
732 (flag-pad-with-spaces (seq-find (lambda (x) (eq x ?_))
733 (match-string 2 format)))
734 (flag-pad-with-zeros (equal (match-string 3 format) "0"))
735 (field-width (string-to-number (or (match-string 4 format) "")))
736 (colon-count (length (match-string 6 format)))
737 (garbage (match-string 7 format))
738 (trailing-string (match-string 8 format)))
739 (concat leading-string
740 (if garbage
741 ""
742 (time-stamp-formatz-from-parsed-options flag-minimize
743 flag-pad-with-spaces
744 flag-pad-with-zeros
745 colon-count
746 field-width
747 offset-secs))
748 trailing-string)))
749
750(defun fz-make+zone (h &optional m s)
751 "Creates a non-negative offset."
752 (let ((m (or m 0))
753 (s (or s 0)))
754 (+ (* 3600 h) (* 60 m) s)))
755
756(defun fz-make-zone (h &optional m s)
757 "Creates a negative offset. The arguments are all non-negative."
758 (- (fz-make+zone h m s)))
759
760(defmacro formatz-should-equal (zone expect)
761 "Formats ZONE and compares it to EXPECT.
762Uses the free variables `form-string' and `pattern-mod'.
763The functions in `pattern-mod' are composed left to right."
764 `(let ((result ,expect))
765 (dolist (fn pattern-mod)
766 (setq result (funcall fn result)))
767 (should (equal (formatz form-string ,zone) result))))
768
769;; These test cases have zeros in all places (first, last, none, both)
770;; for hours, minutes, and seconds.
771
772(defun formatz-hours-exact-helper (form-string pattern-mod)
773 "Tests format %z with whole hours."
774 (formatz-should-equal (fz-make+zone 0) "+00") ;0 sign always +, both digits
775 (formatz-should-equal (fz-make+zone 10) "+10")
776 (formatz-should-equal (fz-make-zone 10) "-10")
777 (formatz-should-equal (fz-make+zone 2) "+02")
778 (formatz-should-equal (fz-make-zone 2) "-02")
779 (formatz-should-equal (fz-make+zone 13) "+13")
780 (formatz-should-equal (fz-make-zone 13) "-13")
781 )
782
783(defun formatz-nonzero-minutes-helper (form-string pattern-mod)
784 "Tests format %z with whole minutes."
785 (formatz-should-equal (fz-make+zone 0 30) "+00:30") ;has hours even though 0
786 (formatz-should-equal (fz-make-zone 0 30) "-00:30")
787 (formatz-should-equal (fz-make+zone 0 4) "+00:04")
788 (formatz-should-equal (fz-make-zone 0 4) "-00:04")
789 (formatz-should-equal (fz-make+zone 8 40) "+08:40")
790 (formatz-should-equal (fz-make-zone 8 40) "-08:40")
791 (formatz-should-equal (fz-make+zone 0 15) "+00:15")
792 (formatz-should-equal (fz-make-zone 0 15) "-00:15")
793 (formatz-should-equal (fz-make+zone 11 30) "+11:30")
794 (formatz-should-equal (fz-make-zone 11 30) "-11:30")
795 (formatz-should-equal (fz-make+zone 3 17) "+03:17")
796 (formatz-should-equal (fz-make-zone 3 17) "-03:17")
797 (formatz-should-equal (fz-make+zone 12 45) "+12:45")
798 (formatz-should-equal (fz-make-zone 12 45) "-12:45")
799 )
800
801(defun formatz-nonzero-seconds-helper (form-string pattern-mod)
802 "Tests format %z with non-0 seconds."
803 ;; non-0 seconds are always included
804 (formatz-should-equal (fz-make+zone 0 0 50) "+00:00:50")
805 (formatz-should-equal (fz-make-zone 0 0 50) "-00:00:50")
806 (formatz-should-equal (fz-make+zone 0 0 06) "+00:00:06")
807 (formatz-should-equal (fz-make-zone 0 0 06) "-00:00:06")
808 (formatz-should-equal (fz-make+zone 0 7 50) "+00:07:50")
809 (formatz-should-equal (fz-make-zone 0 7 50) "-00:07:50")
810 (formatz-should-equal (fz-make+zone 0 0 16) "+00:00:16")
811 (formatz-should-equal (fz-make-zone 0 0 16) "-00:00:16")
812 (formatz-should-equal (fz-make+zone 0 12 36) "+00:12:36")
813 (formatz-should-equal (fz-make-zone 0 12 36) "-00:12:36")
814 (formatz-should-equal (fz-make+zone 0 3 45) "+00:03:45")
815 (formatz-should-equal (fz-make-zone 0 3 45) "-00:03:45")
816 (formatz-should-equal (fz-make+zone 8 45 30) "+08:45:30")
817 (formatz-should-equal (fz-make-zone 8 45 30) "-08:45:30")
818 (formatz-should-equal (fz-make+zone 0 11 45) "+00:11:45")
819 (formatz-should-equal (fz-make-zone 0 11 45) "-00:11:45")
820 (formatz-should-equal (fz-make+zone 3 20 15) "+03:20:15")
821 (formatz-should-equal (fz-make-zone 3 20 15) "-03:20:15")
822 (formatz-should-equal (fz-make+zone 11 14 30) "+11:14:30")
823 (formatz-should-equal (fz-make-zone 11 14 30) "-11:14:30")
824 (formatz-should-equal (fz-make+zone 12 30 49) "+12:30:49")
825 (formatz-should-equal (fz-make-zone 12 30 49) "-12:30:49")
826 (formatz-should-equal (fz-make+zone 12 0 34) "+12:00:34")
827 (formatz-should-equal (fz-make-zone 12 0 34) "-12:00:34")
828 )
829
830(defun formatz-hours-big-helper (form-string pattern-mod)
831 "Tests format %z with hours that don't fit in two digits."
832 (formatz-should-equal (fz-make+zone 101) "+101:00")
833 (formatz-should-equal (fz-make+zone 123 10) "+123:10")
834 (formatz-should-equal (fz-make-zone 123 10) "-123:10")
835 (formatz-should-equal (fz-make+zone 123 2) "+123:02")
836 (formatz-should-equal (fz-make-zone 123 2) "-123:02")
837 )
838
839(defun formatz-seconds-big-helper (form-string pattern-mod)
840 "Tests format %z with hours greater than 99 and non-zero seconds."
841 (formatz-should-equal (fz-make+zone 123 0 30) "+123:00:30")
842 (formatz-should-equal (fz-make-zone 123 0 30) "-123:00:30")
843 (formatz-should-equal (fz-make+zone 120 0 4) "+120:00:04")
844 (formatz-should-equal (fz-make-zone 120 0 4) "-120:00:04")
845 )
846
847;; Functions that modify the expected output string, so that we can
848;; use the above test cases for multiple formats.
849
850(defun formatz-mod-del-colons (string)
851 "Returns STRING with any colons removed."
852 (replace-regexp-in-string ":" "" string))
853
854(defun formatz-mod-add-00 (string)
855 "Returns STRING with \"00\" appended."
856 (concat string "00"))
857
858(defun formatz-mod-add-colon00 (string)
859 "Returns STRING with \":00\" appended."
860 (concat string ":00"))
861
862(defun formatz-mod-pad-r10 (string)
863 "Returns STRING padded on the right to 10 characters."
864 (concat string (make-string (- 10 (length string)) ?\s)))
865
866(defun formatz-mod-pad-r12 (string)
867 "Returns STRING padded on the right to 12 characters."
868 (concat string (make-string (- 12 (length string)) ?\s)))
869
870;; Convenience macro for generating groups of test cases.
871
872(defmacro formatz-generate-tests
873 (form-strings hour-mod mins-mod secs-mod big-mod secbig-mod)
874 "Defines ert-deftest tests for time formats FORM-STRINGS.
875FORM-STRINGS is a list of formats, each \"%z\" or some variation thereof.
876
877Each of the remaining arguments is an unquoted list of the form
878(SAMPLE-OUTPUT . MODIFIERS). SAMPLE-OUTPUT is the result of the
879FORM-STRINGS for a particular offset, detailed below for each argument.
880The remaining elements of the list, the MODIFIERS, are the names of
881functions to modify the expected results for sets of tests.
882The MODIFIERS do not modify the SAMPLE-OUTPUT.
883
884The one, literal sample output is given in the call to this macro
885to provide a visual check at the call site that the format
886behaves as expected.
887
888HOUR-MOD is the result for offset 0 and modifiers for the other
889expected results for whole hours.
890MINS-MOD is the result for offset +30 minutes and modifiers for the
891other expected results for whole minutes.
892SECS-MOD is the result for offset +30 seconds and modifiers for the
893other expected results for offsets with non-zero seconds.
894BIG-MOD is the result for offset +100 hours and modifiers for the other
895expected results for hours greater than 99 with a whole number of minutes.
896SECBIG-MOD is the result for offset +100 hours 30 seconds and modifiers for
897the other expected results for hours greater than 99 with non-zero seconds."
898 (declare (indent 1))
899 ;; Generate a form to create a list of tests to define. When this
900 ;; macro is called, the form is evaluated, thus defining the tests.
901 (let ((ert-test-list '(list)))
902 (dolist (form-string form-strings ert-test-list)
903 (nconc
904 ert-test-list
905 (list
906 `(ert-deftest ,(intern (concat "formatz-" form-string "-hhmm")) ()
907 (should (equal (formatz ,form-string (fz-make+zone 0))
908 ,(car hour-mod)))
909 (formatz-hours-exact-helper ,form-string ',(cdr hour-mod))
910 (should (equal (formatz ,form-string (fz-make+zone 0 30))
911 ,(car mins-mod)))
912 (formatz-nonzero-minutes-helper ,form-string ',(cdr mins-mod)))
913 `(ert-deftest ,(intern (concat "formatz-" form-string "-secs")) ()
914 (should (equal (formatz ,form-string (fz-make+zone 0 0 30))
915 ,(car secs-mod)))
916 (formatz-nonzero-seconds-helper ,form-string ',(cdr secs-mod)))
917 `(ert-deftest ,(intern (concat "formatz-" form-string "-big")) ()
918 (should (equal (formatz ,form-string (fz-make+zone 100))
919 ,(car big-mod)))
920 (formatz-hours-big-helper ,form-string ',(cdr big-mod))
921 (should (equal (formatz ,form-string (fz-make+zone 100 0 30))
922 ,(car secbig-mod)))
923 (formatz-seconds-big-helper ,form-string ',(cdr secbig-mod)))
924 )))))
925
926;;;; The actual test cases for %z
927
928;;; %z formats without colons.
929
930;; Option character "-" (minus) minimizes; it removes "00" minutes.
931(formatz-generate-tests ("%-z" "%-3z")
932 ("+00")
933 ("+0030" formatz-mod-del-colons)
934 ("+000030" formatz-mod-del-colons)
935 ("+100:00")
936 ("+100:00:30"))
937;; Tests that minus with padding pads with spaces.
938(formatz-generate-tests ("%-12z")
939 ("+00 " formatz-mod-pad-r12)
940 ("+0030 " formatz-mod-del-colons formatz-mod-pad-r12)
941 ("+000030 " formatz-mod-del-colons formatz-mod-pad-r12)
942 ("+100:00 " formatz-mod-pad-r12)
943 ("+100:00:30 " formatz-mod-pad-r12))
944;; Tests that 0 after other digits becomes padding of ten, not zero flag.
945(formatz-generate-tests ("%-10z")
946 ("+00 " formatz-mod-pad-r10)
947 ("+0030 " formatz-mod-del-colons formatz-mod-pad-r10)
948 ("+000030 " formatz-mod-del-colons formatz-mod-pad-r10)
949 ("+100:00 " formatz-mod-pad-r10)
950 ("+100:00:30"))
951
952;; Although time-stamp doesn't call us for %z, we do want to spot-check
953;; it here, to verify the implementation we will eventually use.
954;; The legacy exception for %z in time-stamp will need to remain
955;; through at least 2024 and Emacs 28.
956(ert-deftest formatz-%z-spotcheck ()
957 (should (equal (format-time-offset "%z" (fz-make+zone 0)) "+0000"))
958 (should (equal (format-time-offset "%z" (fz-make+zone 0 30)) "+0030"))
959 (should (equal (format-time-offset "%z" (fz-make+zone 0 0 30)) "+000030"))
960 (should (equal (format-time-offset "%z" (fz-make+zone 100)) "+100:00"))
961 (should (equal (format-time-offset "%z" (fz-make+zone 100 0 30)) "+100:00:30"))
962 )
963
964;; Basic %z outputs 4 digits.
965;; Small padding values do not extend the result.
966(formatz-generate-tests (;; We don't check %z here because time-stamp
967 ;; has a legacy behavior for it.
968 ;;"%z"
969 "%5z" "%0z" "%05z")
970 ("+0000" formatz-mod-add-00)
971 ("+0030" formatz-mod-del-colons)
972 ("+000030" formatz-mod-del-colons)
973 ("+100:00")
974 ("+100:00:30"))
975
976;; Tests that padding adds spaces.
977(formatz-generate-tests ("%12z")
978 ("+0000 " formatz-mod-add-00 formatz-mod-pad-r12)
979 ("+0030 " formatz-mod-del-colons formatz-mod-pad-r12)
980 ("+000030 " formatz-mod-del-colons formatz-mod-pad-r12)
981 ("+100:00 " formatz-mod-pad-r12)
982 ("+100:00:30 " formatz-mod-pad-r12))
983
984;; Requiring 0-padding to 6 adds seconds (only) as needed.
985(formatz-generate-tests ("%06z")
986 ("+000000" formatz-mod-add-00 formatz-mod-add-00)
987 ("+003000" formatz-mod-del-colons formatz-mod-add-00)
988 ("+000030" formatz-mod-del-colons)
989 ("+100:00")
990 ("+100:00:30"))
991
992;; Option character "_" always adds seconds.
993(formatz-generate-tests ("%_z" "%_7z")
994 ("+000000" formatz-mod-add-00 formatz-mod-add-00)
995 ("+003000" formatz-mod-del-colons formatz-mod-add-00)
996 ("+000030" formatz-mod-del-colons)
997 ("+100:00:00" formatz-mod-add-colon00)
998 ("+100:00:30"))
999
1000;; Enough 0-padding adds seconds, then adds spaces.
1001(formatz-generate-tests ("%012z" "%_12z")
1002 ("+000000 " formatz-mod-add-00 formatz-mod-add-00 formatz-mod-pad-r12)
1003 ("+003000 " formatz-mod-del-colons formatz-mod-add-00 formatz-mod-pad-r12)
1004 ("+000030 " formatz-mod-del-colons formatz-mod-pad-r12)
1005 ("+100:00:00 " formatz-mod-add-colon00 formatz-mod-pad-r12)
1006 ("+100:00:30 " formatz-mod-pad-r12))
1007
1008;;; %z formats with colons
1009
1010;; Three colons can output hours only,
1011;; like %-z, but uses colons with non-zero minutes and seconds.
1012(formatz-generate-tests ("%:::z" "%0:::z"
1013 "%3:::z" "%03:::z")
1014 ("+00")
1015 ("+00:30")
1016 ("+00:00:30")
1017 ("+100:00")
1018 ("+100:00:30"))
1019
1020;; Padding with three colons adds spaces
1021(formatz-generate-tests ("%12:::z")
1022 ("+00 " formatz-mod-pad-r12)
1023 ("+00:30 " formatz-mod-pad-r12)
1024 ("+00:00:30 " formatz-mod-pad-r12)
1025 ("+100:00 " formatz-mod-pad-r12)
1026 ("+100:00:30 " formatz-mod-pad-r12))
1027;; Tests that 0 after other digits becomes padding of ten, not zero flag.
1028(formatz-generate-tests ("%10:::z")
1029 ("+00 " formatz-mod-pad-r10)
1030 ("+00:30 " formatz-mod-pad-r10)
1031 ("+00:00:30 " formatz-mod-pad-r10)
1032 ("+100:00 " formatz-mod-pad-r10)
1033 ("+100:00:30"))
1034
1035;; One colon outputs minutes, like %z but with colon.
1036(formatz-generate-tests ("%:z" "%6:z" "%0:z" "%06:z" "%06:::z")
1037 ("+00:00" formatz-mod-add-colon00)
1038 ("+00:30")
1039 ("+00:00:30")
1040 ("+100:00")
1041 ("+100:00:30"))
1042
1043;; Padding with one colon adds spaces
1044(formatz-generate-tests ("%12:z")
1045 ("+00:00 " formatz-mod-add-colon00 formatz-mod-pad-r12)
1046 ("+00:30 " formatz-mod-pad-r12)
1047 ("+00:00:30 " formatz-mod-pad-r12)
1048 ("+100:00 " formatz-mod-pad-r12)
1049 ("+100:00:30 " formatz-mod-pad-r12))
1050
1051;; Requiring 0-padding to 7 adds seconds (only) as needed.
1052(formatz-generate-tests ("%07:z" "%07:::z")
1053 ("+00:00:00" formatz-mod-add-colon00 formatz-mod-add-colon00)
1054 ("+00:30:00" formatz-mod-add-colon00)
1055 ("+00:00:30")
1056 ("+100:00")
1057 ("+100:00:30"))
1058
1059;; Two colons outputs HH:MM:SS, like %_z but with colons.
1060(formatz-generate-tests ("%::z" "%9::z" "%0::z" "%09::z")
1061 ("+00:00:00" formatz-mod-add-colon00 formatz-mod-add-colon00)
1062 ("+00:30:00" formatz-mod-add-colon00)
1063 ("+00:00:30")
1064 ("+100:00:00" formatz-mod-add-colon00)
1065 ("+100:00:30"))
1066
1067;; Enough padding adds minutes and seconds, then adds spaces.
1068(formatz-generate-tests ("%012:z" "%012::z" "%12::z" "%012:::z")
1069 ("+00:00:00 " formatz-mod-add-colon00 formatz-mod-add-colon00
1070 formatz-mod-pad-r12)
1071 ("+00:30:00 " formatz-mod-add-colon00 formatz-mod-pad-r12)
1072 ("+00:00:30 " formatz-mod-pad-r12)
1073 ("+100:00:00 " formatz-mod-add-colon00 formatz-mod-pad-r12)
1074 ("+100:00:30 " formatz-mod-pad-r12))
1075
1076;;; Illegal %z formats
1077
1078(ert-deftest formatz-illegal-options ()
1079 "Tests that illegal/nonsensical/ambiguous %z formats don't produce output."
1080 ;; multiple options
1081 (should (equal "" (formatz "%_-z" 0)))
1082 (should (equal "" (formatz "%-_z" 0)))
1083 (should (equal "" (formatz "%_0z" 0)))
1084 (should (equal "" (formatz "%0_z" 0)))
1085 (should (equal "" (formatz "%0-z" 0)))
1086 (should (equal "" (formatz "%-0z" 0)))
1087 ;; inconsistent to both minimize and require mins or secs
1088 (should (equal "" (formatz "%-:z" 0)))
1089 (should (equal "" (formatz "%-::z" 0)))
1090 ;; consistent, but redundant
1091 (should (equal "" (formatz "%-:::z" 0)))
1092 (should (equal "" (formatz "%_::z" 0)))
1093 ;; inconsistent to both pre-expand and default to hours or mins
1094 (should (equal "" (formatz "%_:::z" 0)))
1095 (should (equal "" (formatz "%_:z" 0)))
1096 ;; options that don't make sense with %z
1097 (should (equal "" (formatz "%#z" 0)))
1098 )
1099
694;;; time-stamp-tests.el ends here 1100;;; time-stamp-tests.el ends here