Good evening

This is a story of how a simple presumption led to terrible bugs. And that dealing with time is difficult even in modern languages.

All I wanted to do was to get the same day in the next month.

The simplest solution coming to mind would be using the built-in AddDate, like this:

fmt.Println(time.Date(2019,4,7,12,36,59,1234,time.UTC).AddDate(0,1,0))

We gave it the 7th of April, it returns the 7th of May.

But this approach has a corner-case bug. If you put 31st of January 2019 there, it will output 3rd of March (it jumps to what would be considered 31st of February, and the applies normalization, making it equivalent to 3rd of March). Two calendar months ahead. This was definitely not what I wanted.

For my purposes, I had decided that I wanted it to show the last day of the next month if this day is not present in it, but it must output the next calendar month in any case.

This is not available in Golang out-of-the-box. Let’s implement it.

Last day of month

This is comparatively easy. Found the answer here, I’ll just paste it here.

func GetLastDayOfMonth(t time.Time) int {
	return time.Date(t.Year(), t.Month(), 1, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location()).AddDate(0, 1, -1).Day()
}

You essentially go to the first day of the current month (which is always the 1st), use AddDate to jump one month forward (to the 1st) and then one day backward, yielding the last day of the current month.

Same day (or last) next month

Finally, let’s get the same day next month, or last one.

func GetSameDayNextMonth(t time.Time) time.Time {
	last := GetLastDayOfMonth(time.Date(t.Year(), t.Month()+1, 1, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location()))
	if t.Day() > last {
		return time.Date(t.Year(), t.Month()+1, last, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
	}

	return time.Date(t.Year(), t.Month()+1, t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
}

I simply use time.Date and assign data field by field. That way I circumvent normalization.

Other functions

Previous month

func GetSameDayPreviousMonth(t time.Time) time.Time {
	last := GetLastDayOfMonth(time.Date(t.Year(), t.Month()-1, 1, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location()))
	if t.Day() > last {
		return time.Date(t.Year(), t.Month()-1, last, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
	}

	return time.Date(t.Year(), t.Month()-1, t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
}

Specified day within a month

These are a bit extended. In addition to the date, you provide a day you want to set the date to. But again, if this day does not exist within the given month, it will set it to last day. But they will always be within the calendar month: current or next, respectively.

func GetOtherDayWithinMonth(t time.Time, day int) time.Time {
	last := GetLastDayOfMonth(t)
	dayToSet := day
	if day > last || day <= 0 {
		dayToSet = last
	}

	return time.Date(t.Year(), t.Month(), dayToSet, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
}

func GetOtherDayNextMonth(t time.Time, day int) time.Time {
	last := GetLastDayOfMonth(time.Date(t.Year(), t.Month()+1, 1, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location()))
	if day > last || day <= 0 {
		return time.Date(t.Year(), t.Month()+1, last, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
	}

	return time.Date(t.Year(), t.Month()+1, day, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), t.Location())
}

As a bonus

A function that returns the number of calendar days between dates (as if there were no times in them)

func GetNumberOfDaysBetweenDates(t1, t2 time.Time) int64 {
	return int64(math.Abs(GetMidnightDate(t1).Sub(GetMidnightDate(t2)).Hours() / 24))
}

func GetMidnightDate(t time.Time) time.Time {
	return t.Truncate(time.Hour * 24)
}

In conclusion

Time is a fickle matter to tinker with. And most programming languages struggle with providing enough tools for all developers. Oh, and don’t get me started with timezones, please.

Thanks for tuning in!