diff --git a/README.md b/README.md index 48a83086..e4d132da 100644 --- a/README.md +++ b/README.md @@ -48,26 +48,27 @@ Changes since v1: Renamed methods: -| Was | Use instead | -|--------------|---------------------| -| Date`.Local` | Date`.Midnight` | -| Date`.UTC` | Date`.MidnightUTC` | -| Date`.In` | Date`.MidnightIn` | +| Was | Use instead | +|-------------------|-------------------------| +| date.Date`.Local` | date.Date`.Midnight` | +| date.Date`.UTC` | date.Date`.MidnightUTC` | +| date.Date`.In` | date.Date`.MidnightIn` | Deleted methods and functions: -| Was | Use instead | -|---------------|--------------------| -| Date`.Add` | `+` | -| Date`.Sub` | `-` | -| Date`.IsZero` | `== 0` | -| Date`.Equal` | `==` | -| Date`.Before` | `<` | -| Date`.After` | `>` | -| `date.IsLeap` | `gregorian.IsLeap` | -| `date.DaysIn` | `gregorian.DaysIn` | - -Any v1 dates stored as integers will be incorrect; these can be corrected by adding 719528 to them, which is the number of days between year zero (v2) and 1970 (v1). Dates stored as strings will be unaffected. +| Was | Use instead | +|--------------------------------|--------------------| +| date.Date.`Add` | `+` | +| date.Date.`Sub` | `-` | +| date.Date.`IsZero` | `== 0` | +| date.Date.`Equal` | `==` | +| date.Date.`Before` | `<` | +| date.Date.`After` | `>` | +| `date.IsLeap` | `gregorian.IsLeap` | +| `date.DaysIn` | `gregorian.DaysIn` | +| timespan.DateRange.`Normalise` | (not needed) | + +Any v1 dates persistently stored as integers will be incorrect; these can be corrected by adding 719162 (`date.ZeroOffset`) to them, which is the number of days between year zero (v2) and 1970 (v1). Dates stored as strings will be unaffected. ## Credits diff --git a/date.go b/date.go index 18f8cace..3be126f2 100644 --- a/date.go +++ b/date.go @@ -52,11 +52,11 @@ const ( // ZeroDay was the day of 1st January year 1 AD. ZeroDay = time.Monday - // zeroOffset is the number of days between 0001-01-01 and 1970-01-01, using the - // proleptic Gregorian calendar. It is similar to the Rata Die numbering system, - // for which the offset would be 719163 instead. + // ZeroOffset is the number of days between 0001-01-01 and 1970-01-01, using the + // proleptic Gregorian calendar. // This is based on the same Unix calculation as used by time.Time. - zeroOffset = 719162 + // It is similar to the "Rata Die" numbering system, for which the offset would be 719163 instead. + ZeroOffset = 719162 ) // New returns the Date value corresponding to the given year, month, and day. @@ -99,7 +99,7 @@ func Min() Date { // Max returns the largest representable date, which is nearly 6 million years in the future. func Max() Date { - return Date(math.MaxInt32 - zeroOffset) + return Date(math.MaxInt32 - ZeroOffset) } // MidnightUTC returns a Time value corresponding to midnight on the given date d, diff --git a/date_test.go b/date_test.go index cc6fd9e9..780be5ce 100644 --- a/date_test.go +++ b/date_test.go @@ -5,6 +5,7 @@ package date import ( + "fmt" "runtime/debug" "testing" "time" @@ -80,73 +81,95 @@ func TestDate_Time(t *testing.T) { cases := []struct { d Date }{ - {New(-1234, time.February, 5)}, - {New(-1, time.January, 1)}, - {New(0, time.April, 12)}, - {New(1, time.January, 1)}, - {New(1946, time.February, 4)}, - {New(1970, time.January, 1)}, - {New(1976, time.April, 1)}, - {New(1999, time.December, 1)}, - {New(1111111, time.June, 21)}, + {d: New(-1234, time.February, 5)}, + {d: New(-1, time.January, 1)}, + {d: New(0, time.April, 12)}, + {d: New(1, time.January, 1)}, + {d: New(1946, time.February, 4)}, + {d: New(1970, time.January, 1)}, + {d: New(1976, time.April, 1)}, + {d: New(1999, time.December, 1)}, + {d: New(1111111, time.June, 21)}, } zones := []int{-12, -10, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 8, 12} for i, c := range cases { - d := c.d - tUTC := d.MidnightUTC() - if !same(d, tUTC) { - t.Errorf("%d: %v.MidnightUTC() == %v, want date part %v", i, d, tUTC, d) - } - if tUTC.Location() != time.UTC { - t.Errorf("%d: %v.MidnightUTC() == %v, want %v", i, d, tUTC.Location(), time.UTC) - } - - tLocal := d.Midnight() - if !same(d, tLocal) { - t.Errorf("%d: %v.Midnight() == %v, want date part %v", i, d, tLocal, d) - } - if tLocal.Location() != time.Local { - t.Errorf("%d: %v.Midnight() == %v, want %v", i, d, tLocal.Location(), time.Local) - } - - for _, z := range zones { - location := time.FixedZone("zone", z*60*60) - tInLoc := d.MidnightIn(location) - if !same(d, tInLoc) { - t.Errorf("%d: %v.MidnightIn(%d) == %v, want date part %v", i, d, z, tInLoc, d) + t.Run(fmt.Sprintf("%d %s", i, c.d), func(t *testing.T) { + d := c.d + tUTC := d.MidnightUTC() + if !same(d, tUTC) { + t.Errorf("%d: %v.MidnightUTC() == %v, want date part %v", i, d, tUTC, d) } - h, m, s := tInLoc.Clock() - if s != 0 { - t.Errorf("%d: %v.MidnightIn(%d) == %v, want zero seconds but got %d", i, d, z, tInLoc.Location(), s) - } - if m != 0 { - t.Errorf("%d: %v.MidnightIn(%d) == %v, want zero minutes but got %d", i, d, z, tInLoc.Location(), m) - } - if h != 0 { - t.Errorf("%d: %v.MidnightIn(%d) == %v, want zero hours but got %d", i, d, z, tInLoc.Location(), h) - } - if tInLoc.Location() != location { - t.Errorf("%d: MidnightIn(%v) == %v, want %v", i, d, tInLoc.Location(), z) + if tUTC.Location() != time.UTC { + t.Errorf("%d: %v.MidnightUTC() == %v, want %v", i, d, tUTC.Location(), time.UTC) } - t2 := d.Time(clock.New(1, 2, 3, 0), location) - if !same(d, t2) { - t.Errorf("%d: %v.MidnightIn(%d) == %v, want date part %v", i, d, z, t2, d) - } - h, m, s = t2.Clock() - if s != 3 { - t.Errorf("%d: %v.MidnightIn(%d) == %v, want three seconds but got %d", i, d, z, t2.Location(), s) + tLocal := d.Midnight() + if !same(d, tLocal) { + t.Errorf("%d: %v.Midnight() == %v, want date part %v", i, d, tLocal, d) } - if m != 2 { - t.Errorf("%d: %v.MidnightIn(%d) == %v, want two minutes but got %d", i, d, z, t2.Location(), m) + if tLocal.Location() != time.Local { + t.Errorf("%d: %v.Midnight() == %v, want %v", i, d, tLocal.Location(), time.Local) } - if h != 1 { - t.Errorf("%d: %v.MidnightIn(%d) == %v, want one hour but got %d", i, d, z, t2.Location(), h) + + for _, z := range zones { + location := time.FixedZone("zone", z*60*60) + tInLoc := d.MidnightIn(location) + if !same(d, tInLoc) { + t.Errorf("%d: %v.MidnightIn(%d) == %v, want date part %v", i, d, z, tInLoc, d) + } + h, m, s := tInLoc.Clock() + if s != 0 { + t.Errorf("%d: %v.MidnightIn(%d) == %v, want zero seconds but got %d", i, d, z, tInLoc.Location(), s) + } + if m != 0 { + t.Errorf("%d: %v.MidnightIn(%d) == %v, want zero minutes but got %d", i, d, z, tInLoc.Location(), m) + } + if h != 0 { + t.Errorf("%d: %v.MidnightIn(%d) == %v, want zero hours but got %d", i, d, z, tInLoc.Location(), h) + } + if tInLoc.Location() != location { + t.Errorf("%d: MidnightIn(%v) == %v, want %v", i, d, tInLoc.Location(), z) + } + + t2 := d.Time(clock.New(1, 2, 3, 0), location) + if !same(d, t2) { + t.Errorf("%d: %v.MidnightIn(%d) == %v, want date part %v", i, d, z, t2, d) + } + h, m, s = t2.Clock() + if s != 3 { + t.Errorf("%d: %v.MidnightIn(%d) == %v, want three seconds but got %d", i, d, z, t2.Location(), s) + } + if m != 2 { + t.Errorf("%d: %v.MidnightIn(%d) == %v, want two minutes but got %d", i, d, z, t2.Location(), m) + } + if h != 1 { + t.Errorf("%d: %v.MidnightIn(%d) == %v, want one hour but got %d", i, d, z, t2.Location(), h) + } + if t2.Location() != location { + t.Errorf("%d: MidnightIn(%v) == %v, want %v", i, d, t2.Location(), z) + } } - if t2.Location() != location { - t.Errorf("%d: MidnightIn(%v) == %v, want %v", i, d, t2.Location(), z) + }) + } +} + +func TestDate_LastDayOfMonth(t *testing.T) { + cases := []struct { + d Date + exp int + }{ + {d: New(2005, time.January, 15), exp: 31}, + {d: New(2006, time.June, 15), exp: 30}, + {d: New(1971, time.February, 5), exp: 28}, + {d: New(1972, time.February, 5), exp: 29}, + } + for i, c := range cases { + t.Run(fmt.Sprintf("%d %s", i, c.d), func(t *testing.T) { + ld := c.d.LastDayOfMonth() + if ld != c.exp { + t.Errorf("%d: want %d, got %d", i, c.exp, ld) } - } + }) } } @@ -186,21 +209,23 @@ func TestDate_AddDate(t *testing.T) { years, months, days int expected Date }{ - {New(1970, time.January, 1), 1, 2, 3, New(1971, time.March, 4)}, - {New(1999, time.September, 28), 6, 4, 2, New(2006, time.January, 30)}, - {New(1999, time.September, 28), 0, 0, 3, New(1999, time.October, 1)}, - {New(1999, time.September, 28), 0, 1, 3, New(1999, time.October, 31)}, + {d: New(1970, time.January, 1), years: 1, months: 2, days: 3, expected: New(1971, time.March, 4)}, + {d: New(1999, time.September, 28), years: 6, months: 4, days: 2, expected: New(2006, time.January, 30)}, + {d: New(1999, time.September, 28), days: 3, expected: New(1999, time.October, 1)}, + {d: New(1999, time.September, 28), months: 1, days: 3, expected: New(1999, time.October, 31)}, } - for _, c := range cases { - di := c.d - dj := di.AddDate(c.years, c.months, c.days) - if dj != c.expected { - t.Errorf("%v AddDate(%v,%v,%v) == %v, want %v", di, c.years, c.months, c.days, dj, c.expected) - } - dk := dj.AddDate(-c.years, -c.months, -c.days) - if dk != di { - t.Errorf("%v AddDate(%v,%v,%v) == %v, want %v", dj, -c.years, -c.months, -c.days, dk, di) - } + for i, c := range cases { + t.Run(fmt.Sprintf("%d %s", i, c.d), func(t *testing.T) { + di := c.d + dj := di.AddDate(c.years, c.months, c.days) + if dj != c.expected { + t.Errorf("%v AddDate(%v,%v,%v) == %v, want %v", di, c.years, c.months, c.days, dj, c.expected) + } + dk := dj.AddDate(-c.years, -c.months, -c.days) + if dk != di { + t.Errorf("%v AddDate(%v,%v,%v) == %v, want %v", dj, -c.years, -c.months, -c.days, dk, di) + } + }) } } @@ -210,22 +235,24 @@ func TestDate_AddPeriod(t *testing.T) { delta period.Period expected Date }{ - {New(1970, time.January, 1), period.NewYMWD(0, 0, 0, 0), New(1970, time.January, 1)}, - {New(1971, time.January, 1), period.NewYMWD(10, 0, 0, 0), New(1981, time.January, 1)}, - {New(1972, time.January, 1), period.NewYMWD(0, 10, 0, 0), New(1972, time.November, 1)}, - {New(1972, time.January, 1), period.NewYMWD(0, 24, 0, 0), New(1974, time.January, 1)}, - {New(1973, time.January, 1), period.NewYMWD(0, 0, 1, 0), New(1973, time.January, 8)}, - {New(1973, time.January, 1), period.NewYMWD(0, 0, 0, 10), New(1973, time.January, 11)}, - {New(1973, time.January, 1), period.NewYMWD(0, 0, 0, 365), New(1974, time.January, 1)}, - {New(1973, time.January, 3), period.NewYMWD(0, 0, 0, -2), New(1973, time.January, 1)}, - {New(1974, time.January, 1), period.NewHMS(1, 2, 3), New(1974, time.January, 1)}, - {New(1975, time.January, 1), period.NewHMS(24, 2, 3), New(1975, time.January, 2)}, - {New(1975, time.January, 1), period.NewHMS(0, 1440, 0), New(1975, time.January, 2)}, + {in: New(1970, time.January, 1), delta: period.NewYMWD(0, 0, 0, 0), expected: New(1970, time.January, 1)}, + {in: New(1971, time.January, 1), delta: period.NewYMWD(10, 0, 0, 0), expected: New(1981, time.January, 1)}, + {in: New(1972, time.January, 1), delta: period.NewYMWD(0, 10, 0, 0), expected: New(1972, time.November, 1)}, + {in: New(1972, time.January, 1), delta: period.NewYMWD(0, 24, 0, 0), expected: New(1974, time.January, 1)}, + {in: New(1973, time.January, 1), delta: period.NewYMWD(0, 0, 1, 0), expected: New(1973, time.January, 8)}, + {in: New(1973, time.January, 1), delta: period.NewYMWD(0, 0, 0, 10), expected: New(1973, time.January, 11)}, + {in: New(1973, time.January, 1), delta: period.NewYMWD(0, 0, 0, 365), expected: New(1974, time.January, 1)}, + {in: New(1973, time.January, 3), delta: period.NewYMWD(0, 0, 0, -2), expected: New(1973, time.January, 1)}, + {in: New(1974, time.January, 1), delta: period.NewHMS(1, 2, 3), expected: New(1974, time.January, 1)}, + {in: New(1975, time.January, 1), delta: period.NewHMS(24, 2, 3), expected: New(1975, time.January, 2)}, + {in: New(1975, time.January, 1), delta: period.NewHMS(0, 1440, 0), expected: New(1975, time.January, 2)}, } for i, c := range cases { - out := c.in.AddPeriod(c.delta) - if out != c.expected { - t.Errorf("%d: %v.AddPeriod(%v) == %v, want %v", i, c.in, c.delta, out, c.expected) - } + t.Run(fmt.Sprintf("%d %s", i, c.in), func(t *testing.T) { + out := c.in.AddPeriod(c.delta) + if out != c.expected { + t.Errorf("%d: %v.AddPeriod(%v) == %v, want %v", i, c.in, c.delta, out, c.expected) + } + }) } } diff --git a/go.mod b/go.mod index cf7d04f7..f9994dae 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/rickb777/date/v2 require ( github.com/govalues/decimal v0.1.16 - github.com/rickb777/period v1.0.2-beta + github.com/rickb777/period v1.0.3-beta golang.org/x/text v0.14.0 ) diff --git a/go.sum b/go.sum index 923555c3..6a797e68 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/rickb777/period v1.0.1-beta h1:/+sCJBdraZGfWAlotFRohhuyHPhaoZ+f3vh6EW github.com/rickb777/period v1.0.1-beta/go.mod h1:YjZyKY4ZR0iTA0G72rSUmhgPzLzeyM9kjzFpchbW9xg= github.com/rickb777/period v1.0.2-beta h1:8eK5rM6vAq1LzsZ34kuVi5y31rNEqaB8TrIeiiagfOU= github.com/rickb777/period v1.0.2-beta/go.mod h1:YjZyKY4ZR0iTA0G72rSUmhgPzLzeyM9kjzFpchbW9xg= +github.com/rickb777/period v1.0.3-beta h1:SDLaDRLpvec2o86GVfaBwnlk8g9Ch1u/WP078hD6h7Q= +github.com/rickb777/period v1.0.3-beta/go.mod h1:YjZyKY4ZR0iTA0G72rSUmhgPzLzeyM9kjzFpchbW9xg= github.com/rickb777/plural v1.4.1 h1:5MMLcbIaapLFmvDGRT5iPk8877hpTPt8Y9cdSKRw9sU= github.com/rickb777/plural v1.4.1/go.mod h1:kdmXUpmKBJTS0FtG/TFumd//VBWsNTD7zOw7x4umxNw= golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= diff --git a/rep.go b/rep.go index a2853d4f..f9b56159 100644 --- a/rep.go +++ b/rep.go @@ -7,7 +7,7 @@ package date import "time" const ( - secondsPerDay int64 = 60 * 60 * 24 + secondsPerDay = 60 * 60 * 24 ) // encode returns the number of days elapsed from date zero to the date @@ -22,17 +22,17 @@ func encode(t time.Time) Date { _, offset := t.Zone() secs := t.Unix() + int64(offset) if secs >= 0 { - return zeroOffset + Date(secs/secondsPerDay) + return ZeroOffset + Date(secs/secondsPerDay) } // Unfortunately operator / rounds towards 0, so negative values // must be handled differently - return zeroOffset - Date((secondsPerDay-1-secs)/secondsPerDay) + return ZeroOffset - Date((secondsPerDay-1-secs)/secondsPerDay) } // decode returns the Time value corresponding to 00:00:00 UTC of the date // represented by d, the number of days elapsed since date zero. func decode(d Date) time.Time { - secs := int64(d-zeroOffset) * secondsPerDay + secs := int64(d-ZeroOffset) * secondsPerDay return time.Unix(secs, 0).UTC() } diff --git a/sql_test.go b/sql_test.go index c5a4fb80..38164edf 100644 --- a/sql_test.go +++ b/sql_test.go @@ -21,12 +21,12 @@ func TestDate_Scan(t *testing.T) { //{v: "00000101", expected: 0}, //{v: "00000102", expected: 1}, {v: "0001-01-01", expected: 0}, - {v: "19700101", expected: zeroOffset}, - {v: "1970-01-01", expected: zeroOffset}, - {v: "1971-01-01", expected: 365 + zeroOffset}, - {v: "2018-12-31", expected: 17896 + zeroOffset}, - {v: "31/12/2018", expected: 17896 + zeroOffset}, - {v: []byte("19700101"), expected: zeroOffset}, + {v: "19700101", expected: ZeroOffset}, + {v: "1970-01-01", expected: ZeroOffset}, + {v: "1971-01-01", expected: 365 + ZeroOffset}, + {v: "2018-12-31", expected: 17896 + ZeroOffset}, + {v: "31/12/2018", expected: 17896 + ZeroOffset}, + {v: []byte("19700101"), expected: ZeroOffset}, {v: Date(10000).Midnight(), expected: 10000}, } diff --git a/timespan/daterange.go b/timespan/daterange.go index 73120c23..8f47629d 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -18,28 +18,46 @@ type PeriodOfDays int // DateRange carries a date and a number of days and describes a range between two dates. type DateRange struct { - mark date.Date - days PeriodOfDays + start date.Date + days PeriodOfDays // never negative } +// Implementation note: ranges are normalised on construction. This keeps subsequent +// calculations simple. This differs from TimeSpan (which allows negative durations). + // NewDateRangeOf assembles a new date range from a start time and a duration, discarding // the precise time-of-day information. The start time includes a location, which is not // necessarily UTC. The duration can be negative. func NewDateRangeOf(start time.Time, duration time.Duration) DateRange { sd := date.NewAt(start) - ed := date.NewAt(start.Add(duration)) - return DateRange{mark: sd, days: PeriodOfDays(ed - sd)} + if duration < 0 { + ed := date.NewAt(start.Add(duration)) + return DateRange{start: ed, days: PeriodOfDays(sd - ed)} + } else { + ed := date.NewAt(start.Add(duration)) + return DateRange{start: sd, days: PeriodOfDays(ed - sd)} + } +} + +// NewDateRangePeriod assembles a new date range from a start time and a period, discarding +// the precise time-of-day information. The start time includes a location, which is not +// necessarily UTC. The period can be negative. +func NewDateRangePeriod(start time.Time, p period.Period) DateRange { + sd := date.NewAt(start) + et, _ := p.AddTo(start) + ed := date.NewAt(et) + return BetweenDates(sd, ed) } -// NewDateRange assembles a new date range from two dates. These are half-open, so +// BetweenDates assembles a new date range from two dates. These are half-open, so // if start and end are the same, the range spans zero (not one) day. Similarly, if they // are on subsequent days, the range is one date (not two). // The result is normalised. -func NewDateRange(start, end date.Date) DateRange { +func BetweenDates(start, end date.Date) DateRange { if end < start { - return DateRange{mark: end, days: PeriodOfDays(start - end)} + return DateRange{start: end, days: PeriodOfDays(start - end)} } - return DateRange{mark: start, days: PeriodOfDays(end - start)} + return DateRange{start: start, days: PeriodOfDays(end - start)} } // NewYearOf constructs the range encompassing the whole year specified. @@ -61,39 +79,35 @@ func NewMonthOf(year int, month time.Month) DateRange { // EmptyRange constructs an empty range. This is often a useful basis for // further operations but note that the end date is undefined. func EmptyRange(day date.Date) DateRange { - return DateRange{mark: day} + return DateRange{start: day} } // OneDayRange constructs a range of exactly one day. This is often a useful basis for // further operations. Note that the last date is the same as the start date. func OneDayRange(day date.Date) DateRange { - return DateRange{mark: day, days: 1} + return DateRange{start: day, days: 1} } // DayRange constructs a range of n days. // -// Note that n can be negative. In this case, the specified day will be the end day, -// which is outside of the half-open range; the last day will be the day before the -// day specified. +// Note that n can be negative, in which case the start day will be shifted to the +// corresponding earlier date. func DayRange(day date.Date, n PeriodOfDays) DateRange { if n < 0 { - return DateRange{mark: day + date.Date(n), days: -n} + return DateRange{start: day + date.Date(n), days: -n} } return DateRange{day, n} } // Days returns the period represented by this range. This will never be negative. func (dateRange DateRange) Days() PeriodOfDays { - if dateRange.days < 0 { - return -dateRange.days - } return dateRange.days } // IsZero returns true if this has a zero start date and the the range is empty. // Usually this is because the range was created via the zero value. func (dateRange DateRange) IsZero() bool { - return dateRange.days == 0 && dateRange.mark == 0 + return dateRange.days == 0 && dateRange.start == 0 } // IsEmpty returns true if this has a starting date but the range is empty (zero days). @@ -103,46 +117,28 @@ func (dateRange DateRange) IsEmpty() bool { // Start returns the earliest date represented by this range. func (dateRange DateRange) Start() date.Date { - if dateRange.days < 0 { - return dateRange.mark + date.Date(1+dateRange.days) - } - return dateRange.mark + return dateRange.start } // Last returns the last date (inclusive) represented by this range. Be careful because -// if the range is empty (i.e. has zero days), then the last is undefined so an empty date -// is returned. Therefore it is often more useful to use End() instead of Last(). +// if the range is empty (i.e. has zero days), then the last is undefined so the zero date +// is returned. Therefore, it is often more useful to use End() instead of Last(). // See also IsEmpty(). func (dateRange DateRange) Last() date.Date { - if dateRange.days < 0 { - return dateRange.mark // because mark is at the end - } else if dateRange.days == 0 { + if dateRange.days == 0 { return 0 } - return dateRange.mark + date.Date(dateRange.days-1) + return dateRange.End() - 1 } // End returns the date following the last date of the range. End can be considered to // be the exclusive end, i.e. the final value of a half-open range. // -// If the range is empty (i.e. has zero days), then the start date is returned, this being -// also the (half-open) end value in that case. This is more useful than the undefined result -// returned by Last() for empty ranges. +// If the range is empty (i.e. has zero days), then returned date is the same as the start date, +// this being also the (half-open) end value in that case. This is more useful than the undefined +// result returned by Last() for empty ranges. func (dateRange DateRange) End() date.Date { - if dateRange.days < 0 { - return dateRange.mark + 1 // because mark is at the end - } - return dateRange.mark + date.Date(dateRange.days) -} - -// Normalise ensures that the number of days is zero or positive. -// The normalised date range is returned; -// in this value, the mark date is the same as the start date. -func (dateRange DateRange) Normalise() DateRange { - if dateRange.days < 0 { - return DateRange{dateRange.mark + date.Date(dateRange.days), -dateRange.days} - } - return dateRange + return dateRange.start + date.Date(dateRange.days) } // ShiftBy moves the date range by moving both the start and end dates similarly. @@ -151,18 +147,18 @@ func (dateRange DateRange) ShiftBy(days PeriodOfDays) DateRange { if days == 0 { return dateRange } - newMark := dateRange.mark + date.Date(days) - return DateRange{newMark, dateRange.days} + newStart := dateRange.start + date.Date(days) + return DateRange{newStart, dateRange.days} } // ExtendBy extends (or reduces) the date range by moving the end date. -// A negative parameter is allowed and this may cause the range to become inverted -// (i.e. the mark date becomes the end date instead of the start date). +// A negative parameter is allowed. func (dateRange DateRange) ExtendBy(days PeriodOfDays) DateRange { if days == 0 { return dateRange } - return DateRange{dateRange.mark, dateRange.days + days}.Normalise() + newEnd := dateRange.End() + date.Date(days) + return BetweenDates(dateRange.start, newEnd) } // ShiftByPeriod moves the date range by moving both the start and end dates similarly. @@ -180,33 +176,29 @@ func (dateRange DateRange) ShiftByPeriod(delta period.Period) DateRange { if delta.IsZero() { return dateRange } - newMark := dateRange.mark.AddPeriod(delta) - //fmt.Printf("mark + %v : %v -> %v", delta, dateRange.mark, newMark) - return DateRange{newMark, dateRange.days} + newStart := dateRange.start.AddPeriod(delta) + return DateRange{newStart, dateRange.days} } // ExtendByPeriod extends (or reduces) the date range by moving the end date. -// A negative parameter is allowed and this may cause the range to become inverted -// (i.e. the mark date becomes the end date instead of the start date). +// A negative parameter is allowed. func (dateRange DateRange) ExtendByPeriod(delta period.Period) DateRange { if delta.IsZero() { return dateRange } newEnd := dateRange.End().AddPeriod(delta) - //fmt.Printf("%v, end + %v : %v -> %v", dateRange.mark, delta, dateRange.End(), newEnd) - return NewDateRange(dateRange.Start(), newEnd) + return BetweenDates(dateRange.start, newEnd) } // String describes the date range in human-readable form. func (dateRange DateRange) String() string { - norm := dateRange.Normalise() - switch norm.days { + switch dateRange.days { case 0: - return fmt.Sprintf("0 days at %s", norm.mark) + return fmt.Sprintf("0 days at %s", dateRange.start) case 1: - return fmt.Sprintf("1 day on %s", norm.mark) + return fmt.Sprintf("1 day on %s", dateRange.start) default: - return fmt.Sprintf("%d days from %s to %s", norm.days, norm.Start(), norm.Last()) + return fmt.Sprintf("%d days from %s to %s", dateRange.days, dateRange.start, dateRange.Last()) } } @@ -216,13 +208,13 @@ func (dateRange DateRange) Contains(d date.Date) bool { if dateRange.days == 0 { return false } - return dateRange.Start() <= d && d <= dateRange.Last() + return dateRange.start <= d && d <= dateRange.Last() } // StartUTC assumes that the start date is a UTC date and gets the start time of that date, as UTC. // It returns midnight on the first day of the range. func (dateRange DateRange) StartUTC() time.Time { - return dateRange.Start().MidnightUTC() + return dateRange.start.MidnightUTC() } // EndUTC assumes that the end date is a UTC date and returns the time a nanosecond after the end time @@ -262,9 +254,9 @@ func (dateRange DateRange) Merge(otherRange DateRange) DateRange { if dateRange.IsZero() { return otherRange } - minStart := min(dateRange.Start(), otherRange.Start()) + minStart := min(dateRange.start, otherRange.start) maxEnd := max(dateRange.End(), otherRange.End()) - return NewDateRange(minStart, maxEnd) + return BetweenDates(minStart, maxEnd) } // Duration computes the duration (in nanoseconds) from midnight at the start of the date @@ -274,7 +266,7 @@ func (dateRange DateRange) Merge(otherRange DateRange) DateRange { // If the range is greater than approximately 290 years, the result will hard-limit to the // minimum or maximum possible duration (see time.Sub(t)). func (dateRange DateRange) Duration() time.Duration { - return dateRange.End().MidnightUTC().Sub(dateRange.Start().MidnightUTC()) + return dateRange.End().MidnightUTC().Sub(dateRange.start.MidnightUTC()) } // DurationIn computes the duration (in nanoseconds) from midnight at the start of the date @@ -291,7 +283,7 @@ func (dateRange DateRange) DurationIn(loc *time.Location) time.Duration { // StartTimeIn returns the start time in a specified location. func (dateRange DateRange) StartTimeIn(loc *time.Location) time.Time { - return dateRange.Start().MidnightIn(loc) + return dateRange.start.MidnightIn(loc) } // EndTimeIn returns the nanosecond after the end time in a specified location. Along with diff --git a/timespan/daterange_test.go b/timespan/daterange_test.go index 6eb7f55b..369afa09 100644 --- a/timespan/daterange_test.go +++ b/timespan/daterange_test.go @@ -34,6 +34,8 @@ var ( d0410 = New(2015, time.April, 10) d0501 = New(2015, time.May, 1) d1025 = New(2015, time.October, 25) + d1026 = New(2015, time.October, 26) + d1027 = New(2015, time.October, 27) ) var london *time.Location = mustLoadLocation("Europe/London") @@ -47,44 +49,70 @@ func mustLoadLocation(name string) *time.Location { } func TestNewDateRangeOf(t *testing.T) { - dr := NewDateRangeOf(t0327, 7*24*time.Hour) - isEq(t, 0, dr.mark, d0327) - isEq(t, 0, dr.Days(), PeriodOfDays(7)) - isEq(t, 0, dr.IsEmpty(), false) - isEq(t, 0, dr.Start(), d0327) - isEq(t, 0, dr.Last(), d0402) - isEq(t, 0, dr.End(), d0403) + dr1 := NewDateRangeOf(t0327, 7*24*time.Hour) + isEq(t, 0, dr1.start, d0327) + isEq(t, 0, dr1.Days(), PeriodOfDays(7)) + isEq(t, 0, dr1.IsEmpty(), false) + isEq(t, 0, dr1.Start(), d0327) + isEq(t, 0, dr1.Last(), d0402) + isEq(t, 0, dr1.End(), d0403) dr2 := NewDateRangeOf(t0327, -7*24*time.Hour) - isEq(t, 0, dr2.mark, d0327) + isEq(t, 0, dr2.start, d0320) isEq(t, 0, dr2.Days(), PeriodOfDays(7)) isEq(t, 0, dr2.IsEmpty(), false) - isEq(t, 0, dr2.Start(), d0321) - isEq(t, 0, dr2.Last(), d0327) - isEq(t, 0, dr2.End(), d0328) + isEq(t, 0, dr2.Start(), d0320) + isEq(t, 0, dr2.Last(), d0326) + isEq(t, 0, dr2.End(), d0327) +} + +func TestNewDateRangePeriod(t *testing.T) { + dr0 := NewDateRangePeriod(t0327, period.Zero) + isEq(t, 0, dr0.start, d0327) + isEq(t, 0, dr0.Days(), PeriodOfDays(0)) + isEq(t, 0, dr0.IsEmpty(), true) + isEq(t, 0, dr0.Start(), d0327) + isEq(t, 0, dr0.Last(), Zero) + isEq(t, 0, dr0.End(), d0327) + + dr1 := NewDateRangePeriod(t0327, period.NewYMWD(0, 0, 1, 0)) + isEq(t, 0, dr1.start, d0327) + isEq(t, 0, dr1.Days(), PeriodOfDays(7)) + isEq(t, 0, dr1.IsEmpty(), false) + isEq(t, 0, dr1.Start(), d0327) + isEq(t, 0, dr1.Last(), d0402) + isEq(t, 0, dr1.End(), d0403) + + dr2 := NewDateRangePeriod(t0327, period.NewYMWD(0, 0, -1, 0)) + isEq(t, 0, dr2.start, d0320) + isEq(t, 0, dr2.Days(), PeriodOfDays(7)) + isEq(t, 0, dr2.IsEmpty(), false) + isEq(t, 0, dr2.Start(), d0320) + isEq(t, 0, dr2.Last(), d0326) + isEq(t, 0, dr2.End(), d0327) + + dr3 := NewDateRangePeriod(t0327, period.NewYMWD(0, 7, 0, 0)) + isEq(t, 0, dr3.start, d0327) + isEq(t, 0, dr3.Days(), PeriodOfDays(214)) + isEq(t, 0, dr3.IsEmpty(), false) + isEq(t, 0, dr3.Start(), d0327) + isEq(t, 0, dr3.Last(), d1026) + isEq(t, 0, dr3.End(), d1027) } -func TestNewDateRangeWithNormalise(t *testing.T) { - r1 := NewDateRange(d0327, d0402) +func TestBetweenDates_normalise(t *testing.T) { + r1 := BetweenDates(d0327, d0402) isEq(t, 0, r1.Start(), d0327) isEq(t, 0, r1.Last(), d0401) isEq(t, 0, r1.End(), d0402) - r2 := NewDateRange(d0402, d0327) + r2 := BetweenDates(d0402, d0327) isEq(t, 0, r2.Start(), d0327) isEq(t, 0, r2.Last(), d0401) isEq(t, 0, r2.End(), d0402) } func TestEmptyRange(t *testing.T) { - drN0 := DateRange{d0327, -1} - isEq(t, 0, drN0.Days(), PeriodOfDays(1)) - isEq(t, 0, drN0.IsZero(), false) - isEq(t, 0, drN0.IsEmpty(), false) - isEq(t, 0, drN0.Start(), d0327) - isEq(t, 0, drN0.Last(), d0327) - isEq(t, 0, drN0.String(), "1 day on 2015-03-26") - dr0 := DateRange{} isEq(t, 0, dr0.Days(), PeriodOfDays(0)) isEq(t, 0, dr0.IsZero(), true) @@ -169,7 +197,7 @@ func TestShiftAndExtend(t *testing.T) { {DayRange(d0327, 6).ShiftBy(7), 6, d0403, d0409, "6 days from 2015-04-03 to 2015-04-08"}, {DayRange(d0327, 6).ShiftBy(-1), 6, d0326, d0401, "6 days from 2015-03-26 to 2015-03-31"}, {DayRange(d0327, 6).ShiftBy(-7), 6, d0320, d0326, "6 days from 2015-03-20 to 2015-03-25"}, - {NewDateRange(d0327, d0402).ShiftBy(-7), 6, d0320, d0326, "6 days from 2015-03-20 to 2015-03-25"}, + {BetweenDates(d0327, d0402).ShiftBy(-7), 6, d0320, d0326, "6 days from 2015-03-20 to 2015-03-25"}, {EmptyRange(d0327).ExtendBy(0), 0, d0327, d0327, "0 days at 2015-03-27"}, {EmptyRange(d0327).ExtendBy(6), 6, d0327, d0402, "6 days from 2015-03-27 to 2015-04-01"}, @@ -334,7 +362,7 @@ func TestDurationInZoneWithDaylightSaving(t *testing.T) { isEq(t, 0, OneDayRange(d0328).DurationIn(london), time.Hour*24) isEq(t, 0, OneDayRange(d0329).DurationIn(london), time.Hour*23) isEq(t, 0, OneDayRange(d1025).DurationIn(london), time.Hour*25) - isEq(t, 0, NewDateRange(d0328, d0331).DurationIn(london), time.Hour*71) + isEq(t, 0, BetweenDates(d0328, d0331).DurationIn(london), time.Hour*71) } func isEq(t *testing.T, i int, a, b interface{}, msg ...interface{}) { diff --git a/timespan/timespan.go b/timespan/timespan.go index 36766b3f..97f7522d 100644 --- a/timespan/timespan.go +++ b/timespan/timespan.go @@ -32,22 +32,23 @@ func ZeroTimeSpan(start time.Time) TimeSpan { return TimeSpan{start, 0} } -// TimeSpanOf creates a new time span at a specified time and duration. +// TimeSpanOf creates a new time span at a specified time and duration. The duration can +// be negative, e.g. for an alarm event before the mark time. func TimeSpanOf(start time.Time, d time.Duration) TimeSpan { return TimeSpan{start, d} } -// NewTimeSpan creates a new time span from two times. The start and end can be in either +// BetweenTimes creates a new time span from two times. The start and end can be in either // order; the result will be normalised. The inputs are half-open: the start is included and // the end is excluded. -func NewTimeSpan(t1, t2 time.Time) TimeSpan { +func BetweenTimes(t1, t2 time.Time) TimeSpan { if t2.Before(t1) { return TimeSpan{t2, t1.Sub(t2)} } return TimeSpan{t1, t2.Sub(t1)} } -// Start gets the end time of the time span. +// Start gets the start time of the time span. func (ts TimeSpan) Start() time.Time { if ts.duration < 0 { return ts.mark.Add(ts.duration) @@ -64,6 +65,12 @@ func (ts TimeSpan) End() time.Time { return ts.mark.Add(ts.duration) } +// Mark gets the time marked by this timespan. Typically this is the same as Start, but +// it's the same as End for time spans with negative duration. +func (ts TimeSpan) Mark() time.Time { + return ts.mark +} + // Duration gets the duration of the time span. func (ts TimeSpan) Duration() time.Duration { return ts.duration @@ -126,7 +133,7 @@ func (ts TimeSpan) DateRangeIn(loc *time.Location) DateRange { no := ts.Normalise() startDate := date.NewAt(no.mark.In(loc)) endDate := date.NewAt(no.End().In(loc)) - return NewDateRange(startDate, endDate) + return BetweenDates(startDate, endDate) } // Contains tests whether a given moment of time is enclosed within the time span. The @@ -151,16 +158,20 @@ func (ts TimeSpan) Merge(other TimeSpan) TimeSpan { return ts } else { - return NewTimeSpan(ts.mark, other.End()) + return BetweenTimes(ts.mark, other.End()) } } // RFC5545DateTimeLayout is the format string used by iCalendar (RFC5545). Note // that "Z" is to be appended when the time is UTC. +// +// No dashes are used; this follows ISO-8601 Basic Format practice. const RFC5545DateTimeLayout = "20060102T150405" // RFC5545DateTimeZulu is the UTC format string used by iCalendar (RFC5545). Note // that this cannot be used for parsing with time.Parse. +// +// No dashes are used; this follows ISO-8601 Basic Format practice. const RFC5545DateTimeZulu = RFC5545DateTimeLayout + "Z" func layoutHasTimezone(layout string) bool { @@ -196,15 +207,27 @@ func (ts TimeSpan) Format(layout, separator string, useDuration bool) string { layout = RFC5545DateTimeZulu } - s := ts.Start() - e := ts.End() - if useDuration { - p := period.Between(s, e).Normalise(false) - return fmt.Sprintf("%s%s%s", s.Format(layout), separator, p) + return ts.formatWithDuration(layout, separator) } + return ts.formatTwoTimestamps(layout, separator) +} - return fmt.Sprintf("%s%s%s", s.Format(layout), separator, e.Format(layout)) +func (ts TimeSpan) formatTwoTimestamps(layout, separator string) string { + s := ts.Start().Format(layout) + e := ts.End().Format(layout) + return fmt.Sprintf("%s%s%s", s, separator, e) +} + +func (ts TimeSpan) formatWithDuration(layout, separator string) string { + if ts.duration < 0 { + p := period.NewOf(-ts.duration).Normalise(false) + return fmt.Sprintf("%s%s%s", p, separator, ts.mark.Format(layout)) + + } else { + p := period.NewOf(ts.duration).Normalise(false) + return fmt.Sprintf("%s%s%s", ts.mark.Format(layout), separator, p) + } } // FormatRFC5545 formats the timespan as a string containing the start time and end time, or the @@ -226,50 +249,83 @@ func (ts TimeSpan) MarshalText() (text []byte, err error) { // // time "/" time // time "/" period +// period "/" time // // If the input time(s) ends in "Z", the location is UTC (as per RFC5545). Otherwise, the // specified location will be used for the resulting times; this behaves the same as -// time.ParseInLocation. -func ParseRFC5545InLocation(text string, loc *time.Location) (TimeSpan, error) { +// time.ParseInLocation. Its expected format is RFC5545DateTimeLayout (with optional "Z"). +// +// The period can optionally be preceded by '+' or '-'. If the period comes first, it is treated +// as being the same as a negative period after the '/'. +// +// RFC5545 does not allow the period to contain years or months. However, in this implementation +// they are permitted but discouraged. +func ParseRFC5545InLocation(text string, loc *time.Location) (ts TimeSpan, err error) { slash := strings.IndexByte(text, '/') if slash < 0 { return TimeSpan{}, fmt.Errorf("cannot parse %q because there is no separator '/'", text) } - start := text[:slash] - rest := text[slash+1:] + first := text[:slash] + second := text[slash+1:] + + p := strings.IndexByte(text, 'P') + if p < 0 { + ts, err = parseTwoTimes(first, second, loc) + } else if p < slash { + ts, err = parseTimeAndPeriod(second, first, loc, -1) + } else { + ts, err = parseTimeAndPeriod(first, second, loc, 1) + } - st, err := parseTimeInLocation(start, loc) if err != nil { - return TimeSpan{}, fmt.Errorf("cannot parse start time in %q: %s", text, err.Error()) + return TimeSpan{}, fmt.Errorf("cannot parse %q: ", text) } + return ts, err +} - //fmt.Printf("got %20s %s\n", st.Location(), st.Format(RFC5545DateTimeLayout)) +func parseTwoTimes(s1, s2 string, loc *time.Location) (TimeSpan, error) { + t1, err := parseTimeInLocation(s1, loc) + if err != nil { + return TimeSpan{}, err + } - if rest == "" { - return TimeSpan{}, fmt.Errorf("cannot parse %q because there is end time or duration", text) + t2, err := parseTimeInLocation(s2, loc) + if err != nil { + return TimeSpan{}, err } - if rest[0] == 'P' { - pe, e2 := period.Parse(rest) - if e2 != nil { - return TimeSpan{}, fmt.Errorf("cannot parse period in %q: %s", text, e2.Error()) - } + return TimeSpan{t1, t2.Sub(t1)}, nil +} - du, precise := pe.Duration() - if precise { - return TimeSpan{st, du}, nil - } +func parseTimeAndPeriod(sa, sb string, loc *time.Location, sign time.Duration) (TimeSpan, error) { + t1, err := parseTimeInLocation(sa, loc) + if err != nil { + return TimeSpan{}, err + } + + if sb == "" { + return TimeSpan{}, fmt.Errorf("there is no end time or duration") + } - et, _ := pe.AddTo(st) - return NewTimeSpan(st, et), nil + pe, e2 := period.Parse(sb) + if e2 != nil { + return TimeSpan{}, e2 } - et, err := parseTimeInLocation(rest, loc) - return NewTimeSpan(st, et), err + du, precise := pe.Duration() + if precise { + return TimeSpan{t1, sign * du}, nil + } + + t2, _ := pe.AddTo(t1) + return TimeSpan{t1, sign * t2.Sub(t1)}, nil } func parseTimeInLocation(text string, loc *time.Location) (time.Time, error) { + if strings.HasPrefix(text, "+") { + text = text[1:] // lenient but not strictly required + } if strings.HasSuffix(text, "Z") { text = text[:len(text)-1] return time.ParseInLocation(RFC5545DateTimeLayout, text, time.UTC) @@ -280,7 +336,7 @@ func parseTimeInLocation(text string, loc *time.Location) (time.Time, error) { // UnmarshalText parses a string as a timespan. It expects RFC5545 layout. // // If the receiver timespan is non-nil and has a time with a location, -// this location is used for parsing. Otherwise time.Local is used. +// this location is used for parsing. Otherwise, time.Local is used. // // This implements the encoding.TextUnmarshaler interface. func (ts *TimeSpan) UnmarshalText(text []byte) (err error) { diff --git a/timespan/timespan_test.go b/timespan/timespan_test.go index 7226a657..448106a1 100644 --- a/timespan/timespan_test.go +++ b/timespan/timespan_test.go @@ -19,26 +19,26 @@ var t0330 = time.Date(2015, 3, 30, 0, 0, 0, 0, time.UTC) func TestZeroTimeSpan(t *testing.T) { ts := ZeroTimeSpan(t0327) - isEq(t, 0, ts.mark, t0327) + isEq(t, 0, ts.Mark(), t0327) isEq(t, 0, ts.Duration(), zero) isEq(t, 0, ts.End(), t0327) } func TestNewTimeSpan(t *testing.T) { - ts1 := NewTimeSpan(t0327, t0327) - isEq(t, 0, ts1.mark, t0327) + ts1 := BetweenTimes(t0327, t0327) + isEq(t, 0, ts1.Mark(), t0327) isEq(t, 0, ts1.Duration(), zero) isEq(t, 0, ts1.IsEmpty(), true) isEq(t, 0, ts1.End(), t0327) - ts2 := NewTimeSpan(t0327, t0328) - isEq(t, 0, ts2.mark, t0327) + ts2 := BetweenTimes(t0327, t0328) + isEq(t, 0, ts2.Mark(), t0327) isEq(t, 0, ts2.Duration(), time.Hour*24) isEq(t, 0, ts2.IsEmpty(), false) isEq(t, 0, ts2.End(), t0328) - ts3 := NewTimeSpan(t0329, t0327) - isEq(t, 0, ts3.mark, t0327) + ts3 := BetweenTimes(t0329, t0327) + isEq(t, 0, ts3.Mark(), t0327) isEq(t, 0, ts3.Duration(), time.Hour*48) isEq(t, 0, ts3.IsEmpty(), false) isEq(t, 0, ts3.End(), t0329) @@ -56,43 +56,43 @@ func TestTSEnd(t *testing.T) { } func TestTSShiftBy(t *testing.T) { - ts1 := NewTimeSpan(t0327, t0328).ShiftBy(time.Hour * 24) - isEq(t, 0, ts1.mark, t0328) + ts1 := BetweenTimes(t0327, t0328).ShiftBy(time.Hour * 24) + isEq(t, 0, ts1.Mark(), t0328) isEq(t, 0, ts1.Duration(), time.Hour*24) isEq(t, 0, ts1.End(), t0329) - ts2 := NewTimeSpan(t0328, t0329).ShiftBy(-time.Hour * 24) - isEq(t, 0, ts2.mark, t0327) + ts2 := BetweenTimes(t0328, t0329).ShiftBy(-time.Hour * 24) + isEq(t, 0, ts2.Mark(), t0327) isEq(t, 0, ts2.Duration(), time.Hour*24) isEq(t, 0, ts2.End(), t0328) } func TestTSExtendBy(t *testing.T) { - ts1 := NewTimeSpan(t0327, t0328).ExtendBy(time.Hour * 24) - isEq(t, 0, ts1.mark, t0327) + ts1 := BetweenTimes(t0327, t0328).ExtendBy(time.Hour * 24) + isEq(t, 0, ts1.Mark(), t0327) isEq(t, 0, ts1.Duration(), time.Hour*48) isEq(t, 0, ts1.End(), t0329) - ts2 := NewTimeSpan(t0328, t0329).ExtendBy(-time.Hour * 48) - isEq(t, 0, ts2.mark, t0327) + ts2 := BetweenTimes(t0328, t0329).ExtendBy(-time.Hour * 48) + isEq(t, 0, ts2.Mark(), t0327) isEq(t, 0, ts2.Duration(), time.Hour*24) isEq(t, 0, ts2.End(), t0328) } func TestTSExtendWithoutWrapping(t *testing.T) { - ts1 := NewTimeSpan(t0327, t0328).ExtendWithoutWrapping(time.Hour * 24) - isEq(t, 0, ts1.mark, t0327) + ts1 := BetweenTimes(t0327, t0328).ExtendWithoutWrapping(time.Hour * 24) + isEq(t, 0, ts1.Mark(), t0327) isEq(t, 0, ts1.Duration(), time.Hour*48) isEq(t, 0, ts1.End(), t0329) - ts2 := NewTimeSpan(t0328, t0329).ExtendWithoutWrapping(-time.Hour * 48) - isEq(t, 0, ts2.mark, t0328) + ts2 := BetweenTimes(t0328, t0329).ExtendWithoutWrapping(-time.Hour * 48) + isEq(t, 0, ts2.Mark(), t0328) isEq(t, 0, ts2.Duration(), zero) isEq(t, 0, ts2.End(), t0328) } func TestTSString(t *testing.T) { - s := NewTimeSpan(t0327, t0328).String() + s := BetweenTimes(t0327, t0328).String() isEq(t, 0, s, "24h0m0s from 2015-03-27 00:00:00 to 2015-03-28 00:00:00") } @@ -107,18 +107,20 @@ func TestTSEqual(t *testing.T) { cases := []struct { a, b TimeSpan }{ - {a: z0, b: NewTimeSpan(t0, t0)}, + {a: z0, b: BetweenTimes(t0, t0)}, {a: z0, b: z0.In(berlin)}, {a: ts1, b: ts1}, - {a: ts1, b: NewTimeSpan(t0, t1)}, + {a: ts1, b: BetweenTimes(t0, t1)}, {a: ts1, b: ts1.In(berlin)}, {a: ts1, b: ZeroTimeSpan(t1).ExtendBy(-time.Hour)}, } for i, c := range cases { - if !c.a.Equal(c.b) { - t.Errorf("%d: %v is not equal to %v", i, c.a, c.b) - } + t.Run(fmt.Sprintf("%d %s", i, c.a), func(t *testing.T) { + if !c.a.Equal(c.b) { + t.Errorf("%d: %v is not equal to %v", i, c.a, c.b) + } + }) } } @@ -134,16 +136,19 @@ func TestTSNotEqual(t *testing.T) { } for i, c := range cases { - if c.a.Equal(c.b) { - t.Errorf("%d: %v is not equal to %v", i, c.a, c.b) - } + t.Run(fmt.Sprintf("%d %s", i, c.a), func(t *testing.T) { + if c.a.Equal(c.b) { + t.Errorf("%d: %v is not equal to %v", i, c.a, c.b) + } + }) } } func TestTSFormat(t *testing.T) { // use Berlin, which is UTC-1 berlin, _ := time.LoadLocation("Europe/Berlin") - t0 := time.Date(2015, 3, 27, 10, 13, 14, 0, time.UTC) + tUTC := time.Date(2015, 3, 27, 10, 13, 14, 0, time.UTC) + tBerlin := tUTC.In(berlin) cases := []struct { start time.Time @@ -151,21 +156,23 @@ func TestTSFormat(t *testing.T) { useDuration bool layout, separator, exp string }{ - {start: t0, duration: time.Hour, useDuration: true, separator: " for ", exp: "20150327T101314Z for PT1H"}, - {start: t0, duration: time.Hour, useDuration: true, separator: "/", exp: "20150327T101314Z/PT1H"}, - {start: t0.In(berlin), duration: time.Minute, useDuration: true, separator: "/", exp: "20150327T111314/PT1M"}, - {start: t0.In(berlin), duration: time.Hour, useDuration: true, layout: "2006-01-02T15:04:05", separator: "/", exp: "2015-03-27T11:13:14/PT1H"}, - {start: t0.In(berlin), duration: time.Hour, useDuration: true, layout: "2006-01-02T15:04:05-07", separator: "/", exp: "2015-03-27T11:13:14+01/PT1H"}, - {start: t0, duration: time.Hour, useDuration: true, layout: "2006-01-02T15:04:05-07", separator: "/", exp: "2015-03-27T10:13:14+00/PT1H"}, - {start: t0, duration: time.Hour, useDuration: true, layout: "2006-01-02T15:04:05Z07", separator: "/", exp: "2015-03-27T10:13:14Z/PT1H"}, - - {start: t0, duration: time.Hour, separator: " to ", exp: "20150327T101314Z to 20150327T111314Z"}, - {start: t0, duration: time.Hour, separator: "/", exp: "20150327T101314Z/20150327T111314Z"}, - {start: t0.In(berlin), duration: time.Minute, separator: "/", exp: "20150327T111314/20150327T111414"}, - {start: t0.In(berlin), duration: time.Hour, layout: "2006-01-02T15:04:05", separator: "/", exp: "2015-03-27T11:13:14/2015-03-27T12:13:14"}, - {start: t0.In(berlin), duration: time.Hour, layout: "2006-01-02T15:04:05-07", separator: "/", exp: "2015-03-27T11:13:14+01/2015-03-27T12:13:14+01"}, - {start: t0, duration: time.Hour, layout: "2006-01-02T15:04:05-07", separator: "/", exp: "2015-03-27T10:13:14+00/2015-03-27T11:13:14+00"}, - {start: t0, duration: time.Hour, layout: "2006-01-02T15:04:05Z07", separator: "/", exp: "2015-03-27T10:13:14Z/2015-03-27T11:13:14Z"}, + {start: tUTC, duration: time.Hour, useDuration: true, separator: " for ", exp: "20150327T101314Z for PT1H"}, + {start: tUTC, duration: time.Hour, useDuration: true, separator: "/", exp: "20150327T101314Z/PT1H"}, + {start: tBerlin, duration: time.Minute, useDuration: true, separator: "/", exp: "20150327T111314/PT1M"}, + {start: tBerlin, duration: time.Hour, useDuration: true, layout: "2006-01-02T15:04:05", separator: "/", exp: "2015-03-27T11:13:14/PT1H"}, + {start: tBerlin, duration: time.Hour, useDuration: true, layout: "2006-01-02T15:04:05-07", separator: "/", exp: "2015-03-27T11:13:14+01/PT1H"}, + {start: tUTC, duration: time.Hour, useDuration: true, layout: "2006-01-02T15:04:05-07", separator: "/", exp: "2015-03-27T10:13:14+00/PT1H"}, + {start: tUTC, duration: time.Hour, useDuration: true, layout: "2006-01-02T15:04:05Z07", separator: "/", exp: "2015-03-27T10:13:14Z/PT1H"}, + {start: tUTC, duration: -time.Hour, useDuration: true, layout: "2006-01-02T15:04:05Z07", separator: "/", exp: "PT1H/2015-03-27T10:13:14Z"}, + + {start: tUTC, duration: time.Hour, separator: " to ", exp: "20150327T101314Z to 20150327T111314Z"}, + {start: tUTC, duration: time.Hour, separator: "/", exp: "20150327T101314Z/20150327T111314Z"}, + {start: tBerlin, duration: time.Minute, separator: "/", exp: "20150327T111314/20150327T111414"}, + {start: tBerlin, duration: -time.Minute, separator: "/", exp: "20150327T111214/20150327T111314"}, + {start: tBerlin, duration: time.Hour, layout: "2006-01-02T15:04:05", separator: "/", exp: "2015-03-27T11:13:14/2015-03-27T12:13:14"}, + {start: tBerlin, duration: time.Hour, layout: "2006-01-02T15:04:05-07", separator: "/", exp: "2015-03-27T11:13:14+01/2015-03-27T12:13:14+01"}, + {start: tUTC, duration: time.Hour, layout: "2006-01-02T15:04:05-07", separator: "/", exp: "2015-03-27T10:13:14+00/2015-03-27T11:13:14+00"}, + {start: tUTC, duration: time.Hour, layout: "2006-01-02T15:04:05Z07", separator: "/", exp: "2015-03-27T10:13:14Z/2015-03-27T11:13:14Z"}, } for i, c := range cases { @@ -189,8 +196,8 @@ func TestTSMarshalText(t *testing.T) { }{ {start: t0, duration: time.Hour, exp: "20150214T101314Z/PT1H"}, {start: t1, duration: 2 * time.Hour, exp: "20150627T101315Z/PT2H"}, - {start: t0.In(berlin), duration: time.Minute, exp: "20150214T111314Z/PT1M"}, // UTC+1 - {start: t1.In(berlin), duration: time.Second, exp: "20150627T121315Z/PT1S"}, // UTC+2 + {start: t0.In(berlin), duration: time.Minute, exp: "20150214T111314Z/PT1M"}, // UTC+1 in winter + {start: t1.In(berlin), duration: time.Second, exp: "20150627T121315Z/PT1S"}, // UTC+2 in summer } for i, c := range cases { @@ -212,20 +219,27 @@ func TestTSParseInLocation(t *testing.T) { berlin, _ := time.LoadLocation("Europe/Berlin") t0120 := time.Date(2015, 1, 20, 10, 13, 14, 0, time.UTC) // just before start of daylight savings - t0325 := time.Date(2015, 3, 25, 10, 13, 14, 0, time.UTC) + t0325a := time.Date(2015, 3, 25, 10, 13, 14, 0, time.UTC) + t0325b := time.Date(2015, 3, 25, 11, 13, 14, 0, time.UTC) cases := []struct { start time.Time duration time.Duration text string }{ - {text: "20150325T101314Z/PT1H", start: t0325, duration: time.Hour}, - {text: "20150325T101314Z/PT2S", start: t0325, duration: 2 * time.Second}, + {text: "20150325T101314Z/PT1H", start: t0325a, duration: time.Hour}, + {text: "PT1H/20150325T111314Z", start: t0325b, duration: -time.Hour}, + {text: "20150325T101314Z/20150325T111314Z", start: t0325a, duration: time.Hour}, + {text: "20150325T111314Z/20150325T101314Z", start: t0325b, duration: -time.Hour}, + {text: "20150325T101314Z/PT2S", start: t0325a, duration: 2 * time.Second}, {text: "20150120T111314/PT1M", start: t0120.In(berlin), duration: time.Minute}, - {text: "20150325T101314Z/P2W", start: t0325, duration: 336 * time.Hour}, + {text: "20150120T111314/+PT1M", start: t0120.In(berlin), duration: time.Minute}, + {text: "20150120T111314/-PT1M", start: t0120.In(berlin), duration: -time.Minute}, + {text: "PT1M/+20150120T111314", start: t0120.In(berlin), duration: -time.Minute}, + {text: "20150325T101314Z/P2W", start: t0325a, duration: 336 * time.Hour}, {text: "20150120T111314/P3D", start: t0120.In(berlin), duration: 72 * time.Hour}, // This case has the daylight-savings clock shift - {text: "20150325T111314/P1W", start: t0325.In(berlin), duration: 167 * time.Hour}, + {text: "20150325T111314/P1W", start: t0325a.In(berlin), duration: 167 * time.Hour}, } for i, c := range cases { @@ -235,7 +249,7 @@ func TestTSParseInLocation(t *testing.T) { t.Errorf("%d: %s %v %v", i, c.text, ts1.String(), err) } - if !ts1.Start().Equal(c.start) { + if !ts1.Mark().Equal(c.start) { t.Errorf("%d: %s", i, ts1) } @@ -260,12 +274,14 @@ func TestTSParseInLocationErrors(t *testing.T) { cases := []struct { text string }{ - {"20150327T101314Z PT1H"}, - {"2015XX27T101314/PT1H"}, - {"20150127T101314/2016XX27T101314"}, - {"20150127T101314/P1Z"}, - {"20150327T101314Z/"}, - {"/PT1H"}, + {text: "20150327T101314Z PT1H"}, + {text: "2015XX27T101314/PT1H"}, + {text: "2015XX27T101314/ PT1H"}, + {text: "20150127T101314/2016XX27T101314"}, + {text: "20150127T101314/P1Z"}, + {text: "20150327T101314Z/"}, + {text: "20150327T101314Z/+"}, + {text: "/PT1H"}, } for i, c := range cases { @@ -279,7 +295,7 @@ func TestTSParseInLocationErrors(t *testing.T) { } func TestTSContains(t *testing.T) { - ts := NewTimeSpan(t0327, t0329) + ts := BetweenTimes(t0327, t0329) isEq(t, 0, ts.Contains(t0327.Add(minusOneNano)), false) isEq(t, 0, ts.Contains(t0327), true) isEq(t, 0, ts.Contains(t0328), true) @@ -289,57 +305,57 @@ func TestTSContains(t *testing.T) { func TestTSIn(t *testing.T) { ts := ZeroTimeSpan(t0327).In(time.FixedZone("Test", 7200)) - isEq(t, 0, ts.mark.Equal(t0327), true) + isEq(t, 0, ts.Mark().Equal(t0327), true) isEq(t, 0, ts.Duration(), zero) isEq(t, 0, ts.End().Equal(t0327), true) } func TestTSMerge1(t *testing.T) { - ts1 := NewTimeSpan(t0327, t0328) - ts2 := NewTimeSpan(t0327, t0330) + ts1 := BetweenTimes(t0327, t0328) + ts2 := BetweenTimes(t0327, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) - isEq(t, 0, m1.mark, t0327) + isEq(t, 0, m1.Mark(), t0327) isEq(t, 0, m1.End(), t0330) isEq(t, 0, m1, m2) } func TestTSMerge2(t *testing.T) { - ts1 := NewTimeSpan(t0328, t0329) - ts2 := NewTimeSpan(t0327, t0330) + ts1 := BetweenTimes(t0328, t0329) + ts2 := BetweenTimes(t0327, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) - isEq(t, 0, m1.mark, t0327) + isEq(t, 0, m1.Mark(), t0327) isEq(t, 0, m1.End(), t0330) isEq(t, 0, m1, m2) } func TestTSMerge3(t *testing.T) { - ts1 := NewTimeSpan(t0329, t0330) - ts2 := NewTimeSpan(t0327, t0330) + ts1 := BetweenTimes(t0329, t0330) + ts2 := BetweenTimes(t0327, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) - isEq(t, 0, m1.mark, t0327) + isEq(t, 0, m1.Mark(), t0327) isEq(t, 0, m1.End(), t0330) isEq(t, 0, m1, m2) } func TestTSMergeOverlapping(t *testing.T) { - ts1 := NewTimeSpan(t0327, t0329) - ts2 := NewTimeSpan(t0328, t0330) + ts1 := BetweenTimes(t0327, t0329) + ts2 := BetweenTimes(t0328, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) - isEq(t, 0, m1.mark, t0327) + isEq(t, 0, m1.Mark(), t0327) isEq(t, 0, m1.End(), t0330) isEq(t, 0, m1, m2) } func TestTSMergeNonOverlapping(t *testing.T) { - ts1 := NewTimeSpan(t0327, t0328) - ts2 := NewTimeSpan(t0329, t0330) + ts1 := BetweenTimes(t0327, t0328) + ts2 := BetweenTimes(t0329, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) - isEq(t, 0, m1.mark, t0327) + isEq(t, 0, m1.Mark(), t0327) isEq(t, 0, m1.End(), t0330) isEq(t, 0, m1, m2) } @@ -358,7 +374,7 @@ func TestConversion1(t *testing.T) { } func TestConversion2(t *testing.T) { - ts1 := NewTimeSpan(t0327, t0328) + ts1 := BetweenTimes(t0327, t0328) dr := ts1.DateRangeIn(time.UTC) ts2 := dr.TimeSpanIn(time.UTC) isEq(t, 0, dr.Start(), d0327) @@ -368,7 +384,7 @@ func TestConversion2(t *testing.T) { } func TestConversion3(t *testing.T) { - dr1 := NewDateRange(d0327, d0330) // weekend of clocks changing + dr1 := BetweenDates(d0327, d0330) // weekend of clocks changing ts1 := dr1.TimeSpanIn(london) dr2 := ts1.DateRangeIn(london) ts2 := dr2.TimeSpanIn(london)