reingold@m.cs.uiuc.edu (01/16/89)
After a lot of work, here is the calendar package, now enhanced with a diary mechanism that allows you keep a personal diary of entries that can be displayed in conjunction with the calendar--it's a bit like the Sun calendar, only better. The calendar functions are greatly improved too. As before, I'd welcome any comments and suggestions for polishing this code. I suggest byte-compiling it before playing with it, otherwise the response is irritatingly slow; it's pretty good when byte-compiled. The whole thing is fully described with the '?' command uin the calendar window. ------------------------cut here and byte-compile------------------------------ ;; Calendar and diary functions. ;; Copyright (C) 1988, 1989 Free Software Foundation, Inc. ;; This file is part of GNU Emacs. ;; GNU Emacs is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY. No author or distributor ;; accepts responsibility to anyone for the consequences of using it ;; or for whether it serves any particular purpose or works at all, ;; unless he says so in writing. Refer to the GNU Emacs General Public ;; License for full details. ;; Everyone is granted permission to copy, modify and redistribute ;; GNU Emacs, but only under the conditions described in the ;; GNU Emacs General Public License. A copy of this license is ;; supposed to have been given to you along with GNU Emacs so you ;; can know your rights and responsibilities. It should be in a ;; file named COPYING. Among other things, the copyright notice ;; and this notice must be preserved on all copies. ;; This collection of functions implements a calendar window and diary. It ;; generates a calendar for the current month, together with the previous and ;; coming months, or for any other three-month period. The calendar can be ;; shifted forward and backward in the window to show months in the past or ;; future; the cursor can move forward and back by days or weeks, making it ;; possible, for instance, to jump to the date a specified days or weeks from ;; the date under the cursor. The user can specify that dates that have ;; corresponding diary entries (in a file that the user specifies) be marked; ;; the diary entries for any date can be viewed in a separate window. The ;; diary can be viewed independently of the calendar. ;; Comments, corrections, and improvements should be sent to ;; Edward M. Reingold Department of Computer Science ;; (217) 333-6733 University of Illinois at Urbana-Champaign ;; reingold@a.cs.uiuc.edu 1304 West Springfield Avenue ;; Urbana, Illinois 61801 ;; The author gratefully acknowledges the patient help of Richard Stallman ;; in making this function into a reasonable piece of code! ;; Modification for month-offset arguments suggested and first implemented by ;; Constantine Rasmussen Sun Microsystems, East Coast Division ;; (617) 671-0404 2 Federal Street; Billerica, MA 01824 ;; ARPA: cdr@sun.com USENET: {cbosgd,decvax,hplabs,seismo}!sun!suneast!cdr ;; Modification to mark current day with stars suggested by ;; Franklin Davis Thinking Machines Corp ;; (617) 876-1111 245 First Street, Cambridge, MA 02142 ;; fad@think.com ;; GNU Emacs users too numerous to list pointed out a variety of problems ;; with earlier forms of the 'infinite' calendar. ;; Modification to allow forward and backward movement by days suggested by ;; Ashwin Ram Department of Computer Science ;; (203) 432-7037 Yale University ;; Box 2158 Yale Station; New Haven, CT 06520 ;; ARPA: Ram-Ashwin@cs.yale.edu USENET: yale!Ram-Ashwin BITNET: Ram@yalecs ;; Implementation of mark-diary-entries and view-diary-entries due in part to ;; Michael S. Littman Cognitive Science Research Group ;; (217) 333-6733 Bell Communications Research ;; mlittman@wind.bellcore.com 445 South St. Box 1961 (2L-331) ;; Morristown, NJ 07960-1961 (defvar view-diary-entries-initially nil "*If t then the diary entries for the current date will be displayed in another window when the calendar is first displayed, if the current date is visible. The number of days of diary entries displayed is governed by the variable number-of-diary-entries.") (defvar number-of-diary-entries [1 1 1 1 1 1 1] "*The value indicates how many days of diary entries are to be displayed. For example, if the default value [1 1 1 1 1 1 1] is used, then only the current day's diary entries will be displayed. If it's value is [0 2 2 2 2 4 1] then no diary entries will be displayed on Sunday, the current date's and the next day's diary entries will be displayed Monday through Thursday, Friday through Monday's entries will be displayed on Friday, while on Saturday only that day's entries will be displayed.") (defvar mark-diary-entries-in-calendar nil "*If t then any dates with diary entries will be marked by the character diary-entry-marker.") (defvar diary-entry-marker "+" "*The symbol used to mark dates that have diary entries.") (defvar initial-calendar-window-hook nil "*List of functions to be called when the calendar window is first opened. The functions invoked are called after the calendar window is opened, but once opened is never called again. Leaving the calendar with the 'q' command and reentering it will cause these functions to be called again.") (defvar today-visible-calendar-hook nil "*List of functions called after the calendar buffer has been prepared with the calendar when the current date is visible in the window. This can be used, for example, to replace today's date with asterisks; a function star-date is included for this purpose: (setq today-visible-calendar-hook 'star-date) It could also be used to mark the current date with '*'; a function is also provided for this: (setq today-visible-calendar-hook 'mark-today) The corresponding variable today-invisible-calendar-hook is the list of functions called when the calendar function was called when the current date is not visible in the window. Other than the use of the provided functions, the changing of any characters in the calendar buffer by the hooks may cause the failure of the functions that move by days and weeks.") (defvar today-invisible-calendar-hook nil "*List of functions called after the calendar buffer has been prepared with the calendar when the current date is not visible in the window. The corresponding variable today-visible-calendar-hook is the list of functions called when the calendar function was called when the current date is visible in the window. Other than the use of the provided functions, the changing of any characters in the calendar buffer by the hooks may cause the failure of the functions that move by days and weeks.") (defvar diary-file "~/diary" "*Name of the file in which one's personal diary of dates is kept. The file's entries are lines in the form DAY, MONTH/DAY or MONTH/DAY/YEAR at the beginning of the line, followed by a nondigit; the remainder of the line is taken as the diary entry string for that date. MONTH and DAY are one or two digit numbers, YEAR is a number and must be written in full. If the date does not contain a year, it is generic and applies to any year; if it does not contain a year or a month, it applies to any month. Entries can also be of the form DAYNAME at the beginning of a line, followed by a nonletter; the day name must be spelled out in full and capitalized (e.g., Tuesday). DAYNAME entries apply to any date on which is on that day of the week. Unless mark-weekly-entries is t, DAYNAME entries are nonmarking--that is, they will not be marked on dates in the calendar window. Lines not in one the above formats are ignored. Here are some sample diary entries: 12/22/1988 Twentieth wedding anniversary!! 1/1 Happy New Year! 10/22 Ruth's birthday. 21: Payday Tuesday--weekly meeting with grad students at 10am 1/13/1989 Friday the thirteenth!! Thursday 4pm squash game with Lloyd. ") (defvar mark-weekly-entries nil "*If t, DAYNAME diary entries will cause all the dates that are that day of the week to be marked when the calendar is marked.") (defconst calendar-buffer "*Calendar*" "Name of the buffer used for the calendar.") (defconst diary-buffer "*Diary Entries*" "Name of the buffer used for the displayed diary entries.") (defmacro increment-month (mon yr n) "Move the variables MON and YR to the month and year N months forward if N is positive or backward if N is negative." (` (let (( y (+ (+ (* (, yr) 12) (- (, mon) 1)) (, n)) )) (setq (, mon) (+ (% y 12) 1)) (setq (, yr) (/ y 12))))) (defmacro for (var from init to final do &rest body) "Execute a for loop." (` (let (( (, var) (1- (, init)) )) (while (>= (, final) (setq (, var) (1+ (, var)))) (,@ body))))) (defun calendar (&optional arg) "Display a three-month calendar in another window. The three months appear side by side, with the current month in the middle surrounded by the previous and next months. The cursor is put on today's date. This function is suitable for execution in a .emacs file; appropriate setting of the variable view-diary-entries-initially will cause the diary entries for the current date to be displayed in another window. The value of the variable number-of-diary-entries controls the number of days of diary entries displayed. An optional prefix argument ARG causes the calendar displayed to be ARG months in the future if ARG is positive or in the past if ARG is negative; in this case the cursor goes on the first day of the month. Once in the calendar window, future months can be moved into view with ','; prior months can be moved into view with '.'. Arbitrary months can be displayed with 'o' which prompts for the desired month and year. A space rolls the calendar forward to the next three months; backspace rolls it backward to the previous three months. The cursor can be moved forward one day with 'f' and one week with 'n'. Similarly, 'b' and 'p' move the cursor backward by a one day and one week, respectively. All of these commands take prefix arguments which, when negative, cause movement in the opposite direction. For convenience, the digit keys are automatically prefixes. The window is replotted as necessary to display the desired date. 'd' causes the diary entries, if any, to be displayed in another window. 'q' exits from the calendar, causing the calendar windows and buffers to be deleted. The Gregorian calendar is assumed. After preparing the calendar window initially, the hooks initial-calendar-window-hook are run. The hooks today-visible-calendar-hook are run everytime the calendar window gets shifted, if the current date is visible in the window. If it is not visible, the hooks today-invisible-calendar-hook are run. Thus, for example, setting today-visible-calendar-hook to 'star-date will cause today's date to be replaced by asterisks to highlight it whenever it is in the window. Other than the use of provided functions, the changing of any characters in the calendar buffer by the hooks may cause the failure of the functions that move by days and weeks." (interactive "P") (setq arg (if arg (prefix-numeric-value arg) 0)) (regenerate-calendar-window arg) (let ((date (list current-month current-day current-year))) (if (and view-diary-entries-initially (date-is-visible-p date)) (view-diary-entries (aref number-of-diary-entries (day-of-week date))))) (run-hooks 'initial-calendar-window-hook)) (defun diary () "Generate the diary window for the current date. The number of days of diary entries is governed by number-of-diary-entries. This function is suitable for execution in a .emacs file." (interactive) (if (and diary-file (file-exists-p diary-file)) (save-excursion (let* ( ;; Get today's date and extract the day, month and year. (date (current-time-string)) (garbage (string-match "^\\([A-Z][a-z]*\\) *\\([A-Z][a-z]*\\) *\\([0-9]*\\) .* \\([0-9]*\\)$" date)) (month (substring date (match-beginning 2) (match-end 2))) (month (cdr (assoc month '(("Jan" . 1) ("Feb" . 2) ("Mar" . 3) ("Apr" . 4) ("May" . 5) ("Jun" . 6) ("Jul" . 7) ("Aug" . 8) ("Sep" . 9) ("Oct" . 10) ("Nov" . 11) ("Dec" . 12))))) (day (string-to-int (substring date (match-beginning 3) (match-end 3)))) (year (string-to-int (substring date (match-beginning 4) (match-end 4)))) (date (list month day year)) (day-in-week (day-of-week date)) (number-of-days (aref number-of-diary-entries day-in-week))) (if (< 0 number-of-days) (progn (set-buffer (find-file-noselect diary-file t)) (goto-char (point-min)) (list-diary-entries date number-of-days))))) (error "You don't have a diary file!"))) (defun regenerate-calendar-window (&optional arg) "Generate the calendar window, offset from the current date by ARG months." (if (not arg) (setq arg 0)) (save-excursion (set-buffer (get-buffer-create calendar-buffer)) (calendar-mode) (let* ((buffer-read-only nil) ;; Get today's date and extract the day, month and year. (date (current-time-string)) (garbage (string-match "^\\([A-Z][a-z]*\\) *\\([A-Z][a-z]*\\) *\\([0-9]*\\) .* \\([0-9]*\\)$" date)) (day-in-the-week (substring date (match-beginning 1) (match-end 1))) (month (substring date (match-beginning 2) (match-end 2))) (day-in-the-month (substring date (match-beginning 3) (match-end 3))) (year (substring date (match-beginning 4) (match-end 4))) (date-string (concat day-in-the-week ", " month " " day-in-the-month ", " year))) (setq mode-line-format (concat (format "--period-> Calendar h help/o other/c current %17s <-comma" date-string) "%-")) (setq current-month (cdr (assoc month '(("Jan" . 1) ("Feb" . 2) ("Mar" . 3) ("Apr" . 4) ("May" . 5) ("Jun" . 6) ("Jul" . 7) ("Aug" . 8) ("Sep" . 9) ("Oct" . 10) ("Nov" . 11) ("Dec" . 12))))) (setq current-day (string-to-int day-in-the-month)) (setq current-year (string-to-int year)) (let ((d-month current-month) (d-year current-year)) (increment-month d-month d-year arg) (generate-calendar d-month d-year)))) ;; Display the buffer and put cursor on today's date. Do it in another ;; window, but if this buffer is already visible, just select its window. (pop-to-buffer calendar-buffer) (if mark-diary-entries-in-calendar (mark-diary-entries)) (set-buffer-modified-p nil) (or (one-window-p t) (shrink-window (- (window-height) 9))) (if (and (<= arg 1) (>= arg -1)) (progn;; When the current date is on the screen. (goto-visible-date (list current-month current-day current-year)) (run-hooks 'today-visible-calendar-hook)) (progn;; When the current date is not on the screen. (goto-char (point-min)) (run-hooks 'today-invisible-calendar-hook)))) (defun generate-calendar (month year) "Generate a three-month Gregorian calendar centered around MONTH, YEAR." (if (< (+ d-month (* 12 (1- d-year))) 2) (error "Months before February, 1 AD are not available.")) (setq displayed-month month) (setq displayed-year year) (erase-buffer) (increment-month month year -1) (for i from 0 to 2 do (generate-month month year (+ 5 (* 25 i))) (increment-month month year 1))) (defun generate-month (month year indent) "Produce a calendar for MONTH, YEAR on the Gregorian calendar, inserted in the buffer starting at the line on which point is currently located, but indented INDENT spaces. The indentation is done from the first character on the line and does not disturb the first INDENT characters on the line." (let* ((first-day-of-month (day-of-week (list month 1 year))) (first-saturday (- 7 first-day-of-month)) (last (last-day month year))) (goto-char (point-min)) (insert-indented (format " %s %d" (month-name month) year) indent t) (insert-indented " S M Tu W Th F S" indent t) (insert-indented "" indent);; move point to appropriate spot on line ;; add blank days before the first of the month (for i from 1 to first-day-of-month do (insert " ")) ;; put in the days of the month (for i from 1 to last do (insert (format "%2d " i)) (and (= (% i 7) (% first-saturday 7)) (/= i last) (insert-indented "" 0 t) ;; force onto following line (insert-indented "" indent))))) ;; go to proper spot on line (defun insert-indented (string indent &optional newline) "Insert STRING at column INDENT. If the optional parameter NEWLINE is true, leave point at start of next line, inserting a newline if there was no next line; otherwise, leave point after the inserted text. Value is always `t'." ;; Try to move to that column. (move-to-column indent) ;; If line is too short, indent out to that column. (if (< (current-column) indent) (indent-to indent)) (insert string) ;; Advance to next line, if requested. (if newline (progn (end-of-line) (if (eobp) (newline) (forward-line 1)))) t) (defvar calendar-mode-map nil) (if calendar-mode-map nil (setq calendar-mode-map (make-sparse-keymap)) (for i from 0 to 9 do (define-key calendar-mode-map (int-to-string i) 'digit-argument)) (define-key calendar-mode-map "." 'backward-month) (define-key calendar-mode-map "\b" 'backward-three-months) (define-key calendar-mode-map "," 'forward-month) (define-key calendar-mode-map " " 'forward-three-months) (define-key calendar-mode-map "c" 'show-current-month) (define-key calendar-mode-map "o" 'show-other-month) (define-key calendar-mode-map "q" 'exit-calendar) (define-key calendar-mode-map "f" 'forward-day) (define-key calendar-mode-map "b" 'backward-day) (define-key calendar-mode-map "n" 'next-week) (define-key calendar-mode-map "p" 'previous-week) (define-key calendar-mode-map "m" 'mark-diary-entries) (define-key calendar-mode-map "u" 'unmark-diary-entries) (define-key calendar-mode-map "d" 'view-diary-entries) (define-key calendar-mode-map "h" 'calendar-help) (define-key calendar-mode-map "?" 'describe-mode)) (defun calendar-help () "Print a message with a list of all the calendar commands." (interactive) (message "Commands: SPC BKS . , c o q f b n p m u d h ? for more details")) ;; Calendar mode is suitable only for specially formatted data. (put 'calendar-mode 'mode-class 'special) (defun calendar-mode () "A major mode for the sliding calendar window and diary. The key bindings are \\{calendar-mode-map} The variable view-diary-entries-initially, whose default is nil, can be set to to t cause diary entries for the current date will be displayed in another window when the calendar is first displayed, if the current date is visible. The number of days of diary entries that will be displayed. For example, if the default value [1 1 1 1 1 1 1] is used, then only the current day's diary entries will be displayed. If it's value is [0 2 2 2 2 4 1] then no diary entries will be displayed on Sunday, the current date's and the next day's diary entries will be displayed Monday through Thursday, Friday through Monday's entries will be displayed on Friday, while on Saturday only that day's entries will be displayed. The variable mark-diary-entries-in-calendar can be set to t to cause any dates visible with calendar entries to be marked with the symbol diary-entry-marker, normally a plus sign. The variable initial-calendar-window-hook, whose default value is nil, is list of functions to be called when the calendar window is first opened. The functions invoked are called after the calendar window is opened, but once opened is never called again. Leaving the calendar with the 'q' command and reentering it will cause these functions to be called again. The variable today-visible-calendar-hook, whose default value is nil, is the list of functions called after the calendar buffer has been prepared with the calendar when the current date is visible in the window. This can be used, for example, to replace today's date with asterisks; a function star-date is included for this purpose: (setq today-visible-calendar-hook 'star-date) It could also be used to mark the current date with '*'; a function is also provided for this: (setq today-visible-calendar-hook 'mark-today) The variable today-invisible-calendar-hook, whose default value is nil, is the list of functions called after the calendar buffer has been prepared with the calendar when the current date is not visible in the window. Other than the use of the provided functions, the changing of any characters in the calendar buffer by the hooks may cause the failure of the functions that move by days and weeks. The variable diary-file, whose default value is ~/diary, is name of the file in which one's personal diary of dates is kept. The file's entries are lines in the form DAY, MONTH/DAY or MONTH/DAY/YEAR at the beginning of the line, followed by a nondigit; the remainder of the line is taken as the diary entry string for that date. MONTH and DAY are one or two digit numbers, YEAR is a number and must be written in full. If the date does not contain a year, it is generic and applies to any year; if it does not contain a year or a month, it applies to any month. Entries can also be of the form DAYNAME at the beginning of a line, followed by a nonletter; the day name must be spelled out in full and capitalized (e.g., Tuesday). DAYNAME entries apply to any date on which is on that day of the week. Unless mark-weekly-entries is t, DAYNAME entries are nonmarking--that is, they will not be marked on dates in the calendar window. Lines not in one the above formats are ignored. Here are some sample diary entries: 12/22/1988 Twentieth wedding anniversary!! 1/1 Happy New Year! 10/22 Ruth's birthday. 21: Payday Tuesday--weekly meeting with grad students at 10am 1/13/1989 Friday the thirteenth!! Thursday 4pm squash game with Lloyd. Executing view-diary-entries causes the dairy entries for the date indicated by the cursor in the calendar window to be displayed in another window. This function takes an integer argument that specifies the number of days of calendar entries to be displayed, starting with the date indicated by the cursor. The function diary causes the diary entries for the current date to be displayed, independently of the calendar. The number of days of entries is governed by number-of-diary-entries. The Gregorian calendar is assumed." (kill-all-local-variables) (setq major-mode 'calendar-mode) (setq mode-name "Calendar") (use-local-map calendar-mode-map) (setq buffer-read-only t) (make-local-variable 'mode-line-format) (make-local-variable 'current-month) ;; Current month. (make-local-variable 'current-day) ;; Current day. (make-local-variable 'current-year) ;; Current year. (make-local-variable 'displayed-month);; Month in middle of window. (make-local-variable 'displayed-year));; Year in middle of window. (defun exit-calendar () "Get out of the calendar window and destroy it and related buffers." (interactive) (delete-windows-on (get-buffer calendar-buffer)) (kill-buffer calendar-buffer) (delete-windows-on (get-buffer-create diary-buffer)) (kill-buffer diary-buffer) (let ((calendar-buffer (get-file-buffer diary-file))) (if calendar-buffer (progn (delete-windows-on calendar-buffer) (kill-buffer calendar-buffer))))) (defun show-current-month () "Reposition the calendar window so the current date is visible." (interactive) (let ((current-date (list current-month current-day current-year))) (if (date-is-visible-p current-date) (goto-visible-date current-date) (regenerate-calendar-window)))) (defun forward-month (&optional arg) "Advance the displayed calendar window by one month. An optional prefix argument ARG causes the calendar to be advanced by ARG months if ARG is positive or to be moved backward if ARG is negative. The relative position of the cursor with respect to the calendar is maintained as well as possible." (interactive "p") (let* ((column (current-column)) (new-column (- column (* 25 arg))) (line (progn (beginning-of-line) (1+ (count-lines 1 (point)))))) (regenerate-calendar-window (+ arg (calendar-interval current-month current-year displayed-month displayed-year))) (goto-line line) (beginning-of-line) (if (> new-column 75) (end-of-line) (if (> new-column 0) (forward-char new-column))))) (defun forward-three-months (arg) "Advance the displayed calendar window by 3*ARG months." (interactive "p") (forward-month (* 3 arg)) (let ((current-date (list current-month current-day current-year))) (if (date-is-visible-p current-date) (goto-visible-date current-date)))) (defun backward-month (&optional arg) "Move the displayed calendar window backward by one month. An optional prefix argument ARG causes the calendar to be move backward by ARG months if ARG is positive or to be advanced if ARG is negative." (interactive "p") (forward-month (- arg))) (defun backward-three-months (arg) "Move the displayed calendar window backward by 3*ARG months." (interactive "p") (forward-month (* -3 arg)) (let ((current-date (list current-month current-day current-year))) (if (date-is-visible-p current-date) (goto-visible-date current-date)))) (defun cursor-to-date () "Returns (month day year) of current cursor position, or nil if the cursor is not on a specific day." (if (and (looking-at "[*0-9]") (< 2 (count-lines (point-min) (point)))) (save-excursion (re-search-backward "[^0-9]") (forward-char 1) (let* ((day (string-to-int (buffer-substring (point) (+ 3 (point))))) (day (if (= 0 day) current-day day));; Starred date. (segment (/ (current-column) 25)) (month (% (+ displayed-month segment -1) 12)) (month (if (= 0 month) 12 month)) (year (cond ((and (= 12 month) (= segment 0)) (1- displayed-year)) ((and (= 1 month) (= segment 2)) (1+ displayed-year)) (t displayed-year)))) (list month day year))))) (defun forward-day (&optional arg) "Move the cursor forward ARG days, changing the calendar window if needed. Movement is backward if ARG is negative." (interactive "p") (let* ((cursor-date (or (cursor-to-date) (error "Cursor is not on a date!"))) (new-cursor-date (normal-from-absolute (+ (absolute-from-normal cursor-date) arg))) (new-display-month (extract-month new-cursor-date)) (new-display-year (extract-year new-cursor-date))) ;; Put the new month on the screen, if needed, and go to the new date. (if (not (date-is-visible-p new-cursor-date)) (show-other-month new-display-month new-display-year)) (goto-visible-date new-cursor-date))) (defun extract-month (date) "Extract the month part of DATE which has the form (month day year)." (car date)) (defun extract-day (date) "Extract the day part of DATE which has the form (month day year)." (car (cdr date))) (defun extract-year (date) "Extract the year part of DATE which has the form (month day year)." (car (cdr (cdr date)))) (defun normal-from-absolute (date) "Compute the list (month day year) corresponding to the absolute DATE. The absolute date is the number of days elapsed since the (imaginary) Gregorian date Sunday, December 31, 1 BC." (let ((month 1) (day 0) (year (/ date 366))) ;; Calculate month, day, and year of DATE. (while (< (absolute-from-normal (list 12 31 year)) date) (setq year (1+ year))) (while (< (absolute-from-normal (list month (last-day month year) year)) date) (setq month (1+ month))) (setq day (- (1+ date) (absolute-from-normal (list month 1 year)))) (list month day year))) (defun goto-visible-date (date) "Move the cursor to DATE that is on the screen." (let* ((month (extract-month date)) (day (extract-day date)) (year (extract-year date)) (gap (calendar-interval displayed-month displayed-year month year)) (line (+ 2 (/ (+ day -1 (day-of-week (list month 1 year))) 7))) (segment (1+ gap))) (goto-char (point-min)) (forward-line line) (forward-char (+ 6 (* 25 segment) (* 3 (day-of-week date)))))) (defun backward-day (&optional arg) "Move the cursor back ARG days, changing the calendar window if needed. Movement is forward if ARG is negative." (interactive "p") (forward-day (- arg))) (defun next-week (&optional arg) "Move the cursor forward ARG weeks, changing the calendar window if needed. Movement is backward if ARG is negative." (interactive "p") (forward-day (* arg 7))) (defun previous-week (&optional arg) "Move the cursor back ARG weeks, changing the calendar window if needed. Movement is forward if ARG is negative." (interactive "p") (forward-day (- (* arg 7)))) (defun show-other-month (month year) "Display a three-month calendar centered around MONTH and YEAR." (interactive "nMonth (1-12): \nnYear (>0): ") (if (or (< 12 month) (> 1 month) (> 1 year)) (error "Unacceptable month/year!")) (regenerate-calendar-window (calendar-interval current-month current-year month year))) (defun last-line-p () "Returns true if point is on the last line of the buffer." (save-excursion (end-of-line) (eobp))) (defun calendar-interval (mon1 yr1 mon2 yr2) "The number of months difference between the two specified months." (+ (* 12 (- yr2 yr1)) (- mon2 mon1))) (defun leap-year-p (year) "Returns true if YEAR is a Gregorian leap year, and false if not." (or (and (= (% year 4) 0) (/= (% year 100) 0)) (= (% year 400) 0))) (defun day-number (date) "Return the day number within the year of the date DATE. For example, (day-number (1 1 1987)) returns the value 1, while (day-number (12 31 1980)) returns 366." ;; ;; An explanation of the calculation can be found in PascAlgorithms by ;; Edward and Ruth Reingold, Scott-Foresman/Little, Brown, 1988. ;; (let* ((month (extract-month date)) (day (extract-day date)) (year (extract-year date)) (day-of-year (+ day (* 31 (1- month))))) (if (> month 2) (progn (setq day-of-year (- day-of-year (/ (+ 23 (* 4 month)) 10))) (if (leap-year-p year) (setq day-of-year (1+ day-of-year))))) day-of-year)) (defun day-name (date) "Returns a string with the name of the day of the week of DATE." (aref ["Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday"] (day-of-week date))) (defun day-of-week (date) "Returns the day-of-the-week index of DATE, 0 for Sunday, 1 for Monday, etc." ;; ;; Done by calculating the number of days elapsed since the (imaginary) ;; Gregorian date Sunday, December 31, 1 BC and taking that number mod 7. ;; (% (absolute-from-normal date) 7)) (defun absolute-from-normal (date) "Calculates the number of days elapsed since the (imaginary) Gregorian date Sunday, December 31, 1 BC to DATE." (let ((month (extract-month date)) (day (extract-day date)) (year (extract-year date))) (- (+ (day-number date) (* 365 (1- year)) (/ (1- year) 4)) (let ((correction (* (/ (1- year) 100) 3))) (if (= (% correction 4) 0) (/ correction 4) (1+ (/ correction 4))))))) (defun last-day (month year) "The last day in MONTH during YEAR." (if (and (leap-year-p year) (= month 2)) 29 (aref [31 28 31 30 31 30 31 31 30 31 30 31] (1- month)))) (defun month-name (month) "The name of MONTH." (aref ["January" "February" "March" "April" "May" "June" "July" "August" "September" "October" "November" "December"] (1- month))) (defun view-diary-entries (arg) "Searches the file diary-file for entries that match ARG days starting with the date indicated by the cursor position in the displayed three-month calendar. If there is no calendar buffer, one is generated with the cursor on the current day." (interactive "p") (if (and diary-file (file-exists-p diary-file)) (if (< 0 arg) (let ((cursor-date (or (cursor-to-date) (error "Cursor is not on a date!")))) (set-buffer (find-file-noselect diary-file t)) (goto-char (point-min)) (list-diary-entries cursor-date arg))) (error "You don't have a diary file!"))) (defun list-diary-entries (date number) "Create a buffer containing the lines in diary-file that apply to DATE and the next NUMBER-1 days." (let* ((buffer (current-buffer)) (entry-found-this-date) (any-entries) (month (extract-month date)) (day (extract-day date)) (year (extract-year date)) (date-string (format "%s, %s %d, %d" (day-name date) (month-name month) day year))) (save-excursion (for i from 1 to number do (goto-char (point-min)) (let ((regexp (concat "\\(^" (int-to-string day) "[^0-9/]\\)" "\\|\\(^" (int-to-string month) "/" (int-to-string day) "[^0-9/]\\)" "\\|\\(^" (int-to-string month) "/" (int-to-string day) "/" (int-to-string year) "[^0-9/]\\)" "\\|^" (day-name date)))) (while (re-search-forward regexp nil t) (if (not entry-found-this-date) (save-excursion;; First entry for this date. (set-buffer (get-buffer-create diary-buffer)) (setq buffer-read-only nil) (if (not any-entries);; First entry for the buffer. (progn (make-local-variable 'mode-line-format) (setq mode-line-format (concat "-------------------Diary entries for " date-string "%-")) (erase-buffer) (if (< 1 i) (insert-string (concat "No entries for " date-string ".\n"))))) (if (< 1 i) (insert-string (concat "\n===== " (cond ((= i 2) "Tomorrow ===============================\n") ((= i 3) "Day after tomorrow =====================\n") ((< 3 i) (concat (int-to-string (1- i)) " days from now ========================\n")))))) (setq entry-found-this-date t))) (beginning-of-line 1);; Just insert the entry. (let ((start (point)) (end (progn (forward-line 1) (point)))) (save-excursion (set-buffer diary-buffer) (insert-buffer-substring buffer start end)) (forward-char -1)))) (setq date (normal-from-absolute (1+ (absolute-from-normal date)))) (setq month (extract-month date)) (setq day (extract-day date)) (setq year (extract-year date)) (setq any-entries (or any-entries entry-found-this-date)) (setq entry-found-this-date nil)) (if (not any-entries) (message "No diary entries for %s" date-string ".") (set-buffer diary-buffer) (display-buffer diary-buffer) (setq buffer-read-only t) (set-buffer-modified-p nil))))) (defun mark-diary-entries () "For each DAY, MONTH/DAY, and MONTH/DAY/YEAR entry in diary-file visible in the calendar window, mark the day in the calendar window." (interactive) (if (and diary-file (file-exists-p diary-file)) (save-excursion (set-buffer (find-file-noselect diary-file t)) ;; For each day name in the diary-file, mark dates with that name. (if mark-weekly-entries (for day from 0 to 6 do (goto-char (point-min)) (if (search-forward (aref ["Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday"] day) nil t) (mark-days-named day)))) (goto-char (point-min)) ;; For each date in the diary-file, mark it if it is in range. (while (re-search-forward "^\\([0-9]+\\)[^0-9]" nil t) (let ((month (string-to-int (buffer-substring (match-beginning 1) (match-end 1)))) (day 0) (year 0)) (if (looking-at "[0-9]") (progn (re-search-forward "\\([0-9]+\\)[^0-9]" nil t) (setq day (string-to-int (buffer-substring (match-beginning 1) (match-end 1))))) (setq day month);; It's a DAY entry, not a MONTH. (setq month 0)) (if (looking-at "[0-9]") (progn (re-search-forward "\\([0-9]+\\)[^0-9]" nil t) (setq year (string-to-int (buffer-substring (match-beginning 1) (match-end 1)))))) (mark-date-pattern month day year)))) (error "You don't have a diary file!"))) (defun mark-days-named (dayname) "Mark all dates in the calendar window that are day DAYNAME of the week. 0 means all Sundays, 1 means all Mondays, and so on." (save-excursion (set-buffer calendar-buffer) (let ((prev-month displayed-month) (prev-year displayed-year) (succ-month displayed-month) (succ-year displayed-year) (first-date) (last-date) (day)) (increment-month succ-month succ-year 1) (increment-month prev-month prev-year -1) (setq first-date (list prev-month 1 prev-year)) (setq last-day (absolute-from-normal (list succ-month (last-day succ-month succ-year) succ-year))) (setq day (+ (absolute-from-normal first-date) 7 dayname (- (day-of-week first-date)))) (while (<= day last-day) (mark-visible-date (normal-from-absolute day)) (setq day (+ day 7)))))) (defun mark-date-pattern (month day year) "Mark all dates in the calendar window that conform to MONTH/DAY/YEAR. A value of 0 is a wild-card." (save-excursion (set-buffer calendar-buffer) (let ((prev-month displayed-month) (prev-year displayed-year) (succ-month displayed-month) (succ-year displayed-year)) (increment-month succ-month succ-year 1) (increment-month prev-month prev-year -1) (if (or (= month prev-month) (= month 0)) (if (= year 0) (mark-visible-date (list prev-month day prev-year)) (if (= year displayed-year) (mark-visible-date (list month day displayed-year))))) (if (or (= month succ-month) (= month 0)) (if (= year 0) (mark-visible-date (list succ-month day succ-year)) (if (= year displayed-year) (mark-visible-date (list month day displayed-year))))) (if (and (or (= month displayed-month) (= month 0)) (or (= year 0) (= year displayed-year))) (mark-visible-date (list displayed-month day displayed-year)))))) (defun unmark-diary-entries () "Delete the diary entry marks in the calendar." (interactive) (let ((position (point)) (buffer-read-only nil)) (goto-char (point-min)) (forward-line 2) (replace-string diary-entry-marker " ") (goto-char position))) (defun date-is-visible-p (date) "Returns true if DATE is visible in the calendar window." (save-excursion (set-buffer calendar-buffer) (let ((gap (calendar-interval displayed-month displayed-year (extract-month date) (extract-year date)))) (and (date-is-legal-p date) (> 2 gap) (< -2 gap))))) (defun date-is-legal-p (date) "Returns true if DATE is a legal date." (let ((month (extract-month date)) (day (extract-day date)) (year (extract-year date))) (and (<= 1 month) (<= month 12) (<= 1 day) (<= day (last-day month year)) (<= 1 year)))) (defun mark-visible-date (date &optional mark) "Leave mark DATE with MARK. MARK defaults to diary-entry-marker." (if (date-is-legal-p date) (save-excursion (set-buffer calendar-buffer) (goto-visible-date date) (forward-char 1) (let ((buffer-read-only nil)) (delete-char 1) (insert (if mark mark diary-entry-marker)) (forward-char -2)) (set-buffer-modified-p nil)))) (defun star-date () "Replace today's date with asterisks in the calendar window. This function can be used with the today-visible-calendar-hook run after the calendar window has been prepared." (let ((buffer-read-only nil)) (forward-char 1) (delete-char -2) (insert "**") (backward-char 1) (set-buffer-modified-p nil))) (defun mark-today () "Mark today's date with an asterisk in the calendar window. This function can be used with the today-visible-calendar-hook run after the calendar window has been prepared." (let ((buffer-read-only nil)) (forward-char 1) (delete-char 1) (insert "*") (set-buffer-modified-p nil)))