4

Every semester I teach the same course and for every semester there are several TODOs that should, at least initially, have the same dates relative to the starting date (i.e. the first day of class). How can I take the previous semester's course.org and easily update all of the TODO dates?

To exemplify with a MWE, imagine these were my TODOS this semester (I actually have plenty more)

** TODO prepare and distribute syllabus (first day of class: Sept 03, 2019)
   SCHEDULED: <2019-09-03 Tue>

** TODO explain group project (two weeks after classes started)
   SCHEDULED: <2019-09-17 Tue>

** TODO Create groups (on the Wednesday, three weeks after classes started)
   SCHEDULED: <2019-09-25 Wed>

How can I update these dates for this semester (where the first class falls, say, on January 13, 2020?)

(I am also happy to just create some sort of template (though preferably not a 'capture template') with variables for the initial dates and use a macro to fill them in).

1

I'm guessing one way to shift all scheduled dates relative to a starting date is to calculate the difference in time between the new and old starting dates. Adding the result to the scheduled date should return the new date.

(defvar new-start-date "2020-01-13 Mon")

(defun shift-date (date old-start-date)
  (let* ((encode
          (lambda (d)
            (apply #'encode-time (org-parse-time-string d))))
         (date (funcall encode date))
         (old-start-date (funcall encode old-start-date))
         (new-start-date (funcall encode new-start-date))
         (time (org-time-add
                date
                (org-time-subtract new-start-date old-start-date))))
    (format-time-string "<%F %a>" time)))

(defun reschedule-entries ()
  (interactive)
  (let ((re (concat org-scheduled-string " *<\\([^>]+\\)>"))
        date old-start-date)
    (save-excursion
      (goto-char (point-min))
      (when (re-search-forward re nil t)
        (setq old-start-date (match-string 1))
        (replace-match new-start-date t t nil 1)))
    (org-map-entries
     (lambda ()
       (when (re-search-forward re (save-excursion
                                     (outline-next-heading)
                                     (point))
                                t)
         (setq date (match-string 1))
         (unless (string= date new-start-date)
           (org-schedule nil (shift-date date old-start-date)))))
     "TODO=\"TODO\"")))

Here's another version based on the comments. Please test and report back.

** TODO prepare and distribute syllabus (first day of class: Sept 03, 2019)
   SCHEDULED: <2019-09-03 Tue>
   :PROPERTIES:
   :RELATIVE-DATE: 0 week
   :END:

** TODO explain group project (two weeks after classes started)
   SCHEDULED: <2019-09-17 Tue>
   :PROPERTIES:
   :RELATIVE-DATE: 2 week
   :END:

** TODO Create groups (on the Wednesday, three weeks after classes started)
   SCHEDULED: <2019-09-25 Wed>
   :PROPERTIES:
   :RELATIVE-DATE: 3 week wed
   :END:
(defvar initial-date "<2020-01-13 Mon>")

(defun get-dow (date &optional n)
  "Return the day of the week from DATE and shift it by N days."
  (let ((date (org-time-string-to-time date)))
    (format-time-string
     "%a" (if n (org-time-add date (* 86400 n))
            date))))

(defun shift-to-dow (date dow)
  "Shift DATE to the next DOW (day of the week)."
  (let (newdate
        (dow (capitalize dow)))
    (cl-loop for i from 1 to 7
             if (string= dow (get-dow date))
             collect
             (setq newdate (org-time-string-to-time date))
             else
             when (string= dow (get-dow date i))
             collect
             (setq newdate (time-add
                            (org-time-string-to-time date)
                            (* 86400 i))))
    (format-time-string "<%F %a>" newdate)))

(defun reschedule-entries ()
  (interactive)
  (org-map-entries
   (lambda ()
     (when (assoc "RELATIVE-DATE" (org-entry-properties))
       (let* (newdate
              (elt (org-element-at-point))
              (reldate (split-string
                        (org-element-property :RELATIVE-DATE elt) " "))
              (value (string-to-number (cl-first reldate)))
              (unit (intern (cl-second reldate)))
              (dow (cl-third reldate)))
         (with-temp-buffer
           (insert initial-date)
           (forward-line 0)
           (if (eq unit 'week)
               ;; no 'week in org-timestamp-change so change it to days
               (org-timestamp-change (* value 7) 'day)
             (org-timestamp-change value unit))
           (setq newdate (buffer-substring (point-min)
                                           (point-max))))
         (org-schedule nil (if dow
                               (shift-to-dow newdate dow)
                             newdate)))))
   "TODO=\"TODO\""))
| improve this answer | |
  • There are two more reasons why the answer does not quite do what I wish (one of which I realized after posting the question): 1) the third rescheduled deadline should fall on a Wednesday. 2) When the semester starts, some of the scheduled dates need to be modified (for instance, while I typically like to create the reading package a month in advance, this semester I've moved this deadlne two weeks ahead). I'd like to be able to update the new semester with the "ideal dates", not with the dates that I happened to end up at the end of the semester with). – SaMeji Dec 22 '19 at 21:34
  • I'm not sure why you're seeing different results. Maybe you're using an older version of Org. – jagrg Dec 24 '19 at 0:58
  • @SaMeji I updated the answer to reflect your comments. – jagrg Dec 24 '19 at 1:22
  • Wow! Thanks so much. It works as a charm. Could I ask for one minor tweak? I'd like initial-date to be a customizable variable. Could you replace, in your response, defvar for a defcustom? (or tell me what this defcustom's variable :type should be in the defcustom declaration to do it myself?). – SaMeji Dec 24 '19 at 3:47
  • 1
    You can change the initial-date variable using setq, or use it as a file variable. No need to delete your answer IMO. – jagrg Dec 24 '19 at 11:21
0

I am not sure how to properly answer the question, but I have an idea that may help someone more capable to answer the question.

The trick may be to use two properties/variables: 1) A global variable with the initial date (InitialDate), and 2) a variable local to each TODO that specifies the relative difference with respect to the initial date (relativeDate). As I imane it RelativeDate would be a string in the format used by org-schedule to take advantage of this function's functionality. One would feed the initial date and the relative difference to a function that relies on org-schedule.

Here is an example of how the file with the schedule would look like

:PROPERTIES:
#+MACRO: InitialDate 2020-01-13

** TODO prepare and distribute syllabus (first day of class: Sept 03, 2019)
   SCHEDULED: <2021-01-13 Mon>
   :PROPERTIES:
    #+MACRO: RelativeDate +0

** TODO explain group project (two weeks after classes started)
   SCHEDULED: <2021-01-27 Wed>
   :PROPERTIES:
    #+MACRO: RelativeDate +2w

** TODO Create groups (on the Wednesday, three weeks after classes started)
   SCHEDULED: <2019-12-31 Tue>
   :PROPERTIES:
    #+MACRO: RelativeDate +3Wed
| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.