0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

はじめに

Golang の time パッケージのAddDate()について、
挙動を理解していないが為に重要なバグを生んでしまったので、まとめてみました。

結論

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

実際に起こったこと

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

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

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の実装部分を見てみましょう。

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()では次のような実装になっています。

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
}

月を算出している部分は以下の部分で、

   // 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 は以下のような値です。

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()を実装してみると、

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
}

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

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()で月を加算するとどうなるかまとめてみました。
今思えばそりゃそうだろって感じですが、雑魚なりに意外と深いところまで読めました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?