2022-11-07 Counting working days again

Some time ago I wrote about counting business days in Emacs. I mentioned that I’m going to revisit that topic in the future. Well, the future is now! Recall that the need I had was to know how many business days there are in any given month.

Previously, I examined the Calc functions for date calculations. This time, I also looked at the default Emacs calendar (which also knows about many holidays), only to discover that it is extremely convoluted. One thing I found which might be helpful in my use-case was the calendar-check-holidays function. You can give it a date and get a list of strings describing the holidays on that date.

This, however, is of not much use for me. First of all, I’d prefer something much simpler – a function I can give a date to and get a Boolean telling me if it is a working day or not. Also, I want it to be configurable. Some of the holidays known to Emacs (even ones I do care about and want to be reminded of, if only to know when my American coworkers won’t be available) are working days in Poland. On the other hand, sometimes I take a day off for personal reasons and I don’t want my function to count it as a working day (because for me it is not).

So, I decided to come up with my own code. It turns out that Emacs’ calendar was helpful after all – it gave me functions to get the current date, to get the number of the days in the current month and to get the day of week for a given date. Of course, the most difficult part was the DOW calculations. I won’t explain those in detail – let’s treat that part of the code as a small puzzle for the readers;-).

As for the UI, I decided to go with three commands. workdays-populate-weekends is something to be run at the beginning of each month to seed the list of days off with Saturdays and Sundays. workdays-add-day-off is a simple command to add a day off not falling on a weekend. Lastly, workdays-count-workdays tells the user how many workdays there are in the current month and how many have already elapsed (including today).

(defvar workdays-days-off-this-month ()
  "Days off this month.")

(defun workdays-list-weekends (year month)
  "List Sats and Suns on YEAR-MONTH."
  (let* ((last (calendar-last-day-of-month month year))
         (dow (calendar-day-of-week (list month 1 year)))
         (sat (- dow)))
    (seq-filter
     (lambda (day) (< (% (- day sat) 7) 2))
     (number-sequence 1 last))))

(defun workdays-populate-weekends ()
  "Populate `workdays-days-off-this-month' for this month."
  (interactive)
  (let* ((today (calendar-current-date))
         (year (caddr today))
         (month (car today)))
    (setq workdays-days-off-this-month
          (workdays-list-weekends year month))))

(defun workdays-add-day-off (day)
  "Add DAY to `workdays-days-off-this-month'."
  (interactive (list (if current-prefix-arg
                         (prefix-numeric-value current-prefix-arg)
                       (read-number "Day number: "))))
  (push day workdays-days-off-this-month)
  (setq workdays-days-off-this-month
        (seq-uniq
         (sort workdays-days-off-this-month #'<))))

(defun workdays-count-workdays (&optional msg)
  "Count workdays this month and the number of elapsed ones
(including today).  Return a list (workdays elapsed)."
  (interactive "p")
  (let* ((today (calendar-current-date))
         (year (caddr today))
         (month (car today))
         (day (cadr today))
         (last (calendar-last-day-of-month month year))
         (count
          (- last (length workdays-days-off-this-month)))
         (elapsed
          (- day
             (length (seq-take-while
                      (lambda (d) (<= d day))
                      workdays-days-off-this-month)))))
    (when msg
      (message "Workdays in the current month: %s/%s"
               elapsed count))
    (list elapsed count)))

And that’s it for today. As usual, the takeaway (apart of the code, if anyone needs something like this) is that Emacs is a very nice environment, where writing little helpers for everyday life can be both simple (well, at least compared to what it would look like in some other environments…) and fast.

CategoryEnglish, CategoryBlog, CategoryEmacs