diff --git a/civil/civil.go b/civil/civil.go index cf663022dea4..9e9541dfa6d3 100644 --- a/civil/civil.go +++ b/civil/civil.go @@ -86,6 +86,18 @@ func (d Date) AddDays(n int) Date { return DateOf(d.In(time.UTC).AddDate(0, 0, n)) } +// AddMonths returns the date that is n months in the future. +// n can also be negative to go into the past. +func (d Date) AddMonths(n int) Date { + return DateOf(d.In(time.UTC).AddDate(0, n, 0)) +} + +// AddYears returns the date that is n years in the future. +// n can also be negative to go into the past. +func (d Date) AddYears(n int) Date { + return DateOf(d.In(time.UTC).AddDate(n, 0, 0)) +} + // DaysSince returns the signed number of days between the date and s, not including the end day. // This is the inverse operation to AddDays. func (d Date) DaysSince(s Date) (days int) { @@ -127,6 +139,11 @@ func (d Date) IsZero() bool { return (d.Year == 0) && (int(d.Month) == 0) && (d.Day == 0) } +// Weekday returns the day of the week for the date. +func (d Date) Weekday() time.Weekday { + return d.In(time.UTC).Weekday() +} + // MarshalText implements the encoding.TextMarshaler interface. // The output is the result of d.String(). func (d Date) MarshalText() ([]byte, error) { diff --git a/civil/civil_test.go b/civil/civil_test.go index 590e77b90af9..23cc469d6991 100644 --- a/civil/civil_test.go +++ b/civil/civil_test.go @@ -163,6 +163,106 @@ func TestDateArithmetic(t *testing.T) { } } +func TestDateArithmeticMonths(t *testing.T) { + for _, test := range []struct { + desc string + start Date + end Date + months int + }{ + { + desc: "zero months noop", + start: Date{2024, 12, 16}, + end: Date{2024, 12, 16}, + months: 0, + }, + { + desc: "crossing a year boundary", + start: Date{2014, 8, 31}, + end: Date{2015, 1, 31}, + months: 5, + }, + { + desc: "negative number of months", + start: Date{2015, 1, 1}, + end: Date{2014, 12, 1}, + months: -1, + }, + { + desc: "full leap year", + start: Date{2008, 1, 1}, + end: Date{2009, 1, 1}, + months: 12, + }, + { + desc: "full non-leap year", + start: Date{1997, 1, 1}, + end: Date{1998, 1, 1}, + months: 12, + }, + { + desc: "crossing a leap second", + start: Date{1972, 6, 30}, + end: Date{1972, 7, 30}, + months: 1, + }, + { + desc: "dates before the unix epoch", + start: Date{101, 1, 1}, + end: Date{101, 6, 1}, + months: 5, + }, + } { + if got := test.start.AddMonths(test.months); got.Compare(test.end) != 0 { + t.Errorf("[%s] %#v.AddMonths(%v) = %#v, want %#v", test.desc, test.start, test.months, got, test.end) + } + } +} + +func TestDateArithmeticYears(t *testing.T) { + for _, test := range []struct { + desc string + start Date + end Date + years int + }{ + { + desc: "zero years noop", + start: Date{2024, 6, 19}, + end: Date{2024, 6, 19}, + years: 0, + }, + { + desc: "positive number of years", + start: Date{2012, 4, 29}, + end: Date{2014, 4, 29}, + years: 2, + }, + { + desc: "negative number of years", + start: Date{2027, 1, 1}, + end: Date{2024, 1, 1}, + years: -3, + }, + { + desc: "crossing a leap second", + start: Date{1972, 6, 30}, + end: Date{1973, 6, 30}, + years: 1, + }, + { + desc: "dates before the unix epoch", + start: Date{99, 1, 1}, + end: Date{102, 1, 1}, + years: 3, + }, + } { + if got := test.start.AddYears(test.years); got.Compare(test.end) != 0 { + t.Errorf("[%s] %#v.AddDays(%v) = %#v, want %#v", test.desc, test.start, test.years, got, test.end) + } + } +} + func TestDateBefore(t *testing.T) { for _, test := range []struct { d1, d2 Date @@ -226,6 +326,22 @@ func TestDateIsZero(t *testing.T) { } } +func TestDateWeekday(t *testing.T) { + for _, test := range []struct { + date Date + want time.Weekday + }{ + {Date{2024, 12, 21}, time.Saturday}, + {Date{1900, 1, 1}, time.Monday}, + {Date{2482, 3, 17}, time.Tuesday}, + } { + got := test.date.Weekday() + if got != test.want { + t.Errorf("%#v: got %v, want %v", test.date, got, test.want) + } + } +} + func TestTimeToString(t *testing.T) { for _, test := range []struct { str string