My Sprint Calculations and Support

Table of Contents

A literate program for configuring org files for work-related notes.

Introduction

At the beginning of each Sprint, I create a new org file dedicated to it. This workflow/technique strikes a balance between a single ever-growing file, and a thousand little ones. This also gives me a sense of continuity, as the filename of each sprint is date-based.

I want a single keybinding that always displays the current Sprint note file, regardless of the Sprint. This means, I need to have functions that can calculate what this is.

To have the Org Capture features to be able to write to correct locations in the current file, I need each file to follow a particular format. I create a sprint note template that will be automatically expanded with a new sprint.

This template needs the following functions:

  • sprint-current-name to be both the numeric label as well as the nickname
  • sprint-date-range to include a org-formatted date range beginning and ending the sprint
  • sprint-date-from-start return a date for pre-scheduled and recurring meetings

Naming Sprints

I give each sprint a nickname, based on a theme of some sorts, alphabetized. Since our sprints are every two weeks, this allows me to go through the alphabet once. Yeah, my group likes to boringly number the sprints, so I do both… for myself.

At the beginning of the year, I choose a theme, and make a list for the upcoming sprints. In the org file, this is a list, that gets tangled into an actual Emacs LIsp list. This is pretty cool.

(defvar sprint-nicknames sprint-names
  "List of 26 Sprint Nicknames from A to Z.")

2024

How about Tabaxi names this year … especially since my next character has to be a cat man.

  • Art of Shadows
  • Burning Desire
  • Cloud in the Sky
  • Daydream at Night
  • Elegant Scarf
  • Fine Stripe
  • Game of Chance
  • Half Patch
  • Icy Beauty
  • Joyful Sound
  • Merry Kite
  • Light in the Morning
  • Mad Bell
  • Night Dance
  • Odorous Flower
  • Plume of Smoke
  • Quick Knot
  • Rhythm of Drums
  • Spell of Rain
  • Tranquil Path
  • Under Clouds
  • Vibrant Spell
  • Trail in the Woods
  • Xpaiyoc
  • Yell at Night
  • Zip in the Wind

2023

How about a list of Ent names?

  • ashskin
  • birchblossom
  • cedar king
  • dirk dogwood
  • elmable
  • firneedle
  • gentle gingko
  • hazel hawthorn
  • incensit
  • juniperspine
  • katsura katsu
  • leaf laurel
  • merv maple
  • nutmeg
  • oakspire
  • pine nettle
  • quiverherb
  • redbeardy
  • spruce stand
  • teak tendril
  • upas crown
  • viburnum burr
  • willowscar
  • xylosma
  • yewbiquitous
  • zelkova

2022

Fun sprint names for 2022 lists my favorite D&D monsters, also see this list of monsters from mythology and other sources:

  • ankheg
  • beholder
  • centaur
  • dragon
  • elf
  • fetch
  • goblin
  • hydra
  • illythid
  • jackalwere
  • kobold
  • lich
  • mimic
  • nymph
  • owlbear
  • pegasus
  • quasit
  • remorhaz
  • satyr
  • troll
  • unicorn
  • vampire
  • warg
  • xorn
  • yuan-ti
  • zombie

2021

Choosing Sprint Names based on Funny or Silly Words:

abibliophobia
The fear of running out of reading materials to read
bamboozled
To trick or confuse someone
catawampus
Something positioned diagonally
dweeb
A boring and uninteresting person
eep
Another expression of surprise or fear.
formication
The feeling that ants are crawling on your skin.
goombah
An older friend who protects you.
hootenanny
A country music party or get-together.
Izzat
This relates to your personal respect and dignity.
jabberwock
Something that is complete nonsense or gibberish
kebbie
A Scottish term relating to a walking stick with a hooked end.
lollygagger
Someone who walks around with no aim or goal.
mollycoddle
To be extra nice to someone or to overprotect them.
nacket
A light lunch or snack.
obi
A sash worn around the waist of a kimono
panjandrum
Someone who thinks that they are superior to others.
quoz
Something that is strange.
ratoon
The small root that sprouts from a plant, especially during the springtime.
sialoquent
Someone who splits while talking.
taradiddle
this is a small lie or when someone is speaking nonsense.
urubu
A blank vulture found in South American.
vamp
To make something brand-new.
wabbit
A Scottish word referring to feeling exhausted or a little unwell.
xanthoderm
A person with yellowish skin.
yerk
Pull or push something with a sudden movement.
zazzy
Something that is shiny and flashy

