SHORTCUT: Copy and paste code into the *scratch*
buffer; type M-x eval-buffer RET
; and, then type M-x count-business-days RET
.
DOCUMENTATION: As mentioned in the comment underneath the question, this answer is dependent upon the holiday calendar defined by the particular user -- e.g., federal holidays in U.S.; state court holidays (e.g., California, U.S.); or national holidays in another country. The variable custom-holidays
defined below has common U.S. federal holidays, and the user is free to custom tailor that variable to suit his / her individual needs. The variables mark-target-date
and move-cursor-to-target-date
are both set to t
and should be self-explanitory based on the doc-string. The date format for the initial date uses read-string
because org-read-date
is set up to use overlays instead of the type of default value that I prefer to see squarely placed inside the minibuffer; thereafter, org-read-date
interprets that. In addition, org-read-date
is set up to bury the *Calendar*
buffer after a date is selected, and we don't want that behavior here. We use org-read-date
; however, we only feed it a date already selected by the user. org-read-date
can be picky about the format, so I have included an initial default based upon the location of the cursor position in the *Calendar*
buffer as a starting point. Any format recognized by org-read-date
is acceptable -- see the doc-string of org-read-date
for generally recognized date formats. The mathematics generated correct results in my initial testing using the three types of possibilities -- i.e., beginning date as a holiday, or as a weekend, or as a weekday. If there is a special case I have not considered, or something seems not right, please let me know. The holidays list only goes back two (2) months in time -- for example, if the user is selecting an initial date of January 6, 2015, then the holiday list will include the months of November 2014, December 2014 and January 2015. On the todo-list is a check and error message in the event a user tries to select a number of business days beyond the outer limits of the available holiday schedule. [It is possible to have a year calendar; however, that is beyond the scope of this example.]
HISTORY
First Rough Draft (January 6, 2015): First rough draft.
(defvar mark-target-date t
"When `t`, highlight the target date.
When `nil`, do not highlight the target date.")
(defvar move-cursor-to-target-date t
"When `t`, move the calendar cursor to the target date.
When `nil`, do not move the cursor to the target date.")
(defcustom custom-holidays
(mapcar 'purecopy
'((holiday-fixed 1 1 "New Year's Day")
(holiday-float 1 1 3 "Martin Luther King Day")
(holiday-float 2 1 3 "President's Day")
(holiday-float 5 1 -1 "Memorial Day")
(holiday-fixed 7 4 "Independence Day")
(holiday-float 9 1 1 "Labor Day")
(holiday-float 10 1 2 "Columbus Day")
(holiday-fixed 11 11 "Veteran's Day")
(holiday-float 11 4 4 "Thanksgiving")
(holiday-fixed 12 25 "Christmas")) )
"Custom holidays defined by the user."
:type 'sexp
:group 'holidays)
(defun count-business-days ()
"Count x number of business days backwards -- excluding holidays defined by the
user in the variable `custom-holidays`; and, excluding Saturdays and Sundays."
(interactive)
(require 'holidays)
(require 'org)
(require 'calendar)
(unless (get-buffer-window calendar-buffer (selected-frame))
(calendar))
(let* (
holidays
(counter 1)
(cursor-date (when (get-buffer calendar-buffer) ;; format is: '(month day year)
(with-current-buffer calendar-buffer (calendar-cursor-to-nearest-date))))
(proposed-month (when cursor-date
(if (< (nth 0 cursor-date) 10)
(format "%02d" (nth 0 cursor-date))
(number-to-string (nth 0 cursor-date)))))
(proposed-day (when cursor-date
(if (< (nth 1 cursor-date) 10)
(format "%02d" (nth 1 cursor-date))
(number-to-string (nth 1 cursor-date)))))
(proposed-year (when cursor-date (number-to-string (nth 2 cursor-date))))
;; `org-read-date` likes the format of: year-month-day
(proposed-date
(when cursor-date (concat proposed-year "-" proposed-month "-" proposed-day)))
(initial-date (read-string
"year-month-day of initial date: "
proposed-date nil proposed-date nil))
(days (read-number "Number of Days (before): "))
(day-variable (org-read-date nil t initial-date nil)) )
(let* (
(displayed-month (nth 4 (decode-time day-variable)))
(displayed-year (nth 5 (decode-time day-variable))) )
(calendar-increment-month displayed-month displayed-year -1)
(dolist (holiday
(let (res h)
(dolist (p custom-holidays res)
(when (setq h (eval p))
(setq res (append h res))))))
(setq holidays (append holidays (list holiday)))) )
(catch 'done
(while t
(setq day-variable (time-subtract day-variable (days-to-time 1)))
(let* (
(day (nth 3 (decode-time day-variable)))
(month (nth 4 (decode-time day-variable)))
(year (nth 5 (decode-time day-variable)))
(day-of-week (nth 6 (decode-time day-variable)))
(gregorian (list month day year))
holiday-p)
(mapcar (lambda (x)
(if (equal gregorian (car x))
(setq holiday-p t))) holidays)
;; DEBUGGING
;; (message "holidays: %s" holidays)
;; (message "greg: %s | count: %s | day: %s | day-variable: %s"
;; gregorian counter day-of-week (decode-time day-variable))
(if
(and
(= counter days)
(memq day-of-week '(1 2 3 4 5))
(not holiday-p))
(throw 'done nil)
(when
(and
(memq day-of-week '(1 2 3 4 5))
(not holiday-p))
(setq counter (1+ counter)))))))
(when (get-buffer calendar-buffer)
(let* (
(decoded-target (decode-time day-variable))
(month-target (nth 4 decoded-target))
(day-target (nth 3 decoded-target))
(year-target (nth 5 decoded-target))
(target (list month-target day-target year-target)) )
(unless
(with-current-buffer calendar-buffer (calendar-date-is-visible-p target))
(with-current-buffer calendar-buffer
(calendar-generate month-target year-target)))
(when mark-target-date
(calendar-mark-visible-date target 'holiday)) ;; `'holiday` is the face.
(when move-cursor-to-target-date
(calendar-cursor-to-visible-date target))))
(message "Initial Date: %s | Target Date: %s | Business Days: %s"
initial-date (format-time-string "%Y-%m-%d" day-variable) days)))