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.