990cc577dc
Now the sprint and inserts are better.
505 lines
18 KiB
Org Mode
505 lines
18 KiB
Org Mode
#+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 <http://gitlab.com/howardabrams>
|
||
;; 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:templates/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
|