#+title: My Sprint Calculations and Support #+author: Howard X. Abrams #+date: 2020-09-25 #+tags: emacs work A literate program for configuring org files for work-related notes. #+begin_src emacs-lisp :exports none ;;; org-sprint --- Configuring org files for work-related notes. -*- lexical-binding: t; -*- ;; ;; © 2020-2024 Howard X. Abrams ;; Licensed under a Creative Commons Attribution 4.0 International License. ;; See http://creativecommons.org/licenses/by/4.0/ ;; ;; Author: Howard X. Abrams ;; Maintainer: Howard X. Abrams ;; Created: September 25, 2020 ;; ;; This file is not part of GNU Emacs. ;; ;; *NB:* Do not edit this file. Instead, edit the original literate file at: ;; ~/other/hamacs/org-sprint.org ;; And tangle the file to recreate this one. ;; ;;; Code: #+end_src * 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 [[file:snippets/org-mode/__sprint.org][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. #+begin_src emacs-lisp :var sprint-names=sprint-names-2024 (defvar sprint-nicknames sprint-names "List of 26 Sprint Nicknames from A to Z.") #+end_src ** 2024 How about Tabaxi names this year … especially since my next character has to be a cat man. #+name: sprint-names-2024 - 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? #+name: sprint-names-2023 - 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 [[https://list.fandom.com/wiki/List_of_monsters][this list of monsters]] from mythology and other sources: #+name: sprint-names-2022 - 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 [[https://www.imagineforest.com/blog/funniest-words-in-the-english-language/][Funny or Silly Words]]: #+name: sprint-names-2021 - 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 [[https://en.m.wikipedia.org/wiki/List_of_dinosaur_genera][list of dinosaurs]]. #+name: sprint-names-2020 - 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. #+name: sprint-names-2019 - 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. #+begin_src emacs-lisp (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)) #+end_src ** Sprint Numbering My Sprint starts on Tuesday, but this sometimes changed, so let's make this a variable: #+begin_src emacs-lisp (defvar sprint-starting-day 2 "The day of the week the sprint begins, where 0 is Sunday.") #+end_src We seem to never start our Sprints correctly, and we seem to like offsets: #+begin_src emacs-lisp ;; CHANGEME Each year as this never matches: (defvar sprint-offset-value 11 "The number of the first sprint.") #+end_src 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. #+begin_src emacs-lisp (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))) #+end_src Let's have these tests to make of this /weekly/ perspective: #+begin_src emacs-lisp :tangle no (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 #+end_src 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. #+begin_src emacs-lisp (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+ ))) #+end_src And some tests to verify that: #+begin_src emacs-lisp :tangle no (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))) #+end_src ** Sprint File Name I create my org-file notes based on the Sprint number. #+begin_src emacs-lisp (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))))) #+end_src So given a particular date, I should expect to be able to find the correct Sprint file name: #+begin_src emacs-lisp :tangle no (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")))) #+end_src Daily note-taking goes into my sprint file notes, so this interactive function makes an easy global short-cut key. #+begin_src emacs-lisp (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))) #+end_src The /name/ and /nickname/ of the sprint will be used in the =#+TITLE= section, and it looks something like: =Sprint 2019-07 (darling-dadu)= #+begin_src emacs-lisp (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))) #+end_src These test won't pass any more, as the nickname of the sprint changes from year to year. #+begin_src emacs-lisp :tangle no (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")))) #+end_src ** 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. #+begin_src emacs-lisp ;; (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))) #+end_src Format the start and end so that we can insert this directly in the org file: #+begin_src emacs-lisp (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)))) #+end_src And validate with a test: #+begin_src emacs-lisp (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")))) #+end_src ** 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: #+begin_src emacs-lisp (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)))) #+end_src * Other Date Functions The following functions /were/ helpful at times. But I'm not sure I will use them. #+begin_src emacs-lisp :tangle no (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)))) #+end_src #+begin_src emacs-lisp :tangle no (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))))) #+end_src #+begin_src emacs-lisp :tangle no (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")))) #+end_src #+begin_src emacs-lisp :tangle no (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))) #+end_src * Technical Artifacts :noexport: Let's =provide= a name so we can =require= this file: #+begin_src emacs-lisp :exports none (provide 'ha-org-sprint) ;;; ha-org-sprint.el ends here #+end_src Before you can build this on a new system, make sure that you put the cursor over any of these properties, and hit: ~C-c C-c~ #+description: A literate program for configuring org files for work-related notes. #+property: header-args:sh :tangle no #+property: header-args:emacs-lisp :tangle yes #+property: header-args :results none :eval no-export :comments no mkdirp yes #+options: num:nil toc:t todo:nil tasks:nil tags:nil date:nil #+options: skip:nil author:nil email:nil creator:nil timestamp:nil #+infojs_opt: view:nil toc:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js