aboutsummaryrefslogtreecommitdiffstats
path: root/lisp/time-stamp.el
diff options
context:
space:
mode:
authorStephen Gildea2021-06-21 21:28:20 -0700
committerStephen Gildea2021-06-21 21:30:19 -0700
commit64dd2b1a2a0a65a571c2bef5a004fd59cd61bb1e (patch)
tree506e5d745e616e3b71ff316c63fbaba6a7ff63d1 /lisp/time-stamp.el
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.
Diffstat (limited to 'lisp/time-stamp.el')
-rw-r--r--lisp/time-stamp.el241
1 files changed, 214 insertions, 27 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