diff options
| author | Stephen Gildea | 2021-06-21 21:28:20 -0700 |
|---|---|---|
| committer | Stephen Gildea | 2021-06-21 21:30:19 -0700 |
| commit | 64dd2b1a2a0a65a571c2bef5a004fd59cd61bb1e (patch) | |
| tree | 506e5d745e616e3b71ff316c63fbaba6a7ff63d1 /lisp/time-stamp.el | |
| parent | 3b1d69efc32c8929281f38d55cef773e4680f2ad (diff) | |
| download | emacs-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.el | 241 |
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: \" \" |
| 280 | The time stamp is written between the brackets or quotes: | 280 | The 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 | ||
| 283 | The time stamp is updated only if the variable | 283 | The 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. |
| 425 | With ARG, turn time stamping on if and only if arg is positive." | 425 | With 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. |
| 459 | Optional second argument TIME is only for testing. | 459 | Optional second argument TIME is only for testing. |
| 460 | Implements non-time extensions to `format-time-string' | 460 | Implements extensions to `format-time-string' |
| 461 | and all `time-stamp-format' compatibility." | 461 | and 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. | ||
| 815 | The caller of this function must have already parsed the %z format | ||
| 816 | string; this function accepts just the parts of the format. | ||
| 817 | |||
| 818 | With no flags, the output includes hours and minutes: +-HHMM | ||
| 819 | unless there is a non-zero seconds part, in which case the seconds | ||
| 820 | are included: +-HHMMSS | ||
| 821 | |||
| 822 | FLAG-MINIMIZE is whether \"-\" was specified. If non-nil, the | ||
| 823 | output may be limited to hours if minutes and seconds are zero. | ||
| 824 | |||
| 825 | FLAG-PAD-SPACES-ONLY is whether \"_\" was specified. If non-nil, | ||
| 826 | seconds must be output, so that any padding can be spaces only. | ||
| 827 | |||
| 828 | FLAG-PAD-ZEROS-FIRST is whether \"0\" was specified. If non-nil, | ||
| 829 | padding to the requested FIELD-WIDTH (if any) is done by adding | ||
| 830 | 00 seconds before padding with spaces. | ||
| 831 | |||
| 832 | COLON-COUNT is the number of colons preceding the \"z\" (0-3). One or | ||
| 833 | two colons put that many colons in the output (+-HH:MM or +-HH:MM:SS). | ||
| 834 | Three colons outputs only hours if minutes and seconds are zero and | ||
| 835 | includes colon separators if minutes and seconds are output. | ||
| 836 | |||
| 837 | FIELD-WIDTH is a whole number giving the minimum number of characters | ||
| 838 | in the output; 0 specifies no minimum. Additional characters will be | ||
| 839 | added on the right if necessary. The added characters will be spaces | ||
| 840 | unless FLAG-PAD-ZEROS-FIRST is non-nil. | ||
| 841 | |||
| 842 | OFFSET-SECS is the time zone offset (in seconds east of UTC) to be | ||
| 843 | formatted 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 |