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.