Automatically incrementing JD numbers in filenames

Recently I have really gotten into literate programming after reading about it online and in Donald Knuth’s paper1, which I recommend giving a look. It has provided some much needed motivation around mundane tasks while also serving to document my scripts at work.

I store all my literate programs using a folder structure called Johnny Decimal where each file is numbered according to its parent folders. Here’s an example: 16.01 coolCode.org. This is only a part of the JD system, you can read more about it following the link. The problem I faced was one of repetition. Each time I wanted to write a new literate program I had to manually create the file while making sure to increment the JD number. Repetition gets tedious and I am using Emacs with org mode to do my text editing after all. To avoid the tedium I decided to create a set of functions ending in a capture template that will prompt for a title and create the filename with the next JD number automatically. This article will explain my process.

Incrementing the JD number

The first task I set out to solve was to automatically get all the JD numbers in a directory for a given file type so that I could increment it. I wanted to be able to specify the file type as literate programs will always be .org files and it is likely that there will be other file types with the same filename in the directory.

Since I am not at all proficient in Emacs Lisp I relied heavily on the built-in documentation (C-h o) and searching online.

(defun my/get-next-jd-number-in-jd-dir (dir filetype)
  "Returns the next JD number for a given filetype in a JD directory."
  (setq-local jd-number
        (car
         (last
    (mapcar (lambda (f)
        (substring f 0 5))
      (directory-files dir nil
           (concat "\." filetype "$"))))))
  (concat
   (substring jd-number 0 3)
   (format "%02d" (1+ (string-to-number
           (substring jd-number 3 nil))))))

Going through the function

First I set a local variable using setq-local to avoid polluting the global space. I am unsure whether this is considered best practice in Emacs Lisp, but it works so I am fine with it. mapcar takes a list and applies some function to it before returning the modified list. Here I am using it to call (substring f 0 5) on the outputs of directory-files which lists the contents of a given directory. In this case I am interested in .org files as I am leveraging org mode for my literary programming, but the function could be used for any other file type.

Getting the last element of the list

After some searching I found the function last which returns the last link of a list, meaning you will get a list containing exactly one element. In the docstring for last, as you can see below, it is stated that the car of the return value is the last element. So I figured I would wrap the call to last in car, which worked just fine.

Return the last link of LIST. Its car is the last element.

A note on the use of substring

The use of substring in this code is really a consequence of my limited knowledge of Emacs Lisp and not necessarily the best way to do it. In the case of a default JD system we know that the number we are after will be on index 0 through 5, so the hard coded version will work. However, I would very much appreciate feedback or suggestions for improving the function.

Checking the results against the directory

Calling the function my/get-next-jd-number-in-jd-dir returns the number 16.06, but I am not sure it is correct. After all it is just a number, there are no file names and I can’t be bothered to manually check the directory. So I’ll include this boon of literate programming, free of charge!

Org mode in Emacs is great because it lets me evaluate source code blocks in the prose. Here I wrote a quick little script to return to me the contents of my literate programming directory, automatically, just by placing my cursor in the code block and entering the following keyboard shortcut C-c C-c.

(mapcar (lambda (f)
    (format "%s\n" f))
  (directory-files "~/Documents/JD/10-19 Data Collection/16 Literate Programming/" nil "\.org$"))
  • 16.01 coolCode.org
  • 16.02 uplinkingTheDownload.org
  • 16.03 mainframePhasingAndHowItsDone.org
  • 16.04 elementSubjectToChange.org
  • 16.05 genericEventsAsContentInteractions.org

As you can see the function did indeed return the incremented number. Great success, it will save me a lot of typing.

Creating the file path and capturing to it

The last step was to find out how to create a capture template that would prompt me for the title, set the title in the org file, and save the file with a camel cased version of the title. I had to scour the worldwide web for answers before I finally came upon a solution that worked. A global variable dedicated to hold the title string entered in the prompt.

Getting the file path

The following function utilizes my/get-next-jd-number-in-jd-dir to return the incremented JD number and concatenates the result with a camel cased version of the prompted title.

(defun my/jd-prompt-for-new-lit-filename--capture ()
  (setq jd--lit-title (read-string "Title: "))
  (setq-local jd--lit-dir "~/Documents/JD/10-19 Data Collection/16 Literate Programming/"
        jd--lit-filename (expand-file-name
        (concat (my/get-next-jd-number-in-jd-dir jd--lit-dir "org")
          " "
          (s-lower-camel-case jd--lit-title)
          ".org") jd--lit-dir)))

Here are the results of the previous code block. As you can see from the second line I entered the string This is a Test Title and from the first line you can see the incremented, camel cased result.

/Users/eanilsen/Documents/JD/10-19 Data Collection/16 Literate Programming/16.06 thisIsATestTitle.org

This is a Test Title

Defining the capture template

Here I used the variable, which I named jd--lit-title to be sure it would not interfere with any other piece of code, and as you will see below I used the %(sexp) syntax to include it in the capture template.

(setq org-capture-templates
      '(("l" "Literate Program" plain
   (file my/jd-prompt-for-new-lit-filename--capture)
   "%(format \"#+TITLE: %s\" jd--lit-title)\n\n%?"
   :jump-to-captured t)))

Conclusion

I hope this little snippet can assist others in their quest to improve their workflow and also help me by sharing my solution so that others can make it better.

References


  1. Knuth, D. E. (1984). Literate Programming. The Computer Journal, _27_​(2), 97–111. https://doi.org/10.1093/comjnl/27.2.97 ↩︎