From de08b3856a7df09f69c47f8ab043a995501dcb26 Mon Sep 17 00:00:00 2001 From: Kei Kamikawa Date: Sun, 12 Nov 2023 23:45:15 +0900 Subject: [PATCH] copied some google-cloud-go/civil Date methods into iso8601 package --- iso8601/date.go | 63 ++++++++++++++++++++++++- iso8601/date_test.go | 108 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 1 deletion(-) diff --git a/iso8601/date.go b/iso8601/date.go index f113d86..b371c76 100644 --- a/iso8601/date.go +++ b/iso8601/date.go @@ -370,6 +370,13 @@ var _ interface { encoding.TextUnmarshaler } = (*Date)(nil) +// DateOf returns the iso8601 Date in which a time occurs in that time's location. +func DateOf(t time.Time) Date { + var d Date + d.Year, d.Month, d.Day = t.Date() + return d +} + // String returns the ISO8601 string representation of the format "YYYY-MM-DD". // For example: "2012-12-01". func (d Date) String() string { @@ -422,7 +429,61 @@ func (d Date) Validate() error { // StdTime converts the Date structure to a time.Time object, using UTC for the time. func (d Date) StdTime() time.Time { - return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, time.UTC) + return d.In(time.UTC) +} + +// In returns the time corresponding to time 00:00:00 of the date in the location. +// +// In is always consistent with time.Date, even when time.Date returns a time +// on a different day. For example, if loc is America/Indiana/Vincennes, then both +// +// time.Date(1955, time.May, 1, 0, 0, 0, 0, loc) +// +// and +// +// iso8601.Date{Year: 1955, Month: time.May, Day: 1}.In(loc) +// +// return 23:00:00 on April 30, 1955. +// +// In panics if loc is nil. +func (d Date) In(loc *time.Location) time.Time { + return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, loc) +} + +// AddDays returns the date that is n days in the future. +// n can also be negative to go into the past. +func (d Date) AddDays(n int) Date { + return DateOf(d.StdTime().AddDate(0, 0, n)) +} + +// 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) { + // We convert to Unix time so we do not have to worry about leap seconds: + // Unix time increases by exactly 86400 seconds per day. + deltaUnix := d.StdTime().Unix() - s.StdTime().Unix() + return int(deltaUnix / 86400) +} + +// Before reports whether d occurs before d2. +func (d Date) Before(d2 Date) bool { + if d.Year != d2.Year { + return d.Year < d2.Year + } + if d.Month != d2.Month { + return d.Month < d2.Month + } + return d.Day < d2.Day +} + +// After reports whether d occurs after d2. +func (d Date) After(d2 Date) bool { + return d2.Before(d) +} + +// IsZero reports whether date fields are set to their default value. +func (d Date) IsZero() bool { + return (d.Year == 0) && (int(d.Month) == 0) && (d.Day == 0) } // QuarterDate converts a Date to a QuarterDate. diff --git a/iso8601/date_test.go b/iso8601/date_test.go index 9c57a05..9641d17 100644 --- a/iso8601/date_test.go +++ b/iso8601/date_test.go @@ -3430,3 +3430,111 @@ func TestOrdinalDate_UnmarshalText(t *testing.T) { }) } } + +func TestDateArithmetic(t *testing.T) { + for _, test := range []struct { + desc string + start Date + end Date + days int + }{ + { + desc: "zero days noop", + start: Date{2014, 5, 9}, + end: Date{2014, 5, 9}, + days: 0, + }, + { + desc: "crossing a year boundary", + start: Date{2014, 12, 31}, + end: Date{2015, 1, 1}, + days: 1, + }, + { + desc: "negative number of days", + start: Date{2015, 1, 1}, + end: Date{2014, 12, 31}, + days: -1, + }, + { + desc: "full leap year", + start: Date{2004, 1, 1}, + end: Date{2005, 1, 1}, + days: 366, + }, + { + desc: "full non-leap year", + start: Date{2001, 1, 1}, + end: Date{2002, 1, 1}, + days: 365, + }, + { + desc: "crossing a leap second", + start: Date{1972, 6, 30}, + end: Date{1972, 7, 1}, + days: 1, + }, + { + desc: "dates before the unix epoch", + start: Date{101, 1, 1}, + end: Date{102, 1, 1}, + days: 365, + }, + } { + if got := test.start.AddDays(test.days); got != test.end { + t.Errorf("[%s] %#v.AddDays(%v) = %#v, want %#v", test.desc, test.start, test.days, got, test.end) + } + if got := test.end.DaysSince(test.start); got != test.days { + t.Errorf("[%s] %#v.Sub(%#v) = %v, want %v", test.desc, test.end, test.start, got, test.days) + } + } +} + +func TestDateBefore(t *testing.T) { + for _, test := range []struct { + d1, d2 Date + want bool + }{ + {Date{2016, 12, 31}, Date{2017, 1, 1}, true}, + {Date{2016, 11, 30}, Date{2016, 12, 30}, true}, + {Date{2016, 1, 1}, Date{2016, 1, 1}, false}, + {Date{2016, 12, 30}, Date{2016, 12, 31}, true}, + } { + if got := test.d1.Before(test.d2); got != test.want { + t.Errorf("%v.Before(%v): got %t, want %t", test.d1, test.d2, got, test.want) + } + } +} + +func TestDateAfter(t *testing.T) { + for _, test := range []struct { + d1, d2 Date + want bool + }{ + {Date{2016, 12, 31}, Date{2017, 1, 1}, false}, + {Date{2016, 1, 1}, Date{2016, 1, 1}, false}, + {Date{2016, 12, 30}, Date{2016, 12, 31}, false}, + } { + if got := test.d1.After(test.d2); got != test.want { + t.Errorf("%v.After(%v): got %t, want %t", test.d1, test.d2, got, test.want) + } + } +} + +func TestDateIsZero(t *testing.T) { + for _, test := range []struct { + date Date + want bool + }{ + {Date{2000, 2, 29}, false}, + {Date{10000, 12, 31}, false}, + {Date{-1, 0, 0}, false}, + {Date{0, 0, 0}, true}, + {Date{}, true}, + } { + got := test.date.IsZero() + if got != test.want { + t.Errorf("%#v: got %t, want %t", test.date, got, test.want) + } + } +}