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 nicknamesprint-date-range
to include a org-formatted date range beginning and ending the sprintsprint-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)))