2020

New names from list of dinosaurs.

  • ankylosaurus
  • brontosaurus
  • coelophysis
  • diplodocus
  • eoraptor
  • fruitadens
  • gobiceratops
  • harpymimus
  • iguanodozn
  • jinfengopteryx
  • kentrosaurus
  • lambeosaurus
  • maiasaura
  • neimongosaurus
  • oviraptor
  • pachycephalosaurus
  • quetzalcoatlus
  • rioarribasaurus
  • stegosaurus
  • tyrannosaurus
  • utahraptor
  • velociraptor
  • wannanosaurus
  • xiaotingia
  • yi
  • zuul

2019

Came up with a list of somewhat well-known cities throughout the world (at least, they had to have a population of 100,000 or more), but I didn't want any real obvious ones.

  • achy-aachen
  • bare-bacabal
  • candid-cannes
  • darling-dadu
  • easy-edmonton
  • fancy-fargo
  • gray-gaya
  • handsome-hanoi
  • itchy-incheon
  • jumpy-juba
  • kind-kindia
  • less-liling
  • mad-madrid
  • natural-naga
  • octarine-oakland
  • painful-paris
  • quirky-qufu
  • rabid-rabat
  • slow-slough
  • typing-taipei
  • ugly-ufa
  • vibrant-vienna
  • wacky-waco
  • xenophobic-xichang
  • yellow-yamaguchi
  • zippy-zinder

Sprint Boundaries

Function to help in calculating dates and other features of a two-week sprint that starts on Tuesday and ends on a Monday… how we work at my job.

Emacs have an internal rep of a time.

(defun get-date-time (date)
  "My functions can't deal with dates as string, so this will
parse DATE as a string, or return the value given otherwise."
  (if (and date (stringp date))
      (->> date                 ; Shame that encode-time
           parse-time-string    ; can't take a string, as
           (-take 6)            ; this seems excessive...
           (--map (if (null it) 0 it))
           (apply 'encode-time))
    date))

Sprint Numbering

My Sprint starts on Tuesday, but this sometimes changed, so let's make this a variable:

(defvar sprint-starting-day 2 "The day of the week the sprint begins, where 0 is Sunday.")

We seem to never start our Sprints correctly, and we seem to like offsets:

;; CHANGEME Each year as this never matches:
(defvar sprint-offset-value 11 "The number of the first sprint.")

We label our sprint based on the week number that it starts. Note that on a Monday, I want to consider that we are still numbering from last week.

(defun sprint-week-num (&optional date)
  "Return the week of the current year (or DATE), but starting
the week at Tuesday to Monday."
  (let* ((d (get-date-time date))
         (dow (nth 6 (decode-time d)))    ; Day of the week 0=Sunday
         (week (thread-last d             ; Week number in the year
                            (format-time-string "%U")
                            string-to-number)))
    (if (>= dow sprint-starting-day)
        (1+ week)
      week)))

Let's have these tests to make of this weekly perspective:

(ert-deftest sprint-week-num-test ()
  (should (= (sprint-week-num "2024-01-01") 0))  ; Monday previous week
  (should (= (sprint-week-num "2024-01-02") 1))  ; Tuesday ... this week
  (should (= (sprint-week-num "2024-01-09") 2))  ; Monday, next week, part of last
  (should (= (sprint-week-num "2024-01-10") 3))) ; Tuesday next week

My company has sprints two weeks long, we could be see that on even week numbers, the sprint is actually the previous week's number.

This year, my PM decided to start the sprints sequentially starting with 11, so I’ve decided to follow my own naming convention for my filenames.

(defun sprint-number (&optional date)
  "Return the current sprint number, with some assumptions that
each sprint is two weeks long, starting on Tuesday."
  (let* ((num (sprint-week-num date))
         (bucket (if (cl-oddp num) num (1- num))))
    (thread-first bucket
                  ;; Make 2 week sprints sequential:
                  (/ 2)
                  ;; Sprint offset number:
                  ;; (+ sprint-offset-value)
                  1+
                  )))

And some tests to verify that:

(ert-deftest sprint-number-test ()
  (should (= (sprint-number "2024-01-02") 1))
  (should (= (sprint-number "2024-01-10") 1))
  (should (= (sprint-number "2024-01-15") 1))
  (should (= (sprint-number "2024-01-16") 2))
  (should (= (sprint-number "2024-01-23") 2))
  (should (= (sprint-number "2024-01-29") 2))
  (should (= (sprint-number "2024-01-30") 3))
  (should (= (sprint-number "2024-02-13") 4)))

Sprint File Name

I create my org-file notes based on the Sprint number.

(defun sprint-current-file (&optional date)
  "Return the absolute pathname to the current sprint file."
  (let ((d (get-date-time date)))
    (expand-file-name
     (format "~/Notes/Sprint-%s-%02d.org"
             (format-time-string "%Y" d)
             (sprint-number d)))))

So given a particular date, I should expect to be able to find the correct Sprint file name:

(ert-deftest sprint-current-file-test ()
  (should (s-ends-with? "Sprint-2024-11.org" (sprint-current-file "2024-01-02")))
  (should (s-ends-with? "Sprint-2024-12.org" (sprint-current-file "2024-01-16")))
  (should (s-ends-with? "Sprint-2024-13.org" (sprint-current-file "2024-02-01")))
  (should (s-ends-with? "Sprint-2024-14.org" (sprint-current-file "2024-02-13"))))

Daily note-taking goes into my sprint file notes, so this interactive function makes an easy global short-cut key.

(defun sprint-current-find-file (&optional date)
  "Load the `org-mode' note associated with my current sprint."
  (interactive)
  (let ((filename (sprint-current-file date)))
     (setq org-main-file filename
           org-annotate-file-storage-file filename)
     (add-to-list 'org-agenda-files filename)
     (find-file filename)))

The name and nickname of the sprint will be used in the #+TITLE section, and it looks something like: Sprint 2019-07 (darling-dadu)

(defun sprint-current-name (&optional date)
  "Return the default name of the current sprint (based on DATE)."
  (let* ((d (get-date-time date))
         (sprint-num (sprint-number d))
         (nickname (nth (1- sprint-num) sprint-nicknames)))
    (format "Sprint %s-%02d :: %s"
            (format-time-string "%Y" d)
            (sprint-number d)
            nickname)))

These test won't pass any more, as the nickname of the sprint changes from year to year.

(ert-deftest sprint-current-name-test ()
  (should (equal "Sprint 2024-01 :: Art of Shadows" (sprint-current-name "2024-01-02")))
  (should (equal "Sprint 2024-04 :: Daydream at Night" (sprint-current-name "2024-02-14"))))

Sprint Start and End

I want to print the beginning and ending of the sprint, where we have a sprint number or a data, and we can give the dates that bound the sprint. This odd function calculates this based on knowing the date of the first Tuesday of the year, so I need to begin the year changing this value. I should fix this.

;; (setq number-or-date "2024-01-16")
(defun sprint-range (&optional number-or-date)
  "Return a list of three entries, start of the current sprint,
end of the current sprint, and the start of the next sprint.
Each date value should be formatted with `format-time-string'."
  ;;          The `num' should be 0-based:
  (let* ((num (if (or (null number-or-date) (stringp number-or-date))
                  (* 2 (1- (sprint-number number-or-date)))
                (1- number-or-date)))
         ;; CHANGEME each year to mark the first day of the first sprint:
         (time-start   (-> "2024-01-02"     ; Converted to time
                           get-date-time
                           float-time))
         (day-length   (* 3600 24))      ; Length of day in seconds
         (week-length  (* day-length 7))
         (sprint-start (time-add time-start (* week-length  num)))
         (sprint-next  (time-add time-start (* week-length (+ 2 num))))
         (sprint-end   (time-add sprint-next (- day-length))))
    (list sprint-start sprint-end sprint-next)))

Format the start and end so that we can insert this directly in the org file:

(defun sprint-date-range (&optional number-or-date)
  "Return `org-mode' formatted date range for a given sprint.
The NUMBER-OR-DATE is a week number, a date string, or if `nil'
for the current date."
  (seq-let (sprint-start sprint-end) (sprint-range number-or-date)
    (let* ((formatter    "%Y-%m-%d %a")
           (start        (format-time-string formatter sprint-start))
           (end          (format-time-string formatter sprint-end)))
      (format "[%s]--[%s]" start end))))

And validate with a test:

(ert-deftest sprint-date-range ()
  (should (equal (sprint-date-range)
                 (sprint-date-range (format-time-string "%Y-%m-%d"))))
  (should (equal (sprint-date-range 1)
                 (sprint-date-range "2024-01-02")))
  (should (equal (sprint-date-range 1)
                 (sprint-date-range "2024-01-15")))
  (should (equal (sprint-date-range 3)
                 (sprint-date-range "2024-01-16")))
  (should (equal (sprint-date-range 5)
                 (sprint-date-range "2024-01-31"))))

Pre-scheduled Dates

Due to the regularity of the sprint cadence, I can pre-schedule meetings and other deadlines by counting the number of days from the start of the sprint:

(defun sprint-date-from-start (days &optional formatter)
  "Given a number of DAYS from the start of the sprint, return a formatted date string."
  (let* ((day-length (* 3600 24))
         (start (car (sprint-range)))
         (adate (time-add start (* day-length days))))
    (if formatter
        (format-time-string formatter adate)
      (format-time-string "%Y-%m-%d %a" adate))))

Other Date Functions

The following functions were helpful at times. But I'm not sure I will use them.

(defun sprint-num-days (time-interval)
  "Converts a TIME-INTERVAL to a number of days."
  (let ((day-length (* 3600 24)))
    (round (/ (float-time time-interval) day-length))))
(defun sprint-day-range (&optional date)
  "Returns a list of two values, the number of days from the
start of the sprint, and the number of days to the end of the
sprint based on DATE if given, or from today if DATE is `nil'."
  (seq-let (sprint-start sprint-end) (sprint-range date)
    (let* ((now (get-date-time date))
           (starting (time-subtract sprint-start now))
           (ending (time-subtract sprint-end now)))
      (list (sprint-num-days starting) (sprint-num-days ending)))))
(ert-deftest sprint-day-range ()
  ;; This sprint starts on 2/13 and ends on 2/26
  (should (equal '(0 13) (sprint-day-range "2020-02-13")))
  (should (equal '(-1 12) (sprint-day-range "2020-02-14")))
  (should (equal '(-13 0) (sprint-day-range "2020-02-26"))))
(defun sprint-day-start (&optional date)
  "Return a relative number of days to the start of the current sprint. For instance, if today was Friday, and the sprint started on Tuesday, this would return -1."
  (first (sprint-day-range date)))

(defun sprint-day-end (&optional date)
  "Return a relative number of days to the end of the current sprint. For instance, if today was Monday, and the sprint will end on Monday, this would return 3."
  (second (sprint-day-range date)))