[Go] time.AddDate() で月を加算する

# はじめに

Golang の [time](https://pkg.go.dev/time) パッケージの`AddDate()`について、
挙動を理解していないが為に重要なバグを生んでしまったので、まとめてみました。

# 結論


- `AddDate()`で月を加算すると加算前の月の日数(1月だったら31日)が加算される
- つまり、2022年1月31日に`AddDate()`で月を1加算すると、2022年3月3日になる

# 実際に起こったこと

それは、今月の月と来月の月を返す関数`getMonths()`を実装した際に起こりました。

まずは、実装したコードを見てください。

```golang
func getMonths() []int {
  targetTime := time.Now()                                                                                                                                                                                  
  
  calendars := make([]int, 2)
  
  for i, _ := range calendars {
  
    calendars[i] = int(targetTime.Month())
  
    targetTime = targetTime.AddDate(0, 1, 0)
  }
  
  return calendars
}
```

このコードはバグります。

たとえば1月30日の場合、[1, 3]が返ってしまいます。

# time.AddDateの挙動を見てみる

一旦、time.AddDateの[実装部分](https://github.com/golang/go/blob/a5c0b190809436fd196a348f85eca0416f4de7fe/src/time/time.go#L906)を見てみましょう。

```golang
func (t Time) AddDate(years int, months int, days int) Time {
	year, month, day := t.Date()
	hour, min, sec := t.Clock()
	return Date(year+years, month+Month(months), day+days, hour, min, sec, int(t.nsec()), t.Location())
}
```

上のコードでわかる通り、月の部分は`month+Month(months)`となっており、
現在の月に引数の月(int型をMonthにキャストする)を足した値を返しています。

time.Date()では次のような[実装](https://github.com/golang/go/blob/a5c0b190809436fd196a348f85eca0416f4de7fe/src/time/time.go#L1402)になっています。

```golang
func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time {
	if loc == nil {
		panic("time: missing Location in call to Date")
	}

	// Normalize month, overflowing into year.
	m := int(month) - 1
	year, m = norm(year, m, 12)
	month = Month(m) + 1

	// Normalize nsec, sec, min, hour, overflowing into day.
	sec, nsec = norm(sec, nsec, 1e9)
	min, sec = norm(min, sec, 60)
	hour, min = norm(hour, min, 60)
	day, hour = norm(day, hour, 24)

	// Compute days since the absolute epoch.
	d := daysSinceEpoch(year)

	// Add in days before this month.
	d += uint64(daysBefore[month-1])
	if isLeap(year) && month >= March {
		d++ // February 29
	}

	// Add in days before today.
	d += uint64(day - 1)

	// Add in time elapsed today.
	abs := d * secondsPerDay
	abs += uint64(hour*secondsPerHour + min*secondsPerMinute + sec)

	unix := int64(abs) + (absoluteToInternal + internalToUnix)

	// Look for zone offset for expected time, so we can adjust to UTC.
	// The lookup function expects UTC, so first we pass unix in the
	// hope that it will not be too close to a zone transition,
	// and then adjust if it is.
	_, offset, start, end, _ := loc.lookup(unix)
	if offset != 0 {
		utc := unix - int64(offset)
		// If utc is valid for the time zone we found, then we have the right offset.
		// If not, we get the correct offset by looking up utc in the location.
		if utc < start || utc >= end {
			_, offset, _, _, _ = loc.lookup(utc)
		}
		unix -= int64(offset)
	}

	t := unixTime(unix, int32(nsec))
	t.setLoc(loc)
	return t
}
```
月を算出している部分は以下の部分で、

```golang
   // Compute days since the absolute epoch.
    d := daysSinceEpoch(year)

    // Add in days before this month.
    d += uint64(daysBefore[month-1])
    if isLeap(year) && month >= March {
        d++ // February 29
    }

```

ここでは引数のyearまでの日数を取得し、
引数の月までの日数を加算しています。


daysBefore は以下のような値です。

```golang
var daysBefore = [...]int32{
	0,
	31,
	31 + 28,
	31 + 28 + 31,
	31 + 28 + 31 + 30,
	31 + 28 + 31 + 30 + 31,
	31 + 28 + 31 + 30 + 31 + 30,
	31 + 28 + 31 + 30 + 31 + 30 + 31,
	31 + 28 + 31 + 30 + 31 + 30 + 31 + 31,
	31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
	31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
	31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
	31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31,
}
```

つまり、Date()の引数にyear=2022, month=3を渡した場合、
2022年1月1日までの総日数を取得し、
2022年1月1日〜2022年3月1日 までの日数(31+28)を加算しているわけです。

# 2022年1月31日で`AddDate(0, 1, 0)`をすると

2022年1月31日で`AddDate(0, 1, 0)`をすると、

内部で2022年1月1日までの総日数を取得し、
2022年1月1日から2022年2月1日までの総日数31日分が加算されます。

つまり、
2022年1月31日が2022年3月3日になります。

# 改めて今月の月と来月の月を返す関数を実装してみる

これらを踏まえてもう一度`getMonths()`を実装してみると、

```golang
func getMonths() []int {
  loc, _ := time.LoadLocation("Asia/Tokyo")
  targetTime := time.Now()

  months := make([]int, 2)

  for i, _ := range months {

    months[i] = int(targetTime.Month())

    targetTime = time.Date(targetTime.Year(), targetTime.Month()+1, 1, 0, 0, 0, 0, loc)                                                                                                                     
  }

  return months
}
```
これで意図した関数になりました。
または、ややこしいことはしないで、

```golang
func getMonths() []int {    
  now := time.Now()                                                                                                                                                                                         
  
  month := now.Month()         

  months := make([]int, 2)

  for i, _ := range months {

    months[i] = int(month)

    if month == 12 {
      month = 1
    } else {
      month++
    }
  }

  return months
}
```
これでももちろん十分です。

# まとめ

今回は`AddTime()`で月を加算するとどうなるかまとめてみました。
今思えばそりゃそうだろって感じですが、雑魚なりに意外と深いところまで読めました。