DEV Community is a community of 661,481 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Nordcloud

How a calculation of previous month in golang can break production

Grzegorz Kotlarz
💻 Software Engineer☁️ Cloud Computing Enthusiast🎙️ AjTiTi [PL]

Illustration from “A Journey With Go”, created by Renee French.

In one of the applications that I'm working on, I have to calculate what is a previous month of a given date. It seems easy, isn't it? Golang provides the time package which has a method: AddDate.

I'm quite fresh in Go, previously I was working with C#, Python and (for some time) TypeScript. Every one of these languages has also some methods to calculate time. Let's see how they work.

C# .NET 5:

``````using System;

public class Program
{
public static void Main()
{
var endOfMay = new DateTime(2021, 05, 31, 12, 00, 00);
Console.WriteLine("End of May: {0}", endOfMay);
Console.WriteLine("End of April: {0}", endOfApril);
Console.WriteLine("End of June: {0}", enfOfJune);
}
}
``````

Output:

``````End of May: 05/31/2021 12:00:00
End of April: 04/30/2021 12:00:00
End of June: 06/30/2021 12:00:00
``````

Looks good 👍

Python 3.9

Here I will use dateutil.relativedelta:

``````import datetime
from dateutil.relativedelta import relativedelta

end_of_may = datetime.datetime(2021, 5, 31, 12, 0, 0, 0)
end_of_april = end_of_may + relativedelta(months=-1)
end_of_june = end_of_may + relativedelta(months=1)
print(f'End Of May: {end_of_may}')
print(f'End Of April: {end_of_april}')
print(f'End of June: {end_of_june}')
``````

Let's see logs:

``````End Of May: 2021-05-31 12:00:00
End Of April: 2021-04-30 12:00:00
End of June: 2021-06-30 12:00:00
``````

Exactly what I was looking for 👌

Go

Now, the code which broke my service:

``````package main

import (
"fmt"
"time"
)

func main() {
endOfMay := time.Date(2021, 5, 31, 12, 00, 00, 00, time.UTC)
fmt.Printf("End of May: %s\n", endOfMay)
fmt.Printf("End of April: %s\n", endOfApril)
fmt.Printf("End of June: %s\n", endOfJune)
}
``````

And the output:

``````End of May: 2021-05-31 12:00:00 +0000 UTC
End of April: 2021-05-01 12:00:00 +0000 UTC
End of June: 2021-07-01 12:00:00 +0000 UTC
``````

🤔 🤔 🤔
Something is wrong...
... and exactly that happened on production on 31 May 2021 in my project. This piece of code broke production. Funny, because it was broken just for one day. And in the next few days, until the end of July, it will work fine again.

This behavior of the `AddDate` was totally unexpected for me. I wanted to blame Go for doing it, but... I can only blame myself 😅 because in documentation of AddDate method we can find:

AddDate normalizes its result in the same way that Date does, so, for example, adding one month to October 31 yields December 1, the normalized form for November 31.

Not what I was expecting from my experience with Python and C# 🤷‍♂️ But, documented. So - my fault 😕

Takeaways

In the end, 3 takeaways for you and myself from the future:

1. Reading documentation can be useful 😄
2. Write tests for edge cases.
3. Handling dates in the programming world is a tough job.

Postscriptum - TypeScript

TypeScript is doing the same thing as Go.

``````const endOfMay: Date = new Date(2021, 4, 31, 12, 0, 0, 0); // months are starting from 0 here 🤦
console.log(`End of May: \${endOfMay}`);
const endOfApril = new Date(endOfMay);
endOfApril.setMonth(endOfMay.getMonth() - 1);
console.log(`End of April: \${endOfApril}`);
const endOfJune = new Date(endOfMay);
endOfJune.setMonth(endOfMay.getMonth() + 1);
console.log(`End of June: \${endOfJune}`);
``````

Output:

``````End of May: Mon May 31 2021 12:00:00 GMT+0200 (Central European Summer Time)
End of April: Sat May 01 2021 12:00:00 GMT+0200 (Central European Summer Time)
End of June: Thu Jul 01 2021 12:00:00 GMT+0200 (Central European Summer Time)
``````

but dayjs handles it perfectly.

``````import dayjs from "dayjs";

const endOfMayDate = new Date(2021, 4, 31, 12, 0, 0, 0);
const endOfMay = dayjs(endOfMayDate);
console.log(`End of May: \${endOfMay}`);
console.log(`End of April: \${endOfApril}`);
console.log(`End of June: \${endOfJune}`);
``````

Output:

``````End of May: Mon, 31 May 2021 10:00:00 GMT
End of April: Fri, 30 Apr 2021 10:00:00 GMT
End of June: Wed, 30 Jun 2021 10:00:00 GMT
``````

Discussion (8)

Glavić

Go and TypeScript produce correct date. If you subtract 1 month from 31 May, you will get 1 May and not last of previous month. 31 May - 1month is 31 April, but that is overflown date, so 31 April is converted to 1 May.
PHP also works correctly: sandbox.onlinephpfunctions.com/cod...

Grzegorz Kotlarz

Hello, thanks for your input 😊 Interesting to see how other languages work! Also, thanks for your clarification on why it happens - I forgot to write it in the post.

About the correctness - from my perspective, there is no easy way to tell which of the outputs is correct. Based on my experience from Python and C#, I've expectations that GoLang will produce the same output. I'm not blaming any of the languages, cause, for me, there is no correct answer 🤷‍♂️ There is only an assumption of language/library creator, and I wrote this post to aware other programmers.

BTW - Interesting things happen when you are subtracting from 31.05.2021 and, immediately, adding a one month to the previous calculation - neither in C# nor Go, you are not back to 31.05.2021. You will be at 30.05.2021, or at 1.06.2021.
Examples:
play.golang.org/p/rd1xjc1CbKJ
dotnetfiddle.net/fTO9IX

Glavić

1.6. is correct when calculating overflow dates into equation.
31.5. minus 1 month is 31.4. whish is overflown by 1 day, so this becomes 1.5.
And then you add 1 month on 1.5. which becomes 1.6. This is expacted bahavious in Go and PHP. Sorry I don't know why python and C# are doing that way :(

Glavić • Edited

On other hand, PostgreSQL and MySQL both return 30.4. on 31.5.-month.

MySQL:

``````select date_add("2021-05-30", interval -1 month)
``````

PostgreSQL:

``````select '2021-05-31'::timestamp - '1 month'::interval
``````
Grzegorz Kotlarz • Edited

Very thanks for your input and curiosity. I also dig deeper into other languages. Ruby and Java behave like C#.

Interesting thing happens in Rust, where you don't have a method to add or subtract months. You can however do something like this:

``````    let end_of_april = NaiveDate::from_ymd(end_of_may.year(), end_of_may.month() - 1, end_of_may.day());
``````

and it... panics because of the out-of-range date. You can check the snippet

I ran onto it here.