Wednesday 19 September 2012 — This is more than 12 years old. Be careful.
Today I had to update a function that computed a date range for charting. It accepted a parameter of how many days back to draw the chart. I needed to adapt it to do months too.
Days are easy because datetime.timedelta() accepts a days= argument, and then you can subtract the timedelta from your date. But timedelta does not accept a months= argument, because the date N months before the current date is not well-defined, and depends on exactly what you mean by “month.” For example, what is the date one month before March 30th?
So I had to write my own code to subtract months from a date just as I wanted. Since I was charting months, I actually only needed the answer to be the first day of the month N months ago, which simplified the problem.
Rather than drop the logic right into the compute_chart_dates() function, I pulled it out into its own function. This made it easy to test the month subtraction logic directly. Thinking about testing often leads to better-structured code, because of the extra demands of having usable surface area for the tests to attach to.
Here’s my code:
def subtract_months(when, months):
"""Return the first of the month a certain number of months ago.
`when`: the datetime.date to count back from.
`months`: the number of months to subtract.
"""
when = when.replace(day=1)
years, months = divmod(months, 12)
if years:
when = when.replace(year=when.year-years)
for _ in range(months):
when -= datetime.timedelta(days=1)
when = when.replace(day=1)
return when
When I was done, I had a function that did just what I wanted. I also had a handful of tests of the tricky edge cases, to be sure I had gotten those right. Then I could very simply use that function to extend my chart date function.
Afterward, I thought about how pleasing it was to write the subtract_months() function, and to consider all of its aspects, and to get it just right. It felt like a gem in my hand.
I felt a little bad, too, because it felt indulgent to focus so much effort on this one small function. Shouldn’t I be concentrating more on the rest of the code?
But I realized, this code is the way it’s supposed to be: tight, focused, solid. It’s a pure function, which makes it easy to reason about, and easy to test. This code is the ideal to aim for. Rather than scolding myself for thinking about the gem, I should be trying to make all the code gems.
Of course, the little utilities like subtract_months are easier to make gem-like than the twisty traffic jams at the heart of any real piece of software. But just because it’s hard doesn’t mean you shouldn’t try. The best code will be gems all the way down.
Comments
Note how you can say stuff like date('now','start of month','+1 month','-1 day'); - the C code that implements this is public domain, and there is also a test suite.
Don't feel guilty for spending time making your tools work well.
And I think your point in the above comment is a very good one - if you can't read the code, you won't be able to maintain it later. While I don't really like the loop, I can at least tell what it's doing, and in any realistic application, it'll never be the thing slowing down your code.
However, I believe that it's possible to keep the readability and remove the loop with a modified version of Anton's code (to make the mathematical operations more self-explanatory).
Add a comment: