Code vs. Cells
Motivation
I like Domain-driven design, and seeing how to utilize it. A fun exercise is trying to model everyday problems while building them test driven. I've started doing that with simple problems I would previously have solved with a spreadsheet (i.e. Google Sheets). It's great for learning, and I've realized I get a better result.
Calculating velocity
I have on multiple occasions acted as Scrum Master (I'd argue I haven't run scrum, nor that I want to), and the question of velocity comes up. I've previously created excel sheets where each sheet was a sprint, and then I had a "front page" with the summarized calculations. It was a mess when I suddenly wanted to introduce a new datapoint, or reformat a given page - as references to specific cells would suddenly be broken. It was a mess.
Instead, I created Sprinthon. It's a very simple Python tool - Utilizing Pydantic I was able to fletch out the domain model rather quickly. A sprint simply consists of:
from pydantic import BaseModel, validatorclass SprintScope(BaseModel): initial: Optional[StoryPoints] final: Optional[StoryPoints] burned: Optional[StoryPoints] @property def growth(self) -> Goal: if self.final and self.initial: return self.final - self.initial return None @property def spillover(self) -> Goal: if self.final and self.burned: return self.final - self.burned return Noneclass Sprint(BaseModel): number: int scope: Optional[SprintScope] = None
And suddenly I had a list of sprints:
sprints = [ Sprint( number=2, scope=SprintScope( initial=50, final=55, burned=45, ), workdays=5, ), Sprint( number=1, scope=SprintScope( initial=60, final=61, burned=55, ), workdays=5, ),]
And getting any sort of statistics out of this is pretty trivial. The average burned story points are pretty easy to get:
avg_burned = avg(sprint.scope.burned for sprint in sprints if sprint.scope),
One could easily make it a Rolling average or something more complex if wanted.
Furthermore, I realized I'd want to add working_days
to the model after we had a sprint that was partly swallowed by vacations. Easy to add to the model:
class Sprint(BaseModel): number: int workdays: float | None = None scope: Optional[SprintScope] = None def get_daily_velocity(self) -> float | None: if self.workdays is None or self.scope is None or self.scope.burned is None: return None return round(self.scope.burned / self.workdays, 2)
By allowing optional fields, I can easily enforce various backward and future-proofing, and having an IDE, I can easily update the sprints that don't conform. By writing a few unit tests it is easy to validate that my results are what I expect:
def test_has_daily_velocity(some_sprint: Sprint): some_sprint.scope.burned = 10 some_sprint.workdays = 5 assert some_sprint.get_daily_velocity() == 2
I ended up extending it to create a MarkdownWriter
which means I can export a report of the sprints, which I can convert to PDF or whatever I'd want. That would have been a lot more work in a spreadsheet.
Conclusion
I often forget how easy it is to write code. I often forget how much I enjoy writing code. By using scripting languages (I also wrote a similar tool for another purpose in F#), I can easily help automate the boring stuff. It might be what Automate The Boring Stuff With Python argues as well. But it's quite neat - and by making micro tools they might grow into an open-source package that can help others.
Worst case, you've solved your problem and improved your coding and problem-solving skills in the process.