Вопрос

In Emacs calendar, one can count days between two dates (including both the start and the end date) using the M-= which runs the command calendar-count-days-region. How can I count days excluding the weekends (Saturday and Sunday) and if defined holidays coming from the variables: holiday-general-holidays and holiday-local-holidays?

Это было полезно?

Решение

I think this essentially breaks down into three parts:

  • Count the days in a region
  • subtract the weekend days
  • subtract the holidays

Emacs already has the first part covered with M-= (calendar-count-days-region), so let's take a look at that function.

Helpful, but unfortunately it reads the buffer and sends the output directly. Let's make a generalized version which takes start and end date parameters and returns the number of days instead of printing them:

(defun my-calendar-count-days(d1 d2)
  (let* ((days (- (calendar-absolute-from-gregorian d1)
                  (calendar-absolute-from-gregorian d2)))
         (days (1+ (if (> days 0) days (- days)))))
    days))

This is pretty much just a copy of the calendar-count-days-region function, but without the buffer reading & writing stuff. Some tests:

(ert-deftest test-count-days ()
  "Test my-calendar-count-days function"
  (should (equal (my-calendar-count-days '(5 1 2014) '(5 31 2014)) 31))
  (should (equal (my-calendar-count-days '(12 29 2013) '(1 4 2014)) 7))
  (should (equal (my-calendar-count-days '(2 28 2012) '(3 1 2012)) 3))
  (should (equal (my-calendar-count-days '(2 28 2014) '(3 1 2014)) 2)))

Now, for step 2, I can't find any built-in function to calculate weekend days for a date range (surprisingly!). Luckily, this /might/ be pretty simple when working with absolute dates. Here's a very naive attempt which simply loops through all absolute dates in the range and looks for Saturdays & Sundays:

(defun my-calendar-count-weekend-days(date1 date2)
  (let* ((tmp-date (if (< date1 date2) date1 date2))
         (end-date (if (> date1 date2) date1 date2))
         (weekend-days 0))
    (while (<= tmp-date end-date)
      (let ((day-of-week (calendar-day-of-week
                          (calendar-gregorian-from-absolute tmp-date))))
        (if (or (= day-of-week 0)
                (= day-of-week 6))
            (incf weekend-days ))
        (incf tmp-date)))
    weekend-days))

That function should be optimized since it does a bunch of unnecessary looping (e.g. we know that the 5 days after Sunday won't be weekend days, so there is no need to convert & test them), but for the purpose of this example I think it's pretty clear and simple. Good Enough for now, indeed. Some tests:

(ert-deftest test-count-weekend-days ()
  "Test my-calendar-count-weekend-days function"
  (should (equal (my-calendar-count-weekend-days
          (calendar-absolute-from-gregorian '(5 1 2014))
          (calendar-absolute-from-gregorian '(5 31 2014))) 9))
  (should (equal (my-calendar-count-weekend-days
          (calendar-absolute-from-gregorian '(4 28 2014))
          (calendar-absolute-from-gregorian '(5 2 2014))) 0))
  (should (equal (my-calendar-count-weekend-days
          (calendar-absolute-from-gregorian '(2 27 2004))
          (calendar-absolute-from-gregorian '(2 29 2004))) 2)))

Lastly, we need to know the holidays in the range, and emacs provides this in the holiday-in-range function! Note that this function calls calendar-holiday-list to determine which holidays to include, so if you really want to search only holiday-general-holidays and holiday-local-holidays you would need to set your calendar-holidays variable appropriately. See C-h v calendar-holidays for the details.

Now we can wrap all this up in a new interactive function which does the three steps above. This is essentially another modified version of calendar-count-days-region that subtracts weekends and holidays before printing the results (see edit below before running):

(defun calendar-count-days-region2 ()
  "Count the number of days (inclusive) between point and the mark 
  excluding weekends and holidays."
  (interactive)
  (let* ((d1 (calendar-cursor-to-date t))
         (d2 (car calendar-mark-ring))
         (date1 (calendar-absolute-from-gregorian d1))
         (date2 (calendar-absolute-from-gregorian d2))
         (start-date (if (<  date1 date2) date1 date2))
         (end-date (if (> date1 date2) date1 date2))
         (days (- (my-calendar-count-days d1 d2)
                  (+ (my-calendar-count-weekend-days start-date end-date)
                     (my-calendar-count-holidays-on-weekdays-in-range
                      start-date end-date)))))
    (message "Region has %d workday%s (inclusive)"
             days (if (> days 1) "s" ""))))

I'm sure someone more knowledgeable about lisp/elisp could simplify/improve these examples considerably, but I hope it at least serves as a starting point.

Actually, now that I've gone through it, I expect somebody to come along any minute and point out that there is an emacs package that already does this...

Edit: DOH!, Bug #001: If a holiday falls on a weekend, that day is removed twice...

Once solution would be to simply wrap holiday-in-range so we can eliminate holidays which were already removed for being on a weekend:

 (defun my-calendar-count-holidays-on-weekdays-in-range (start end)
  (let ((holidays (holiday-in-range start end))
        (counter 0))
    (dolist (element holidays)
      (let ((day (calendar-day-of-week (car element))))
        (if (and (> day 0)
                 (< day 6))
            (incf counter))))
    counter))

I've updated the calendar-count-days-region2 above to use this new function.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top