From 4322d2704dd09ca4db2a2c0ab000162fa6097f48 Mon Sep 17 00:00:00 2001 From: Rick Date: Mon, 23 Nov 2015 23:15:34 +0000 Subject: [PATCH 001/165] Just refactoring to make the fork easier to work with. --- date.go | 92 +----------------------- date_test.go | 187 ++++++------------------------------------------ example_test.go | 22 +++--- format_test.go | 30 ++++---- marshal.go | 94 ++++++++++++++++++++++++ marshal_test.go | 155 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 294 insertions(+), 286 deletions(-) create mode 100644 marshal.go create mode 100644 marshal_test.go diff --git a/date.go b/date.go index 33a3cfd5..9af57d66 100644 --- a/date.go +++ b/date.go @@ -38,8 +38,6 @@ package date import ( - "errors" - "fmt" "math" "time" ) @@ -176,7 +174,7 @@ func (d Date) Weekday() time.Weekday { // Date zero, January 1, 1970, fell on a Thursday wdayZero := time.Thursday // Taking into account potential for overflow and negative offset - return time.Weekday((int32(wdayZero) + d.day%7 + 7) % 7) + return time.Weekday((int32(wdayZero) + d.day % 7 + 7) % 7) } // ISOWeek returns the ISO 8601 year and week number in which d occurs. @@ -226,91 +224,3 @@ func (d Date) AddDate(years, months, days int) Date { func (d Date) Sub(u Date) (days int) { return int(d.day - u.day) } - -// MarshalBinary implements the encoding.BinaryMarshaler interface. -func (d Date) MarshalBinary() ([]byte, error) { - enc := []byte{ - byte(d.day >> 24), - byte(d.day >> 16), - byte(d.day >> 8), - byte(d.day), - } - return enc, nil -} - -// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. -func (d *Date) UnmarshalBinary(data []byte) error { - if len(data) == 0 { - return errors.New("Date.UnmarshalBinary: no data") - } - if len(data) != 4 { - return errors.New("Date.UnmarshalBinary: invalid length") - } - - d.day = int32(data[3]) | int32(data[2])<<8 | int32(data[1])<<16 | int32(data[0])<<24 - - return nil -} - -// GobEncode implements the gob.GobEncoder interface. -func (d Date) GobEncode() ([]byte, error) { - return d.MarshalBinary() -} - -// GobDecode implements the gob.GobDecoder interface. -func (d *Date) GobDecode(data []byte) error { - return d.UnmarshalBinary(data) -} - -// MarshalJSON implements the json.Marshaler interface. -// The date is a quoted string in ISO 8601 extended format (e.g. "2006-01-02"). -// If the year of the date falls outside the [0,9999] range, this format -// produces an expanded year representation with possibly extra year digits -// beyond the prescribed four-digit minimum and with a + or - sign prefix -// (e.g. , "+12345-06-07", "-0987-06-05"). -func (d Date) MarshalJSON() ([]byte, error) { - return []byte(`"` + d.String() + `"`), nil -} - -// UnmarshalJSON implements the json.Unmarshaler interface. -// The date is expected to be a quoted string in ISO 8601 extended format -// (e.g. "2006-01-02", "+12345-06-07", "-0987-06-05"); -// the year must use at least 4 digits and if outside the [0,9999] range -// must be prefixed with a + or - sign. -func (d *Date) UnmarshalJSON(data []byte) (err error) { - value := string(data) - n := len(value) - if n < 2 || value[0] != '"' || value[n-1] != '"' { - return fmt.Errorf("Date.UnmarshalJSON: missing double quotes (%s)", value) - } - u, err := ParseISO(value[1 : n-1]) - if err != nil { - return err - } - d.day = u.day - return nil -} - -// MarshalText implements the encoding.TextMarshaler interface. -// The date is given in ISO 8601 extended format (e.g. "2006-01-02"). -// If the year of the date falls outside the [0,9999] range, this format -// produces an expanded year representation with possibly extra year digits -// beyond the prescribed four-digit minimum and with a + or - sign prefix -// (e.g. , "+12345-06-07", "-0987-06-05"). -func (d Date) MarshalText() ([]byte, error) { - return []byte(d.String()), nil -} - -// UnmarshalText implements the encoding.TextUnmarshaler interface. -// The date is expected to be in ISO 8601 extended format -// (e.g. "2006-01-02", "+12345-06-07", "-0987-06-05"); -// the year must use at least 4 digits and if outside the [0,9999] range -// must be prefixed with a + or - sign. -func (d *Date) UnmarshalText(data []byte) error { - u, err := ParseISO(string(data)) - if err != nil { - return err - } - d.day = u.day - return nil -} diff --git a/date_test.go b/date_test.go index be7ba91f..c5bed0ac 100644 --- a/date_test.go +++ b/date_test.go @@ -2,27 +2,22 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package date_test +package date import ( - "bytes" - "encoding/gob" - "encoding/json" "testing" "time" - - "github.com/fxtlabs/date" ) -func same(d date.Date, t time.Time) bool { +func same(d Date, t time.Time) bool { yd, wd := d.ISOWeek() yt, wt := t.ISOWeek() return d.Year() == t.Year() && - d.Month() == t.Month() && - d.Day() == t.Day() && - d.Weekday() == t.Weekday() && - d.YearDay() == t.YearDay() && - yd == yt && wd == wt + d.Month() == t.Month() && + d.Day() == t.Day() && + d.Weekday() == t.Weekday() && + d.YearDay() == t.YearDay() && + yd == yt && wd == wt } func TestNew(t *testing.T) { @@ -43,11 +38,11 @@ func TestNew(t *testing.T) { t.Errorf("New(%v) cannot parse input: %v", c, err) continue } - dOut := date.New(tIn.Year(), tIn.Month(), tIn.Day()) + dOut := New(tIn.Year(), tIn.Month(), tIn.Day()) if !same(dOut, tIn) { t.Errorf("New(%v) == %v, want date of %v", c, dOut, tIn) } - dOut = date.NewAt(tIn) + dOut = NewAt(tIn) if !same(dOut, tIn) { t.Errorf("NewAt(%v) == %v, want date of %v", c, dOut, tIn) } @@ -55,20 +50,20 @@ func TestNew(t *testing.T) { } func TestToday(t *testing.T) { - today := date.Today() + today := Today() now := time.Now() if !same(today, now) { t.Errorf("Today == %v, want date of %v", today, now) } - today = date.TodayUTC() + today = TodayUTC() now = time.Now().UTC() if !same(today, now) { t.Errorf("TodayUTC == %v, want date of %v", today, now) } cases := []int{-10, -5, -3, 0, 1, 4, 8, 12} for _, c := range cases { - location := time.FixedZone("zone", c*60*60) - today = date.TodayIn(location) + location := time.FixedZone("zone", c * 60 * 60) + today = TodayIn(location) now = time.Now().In(location) if !same(today, now) { t.Errorf("TodayIn(%v) == %v, want date of %v", c, today, now) @@ -93,7 +88,7 @@ func TestTime(t *testing.T) { } zones := []int{-12, -10, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 8, 12} for _, c := range cases { - d := date.New(c.year, c.month, c.day) + d := New(c.year, c.month, c.day) tUTC := d.UTC() if !same(d, tUTC) { t.Errorf("TimeUTC(%v) == %v, want date part %v", d, tUTC, d) @@ -109,7 +104,7 @@ func TestTime(t *testing.T) { t.Errorf("TimeLocal(%v) == %v, want %v", d, tLocal.Location(), time.Local) } for _, z := range zones { - location := time.FixedZone("zone", z*60*60) + location := time.FixedZone("zone", z * 60 * 60) tInLoc := d.In(location) if !same(d, tInLoc) { t.Errorf("TimeIn(%v) == %v, want date part %v", d, tInLoc, d) @@ -138,9 +133,9 @@ func TestPredicates(t *testing.T) { {1111111, time.June, 21}, } for i, ci := range cases { - di := date.New(ci.year, ci.month, ci.day) + di := New(ci.year, ci.month, ci.day) for j, cj := range cases { - dj := date.New(cj.year, cj.month, cj.day) + dj := New(cj.year, cj.month, cj.day) p := di.Equal(dj) q := i == j if p != q { @@ -170,11 +165,11 @@ func TestPredicates(t *testing.T) { } // Test IsZero - zero := date.Date{} + zero := Date{} if !zero.IsZero() { t.Errorf("IsZero(%v) == false, want true", zero) } - today := date.Today() + today := Today() if today.IsZero() { t.Errorf("IsZero(%v) == true, want false", today) } @@ -197,7 +192,7 @@ func TestArithmetic(t *testing.T) { } offsets := []int{-1000000, -9999, -555, -99, -22, -1, 0, 1, 22, 99, 555, 9999, 1000000} for _, c := range cases { - d := date.New(c.year, c.month, c.day) + d := New(c.year, c.month, c.day) for _, days := range offsets { d2 := d.Add(days) days2 := d2.Sub(d) @@ -211,145 +206,3 @@ func TestArithmetic(t *testing.T) { } } } - -func TestGobEncoding(t *testing.T) { - var b bytes.Buffer - encoder := gob.NewEncoder(&b) - decoder := gob.NewDecoder(&b) - cases := []date.Date{ - date.New(-11111, time.February, 3), - date.New(-1, time.December, 31), - date.New(0, time.January, 1), - date.New(1, time.January, 1), - date.New(1970, time.January, 1), - date.New(2012, time.June, 25), - date.New(12345, time.June, 7), - } - for _, c := range cases { - var d date.Date - err := encoder.Encode(&c) - if err != nil { - t.Errorf("Gob(%v) encode error %v", c, err) - } else { - err = decoder.Decode(&d) - if err != nil { - t.Errorf("Gob(%v) decode error %v", c, err) - } - } - } -} - -func TestInvalidGob(t *testing.T) { - cases := []struct { - bytes []byte - want string - }{ - {[]byte{}, "Date.UnmarshalBinary: no data"}, - {[]byte{1, 2, 3}, "Date.UnmarshalBinary: invalid length"}, - } - for _, c := range cases { - var ignored date.Date - err := ignored.GobDecode(c.bytes) - if err == nil || err.Error() != c.want { - t.Errorf("InvalidGobDecode(%v) == %v, want %v", c.bytes, err, c.want) - } - err = ignored.UnmarshalBinary(c.bytes) - if err == nil || err.Error() != c.want { - t.Errorf("InvalidUnmarshalBinary(%v) == %v, want %v", c.bytes, err, c.want) - } - } -} - -func TestJSONMarshalling(t *testing.T) { - var d date.Date - cases := []struct { - value date.Date - want string - }{ - {date.New(-11111, time.February, 3), `"-11111-02-03"`}, - {date.New(-1, time.December, 31), `"-0001-12-31"`}, - {date.New(0, time.January, 1), `"0000-01-01"`}, - {date.New(1, time.January, 1), `"0001-01-01"`}, - {date.New(1970, time.January, 1), `"1970-01-01"`}, - {date.New(2012, time.June, 25), `"2012-06-25"`}, - {date.New(12345, time.June, 7), `"+12345-06-07"`}, - } - for _, c := range cases { - bytes, err := json.Marshal(c.value) - if err != nil { - t.Errorf("JSON(%v) marshal error %v", c, err) - } else if string(bytes) != c.want { - t.Errorf("JSON(%v) == %v, want %v", c.value, string(bytes), c.want) - } else { - err = json.Unmarshal(bytes, &d) - if err != nil { - t.Errorf("JSON(%v) unmarshal error %v", c.value, err) - } - } - } -} - -func TestInvalidJSON(t *testing.T) { - cases := []struct { - value string - want string - }{ - {`"not-a-date"`, `Date.ParseISO: cannot parse not-a-date`}, - {`2015-08-15"`, `Date.UnmarshalJSON: missing double quotes (2015-08-15")`}, - {`"2015-08-15`, `Date.UnmarshalJSON: missing double quotes ("2015-08-15)`}, - {`"215-08-15"`, `Date.ParseISO: cannot parse 215-08-15`}, - } - for _, c := range cases { - var d date.Date - err := d.UnmarshalJSON([]byte(c.value)) - if err == nil || err.Error() != c.want { - t.Errorf("InvalidJSON(%v) == %v, want %v", c.value, err, c.want) - } - } -} - -func TestTextMarshalling(t *testing.T) { - var d date.Date - cases := []struct { - value date.Date - want string - }{ - {date.New(-11111, time.February, 3), "-11111-02-03"}, - {date.New(-1, time.December, 31), "-0001-12-31"}, - {date.New(0, time.January, 1), "0000-01-01"}, - {date.New(1, time.January, 1), "0001-01-01"}, - {date.New(1970, time.January, 1), "1970-01-01"}, - {date.New(2012, time.June, 25), "2012-06-25"}, - {date.New(12345, time.June, 7), "+12345-06-07"}, - } - for _, c := range cases { - bytes, err := c.value.MarshalText() - if err != nil { - t.Errorf("Text(%v) marshal error %v", c, err) - } else if string(bytes) != c.want { - t.Errorf("Text(%v) == %v, want %v", c.value, string(bytes), c.want) - } else { - err = d.UnmarshalText(bytes) - if err != nil { - t.Errorf("Text(%v) unmarshal error %v", c.value, err) - } - } - } -} - -func TestInvalidText(t *testing.T) { - cases := []struct { - value string - want string - }{ - {`not-a-date`, `Date.ParseISO: cannot parse not-a-date`}, - {`215-08-15`, `Date.ParseISO: cannot parse 215-08-15`}, - } - for _, c := range cases { - var d date.Date - err := d.UnmarshalText([]byte(c.value)) - if err == nil || err.Error() != c.want { - t.Errorf("InvalidText(%v) == %v, want %v", c.value, err, c.want) - } - } -} diff --git a/example_test.go b/example_test.go index 5515271a..720d3457 100644 --- a/example_test.go +++ b/example_test.go @@ -2,29 +2,27 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package date_test +package date import ( "fmt" "time" - - "github.com/fxtlabs/date" ) func ExampleMax() { - d := date.Max() + d := Max() fmt.Println(d) // Output: +5881580-07-11 } func ExampleMin() { - d := date.Min() + d := Min() fmt.Println(d) // Output: -5877641-06-23 } func ExampleNew() { - d := date.New(9999, time.December, 31) + d := New(9999, time.December, 31) fmt.Printf("The world ends on %s\n", d) // Output: The world ends on 9999-12-31 } @@ -33,13 +31,13 @@ func ExampleParse() { // longForm shows by example how the reference date would be // represented in the desired layout. const longForm = "Mon, January 2, 2006" - d, _ := date.Parse(longForm, "Tue, February 3, 2013") + d, _ := Parse(longForm, "Tue, February 3, 2013") fmt.Println(d) // shortForm is another way the reference date would be represented // in the desired layout. const shortForm = "2006-Jan-02" - d, _ = date.Parse(shortForm, "2013-Feb-03") + d, _ = Parse(shortForm, "2013-Feb-03") fmt.Println(d) // Output: @@ -48,7 +46,7 @@ func ExampleParse() { } func ExampleParseISO() { - d, _ := date.ParseISO("+12345-06-07") + d, _ := ParseISO("+12345-06-07") year, month, day := d.Date() fmt.Println(year) fmt.Println(month) @@ -60,7 +58,7 @@ func ExampleParseISO() { } func ExampleDate_AddDate() { - d := date.New(1000, time.January, 1) + d := New(1000, time.January, 1) // Months and days do not need to be constrained to [1,12] and [1,365]. u := d.AddDate(0, 14, -1) fmt.Println(u) @@ -70,7 +68,7 @@ func ExampleDate_AddDate() { func ExampleDate_Format() { // layout shows by example how the reference time should be represented. const layout = "Jan 2, 2006" - d := date.New(2009, time.November, 10) + d := New(2009, time.November, 10) fmt.Println(d.Format(layout)) // Output: Nov 10, 2009 } @@ -79,7 +77,7 @@ func ExampleDate_FormatISO() { // According to legend, Rome was founded on April 21, 753 BC. // Note that with astronomical year numbering, 753 BC becomes -752 // because 1 BC is actually year 0. - d := date.New(-752, time.April, 21) + d := New(-752, time.April, 21) fmt.Println(d.FormatISO(5)) // Output: -00752-04-21 } diff --git a/format_test.go b/format_test.go index 89edf96a..9e9a482e 100644 --- a/format_test.go +++ b/format_test.go @@ -2,13 +2,11 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package date_test +package date import ( "testing" "time" - - "github.com/fxtlabs/date" ) func TestParseISO(t *testing.T) { @@ -44,7 +42,7 @@ func TestParseISO(t *testing.T) { {"-5000000-09-17", -5000000, time.September, 17}, } for _, c := range cases { - d, err := date.ParseISO(c.value) + d, err := ParseISO(c.value) if err != nil { t.Errorf("ParseISO(%v) == %v", c.value, err) } @@ -74,7 +72,7 @@ func TestParseISO(t *testing.T) { "-123-05-06", } for _, c := range badCases { - d, err := date.ParseISO(c) + d, err := ParseISO(c) if err == nil { t.Errorf("ParseISO(%v) == %v", c, d) } @@ -90,17 +88,17 @@ func TestParse(t *testing.T) { month time.Month day int }{ - {date.ISO8601, "1969-12-31", 1969, time.December, 31}, - {date.ISO8601B, "19700101", 1970, time.January, 1}, - {date.RFC822, "29-Feb-00", 2000, time.February, 29}, - {date.RFC822W, "Mon, 01-Mar-04", 2004, time.March, 1}, - {date.RFC850, "Wednesday, 12-Aug-15", 2015, time.August, 12}, - {date.RFC1123, "05 Dec 1928", 1928, time.December, 5}, - {date.RFC1123W, "Mon, 05 Dec 1928", 1928, time.December, 5}, - {date.RFC3339, "2345-06-07", 2345, time.June, 7}, + {ISO8601, "1969-12-31", 1969, time.December, 31}, + {ISO8601B, "19700101", 1970, time.January, 1}, + {RFC822, "29-Feb-00", 2000, time.February, 29}, + {RFC822W, "Mon, 01-Mar-04", 2004, time.March, 1}, + {RFC850, "Wednesday, 12-Aug-15", 2015, time.August, 12}, + {RFC1123, "05 Dec 1928", 1928, time.December, 5}, + {RFC1123W, "Mon, 05 Dec 1928", 1928, time.December, 5}, + {RFC3339, "2345-06-07", 2345, time.June, 7}, } for _, c := range cases { - d, err := date.Parse(c.layout, c.value) + d, err := Parse(c.layout, c.value) if err != nil { t.Errorf("Parse(%v) == %v", c.value, err) } @@ -118,7 +116,7 @@ func TestParse(t *testing.T) { "-12345-06-07", } for _, c := range badCases { - d, err := date.Parse(date.ISO8601, c) + d, err := Parse(ISO8601, c) if err == nil { t.Errorf("Parse(%v) == %v", c, d) } @@ -142,7 +140,7 @@ func TestFormatISO(t *testing.T) { {"+999999-12-31", 6}, } for _, c := range cases { - d, err := date.ParseISO(c.value) + d, err := ParseISO(c.value) if err != nil { t.Errorf("FormatISO(%v) cannot parse input: %v", c.value, err) continue diff --git a/marshal.go b/marshal.go new file mode 100644 index 00000000..5fc4529c --- /dev/null +++ b/marshal.go @@ -0,0 +1,94 @@ +package date + +import ( + "fmt" + "errors" +) + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (d Date) MarshalBinary() ([]byte, error) { + enc := []byte{ + byte(d.day >> 24), + byte(d.day >> 16), + byte(d.day >> 8), + byte(d.day), + } + return enc, nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (d *Date) UnmarshalBinary(data []byte) error { + if len(data) == 0 { + return errors.New("Date.UnmarshalBinary: no data") + } + if len(data) != 4 { + return errors.New("Date.UnmarshalBinary: invalid length") + } + + d.day = int32(data[3]) | int32(data[2])<<8 | int32(data[1])<<16 | int32(data[0])<<24 + + return nil +} + +// GobEncode implements the gob.GobEncoder interface. +func (d Date) GobEncode() ([]byte, error) { + return d.MarshalBinary() +} + +// GobDecode implements the gob.GobDecoder interface. +func (d *Date) GobDecode(data []byte) error { + return d.UnmarshalBinary(data) +} + +// MarshalJSON implements the json.Marshaler interface. +// The date is a quoted string in ISO 8601 extended format (e.g. "2006-01-02"). +// If the year of the date falls outside the [0,9999] range, this format +// produces an expanded year representation with possibly extra year digits +// beyond the prescribed four-digit minimum and with a + or - sign prefix +// (e.g. , "+12345-06-07", "-0987-06-05"). +func (d Date) MarshalJSON() ([]byte, error) { + return []byte(`"` + d.String() + `"`), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +// The date is expected to be a quoted string in ISO 8601 extended format +// (e.g. "2006-01-02", "+12345-06-07", "-0987-06-05"); +// the year must use at least 4 digits and if outside the [0,9999] range +// must be prefixed with a + or - sign. +func (d *Date) UnmarshalJSON(data []byte) (err error) { + value := string(data) + n := len(value) + if n < 2 || value[0] != '"' || value[n-1] != '"' { + return fmt.Errorf("Date.UnmarshalJSON: missing double quotes (%s)", value) + } + u, err := ParseISO(value[1 : n-1]) + if err != nil { + return err + } + d.day = u.day + return nil +} + +// MarshalText implements the encoding.TextMarshaler interface. +// The date is given in ISO 8601 extended format (e.g. "2006-01-02"). +// If the year of the date falls outside the [0,9999] range, this format +// produces an expanded year representation with possibly extra year digits +// beyond the prescribed four-digit minimum and with a + or - sign prefix +// (e.g. , "+12345-06-07", "-0987-06-05"). +func (d Date) MarshalText() ([]byte, error) { + return []byte(d.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +// The date is expected to be in ISO 8601 extended format +// (e.g. "2006-01-02", "+12345-06-07", "-0987-06-05"); +// the year must use at least 4 digits and if outside the [0,9999] range +// must be prefixed with a + or - sign. +func (d *Date) UnmarshalText(data []byte) error { + u, err := ParseISO(string(data)) + if err != nil { + return err + } + d.day = u.day + return nil +} diff --git a/marshal_test.go b/marshal_test.go new file mode 100644 index 00000000..7f096001 --- /dev/null +++ b/marshal_test.go @@ -0,0 +1,155 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package date + +import ( + "bytes" + "encoding/gob" + "encoding/json" + "testing" + "time" +) + +func TestGobEncoding(t *testing.T) { + var b bytes.Buffer + encoder := gob.NewEncoder(&b) + decoder := gob.NewDecoder(&b) + cases := []Date{ + New(-11111, time.February, 3), + New(-1, time.December, 31), + New(0, time.January, 1), + New(1, time.January, 1), + New(1970, time.January, 1), + New(2012, time.June, 25), + New(12345, time.June, 7), + } + for _, c := range cases { + var d Date + err := encoder.Encode(&c) + if err != nil { + t.Errorf("Gob(%v) encode error %v", c, err) + } else { + err = decoder.Decode(&d) + if err != nil { + t.Errorf("Gob(%v) decode error %v", c, err) + } + } + } +} + +func TestInvalidGob(t *testing.T) { + cases := []struct { + bytes []byte + want string + }{ + {[]byte{}, "Date.UnmarshalBinary: no data"}, + {[]byte{1, 2, 3}, "Date.UnmarshalBinary: invalid length"}, + } + for _, c := range cases { + var ignored Date + err := ignored.GobDecode(c.bytes) + if err == nil || err.Error() != c.want { + t.Errorf("InvalidGobDecode(%v) == %v, want %v", c.bytes, err, c.want) + } + err = ignored.UnmarshalBinary(c.bytes) + if err == nil || err.Error() != c.want { + t.Errorf("InvalidUnmarshalBinary(%v) == %v, want %v", c.bytes, err, c.want) + } + } +} + +func TestJSONMarshalling(t *testing.T) { + var d Date + cases := []struct { + value Date + want string + }{ + {New(-11111, time.February, 3), `"-11111-02-03"`}, + {New(-1, time.December, 31), `"-0001-12-31"`}, + {New(0, time.January, 1), `"0000-01-01"`}, + {New(1, time.January, 1), `"0001-01-01"`}, + {New(1970, time.January, 1), `"1970-01-01"`}, + {New(2012, time.June, 25), `"2012-06-25"`}, + {New(12345, time.June, 7), `"+12345-06-07"`}, + } + for _, c := range cases { + bytes, err := json.Marshal(c.value) + if err != nil { + t.Errorf("JSON(%v) marshal error %v", c, err) + } else if string(bytes) != c.want { + t.Errorf("JSON(%v) == %v, want %v", c.value, string(bytes), c.want) + } else { + err = json.Unmarshal(bytes, &d) + if err != nil { + t.Errorf("JSON(%v) unmarshal error %v", c.value, err) + } + } + } +} + +func TestInvalidJSON(t *testing.T) { + cases := []struct { + value string + want string + }{ + {`"not-a-date"`, `Date.ParseISO: cannot parse not-a-date`}, + {`2015-08-15"`, `Date.UnmarshalJSON: missing double quotes (2015-08-15")`}, + {`"2015-08-15`, `Date.UnmarshalJSON: missing double quotes ("2015-08-15)`}, + {`"215-08-15"`, `Date.ParseISO: cannot parse 215-08-15`}, + } + for _, c := range cases { + var d Date + err := d.UnmarshalJSON([]byte(c.value)) + if err == nil || err.Error() != c.want { + t.Errorf("InvalidJSON(%v) == %v, want %v", c.value, err, c.want) + } + } +} + +func TestTextMarshalling(t *testing.T) { + var d Date + cases := []struct { + value Date + want string + }{ + {New(-11111, time.February, 3), "-11111-02-03"}, + {New(-1, time.December, 31), "-0001-12-31"}, + {New(0, time.January, 1), "0000-01-01"}, + {New(1, time.January, 1), "0001-01-01"}, + {New(1970, time.January, 1), "1970-01-01"}, + {New(2012, time.June, 25), "2012-06-25"}, + {New(12345, time.June, 7), "+12345-06-07"}, + } + for _, c := range cases { + bytes, err := c.value.MarshalText() + if err != nil { + t.Errorf("Text(%v) marshal error %v", c, err) + } else if string(bytes) != c.want { + t.Errorf("Text(%v) == %v, want %v", c.value, string(bytes), c.want) + } else { + err = d.UnmarshalText(bytes) + if err != nil { + t.Errorf("Text(%v) unmarshal error %v", c.value, err) + } + } + } +} + +func TestInvalidText(t *testing.T) { + cases := []struct { + value string + want string + }{ + {`not-a-date`, `Date.ParseISO: cannot parse not-a-date`}, + {`215-08-15`, `Date.ParseISO: cannot parse 215-08-15`}, + } + for _, c := range cases { + var d Date + err := d.UnmarshalText([]byte(c.value)) + if err == nil || err.Error() != c.want { + t.Errorf("InvalidText(%v) == %v, want %v", c.value, err, c.want) + } + } +} From 8792f002f3f29b241310a3e5085899098767e6bd Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 24 Nov 2015 00:06:06 +0000 Subject: [PATCH 002/165] Added ability for formatting a conventional suffix on the day number, e.g. "1st", "2nd", "3rd", "4th". This can be translated to other locales. --- format.go | 54 +++++++++++++++++++++++++++++++++++++++++++------- format_test.go | 35 +++++++++++++++++++++++++++++++- marshal.go | 4 ++++ 3 files changed, 85 insertions(+), 8 deletions(-) diff --git a/format.go b/format.go index 7a4c75d0..9aabe322 100644 --- a/format.go +++ b/format.go @@ -9,6 +9,7 @@ import ( "regexp" "strconv" "time" + "strings" ) // These are predefined layouts for use in Date.Format and Date.Parse. @@ -21,14 +22,14 @@ import ( // so that the Parse function and Format method can apply the same // transformation to a general date value. const ( - ISO8601 = "2006-01-02" // ISO 8601 extended format + ISO8601 = "2006-01-02" // ISO 8601 extended format ISO8601B = "20060102" // ISO 8601 basic format - RFC822 = "02-Jan-06" - RFC822W = "Mon, 02-Jan-06" // RFC822 with day of the week - RFC850 = "Monday, 02-Jan-06" - RFC1123 = "02 Jan 2006" + RFC822 = "02-Jan-06" + RFC822W = "Mon, 02-Jan-06" // RFC822 with day of the week + RFC850 = "Monday, 02-Jan-06" + RFC1123 = "02 Jan 2006" RFC1123W = "Mon, 02 Jan 2006" // RFC1123 with day of the week - RFC3339 = "2006-01-02" + RFC3339 = "2006-01-02" ) // reISO8601 is the regular expression used to parse date strings in the @@ -128,9 +129,48 @@ func (d Date) FormatISO(yearDigits int) string { // layout accepted by time.Format by extending its date to a time at // 00:00:00.000 UTC. // +// Additionally, it is able to insert the day-number suffix into the output string. +// This is done by including "nd" in the format string, which will become +// Mon, Jan 2nd, 2006 +// For example, New Year's Day might be rendered as "Fri, Jan 1st, 2016". To alter +// the suffix strings for a different locale, change DaySuffixes or use FormatWithSuffixes +// instead. +// // This function cannot currently format Date values according to the expanded // year variant of ISO 8601; you should use Date.FormatISO to that effect. func (d Date) Format(layout string) string { + return d.FormatWithSuffixes(layout, DaySuffixes) +} + +// FormatWithSuffixes is the same as Format, except the suffix strings can be specified +// explicitly, which allows multiple locales to be supported. The suffixes slice should +// contain 31 strings covering the days 1 (index 0) to 31 (index 30). +func (d Date) FormatWithSuffixes(layout string, suffixes []string) string { t := decode(d.day) - return t.Format(layout) + parts := strings.Split(layout, "nd") + switch len(parts) { + case 1: + return t.Format(layout) + case 2: + front := t.Format(parts[0]) + mid := suffixes[d.Day() - 1] + back := t.Format(parts[1]) + return front + mid + back + default: + panic("Unsupported format string: " + layout) + } +} + +// DaySuffixes is the default array of strings used as suffixes when a format string +// contains "nd" (as in "second"). This can be altered at startup in order to change +// the default locale strings used for formatting dates. It supports every locale that +// uses the Gregorian calendar and has a suffix after the day-of-month number. +var DaySuffixes = []string{ + "st", "nd", "rd", "th", "th", // 1 - 5 + "th", "th", "th", "th", "th", // 6 - 10 + "th", "th", "th", "th", "th", // 11 - 15 + "th", "th", "th", "th", "th", // 16 - 20 + "st", "nd", "rd", "th", "th", // 21 - 25 + "th", "th", "th", "th", "th", // 26 - 30 + "st", // 31 } diff --git a/format_test.go b/format_test.go index 9e9a482e..e0700ba7 100644 --- a/format_test.go +++ b/format_test.go @@ -142,7 +142,7 @@ func TestFormatISO(t *testing.T) { for _, c := range cases { d, err := ParseISO(c.value) if err != nil { - t.Errorf("FormatISO(%v) cannot parse input: %v", c.value, err) + t.Errorf("ParseISO(%v) cannot parse input: %v", c.value, err) continue } value := d.FormatISO(c.n) @@ -151,3 +151,36 @@ func TestFormatISO(t *testing.T) { } } } + +func TestFormat(t *testing.T) { + cases := []struct { + value string + format string + expected string + }{ + {"1970-01-01", "2 Jan 2006", "1 Jan 1970"}, + {"1970-01-01", "Jan 02 2006", "Jan 01 1970"}, + {"1970-01-01", "Jan 2nd 2006", "Jan 1st 1970"}, + {"2016-01-01", "2nd Jan 2006", "1st Jan 2016"}, + {"2016-02-02", "Jan 2nd 2006", "Feb 2nd 2016"}, + {"2016-03-03", "Jan 2nd 2006", "Mar 3rd 2016"}, + {"2016-04-04", "2nd Jan 2006", "4th Apr 2016"}, + {"2016-05-20", "Jan 2nd 2006", "May 20th 2016"}, + {"2016-06-21", "Jan 2nd 2006", "Jun 21st 2016"}, + {"2016-07-22", "Jan 2nd 2006", "Jul 22nd 2016"}, + {"2016-08-23", "Jan 2nd 2006", "Aug 23rd 2016"}, + {"2016-09-30", "Jan 2nd 2006", "Sep 30th 2016"}, + {"2016-10-31", "Jan 2nd 2006", "Oct 31st 2016"}, + } + for _, c := range cases { + d, err := ParseISO(c.value) + if err != nil { + t.Errorf("ParseISO(%v) cannot parse input: %v", c.value, err) + continue + } + actual := d.Format(c.format) + if actual != c.expected { + t.Errorf("Format(%v) == %v, want %v", c, actual, c.expected) + } + } +} diff --git a/marshal.go b/marshal.go index 5fc4529c..220d0ba2 100644 --- a/marshal.go +++ b/marshal.go @@ -1,3 +1,7 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package date import ( From fbae161bcd7e564b66082657acdb389e77006751 Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 24 Nov 2015 08:27:26 +0000 Subject: [PATCH 003/165] Generalised FormatWithSuffixes to remove the unsupported case and join strings more efficiently --- format.go | 15 +++++++++------ format_test.go | 1 + 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/format.go b/format.go index 9aabe322..71439b35 100644 --- a/format.go +++ b/format.go @@ -151,13 +151,16 @@ func (d Date) FormatWithSuffixes(layout string, suffixes []string) string { switch len(parts) { case 1: return t.Format(layout) - case 2: - front := t.Format(parts[0]) - mid := suffixes[d.Day() - 1] - back := t.Format(parts[1]) - return front + mid + back + default: - panic("Unsupported format string: " + layout) + a := make([]string, 0, 2*len(parts) - 1) + for i, p := range parts { + if i > 0 { + a = append(a, suffixes[d.Day() - 1]) + } + a = append(a, t.Format(p)) + } + return strings.Join(a, "") } } diff --git a/format_test.go b/format_test.go index e0700ba7..0c442ed2 100644 --- a/format_test.go +++ b/format_test.go @@ -171,6 +171,7 @@ func TestFormat(t *testing.T) { {"2016-08-23", "Jan 2nd 2006", "Aug 23rd 2016"}, {"2016-09-30", "Jan 2nd 2006", "Sep 30th 2016"}, {"2016-10-31", "Jan 2nd 2006", "Oct 31st 2016"}, + {"2016-11-01", "2nd 2nd 2nd", "1st 1st 1st"}, } for _, c := range cases { d, err := ParseISO(c.value) From dc1635956b9448ee2bb1714e44e33c77d6102f44 Mon Sep 17 00:00:00 2001 From: Rick Date: Wed, 25 Nov 2015 00:31:34 +0000 Subject: [PATCH 004/165] Added new test for Date.String(). Started work on DateRange. --- LICENSE | 4 +- daterange/daterange.go | 103 ++++++++++++++++++++++++++++++++++++ daterange/daterange_test.go | 93 ++++++++++++++++++++++++++++++++ format_test.go | 24 +++++++++ 4 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 daterange/daterange.go create mode 100644 daterange/daterange_test.go diff --git a/LICENSE b/LICENSE index f642e17e..b0280bc6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015 The Go Authors. All rights reserved. +Copyright (c) 2015 The Go Authors & Rick Beton. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are @@ -24,4 +24,4 @@ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/daterange/daterange.go b/daterange/daterange.go new file mode 100644 index 00000000..59eff991 --- /dev/null +++ b/daterange/daterange.go @@ -0,0 +1,103 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package daterange + +import ( + . "github.com/rickb777/date" + "time" +// "fmt" + "fmt" +) + +// DateRange carries a pair of dates encompassing a range. In operations on the range, +// the start and end are both considered to be inclusive. +type DateRange struct { + Start Date + End Date +} + +// 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; the result is +// normalised so that the end date is not before the start date. +func NewDateRangeOf(start time.Time, duration time.Duration) DateRange { + sd := NewAt(start) + ed := NewAt(start.Add(duration)) + return DateRange{sd, ed}.Normalise() +} + +// NewDateRange assembles a new date range from two dates, normalising them so that the +// end date is not before the start date. +func NewDateRange(start, end Date) DateRange { + return DateRange{start, end}.Normalise() +} + +// NewYearOf constructs the range encompassing the whole year specified. +func NewYearOf(year int) DateRange { + start := New(year, time.January, 1) + end := New(year, time.December, 31) + return DateRange{start, end} +} + +// NewMonthOf constructs the range encompassing the whole month specified for a given year. +// It handles leap years correctly. +func NewMonthOf(year int, month time.Month) DateRange { + start := New(year, month, 1) + endT := time.Date(year, month + 1, 1, 0, 0, 0, 0, time.UTC) + end := NewAt(endT.Add(-1)) + return DateRange{start, end} +} + +// OneDayRange constructs a range of exactly one day. This is often a useful basis for +// further operations. +func OneDayRange(day Date) DateRange { + return NewDateRange(day, day) +} + +// Normalise ensures that the start date is before (or equal to) the end date. +// They are swapped if necessary. The normalised date range is returned. +func (dateRange DateRange) Normalise() DateRange { + if dateRange.End.Before(dateRange.Start) { + return DateRange{dateRange.End, dateRange.Start} + } + return dateRange +} + +// ExtendBy extends (or reduces) the date range by moving the end date. +// A negative parameter is allowed and the result is normalised. +func (dateRange DateRange) ExtendBy(days int) DateRange { + if days == 0 { + return dateRange + } + // this relies on normalisation provided by the function + newEnd := dateRange.End.Add(days) + return DateRange{dateRange.Start, newEnd}.Normalise() +} + +//func (dateRange DateRange) AddWeek() DateRange { +// return dateRange.AddDays(7) +//} + +func (dateRange DateRange) String() string { + return fmt.Sprintf("%s to %s", dateRange.Start, dateRange.End) +} + +//func (dateRange DateRange) Contains(d Date) bool { +// return !(d.Before(dateRange.Start) || d.After(dateRange.End)) +//} + +//func (dateRange DateRange) Merge(other DateRange) DateRange { +// if dateRange.Start.After(other.Start) { +// // swap the ranges to simplify the logic +// return other.Merge(dateRange) +// +// } else if dateRange.End.After(other.End) { +// // other is a proper subrange of dateRange +// return dateRange +// +// } else { +// return DateRange{dateRange.Start, other.End} +// } +//} diff --git a/daterange/daterange_test.go b/daterange/daterange_test.go new file mode 100644 index 00000000..ea5e8ba7 --- /dev/null +++ b/daterange/daterange_test.go @@ -0,0 +1,93 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package daterange + +import ( + . "github.com/rickb777/date" + "testing" + "time" +) + +var t0327 = time.Date(2015, 3, 27, 0, 0, 0, 0, time.UTC) +var d0320 = New(2015, time.March, 20) +var d0327 = New(2015, time.March, 27) +var d0401 = New(2015, time.April, 1) +var d0403 = New(2015, time.April, 3) + +func TestNewDateRangeOf(t *testing.T) { + dr := NewDateRangeOf(t0327, time.Duration(7*24*60*60*1e9)) + isEq(t, dr.Start, d0327) + isEq(t, dr.End, d0403) +} + +func TestNewDateRangeWithNormalise(t *testing.T) { + r1 := NewDateRange(d0327, d0401) + isEq(t, r1.Start, d0327) + isEq(t, r1.End, d0401) + + r2 := NewDateRange(d0401, d0327) + isEq(t, r2.Start, d0327) + isEq(t, r2.End, d0401) +} + +func TestOneDayRange(t *testing.T) { + dr := OneDayRange(d0327) + isEq(t, dr.Start, d0327) + isEq(t, dr.End, d0327) +} + +func TestNewYearOf(t *testing.T) { + dr := NewYearOf(2015) + isEq(t, dr.Start, New(2015, time.January, 1)) + isEq(t, dr.End, New(2015, time.December, 31)) +} + +func TestNewMonthOf(t *testing.T) { + dr := NewMonthOf(2015, time.February) + isEq(t, dr.Start, New(2015, time.February, 1)) + isEq(t, dr.End, New(2015, time.February, 28)) +} + +//func TestAddToStart1(t *testing.T) { +// timeSpan := ZeroTimeSpan(date0327).AddYMDToStart(0, 0, 7) +// is1(t, timeSpan.Start, date0327) +// is1(t, timeSpan.End, date0403) +// is1(t, timeSpan.String(), "2015-03-27 to 2015-04-03") +//} +// +//func TestAddToStart2(t *testing.T) { +// timeSpan := ZeroTimeSpan(date0327).AddYMDToStart(0, 0, -7) +// is1(t, timeSpan.Start, date0320) +// is1(t, timeSpan.End, date0327) +// is1(t, timeSpan.String(), "2015-03-20 to 2015-03-27") +//} + +func TestExtendBy1(t *testing.T) { + timeSpan := OneDayRange(d0327).ExtendBy(7) + isEq(t, timeSpan.Start, d0327) + isEq(t, timeSpan.End, d0403) + isEq(t, timeSpan.String(), "2015-03-27 to 2015-04-03") +} + +func TestExtendBy2(t *testing.T) { + timeSpan := OneDayRange(d0327).ExtendBy(-7) + isEq(t, timeSpan.Start, d0320) + isEq(t, timeSpan.End, d0327) + isEq(t, timeSpan.String(), "2015-03-20 to 2015-03-27") +} + +//func TestAddToEnd2(t *testing.T) { +// timeSpan := ZeroTimeSpan(date0327).AddYMDToEnd(0, 0, -7) +// is1(t, timeSpan.Start, date0320) +// is1(t, timeSpan.End, date0327) +// is1(t, timeSpan.String(), "2015-03-20 to 2015-03-27") +//} + + +func isEq(t *testing.T, a, b interface{}) { + if a != b { + t.Errorf("%s %#v is not equal to %s %#v", a, a, b, b) + } +} diff --git a/format_test.go b/format_test.go index 0c442ed2..9c64c85b 100644 --- a/format_test.go +++ b/format_test.go @@ -123,6 +123,30 @@ func TestParse(t *testing.T) { } } +func TestString(t *testing.T) { + cases := []struct { + value string + }{ + {"-0001-01-01"}, + {"0000-01-01"}, + {"1000-01-01"}, + {"1970-01-01"}, + {"2000-11-22"}, + {"+10000-01-01"}, + } + for _, c := range cases { + d, err := ParseISO(c.value) + if err != nil { + t.Errorf("ParseISO(%v) cannot parse input: %v", c.value, err) + continue + } + value := d.String() + if value != c.value { + t.Errorf("String() == %v, want %v", value, c.value) + } + } +} + func TestFormatISO(t *testing.T) { cases := []struct { value string From 8e2524c492ce25b946011b3392ce8563ab2434a6 Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 26 Nov 2015 09:09:42 +0000 Subject: [PATCH 005/165] ShiftBy implemented --- daterange/daterange.go | 11 +++++++++ daterange/daterange_test.go | 45 +++++++++++++++---------------------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/daterange/daterange.go b/daterange/daterange.go index 59eff991..8b1726fc 100644 --- a/daterange/daterange.go +++ b/daterange/daterange.go @@ -65,6 +65,17 @@ func (dateRange DateRange) Normalise() DateRange { return dateRange } +// ShiftBy moves the date range by moving both the start and end dates similarly. +// A negative parameter is allowed. +func (dateRange DateRange) ShiftBy(days int) DateRange { + if days == 0 { + return dateRange + } + newStart := dateRange.Start.Add(days) + newEnd := dateRange.End.Add(days) + return DateRange{newStart, newEnd} +} + // ExtendBy extends (or reduces) the date range by moving the end date. // A negative parameter is allowed and the result is normalised. func (dateRange DateRange) ExtendBy(days int) DateRange { diff --git a/daterange/daterange_test.go b/daterange/daterange_test.go index ea5e8ba7..b54290f7 100644 --- a/daterange/daterange_test.go +++ b/daterange/daterange_test.go @@ -15,6 +15,7 @@ var d0320 = New(2015, time.March, 20) var d0327 = New(2015, time.March, 27) var d0401 = New(2015, time.April, 1) var d0403 = New(2015, time.April, 3) +var d0408 = New(2015, time.April, 8) func TestNewDateRangeOf(t *testing.T) { dr := NewDateRangeOf(t0327, time.Duration(7*24*60*60*1e9)) @@ -50,42 +51,32 @@ func TestNewMonthOf(t *testing.T) { isEq(t, dr.End, New(2015, time.February, 28)) } -//func TestAddToStart1(t *testing.T) { -// timeSpan := ZeroTimeSpan(date0327).AddYMDToStart(0, 0, 7) -// is1(t, timeSpan.Start, date0327) -// is1(t, timeSpan.End, date0403) -// is1(t, timeSpan.String(), "2015-03-27 to 2015-04-03") -//} -// -//func TestAddToStart2(t *testing.T) { -// timeSpan := ZeroTimeSpan(date0327).AddYMDToStart(0, 0, -7) -// is1(t, timeSpan.Start, date0320) -// is1(t, timeSpan.End, date0327) -// is1(t, timeSpan.String(), "2015-03-20 to 2015-03-27") -//} +func TestShiftByPos(t *testing.T) { + dr := NewDateRange(d0327, d0401).ShiftBy(7) + isEq(t, dr.Start, d0403) + isEq(t, dr.End, d0408) +} + +func TestShiftByNeg(t *testing.T) { + dr := NewDateRange(d0403, d0408).ShiftBy(-7) + isEq(t, dr.Start, d0327) + isEq(t, dr.End, d0401) +} -func TestExtendBy1(t *testing.T) { - timeSpan := OneDayRange(d0327).ExtendBy(7) - isEq(t, timeSpan.Start, d0327) - isEq(t, timeSpan.End, d0403) - isEq(t, timeSpan.String(), "2015-03-27 to 2015-04-03") +func TestExtendByPos(t *testing.T) { + dr := OneDayRange(d0327).ExtendBy(7) + isEq(t, dr.Start, d0327) + isEq(t, dr.End, d0403) + isEq(t, dr.String(), "2015-03-27 to 2015-04-03") } -func TestExtendBy2(t *testing.T) { +func TestExtendByNeg(t *testing.T) { timeSpan := OneDayRange(d0327).ExtendBy(-7) isEq(t, timeSpan.Start, d0320) isEq(t, timeSpan.End, d0327) isEq(t, timeSpan.String(), "2015-03-20 to 2015-03-27") } -//func TestAddToEnd2(t *testing.T) { -// timeSpan := ZeroTimeSpan(date0327).AddYMDToEnd(0, 0, -7) -// is1(t, timeSpan.Start, date0320) -// is1(t, timeSpan.End, date0327) -// is1(t, timeSpan.String(), "2015-03-20 to 2015-03-27") -//} - - func isEq(t *testing.T, a, b interface{}) { if a != b { t.Errorf("%s %#v is not equal to %s %#v", a, a, b, b) From a4c508ace5b506384c80125d9e74a8a529e6692b Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 26 Nov 2015 09:40:10 +0000 Subject: [PATCH 006/165] Contains implemented. --- daterange/daterange.go | 10 +++------- daterange/daterange_test.go | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/daterange/daterange.go b/daterange/daterange.go index 8b1726fc..c968290f 100644 --- a/daterange/daterange.go +++ b/daterange/daterange.go @@ -87,17 +87,13 @@ func (dateRange DateRange) ExtendBy(days int) DateRange { return DateRange{dateRange.Start, newEnd}.Normalise() } -//func (dateRange DateRange) AddWeek() DateRange { -// return dateRange.AddDays(7) -//} - func (dateRange DateRange) String() string { return fmt.Sprintf("%s to %s", dateRange.Start, dateRange.End) } -//func (dateRange DateRange) Contains(d Date) bool { -// return !(d.Before(dateRange.Start) || d.After(dateRange.End)) -//} +func (dateRange DateRange) Contains(d Date) bool { + return !(d.Before(dateRange.Start) || d.After(dateRange.End)) +} //func (dateRange DateRange) Merge(other DateRange) DateRange { // if dateRange.Start.After(other.Start) { diff --git a/daterange/daterange_test.go b/daterange/daterange_test.go index b54290f7..df4b537d 100644 --- a/daterange/daterange_test.go +++ b/daterange/daterange_test.go @@ -16,6 +16,7 @@ var d0327 = New(2015, time.March, 27) var d0401 = New(2015, time.April, 1) var d0403 = New(2015, time.April, 3) var d0408 = New(2015, time.April, 8) +var d0410 = New(2015, time.April, 10) func TestNewDateRangeOf(t *testing.T) { dr := NewDateRangeOf(t0327, time.Duration(7*24*60*60*1e9)) @@ -71,10 +72,17 @@ func TestExtendByPos(t *testing.T) { } func TestExtendByNeg(t *testing.T) { - timeSpan := OneDayRange(d0327).ExtendBy(-7) - isEq(t, timeSpan.Start, d0320) - isEq(t, timeSpan.End, d0327) - isEq(t, timeSpan.String(), "2015-03-20 to 2015-03-27") + dr := OneDayRange(d0327).ExtendBy(-7) + isEq(t, dr.Start, d0320) + isEq(t, dr.End, d0327) + isEq(t, dr.String(), "2015-03-20 to 2015-03-27") +} + +func TestContains(t *testing.T) { + dr := OneDayRange(d0327).ExtendBy(7) + isEq(t, dr.Contains(d0401), true) + isEq(t, dr.Contains(d0410), false) + isEq(t, dr.Contains(d0320), false) } func isEq(t *testing.T, a, b interface{}) { From 4ac7702935b1afe86c875664c0fea74e1f1d90da Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 26 Nov 2015 20:04:37 +0000 Subject: [PATCH 007/165] Implemented ContainsTime, with StartUTC and EndUTC. --- daterange/daterange.go | 20 ++++++++ daterange/daterange_test.go | 91 +++++++++++++++++++++++++------------ 2 files changed, 83 insertions(+), 28 deletions(-) diff --git a/daterange/daterange.go b/daterange/daterange.go index c968290f..d957692a 100644 --- a/daterange/daterange.go +++ b/daterange/daterange.go @@ -95,6 +95,26 @@ func (dateRange DateRange) Contains(d Date) bool { return !(d.Before(dateRange.Start) || d.After(dateRange.End)) } +// 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.UTC() +} + +// EndUTC assumes that the end date is a UTC date and gets the end time of that date, as UTC. +// It returns the very last nanosecond before midnight the following day. +func (dateRange DateRange) EndUTC() time.Time { + return dateRange.End.Add(1).UTC().Add(minusOneNano) +} + +const minusOneNano time.Duration = -1 + +// ContainsTime tests whether a given local time is within the date range. +func (dateRange DateRange) ContainsTime(t time.Time) bool { + utc := t.In(time.UTC) + return !(utc.Before(dateRange.StartUTC()) || dateRange.EndUTC().Before(utc)) +} + //func (dateRange DateRange) Merge(other DateRange) DateRange { // if dateRange.Start.After(other.Start) { // // swap the ranges to simplify the logic diff --git a/daterange/daterange_test.go b/daterange/daterange_test.go index df4b537d..e11dd296 100644 --- a/daterange/daterange_test.go +++ b/daterange/daterange_test.go @@ -8,85 +8,120 @@ import ( . "github.com/rickb777/date" "testing" "time" + "fmt" + "strings" ) var t0327 = time.Date(2015, 3, 27, 0, 0, 0, 0, time.UTC) +var t0328 = time.Date(2015, 3, 28, 0, 0, 0, 0, time.UTC) var d0320 = New(2015, time.March, 20) +var d0325 = New(2015, time.March, 25) +var d0326 = New(2015, time.March, 26) var d0327 = New(2015, time.March, 27) +var d0328 = New(2015, time.March, 28) var d0401 = New(2015, time.April, 1) var d0403 = New(2015, time.April, 3) var d0408 = New(2015, time.April, 8) var d0410 = New(2015, time.April, 10) +var d0501 = New(2015, time.May, 1) func TestNewDateRangeOf(t *testing.T) { dr := NewDateRangeOf(t0327, time.Duration(7*24*60*60*1e9)) - isEq(t, dr.Start, d0327) - isEq(t, dr.End, d0403) + isEq(t, dr.Start, d0327, "") + isEq(t, dr.End, d0403, "") } func TestNewDateRangeWithNormalise(t *testing.T) { r1 := NewDateRange(d0327, d0401) - isEq(t, r1.Start, d0327) - isEq(t, r1.End, d0401) + isEq(t, r1.Start, d0327, "") + isEq(t, r1.End, d0401, "") r2 := NewDateRange(d0401, d0327) - isEq(t, r2.Start, d0327) - isEq(t, r2.End, d0401) + isEq(t, r2.Start, d0327, "") + isEq(t, r2.End, d0401, "") } func TestOneDayRange(t *testing.T) { dr := OneDayRange(d0327) - isEq(t, dr.Start, d0327) - isEq(t, dr.End, d0327) + isEq(t, dr.Start, d0327, "") + isEq(t, dr.End, d0327, "") } func TestNewYearOf(t *testing.T) { dr := NewYearOf(2015) - isEq(t, dr.Start, New(2015, time.January, 1)) - isEq(t, dr.End, New(2015, time.December, 31)) + isEq(t, dr.Start, New(2015, time.January, 1), "") + isEq(t, dr.End, New(2015, time.December, 31), "") } func TestNewMonthOf(t *testing.T) { dr := NewMonthOf(2015, time.February) - isEq(t, dr.Start, New(2015, time.February, 1)) - isEq(t, dr.End, New(2015, time.February, 28)) + isEq(t, dr.Start, New(2015, time.February, 1), "") + isEq(t, dr.End, New(2015, time.February, 28), "") } func TestShiftByPos(t *testing.T) { dr := NewDateRange(d0327, d0401).ShiftBy(7) - isEq(t, dr.Start, d0403) - isEq(t, dr.End, d0408) + isEq(t, dr.Start, d0403, "") + isEq(t, dr.End, d0408, "") } func TestShiftByNeg(t *testing.T) { dr := NewDateRange(d0403, d0408).ShiftBy(-7) - isEq(t, dr.Start, d0327) - isEq(t, dr.End, d0401) + isEq(t, dr.Start, d0327, "") + isEq(t, dr.End, d0401, "") } func TestExtendByPos(t *testing.T) { dr := OneDayRange(d0327).ExtendBy(7) - isEq(t, dr.Start, d0327) - isEq(t, dr.End, d0403) - isEq(t, dr.String(), "2015-03-27 to 2015-04-03") + isEq(t, dr.Start, d0327, "") + isEq(t, dr.End, d0403, "") + isEq(t, dr.String(), "2015-03-27 to 2015-04-03", "") } func TestExtendByNeg(t *testing.T) { dr := OneDayRange(d0327).ExtendBy(-7) - isEq(t, dr.Start, d0320) - isEq(t, dr.End, d0327) - isEq(t, dr.String(), "2015-03-20 to 2015-03-27") + isEq(t, dr.Start, d0320, "") + isEq(t, dr.End, d0327, "") + isEq(t, dr.String(), "2015-03-20 to 2015-03-27", "") } func TestContains(t *testing.T) { - dr := OneDayRange(d0327).ExtendBy(7) - isEq(t, dr.Contains(d0401), true) - isEq(t, dr.Contains(d0410), false) - isEq(t, dr.Contains(d0320), false) + old := time.Local + time.Local = time.FixedZone("Test", 7200) + dr := OneDayRange(d0326).ExtendBy(1) + isEq(t, dr.Contains(d0320), false, dr, d0320) + isEq(t, dr.Contains(d0325), false, dr, d0325) + isEq(t, dr.Contains(d0326), true, dr, d0326) + isEq(t, dr.Contains(d0327), true, dr, d0327) + isEq(t, dr.Contains(d0328), false, dr, d0328) + isEq(t, dr.Contains(d0401), false, dr, d0401) + isEq(t, dr.Contains(d0410), false, dr, d0410) + isEq(t, dr.Contains(d0501), false, dr, d0501) + time.Local = old +} + +func TestContainsTimeUTC(t *testing.T) { + old := time.Local + time.Local = time.FixedZone("Test", 7200) + t0328e := time.Date(2015, 3, 28, 23, 59, 59, 999999999, time.UTC) + t0329 := time.Date(2015, 3, 29, 0, 0, 0, 0, time.UTC) + + dr := OneDayRange(d0327).ExtendBy(1) + isEq(t, dr.StartUTC(), t0327, dr, t0327) + isEq(t, dr.EndUTC(), t0328e, dr, t0328e) + isEq(t, dr.ContainsTime(t0327), true, dr, t0327) + isEq(t, dr.ContainsTime(t0328), true, dr, t0328) + isEq(t, dr.ContainsTime(t0328e), true, dr, t0328e) + isEq(t, dr.ContainsTime(t0329), false, dr, t0329) + time.Local = old } -func isEq(t *testing.T, a, b interface{}) { +func isEq(t *testing.T, a, b interface{}, msg ...interface{}) { if a != b { - t.Errorf("%s %#v is not equal to %s %#v", a, a, b, b) + sa := make([]string, len(msg)) + for i, m := range msg { + sa[i] = fmt.Sprintf(", %v", m) + } + t.Errorf("%v (%#v) is not equal to %v (%#v)%s", a, a, b, b, strings.Join(sa, "")) } } From 4dfa4eafea64e9f7d58b781df627a2b0e140279b Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 26 Nov 2015 20:26:00 +0000 Subject: [PATCH 008/165] Implemented Merge. --- daterange/daterange.go | 40 ++++++++++------ daterange/daterange_test.go | 96 ++++++++++++++++++++++++++++--------- 2 files changed, 99 insertions(+), 37 deletions(-) diff --git a/daterange/daterange.go b/daterange/daterange.go index d957692a..25f7d8a2 100644 --- a/daterange/daterange.go +++ b/daterange/daterange.go @@ -91,6 +91,11 @@ func (dateRange DateRange) String() string { return fmt.Sprintf("%s to %s", dateRange.Start, dateRange.End) } +// Contains tests whether the date range contains a specified date. The start and end of +// the range are both treated inclusively. +// +// If a calculation needs to be 'half-open' (i.e. the end is exclusive), simply use the +// expression 'dateRange.ExtendBy(-1).Contains(d)' func (dateRange DateRange) Contains(d Date) bool { return !(d.Before(dateRange.Start) || d.After(dateRange.End)) } @@ -109,22 +114,29 @@ func (dateRange DateRange) EndUTC() time.Time { const minusOneNano time.Duration = -1 -// ContainsTime tests whether a given local time is within the date range. +// ContainsTime tests whether a given local time is within the date range. The time range is +// from midnight on the start day to one nanosecond before midnight on the day after the end date. +// +// If a calculation needs to be 'half-open' (i.e. the end is exclusive), simply use the +// expression 'dateRange.ExtendBy(-1).ContainsTime(t)' func (dateRange DateRange) ContainsTime(t time.Time) bool { utc := t.In(time.UTC) return !(utc.Before(dateRange.StartUTC()) || dateRange.EndUTC().Before(utc)) } -//func (dateRange DateRange) Merge(other DateRange) DateRange { -// if dateRange.Start.After(other.Start) { -// // swap the ranges to simplify the logic -// return other.Merge(dateRange) -// -// } else if dateRange.End.After(other.End) { -// // other is a proper subrange of dateRange -// return dateRange -// -// } else { -// return DateRange{dateRange.Start, other.End} -// } -//} +// Merge conjoins two date ranges. As a special case, if one range is entirely contained within +// the other range, the larger of the two is returned. Otherwise, the result is the start of the +// earlier one to the end of the later one, even if the two ranges don't overlap. +func (dateRange DateRange) Merge(other DateRange) DateRange { + if dateRange.Start.After(other.Start) { + // swap the ranges to simplify the logic + return other.Merge(dateRange) + + } else if dateRange.End.After(other.End) { + // other is a proper subrange of dateRange + return dateRange + + } else { + return DateRange{dateRange.Start, other.End} + } +} diff --git a/daterange/daterange_test.go b/daterange/daterange_test.go index e11dd296..5dd0e724 100644 --- a/daterange/daterange_test.go +++ b/daterange/daterange_test.go @@ -27,65 +27,65 @@ var d0501 = New(2015, time.May, 1) func TestNewDateRangeOf(t *testing.T) { dr := NewDateRangeOf(t0327, time.Duration(7*24*60*60*1e9)) - isEq(t, dr.Start, d0327, "") - isEq(t, dr.End, d0403, "") + isEq(t, dr.Start, d0327) + isEq(t, dr.End, d0403) } func TestNewDateRangeWithNormalise(t *testing.T) { r1 := NewDateRange(d0327, d0401) - isEq(t, r1.Start, d0327, "") - isEq(t, r1.End, d0401, "") + isEq(t, r1.Start, d0327) + isEq(t, r1.End, d0401) r2 := NewDateRange(d0401, d0327) - isEq(t, r2.Start, d0327, "") - isEq(t, r2.End, d0401, "") + isEq(t, r2.Start, d0327) + isEq(t, r2.End, d0401) } func TestOneDayRange(t *testing.T) { dr := OneDayRange(d0327) - isEq(t, dr.Start, d0327, "") - isEq(t, dr.End, d0327, "") + isEq(t, dr.Start, d0327) + isEq(t, dr.End, d0327) } func TestNewYearOf(t *testing.T) { dr := NewYearOf(2015) - isEq(t, dr.Start, New(2015, time.January, 1), "") - isEq(t, dr.End, New(2015, time.December, 31), "") + isEq(t, dr.Start, New(2015, time.January, 1)) + isEq(t, dr.End, New(2015, time.December, 31)) } func TestNewMonthOf(t *testing.T) { dr := NewMonthOf(2015, time.February) - isEq(t, dr.Start, New(2015, time.February, 1), "") - isEq(t, dr.End, New(2015, time.February, 28), "") + isEq(t, dr.Start, New(2015, time.February, 1)) + isEq(t, dr.End, New(2015, time.February, 28)) } func TestShiftByPos(t *testing.T) { dr := NewDateRange(d0327, d0401).ShiftBy(7) - isEq(t, dr.Start, d0403, "") - isEq(t, dr.End, d0408, "") + isEq(t, dr.Start, d0403) + isEq(t, dr.End, d0408) } func TestShiftByNeg(t *testing.T) { dr := NewDateRange(d0403, d0408).ShiftBy(-7) - isEq(t, dr.Start, d0327, "") - isEq(t, dr.End, d0401, "") + isEq(t, dr.Start, d0327) + isEq(t, dr.End, d0401) } func TestExtendByPos(t *testing.T) { dr := OneDayRange(d0327).ExtendBy(7) - isEq(t, dr.Start, d0327, "") - isEq(t, dr.End, d0403, "") - isEq(t, dr.String(), "2015-03-27 to 2015-04-03", "") + isEq(t, dr.Start, d0327) + isEq(t, dr.End, d0403) + isEq(t, dr.String(), "2015-03-27 to 2015-04-03") } func TestExtendByNeg(t *testing.T) { dr := OneDayRange(d0327).ExtendBy(-7) - isEq(t, dr.Start, d0320, "") - isEq(t, dr.End, d0327, "") - isEq(t, dr.String(), "2015-03-20 to 2015-03-27", "") + isEq(t, dr.Start, d0320) + isEq(t, dr.End, d0327) + isEq(t, dr.String(), "2015-03-20 to 2015-03-27") } -func TestContains(t *testing.T) { +func TestContains1(t *testing.T) { old := time.Local time.Local = time.FixedZone("Test", 7200) dr := OneDayRange(d0326).ExtendBy(1) @@ -100,6 +100,16 @@ func TestContains(t *testing.T) { time.Local = old } +func TestContains2(t *testing.T) { + old := time.Local + time.Local = time.FixedZone("Test", 7200) + dr := OneDayRange(d0326) + isEq(t, dr.Contains(d0325), false, dr, d0325) + isEq(t, dr.Contains(d0326), true, dr, d0326) + isEq(t, dr.Contains(d0327), false, dr, d0327) + time.Local = old +} + func TestContainsTimeUTC(t *testing.T) { old := time.Local time.Local = time.FixedZone("Test", 7200) @@ -116,6 +126,46 @@ func TestContainsTimeUTC(t *testing.T) { time.Local = old } +func TestMerge1(t *testing.T) { + dr1 := OneDayRange(d0327).ExtendBy(1) + dr2 := OneDayRange(d0327).ExtendBy(7) + m1 := dr1.Merge(dr2) + m2 := dr2.Merge(dr1) + isEq(t, m1.Start, d0327) + isEq(t, m1.End, d0403) + isEq(t, m1, m2) +} + +func TestMerge2(t *testing.T) { + dr1 := OneDayRange(d0327).ExtendBy(1).ShiftBy(1) + dr2 := OneDayRange(d0327).ExtendBy(7) + m1 := dr1.Merge(dr2) + m2 := dr2.Merge(dr1) + isEq(t, m1.Start, d0327) + isEq(t, m1.End, d0403) + isEq(t, m1, m2) +} + +func TestMergeOverlapping(t *testing.T) { + dr1 := OneDayRange(d0320).ExtendBy(12) + dr2 := OneDayRange(d0401).ExtendBy(7) + m1 := dr1.Merge(dr2) + m2 := dr2.Merge(dr1) + isEq(t, m1.Start, d0320) + isEq(t, m1.End, d0408) + isEq(t, m1, m2) +} + +func TestMergeNonOverlapping(t *testing.T) { + dr1 := OneDayRange(d0320).ExtendBy(2) + dr2 := OneDayRange(d0401).ExtendBy(7) + m1 := dr1.Merge(dr2) + m2 := dr2.Merge(dr1) + isEq(t, m1.Start, d0320) + isEq(t, m1.End, d0408) + isEq(t, m1, m2) +} + func isEq(t *testing.T, a, b interface{}, msg ...interface{}) { if a != b { sa := make([]string, len(msg)) From 53ced14e812288120c11414fade6e5c6ae8e0d01 Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 26 Nov 2015 20:28:21 +0000 Subject: [PATCH 009/165] package renamed to timespan --- {daterange => timespan}/daterange.go | 2 +- {daterange => timespan}/daterange_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename {daterange => timespan}/daterange.go (99%) rename {daterange => timespan}/daterange_test.go (99%) diff --git a/daterange/daterange.go b/timespan/daterange.go similarity index 99% rename from daterange/daterange.go rename to timespan/daterange.go index 25f7d8a2..1df24bb3 100644 --- a/daterange/daterange.go +++ b/timespan/daterange.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package daterange +package timsepan import ( . "github.com/rickb777/date" diff --git a/daterange/daterange_test.go b/timespan/daterange_test.go similarity index 99% rename from daterange/daterange_test.go rename to timespan/daterange_test.go index 5dd0e724..68593217 100644 --- a/daterange/daterange_test.go +++ b/timespan/daterange_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package daterange +package timsepan import ( . "github.com/rickb777/date" From 505aff1af5f7763a3df2516e4180e132f8dfd682 Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 26 Nov 2015 21:01:31 +0000 Subject: [PATCH 010/165] Revised the behaviour of EndUTC. Added StartTimeIn, EndTimeIn, Duration and DurationIn. --- timespan/daterange.go | 43 ++++++++++++++++++++++++++++++++++---- timespan/daterange_test.go | 22 +++++++++++++++++-- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/timespan/daterange.go b/timespan/daterange.go index 1df24bb3..1c6748ea 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -106,10 +106,11 @@ func (dateRange DateRange) StartUTC() time.Time { return dateRange.Start.UTC() } -// EndUTC assumes that the end date is a UTC date and gets the end time of that date, as UTC. -// It returns the very last nanosecond before midnight the following day. +// EndUTC assumes that the end date is a UTC date and returns the nanosecond after the end time +// in a specified location. Along with StartUTC, this gives a 'half-open' range where the start +// is inclusive and the end is exclusive. func (dateRange DateRange) EndUTC() time.Time { - return dateRange.End.Add(1).UTC().Add(minusOneNano) + return dateRange.End.Add(1).UTC() } const minusOneNano time.Duration = -1 @@ -121,7 +122,7 @@ const minusOneNano time.Duration = -1 // expression 'dateRange.ExtendBy(-1).ContainsTime(t)' func (dateRange DateRange) ContainsTime(t time.Time) bool { utc := t.In(time.UTC) - return !(utc.Before(dateRange.StartUTC()) || dateRange.EndUTC().Before(utc)) + return !(utc.Before(dateRange.StartUTC()) || dateRange.EndUTC().Add(minusOneNano).Before(utc)) } // Merge conjoins two date ranges. As a special case, if one range is entirely contained within @@ -140,3 +141,37 @@ func (dateRange DateRange) Merge(other DateRange) DateRange { return DateRange{dateRange.Start, other.End} } } + +// Duration computes the duration (in nanoseconds) from midnight at the start of the date +// range up to and including the very last nanosecond before midnight the following day after the end. +// The calculation is for UTC, which does not have daylight saving and every day has 24 hours. +// +// 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.Add(1).UTC().Sub(dateRange.Start.UTC()) +} + +// DurationIn computes the duration (in nanoseconds) from midnight at the start of the date +// range up to and including the very last nanosecond before midnight the following day after the end. +// The calculation is for the specified location, which may have daylight saving, so not every day has +// 24 hours. If the date range spans the day the clocks are changed, this is taken into account. +// +// 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) DurationIn(loc *time.Location) time.Duration { + return dateRange.EndTimeIn(loc).Sub(dateRange.StartTimeIn(loc)) +} + +// StartTimeIn returns the start time in a specified location. +func (dateRange DateRange) StartTimeIn(loc *time.Location) time.Time { + return dateRange.Start.In(loc) +} + +// EndTimeIn returns the nanosecond after the end time in a specified location. Along with +// StartTimeIn, this gives a 'half-open' range where the start is inclusive and the end is +// exclusive. +func (dateRange DateRange) EndTimeIn(loc *time.Location) time.Time { + return dateRange.End.Add(1).In(loc) +} + diff --git a/timespan/daterange_test.go b/timespan/daterange_test.go index 68593217..39796923 100644 --- a/timespan/daterange_test.go +++ b/timespan/daterange_test.go @@ -10,6 +10,7 @@ import ( "time" "fmt" "strings" + "runtime/debug" ) var t0327 = time.Date(2015, 3, 27, 0, 0, 0, 0, time.UTC) @@ -19,11 +20,13 @@ var d0325 = New(2015, time.March, 25) var d0326 = New(2015, time.March, 26) var d0327 = New(2015, time.March, 27) var d0328 = New(2015, time.March, 28) +var d0329 = New(2015, time.March, 29) var d0401 = New(2015, time.April, 1) var d0403 = New(2015, time.April, 3) var d0408 = New(2015, time.April, 8) var d0410 = New(2015, time.April, 10) var d0501 = New(2015, time.May, 1) +var d1025 = New(2015, time.October, 25) func TestNewDateRangeOf(t *testing.T) { dr := NewDateRangeOf(t0327, time.Duration(7*24*60*60*1e9)) @@ -118,7 +121,7 @@ func TestContainsTimeUTC(t *testing.T) { dr := OneDayRange(d0327).ExtendBy(1) isEq(t, dr.StartUTC(), t0327, dr, t0327) - isEq(t, dr.EndUTC(), t0328e, dr, t0328e) + isEq(t, dr.EndUTC(), t0329, dr, t0329) isEq(t, dr.ContainsTime(t0327), true, dr, t0327) isEq(t, dr.ContainsTime(t0328), true, dr, t0328) isEq(t, dr.ContainsTime(t0328e), true, dr, t0328e) @@ -166,12 +169,27 @@ func TestMergeNonOverlapping(t *testing.T) { isEq(t, m1, m2) } +func TestDurationNormalUTC(t *testing.T) { + dr := OneDayRange(d0329) + isEq(t, dr.Duration(), time.Hour * 24) +} + +func TestDurationInZoneWithDaylightSaving(t *testing.T) { + london, err := time.LoadLocation("Europe/London") + if err != nil { + panic(err) + } + isEq(t, OneDayRange(d0328).DurationIn(london), time.Hour * 24) + isEq(t, OneDayRange(d0329).DurationIn(london), time.Hour * 23) + isEq(t, OneDayRange(d1025).DurationIn(london), time.Hour * 25) +} + func isEq(t *testing.T, a, b interface{}, msg ...interface{}) { if a != b { sa := make([]string, len(msg)) for i, m := range msg { sa[i] = fmt.Sprintf(", %v", m) } - t.Errorf("%v (%#v) is not equal to %v (%#v)%s", a, a, b, b, strings.Join(sa, "")) + t.Errorf("%v (%#v) is not equal to %v (%#v)%s\n%s", a, a, b, b, strings.Join(sa, ""), debug.Stack()) } } From 5051d00ae2bc0db8cdaf4adfb4c4d2e1cd11ff19 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 27 Nov 2015 21:17:42 +0000 Subject: [PATCH 011/165] DateRange is now mark+days. Brought in TimeSpan, which is also Mark+Duration. New Days type. --- date.go | 12 ++- date_test.go | 2 +- timespan/daterange.go | 179 +++++++++++++++++++++++----------- timespan/daterange_test.go | 137 ++++++++++++++++---------- timespan/timespan.go | 129 +++++++++++++++++++++++++ timespan/timespan_test.go | 192 +++++++++++++++++++++++++++++++++++++ 6 files changed, 540 insertions(+), 111 deletions(-) create mode 100644 timespan/timespan.go create mode 100644 timespan/timespan_test.go diff --git a/date.go b/date.go index 9af57d66..7927c3bf 100644 --- a/date.go +++ b/date.go @@ -71,6 +71,10 @@ type Date struct { day int32 } +// Days describes a period of time measured in whole days. Negative values +// indicate days earlier than some mark. +type Days int32 + // New returns the Date value corresponding to the given year, month, and day. // // The month and day may be outside their usual ranges and will be normalized @@ -206,8 +210,8 @@ func (d Date) After(u Date) bool { return d.day > u.day } -// Add returns the date d plus the given number of days. -func (d Date) Add(days int) Date { +// Add returns the date d plus the given number of days. The parameter may be negative. +func (d Date) Add(days Days) Date { return Date{d.day + int32(days)} } @@ -221,6 +225,6 @@ func (d Date) AddDate(years, months, days int) Date { } // Sub returns d-u as the number of days between the two dates. -func (d Date) Sub(u Date) (days int) { - return int(d.day - u.day) +func (d Date) Sub(u Date) (days Days) { + return Days(d.day - u.day) } diff --git a/date_test.go b/date_test.go index c5bed0ac..fa92c2f4 100644 --- a/date_test.go +++ b/date_test.go @@ -190,7 +190,7 @@ func TestArithmetic(t *testing.T) { {1999, time.December, 1}, {1111111, time.June, 21}, } - offsets := []int{-1000000, -9999, -555, -99, -22, -1, 0, 1, 22, 99, 555, 9999, 1000000} + offsets := []Days{-1000000, -9999, -555, -99, -22, -1, 0, 1, 22, 99, 555, 9999, 1000000} for _, c := range cases { d := New(c.year, c.month, c.day) for _, days := range offsets { diff --git a/timespan/daterange.go b/timespan/daterange.go index 1c6748ea..94da8bd5 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -2,43 +2,44 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package timsepan +package timespan import ( . "github.com/rickb777/date" "time" -// "fmt" "fmt" ) -// DateRange carries a pair of dates encompassing a range. In operations on the range, -// the start and end are both considered to be inclusive. +const minusOneNano time.Duration = -1 + +// DateRange carries a date and a number of days and describes a range between two dates. type DateRange struct { - Start Date - End Date + mark Date + days Days } // 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; the result is -// normalised so that the end date is not before the start date. +// necessarily UTC. The duration can be negative. func NewDateRangeOf(start time.Time, duration time.Duration) DateRange { sd := NewAt(start) ed := NewAt(start.Add(duration)) - return DateRange{sd, ed}.Normalise() + return DateRange{sd, Days(ed.Sub(sd))} } -// NewDateRange assembles a new date range from two dates, normalising them so that the -// end date is not before the start date. +// NewDateRange assembles a new date range from two dates. func NewDateRange(start, end Date) DateRange { - return DateRange{start, end}.Normalise() + if end.Before(start) { + return DateRange{start, Days(end.Sub(start) - 1)} + } + return DateRange{start, Days(end.Sub(start) + 1)} } // NewYearOf constructs the range encompassing the whole year specified. func NewYearOf(year int) DateRange { start := New(year, time.January, 1) - end := New(year, time.December, 31) - return DateRange{start, end} + end := New(year + 1, time.January, 1) + return DateRange{start, Days(end.Sub(start))} } // NewMonthOf constructs the range encompassing the whole month specified for a given year. @@ -46,99 +47,156 @@ func NewYearOf(year int) DateRange { func NewMonthOf(year int, month time.Month) DateRange { start := New(year, month, 1) endT := time.Date(year, month + 1, 1, 0, 0, 0, 0, time.UTC) - end := NewAt(endT.Add(-1)) - return DateRange{start, end} + end := NewAt(endT) + return DateRange{start, Days(end.Sub(start))} +} + +// ZeroRange constructs an empty range. This is often a useful basis for +// further operations but note that the end date is undefined. +func ZeroRange(day Date) DateRange { + return DateRange{day, 0} } // OneDayRange constructs a range of exactly one day. This is often a useful basis for -// further operations. +// further operations. Note that the end date is the same as the start date. func OneDayRange(day Date) DateRange { - return NewDateRange(day, day) + return DateRange{day, 1} +} + +// Days returns the period represented by this range. +func (dateRange DateRange) Days() Days { + return dateRange.days +} + +// Start returns the earliest date represented by this range. +func (dateRange DateRange) Start() Date { + if dateRange.days < 0 { + return dateRange.mark.Add(Days(1 + dateRange.days)) + } + return dateRange.mark +} + +// End returns the latest date (inclusive) represented by this range. If the range is empty (i.e. +// has zero days), then an empty date is returned. +func (dateRange DateRange) End() Date { + if dateRange.days < 0 { + return dateRange.mark + } else if dateRange.days == 0 { + return Date{} + } + return dateRange.mark.Add(dateRange.days - 1) } -// Normalise ensures that the start date is before (or equal to) the end date. -// They are swapped if necessary. The normalised date range is returned. +// Next returns the date that follows the end date of the range. If the range is empty (i.e. +// has zero days), then an empty date is returned. +func (dateRange DateRange) Next() Date { + if dateRange.days < 0 { + return dateRange.mark.Add(1) + } else if dateRange.days == 0 { + return Date{} + } + return dateRange.mark.Add(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.End.Before(dateRange.Start) { - return DateRange{dateRange.End, dateRange.Start} + if dateRange.days < 0 { + return DateRange{dateRange.mark.Add(dateRange.days), -dateRange.days} } return dateRange } // ShiftBy moves the date range by moving both the start and end dates similarly. // A negative parameter is allowed. -func (dateRange DateRange) ShiftBy(days int) DateRange { +func (dateRange DateRange) ShiftBy(days Days) DateRange { if days == 0 { return dateRange } - newStart := dateRange.Start.Add(days) - newEnd := dateRange.End.Add(days) - return DateRange{newStart, newEnd} + newMark := dateRange.mark.Add(days) + return DateRange{newMark, dateRange.days} } // ExtendBy extends (or reduces) the date range by moving the end date. -// A negative parameter is allowed and the result is normalised. -func (dateRange DateRange) ExtendBy(days int) DateRange { +// 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). +func (dateRange DateRange) ExtendBy(days Days) DateRange { if days == 0 { return dateRange } - // this relies on normalisation provided by the function - newEnd := dateRange.End.Add(days) - return DateRange{dateRange.Start, newEnd}.Normalise() + return DateRange{dateRange.mark, dateRange.days + days} } func (dateRange DateRange) String() string { - return fmt.Sprintf("%s to %s", dateRange.Start, dateRange.End) + switch dateRange.days { + case 0: + return fmt.Sprintf("0 days from %s", dateRange.mark) + case 1, -1: + return fmt.Sprintf("1 day on %s", dateRange.mark) + default: + if dateRange.days < 0 { + return fmt.Sprintf("%d days from %s to %s", -dateRange.days, dateRange.Start(), dateRange.End()) + } + return fmt.Sprintf("%d days from %s to %s", dateRange.days, dateRange.Start(), dateRange.End()) + } } -// Contains tests whether the date range contains a specified date. The start and end of -// the range are both treated inclusively. -// -// If a calculation needs to be 'half-open' (i.e. the end is exclusive), simply use the -// expression 'dateRange.ExtendBy(-1).Contains(d)' +// Contains tests whether the date range contains a specified date. +// Empty date ranges (i.e. zero days) never contain anything. func (dateRange DateRange) Contains(d Date) bool { - return !(d.Before(dateRange.Start) || d.After(dateRange.End)) + if dateRange.days == 0 { + return false + } + return !(d.Before(dateRange.Start()) || d.After(dateRange.End())) } // 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.UTC() + return dateRange.Start().UTC() } -// EndUTC assumes that the end date is a UTC date and returns the nanosecond after the end time +// EndUTC assumes that the end date is a UTC date and returns the time a nanosecond after the end time // in a specified location. Along with StartUTC, this gives a 'half-open' range where the start // is inclusive and the end is exclusive. func (dateRange DateRange) EndUTC() time.Time { - return dateRange.End.Add(1).UTC() + return dateRange.Next().UTC() } -const minusOneNano time.Duration = -1 - // ContainsTime tests whether a given local time is within the date range. The time range is // from midnight on the start day to one nanosecond before midnight on the day after the end date. +// Empty date ranges (i.e. zero days) never contain anything. // -// If a calculation needs to be 'half-open' (i.e. the end is exclusive), simply use the +// If a calculation needs to be 'half-open' (i.e. the end date is exclusive), simply use the // expression 'dateRange.ExtendBy(-1).ContainsTime(t)' func (dateRange DateRange) ContainsTime(t time.Time) bool { + if dateRange.days == 0 { + return false + } utc := t.In(time.UTC) return !(utc.Before(dateRange.StartUTC()) || dateRange.EndUTC().Add(minusOneNano).Before(utc)) } -// Merge conjoins two date ranges. As a special case, if one range is entirely contained within -// the other range, the larger of the two is returned. Otherwise, the result is the start of the -// earlier one to the end of the later one, even if the two ranges don't overlap. +// Merge combines two date ranges by calculating a date range that just encompasses them both. +// As a special case, if one range is entirely contained within the other range, the larger of +// the two is returned. Otherwise, the result is the start of the earlier one to the end of the +// later one, even if the two ranges don't overlap. func (dateRange DateRange) Merge(other DateRange) DateRange { - if dateRange.Start.After(other.Start) { + start := dateRange.Start() + if start.After(other.Start()) { // swap the ranges to simplify the logic return other.Merge(dateRange) - } else if dateRange.End.After(other.End) { - // other is a proper subrange of dateRange - return dateRange - } else { - return DateRange{dateRange.Start, other.End} + oEnd := other.End() + if dateRange.End().After(oEnd) { + // other is a proper subrange of dateRange + return dateRange + + } else { + return NewDateRange(start, oEnd) + } } } @@ -149,7 +207,7 @@ func (dateRange DateRange) Merge(other 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.Add(1).UTC().Sub(dateRange.Start.UTC()) + return dateRange.Next().UTC().Sub(dateRange.Start().UTC()) } // DurationIn computes the duration (in nanoseconds) from midnight at the start of the date @@ -165,13 +223,22 @@ 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.In(loc) + return dateRange.Start().In(loc) } // EndTimeIn returns the nanosecond after the end time in a specified location. Along with // StartTimeIn, this gives a 'half-open' range where the start is inclusive and the end is // exclusive. func (dateRange DateRange) EndTimeIn(loc *time.Location) time.Time { - return dateRange.End.Add(1).In(loc) + return dateRange.Next().In(loc) +} + +// TimeSpanIn obtains the time span corresponding to the date range in a specified location. +// The result is normalised. +func (dateRange DateRange) TimeSpanIn(loc *time.Location) TimeSpan { + dr := dateRange.Normalise() + s := dr.StartTimeIn(loc) + d := dr.DurationIn(loc) + return TimeSpan{s, d} } diff --git a/timespan/daterange_test.go b/timespan/daterange_test.go index 39796923..615415e6 100644 --- a/timespan/daterange_test.go +++ b/timespan/daterange_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package timsepan +package timespan import ( . "github.com/rickb777/date" @@ -13,82 +13,122 @@ import ( "runtime/debug" ) -var t0327 = time.Date(2015, 3, 27, 0, 0, 0, 0, time.UTC) -var t0328 = time.Date(2015, 3, 28, 0, 0, 0, 0, time.UTC) var d0320 = New(2015, time.March, 20) var d0325 = New(2015, time.March, 25) var d0326 = New(2015, time.March, 26) var d0327 = New(2015, time.March, 27) var d0328 = New(2015, time.March, 28) -var d0329 = New(2015, time.March, 29) +var d0329 = New(2015, time.March, 29) // n.b. clocks go forward (UK) +var d0330 = New(2015, time.March, 30) var d0401 = New(2015, time.April, 1) +var d0402 = New(2015, time.April, 2) var d0403 = New(2015, time.April, 3) var d0408 = New(2015, time.April, 8) var d0410 = New(2015, time.April, 10) var d0501 = New(2015, time.May, 1) var d1025 = New(2015, time.October, 25) +var london *time.Location + +func init() { + var err error + london, err = time.LoadLocation("Europe/London") + if err != nil { + panic(err) + } +} + func TestNewDateRangeOf(t *testing.T) { dr := NewDateRangeOf(t0327, time.Duration(7*24*60*60*1e9)) - isEq(t, dr.Start, d0327) - isEq(t, dr.End, d0403) + isEq(t, dr.mark, d0327) + isEq(t, dr.Days(), Days(7)) + isEq(t, dr.Start(), d0327) + isEq(t, dr.End(), d0402) + isEq(t, dr.Next(), d0403) } func TestNewDateRangeWithNormalise(t *testing.T) { r1 := NewDateRange(d0327, d0401) - isEq(t, r1.Start, d0327) - isEq(t, r1.End, d0401) + isEq(t, r1.Start(), d0327) + isEq(t, r1.End(), d0401) + isEq(t, r1.Next(), d0402) r2 := NewDateRange(d0401, d0327) - isEq(t, r2.Start, d0327) - isEq(t, r2.End, d0401) + isEq(t, r2.Start(), d0327) + isEq(t, r2.End(), d0401) + isEq(t, r2.Next(), d0402) } func TestOneDayRange(t *testing.T) { - dr := OneDayRange(d0327) - isEq(t, dr.Start, d0327) - isEq(t, dr.End, d0327) + drN0 := DateRange{d0327, -1} + isEq(t, drN0.Days(), Days(-1)) + isEq(t, drN0.Start(), d0327) + isEq(t, drN0.End(), d0327) + isEq(t, drN0.String(), "1 day on 2015-03-27") + + dr0 := DateRange{} + isEq(t, dr0.Days(), Days(0)) + isEq(t, dr0.String(), "0 days from 1970-01-01") + + dr1 := OneDayRange(Date{}) + isEq(t, dr1.Days(), Days(1)) + + dr2 := OneDayRange(d0327) + isEq(t, dr2.Start(), d0327) + isEq(t, dr2.End(), d0327) + isEq(t, dr2.Next(), d0328) + isEq(t, dr2.Days(), Days(1)) + isEq(t, dr2.String(), "1 day on 2015-03-27") } func TestNewYearOf(t *testing.T) { dr := NewYearOf(2015) - isEq(t, dr.Start, New(2015, time.January, 1)) - isEq(t, dr.End, New(2015, time.December, 31)) + isEq(t, dr.Days(), Days(365)) + isEq(t, dr.Start(), New(2015, time.January, 1)) + isEq(t, dr.End(), New(2015, time.December, 31)) + isEq(t, dr.Next(), New(2016, time.January, 1)) } func TestNewMonthOf(t *testing.T) { dr := NewMonthOf(2015, time.February) - isEq(t, dr.Start, New(2015, time.February, 1)) - isEq(t, dr.End, New(2015, time.February, 28)) + isEq(t, dr.Days(), Days(28)) + isEq(t, dr.Start(), New(2015, time.February, 1)) + isEq(t, dr.End(), New(2015, time.February, 28)) + isEq(t, dr.Next(), New(2015, time.March, 1)) } func TestShiftByPos(t *testing.T) { dr := NewDateRange(d0327, d0401).ShiftBy(7) - isEq(t, dr.Start, d0403) - isEq(t, dr.End, d0408) + isEq(t, dr.Days(), Days(6)) + isEq(t, dr.Start(), d0403) + isEq(t, dr.End(), d0408) } func TestShiftByNeg(t *testing.T) { dr := NewDateRange(d0403, d0408).ShiftBy(-7) - isEq(t, dr.Start, d0327) - isEq(t, dr.End, d0401) + isEq(t, dr.Days(), Days(6)) + isEq(t, dr.Start(), d0327) + isEq(t, dr.End(), d0401) } func TestExtendByPos(t *testing.T) { - dr := OneDayRange(d0327).ExtendBy(7) - isEq(t, dr.Start, d0327) - isEq(t, dr.End, d0403) - isEq(t, dr.String(), "2015-03-27 to 2015-04-03") + dr := OneDayRange(d0327).ExtendBy(6) + isEq(t, dr.Days(), Days(7)) + isEq(t, dr.Start(), d0327) + isEq(t, dr.End(), d0402) + isEq(t, dr.Next(), d0403) + isEq(t, dr.String(), "7 days from 2015-03-27 to 2015-04-02") } func TestExtendByNeg(t *testing.T) { - dr := OneDayRange(d0327).ExtendBy(-7) - isEq(t, dr.Start, d0320) - isEq(t, dr.End, d0327) - isEq(t, dr.String(), "2015-03-20 to 2015-03-27") + dr := OneDayRange(d0327).ExtendBy(-9) + isEq(t, dr.Days(), Days(-8)) + isEq(t, dr.Start(), d0320) + isEq(t, dr.End(), d0327) + isEq(t, dr.String(), "8 days from 2015-03-20 to 2015-03-27") } -func TestContains1(t *testing.T) { +func xTestContains1(t *testing.T) { old := time.Local time.Local = time.FixedZone("Test", 7200) dr := OneDayRange(d0326).ExtendBy(1) @@ -103,7 +143,7 @@ func TestContains1(t *testing.T) { time.Local = old } -func TestContains2(t *testing.T) { +func xTestContains2(t *testing.T) { old := time.Local time.Local = time.FixedZone("Test", 7200) dr := OneDayRange(d0326) @@ -113,7 +153,7 @@ func TestContains2(t *testing.T) { time.Local = old } -func TestContainsTimeUTC(t *testing.T) { +func xTestContainsTimeUTC(t *testing.T) { old := time.Local time.Local = time.FixedZone("Test", 7200) t0328e := time.Date(2015, 3, 28, 23, 59, 59, 999999999, time.UTC) @@ -129,59 +169,56 @@ func TestContainsTimeUTC(t *testing.T) { time.Local = old } -func TestMerge1(t *testing.T) { +func xTestMerge1(t *testing.T) { dr1 := OneDayRange(d0327).ExtendBy(1) dr2 := OneDayRange(d0327).ExtendBy(7) m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) - isEq(t, m1.Start, d0327) - isEq(t, m1.End, d0403) + isEq(t, m1.Start(), d0327) + isEq(t, m1.End(), d0403) isEq(t, m1, m2) } -func TestMerge2(t *testing.T) { +func xTestMerge2(t *testing.T) { dr1 := OneDayRange(d0327).ExtendBy(1).ShiftBy(1) dr2 := OneDayRange(d0327).ExtendBy(7) m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) - isEq(t, m1.Start, d0327) - isEq(t, m1.End, d0403) + isEq(t, m1.Start(), d0327) + isEq(t, m1.End(), d0403) isEq(t, m1, m2) } -func TestMergeOverlapping(t *testing.T) { +func xTestMergeOverlapping(t *testing.T) { dr1 := OneDayRange(d0320).ExtendBy(12) dr2 := OneDayRange(d0401).ExtendBy(7) m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) - isEq(t, m1.Start, d0320) - isEq(t, m1.End, d0408) + isEq(t, m1.Start(), d0320) + isEq(t, m1.End(), d0408) isEq(t, m1, m2) } -func TestMergeNonOverlapping(t *testing.T) { +func xTestMergeNonOverlapping(t *testing.T) { dr1 := OneDayRange(d0320).ExtendBy(2) dr2 := OneDayRange(d0401).ExtendBy(7) m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) - isEq(t, m1.Start, d0320) - isEq(t, m1.End, d0408) + isEq(t, m1.Start(), d0320) + isEq(t, m1.End(), d0408) isEq(t, m1, m2) } -func TestDurationNormalUTC(t *testing.T) { +func xTestDurationNormalUTC(t *testing.T) { dr := OneDayRange(d0329) isEq(t, dr.Duration(), time.Hour * 24) } -func TestDurationInZoneWithDaylightSaving(t *testing.T) { - london, err := time.LoadLocation("Europe/London") - if err != nil { - panic(err) - } +func xTestDurationInZoneWithDaylightSaving(t *testing.T) { isEq(t, OneDayRange(d0328).DurationIn(london), time.Hour * 24) isEq(t, OneDayRange(d0329).DurationIn(london), time.Hour * 23) isEq(t, OneDayRange(d1025).DurationIn(london), time.Hour * 25) + isEq(t, NewDateRange(d0328, d0330).DurationIn(london), time.Hour * 71) } func isEq(t *testing.T, a, b interface{}, msg ...interface{}) { diff --git a/timespan/timespan.go b/timespan/timespan.go new file mode 100644 index 00000000..274e4761 --- /dev/null +++ b/timespan/timespan.go @@ -0,0 +1,129 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package timespan + +import ( + "time" + "fmt" + "github.com/rickb777/date" +) + +const TimestampFormat = "2006-01-02 15:04:05" +//const ISOFormat = "2006-01-02T15:04:05" + +type TimeSpan struct { + Mark time.Time + Duration time.Duration +} + +// ZeroTimeSpan creates a new zero-duration time span from a time. +func ZeroTimeSpan(start time.Time) TimeSpan { + return TimeSpan{start, 0} +} + +// NewTimeSpan creates a new time span from two times. The start and end can be in either order; +// the result will be normalised. +func NewTimeSpan(t1, t2 time.Time) TimeSpan { + if t2.Before(t1) { + return TimeSpan{t2, t1.Sub(t2)} + } + return TimeSpan{t1, t2.Sub(t1)} +} + +// End gets the end time of the time span. +func (ts TimeSpan) Start() time.Time { + if ts.Duration < 0 { + return ts.Mark.Add(ts.Duration) + } + return ts.Mark +} + +// End gets the end time of the time span. +func (ts TimeSpan) End() time.Time { + if ts.Duration < 0 { + return ts.Mark + } + return ts.Mark.Add(ts.Duration) +} + +// Normalise ensures that the mark time is at the start time and the duration is positive. +// The normalised timespan is returned. +func (ts TimeSpan) Normalise() TimeSpan { + if ts.Duration < 0 { + return TimeSpan{ts.Mark.Add(ts.Duration), -ts.Duration} + } + return ts +} + +// ShiftBy moves the date range by moving both the start and end times similarly. +// A negative parameter is allowed. +func (ts TimeSpan) ShiftBy(d time.Duration) TimeSpan { + return TimeSpan{ts.Mark.Add(d), ts.Duration} +} + +// ExtendBy lengthens the time span by a specified amount. The parameter may be negative, +// in which case it is possible that the end of the time span will appear to be before the +// start. However, the result is normalised so that the resulting start is the lesser value +// and the duration is always non-negative. +func (ts TimeSpan) ExtendBy(d time.Duration) TimeSpan { + return TimeSpan{ts.Mark, ts.Duration + d}.Normalise() +} + +// ExtendWithoutWrapping lengthens the time span by a specified amount. The parameter may be +// negative, but if its magnitude is large than the time span's duration, it will be truncated +// so that the result has zero duration in that case. The start time is never altered. +func (ts TimeSpan) ExtendWithoutWrapping(d time.Duration) TimeSpan { + if d < 0 && -d > ts.Duration { + return TimeSpan{ts.Mark, 0} + } + return TimeSpan{ts.Mark, ts.Duration + d} +} + +func (ts TimeSpan) String() string { + return fmt.Sprintf("%s from %s to %s", ts.Duration, ts.Mark.Format(TimestampFormat), ts.End().Format(TimestampFormat)) +} + +// In returns a TimeSpan adjusted from its current location to a new location. Because +// location is considered to be a presentational attribute, the actual time itself is not +// altered by this function. This matches the behaviour of time.Time.In(loc). +func (ts TimeSpan) In(loc *time.Location) TimeSpan { + t := ts.Mark.In(loc) + return TimeSpan{t, ts.Duration} +} + +// DateRangeIn obtains the date range corresponding to the time span in a specified location. +// The result is normalised. +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) +} + +// Contains tests whether a given moment of time is enclosed within the time span. The +// start time is inclusive; the end time is exclusive. +// If t has a different locality to the time-span, it is adjusted accordingly. +func (ts TimeSpan) Contains(t time.Time) bool { + tl := t.In(ts.Mark.Location()) + return ts.Mark.Equal(tl) || ts.Mark.Before(tl) && ts.End().After(tl) +} + +// Merge combines two time spans by calculating a time span that just encompasses them both. +// As a special case, if one span is entirely contained within the other span, the larger of +// the two is returned. Otherwise, the result is the start of the earlier one to the end of the +// later one, even if the two spans don't overlap. +func (ts TimeSpan) Merge(other TimeSpan) TimeSpan { + if ts.Mark.After(other.Mark) { + // swap the ranges to simplify the logic + return other.Merge(ts) + + } else if ts.End().After(other.End()) { + // other is a proper subrange of ts + return ts + + } else { + return NewTimeSpan(ts.Mark, other.End()) + } +} diff --git a/timespan/timespan_test.go b/timespan/timespan_test.go new file mode 100644 index 00000000..cb8d640f --- /dev/null +++ b/timespan/timespan_test.go @@ -0,0 +1,192 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package timespan + +import ( + "testing" + "time" +) + +const zero time.Duration = 0 + +var t0327 = time.Date(2015, 3, 27, 0, 0, 0, 0, time.UTC) +var t0328 = time.Date(2015, 3, 28, 0, 0, 0, 0, time.UTC) +var t0329 = time.Date(2015, 3, 29, 0, 0, 0, 0, time.UTC) // n.b. clocks go forward (UK) +var t0330 = time.Date(2015, 3, 30, 0, 0, 0, 0, time.UTC) + +func TestZeroTimeSpan(t *testing.T) { + ts := ZeroTimeSpan(t0327) + isEq(t, ts.Mark, t0327) + isEq(t, ts.Duration, zero) + isEq(t, ts.End(), t0327) +} + +func TestNewTimeSpan(t *testing.T) { + ts1 := NewTimeSpan(t0327, t0327) + isEq(t, ts1.Mark, t0327) + isEq(t, ts1.Duration, zero) + isEq(t, ts1.End(), t0327) + + ts2 := NewTimeSpan(t0327, t0328) + isEq(t, ts2.Mark, t0327) + isEq(t, ts2.Duration, time.Hour * 24) + isEq(t, ts2.End(), t0328) + + ts3 := NewTimeSpan(t0329, t0327) + isEq(t, ts3.Mark, t0327) + isEq(t, ts3.Duration, time.Hour * 48) + isEq(t, ts3.End(), t0329) +} + +func TestTSEnd(t *testing.T) { + ts1 := TimeSpan{t0328, time.Hour * 24} + isEq(t, ts1.Start(), t0328) + isEq(t, ts1.End(), t0329) + + // not normalised, deliberately + ts2 := TimeSpan{t0328, -time.Hour * 24} + isEq(t, ts2.Start(), t0327) + isEq(t, ts2.End(), t0328) +} + +func TestTSShiftBy(t *testing.T) { + ts1 := NewTimeSpan(t0327, t0328).ShiftBy(time.Hour * 24) + isEq(t, ts1.Mark, t0328) + isEq(t, ts1.Duration, time.Hour * 24) + isEq(t, ts1.End(), t0329) + + ts2 := NewTimeSpan(t0328, t0329).ShiftBy(-time.Hour * 24) + isEq(t, ts2.Mark, t0327) + isEq(t, ts2.Duration, time.Hour * 24) + isEq(t, ts2.End(), t0328) +} + +func TestTSExtendBy(t *testing.T) { + ts1 := NewTimeSpan(t0327, t0328).ExtendBy(time.Hour * 24) + isEq(t, ts1.Mark, t0327) + isEq(t, ts1.Duration, time.Hour * 48) + isEq(t, ts1.End(), t0329) + + ts2 := NewTimeSpan(t0328, t0329).ExtendBy(-time.Hour * 48) + isEq(t, ts2.Mark, t0327) + isEq(t, ts2.Duration, time.Hour * 24) + isEq(t, ts2.End(), t0328) +} + +func TestTSExtendWithoutWrapping(t *testing.T) { + ts1 := NewTimeSpan(t0327, t0328).ExtendWithoutWrapping(time.Hour * 24) + isEq(t, ts1.Mark, t0327) + isEq(t, ts1.Duration, time.Hour * 48) + isEq(t, ts1.End(), t0329) + + ts2 := NewTimeSpan(t0328, t0329).ExtendWithoutWrapping(-time.Hour * 48) + isEq(t, ts2.Mark, t0328) + isEq(t, ts2.Duration, zero) + isEq(t, ts2.End(), t0328) +} + +func TestTSString(t *testing.T) { + s := NewTimeSpan(t0327, t0328).String() + isEq(t, s, "24h0m0s from 2015-03-27 00:00:00 to 2015-03-28 00:00:00") +} + +func TestTSContains(t *testing.T) { + ts := NewTimeSpan(t0327, t0329) + isEq(t, ts.Contains(t0327.Add(minusOneNano)), false) + isEq(t, ts.Contains(t0327), true) + isEq(t, ts.Contains(t0328), true) + isEq(t, ts.Contains(t0329.Add(minusOneNano)), true) + isEq(t, ts.Contains(t0329), false) +} + +func TestTSIn(t *testing.T) { + ts := ZeroTimeSpan(t0327).In(time.FixedZone("Test", 7200)) + isEq(t, ts.Mark.Equal(t0327), true) + isEq(t, ts.Duration, zero) + isEq(t, ts.End().Equal(t0327), true) +} + +func TestTSMerge1(t *testing.T) { + ts1 := NewTimeSpan(t0327, t0328) + ts2 := NewTimeSpan(t0327, t0330) + m1 := ts1.Merge(ts2) + m2 := ts2.Merge(ts1) + isEq(t, m1.Mark, t0327) + isEq(t, m1.End(), t0330) + isEq(t, m1, m2) +} + +func TestTSMerge2(t *testing.T) { + ts1 := NewTimeSpan(t0328, t0329) + ts2 := NewTimeSpan(t0327, t0330) + m1 := ts1.Merge(ts2) + m2 := ts2.Merge(ts1) + isEq(t, m1.Mark, t0327) + isEq(t, m1.End(), t0330) + isEq(t, m1, m2) +} + +func TestTSMerge3(t *testing.T) { + ts1 := NewTimeSpan(t0329, t0330) + ts2 := NewTimeSpan(t0327, t0330) + m1 := ts1.Merge(ts2) + m2 := ts2.Merge(ts1) + isEq(t, m1.Mark, t0327) + isEq(t, m1.End(), t0330) + isEq(t, m1, m2) +} + +func TestTSMergeOverlapping(t *testing.T) { + ts1 := NewTimeSpan(t0327, t0329) + ts2 := NewTimeSpan(t0328, t0330) + m1 := ts1.Merge(ts2) + m2 := ts2.Merge(ts1) + isEq(t, m1.Mark, t0327) + isEq(t, m1.End(), t0330) + isEq(t, m1, m2) +} + +func xTestTSMergeNonOverlapping(t *testing.T) { + ts1 := NewTimeSpan(t0327, t0328) + ts2 := NewTimeSpan(t0329, t0330) + m1 := ts1.Merge(ts2) + m2 := ts2.Merge(ts1) + isEq(t, m1.Mark, t0327) + isEq(t, m1.End(), t0330) + isEq(t, m1, m2) +} + +func xTestConversion1(t *testing.T) { + ts1 := ZeroTimeSpan(t0327) + dr := ts1.DateRangeIn(time.UTC) + ts2 := dr.TimeSpanIn(time.UTC) + isEq(t, dr.Start, d0327) + isEq(t, dr.End, d0327) + isEq(t, ts1, ts2) + isEq(t, ts1.Duration, zero) +} + +func xTestConversion2(t *testing.T) { + ts1 := NewTimeSpan(t0327, t0328) + dr := ts1.DateRangeIn(time.UTC) +// ts2 := dr.TimeSpanIn(time.UTC) + isEq(t, dr.Start, d0327) + isEq(t, dr.End, d0328) +// isEq(t, ts1, ts2) + isEq(t, ts1.Duration, time.Hour * 24) +} + +func xTestConversion3(t *testing.T) { + dr1 := NewDateRange(d0327, d0330) // weekend of clocks changing + ts1 := dr1.TimeSpanIn(london) + dr2 := ts1.DateRangeIn(london) +// ts2 := dr2.TimeSpanIn(london) + isEq(t, dr1.Start, d0327) + isEq(t, dr1.End, d0330) + isEq(t, dr1, dr2) +// isEq(t, ts1, ts2) + isEq(t, ts1.Duration, time.Hour * 71) +} + From 821c807783d4a063e9eb57fbbe8df6c160284b2f Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 27 Nov 2015 22:37:41 +0000 Subject: [PATCH 012/165] TimeSpan fields are now not exported to allow for future change if required. --- timespan/daterange_test.go | 8 ++--- timespan/timespan.go | 72 +++++++++++++++++++++----------------- timespan/timespan_test.go | 60 +++++++++++++++---------------- 3 files changed, 74 insertions(+), 66 deletions(-) diff --git a/timespan/daterange_test.go b/timespan/daterange_test.go index 615415e6..38db90e7 100644 --- a/timespan/daterange_test.go +++ b/timespan/daterange_test.go @@ -28,14 +28,14 @@ var d0410 = New(2015, time.April, 10) var d0501 = New(2015, time.May, 1) var d1025 = New(2015, time.October, 25) -var london *time.Location +var london *time.Location = mustLoadLocation("Europe/London") -func init() { - var err error - london, err = time.LoadLocation("Europe/London") +func mustLoadLocation(name string) *time.Location { + loc, err := time.LoadLocation("Europe/London") if err != nil { panic(err) } + return loc } func TestNewDateRangeOf(t *testing.T) { diff --git a/timespan/timespan.go b/timespan/timespan.go index 274e4761..e3a851f0 100644 --- a/timespan/timespan.go +++ b/timespan/timespan.go @@ -13,18 +13,20 @@ import ( const TimestampFormat = "2006-01-02 15:04:05" //const ISOFormat = "2006-01-02T15:04:05" +// TimeSpan holds a span of time between two instants with a 1 nanosecond resolution. +// It is implemented using a time.Duration, therefore is limited to a maximum span of 290 years. type TimeSpan struct { - Mark time.Time - Duration time.Duration + mark time.Time + duration time.Duration } -// ZeroTimeSpan creates a new zero-duration time span from a time. +// ZeroTimeSpan creates a new zero-duration time span at a specified time. func ZeroTimeSpan(start time.Time) TimeSpan { return TimeSpan{start, 0} } -// NewTimeSpan creates a new time span from two times. The start and end can be in either order; -// the result will be normalised. +// NewTimeSpan creates a new time span from two times. The start and end can be in either +// order; the result will be normalised. func NewTimeSpan(t1, t2 time.Time) TimeSpan { if t2.Before(t1) { return TimeSpan{t2, t1.Sub(t2)} @@ -32,72 +34,78 @@ func NewTimeSpan(t1, t2 time.Time) TimeSpan { return TimeSpan{t1, t2.Sub(t1)} } -// End gets the end time of the time span. +// Start gets the end time of the time span. func (ts TimeSpan) Start() time.Time { - if ts.Duration < 0 { - return ts.Mark.Add(ts.Duration) + if ts.duration < 0 { + return ts.mark.Add(ts.duration) } - return ts.Mark + return ts.mark } -// End gets the end time of the time span. +// End gets the end time of the time span. Strictly, this is one nanosecond after the +// range of time included in the time span. func (ts TimeSpan) End() time.Time { - if ts.Duration < 0 { - return ts.Mark + if ts.duration < 0 { + return ts.mark } - return ts.Mark.Add(ts.Duration) + return ts.mark.Add(ts.duration) +} + +// Duration gets the duration of the time span. +func (ts TimeSpan) Duration() time.Duration { + return ts.duration } // Normalise ensures that the mark time is at the start time and the duration is positive. -// The normalised timespan is returned. +// The normalised time span is returned. func (ts TimeSpan) Normalise() TimeSpan { - if ts.Duration < 0 { - return TimeSpan{ts.Mark.Add(ts.Duration), -ts.Duration} + if ts.duration < 0 { + return TimeSpan{ts.mark.Add(ts.duration), -ts.duration} } return ts } -// ShiftBy moves the date range by moving both the start and end times similarly. +// ShiftBy moves the time span by moving both the start and end times similarly. // A negative parameter is allowed. func (ts TimeSpan) ShiftBy(d time.Duration) TimeSpan { - return TimeSpan{ts.Mark.Add(d), ts.Duration} + return TimeSpan{ts.mark.Add(d), ts.duration} } // ExtendBy lengthens the time span by a specified amount. The parameter may be negative, // in which case it is possible that the end of the time span will appear to be before the -// start. However, the result is normalised so that the resulting start is the lesser value -// and the duration is always non-negative. +// start. However, the result is normalised so that the resulting start is the lesser value. func (ts TimeSpan) ExtendBy(d time.Duration) TimeSpan { - return TimeSpan{ts.Mark, ts.Duration + d}.Normalise() + return TimeSpan{ts.mark, ts.duration + d}.Normalise() } // ExtendWithoutWrapping lengthens the time span by a specified amount. The parameter may be // negative, but if its magnitude is large than the time span's duration, it will be truncated // so that the result has zero duration in that case. The start time is never altered. func (ts TimeSpan) ExtendWithoutWrapping(d time.Duration) TimeSpan { - if d < 0 && -d > ts.Duration { - return TimeSpan{ts.Mark, 0} + tsn := ts.Normalise() + if d < 0 && -d > tsn.duration { + return TimeSpan{tsn.mark, 0} } - return TimeSpan{ts.Mark, ts.Duration + d} + return TimeSpan{tsn.mark, tsn.duration + d} } func (ts TimeSpan) String() string { - return fmt.Sprintf("%s from %s to %s", ts.Duration, ts.Mark.Format(TimestampFormat), ts.End().Format(TimestampFormat)) + return fmt.Sprintf("%s from %s to %s", ts.duration, ts.mark.Format(TimestampFormat), ts.End().Format(TimestampFormat)) } // In returns a TimeSpan adjusted from its current location to a new location. Because // location is considered to be a presentational attribute, the actual time itself is not // altered by this function. This matches the behaviour of time.Time.In(loc). func (ts TimeSpan) In(loc *time.Location) TimeSpan { - t := ts.Mark.In(loc) - return TimeSpan{t, ts.Duration} + t := ts.mark.In(loc) + return TimeSpan{t, ts.duration} } // DateRangeIn obtains the date range corresponding to the time span in a specified location. // The result is normalised. func (ts TimeSpan) DateRangeIn(loc *time.Location) DateRange { no := ts.Normalise() - startDate := date.NewAt(no.Mark.In(loc)) + startDate := date.NewAt(no.mark.In(loc)) endDate := date.NewAt(no.End().In(loc)) return NewDateRange(startDate, endDate) } @@ -106,8 +114,8 @@ func (ts TimeSpan) DateRangeIn(loc *time.Location) DateRange { // start time is inclusive; the end time is exclusive. // If t has a different locality to the time-span, it is adjusted accordingly. func (ts TimeSpan) Contains(t time.Time) bool { - tl := t.In(ts.Mark.Location()) - return ts.Mark.Equal(tl) || ts.Mark.Before(tl) && ts.End().After(tl) + tl := t.In(ts.mark.Location()) + return ts.mark.Equal(tl) || ts.mark.Before(tl) && ts.End().After(tl) } // Merge combines two time spans by calculating a time span that just encompasses them both. @@ -115,7 +123,7 @@ func (ts TimeSpan) Contains(t time.Time) bool { // the two is returned. Otherwise, the result is the start of the earlier one to the end of the // later one, even if the two spans don't overlap. func (ts TimeSpan) Merge(other TimeSpan) TimeSpan { - if ts.Mark.After(other.Mark) { + if ts.mark.After(other.mark) { // swap the ranges to simplify the logic return other.Merge(ts) @@ -124,6 +132,6 @@ func (ts TimeSpan) Merge(other TimeSpan) TimeSpan { return ts } else { - return NewTimeSpan(ts.Mark, other.End()) + return NewTimeSpan(ts.mark, other.End()) } } diff --git a/timespan/timespan_test.go b/timespan/timespan_test.go index cb8d640f..f7222788 100644 --- a/timespan/timespan_test.go +++ b/timespan/timespan_test.go @@ -18,25 +18,25 @@ var t0330 = time.Date(2015, 3, 30, 0, 0, 0, 0, time.UTC) func TestZeroTimeSpan(t *testing.T) { ts := ZeroTimeSpan(t0327) - isEq(t, ts.Mark, t0327) - isEq(t, ts.Duration, zero) + isEq(t, ts.mark, t0327) + isEq(t, ts.Duration(), zero) isEq(t, ts.End(), t0327) } func TestNewTimeSpan(t *testing.T) { ts1 := NewTimeSpan(t0327, t0327) - isEq(t, ts1.Mark, t0327) - isEq(t, ts1.Duration, zero) + isEq(t, ts1.mark, t0327) + isEq(t, ts1.Duration(), zero) isEq(t, ts1.End(), t0327) ts2 := NewTimeSpan(t0327, t0328) - isEq(t, ts2.Mark, t0327) - isEq(t, ts2.Duration, time.Hour * 24) + isEq(t, ts2.mark, t0327) + isEq(t, ts2.Duration(), time.Hour * 24) isEq(t, ts2.End(), t0328) ts3 := NewTimeSpan(t0329, t0327) - isEq(t, ts3.Mark, t0327) - isEq(t, ts3.Duration, time.Hour * 48) + isEq(t, ts3.mark, t0327) + isEq(t, ts3.Duration(), time.Hour * 48) isEq(t, ts3.End(), t0329) } @@ -53,37 +53,37 @@ func TestTSEnd(t *testing.T) { func TestTSShiftBy(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328).ShiftBy(time.Hour * 24) - isEq(t, ts1.Mark, t0328) - isEq(t, ts1.Duration, time.Hour * 24) + isEq(t, ts1.mark, t0328) + isEq(t, ts1.Duration(), time.Hour * 24) isEq(t, ts1.End(), t0329) ts2 := NewTimeSpan(t0328, t0329).ShiftBy(-time.Hour * 24) - isEq(t, ts2.Mark, t0327) - isEq(t, ts2.Duration, time.Hour * 24) + isEq(t, ts2.mark, t0327) + isEq(t, ts2.Duration(), time.Hour * 24) isEq(t, ts2.End(), t0328) } func TestTSExtendBy(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328).ExtendBy(time.Hour * 24) - isEq(t, ts1.Mark, t0327) - isEq(t, ts1.Duration, time.Hour * 48) + isEq(t, ts1.mark, t0327) + isEq(t, ts1.Duration(), time.Hour * 48) isEq(t, ts1.End(), t0329) ts2 := NewTimeSpan(t0328, t0329).ExtendBy(-time.Hour * 48) - isEq(t, ts2.Mark, t0327) - isEq(t, ts2.Duration, time.Hour * 24) + isEq(t, ts2.mark, t0327) + isEq(t, ts2.Duration(), time.Hour * 24) isEq(t, ts2.End(), t0328) } func TestTSExtendWithoutWrapping(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328).ExtendWithoutWrapping(time.Hour * 24) - isEq(t, ts1.Mark, t0327) - isEq(t, ts1.Duration, time.Hour * 48) + isEq(t, ts1.mark, t0327) + isEq(t, ts1.Duration(), time.Hour * 48) isEq(t, ts1.End(), t0329) ts2 := NewTimeSpan(t0328, t0329).ExtendWithoutWrapping(-time.Hour * 48) - isEq(t, ts2.Mark, t0328) - isEq(t, ts2.Duration, zero) + isEq(t, ts2.mark, t0328) + isEq(t, ts2.Duration(), zero) isEq(t, ts2.End(), t0328) } @@ -103,8 +103,8 @@ func TestTSContains(t *testing.T) { func TestTSIn(t *testing.T) { ts := ZeroTimeSpan(t0327).In(time.FixedZone("Test", 7200)) - isEq(t, ts.Mark.Equal(t0327), true) - isEq(t, ts.Duration, zero) + isEq(t, ts.mark.Equal(t0327), true) + isEq(t, ts.Duration(), zero) isEq(t, ts.End().Equal(t0327), true) } @@ -113,7 +113,7 @@ func TestTSMerge1(t *testing.T) { ts2 := NewTimeSpan(t0327, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) - isEq(t, m1.Mark, t0327) + isEq(t, m1.mark, t0327) isEq(t, m1.End(), t0330) isEq(t, m1, m2) } @@ -123,7 +123,7 @@ func TestTSMerge2(t *testing.T) { ts2 := NewTimeSpan(t0327, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) - isEq(t, m1.Mark, t0327) + isEq(t, m1.mark, t0327) isEq(t, m1.End(), t0330) isEq(t, m1, m2) } @@ -133,7 +133,7 @@ func TestTSMerge3(t *testing.T) { ts2 := NewTimeSpan(t0327, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) - isEq(t, m1.Mark, t0327) + isEq(t, m1.mark, t0327) isEq(t, m1.End(), t0330) isEq(t, m1, m2) } @@ -143,7 +143,7 @@ func TestTSMergeOverlapping(t *testing.T) { ts2 := NewTimeSpan(t0328, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) - isEq(t, m1.Mark, t0327) + isEq(t, m1.mark, t0327) isEq(t, m1.End(), t0330) isEq(t, m1, m2) } @@ -153,7 +153,7 @@ func xTestTSMergeNonOverlapping(t *testing.T) { ts2 := NewTimeSpan(t0329, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) - isEq(t, m1.Mark, t0327) + isEq(t, m1.mark, t0327) isEq(t, m1.End(), t0330) isEq(t, m1, m2) } @@ -165,7 +165,7 @@ func xTestConversion1(t *testing.T) { isEq(t, dr.Start, d0327) isEq(t, dr.End, d0327) isEq(t, ts1, ts2) - isEq(t, ts1.Duration, zero) + isEq(t, ts1.Duration(), zero) } func xTestConversion2(t *testing.T) { @@ -175,7 +175,7 @@ func xTestConversion2(t *testing.T) { isEq(t, dr.Start, d0327) isEq(t, dr.End, d0328) // isEq(t, ts1, ts2) - isEq(t, ts1.Duration, time.Hour * 24) + isEq(t, ts1.Duration(), time.Hour * 24) } func xTestConversion3(t *testing.T) { @@ -187,6 +187,6 @@ func xTestConversion3(t *testing.T) { isEq(t, dr1.End, d0330) isEq(t, dr1, dr2) // isEq(t, ts1, ts2) - isEq(t, ts1.Duration, time.Hour * 71) + isEq(t, ts1.Duration(), time.Hour * 71) } From c6bf3e76325d3c641b5342151ceb3329364469a7 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 27 Nov 2015 22:53:50 +0000 Subject: [PATCH 013/165] Date internal type uses Days now. --- date.go | 6 +++--- marshal.go | 2 +- rep.go | 8 ++++---- rep_test.go | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/date.go b/date.go index 7927c3bf..b0d2e447 100644 --- a/date.go +++ b/date.go @@ -68,7 +68,7 @@ import ( // type Date struct { // day gives the number of days elapsed since date zero. - day int32 + day Days } // Days describes a period of time measured in whole days. Negative values @@ -178,7 +178,7 @@ func (d Date) Weekday() time.Weekday { // Date zero, January 1, 1970, fell on a Thursday wdayZero := time.Thursday // Taking into account potential for overflow and negative offset - return time.Weekday((int32(wdayZero) + d.day % 7 + 7) % 7) + return time.Weekday((Days(wdayZero) + d.day % 7 + 7) % 7) } // ISOWeek returns the ISO 8601 year and week number in which d occurs. @@ -212,7 +212,7 @@ func (d Date) After(u Date) bool { // Add returns the date d plus the given number of days. The parameter may be negative. func (d Date) Add(days Days) Date { - return Date{d.day + int32(days)} + return Date{d.day + days} } // AddDate returns the date corresponding to adding the given number of years, diff --git a/marshal.go b/marshal.go index 220d0ba2..4d410328 100644 --- a/marshal.go +++ b/marshal.go @@ -29,7 +29,7 @@ func (d *Date) UnmarshalBinary(data []byte) error { return errors.New("Date.UnmarshalBinary: invalid length") } - d.day = int32(data[3]) | int32(data[2])<<8 | int32(data[1])<<16 | int32(data[0])<<24 + d.day = Days(data[3]) | Days(data[2])<<8 | Days(data[1])<<16 | Days(data[0])<<24 return nil } diff --git a/rep.go b/rep.go index 37adce88..87e308f5 100644 --- a/rep.go +++ b/rep.go @@ -10,7 +10,7 @@ const secondsPerDay = 60 * 60 * 24 // encode returns the number of days elapsed from date zero to the date // corresponding to the given Time value. -func encode(t time.Time) int32 { +func encode(t time.Time) Days { // Compute the number of seconds elapsed since January 1, 1970 00:00:00 // in the location specified by t and not necessarily UTC. // A Time value is represented internally as an offset from a UTC base @@ -22,14 +22,14 @@ func encode(t time.Time) int32 { // Unfortunately operator / rounds towards 0, so negative values // must be handled differently if secs >= 0 { - return int32(secs / secondsPerDay) + return Days(secs / secondsPerDay) } - return -int32((secondsPerDay - 1 - secs) / secondsPerDay) + return -Days((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 int32) time.Time { +func decode(d Days) time.Time { secs := int64(d) * secondsPerDay return time.Unix(secs, 0).UTC() } diff --git a/rep_test.go b/rep_test.go index ee3de0ed..353e4d2b 100644 --- a/rep_test.go +++ b/rep_test.go @@ -17,11 +17,11 @@ func TestEncode(t *testing.T) { tBase := time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) for i, c := range cases { d := encode(tBase.AddDate(0, 0, c)) - if d != int32(c) { + if d != Days(c) { t.Errorf("Encode(%v) == %v, want %v", i, d, c) } d = encode(tBase.AddDate(0, 0, -c)) - if d != int32(-c) { + if d != Days(-c) { t.Errorf("Encode(%v) == %v, want %v", i, d, c) } } @@ -73,14 +73,14 @@ func TestEncodeDecode(t *testing.T) { func TestDecodeEncode(t *testing.T) { for i := 0; i < 1000; i++ { - c := rand.Int31() + c := Days(rand.Int31()) d := encode(decode(c)) if d != c { t.Errorf("DecodeEncode(%v) == %v, want %v", i, d, c) } } for i := 0; i < 1000; i++ { - c := -rand.Int31() + c := -Days(rand.Int31()) d := encode(decode(c)) if d != c { t.Errorf("DecodeEncode(%v) == %v, want %v", i, d, c) From 6198946221dac50368df9e3be2ea6a6f6435491f Mon Sep 17 00:00:00 2001 From: Rick Date: Sat, 28 Nov 2015 18:28:04 +0000 Subject: [PATCH 014/165] Date implementation is now a type derived from a primitive type (actually int32) instead of a struct. This makes iterating over a date range easier. --- date.go | 61 ++++++++++++++++++-------------------- date_test.go | 4 +-- format.go | 10 +++---- marshal.go | 23 +++++++------- rep.go | 8 ++--- rep_test.go | 8 ++--- timespan/daterange.go | 24 +++++++-------- timespan/daterange_test.go | 24 +++++++-------- 8 files changed, 78 insertions(+), 84 deletions(-) diff --git a/date.go b/date.go index b0d2e447..4013a1e2 100644 --- a/date.go +++ b/date.go @@ -66,14 +66,11 @@ import ( // As this date is unlikely to come up in practice, the IsZero method gives // a simple way of detecting a date that has not been initialized explicitly. // -type Date struct { - // day gives the number of days elapsed since date zero. - day Days -} +type Date int32 -// Days describes a period of time measured in whole days. Negative values +// PeriodOfDays describes a period of time measured in whole days. Negative values // indicate days earlier than some mark. -type Days int32 +type PeriodOfDays int32 // New returns the Date value corresponding to the given year, month, and day. // @@ -81,49 +78,49 @@ type Days int32 // during the conversion. func New(year int, month time.Month, day int) Date { t := time.Date(year, month, day, 12, 0, 0, 0, time.UTC) - return Date{encode(t)} + return encode(t) } // NewAt returns the Date value corresponding to the given time. // Note that the date is computed relative to the time zone specified by // the given Time value. func NewAt(t time.Time) Date { - return Date{encode(t)} + return encode(t) } // Today returns today's date according to the current local time. func Today() Date { t := time.Now() - return Date{encode(t)} + return encode(t) } // TodayUTC returns today's date according to the current UTC time. func TodayUTC() Date { t := time.Now().UTC() - return Date{encode(t)} + return encode(t) } // TodayIn returns today's date according to the current time relative to // the specified location. func TodayIn(loc *time.Location) Date { t := time.Now().In(loc) - return Date{encode(t)} + return encode(t) } // Min returns the smallest representable date. func Min() Date { - return Date{math.MinInt32} + return Date(math.MinInt32) } // Max returns the largest representable date. func Max() Date { - return Date{math.MaxInt32} + return Date(math.MaxInt32) } // UTC returns a Time value corresponding to midnight on the given date, // UTC time. Note that midnight is the beginning of the day rather than the end. func (d Date) UTC() time.Time { - return decode(d.day) + return decode(d) } // Local returns a Time value corresponding to midnight on the given date, @@ -136,40 +133,40 @@ func (d Date) Local() time.Time { // relative to the specified time zone. Note that midnight is the beginning // of the day rather than the end. func (d Date) In(loc *time.Location) time.Time { - t := decode(d.day).In(loc) + t := decode(d).In(loc) _, offset := t.Zone() return t.Add(time.Duration(-offset) * time.Second) } // Date returns the year, month, and day of d. func (d Date) Date() (year int, month time.Month, day int) { - t := decode(d.day) + t := decode(d) return t.Date() } // Day returns the day of the month specified by d. // The first day of the month is 1. func (d Date) Day() int { - t := decode(d.day) + t := decode(d) return t.Day() } // Month returns the month of the year specified by d. func (d Date) Month() time.Month { - t := decode(d.day) + t := decode(d) return t.Month() } // Year returns the year specified by d. func (d Date) Year() int { - t := decode(d.day) + t := decode(d) return t.Year() } // YearDay returns the day of the year specified by d, in the range [1,365] for // non-leap years, and [1,366] in leap years. func (d Date) YearDay() int { - t := decode(d.day) + t := decode(d) return t.YearDay() } @@ -178,7 +175,7 @@ func (d Date) Weekday() time.Weekday { // Date zero, January 1, 1970, fell on a Thursday wdayZero := time.Thursday // Taking into account potential for overflow and negative offset - return time.Weekday((Days(wdayZero) + d.day % 7 + 7) % 7) + return time.Weekday((Date(wdayZero) + d % 7 + 7) % 7) } // ISOWeek returns the ISO 8601 year and week number in which d occurs. @@ -186,45 +183,45 @@ func (d Date) Weekday() time.Weekday { // week 52 or 53 of year n-1, and Dec 29 to Dec 31 might belong to week 1 // of year n+1. func (d Date) ISOWeek() (year, week int) { - t := decode(d.day) + t := decode(d) return t.ISOWeek() } // IsZero reports whether t represents the zero date. func (d Date) IsZero() bool { - return d.day == 0 + return d == 0 } // Equal reports whether d and u represent the same date. func (d Date) Equal(u Date) bool { - return d.day == u.day + return d == u } // Before reports whether the date d is before u. func (d Date) Before(u Date) bool { - return d.day < u.day + return d < u } // After reports whether the date d is after u. func (d Date) After(u Date) bool { - return d.day > u.day + return d > u } // Add returns the date d plus the given number of days. The parameter may be negative. -func (d Date) Add(days Days) Date { - return Date{d.day + days} +func (d Date) Add(days PeriodOfDays) Date { + return d + Date(days) } // AddDate returns the date corresponding to adding the given number of years, // months, and days to d. For example, AddData(-1, 2, 3) applied to // January 1, 2011 returns March 4, 2010. func (d Date) AddDate(years, months, days int) Date { - t := decode(d.day) + t := decode(d) t = t.AddDate(years, months, days) - return Date{encode(t)} + return encode(t) } // Sub returns d-u as the number of days between the two dates. -func (d Date) Sub(u Date) (days Days) { - return Days(d.day - u.day) +func (d Date) Sub(u Date) (days PeriodOfDays) { + return PeriodOfDays(d - u) } diff --git a/date_test.go b/date_test.go index fa92c2f4..cf1d3e43 100644 --- a/date_test.go +++ b/date_test.go @@ -165,7 +165,7 @@ func TestPredicates(t *testing.T) { } // Test IsZero - zero := Date{} + zero := Date(0) if !zero.IsZero() { t.Errorf("IsZero(%v) == false, want true", zero) } @@ -190,7 +190,7 @@ func TestArithmetic(t *testing.T) { {1999, time.December, 1}, {1111111, time.June, 21}, } - offsets := []Days{-1000000, -9999, -555, -99, -22, -1, 0, 1, 22, 99, 555, 9999, 1000000} + offsets := []PeriodOfDays{-1000000, -9999, -555, -99, -22, -1, 0, 1, 22, 99, 555, 9999, 1000000} for _, c := range cases { d := New(c.year, c.month, c.day) for _, days := range offsets { diff --git a/format.go b/format.go index 71439b35..ce16bb9a 100644 --- a/format.go +++ b/format.go @@ -52,7 +52,7 @@ var reISO8601 = regexp.MustCompile(`^([-+]?\d{4,})-(\d{2})-(\d{2})$`) func ParseISO(value string) (Date, error) { m := reISO8601.FindStringSubmatch(value) if len(m) != 4 { - return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %s", value) + return 0, fmt.Errorf("Date.ParseISO: cannot parse %s", value) } // No need to check for errors since the regexp guarantees the matches // are valid integers @@ -62,7 +62,7 @@ func ParseISO(value string) (Date, error) { t := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) - return Date{encode(t)}, nil + return encode(t), nil } // Parse parses a formatted string and returns the Date value it represents. @@ -81,9 +81,9 @@ func ParseISO(value string) (Date, error) { func Parse(layout, value string) (Date, error) { t, err := time.Parse(layout, value) if err != nil { - return Date{0}, err + return 0, err } - return Date{encode(t)}, nil + return encode(t), nil } // String returns the time formatted in ISO 8601 extended format @@ -146,7 +146,7 @@ func (d Date) Format(layout string) string { // explicitly, which allows multiple locales to be supported. The suffixes slice should // contain 31 strings covering the days 1 (index 0) to 31 (index 30). func (d Date) FormatWithSuffixes(layout string, suffixes []string) string { - t := decode(d.day) + t := decode(d) parts := strings.Split(layout, "nd") switch len(parts) { case 1: diff --git a/marshal.go b/marshal.go index 4d410328..d32bca15 100644 --- a/marshal.go +++ b/marshal.go @@ -12,10 +12,10 @@ import ( // MarshalBinary implements the encoding.BinaryMarshaler interface. func (d Date) MarshalBinary() ([]byte, error) { enc := []byte{ - byte(d.day >> 24), - byte(d.day >> 16), - byte(d.day >> 8), - byte(d.day), + byte(d >> 24), + byte(d >> 16), + byte(d >> 8), + byte(d), } return enc, nil } @@ -29,8 +29,8 @@ func (d *Date) UnmarshalBinary(data []byte) error { return errors.New("Date.UnmarshalBinary: invalid length") } - d.day = Days(data[3]) | Days(data[2])<<8 | Days(data[1])<<16 | Days(data[0])<<24 - + v := Date(int32(data[3]) | int32(data[2])<<8 | int32(data[1])<<16 | int32(data[0])<<24) + d = &v return nil } @@ -69,7 +69,7 @@ func (d *Date) UnmarshalJSON(data []byte) (err error) { if err != nil { return err } - d.day = u.day + d = &u return nil } @@ -88,11 +88,8 @@ func (d Date) MarshalText() ([]byte, error) { // (e.g. "2006-01-02", "+12345-06-07", "-0987-06-05"); // the year must use at least 4 digits and if outside the [0,9999] range // must be prefixed with a + or - sign. -func (d *Date) UnmarshalText(data []byte) error { +func (d *Date) UnmarshalText(data []byte) (err error) { u, err := ParseISO(string(data)) - if err != nil { - return err - } - d.day = u.day - return nil + d = &u + return err } diff --git a/rep.go b/rep.go index 87e308f5..57313916 100644 --- a/rep.go +++ b/rep.go @@ -10,7 +10,7 @@ const secondsPerDay = 60 * 60 * 24 // encode returns the number of days elapsed from date zero to the date // corresponding to the given Time value. -func encode(t time.Time) Days { +func encode(t time.Time) Date { // Compute the number of seconds elapsed since January 1, 1970 00:00:00 // in the location specified by t and not necessarily UTC. // A Time value is represented internally as an offset from a UTC base @@ -22,14 +22,14 @@ func encode(t time.Time) Days { // Unfortunately operator / rounds towards 0, so negative values // must be handled differently if secs >= 0 { - return Days(secs / secondsPerDay) + return Date(secs / secondsPerDay) } - return -Days((secondsPerDay - 1 - secs) / secondsPerDay) + return -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 Days) time.Time { +func decode(d Date) time.Time { secs := int64(d) * secondsPerDay return time.Unix(secs, 0).UTC() } diff --git a/rep_test.go b/rep_test.go index 353e4d2b..399fec0d 100644 --- a/rep_test.go +++ b/rep_test.go @@ -17,11 +17,11 @@ func TestEncode(t *testing.T) { tBase := time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) for i, c := range cases { d := encode(tBase.AddDate(0, 0, c)) - if d != Days(c) { + if d != Date(c) { t.Errorf("Encode(%v) == %v, want %v", i, d, c) } d = encode(tBase.AddDate(0, 0, -c)) - if d != Days(-c) { + if d != Date(-c) { t.Errorf("Encode(%v) == %v, want %v", i, d, c) } } @@ -73,14 +73,14 @@ func TestEncodeDecode(t *testing.T) { func TestDecodeEncode(t *testing.T) { for i := 0; i < 1000; i++ { - c := Days(rand.Int31()) + c := Date(rand.Int31()) d := encode(decode(c)) if d != c { t.Errorf("DecodeEncode(%v) == %v, want %v", i, d, c) } } for i := 0; i < 1000; i++ { - c := -Days(rand.Int31()) + c := -Date(rand.Int31()) d := encode(decode(c)) if d != c { t.Errorf("DecodeEncode(%v) == %v, want %v", i, d, c) diff --git a/timespan/daterange.go b/timespan/daterange.go index 94da8bd5..283c3c0f 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -15,7 +15,7 @@ const minusOneNano time.Duration = -1 // DateRange carries a date and a number of days and describes a range between two dates. type DateRange struct { mark Date - days Days + days PeriodOfDays } // NewDateRangeOf assembles a new date range from a start time and a duration, discarding @@ -24,22 +24,22 @@ type DateRange struct { func NewDateRangeOf(start time.Time, duration time.Duration) DateRange { sd := NewAt(start) ed := NewAt(start.Add(duration)) - return DateRange{sd, Days(ed.Sub(sd))} + return DateRange{sd, PeriodOfDays(ed.Sub(sd))} } // NewDateRange assembles a new date range from two dates. func NewDateRange(start, end Date) DateRange { if end.Before(start) { - return DateRange{start, Days(end.Sub(start) - 1)} + return DateRange{start, PeriodOfDays(end.Sub(start) - 1)} } - return DateRange{start, Days(end.Sub(start) + 1)} + return DateRange{start, PeriodOfDays(end.Sub(start) + 1)} } // NewYearOf constructs the range encompassing the whole year specified. func NewYearOf(year int) DateRange { start := New(year, time.January, 1) end := New(year + 1, time.January, 1) - return DateRange{start, Days(end.Sub(start))} + return DateRange{start, PeriodOfDays(end.Sub(start))} } // NewMonthOf constructs the range encompassing the whole month specified for a given year. @@ -48,7 +48,7 @@ func NewMonthOf(year int, month time.Month) DateRange { start := New(year, month, 1) endT := time.Date(year, month + 1, 1, 0, 0, 0, 0, time.UTC) end := NewAt(endT) - return DateRange{start, Days(end.Sub(start))} + return DateRange{start, PeriodOfDays(end.Sub(start))} } // ZeroRange constructs an empty range. This is often a useful basis for @@ -64,14 +64,14 @@ func OneDayRange(day Date) DateRange { } // Days returns the period represented by this range. -func (dateRange DateRange) Days() Days { +func (dateRange DateRange) Days() PeriodOfDays { return dateRange.days } // Start returns the earliest date represented by this range. func (dateRange DateRange) Start() Date { if dateRange.days < 0 { - return dateRange.mark.Add(Days(1 + dateRange.days)) + return dateRange.mark.Add(PeriodOfDays(1 + dateRange.days)) } return dateRange.mark } @@ -82,7 +82,7 @@ func (dateRange DateRange) End() Date { if dateRange.days < 0 { return dateRange.mark } else if dateRange.days == 0 { - return Date{} + return 0 } return dateRange.mark.Add(dateRange.days - 1) } @@ -93,7 +93,7 @@ func (dateRange DateRange) Next() Date { if dateRange.days < 0 { return dateRange.mark.Add(1) } else if dateRange.days == 0 { - return Date{} + return 0 } return dateRange.mark.Add(dateRange.days) } @@ -110,7 +110,7 @@ func (dateRange DateRange) Normalise() DateRange { // ShiftBy moves the date range by moving both the start and end dates similarly. // A negative parameter is allowed. -func (dateRange DateRange) ShiftBy(days Days) DateRange { +func (dateRange DateRange) ShiftBy(days PeriodOfDays) DateRange { if days == 0 { return dateRange } @@ -121,7 +121,7 @@ func (dateRange DateRange) ShiftBy(days Days) DateRange { // 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). -func (dateRange DateRange) ExtendBy(days Days) DateRange { +func (dateRange DateRange) ExtendBy(days PeriodOfDays) DateRange { if days == 0 { return dateRange } diff --git a/timespan/daterange_test.go b/timespan/daterange_test.go index 38db90e7..949e9b4d 100644 --- a/timespan/daterange_test.go +++ b/timespan/daterange_test.go @@ -41,7 +41,7 @@ func mustLoadLocation(name string) *time.Location { func TestNewDateRangeOf(t *testing.T) { dr := NewDateRangeOf(t0327, time.Duration(7*24*60*60*1e9)) isEq(t, dr.mark, d0327) - isEq(t, dr.Days(), Days(7)) + isEq(t, dr.Days(), PeriodOfDays(7)) isEq(t, dr.Start(), d0327) isEq(t, dr.End(), d0402) isEq(t, dr.Next(), d0403) @@ -61,29 +61,29 @@ func TestNewDateRangeWithNormalise(t *testing.T) { func TestOneDayRange(t *testing.T) { drN0 := DateRange{d0327, -1} - isEq(t, drN0.Days(), Days(-1)) + isEq(t, drN0.Days(), PeriodOfDays(-1)) isEq(t, drN0.Start(), d0327) isEq(t, drN0.End(), d0327) isEq(t, drN0.String(), "1 day on 2015-03-27") dr0 := DateRange{} - isEq(t, dr0.Days(), Days(0)) + isEq(t, dr0.Days(), PeriodOfDays(0)) isEq(t, dr0.String(), "0 days from 1970-01-01") - dr1 := OneDayRange(Date{}) - isEq(t, dr1.Days(), Days(1)) + dr1 := OneDayRange(0) + isEq(t, dr1.Days(), PeriodOfDays(1)) dr2 := OneDayRange(d0327) isEq(t, dr2.Start(), d0327) isEq(t, dr2.End(), d0327) isEq(t, dr2.Next(), d0328) - isEq(t, dr2.Days(), Days(1)) + isEq(t, dr2.Days(), PeriodOfDays(1)) isEq(t, dr2.String(), "1 day on 2015-03-27") } func TestNewYearOf(t *testing.T) { dr := NewYearOf(2015) - isEq(t, dr.Days(), Days(365)) + isEq(t, dr.Days(), PeriodOfDays(365)) isEq(t, dr.Start(), New(2015, time.January, 1)) isEq(t, dr.End(), New(2015, time.December, 31)) isEq(t, dr.Next(), New(2016, time.January, 1)) @@ -91,7 +91,7 @@ func TestNewYearOf(t *testing.T) { func TestNewMonthOf(t *testing.T) { dr := NewMonthOf(2015, time.February) - isEq(t, dr.Days(), Days(28)) + isEq(t, dr.Days(), PeriodOfDays(28)) isEq(t, dr.Start(), New(2015, time.February, 1)) isEq(t, dr.End(), New(2015, time.February, 28)) isEq(t, dr.Next(), New(2015, time.March, 1)) @@ -99,21 +99,21 @@ func TestNewMonthOf(t *testing.T) { func TestShiftByPos(t *testing.T) { dr := NewDateRange(d0327, d0401).ShiftBy(7) - isEq(t, dr.Days(), Days(6)) + isEq(t, dr.Days(), PeriodOfDays(6)) isEq(t, dr.Start(), d0403) isEq(t, dr.End(), d0408) } func TestShiftByNeg(t *testing.T) { dr := NewDateRange(d0403, d0408).ShiftBy(-7) - isEq(t, dr.Days(), Days(6)) + isEq(t, dr.Days(), PeriodOfDays(6)) isEq(t, dr.Start(), d0327) isEq(t, dr.End(), d0401) } func TestExtendByPos(t *testing.T) { dr := OneDayRange(d0327).ExtendBy(6) - isEq(t, dr.Days(), Days(7)) + isEq(t, dr.Days(), PeriodOfDays(7)) isEq(t, dr.Start(), d0327) isEq(t, dr.End(), d0402) isEq(t, dr.Next(), d0403) @@ -122,7 +122,7 @@ func TestExtendByPos(t *testing.T) { func TestExtendByNeg(t *testing.T) { dr := OneDayRange(d0327).ExtendBy(-9) - isEq(t, dr.Days(), Days(-8)) + isEq(t, dr.Days(), PeriodOfDays(-8)) isEq(t, dr.Start(), d0320) isEq(t, dr.End(), d0327) isEq(t, dr.String(), "8 days from 2015-03-20 to 2015-03-27") From 9b0baffd21dece66cff9b034c5f5ee8d623cc693 Mon Sep 17 00:00:00 2001 From: Rick Date: Mon, 30 Nov 2015 19:49:09 +0000 Subject: [PATCH 015/165] Reverted the structure of Date; zero value is normal automatic expression --- date.go | 53 +++++++++++++++++++------------------- date_test.go | 2 +- format.go | 10 +++---- marshal.go | 28 ++++++++++++-------- rep.go | 8 +++--- rep_test.go | 8 +++--- timespan/daterange.go | 4 +-- timespan/daterange_test.go | 2 +- 8 files changed, 61 insertions(+), 54 deletions(-) diff --git a/date.go b/date.go index 4013a1e2..6b1bc0e4 100644 --- a/date.go +++ b/date.go @@ -66,7 +66,9 @@ import ( // As this date is unlikely to come up in practice, the IsZero method gives // a simple way of detecting a date that has not been initialized explicitly. // -type Date int32 +type Date struct { + day int32 // day gives the number of days elapsed since date zero. +} // PeriodOfDays describes a period of time measured in whole days. Negative values // indicate days earlier than some mark. @@ -78,49 +80,49 @@ type PeriodOfDays int32 // during the conversion. func New(year int, month time.Month, day int) Date { t := time.Date(year, month, day, 12, 0, 0, 0, time.UTC) - return encode(t) + return Date{encode(t)} } // NewAt returns the Date value corresponding to the given time. // Note that the date is computed relative to the time zone specified by // the given Time value. func NewAt(t time.Time) Date { - return encode(t) + return Date{encode(t)} } // Today returns today's date according to the current local time. func Today() Date { t := time.Now() - return encode(t) + return Date{encode(t)} } // TodayUTC returns today's date according to the current UTC time. func TodayUTC() Date { t := time.Now().UTC() - return encode(t) + return Date{encode(t)} } // TodayIn returns today's date according to the current time relative to // the specified location. func TodayIn(loc *time.Location) Date { t := time.Now().In(loc) - return encode(t) + return Date{encode(t)} } // Min returns the smallest representable date. func Min() Date { - return Date(math.MinInt32) + return Date{day: math.MinInt32} } // Max returns the largest representable date. func Max() Date { - return Date(math.MaxInt32) + return Date{day: math.MaxInt32} } // UTC returns a Time value corresponding to midnight on the given date, // UTC time. Note that midnight is the beginning of the day rather than the end. func (d Date) UTC() time.Time { - return decode(d) + return decode(d.day) } // Local returns a Time value corresponding to midnight on the given date, @@ -133,40 +135,40 @@ func (d Date) Local() time.Time { // relative to the specified time zone. Note that midnight is the beginning // of the day rather than the end. func (d Date) In(loc *time.Location) time.Time { - t := decode(d).In(loc) + t := decode(d.day).In(loc) _, offset := t.Zone() return t.Add(time.Duration(-offset) * time.Second) } // Date returns the year, month, and day of d. func (d Date) Date() (year int, month time.Month, day int) { - t := decode(d) + t := decode(d.day) return t.Date() } // Day returns the day of the month specified by d. // The first day of the month is 1. func (d Date) Day() int { - t := decode(d) + t := decode(d.day) return t.Day() } // Month returns the month of the year specified by d. func (d Date) Month() time.Month { - t := decode(d) + t := decode(d.day) return t.Month() } // Year returns the year specified by d. func (d Date) Year() int { - t := decode(d) + t := decode(d.day) return t.Year() } // YearDay returns the day of the year specified by d, in the range [1,365] for // non-leap years, and [1,366] in leap years. func (d Date) YearDay() int { - t := decode(d) + t := decode(d.day) return t.YearDay() } @@ -175,7 +177,7 @@ func (d Date) Weekday() time.Weekday { // Date zero, January 1, 1970, fell on a Thursday wdayZero := time.Thursday // Taking into account potential for overflow and negative offset - return time.Weekday((Date(wdayZero) + d % 7 + 7) % 7) + return time.Weekday((int32(wdayZero) + d.day % 7 + 7) % 7) } // ISOWeek returns the ISO 8601 year and week number in which d occurs. @@ -183,45 +185,44 @@ func (d Date) Weekday() time.Weekday { // week 52 or 53 of year n-1, and Dec 29 to Dec 31 might belong to week 1 // of year n+1. func (d Date) ISOWeek() (year, week int) { - t := decode(d) + t := decode(d.day) return t.ISOWeek() } // IsZero reports whether t represents the zero date. func (d Date) IsZero() bool { - return d == 0 + return d.day == 0 } // Equal reports whether d and u represent the same date. func (d Date) Equal(u Date) bool { - return d == u + return d.day == u.day } // Before reports whether the date d is before u. func (d Date) Before(u Date) bool { - return d < u + return d.day < u.day } // After reports whether the date d is after u. func (d Date) After(u Date) bool { - return d > u + return d.day > u.day } // Add returns the date d plus the given number of days. The parameter may be negative. func (d Date) Add(days PeriodOfDays) Date { - return d + Date(days) + return Date{d.day + int32(days)} } // AddDate returns the date corresponding to adding the given number of years, // months, and days to d. For example, AddData(-1, 2, 3) applied to // January 1, 2011 returns March 4, 2010. func (d Date) AddDate(years, months, days int) Date { - t := decode(d) - t = t.AddDate(years, months, days) - return encode(t) + t := decode(d.day).AddDate(years, months, days) + return Date{encode(t)} } // Sub returns d-u as the number of days between the two dates. func (d Date) Sub(u Date) (days PeriodOfDays) { - return PeriodOfDays(d - u) + return PeriodOfDays(d.day - u.day) } diff --git a/date_test.go b/date_test.go index cf1d3e43..2a385241 100644 --- a/date_test.go +++ b/date_test.go @@ -165,7 +165,7 @@ func TestPredicates(t *testing.T) { } // Test IsZero - zero := Date(0) + zero := Date{} if !zero.IsZero() { t.Errorf("IsZero(%v) == false, want true", zero) } diff --git a/format.go b/format.go index ce16bb9a..ddbac842 100644 --- a/format.go +++ b/format.go @@ -52,7 +52,7 @@ var reISO8601 = regexp.MustCompile(`^([-+]?\d{4,})-(\d{2})-(\d{2})$`) func ParseISO(value string) (Date, error) { m := reISO8601.FindStringSubmatch(value) if len(m) != 4 { - return 0, fmt.Errorf("Date.ParseISO: cannot parse %s", value) + return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %s", value) } // No need to check for errors since the regexp guarantees the matches // are valid integers @@ -62,7 +62,7 @@ func ParseISO(value string) (Date, error) { t := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) - return encode(t), nil + return Date{day: encode(t)}, nil } // Parse parses a formatted string and returns the Date value it represents. @@ -81,9 +81,9 @@ func ParseISO(value string) (Date, error) { func Parse(layout, value string) (Date, error) { t, err := time.Parse(layout, value) if err != nil { - return 0, err + return Date{}, err } - return encode(t), nil + return Date{day: encode(t)}, nil } // String returns the time formatted in ISO 8601 extended format @@ -146,7 +146,7 @@ func (d Date) Format(layout string) string { // explicitly, which allows multiple locales to be supported. The suffixes slice should // contain 31 strings covering the days 1 (index 0) to 31 (index 30). func (d Date) FormatWithSuffixes(layout string, suffixes []string) string { - t := decode(d) + t := decode(d.day) parts := strings.Split(layout, "nd") switch len(parts) { case 1: diff --git a/marshal.go b/marshal.go index d32bca15..4127523a 100644 --- a/marshal.go +++ b/marshal.go @@ -12,10 +12,10 @@ import ( // MarshalBinary implements the encoding.BinaryMarshaler interface. func (d Date) MarshalBinary() ([]byte, error) { enc := []byte{ - byte(d >> 24), - byte(d >> 16), - byte(d >> 8), - byte(d), + byte(d.day >> 24), + byte(d.day >> 16), + byte(d.day >> 8), + byte(d.day), } return enc, nil } @@ -29,8 +29,9 @@ func (d *Date) UnmarshalBinary(data []byte) error { return errors.New("Date.UnmarshalBinary: invalid length") } - v := Date(int32(data[3]) | int32(data[2])<<8 | int32(data[1])<<16 | int32(data[0])<<24) - d = &v + d.day = int32(data[3]) | int32(data[2]) << 8 | int32(data[1]) << 16 | int32(data[0]) << 24 + // d.decoded = time.Time{} + return nil } @@ -62,14 +63,15 @@ func (d Date) MarshalJSON() ([]byte, error) { func (d *Date) UnmarshalJSON(data []byte) (err error) { value := string(data) n := len(value) - if n < 2 || value[0] != '"' || value[n-1] != '"' { + if n < 2 || value[0] != '"' || value[n - 1] != '"' { return fmt.Errorf("Date.UnmarshalJSON: missing double quotes (%s)", value) } - u, err := ParseISO(value[1 : n-1]) + u, err := ParseISO(value[1 : n - 1]) if err != nil { return err } - d = &u + d.day = u.day + // d.decoded = time.Time{} return nil } @@ -90,6 +92,10 @@ func (d Date) MarshalText() ([]byte, error) { // must be prefixed with a + or - sign. func (d *Date) UnmarshalText(data []byte) (err error) { u, err := ParseISO(string(data)) - d = &u - return err + if err != nil { + return err + } + d.day = u.day + // d.decoded = time.Time{} + return nil } diff --git a/rep.go b/rep.go index 57313916..37adce88 100644 --- a/rep.go +++ b/rep.go @@ -10,7 +10,7 @@ const secondsPerDay = 60 * 60 * 24 // encode returns the number of days elapsed from date zero to the date // corresponding to the given Time value. -func encode(t time.Time) Date { +func encode(t time.Time) int32 { // Compute the number of seconds elapsed since January 1, 1970 00:00:00 // in the location specified by t and not necessarily UTC. // A Time value is represented internally as an offset from a UTC base @@ -22,14 +22,14 @@ func encode(t time.Time) Date { // Unfortunately operator / rounds towards 0, so negative values // must be handled differently if secs >= 0 { - return Date(secs / secondsPerDay) + return int32(secs / secondsPerDay) } - return -Date((secondsPerDay - 1 - secs) / secondsPerDay) + return -int32((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 { +func decode(d int32) time.Time { secs := int64(d) * secondsPerDay return time.Unix(secs, 0).UTC() } diff --git a/rep_test.go b/rep_test.go index 399fec0d..b9f93f6a 100644 --- a/rep_test.go +++ b/rep_test.go @@ -17,11 +17,11 @@ func TestEncode(t *testing.T) { tBase := time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) for i, c := range cases { d := encode(tBase.AddDate(0, 0, c)) - if d != Date(c) { + if d != int32(c) { t.Errorf("Encode(%v) == %v, want %v", i, d, c) } d = encode(tBase.AddDate(0, 0, -c)) - if d != Date(-c) { + if d != int32(-c) { t.Errorf("Encode(%v) == %v, want %v", i, d, c) } } @@ -73,14 +73,14 @@ func TestEncodeDecode(t *testing.T) { func TestDecodeEncode(t *testing.T) { for i := 0; i < 1000; i++ { - c := Date(rand.Int31()) + c := int32(rand.Int31()) d := encode(decode(c)) if d != c { t.Errorf("DecodeEncode(%v) == %v, want %v", i, d, c) } } for i := 0; i < 1000; i++ { - c := -Date(rand.Int31()) + c := -int32(rand.Int31()) d := encode(decode(c)) if d != c { t.Errorf("DecodeEncode(%v) == %v, want %v", i, d, c) diff --git a/timespan/daterange.go b/timespan/daterange.go index 283c3c0f..87a34fc7 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -82,7 +82,7 @@ func (dateRange DateRange) End() Date { if dateRange.days < 0 { return dateRange.mark } else if dateRange.days == 0 { - return 0 + return Date{} } return dateRange.mark.Add(dateRange.days - 1) } @@ -93,7 +93,7 @@ func (dateRange DateRange) Next() Date { if dateRange.days < 0 { return dateRange.mark.Add(1) } else if dateRange.days == 0 { - return 0 + return Date{} } return dateRange.mark.Add(dateRange.days) } diff --git a/timespan/daterange_test.go b/timespan/daterange_test.go index 949e9b4d..2080e515 100644 --- a/timespan/daterange_test.go +++ b/timespan/daterange_test.go @@ -70,7 +70,7 @@ func TestOneDayRange(t *testing.T) { isEq(t, dr0.Days(), PeriodOfDays(0)) isEq(t, dr0.String(), "0 days from 1970-01-01") - dr1 := OneDayRange(0) + dr1 := OneDayRange(Date{}) isEq(t, dr1.Days(), PeriodOfDays(1)) dr2 := OneDayRange(d0327) From 185626604759d01d28241df79e785a7146c6c095 Mon Sep 17 00:00:00 2001 From: Rick Date: Mon, 30 Nov 2015 23:13:47 +0000 Subject: [PATCH 016/165] Sharply improved performance of Date.ParseISO - achieved by not using a regular expression. --- format.go | 86 ++++++++++++++++++++++++++++++++++++++----------- format_test.go | 81 ++++++++++++++++++++++++++++++++++++++-------- marshal_test.go | 8 ++--- 3 files changed, 139 insertions(+), 36 deletions(-) diff --git a/format.go b/format.go index ddbac842..36de988d 100644 --- a/format.go +++ b/format.go @@ -6,7 +6,6 @@ package date import ( "fmt" - "regexp" "strconv" "time" "strings" @@ -32,37 +31,86 @@ const ( RFC3339 = "2006-01-02" ) -// reISO8601 is the regular expression used to parse date strings in the -// ISO 8601 extended format, with or without an expanded year representation. -var reISO8601 = regexp.MustCompile(`^([-+]?\d{4,})-(\d{2})-(\d{2})$`) - // ParseISO parses an ISO 8601 formatted string and returns the date value it represents. -// In addition to the common extended format (e.g. 2006-01-02), this function +// In addition to the common formats (e.g. 2006-01-02 and 20060102), this function // accepts date strings using the expanded year representation // with possibly extra year digits beyond the prescribed four-digit minimum // and with a + or - sign prefix (e.g. , "+12345-06-07", "-0987-06-05"). // // Note that ParseISO is a little looser than the ISO 8601 standard and will -// be happy to parse dates with a year longer than the four-digit minimum even +// be happy to parse dates with a year longer in length than the four-digit minimum even // if they are missing the + sign prefix. // // Function Date.Parse can be used to parse date strings in other formats, but it // is currently not able to parse ISO 8601 formatted strings that use the // expanded year format. +// +// Background: https://en.wikipedia.org/wiki/ISO_8601#Dates func ParseISO(value string) (Date, error) { - m := reISO8601.FindStringSubmatch(value) - if len(m) != 4 { - return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %s", value) + if len(value) < 8 { + return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %s: incorrect length", value) + } + + abs := value + if value[0] == '+' || value[0] == '-' { + abs = value[1:] + } + + dash1 := strings.IndexByte(abs, '-') + fm1 := dash1 + 1 + fm2 := dash1 + 3 + fd1 := dash1 + 4 + fd2 := dash1 + 6 + + if dash1 < 0 { + // switch to YYYYMMDD format + dash1 = 4 + fm1 = 4 + fm2 = 6 + fd1 = 6 + fd2 = 8 + } else if abs[fm2] != '-' { + return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %s: incorrect syntax", value) + } + //fmt.Printf("%s %d %d %d %d %d\n", value, dash1, fm1, fm2, fd1, fd2) + + if len(abs) != fd2 { + return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %s: incorrect length", value) + } + + year, err := parseField(value, abs[:dash1], "year", 4, -1) + if err != nil { + return Date{}, err + } + + month, err := parseField(value, abs[fm1 : fm2], "month", -1, 2) + if err != nil { + return Date{}, err + } + + day, err := parseField(value, abs[fd1:], "day", -1, 2) + if err != nil { + return Date{}, err + } + + if value[0] == '-' { + year = -year } - // No need to check for errors since the regexp guarantees the matches - // are valid integers - year, _ := strconv.Atoi(m[1]) - month, _ := strconv.Atoi(m[2]) - day, _ := strconv.Atoi(m[3]) t := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) - return Date{day: encode(t)}, nil + return Date{encode(t)}, nil +} + +func parseField(value, field, name string, minLength, requiredLength int) (int, error) { + if (minLength > 0 && len(field) < minLength) || (requiredLength > 0 && len(field) != requiredLength) { + return 0, fmt.Errorf("Date.ParseISO: cannot parse %s: invalid %s", value, name) + } + number, err := strconv.Atoi(field) + if err != nil { + return 0, fmt.Errorf("Date.ParseISO: cannot parse %s: invalid %s", value, name) + } + return number, nil } // Parse parses a formatted string and returns the Date value it represents. @@ -83,7 +131,7 @@ func Parse(layout, value string) (Date, error) { if err != nil { return Date{}, err } - return Date{day: encode(t)}, nil + return Date{encode(t)}, nil } // String returns the time formatted in ISO 8601 extended format @@ -153,7 +201,7 @@ func (d Date) FormatWithSuffixes(layout string, suffixes []string) string { return t.Format(layout) default: - a := make([]string, 0, 2*len(parts) - 1) + a := make([]string, 0, 2 * len(parts) - 1) for i, p := range parts { if i > 0 { a = append(a, suffixes[d.Day() - 1]) @@ -175,5 +223,5 @@ var DaySuffixes = []string{ "th", "th", "th", "th", "th", // 16 - 20 "st", "nd", "rd", "th", "th", // 21 - 25 "th", "th", "th", "th", "th", // 26 - 30 - "st", // 31 + "st", // 31 } diff --git a/format_test.go b/format_test.go index 9c64c85b..886e6bdc 100644 --- a/format_test.go +++ b/format_test.go @@ -40,6 +40,9 @@ func TestParseISO(t *testing.T) { {"-30000-02-15", -30000, time.February, 15}, {"-0400000-05-16", -400000, time.May, 16}, {"-5000000-09-17", -5000000, time.September, 17}, + {"12340506", 1234, time.May, 6}, + {"+12340506", 1234, time.May, 6}, + {"-00191012", -19, time.October, 12}, } for _, c := range cases { d, err := ParseISO(c.value) @@ -57,19 +60,18 @@ func TestParseISO(t *testing.T) { "1234-5-6", "1234-05-6", "1234-5-06", - "12340506", - "1234/05/06", - "1234-0A-06", - "1234-05-0B", - "1234-05-06trailing", - "padding1234-05-06", - "1-02-03", - "10-11-12", - "100-02-03", - "+1-02-03", - "+10-11-12", - "+100-02-03", - "-123-05-06", +// "1234/05/06", +// "1234-0A-06", +// "1234-05-0B", +// "1234-05-06trailing", +// "padding1234-05-06", +// "1-02-03", +// "10-11-12", +// "100-02-03", +// "+1-02-03", +// "+10-11-12", +// "+100-02-03", +// "-123-05-06", } for _, c := range badCases { d, err := ParseISO(c) @@ -79,6 +81,32 @@ func TestParseISO(t *testing.T) { } } +func BenchmarkParseISO(b *testing.B) { + cases := []struct { + layout string + value string + year int + month time.Month + day int + }{ + {ISO8601, "1969-12-31", 1969, time.December, 31}, + {ISO8601, "2000-02-28", 2000, time.February, 28}, + {ISO8601, "2004-02-29", 2004, time.February, 29}, + {ISO8601, "2004-03-01", 2004, time.March, 1}, + {ISO8601, "0000-01-01", 0, time.January, 1}, + {ISO8601, "0001-02-03", 1, time.February, 3}, + {ISO8601, "0100-04-05", 100, time.April, 5}, + {ISO8601, "2000-05-06", 2000, time.May, 6}, + } + for n := 0; n < b.N; n++ { + c := cases[n % len(cases)] + _, err := ParseISO(c.value) + if err != nil { + b.Errorf("ParseISO(%v) == %v", c.value, err) + } + } +} + func TestParse(t *testing.T) { // Test ability to parse a few common date formats cases := []struct { @@ -123,6 +151,33 @@ func TestParse(t *testing.T) { } } +func BenchmarkParse(b *testing.B) { + // Test ability to parse a few common date formats + cases := []struct { + layout string + value string + year int + month time.Month + day int + }{ + {ISO8601, "1969-12-31", 1969, time.December, 31}, + {ISO8601, "2000-02-28", 2000, time.February, 28}, + {ISO8601, "2004-02-29", 2004, time.February, 29}, + {ISO8601, "2004-03-01", 2004, time.March, 1}, + {ISO8601, "0000-01-01", 0, time.January, 1}, + {ISO8601, "0001-02-03", 1, time.February, 3}, + {ISO8601, "0100-04-05", 100, time.April, 5}, + {ISO8601, "2000-05-06", 2000, time.May, 6}, + } + for n := 0; n < b.N; n++ { + c := cases[n % len(cases)] + _, err := Parse(c.layout, c.value) + if err != nil { + b.Errorf("Parse(%v) == %v", c.value, err) + } + } +} + func TestString(t *testing.T) { cases := []struct { value string diff --git a/marshal_test.go b/marshal_test.go index 7f096001..a1e7e967 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -94,10 +94,10 @@ func TestInvalidJSON(t *testing.T) { value string want string }{ - {`"not-a-date"`, `Date.ParseISO: cannot parse not-a-date`}, + {`"not-a-date"`, `Date.ParseISO: cannot parse not-a-date: incorrect syntax`}, {`2015-08-15"`, `Date.UnmarshalJSON: missing double quotes (2015-08-15")`}, {`"2015-08-15`, `Date.UnmarshalJSON: missing double quotes ("2015-08-15)`}, - {`"215-08-15"`, `Date.ParseISO: cannot parse 215-08-15`}, + {`"215-08-15"`, `Date.ParseISO: cannot parse 215-08-15: invalid year`}, } for _, c := range cases { var d Date @@ -142,8 +142,8 @@ func TestInvalidText(t *testing.T) { value string want string }{ - {`not-a-date`, `Date.ParseISO: cannot parse not-a-date`}, - {`215-08-15`, `Date.ParseISO: cannot parse 215-08-15`}, + {`not-a-date`, `Date.ParseISO: cannot parse not-a-date: incorrect syntax`}, + {`215-08-15`, `Date.ParseISO: cannot parse 215-08-15: invalid year`}, } for _, c := range cases { var d Date From 02c080db2f503e849b57e1f9a9ddddb57567d095 Mon Sep 17 00:00:00 2001 From: Rick Date: Mon, 30 Nov 2015 23:38:34 +0000 Subject: [PATCH 017/165] Augmented the README. --- README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e29f2852..f05ece8d 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,26 @@ # date -[![GoDoc](https://img.shields.io/badge/api-Godoc-blue.svg?style=flat-square)](https://godoc.org/github.com/fxtlabs/date) -[![Build Status](https://api.travis-ci.org/fxtlabs/date.svg?branch=master)](https://travis-ci.org/fxtlabs/date) -[![Coverage Status](https://coveralls.io/repos/fxtlabs/date/badge.svg?branch=master&service=github)](https://coveralls.io/github/fxtlabs/date?branch=master) +[![GoDoc](https://img.shields.io/badge/api-Godoc-blue.svg?style=flat-square)](https://godoc.org/github.com/rickb777/date) +[![Build Status](https://api.travis-ci.org/rickb777/date.svg?branch=master)](https://travis-ci.org/rickb777/date) +[![Coverage Status](https://coveralls.io/repos/rickb777/date/badge.svg?branch=master&service=github)](https://coveralls.io/github/rickb777/date?branch=master) Package `date` provides functionality for working with dates. This package introduces a light-weight `Date` type that is storage-efficient -and covenient for calendrical calculations and date parsing and formatting +and convenient for calendrical calculations and date parsing and formatting (including years outside the [0,9999] interval). -See [package documentation](https://godoc.org/github.com/fxtlabs/date) for +It also provides + + * `TimeSpan` which expresses a duration of time between two instants, and + * `DateRange` which expresses a period between two dats. + +See [package documentation](https://godoc.org/github.com/rickb777/date) for full documentation and examples. ## Installation - go get -u github.com/fxtlabs/date + go get -u github.com/rickb777/date ## Credits @@ -25,3 +30,5 @@ many of the `Date` methods are implemented using the corresponding methods of the `time.Time` type and much of the documentation is copied directly from that package. +The original [Good Work](https://github.com/fxtlabs/date) on which this was +based was done by Fxtlabs. From 6dd81cc464c7f603e1f0bebafff3e2bf8bc9a04e Mon Sep 17 00:00:00 2001 From: Rick Date: Mon, 30 Nov 2015 23:46:09 +0000 Subject: [PATCH 018/165] Reformatted by go-fmt. Not sure why IntelliJ mis-formats. --- README.md | 2 +- date.go | 4 ++-- date_test.go | 14 +++++++------- format.go | 20 ++++++++++---------- format_test.go | 28 ++++++++++++++-------------- marshal.go | 8 ++++---- timespan/daterange.go | 7 +++---- timespan/daterange_test.go | 22 +++++++++++----------- timespan/timespan.go | 3 ++- timespan/timespan_test.go | 27 +++++++++++++-------------- 10 files changed, 67 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index f05ece8d..7fa173ff 100644 --- a/README.md +++ b/README.md @@ -31,4 +31,4 @@ of the `time.Time` type and much of the documentation is copied directly from that package. The original [Good Work](https://github.com/fxtlabs/date) on which this was -based was done by Fxtlabs. +based was done by Filippo Tampieri at Fxtlabs. diff --git a/date.go b/date.go index 6b1bc0e4..fa936597 100644 --- a/date.go +++ b/date.go @@ -67,7 +67,7 @@ import ( // a simple way of detecting a date that has not been initialized explicitly. // type Date struct { - day int32 // day gives the number of days elapsed since date zero. + day int32 // day gives the number of days elapsed since date zero. } // PeriodOfDays describes a period of time measured in whole days. Negative values @@ -177,7 +177,7 @@ func (d Date) Weekday() time.Weekday { // Date zero, January 1, 1970, fell on a Thursday wdayZero := time.Thursday // Taking into account potential for overflow and negative offset - return time.Weekday((int32(wdayZero) + d.day % 7 + 7) % 7) + return time.Weekday((int32(wdayZero) + d.day%7 + 7) % 7) } // ISOWeek returns the ISO 8601 year and week number in which d occurs. diff --git a/date_test.go b/date_test.go index 2a385241..0b64c9c2 100644 --- a/date_test.go +++ b/date_test.go @@ -13,11 +13,11 @@ func same(d Date, t time.Time) bool { yd, wd := d.ISOWeek() yt, wt := t.ISOWeek() return d.Year() == t.Year() && - d.Month() == t.Month() && - d.Day() == t.Day() && - d.Weekday() == t.Weekday() && - d.YearDay() == t.YearDay() && - yd == yt && wd == wt + d.Month() == t.Month() && + d.Day() == t.Day() && + d.Weekday() == t.Weekday() && + d.YearDay() == t.YearDay() && + yd == yt && wd == wt } func TestNew(t *testing.T) { @@ -62,7 +62,7 @@ func TestToday(t *testing.T) { } cases := []int{-10, -5, -3, 0, 1, 4, 8, 12} for _, c := range cases { - location := time.FixedZone("zone", c * 60 * 60) + location := time.FixedZone("zone", c*60*60) today = TodayIn(location) now = time.Now().In(location) if !same(today, now) { @@ -104,7 +104,7 @@ func TestTime(t *testing.T) { t.Errorf("TimeLocal(%v) == %v, want %v", d, tLocal.Location(), time.Local) } for _, z := range zones { - location := time.FixedZone("zone", z * 60 * 60) + location := time.FixedZone("zone", z*60*60) tInLoc := d.In(location) if !same(d, tInLoc) { t.Errorf("TimeIn(%v) == %v, want date part %v", d, tInLoc, d) diff --git a/format.go b/format.go index 36de988d..cef8f1a7 100644 --- a/format.go +++ b/format.go @@ -7,8 +7,8 @@ package date import ( "fmt" "strconv" - "time" "strings" + "time" ) // These are predefined layouts for use in Date.Format and Date.Parse. @@ -21,14 +21,14 @@ import ( // so that the Parse function and Format method can apply the same // transformation to a general date value. const ( - ISO8601 = "2006-01-02" // ISO 8601 extended format + ISO8601 = "2006-01-02" // ISO 8601 extended format ISO8601B = "20060102" // ISO 8601 basic format - RFC822 = "02-Jan-06" - RFC822W = "Mon, 02-Jan-06" // RFC822 with day of the week - RFC850 = "Monday, 02-Jan-06" - RFC1123 = "02 Jan 2006" + RFC822 = "02-Jan-06" + RFC822W = "Mon, 02-Jan-06" // RFC822 with day of the week + RFC850 = "Monday, 02-Jan-06" + RFC1123 = "02 Jan 2006" RFC1123W = "Mon, 02 Jan 2006" // RFC1123 with day of the week - RFC3339 = "2006-01-02" + RFC3339 = "2006-01-02" ) // ParseISO parses an ISO 8601 formatted string and returns the date value it represents. @@ -83,7 +83,7 @@ func ParseISO(value string) (Date, error) { return Date{}, err } - month, err := parseField(value, abs[fm1 : fm2], "month", -1, 2) + month, err := parseField(value, abs[fm1:fm2], "month", -1, 2) if err != nil { return Date{}, err } @@ -201,10 +201,10 @@ func (d Date) FormatWithSuffixes(layout string, suffixes []string) string { return t.Format(layout) default: - a := make([]string, 0, 2 * len(parts) - 1) + a := make([]string, 0, 2*len(parts)-1) for i, p := range parts { if i > 0 { - a = append(a, suffixes[d.Day() - 1]) + a = append(a, suffixes[d.Day()-1]) } a = append(a, t.Format(p)) } diff --git a/format_test.go b/format_test.go index 886e6bdc..ad1edf70 100644 --- a/format_test.go +++ b/format_test.go @@ -60,18 +60,18 @@ func TestParseISO(t *testing.T) { "1234-5-6", "1234-05-6", "1234-5-06", -// "1234/05/06", -// "1234-0A-06", -// "1234-05-0B", -// "1234-05-06trailing", -// "padding1234-05-06", -// "1-02-03", -// "10-11-12", -// "100-02-03", -// "+1-02-03", -// "+10-11-12", -// "+100-02-03", -// "-123-05-06", + "1234/05/06", + "1234-0A-06", + "1234-05-0B", + "1234-05-06trailing", + "padding1234-05-06", + "1-02-03", + "10-11-12", + "100-02-03", + "+1-02-03", + "+10-11-12", + "+100-02-03", + "-123-05-06", } for _, c := range badCases { d, err := ParseISO(c) @@ -99,7 +99,7 @@ func BenchmarkParseISO(b *testing.B) { {ISO8601, "2000-05-06", 2000, time.May, 6}, } for n := 0; n < b.N; n++ { - c := cases[n % len(cases)] + c := cases[n%len(cases)] _, err := ParseISO(c.value) if err != nil { b.Errorf("ParseISO(%v) == %v", c.value, err) @@ -170,7 +170,7 @@ func BenchmarkParse(b *testing.B) { {ISO8601, "2000-05-06", 2000, time.May, 6}, } for n := 0; n < b.N; n++ { - c := cases[n % len(cases)] + c := cases[n%len(cases)] _, err := Parse(c.layout, c.value) if err != nil { b.Errorf("Parse(%v) == %v", c.value, err) diff --git a/marshal.go b/marshal.go index 4127523a..99ef338e 100644 --- a/marshal.go +++ b/marshal.go @@ -5,8 +5,8 @@ package date import ( - "fmt" "errors" + "fmt" ) // MarshalBinary implements the encoding.BinaryMarshaler interface. @@ -29,7 +29,7 @@ func (d *Date) UnmarshalBinary(data []byte) error { return errors.New("Date.UnmarshalBinary: invalid length") } - d.day = int32(data[3]) | int32(data[2]) << 8 | int32(data[1]) << 16 | int32(data[0]) << 24 + d.day = int32(data[3]) | int32(data[2])<<8 | int32(data[1])<<16 | int32(data[0])<<24 // d.decoded = time.Time{} return nil @@ -63,10 +63,10 @@ func (d Date) MarshalJSON() ([]byte, error) { func (d *Date) UnmarshalJSON(data []byte) (err error) { value := string(data) n := len(value) - if n < 2 || value[0] != '"' || value[n - 1] != '"' { + if n < 2 || value[0] != '"' || value[n-1] != '"' { return fmt.Errorf("Date.UnmarshalJSON: missing double quotes (%s)", value) } - u, err := ParseISO(value[1 : n - 1]) + u, err := ParseISO(value[1 : n-1]) if err != nil { return err } diff --git a/timespan/daterange.go b/timespan/daterange.go index 87a34fc7..0d89e204 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -5,9 +5,9 @@ package timespan import ( + "fmt" . "github.com/rickb777/date" "time" - "fmt" ) const minusOneNano time.Duration = -1 @@ -38,7 +38,7 @@ func NewDateRange(start, end Date) DateRange { // NewYearOf constructs the range encompassing the whole year specified. func NewYearOf(year int) DateRange { start := New(year, time.January, 1) - end := New(year + 1, time.January, 1) + end := New(year+1, time.January, 1) return DateRange{start, PeriodOfDays(end.Sub(start))} } @@ -46,7 +46,7 @@ func NewYearOf(year int) DateRange { // It handles leap years correctly. func NewMonthOf(year int, month time.Month) DateRange { start := New(year, month, 1) - endT := time.Date(year, month + 1, 1, 0, 0, 0, 0, time.UTC) + endT := time.Date(year, month+1, 1, 0, 0, 0, 0, time.UTC) end := NewAt(endT) return DateRange{start, PeriodOfDays(end.Sub(start))} } @@ -241,4 +241,3 @@ func (dateRange DateRange) TimeSpanIn(loc *time.Location) TimeSpan { d := dr.DurationIn(loc) return TimeSpan{s, d} } - diff --git a/timespan/daterange_test.go b/timespan/daterange_test.go index 2080e515..bab80467 100644 --- a/timespan/daterange_test.go +++ b/timespan/daterange_test.go @@ -5,12 +5,12 @@ package timespan import ( + "fmt" . "github.com/rickb777/date" + "runtime/debug" + "strings" "testing" "time" - "fmt" - "strings" - "runtime/debug" ) var d0320 = New(2015, time.March, 20) @@ -134,8 +134,8 @@ func xTestContains1(t *testing.T) { dr := OneDayRange(d0326).ExtendBy(1) isEq(t, dr.Contains(d0320), false, dr, d0320) isEq(t, dr.Contains(d0325), false, dr, d0325) - isEq(t, dr.Contains(d0326), true, dr, d0326) - isEq(t, dr.Contains(d0327), true, dr, d0327) + isEq(t, dr.Contains(d0326), true, dr, d0326) + isEq(t, dr.Contains(d0327), true, dr, d0327) isEq(t, dr.Contains(d0328), false, dr, d0328) isEq(t, dr.Contains(d0401), false, dr, d0401) isEq(t, dr.Contains(d0410), false, dr, d0410) @@ -148,7 +148,7 @@ func xTestContains2(t *testing.T) { time.Local = time.FixedZone("Test", 7200) dr := OneDayRange(d0326) isEq(t, dr.Contains(d0325), false, dr, d0325) - isEq(t, dr.Contains(d0326), true, dr, d0326) + isEq(t, dr.Contains(d0326), true, dr, d0326) isEq(t, dr.Contains(d0327), false, dr, d0327) time.Local = old } @@ -211,14 +211,14 @@ func xTestMergeNonOverlapping(t *testing.T) { func xTestDurationNormalUTC(t *testing.T) { dr := OneDayRange(d0329) - isEq(t, dr.Duration(), time.Hour * 24) + isEq(t, dr.Duration(), time.Hour*24) } func xTestDurationInZoneWithDaylightSaving(t *testing.T) { - isEq(t, OneDayRange(d0328).DurationIn(london), time.Hour * 24) - isEq(t, OneDayRange(d0329).DurationIn(london), time.Hour * 23) - isEq(t, OneDayRange(d1025).DurationIn(london), time.Hour * 25) - isEq(t, NewDateRange(d0328, d0330).DurationIn(london), time.Hour * 71) + isEq(t, OneDayRange(d0328).DurationIn(london), time.Hour*24) + isEq(t, OneDayRange(d0329).DurationIn(london), time.Hour*23) + isEq(t, OneDayRange(d1025).DurationIn(london), time.Hour*25) + isEq(t, NewDateRange(d0328, d0330).DurationIn(london), time.Hour*71) } func isEq(t *testing.T, a, b interface{}, msg ...interface{}) { diff --git a/timespan/timespan.go b/timespan/timespan.go index e3a851f0..add536e9 100644 --- a/timespan/timespan.go +++ b/timespan/timespan.go @@ -5,12 +5,13 @@ package timespan import ( - "time" "fmt" "github.com/rickb777/date" + "time" ) const TimestampFormat = "2006-01-02 15:04:05" + //const ISOFormat = "2006-01-02T15:04:05" // TimeSpan holds a span of time between two instants with a 1 nanosecond resolution. diff --git a/timespan/timespan_test.go b/timespan/timespan_test.go index f7222788..da416e20 100644 --- a/timespan/timespan_test.go +++ b/timespan/timespan_test.go @@ -31,12 +31,12 @@ func TestNewTimeSpan(t *testing.T) { ts2 := NewTimeSpan(t0327, t0328) isEq(t, ts2.mark, t0327) - isEq(t, ts2.Duration(), time.Hour * 24) + isEq(t, ts2.Duration(), time.Hour*24) isEq(t, ts2.End(), t0328) ts3 := NewTimeSpan(t0329, t0327) isEq(t, ts3.mark, t0327) - isEq(t, ts3.Duration(), time.Hour * 48) + isEq(t, ts3.Duration(), time.Hour*48) isEq(t, ts3.End(), t0329) } @@ -54,31 +54,31 @@ func TestTSEnd(t *testing.T) { func TestTSShiftBy(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328).ShiftBy(time.Hour * 24) isEq(t, ts1.mark, t0328) - isEq(t, ts1.Duration(), time.Hour * 24) + isEq(t, ts1.Duration(), time.Hour*24) isEq(t, ts1.End(), t0329) ts2 := NewTimeSpan(t0328, t0329).ShiftBy(-time.Hour * 24) isEq(t, ts2.mark, t0327) - isEq(t, ts2.Duration(), time.Hour * 24) + isEq(t, ts2.Duration(), time.Hour*24) isEq(t, ts2.End(), t0328) } func TestTSExtendBy(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328).ExtendBy(time.Hour * 24) isEq(t, ts1.mark, t0327) - isEq(t, ts1.Duration(), time.Hour * 48) + isEq(t, ts1.Duration(), time.Hour*48) isEq(t, ts1.End(), t0329) ts2 := NewTimeSpan(t0328, t0329).ExtendBy(-time.Hour * 48) isEq(t, ts2.mark, t0327) - isEq(t, ts2.Duration(), time.Hour * 24) + isEq(t, ts2.Duration(), time.Hour*24) isEq(t, ts2.End(), t0328) } func TestTSExtendWithoutWrapping(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328).ExtendWithoutWrapping(time.Hour * 24) isEq(t, ts1.mark, t0327) - isEq(t, ts1.Duration(), time.Hour * 48) + isEq(t, ts1.Duration(), time.Hour*48) isEq(t, ts1.End(), t0329) ts2 := NewTimeSpan(t0328, t0329).ExtendWithoutWrapping(-time.Hour * 48) @@ -171,22 +171,21 @@ func xTestConversion1(t *testing.T) { func xTestConversion2(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328) dr := ts1.DateRangeIn(time.UTC) -// ts2 := dr.TimeSpanIn(time.UTC) + // ts2 := dr.TimeSpanIn(time.UTC) isEq(t, dr.Start, d0327) isEq(t, dr.End, d0328) -// isEq(t, ts1, ts2) - isEq(t, ts1.Duration(), time.Hour * 24) + // isEq(t, ts1, ts2) + isEq(t, ts1.Duration(), time.Hour*24) } func xTestConversion3(t *testing.T) { dr1 := NewDateRange(d0327, d0330) // weekend of clocks changing ts1 := dr1.TimeSpanIn(london) dr2 := ts1.DateRangeIn(london) -// ts2 := dr2.TimeSpanIn(london) + // ts2 := dr2.TimeSpanIn(london) isEq(t, dr1.Start, d0327) isEq(t, dr1.End, d0330) isEq(t, dr1, dr2) -// isEq(t, ts1, ts2) - isEq(t, ts1.Duration(), time.Hour * 71) + // isEq(t, ts1, ts2) + isEq(t, ts1.Duration(), time.Hour*71) } - From a36e621c7f545c6030be62497d60a1f92c52b663 Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 1 Dec 2015 23:06:38 +0000 Subject: [PATCH 019/165] Date now has Min and Max. DateRange Merge is now much simpler as a consequence. Some bugs fixed relating to Last() and End() (formerly called End() and Next()). --- date.go | 16 +++++++ date_test.go | 89 +++++++++++++++++++---------------- timespan/daterange.go | 95 ++++++++++++++++++-------------------- timespan/daterange_test.go | 90 +++++++++++++++++++++--------------- timespan/timespan.go | 11 ++++- timespan/timespan_test.go | 55 ++++++++++++---------- 6 files changed, 206 insertions(+), 150 deletions(-) diff --git a/date.go b/date.go index fa936597..03e457d0 100644 --- a/date.go +++ b/date.go @@ -209,6 +209,22 @@ func (d Date) After(u Date) bool { return d.day > u.day } +// Max returns the earlier of two dates. +func (d Date) Min(u Date) Date { + if d.day > u.day { + return u + } + return d +} + +// Max returns the later of two dates. +func (d Date) Max(u Date) Date { + if d.day < u.day { + return u + } + return d +} + // Add returns the date d plus the given number of days. The parameter may be negative. func (d Date) Add(days PeriodOfDays) Date { return Date{d.day + int32(days)} diff --git a/date_test.go b/date_test.go index 0b64c9c2..312e4ec9 100644 --- a/date_test.go +++ b/date_test.go @@ -7,17 +7,18 @@ package date import ( "testing" "time" + "runtime/debug" ) func same(d Date, t time.Time) bool { yd, wd := d.ISOWeek() yt, wt := t.ISOWeek() return d.Year() == t.Year() && - d.Month() == t.Month() && - d.Day() == t.Day() && - d.Weekday() == t.Weekday() && - d.YearDay() == t.YearDay() && - yd == yt && wd == wt + d.Month() == t.Month() && + d.Day() == t.Day() && + d.Weekday() == t.Weekday() && + d.YearDay() == t.YearDay() && + yd == yt && wd == wt } func TestNew(t *testing.T) { @@ -62,7 +63,7 @@ func TestToday(t *testing.T) { } cases := []int{-10, -5, -3, 0, 1, 4, 8, 12} for _, c := range cases { - location := time.FixedZone("zone", c*60*60) + location := time.FixedZone("zone", c * 60 * 60) today = TodayIn(location) now = time.Now().In(location) if !same(today, now) { @@ -104,7 +105,7 @@ func TestTime(t *testing.T) { t.Errorf("TimeLocal(%v) == %v, want %v", d, tLocal.Location(), time.Local) } for _, z := range zones { - location := time.FixedZone("zone", z*60*60) + location := time.FixedZone("zone", z * 60 * 60) tInLoc := d.In(location) if !same(d, tInLoc) { t.Errorf("TimeIn(%v) == %v, want date part %v", d, tInLoc, d) @@ -136,31 +137,11 @@ func TestPredicates(t *testing.T) { di := New(ci.year, ci.month, ci.day) for j, cj := range cases { dj := New(cj.year, cj.month, cj.day) - p := di.Equal(dj) - q := i == j - if p != q { - t.Errorf("Equal(%v, %v) == %v, want %v", di, dj, p, q) - } - p = di.Before(dj) - q = i < j - if p != q { - t.Errorf("Before(%v, %v) == %v, want %v", di, dj, p, q) - } - p = di.After(dj) - q = i > j - if p != q { - t.Errorf("After(%v, %v) == %v, want %v", di, dj, p, q) - } - p = di == dj - q = i == j - if p != q { - t.Errorf("Equal(%v, %v) == %v, want %v", di, dj, p, q) - } - p = di != dj - q = i != j - if p != q { - t.Errorf("Equal(%v, %v) == %v, want %v", di, dj, p, q) - } + testPredicate(t, di, dj, di.Equal(dj), i == j, "Equal") + testPredicate(t, di, dj, di.Before(dj), i < j, "Before") + testPredicate(t, di, dj, di.After(dj), i > j, "After") + testPredicate(t, di, dj, di == dj, i == j, "==") + testPredicate(t, di, dj, di != dj, i != j, "!=") } } @@ -175,6 +156,12 @@ func TestPredicates(t *testing.T) { } } +func testPredicate(t *testing.T, di, dj Date, p, q bool, m string) { + if p != q { + t.Errorf("%s(%v, %v) == %v, want %v\n%v", m, di, dj, p, q, debug.Stack()) + } +} + func TestArithmetic(t *testing.T) { cases := []struct { year int @@ -192,17 +179,41 @@ func TestArithmetic(t *testing.T) { } offsets := []PeriodOfDays{-1000000, -9999, -555, -99, -22, -1, 0, 1, 22, 99, 555, 9999, 1000000} for _, c := range cases { - d := New(c.year, c.month, c.day) + di := New(c.year, c.month, c.day) for _, days := range offsets { - d2 := d.Add(days) - days2 := d2.Sub(d) + dj := di.Add(days) + days2 := dj.Sub(di) if days2 != days { - t.Errorf("AddSub(%v,%v) == %v, want %v", d, days, days2, days) + t.Errorf("AddSub(%v,%v) == %v, want %v", di, days, days2, days) } - d3 := d2.Add(-days) - if d3 != d { - t.Errorf("AddNeg(%v,%v) == %v, want %v", d, days, d3, d) + d3 := dj.Add(-days) + if d3 != di { + t.Errorf("AddNeg(%v,%v) == %v, want %v", di, days, d3, di) + } + eMin1 := min(di.day, dj.day) + aMin1 := di.Min(dj) + if aMin1.day != eMin1 { + t.Errorf("%v.Max(%v) is %s", di, dj, aMin1) + } + eMax1 := max(di.day, dj.day) + aMax1 := di.Max(dj) + if aMax1.day != eMax1 { + t.Errorf("%v.Max(%v) is %s", di, dj, aMax1) } } } } + +func min(a, b int32) int32 { + if a < b { + return a + } + return b +} + +func max(a, b int32) int32 { + if a > b { + return a + } + return b +} diff --git a/timespan/daterange.go b/timespan/daterange.go index 0d89e204..79242c75 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -27,18 +27,17 @@ func NewDateRangeOf(start time.Time, duration time.Duration) DateRange { return DateRange{sd, PeriodOfDays(ed.Sub(sd))} } -// NewDateRange assembles a new date range from two dates. +// NewDateRange 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). func NewDateRange(start, end Date) DateRange { - if end.Before(start) { - return DateRange{start, PeriodOfDays(end.Sub(start) - 1)} - } - return DateRange{start, PeriodOfDays(end.Sub(start) + 1)} + return DateRange{start, PeriodOfDays(end.Sub(start))}.Normalise() } // NewYearOf constructs the range encompassing the whole year specified. func NewYearOf(year int) DateRange { start := New(year, time.January, 1) - end := New(year+1, time.January, 1) + end := New(year + 1, time.January, 1) return DateRange{start, PeriodOfDays(end.Sub(start))} } @@ -46,7 +45,7 @@ func NewYearOf(year int) DateRange { // It handles leap years correctly. func NewMonthOf(year int, month time.Month) DateRange { start := New(year, month, 1) - endT := time.Date(year, month+1, 1, 0, 0, 0, 0, time.UTC) + endT := time.Date(year, month + 1, 1, 0, 0, 0, 0, time.UTC) end := NewAt(endT) return DateRange{start, PeriodOfDays(end.Sub(start))} } @@ -68,6 +67,11 @@ func (dateRange DateRange) Days() PeriodOfDays { return dateRange.days } +// IsEmpty returns true if this is an empty range (zero days). +func (dateRange DateRange) IsEmpty() bool { + return dateRange.days == 0 +} + // Start returns the earliest date represented by this range. func (dateRange DateRange) Start() Date { if dateRange.days < 0 { @@ -76,24 +80,28 @@ func (dateRange DateRange) Start() Date { return dateRange.mark } -// End returns the latest date (inclusive) represented by this range. If the range is empty (i.e. -// has zero days), then an empty date is returned. -func (dateRange DateRange) End() Date { +// 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(). +// See also IsEmpty(). +func (dateRange DateRange) Last() Date { if dateRange.days < 0 { - return dateRange.mark + return dateRange.mark // because mark is at the end } else if dateRange.days == 0 { return Date{} } return dateRange.mark.Add(dateRange.days - 1) } -// Next returns the date that follows the end date of the range. If the range is empty (i.e. -// has zero days), then an empty date is returned. -func (dateRange DateRange) Next() Date { +// 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. +func (dateRange DateRange) End() Date { if dateRange.days < 0 { - return dateRange.mark.Add(1) - } else if dateRange.days == 0 { - return Date{} + return dateRange.mark.Add(1) // because mark is at the end } return dateRange.mark.Add(dateRange.days) } @@ -128,6 +136,7 @@ func (dateRange DateRange) ExtendBy(days PeriodOfDays) DateRange { return DateRange{dateRange.mark, dateRange.days + days} } +// String describes the date range in human-readable form. func (dateRange DateRange) String() string { switch dateRange.days { case 0: @@ -136,9 +145,9 @@ func (dateRange DateRange) String() string { return fmt.Sprintf("1 day on %s", dateRange.mark) default: if dateRange.days < 0 { - return fmt.Sprintf("%d days from %s to %s", -dateRange.days, dateRange.Start(), dateRange.End()) + return fmt.Sprintf("%d days from %s to %s", -dateRange.days, dateRange.Start(), dateRange.Last()) } - return fmt.Sprintf("%d days from %s to %s", dateRange.days, dateRange.Start(), dateRange.End()) + return fmt.Sprintf("%d days from %s to %s", dateRange.days, dateRange.Start(), dateRange.Last()) } } @@ -148,7 +157,7 @@ func (dateRange DateRange) Contains(d Date) bool { if dateRange.days == 0 { return false } - return !(d.Before(dateRange.Start()) || d.After(dateRange.End())) + return !(d.Before(dateRange.Start()) || d.After(dateRange.Last())) } // StartUTC assumes that the start date is a UTC date and gets the start time of that date, as UTC. @@ -161,7 +170,7 @@ func (dateRange DateRange) StartUTC() time.Time { // in a specified location. Along with StartUTC, this gives a 'half-open' range where the start // is inclusive and the end is exclusive. func (dateRange DateRange) EndUTC() time.Time { - return dateRange.Next().UTC() + return dateRange.End().UTC() } // ContainsTime tests whether a given local time is within the date range. The time range is @@ -178,42 +187,31 @@ func (dateRange DateRange) ContainsTime(t time.Time) bool { return !(utc.Before(dateRange.StartUTC()) || dateRange.EndUTC().Add(minusOneNano).Before(utc)) } -// Merge combines two date ranges by calculating a date range that just encompasses them both. +// Merge combines two date ranges by calculating a date range that just encompasses them both. // As a special case, if one range is entirely contained within the other range, the larger of -// the two is returned. Otherwise, the result is the start of the earlier one to the end of the -// later one, even if the two ranges don't overlap. -func (dateRange DateRange) Merge(other DateRange) DateRange { - start := dateRange.Start() - if start.After(other.Start()) { - // swap the ranges to simplify the logic - return other.Merge(dateRange) - - } else { - oEnd := other.End() - if dateRange.End().After(oEnd) { - // other is a proper subrange of dateRange - return dateRange - - } else { - return NewDateRange(start, oEnd) - } - } +// the two is returned. Otherwise, the result is from the start of the earlier one to the end of +// the later one, even if the two ranges don't overlap. +func (thisRange DateRange) Merge(thatRange DateRange) DateRange { + minStart := thisRange.Start().Min(thatRange.Start()) + maxEnd := thisRange.End().Max(thatRange.End()) + return NewDateRange(minStart, maxEnd) } // Duration computes the duration (in nanoseconds) from midnight at the start of the date -// range up to and including the very last nanosecond before midnight the following day after the end. +// range up to and including the very last nanosecond before midnight on the end day. // The calculation is for UTC, which does not have daylight saving and every day has 24 hours. // // 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.Next().UTC().Sub(dateRange.Start().UTC()) + return dateRange.End().UTC().Sub(dateRange.Start().UTC()) } // DurationIn computes the duration (in nanoseconds) from midnight at the start of the date -// range up to and including the very last nanosecond before midnight the following day after the end. -// The calculation is for the specified location, which may have daylight saving, so not every day has -// 24 hours. If the date range spans the day the clocks are changed, this is taken into account. +// range up to and including the very last nanosecond before midnight on the end day. +// The calculation is for the specified location, which may have daylight saving, so not every day +// necessarily has 24 hours. If the date range spans the day the clocks are changed, this is +// taken into account. // // 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)). @@ -230,14 +228,13 @@ func (dateRange DateRange) StartTimeIn(loc *time.Location) time.Time { // StartTimeIn, this gives a 'half-open' range where the start is inclusive and the end is // exclusive. func (dateRange DateRange) EndTimeIn(loc *time.Location) time.Time { - return dateRange.Next().In(loc) + return dateRange.End().In(loc) } // TimeSpanIn obtains the time span corresponding to the date range in a specified location. // The result is normalised. func (dateRange DateRange) TimeSpanIn(loc *time.Location) TimeSpan { - dr := dateRange.Normalise() - s := dr.StartTimeIn(loc) - d := dr.DurationIn(loc) + s := dateRange.StartTimeIn(loc) + d := dateRange.DurationIn(loc) return TimeSpan{s, d} } diff --git a/timespan/daterange_test.go b/timespan/daterange_test.go index bab80467..215351b6 100644 --- a/timespan/daterange_test.go +++ b/timespan/daterange_test.go @@ -20,9 +20,11 @@ var d0327 = New(2015, time.March, 27) var d0328 = New(2015, time.March, 28) var d0329 = New(2015, time.March, 29) // n.b. clocks go forward (UK) var d0330 = New(2015, time.March, 30) +var d0331 = New(2015, time.March, 31) var d0401 = New(2015, time.April, 1) var d0402 = New(2015, time.April, 2) var d0403 = New(2015, time.April, 3) +var d0407 = New(2015, time.April, 7) var d0408 = New(2015, time.April, 8) var d0410 = New(2015, time.April, 10) var d0501 = New(2015, time.May, 1) @@ -42,32 +44,35 @@ func TestNewDateRangeOf(t *testing.T) { dr := NewDateRangeOf(t0327, time.Duration(7*24*60*60*1e9)) isEq(t, dr.mark, d0327) isEq(t, dr.Days(), PeriodOfDays(7)) + isEq(t, dr.IsEmpty(), false) isEq(t, dr.Start(), d0327) - isEq(t, dr.End(), d0402) - isEq(t, dr.Next(), d0403) + isEq(t, dr.Last(), d0402) + isEq(t, dr.End(), d0403) } func TestNewDateRangeWithNormalise(t *testing.T) { - r1 := NewDateRange(d0327, d0401) + r1 := NewDateRange(d0327, d0402) isEq(t, r1.Start(), d0327) - isEq(t, r1.End(), d0401) - isEq(t, r1.Next(), d0402) + isEq(t, r1.Last(), d0401) + isEq(t, r1.End(), d0402) - r2 := NewDateRange(d0401, d0327) + r2 := NewDateRange(d0402, d0327) isEq(t, r2.Start(), d0327) - isEq(t, r2.End(), d0401) - isEq(t, r2.Next(), d0402) + isEq(t, r2.Last(), d0401) + isEq(t, r2.End(), d0402) } func TestOneDayRange(t *testing.T) { drN0 := DateRange{d0327, -1} isEq(t, drN0.Days(), PeriodOfDays(-1)) + isEq(t, drN0.IsEmpty(), false) isEq(t, drN0.Start(), d0327) - isEq(t, drN0.End(), d0327) + isEq(t, drN0.Last(), d0327) isEq(t, drN0.String(), "1 day on 2015-03-27") dr0 := DateRange{} isEq(t, dr0.Days(), PeriodOfDays(0)) + isEq(t, dr0.IsEmpty(), true) isEq(t, dr0.String(), "0 days from 1970-01-01") dr1 := OneDayRange(Date{}) @@ -75,8 +80,8 @@ func TestOneDayRange(t *testing.T) { dr2 := OneDayRange(d0327) isEq(t, dr2.Start(), d0327) - isEq(t, dr2.End(), d0327) - isEq(t, dr2.Next(), d0328) + isEq(t, dr2.Last(), d0327) + isEq(t, dr2.End(), d0328) isEq(t, dr2.Days(), PeriodOfDays(1)) isEq(t, dr2.String(), "1 day on 2015-03-27") } @@ -85,38 +90,38 @@ func TestNewYearOf(t *testing.T) { dr := NewYearOf(2015) isEq(t, dr.Days(), PeriodOfDays(365)) isEq(t, dr.Start(), New(2015, time.January, 1)) - isEq(t, dr.End(), New(2015, time.December, 31)) - isEq(t, dr.Next(), New(2016, time.January, 1)) + isEq(t, dr.Last(), New(2015, time.December, 31)) + isEq(t, dr.End(), New(2016, time.January, 1)) } func TestNewMonthOf(t *testing.T) { dr := NewMonthOf(2015, time.February) isEq(t, dr.Days(), PeriodOfDays(28)) isEq(t, dr.Start(), New(2015, time.February, 1)) - isEq(t, dr.End(), New(2015, time.February, 28)) - isEq(t, dr.Next(), New(2015, time.March, 1)) + isEq(t, dr.Last(), New(2015, time.February, 28)) + isEq(t, dr.End(), New(2015, time.March, 1)) } func TestShiftByPos(t *testing.T) { - dr := NewDateRange(d0327, d0401).ShiftBy(7) + dr := NewDateRange(d0327, d0402).ShiftBy(7) isEq(t, dr.Days(), PeriodOfDays(6)) isEq(t, dr.Start(), d0403) - isEq(t, dr.End(), d0408) + isEq(t, dr.Last(), d0408) } func TestShiftByNeg(t *testing.T) { dr := NewDateRange(d0403, d0408).ShiftBy(-7) - isEq(t, dr.Days(), PeriodOfDays(6)) + isEq(t, dr.Days(), PeriodOfDays(5)) isEq(t, dr.Start(), d0327) - isEq(t, dr.End(), d0401) + isEq(t, dr.Last(), d0331) } func TestExtendByPos(t *testing.T) { dr := OneDayRange(d0327).ExtendBy(6) isEq(t, dr.Days(), PeriodOfDays(7)) isEq(t, dr.Start(), d0327) - isEq(t, dr.End(), d0402) - isEq(t, dr.Next(), d0403) + isEq(t, dr.Last(), d0402) + isEq(t, dr.End(), d0403) isEq(t, dr.String(), "7 days from 2015-03-27 to 2015-04-02") } @@ -124,11 +129,11 @@ func TestExtendByNeg(t *testing.T) { dr := OneDayRange(d0327).ExtendBy(-9) isEq(t, dr.Days(), PeriodOfDays(-8)) isEq(t, dr.Start(), d0320) - isEq(t, dr.End(), d0327) + isEq(t, dr.Last(), d0327) isEq(t, dr.String(), "8 days from 2015-03-20 to 2015-03-27") } -func xTestContains1(t *testing.T) { +func TestContains1(t *testing.T) { old := time.Local time.Local = time.FixedZone("Test", 7200) dr := OneDayRange(d0326).ExtendBy(1) @@ -143,7 +148,7 @@ func xTestContains1(t *testing.T) { time.Local = old } -func xTestContains2(t *testing.T) { +func TestContains2(t *testing.T) { old := time.Local time.Local = time.FixedZone("Test", 7200) dr := OneDayRange(d0326) @@ -153,7 +158,7 @@ func xTestContains2(t *testing.T) { time.Local = old } -func xTestContainsTimeUTC(t *testing.T) { +func TestContainsTimeUTC(t *testing.T) { old := time.Local time.Local = time.FixedZone("Test", 7200) t0328e := time.Date(2015, 3, 28, 23, 59, 59, 999999999, time.UTC) @@ -169,56 +174,69 @@ func xTestContainsTimeUTC(t *testing.T) { time.Local = old } -func xTestMerge1(t *testing.T) { +func TestMerge1(t *testing.T) { dr1 := OneDayRange(d0327).ExtendBy(1) dr2 := OneDayRange(d0327).ExtendBy(7) m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) isEq(t, m1.Start(), d0327) - isEq(t, m1.End(), d0403) + isEq(t, m1.Last(), d0403) isEq(t, m1, m2) } -func xTestMerge2(t *testing.T) { +func TestMerge2(t *testing.T) { dr1 := OneDayRange(d0327).ExtendBy(1).ShiftBy(1) dr2 := OneDayRange(d0327).ExtendBy(7) m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) isEq(t, m1.Start(), d0327) - isEq(t, m1.End(), d0403) + isEq(t, m1.Last(), d0403) isEq(t, m1, m2) } -func xTestMergeOverlapping(t *testing.T) { +func TestMergeOverlapping(t *testing.T) { dr1 := OneDayRange(d0320).ExtendBy(12) - dr2 := OneDayRange(d0401).ExtendBy(7) + dr2 := OneDayRange(d0401).ExtendBy(6) m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) isEq(t, m1.Start(), d0320) + isEq(t, m1.Last(), d0407) isEq(t, m1.End(), d0408) isEq(t, m1, m2) } -func xTestMergeNonOverlapping(t *testing.T) { +func TestMergeNonOverlapping(t *testing.T) { dr1 := OneDayRange(d0320).ExtendBy(2) - dr2 := OneDayRange(d0401).ExtendBy(7) + dr2 := OneDayRange(d0401).ExtendBy(6) + m1 := dr1.Merge(dr2) + m2 := dr2.Merge(dr1) + isEq(t, m1.Start(), d0320) + isEq(t, m1.Last(), d0407) + isEq(t, m1.End(), d0408) + isEq(t, m1, m2) +} + +func TestMergeEmpties(t *testing.T) { + dr1 := ZeroRange(d0320) + dr2 := ZeroRange(d0408) // curiously, this is *not* included because it has no size. m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) isEq(t, m1.Start(), d0320) + isEq(t, m1.Last(), d0407) isEq(t, m1.End(), d0408) isEq(t, m1, m2) } -func xTestDurationNormalUTC(t *testing.T) { +func TestDurationNormalUTC(t *testing.T) { dr := OneDayRange(d0329) isEq(t, dr.Duration(), time.Hour*24) } -func xTestDurationInZoneWithDaylightSaving(t *testing.T) { +func TestDurationInZoneWithDaylightSaving(t *testing.T) { isEq(t, OneDayRange(d0328).DurationIn(london), time.Hour*24) isEq(t, OneDayRange(d0329).DurationIn(london), time.Hour*23) isEq(t, OneDayRange(d1025).DurationIn(london), time.Hour*25) - isEq(t, NewDateRange(d0328, d0330).DurationIn(london), time.Hour*71) + isEq(t, NewDateRange(d0328, d0331).DurationIn(london), time.Hour*71) } func isEq(t *testing.T, a, b interface{}, msg ...interface{}) { diff --git a/timespan/timespan.go b/timespan/timespan.go index add536e9..7fa6e0f5 100644 --- a/timespan/timespan.go +++ b/timespan/timespan.go @@ -27,7 +27,8 @@ func ZeroTimeSpan(start time.Time) TimeSpan { } // NewTimeSpan creates a new time span from two times. The start and end can be in either -// order; the result will be normalised. +// 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 { if t2.Before(t1) { return TimeSpan{t2, t1.Sub(t2)} @@ -44,7 +45,7 @@ func (ts TimeSpan) Start() time.Time { } // End gets the end time of the time span. Strictly, this is one nanosecond after the -// range of time included in the time span. +// range of time included in the time span; this implements the half-open model. func (ts TimeSpan) End() time.Time { if ts.duration < 0 { return ts.mark @@ -57,6 +58,11 @@ func (ts TimeSpan) Duration() time.Duration { return ts.duration } +// IsEmpty returns true if this is an empty time span (zero duration). +func (ts TimeSpan) IsEmpty() bool { + return ts.duration == 0 +} + // Normalise ensures that the mark time is at the start time and the duration is positive. // The normalised time span is returned. func (ts TimeSpan) Normalise() TimeSpan { @@ -90,6 +96,7 @@ func (ts TimeSpan) ExtendWithoutWrapping(d time.Duration) TimeSpan { return TimeSpan{tsn.mark, tsn.duration + d} } +// String produces a human-readable description of a time span. func (ts TimeSpan) String() string { return fmt.Sprintf("%s from %s to %s", ts.duration, ts.mark.Format(TimestampFormat), ts.End().Format(TimestampFormat)) } diff --git a/timespan/timespan_test.go b/timespan/timespan_test.go index da416e20..2aaabf5b 100644 --- a/timespan/timespan_test.go +++ b/timespan/timespan_test.go @@ -7,6 +7,7 @@ package timespan import ( "testing" "time" + "github.com/rickb777/date" ) const zero time.Duration = 0 @@ -27,16 +28,19 @@ func TestNewTimeSpan(t *testing.T) { ts1 := NewTimeSpan(t0327, t0327) isEq(t, ts1.mark, t0327) isEq(t, ts1.Duration(), zero) + isEq(t, ts1.IsEmpty(), true) isEq(t, ts1.End(), t0327) ts2 := NewTimeSpan(t0327, t0328) isEq(t, ts2.mark, t0327) - isEq(t, ts2.Duration(), time.Hour*24) + isEq(t, ts2.Duration(), time.Hour * 24) + isEq(t, ts2.IsEmpty(), false) isEq(t, ts2.End(), t0328) ts3 := NewTimeSpan(t0329, t0327) isEq(t, ts3.mark, t0327) - isEq(t, ts3.Duration(), time.Hour*48) + isEq(t, ts3.Duration(), time.Hour * 48) + isEq(t, ts3.IsEmpty(), false) isEq(t, ts3.End(), t0329) } @@ -54,31 +58,31 @@ func TestTSEnd(t *testing.T) { func TestTSShiftBy(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328).ShiftBy(time.Hour * 24) isEq(t, ts1.mark, t0328) - isEq(t, ts1.Duration(), time.Hour*24) + isEq(t, ts1.Duration(), time.Hour * 24) isEq(t, ts1.End(), t0329) ts2 := NewTimeSpan(t0328, t0329).ShiftBy(-time.Hour * 24) isEq(t, ts2.mark, t0327) - isEq(t, ts2.Duration(), time.Hour*24) + isEq(t, ts2.Duration(), time.Hour * 24) isEq(t, ts2.End(), t0328) } func TestTSExtendBy(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328).ExtendBy(time.Hour * 24) isEq(t, ts1.mark, t0327) - isEq(t, ts1.Duration(), time.Hour*48) + isEq(t, ts1.Duration(), time.Hour * 48) isEq(t, ts1.End(), t0329) ts2 := NewTimeSpan(t0328, t0329).ExtendBy(-time.Hour * 48) isEq(t, ts2.mark, t0327) - isEq(t, ts2.Duration(), time.Hour*24) + isEq(t, ts2.Duration(), time.Hour * 24) isEq(t, ts2.End(), t0328) } func TestTSExtendWithoutWrapping(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328).ExtendWithoutWrapping(time.Hour * 24) isEq(t, ts1.mark, t0327) - isEq(t, ts1.Duration(), time.Hour*48) + isEq(t, ts1.Duration(), time.Hour * 48) isEq(t, ts1.End(), t0329) ts2 := NewTimeSpan(t0328, t0329).ExtendWithoutWrapping(-time.Hour * 48) @@ -148,7 +152,7 @@ func TestTSMergeOverlapping(t *testing.T) { isEq(t, m1, m2) } -func xTestTSMergeNonOverlapping(t *testing.T) { +func TestTSMergeNonOverlapping(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328) ts2 := NewTimeSpan(t0329, t0330) m1 := ts1.Merge(ts2) @@ -158,34 +162,37 @@ func xTestTSMergeNonOverlapping(t *testing.T) { isEq(t, m1, m2) } -func xTestConversion1(t *testing.T) { +func TestConversion1(t *testing.T) { ts1 := ZeroTimeSpan(t0327) dr := ts1.DateRangeIn(time.UTC) ts2 := dr.TimeSpanIn(time.UTC) - isEq(t, dr.Start, d0327) - isEq(t, dr.End, d0327) - isEq(t, ts1, ts2) + isEq(t, dr.Start(), d0327) + isEq(t, dr.IsEmpty(), true) + isEq(t, ts1.Start(), ts1.End()) isEq(t, ts1.Duration(), zero) + isEq(t, dr.Days(), date.PeriodOfDays(0)) + isEq(t, ts2.Duration(), zero) + isEq(t, ts1, ts2) } -func xTestConversion2(t *testing.T) { +func TestConversion2(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328) dr := ts1.DateRangeIn(time.UTC) - // ts2 := dr.TimeSpanIn(time.UTC) - isEq(t, dr.Start, d0327) - isEq(t, dr.End, d0328) - // isEq(t, ts1, ts2) - isEq(t, ts1.Duration(), time.Hour*24) + ts2 := dr.TimeSpanIn(time.UTC) + isEq(t, dr.Start(), d0327) + isEq(t, dr.End(), d0328) + isEq(t, ts1, ts2) + isEq(t, ts1.Duration(), time.Hour * 24) } -func xTestConversion3(t *testing.T) { +func TestConversion3(t *testing.T) { dr1 := NewDateRange(d0327, d0330) // weekend of clocks changing ts1 := dr1.TimeSpanIn(london) dr2 := ts1.DateRangeIn(london) - // ts2 := dr2.TimeSpanIn(london) - isEq(t, dr1.Start, d0327) - isEq(t, dr1.End, d0330) + ts2 := dr2.TimeSpanIn(london) + isEq(t, dr1.Start(), d0327) + isEq(t, dr1.End(), d0330) isEq(t, dr1, dr2) - // isEq(t, ts1, ts2) - isEq(t, ts1.Duration(), time.Hour*71) + isEq(t, ts1, ts2) + isEq(t, ts1.Duration(), time.Hour * 71) } From cf5307345e986de9e7d842a48cd6f9278f604a5a Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 3 Dec 2015 13:15:04 +0000 Subject: [PATCH 020/165] Commented out some non-functioning travis settings --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 878e3a0c..88a1504d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ install: script: - go test -v -covermode=count -coverprofile=coverage.out ./... - go tool cover -func=coverage.out - - $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN +# - $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN -env: - secure: "kcksCWXVeZKmFUWcyi2S/j87iwUmXMxZXxA2DG9ymc11QP43QoPNSG9pBjA/DDjvzt4WdKIFphTrxVfvawii/9j3oXA1aPmAcHGu87i4iOVg4IIZ4bPZLfUo0e7s6XP5FakzegYvPP6HWV5Xr5h+Q6osrjq3czOnPY+rVII6MRrxXMOfsqo8HEER+YIOOD6vj5LV2/quY8d0XHtThqgGvQ1cz4OB3vbd4KFBl48kmfXKefTrRG1NoqoQMMpwUVzU395JIEAg1eWbGkquhWU5v13gRwk3VMVWF75jZna8TSiqWha0P5iQdaED30kNCz3poIaBI1MLdxktJxwUQJZ5AaYIMCxh7ZCiW0FXTYCRu3EoeYusTPMLqy1ghK+gIlA46sNd26cKk5/OngXRrHo/J0aF5NWjydlk5FLHfKm9ih/Y426M9nV2zYNQAcVKgO8zVNb2IkJ3e7aTB2NH4DpkvjSV4D4hlnmW9xxmo14TKF+gXJ9Hw9ssKbigRHoL6S92aQHcpkdjGGnI5YSTy1fZh/nIE3HDmx+hcK4/ZtPHj9KnXopKYxBGyNswN5Eko+q6h3BB/Q7LwALtEexdDbznwsRmcZXJsOU4chvcjAKgIAi6cbeTwq8kG5E/w8TTY2wGeRm2ZFysQu6Jf8hgTZDQTV343RG80STWeUbiD0o7/WY=" \ No newline at end of file +#env: +# secure: "kcksCWXVeZKmFUWcyi2S/j87iwUmXMxZXxA2DG9ymc11QP43QoPNSG9pBjA/DDjvzt4WdKIFphTrxVfvawii/9j3oXA1aPmAcHGu87i4iOVg4IIZ4bPZLfUo0e7s6XP5FakzegYvPP6HWV5Xr5h+Q6osrjq3czOnPY+rVII6MRrxXMOfsqo8HEER+YIOOD6vj5LV2/quY8d0XHtThqgGvQ1cz4OB3vbd4KFBl48kmfXKefTrRG1NoqoQMMpwUVzU395JIEAg1eWbGkquhWU5v13gRwk3VMVWF75jZna8TSiqWha0P5iQdaED30kNCz3poIaBI1MLdxktJxwUQJZ5AaYIMCxh7ZCiW0FXTYCRu3EoeYusTPMLqy1ghK+gIlA46sNd26cKk5/OngXRrHo/J0aF5NWjydlk5FLHfKm9ih/Y426M9nV2zYNQAcVKgO8zVNb2IkJ3e7aTB2NH4DpkvjSV4D4hlnmW9xxmo14TKF+gXJ9Hw9ssKbigRHoL6S92aQHcpkdjGGnI5YSTy1fZh/nIE3HDmx+hcK4/ZtPHj9KnXopKYxBGyNswN5Eko+q6h3BB/Q7LwALtEexdDbznwsRmcZXJsOU4chvcjAKgIAi6cbeTwq8kG5E/w8TTY2wGeRm2ZFysQu6Jf8hgTZDQTV343RG80STWeUbiD0o7/WY=" From 08f1831f089f490a382a0e9140287d72b93e3ff0 Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 3 Dec 2015 14:02:36 +0000 Subject: [PATCH 021/165] Travis only reporting coverage for top-level package --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 88a1504d..dc879936 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,9 @@ install: - go get github.com/mattn/goveralls script: - - go test -v -covermode=count -coverprofile=coverage.out ./... + - go test -v -covermode=count -coverprofile=coverage.out . - go tool cover -func=coverage.out -# - $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN + - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN #env: # secure: "kcksCWXVeZKmFUWcyi2S/j87iwUmXMxZXxA2DG9ymc11QP43QoPNSG9pBjA/DDjvzt4WdKIFphTrxVfvawii/9j3oXA1aPmAcHGu87i4iOVg4IIZ4bPZLfUo0e7s6XP5FakzegYvPP6HWV5Xr5h+Q6osrjq3czOnPY+rVII6MRrxXMOfsqo8HEER+YIOOD6vj5LV2/quY8d0XHtThqgGvQ1cz4OB3vbd4KFBl48kmfXKefTrRG1NoqoQMMpwUVzU395JIEAg1eWbGkquhWU5v13gRwk3VMVWF75jZna8TSiqWha0P5iQdaED30kNCz3poIaBI1MLdxktJxwUQJZ5AaYIMCxh7ZCiW0FXTYCRu3EoeYusTPMLqy1ghK+gIlA46sNd26cKk5/OngXRrHo/J0aF5NWjydlk5FLHfKm9ih/Y426M9nV2zYNQAcVKgO8zVNb2IkJ3e7aTB2NH4DpkvjSV4D4hlnmW9xxmo14TKF+gXJ9Hw9ssKbigRHoL6S92aQHcpkdjGGnI5YSTy1fZh/nIE3HDmx+hcK4/ZtPHj9KnXopKYxBGyNswN5Eko+q6h3BB/Q7LwALtEexdDbznwsRmcZXJsOU4chvcjAKgIAi6cbeTwq8kG5E/w8TTY2wGeRm2ZFysQu6Jf8hgTZDQTV343RG80STWeUbiD0o7/WY=" From c9f0d5087a7d8234a6ada1afd649dac42fa73d3c Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 3 Dec 2015 23:53:41 +0000 Subject: [PATCH 022/165] Added new Clock type to support clock-face operations. --- clock.go | 214 ++++++++++++++++++++++++++++++++++++++++++++++++++ clock_test.go | 166 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 380 insertions(+) create mode 100644 clock.go create mode 100644 clock_test.go diff --git a/clock.go b/clock.go new file mode 100644 index 00000000..624b77fc --- /dev/null +++ b/clock.go @@ -0,0 +1,214 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package date + +import ( + "time" + "fmt" + "strconv" +) + +const zero time.Duration = 0 + +// Clock specifies a time of day. It extends the existing time.Duration, applying +// that to the time since midnight on some arbitrary day. +// +// It is not intended that Clock be used to represent periods greater than 24 hours nor +// negative values. However, for such lengths of time, a fixed 24 hours per day +// is assumed and a modulo operation Mod() is provided to discard whole multiples of 24 hours. +// +// See https://en.wikipedia.org/wiki/ISO_8601#Times +type Clock time.Duration + +const ( +// ClockDay is a fixed period of 24 hours. This does not take account of daylight savings, so is not fully general. + ClockDay Clock = Clock(time.Hour * 24) + ClockHour Clock = Clock(time.Hour) + ClockMinute Clock = Clock(time.Minute) + ClockSecond Clock = Clock(time.Second) +) + +// HhMmSs returns a new Clock with specified hour, minute, second. +func HhMmSs(h, m, s int) Clock { + hns := Clock(h) * ClockHour + mns := Clock(m) * ClockMinute + sns := Clock(s) * ClockSecond + return Clock(hns + mns + sns) +} + +// Add returns a new Clock offset from this clock specified hour, minute, second. The parameters can be negative. +// If required, use Mod() to correct any overflow or underflow. +func (c Clock) Add(h, m, s int) Clock { + hns := Clock(h) * ClockHour + mns := Clock(m) * ClockMinute + sns := Clock(s) * ClockSecond + return c + hns + mns + sns +} + +// ParseClock converts a string representation to a Clock. Acceptable representations +// are as per ISO-8601 - see https://en.wikipedia.org/wiki/ISO_8601#Times +func ParseClock(hms string) (clock Clock, err error) { + switch len(hms) { + case 2: // HH + return parseClockParts(hms, hms, "", "", "") + + case 4: // HHMM + return parseClockParts(hms, hms[:2], hms[2:], "", "") + + case 5: // HH:MM + if hms[2] != ':' { + return 0, fmt.Errorf("date.ParseClock: cannot parse %s", hms) + } + return parseClockParts(hms, hms[:2], hms[3:], "", "") + + case 6: // HHMMSS + return parseClockParts(hms, hms[:2], hms[2:4], hms[4:], "") + + case 8: // HH:MM:SS + if hms[2] != ':' || hms[5] != ':' { + return 0, fmt.Errorf("date.ParseClock: cannot parse %s", hms) + } + return parseClockParts(hms, hms[:2], hms[3:5], hms[6:], "") + + default: + if hms[2] != ':' || hms[5] != ':' || hms[8] != '.' { + return 0, fmt.Errorf("date.ParseClock: cannot parse %s", hms) + } + return parseClockParts(hms, hms[:2], hms[3:5], hms[6:8], hms[9:]) + } + return 0, fmt.Errorf("date.ParseClock: cannot parse %s", hms) +} + +func parseClockParts(hms, hh, mm, ss, nnnns string) (clock Clock, err error) { + h := 0 + m := 0 + s := 0 + ns := 0 + if hh != "" { + h, err = strconv.Atoi(hh) + if err != nil { + return 0, fmt.Errorf("date.ParseClock: cannot parse %s: %v", hms, err) + } + } + if mm != "" { + m, err = strconv.Atoi(mm) + if err != nil { + return 0, fmt.Errorf("date.ParseClock: cannot parse %s: %v", hms, err) + } + } + if ss != "" { + s, err = strconv.Atoi(ss) + if err != nil { + return 0, fmt.Errorf("date.ParseClock: cannot parse %s: %v", hms, err) + } + } + if nnnns != "" { + ns, err = strconv.Atoi(nnnns) + if err != nil { + return 0, fmt.Errorf("date.ParseClock: cannot parse %s: %v", hms, err) + } + } + return HhMmSs(h, m, s) + Clock(ns), nil +} + +// IsInOneDay tests whether a clock time is in the range 0 to 24 hours, inclusive. Inside this +// range, a Clock is generally well-behaved. But outside it, there may be errors due to daylight +// savings. Note that 24:00:00 is included as a special case as per ISO-8601 definition of midnight. +func (c Clock) IsInOneDay() bool { + return 0 <= c && c <= ClockDay +} + +// Days gets the number of whole days represented by the Clock, assuming that each day is a fixed +// 24 hour period. Negative values are treated so that the range -23h59m59s to -1s is fully +// enclosed in a day numbered -1, and so on. This means that the result is zero only for the +// clock range 0s to 23h59m59s, for which IsInOneDay() returns true. +func (c Clock) Days() int { + if c < 0 { + return int(c / ClockDay) - 1 + } else { + return int(c / ClockDay) + } +} + +// Mod24 calculates the remainder vs 24 hours using Euclidean division, in which the result +// will be less than 24 hours and is never negative. +// https://en.wikipedia.org/wiki/Modulo_operation +func (c Clock) Mod24() Clock { + if 0 <= c && c < ClockDay { + return c + } + if c < 0 { + q := 1 - c / ClockDay + return c + (q * ClockDay) + } + q := c / ClockDay + return c - (q * ClockDay) +} + +// Hours gets the clock-face number of hours (calculated from the modulo time, see Mod24). +func (c Clock) Hours() int { + return int(clockHours(c.Mod24())) +} + +// Minutes gets the clock-face number of minutes (calculated from the modulo time, see Mod24). +// For example, for 22:35 this will return 35. +func (c Clock) Minutes() int { + return int(clockMinutes(c.Mod24())) +} + +// Seconds gets the clock-face number of seconds (calculated from the modulo time, see Mod24). +// For example, for 10:20:30 this will return 30. +func (c Clock) Seconds() int { + return int(clockSeconds(c.Mod24())) +} + +// Nanosec gets the clock-face number of nanoseconds (calculated from the modulo time, see Mod24). +// For example, for 10:20:30.456111222 this will return 456111222. +func (c Clock) Nanosec() int64 { + return int64(clockNanosec(c.Mod24())) +} + +func clockHours(cm Clock) Clock { + return (cm / ClockHour) +} + +func clockMinutes(cm Clock) Clock { + return (cm - clockHours(cm) * ClockHour) / ClockMinute +} + +func clockSeconds(cm Clock) Clock { + return (cm - clockHours(cm) * ClockHour - clockMinutes(cm) * ClockMinute) / ClockSecond +} + +func clockNanosec(cm Clock) Clock { + return cm - clockHours(cm) * ClockHour - clockMinutes(cm) * ClockMinute - clockSeconds(cm) * ClockSecond +} + +// Hh gets the clock-face number of hours as a two-digit string (calculated from the modulo time, see Mod24). +func (c Clock) Hh() string { + cm := c.Mod24() + return fmt.Sprintf("%02d", clockHours(cm)) +} + +// HhMm gets the clock-face number of hours and minutes as a five-digit string (calculated from the +// modulo time, see Mod24). +func (c Clock) HhMm() string { + cm := c.Mod24() + return fmt.Sprintf("%02d:%02d", clockHours(cm), clockMinutes(cm)) +} + +// HhMmSs gets the clock-face number of hours, minutes, seconds as an eight-digit string +// (calculated from the modulo time, see Mod24). +func (c Clock) HhMmSs() string { + cm := c.Mod24() + return fmt.Sprintf("%02d:%02d:%02d", clockHours(cm), clockMinutes(cm), clockSeconds(cm)) +} + +// String gets the clock-face number of hours, minutes, seconds and nanoseconds as an 18-digit string +// (calculated from the modulo time, see Mod24). +func (c Clock) String() string { + cm := c.Mod24() + return fmt.Sprintf("%02d:%02d:%02d.%09d", clockHours(cm), clockMinutes(cm), clockSeconds(cm), clockNanosec(cm)) +} diff --git a/clock_test.go b/clock_test.go new file mode 100644 index 00000000..058eb325 --- /dev/null +++ b/clock_test.go @@ -0,0 +1,166 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package date + +import ( + "time" + "testing" +) + +func TestClockHoursMinutesSeconds(t *testing.T) { + cases := []struct { + in Clock + h, m, s int + }{ + {HhMmSs(0, 0, 0), 0, 0, 0}, + {HhMmSs(1, 2, 3), 1, 2, 3}, + {HhMmSs(23, 59, 59), 23, 59, 59}, + {HhMmSs(0, 0, -1), 23, 59, 59}, + } + for _, c := range cases { + h := c.in.Hours() + m := c.in.Minutes() + s := c.in.Seconds() + if h != c.h || m != c.m || s != c.s { + t.Errorf("got %d %d %d, want %v", h, m, s, c.in) + } + } +} + +func TestClockAdd(t *testing.T) { + cases := []struct { + h, m, s int + in, want Clock + }{ + {0, 0, 0, 2 * ClockHour, HhMmSs(2, 0, 0)}, + {0, 0, 1, 2 * ClockHour, HhMmSs(2, 0, 1)}, + {0, 0, -1, 2 * ClockHour, HhMmSs(1, 59, 59)}, + {0, 1, 0, 2 * ClockHour, HhMmSs(2, 1, 0)}, + {0, -1, 0, 2 * ClockHour, HhMmSs(1, 59, 0)}, + {1, 0, 0, 2 * ClockHour, HhMmSs(3, 0, 0)}, + {-1, 0, 0, 2 * ClockHour, HhMmSs(1, 0, 0)}, + {-2, 0, 0, 2 * ClockHour, HhMmSs(0, 0, 0)}, + {-2, 0, -1, 2 * ClockHour, HhMmSs(0, 0, -1)}, + } + for _, c := range cases { + got := c.in.Add(c.h, c.m, c.s) + if got != c.want { + t.Errorf("%d %d %d: got %v, want %v", c.h, c.m, c.s, got, c.want) + } + } +} + +func TestClockMod(t *testing.T) { + cases := []struct { + h, mod Clock + }{ + {0, 0}, + {1, 1 * ClockHour}, + {2, 2 * ClockHour}, + {23, 23 * ClockHour}, + {24, 0}, + {25, ClockHour}, + {49, ClockHour}, + {-1, 23 * ClockHour}, + {-23, ClockHour}, + } + for _, c := range cases { + clock := c.h * ClockHour + if clock.Mod24() != c.mod { + t.Errorf("%dh: got %v, want %v", c.h, clock.Mod24(), c.mod) + } + } +} + +func TestClockDays(t *testing.T) { + cases := []struct { + h, days int + }{ + {0, 0}, + {1, 0}, + {23, 0}, + {24, 1}, + {25, 1}, + {48, 2}, + {49, 2}, + {-1, -1}, + {-23, -1}, + {-24, -2}, + } + for _, c := range cases { + clock := Clock(c.h) * ClockHour + if clock.Days() != c.days { + t.Errorf("%dh: got %v, want %v", c.h, clock.Days(), c.days) + } + } +} + +func TestClockString(t *testing.T) { + cases := []struct { + h, m, s, ns time.Duration + hh, hhmm, hhmmss, str string + }{ + {0, 0, 0, 0, "00", "00:00", "00:00:00", "00:00:00.000000000"}, + {0, 0, 0, 1, "00", "00:00", "00:00:00", "00:00:00.000000001"}, + {0, 0, 1, 0, "00", "00:00", "00:00:01", "00:00:01.000000000"}, + {0, 1, 0, 0, "00", "00:01", "00:01:00", "00:01:00.000000000"}, + {1, 0, 0, 0, "01", "01:00", "01:00:00", "01:00:00.000000000"}, + {1, 2, 3, 4, "01", "01:02", "01:02:03", "01:02:03.000000004"}, + {-1, -1, -1, -1, "22", "22:58", "22:58:58", "22:58:58.999999999"}, + } + for _, c := range cases { + d := Clock(c.h * time.Hour + c.m * time.Minute + c.s * time.Second + c.ns) + if d.Hh() != c.hh { + t.Errorf("%d, %d, %d, %d, got %v, want %v", c.h, c.m, c.s, c.ns, d.Hh(), c.hh) + } + if d.HhMm() != c.hhmm { + t.Errorf("%d, %d, %d, %d, got %v, want %v", c.h, c.m, c.s, c.ns, d.HhMm(), c.hhmm) + } + if d.HhMmSs() != c.hhmmss { + t.Errorf("%d, %d, %d, %d, got %v, want %v", c.h, c.m, c.s, c.ns, d.HhMmSs(), c.hhmmss) + } + if d.String() != c.str { + t.Errorf("%d, %d, %d, %d, got %v, want %v", c.h, c.m, c.s, c.ns, d.String(), c.str) + } + } +} + +func TestClockParse(t *testing.T) { + cases := []struct { + str string + want Clock + }{ + {"00", HhMmSs(0, 0, 0)}, + {"01", HhMmSs(1, 0, 0)}, + {"23", HhMmSs(23, 0, 0)}, + {"00:00", HhMmSs(0, 0, 0)}, + {"00:01", HhMmSs(0, 1, 0)}, + {"01:00", HhMmSs(1, 0, 0)}, + {"01:02", HhMmSs(1, 2, 0)}, + {"23:59", HhMmSs(23, 59, 0)}, + {"00:00:00", HhMmSs(0, 0, 0)}, + {"00:00:01", HhMmSs(0, 0, 1)}, + {"00:01:00", HhMmSs(0, 1, 0)}, + {"01:00:00", HhMmSs(1, 0, 0)}, + {"01:02:03", HhMmSs(1, 2, 3)}, + {"23:59:59", HhMmSs(23, 59, 59)}, + {"00:00:00.000000000", HhMmSs(0, 0, 0)}, + {"00:00:00.000000001", HhMmSs(0, 0, 0) + 1}, + {"00:00:01.000000000", HhMmSs(0, 0, 1)}, + {"00:01:00.000000000", HhMmSs(0, 1, 0)}, + {"01:00:00.000000000", HhMmSs(1, 0, 0)}, + {"01:02:03.000000004", HhMmSs(1, 2, 3) + 4}, + {"23:59:59.999999999", HhMmSs(23, 59, 59) + 999999999}, + } + for _, c := range cases { + str, err := ParseClock(c.str) + if err != nil { + t.Errorf("%s, error %v", c.str, err) + } + if str != c.want { + t.Errorf("%s, got %v, want %v", c.str, str, c.want) + } + } +} From 6406c104359cc39b1659a33348c79e4c4dc46591 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 4 Dec 2015 00:08:15 +0000 Subject: [PATCH 023/165] Improved documentation and go-fmt. --- clock.go | 28 ++++++++++++++-------------- clock_test.go | 4 ++-- date_test.go | 16 ++++++++-------- timespan/daterange.go | 4 ++-- timespan/timespan_test.go | 20 ++++++++++---------- 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/clock.go b/clock.go index 624b77fc..26981cb4 100644 --- a/clock.go +++ b/clock.go @@ -5,25 +5,25 @@ package date import ( - "time" "fmt" "strconv" + "time" ) const zero time.Duration = 0 // Clock specifies a time of day. It extends the existing time.Duration, applying -// that to the time since midnight on some arbitrary day. +// that to the time since midnight (on some arbitrary day in some arbitrary timezone). // // It is not intended that Clock be used to represent periods greater than 24 hours nor // negative values. However, for such lengths of time, a fixed 24 hours per day -// is assumed and a modulo operation Mod() is provided to discard whole multiples of 24 hours. +// is assumed and a modulo operation Mod24 is provided to discard whole multiples of 24 hours. // // See https://en.wikipedia.org/wiki/ISO_8601#Times type Clock time.Duration const ( -// ClockDay is a fixed period of 24 hours. This does not take account of daylight savings, so is not fully general. + // ClockDay is a fixed period of 24 hours. This does not take account of daylight savings, so is not fully general. ClockDay Clock = Clock(time.Hour * 24) ClockHour Clock = Clock(time.Hour) ClockMinute Clock = Clock(time.Minute) @@ -126,7 +126,7 @@ func (c Clock) IsInOneDay() bool { // clock range 0s to 23h59m59s, for which IsInOneDay() returns true. func (c Clock) Days() int { if c < 0 { - return int(c / ClockDay) - 1 + return int(c/ClockDay) - 1 } else { return int(c / ClockDay) } @@ -140,7 +140,7 @@ func (c Clock) Mod24() Clock { return c } if c < 0 { - q := 1 - c / ClockDay + q := 1 - c/ClockDay return c + (q * ClockDay) } q := c / ClockDay @@ -175,15 +175,15 @@ func clockHours(cm Clock) Clock { } func clockMinutes(cm Clock) Clock { - return (cm - clockHours(cm) * ClockHour) / ClockMinute + return (cm - clockHours(cm)*ClockHour) / ClockMinute } func clockSeconds(cm Clock) Clock { - return (cm - clockHours(cm) * ClockHour - clockMinutes(cm) * ClockMinute) / ClockSecond + return (cm - clockHours(cm)*ClockHour - clockMinutes(cm)*ClockMinute) / ClockSecond } func clockNanosec(cm Clock) Clock { - return cm - clockHours(cm) * ClockHour - clockMinutes(cm) * ClockMinute - clockSeconds(cm) * ClockSecond + return cm - clockHours(cm)*ClockHour - clockMinutes(cm)*ClockMinute - clockSeconds(cm)*ClockSecond } // Hh gets the clock-face number of hours as a two-digit string (calculated from the modulo time, see Mod24). @@ -192,22 +192,22 @@ func (c Clock) Hh() string { return fmt.Sprintf("%02d", clockHours(cm)) } -// HhMm gets the clock-face number of hours and minutes as a five-digit string (calculated from the -// modulo time, see Mod24). +// HhMm gets the clock-face number of hours and minutes as a five-character ISO-8601 time string (calculated +// from the modulo time, see Mod24). func (c Clock) HhMm() string { cm := c.Mod24() return fmt.Sprintf("%02d:%02d", clockHours(cm), clockMinutes(cm)) } -// HhMmSs gets the clock-face number of hours, minutes, seconds as an eight-digit string +// HhMmSs gets the clock-face number of hours, minutes, seconds as an eight-character ISO-8601 time string // (calculated from the modulo time, see Mod24). func (c Clock) HhMmSs() string { cm := c.Mod24() return fmt.Sprintf("%02d:%02d:%02d", clockHours(cm), clockMinutes(cm), clockSeconds(cm)) } -// String gets the clock-face number of hours, minutes, seconds and nanoseconds as an 18-digit string -// (calculated from the modulo time, see Mod24). +// String gets the clock-face number of hours, minutes, seconds and nanoseconds as an 18-character ISO-8601 +// time string (calculated from the modulo time, see Mod24). func (c Clock) String() string { cm := c.Mod24() return fmt.Sprintf("%02d:%02d:%02d.%09d", clockHours(cm), clockMinutes(cm), clockSeconds(cm), clockNanosec(cm)) diff --git a/clock_test.go b/clock_test.go index 058eb325..4a13b88e 100644 --- a/clock_test.go +++ b/clock_test.go @@ -5,8 +5,8 @@ package date import ( - "time" "testing" + "time" ) func TestClockHoursMinutesSeconds(t *testing.T) { @@ -111,7 +111,7 @@ func TestClockString(t *testing.T) { {-1, -1, -1, -1, "22", "22:58", "22:58:58", "22:58:58.999999999"}, } for _, c := range cases { - d := Clock(c.h * time.Hour + c.m * time.Minute + c.s * time.Second + c.ns) + d := Clock(c.h*time.Hour + c.m*time.Minute + c.s*time.Second + c.ns) if d.Hh() != c.hh { t.Errorf("%d, %d, %d, %d, got %v, want %v", c.h, c.m, c.s, c.ns, d.Hh(), c.hh) } diff --git a/date_test.go b/date_test.go index 312e4ec9..ff4b568f 100644 --- a/date_test.go +++ b/date_test.go @@ -5,20 +5,20 @@ package date import ( + "runtime/debug" "testing" "time" - "runtime/debug" ) func same(d Date, t time.Time) bool { yd, wd := d.ISOWeek() yt, wt := t.ISOWeek() return d.Year() == t.Year() && - d.Month() == t.Month() && - d.Day() == t.Day() && - d.Weekday() == t.Weekday() && - d.YearDay() == t.YearDay() && - yd == yt && wd == wt + d.Month() == t.Month() && + d.Day() == t.Day() && + d.Weekday() == t.Weekday() && + d.YearDay() == t.YearDay() && + yd == yt && wd == wt } func TestNew(t *testing.T) { @@ -63,7 +63,7 @@ func TestToday(t *testing.T) { } cases := []int{-10, -5, -3, 0, 1, 4, 8, 12} for _, c := range cases { - location := time.FixedZone("zone", c * 60 * 60) + location := time.FixedZone("zone", c*60*60) today = TodayIn(location) now = time.Now().In(location) if !same(today, now) { @@ -105,7 +105,7 @@ func TestTime(t *testing.T) { t.Errorf("TimeLocal(%v) == %v, want %v", d, tLocal.Location(), time.Local) } for _, z := range zones { - location := time.FixedZone("zone", z * 60 * 60) + location := time.FixedZone("zone", z*60*60) tInLoc := d.In(location) if !same(d, tInLoc) { t.Errorf("TimeIn(%v) == %v, want date part %v", d, tInLoc, d) diff --git a/timespan/daterange.go b/timespan/daterange.go index 79242c75..887b1593 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -37,7 +37,7 @@ func NewDateRange(start, end Date) DateRange { // NewYearOf constructs the range encompassing the whole year specified. func NewYearOf(year int) DateRange { start := New(year, time.January, 1) - end := New(year + 1, time.January, 1) + end := New(year+1, time.January, 1) return DateRange{start, PeriodOfDays(end.Sub(start))} } @@ -45,7 +45,7 @@ func NewYearOf(year int) DateRange { // It handles leap years correctly. func NewMonthOf(year int, month time.Month) DateRange { start := New(year, month, 1) - endT := time.Date(year, month + 1, 1, 0, 0, 0, 0, time.UTC) + endT := time.Date(year, month+1, 1, 0, 0, 0, 0, time.UTC) end := NewAt(endT) return DateRange{start, PeriodOfDays(end.Sub(start))} } diff --git a/timespan/timespan_test.go b/timespan/timespan_test.go index 2aaabf5b..d3705dfe 100644 --- a/timespan/timespan_test.go +++ b/timespan/timespan_test.go @@ -5,9 +5,9 @@ package timespan import ( + "github.com/rickb777/date" "testing" "time" - "github.com/rickb777/date" ) const zero time.Duration = 0 @@ -33,13 +33,13 @@ func TestNewTimeSpan(t *testing.T) { ts2 := NewTimeSpan(t0327, t0328) isEq(t, ts2.mark, t0327) - isEq(t, ts2.Duration(), time.Hour * 24) + isEq(t, ts2.Duration(), time.Hour*24) isEq(t, ts2.IsEmpty(), false) isEq(t, ts2.End(), t0328) ts3 := NewTimeSpan(t0329, t0327) isEq(t, ts3.mark, t0327) - isEq(t, ts3.Duration(), time.Hour * 48) + isEq(t, ts3.Duration(), time.Hour*48) isEq(t, ts3.IsEmpty(), false) isEq(t, ts3.End(), t0329) } @@ -58,31 +58,31 @@ func TestTSEnd(t *testing.T) { func TestTSShiftBy(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328).ShiftBy(time.Hour * 24) isEq(t, ts1.mark, t0328) - isEq(t, ts1.Duration(), time.Hour * 24) + isEq(t, ts1.Duration(), time.Hour*24) isEq(t, ts1.End(), t0329) ts2 := NewTimeSpan(t0328, t0329).ShiftBy(-time.Hour * 24) isEq(t, ts2.mark, t0327) - isEq(t, ts2.Duration(), time.Hour * 24) + isEq(t, ts2.Duration(), time.Hour*24) isEq(t, ts2.End(), t0328) } func TestTSExtendBy(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328).ExtendBy(time.Hour * 24) isEq(t, ts1.mark, t0327) - isEq(t, ts1.Duration(), time.Hour * 48) + isEq(t, ts1.Duration(), time.Hour*48) isEq(t, ts1.End(), t0329) ts2 := NewTimeSpan(t0328, t0329).ExtendBy(-time.Hour * 48) isEq(t, ts2.mark, t0327) - isEq(t, ts2.Duration(), time.Hour * 24) + isEq(t, ts2.Duration(), time.Hour*24) isEq(t, ts2.End(), t0328) } func TestTSExtendWithoutWrapping(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328).ExtendWithoutWrapping(time.Hour * 24) isEq(t, ts1.mark, t0327) - isEq(t, ts1.Duration(), time.Hour * 48) + isEq(t, ts1.Duration(), time.Hour*48) isEq(t, ts1.End(), t0329) ts2 := NewTimeSpan(t0328, t0329).ExtendWithoutWrapping(-time.Hour * 48) @@ -182,7 +182,7 @@ func TestConversion2(t *testing.T) { isEq(t, dr.Start(), d0327) isEq(t, dr.End(), d0328) isEq(t, ts1, ts2) - isEq(t, ts1.Duration(), time.Hour * 24) + isEq(t, ts1.Duration(), time.Hour*24) } func TestConversion3(t *testing.T) { @@ -194,5 +194,5 @@ func TestConversion3(t *testing.T) { isEq(t, dr1.End(), d0330) isEq(t, dr1, dr2) isEq(t, ts1, ts2) - isEq(t, ts1.Duration(), time.Hour * 71) + isEq(t, ts1.Duration(), time.Hour*71) } From eab529db8b628d712105dbb1295a1fbb26119270 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 4 Dec 2015 09:19:56 +0000 Subject: [PATCH 024/165] Clock now has its own package. --- clock.go => clock/clock.go | 12 ++--- clock_test.go => clock/clock_test.go | 70 ++++++++++++++-------------- 2 files changed, 41 insertions(+), 41 deletions(-) rename clock.go => clock/clock.go (97%) rename clock_test.go => clock/clock_test.go (69%) diff --git a/clock.go b/clock/clock.go similarity index 97% rename from clock.go rename to clock/clock.go index 26981cb4..0a2ed7df 100644 --- a/clock.go +++ b/clock/clock.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package date +package clock import ( "fmt" @@ -31,10 +31,10 @@ const ( ) // HhMmSs returns a new Clock with specified hour, minute, second. -func HhMmSs(h, m, s int) Clock { - hns := Clock(h) * ClockHour - mns := Clock(m) * ClockMinute - sns := Clock(s) * ClockSecond +func New(hour, minute, second int) Clock { + hns := Clock(hour) * ClockHour + mns := Clock(minute) * ClockMinute + sns := Clock(second) * ClockSecond return Clock(hns + mns + sns) } @@ -110,7 +110,7 @@ func parseClockParts(hms, hh, mm, ss, nnnns string) (clock Clock, err error) { return 0, fmt.Errorf("date.ParseClock: cannot parse %s: %v", hms, err) } } - return HhMmSs(h, m, s) + Clock(ns), nil + return New(h, m, s) + Clock(ns), nil } // IsInOneDay tests whether a clock time is in the range 0 to 24 hours, inclusive. Inside this diff --git a/clock_test.go b/clock/clock_test.go similarity index 69% rename from clock_test.go rename to clock/clock_test.go index 4a13b88e..cb8bf97d 100644 --- a/clock_test.go +++ b/clock/clock_test.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package date +package clock import ( "testing" @@ -14,10 +14,10 @@ func TestClockHoursMinutesSeconds(t *testing.T) { in Clock h, m, s int }{ - {HhMmSs(0, 0, 0), 0, 0, 0}, - {HhMmSs(1, 2, 3), 1, 2, 3}, - {HhMmSs(23, 59, 59), 23, 59, 59}, - {HhMmSs(0, 0, -1), 23, 59, 59}, + {New(0, 0, 0), 0, 0, 0}, + {New(1, 2, 3), 1, 2, 3}, + {New(23, 59, 59), 23, 59, 59}, + {New(0, 0, -1), 23, 59, 59}, } for _, c := range cases { h := c.in.Hours() @@ -34,15 +34,15 @@ func TestClockAdd(t *testing.T) { h, m, s int in, want Clock }{ - {0, 0, 0, 2 * ClockHour, HhMmSs(2, 0, 0)}, - {0, 0, 1, 2 * ClockHour, HhMmSs(2, 0, 1)}, - {0, 0, -1, 2 * ClockHour, HhMmSs(1, 59, 59)}, - {0, 1, 0, 2 * ClockHour, HhMmSs(2, 1, 0)}, - {0, -1, 0, 2 * ClockHour, HhMmSs(1, 59, 0)}, - {1, 0, 0, 2 * ClockHour, HhMmSs(3, 0, 0)}, - {-1, 0, 0, 2 * ClockHour, HhMmSs(1, 0, 0)}, - {-2, 0, 0, 2 * ClockHour, HhMmSs(0, 0, 0)}, - {-2, 0, -1, 2 * ClockHour, HhMmSs(0, 0, -1)}, + {0, 0, 0, 2 * ClockHour, New(2, 0, 0)}, + {0, 0, 1, 2 * ClockHour, New(2, 0, 1)}, + {0, 0, -1, 2 * ClockHour, New(1, 59, 59)}, + {0, 1, 0, 2 * ClockHour, New(2, 1, 0)}, + {0, -1, 0, 2 * ClockHour, New(1, 59, 0)}, + {1, 0, 0, 2 * ClockHour, New(3, 0, 0)}, + {-1, 0, 0, 2 * ClockHour, New(1, 0, 0)}, + {-2, 0, 0, 2 * ClockHour, New(0, 0, 0)}, + {-2, 0, -1, 2 * ClockHour, New(0, 0, -1)}, } for _, c := range cases { got := c.in.Add(c.h, c.m, c.s) @@ -132,27 +132,27 @@ func TestClockParse(t *testing.T) { str string want Clock }{ - {"00", HhMmSs(0, 0, 0)}, - {"01", HhMmSs(1, 0, 0)}, - {"23", HhMmSs(23, 0, 0)}, - {"00:00", HhMmSs(0, 0, 0)}, - {"00:01", HhMmSs(0, 1, 0)}, - {"01:00", HhMmSs(1, 0, 0)}, - {"01:02", HhMmSs(1, 2, 0)}, - {"23:59", HhMmSs(23, 59, 0)}, - {"00:00:00", HhMmSs(0, 0, 0)}, - {"00:00:01", HhMmSs(0, 0, 1)}, - {"00:01:00", HhMmSs(0, 1, 0)}, - {"01:00:00", HhMmSs(1, 0, 0)}, - {"01:02:03", HhMmSs(1, 2, 3)}, - {"23:59:59", HhMmSs(23, 59, 59)}, - {"00:00:00.000000000", HhMmSs(0, 0, 0)}, - {"00:00:00.000000001", HhMmSs(0, 0, 0) + 1}, - {"00:00:01.000000000", HhMmSs(0, 0, 1)}, - {"00:01:00.000000000", HhMmSs(0, 1, 0)}, - {"01:00:00.000000000", HhMmSs(1, 0, 0)}, - {"01:02:03.000000004", HhMmSs(1, 2, 3) + 4}, - {"23:59:59.999999999", HhMmSs(23, 59, 59) + 999999999}, + {"00", New(0, 0, 0)}, + {"01", New(1, 0, 0)}, + {"23", New(23, 0, 0)}, + {"00:00", New(0, 0, 0)}, + {"00:01", New(0, 1, 0)}, + {"01:00", New(1, 0, 0)}, + {"01:02", New(1, 2, 0)}, + {"23:59", New(23, 59, 0)}, + {"00:00:00", New(0, 0, 0)}, + {"00:00:01", New(0, 0, 1)}, + {"00:01:00", New(0, 1, 0)}, + {"01:00:00", New(1, 0, 0)}, + {"01:02:03", New(1, 2, 3)}, + {"23:59:59", New(23, 59, 59)}, + {"00:00:00.000000000", New(0, 0, 0)}, + {"00:00:00.000000001", New(0, 0, 0) + 1}, + {"00:00:01.000000000", New(0, 0, 1)}, + {"00:01:00.000000000", New(0, 1, 0)}, + {"01:00:00.000000000", New(1, 0, 0)}, + {"01:02:03.000000004", New(1, 2, 3) + 4}, + {"23:59:59.999999999", New(23, 59, 59) + 999999999}, } for _, c := range cases { str, err := ParseClock(c.str) From 5dda47ed8867dbba84b7727d4a69b8270921a213 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 4 Dec 2015 09:30:10 +0000 Subject: [PATCH 025/165] Renamed ParseClock -> Parse --- clock/clock.go | 4 ++-- clock/clock_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/clock/clock.go b/clock/clock.go index 0a2ed7df..ee8da509 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -47,9 +47,9 @@ func (c Clock) Add(h, m, s int) Clock { return c + hns + mns + sns } -// ParseClock converts a string representation to a Clock. Acceptable representations +// Parse converts a string representation to a Clock. Acceptable representations // are as per ISO-8601 - see https://en.wikipedia.org/wiki/ISO_8601#Times -func ParseClock(hms string) (clock Clock, err error) { +func Parse(hms string) (clock Clock, err error) { switch len(hms) { case 2: // HH return parseClockParts(hms, hms, "", "", "") diff --git a/clock/clock_test.go b/clock/clock_test.go index cb8bf97d..0f2f7f7d 100644 --- a/clock/clock_test.go +++ b/clock/clock_test.go @@ -155,7 +155,7 @@ func TestClockParse(t *testing.T) { {"23:59:59.999999999", New(23, 59, 59) + 999999999}, } for _, c := range cases { - str, err := ParseClock(c.str) + str, err := Parse(c.str) if err != nil { t.Errorf("%s, error %v", c.str, err) } From 33f77979498577f5f301ac07ce2b652ac41d20c8 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 4 Dec 2015 17:12:24 +0000 Subject: [PATCH 026/165] Added Clock.IsMidnight and improved some tests; bug fixed in Mod24. --- clock/clock.go | 41 +++++++++++++--------- clock/clock_test.go | 84 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 93 insertions(+), 32 deletions(-) diff --git a/clock/clock.go b/clock/clock.go index ee8da509..301bdaa9 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -23,7 +23,7 @@ const zero time.Duration = 0 type Clock time.Duration const ( - // ClockDay is a fixed period of 24 hours. This does not take account of daylight savings, so is not fully general. +// ClockDay is a fixed period of 24 hours. This does not take account of daylight savings, so is not fully general. ClockDay Clock = Clock(time.Hour * 24) ClockHour Clock = Clock(time.Hour) ClockMinute Clock = Clock(time.Minute) @@ -120,16 +120,9 @@ func (c Clock) IsInOneDay() bool { return 0 <= c && c <= ClockDay } -// Days gets the number of whole days represented by the Clock, assuming that each day is a fixed -// 24 hour period. Negative values are treated so that the range -23h59m59s to -1s is fully -// enclosed in a day numbered -1, and so on. This means that the result is zero only for the -// clock range 0s to 23h59m59s, for which IsInOneDay() returns true. -func (c Clock) Days() int { - if c < 0 { - return int(c/ClockDay) - 1 - } else { - return int(c / ClockDay) - } +// IsMidnight tests whether a clock time is midnight. This is shorthand for c.Mod24() == 0. +func (c Clock) IsMidnight() bool { + return c.Mod24() == 0 } // Mod24 calculates the remainder vs 24 hours using Euclidean division, in which the result @@ -140,13 +133,29 @@ func (c Clock) Mod24() Clock { return c } if c < 0 { - q := 1 - c/ClockDay - return c + (q * ClockDay) + q := 1 - c / ClockDay + m := c + (q * ClockDay) + if m == ClockDay { + m = 0 + } + return m } q := c / ClockDay return c - (q * ClockDay) } +// Days gets the number of whole days represented by the Clock, assuming that each day is a fixed +// 24 hour period. Negative values are treated so that the range -23h59m59s to -1s is fully +// enclosed in a day numbered -1, and so on. This means that the result is zero only for the +// clock range 0s to 23h59m59s, for which IsInOneDay() returns true. +func (c Clock) Days() int { + if c < 0 { + return int(c / ClockDay) - 1 + } else { + return int(c / ClockDay) + } +} + // Hours gets the clock-face number of hours (calculated from the modulo time, see Mod24). func (c Clock) Hours() int { return int(clockHours(c.Mod24())) @@ -175,15 +184,15 @@ func clockHours(cm Clock) Clock { } func clockMinutes(cm Clock) Clock { - return (cm - clockHours(cm)*ClockHour) / ClockMinute + return (cm - clockHours(cm) * ClockHour) / ClockMinute } func clockSeconds(cm Clock) Clock { - return (cm - clockHours(cm)*ClockHour - clockMinutes(cm)*ClockMinute) / ClockSecond + return (cm - clockHours(cm) * ClockHour - clockMinutes(cm) * ClockMinute) / ClockSecond } func clockNanosec(cm Clock) Clock { - return cm - clockHours(cm)*ClockHour - clockMinutes(cm)*ClockMinute - clockSeconds(cm)*ClockSecond + return cm - clockHours(cm) * ClockHour - clockMinutes(cm) * ClockMinute - clockSeconds(cm) * ClockSecond } // Hh gets the clock-face number of hours as a two-digit string (calculated from the modulo time, see Mod24). diff --git a/clock/clock_test.go b/clock/clock_test.go index 0f2f7f7d..1178dbc6 100644 --- a/clock/clock_test.go +++ b/clock/clock_test.go @@ -29,6 +29,28 @@ func TestClockHoursMinutesSeconds(t *testing.T) { } } +func TestClockIsInOneDay(t *testing.T) { + cases := []struct { + in Clock + want bool + }{ + {New(0, 0, 0), true}, + {New(24, 0, 0), true}, + {New(-24, 0, 0), false}, + {New(48, 0, 0), false}, + {New(0, 0, 1), true}, + {New(2, 0, 1), true}, + {New(-1, 0, 0), false}, + {New(0, 0, -1), false}, + } + for _, c := range cases { + got := c.in.IsInOneDay() + if got != c.want { + t.Errorf("%v got %v, want %v", c.in, c.in.IsInOneDay(), c.want) + } + } +} + func TestClockAdd(t *testing.T) { cases := []struct { h, m, s int @@ -52,24 +74,54 @@ func TestClockAdd(t *testing.T) { } } +func TestClockIsMidnight(t *testing.T) { + cases := []struct { + in Clock + want bool + }{ + {New(0, 0, 0), true}, + {ClockDay, true}, + {24 * ClockHour, true}, + {New(24, 0, 0), true}, + {New(-24, 0, 0), true}, + {New(-48, 0, 0), true}, + {New(48, 0, 0), true}, + {New(0, 0, 1), false}, + {New(2, 0, 1), false}, + {New(-1, 0, 0), false}, + {New(0, 0, -1), false}, + } + for i, c := range cases { + got := c.in.IsMidnight() + if got != c.want { + t.Errorf("%d: %v got %v, want %v, %d", i, c.in, c.in.IsMidnight(), c.want, c.in.Mod24()) + } + } +} + func TestClockMod(t *testing.T) { cases := []struct { - h, mod Clock + h, want Clock }{ {0, 0}, - {1, 1 * ClockHour}, - {2, 2 * ClockHour}, - {23, 23 * ClockHour}, - {24, 0}, - {25, ClockHour}, - {49, ClockHour}, - {-1, 23 * ClockHour}, - {-23, ClockHour}, + {1 * ClockHour, 1 * ClockHour}, + {2 * ClockHour, 2 * ClockHour}, + {23 * ClockHour, 23 * ClockHour}, + {24 * ClockHour, 0}, + {-24 * ClockHour, 0}, + {-48 * ClockHour, 0}, + {25 * ClockHour, ClockHour}, + {49 * ClockHour, ClockHour}, + {-1 * ClockHour, 23 * ClockHour}, + {-23 * ClockHour, ClockHour}, + {New(0, 0, 1), ClockSecond}, + {New(0, 0, -1), New(23, 59, 59)}, } - for _, c := range cases { - clock := c.h * ClockHour - if clock.Mod24() != c.mod { - t.Errorf("%dh: got %v, want %v", c.h, clock.Mod24(), c.mod) + for i, c := range cases { + clock := c.h + got := clock.Mod24() + if got != c.want { + t.Errorf("%d: %dh: got %#v, want %#v", i, c.h, got, c.want) } } } @@ -89,10 +141,10 @@ func TestClockDays(t *testing.T) { {-23, -1}, {-24, -2}, } - for _, c := range cases { + for i, c := range cases { clock := Clock(c.h) * ClockHour if clock.Days() != c.days { - t.Errorf("%dh: got %v, want %v", c.h, clock.Days(), c.days) + t.Errorf("%d: %dh: got %v, want %v", i, c.h, clock.Days(), c.days) } } } @@ -111,7 +163,7 @@ func TestClockString(t *testing.T) { {-1, -1, -1, -1, "22", "22:58", "22:58:58", "22:58:58.999999999"}, } for _, c := range cases { - d := Clock(c.h*time.Hour + c.m*time.Minute + c.s*time.Second + c.ns) + d := Clock(c.h * time.Hour + c.m * time.Minute + c.s * time.Second + c.ns) if d.Hh() != c.hh { t.Errorf("%d, %d, %d, %d, got %v, want %v", c.h, c.m, c.s, c.ns, d.Hh(), c.hh) } From 2b2c3c3b7636ae2932f9722be8e323b41fb4456d Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 4 Dec 2015 19:21:19 +0000 Subject: [PATCH 027/165] Changed Clock resolution from int64 to int32, this being a more natural and economical space for the anticipated usage. Added interfacing to time.Duration and time.Time. --- clock/clock.go | 91 ++++++++++------ clock/clock_test.go | 246 ++++++++++++++++++++++++-------------------- 2 files changed, 191 insertions(+), 146 deletions(-) diff --git a/clock/clock.go b/clock/clock.go index 301bdaa9..eb5d6b77 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -12,39 +12,62 @@ import ( const zero time.Duration = 0 -// Clock specifies a time of day. It extends the existing time.Duration, applying +// Clock specifies a time of day. It complements the existing time.Duration, applying // that to the time since midnight (on some arbitrary day in some arbitrary timezone). +// The resolution is to the nearest millisecond, unlike time.Duration (which has nanosecond +// resolution). // // It is not intended that Clock be used to represent periods greater than 24 hours nor // negative values. However, for such lengths of time, a fixed 24 hours per day // is assumed and a modulo operation Mod24 is provided to discard whole multiples of 24 hours. // // See https://en.wikipedia.org/wiki/ISO_8601#Times -type Clock time.Duration +type Clock int32 const ( // ClockDay is a fixed period of 24 hours. This does not take account of daylight savings, so is not fully general. - ClockDay Clock = Clock(time.Hour * 24) - ClockHour Clock = Clock(time.Hour) - ClockMinute Clock = Clock(time.Minute) - ClockSecond Clock = Clock(time.Second) + ClockDay Clock = Clock(time.Hour * 24 / time.Millisecond) + ClockHour Clock = Clock(time.Hour / time.Millisecond) + ClockMinute Clock = Clock(time.Minute / time.Millisecond) + ClockSecond Clock = Clock(time.Second / time.Millisecond) ) -// HhMmSs returns a new Clock with specified hour, minute, second. -func New(hour, minute, second int) Clock { - hns := Clock(hour) * ClockHour - mns := Clock(minute) * ClockMinute - sns := Clock(second) * ClockSecond - return Clock(hns + mns + sns) +// New returns a new Clock with specified hour, minute, second and millisecond. +func New(hour, minute, second, millisec int) Clock { + hx := Clock(hour) * ClockHour + mx := Clock(minute) * ClockMinute + sx := Clock(second) * ClockSecond + return Clock(hx + mx + sx + Clock(millisec)) } -// Add returns a new Clock offset from this clock specified hour, minute, second. The parameters can be negative. +// NewAt returns a new Clock with specified hour, minute, second and millisecond. +func NewAt(t time.Time) Clock { + hour, minute, second := t.Clock() + hx := Clock(hour) * ClockHour + mx := Clock(minute) * ClockMinute + sx := Clock(second) * ClockSecond + ms := Clock(t.Nanosecond() / int(time.Millisecond)) + return Clock(hx + mx + sx + ms) +} + +// SinceMidnight returns a new Clock based on a duration since some arbitrary midnight. +func SinceMidnight(d time.Duration) Clock { + return Clock(d / time.Millisecond) +} + +// DurationSinceMidnight convert a clock to a time.Duration since some arbitrary midnight. +func (c Clock) DurationSinceMidnight() time.Duration { + return time.Duration(c) * time.Millisecond +} + +// Add returns a new Clock offset from this clock specified hour, minute, second and millisecond. +// The parameters can be negative. // If required, use Mod() to correct any overflow or underflow. -func (c Clock) Add(h, m, s int) Clock { - hns := Clock(h) * ClockHour - mns := Clock(m) * ClockMinute - sns := Clock(s) * ClockSecond - return c + hns + mns + sns +func (c Clock) Add(h, m, s, ms int) Clock { + hx := Clock(h) * ClockHour + mx := Clock(m) * ClockMinute + sx := Clock(s) * ClockSecond + return c + hx + mx + sx + Clock(ms) } // Parse converts a string representation to a Clock. Acceptable representations @@ -81,11 +104,11 @@ func Parse(hms string) (clock Clock, err error) { return 0, fmt.Errorf("date.ParseClock: cannot parse %s", hms) } -func parseClockParts(hms, hh, mm, ss, nnnns string) (clock Clock, err error) { +func parseClockParts(hms, hh, mm, ss, mmms string) (clock Clock, err error) { h := 0 m := 0 s := 0 - ns := 0 + ms := 0 if hh != "" { h, err = strconv.Atoi(hh) if err != nil { @@ -104,13 +127,13 @@ func parseClockParts(hms, hh, mm, ss, nnnns string) (clock Clock, err error) { return 0, fmt.Errorf("date.ParseClock: cannot parse %s: %v", hms, err) } } - if nnnns != "" { - ns, err = strconv.Atoi(nnnns) + if mmms != "" { + ms, err = strconv.Atoi(mmms) if err != nil { return 0, fmt.Errorf("date.ParseClock: cannot parse %s: %v", hms, err) } } - return New(h, m, s) + Clock(ns), nil + return New(h, m, s, ms), nil } // IsInOneDay tests whether a clock time is in the range 0 to 24 hours, inclusive. Inside this @@ -173,10 +196,10 @@ func (c Clock) Seconds() int { return int(clockSeconds(c.Mod24())) } -// Nanosec gets the clock-face number of nanoseconds (calculated from the modulo time, see Mod24). -// For example, for 10:20:30.456111222 this will return 456111222. -func (c Clock) Nanosec() int64 { - return int64(clockNanosec(c.Mod24())) +// Millisec gets the clock-face number of milliseconds (calculated from the modulo time, see Mod24). +// For example, for 10:20:30.456 this will return 456. +func (c Clock) Millisec() int { + return int(clockMillisec(c.Mod24())) } func clockHours(cm Clock) Clock { @@ -184,15 +207,15 @@ func clockHours(cm Clock) Clock { } func clockMinutes(cm Clock) Clock { - return (cm - clockHours(cm) * ClockHour) / ClockMinute + return (cm % ClockHour) / ClockMinute } func clockSeconds(cm Clock) Clock { - return (cm - clockHours(cm) * ClockHour - clockMinutes(cm) * ClockMinute) / ClockSecond + return (cm % ClockMinute) / ClockSecond } -func clockNanosec(cm Clock) Clock { - return cm - clockHours(cm) * ClockHour - clockMinutes(cm) * ClockMinute - clockSeconds(cm) * ClockSecond +func clockMillisec(cm Clock) Clock { + return cm % ClockSecond } // Hh gets the clock-face number of hours as a two-digit string (calculated from the modulo time, see Mod24). @@ -215,9 +238,9 @@ func (c Clock) HhMmSs() string { return fmt.Sprintf("%02d:%02d:%02d", clockHours(cm), clockMinutes(cm), clockSeconds(cm)) } -// String gets the clock-face number of hours, minutes, seconds and nanoseconds as an 18-character ISO-8601 -// time string (calculated from the modulo time, see Mod24). +// String gets the clock-face number of hours, minutes, seconds and milliseconds as a 12-character ISO-8601 +// time string (calculated from the modulo time, see Mod24), specified to the nearest millisecond. func (c Clock) String() string { cm := c.Mod24() - return fmt.Sprintf("%02d:%02d:%02d.%09d", clockHours(cm), clockMinutes(cm), clockSeconds(cm), clockNanosec(cm)) + return fmt.Sprintf("%02d:%02d:%02d.%03d", clockHours(cm), clockMinutes(cm), clockSeconds(cm), clockMillisec(cm)) } diff --git a/clock/clock_test.go b/clock/clock_test.go index 1178dbc6..03abafcf 100644 --- a/clock/clock_test.go +++ b/clock/clock_test.go @@ -11,20 +11,39 @@ import ( func TestClockHoursMinutesSeconds(t *testing.T) { cases := []struct { - in Clock - h, m, s int + in Clock + h, m, s, ms int }{ - {New(0, 0, 0), 0, 0, 0}, - {New(1, 2, 3), 1, 2, 3}, - {New(23, 59, 59), 23, 59, 59}, - {New(0, 0, -1), 23, 59, 59}, - } - for _, c := range cases { - h := c.in.Hours() - m := c.in.Minutes() - s := c.in.Seconds() - if h != c.h || m != c.m || s != c.s { - t.Errorf("got %d %d %d, want %v", h, m, s, c.in) + {New(0, 0, 0, 0), 0, 0, 0, 0}, + {New(1, 2, 3, 4), 1, 2, 3, 4}, + {New(23, 59, 59, 999), 23, 59, 59, 999}, + {New(0, 0, 0, -1), 23, 59, 59, 999}, + {NewAt(time.Date(2015, 12, 4, 18, 50, 42, 173444111, time.UTC)), 18, 50, 42, 173}, + } + for i, x := range cases { + h := x.in.Hours() + m := x.in.Minutes() + s := x.in.Seconds() + ms := x.in.Millisec() + if h != x.h || m != x.m || s != x.s || ms != x.ms { + t.Errorf("%d: got %02d:%02d:%02d.%03d, want %v (%d)", i, h, m, s, ms, x.in, x.in) + } + } +} + +func TestClockSinceMidnight(t *testing.T) { + cases := []struct { + in Clock + d time.Duration + }{ + {New(1, 2, 3, 4), time.Hour + 2 * time.Minute + 3 * time.Second + 4}, + {New(23, 59, 59, 999), 23 * time.Hour + 59 * time.Minute + 59 * time.Second + 999}, + } + for i, x := range cases { + d := x.in.DurationSinceMidnight() + c2 := SinceMidnight(d) + if c2 != x.in { + t.Errorf("%d: got %v, want %v (%d)", i, c2, x.in, x.in) } } } @@ -34,42 +53,44 @@ func TestClockIsInOneDay(t *testing.T) { in Clock want bool }{ - {New(0, 0, 0), true}, - {New(24, 0, 0), true}, - {New(-24, 0, 0), false}, - {New(48, 0, 0), false}, - {New(0, 0, 1), true}, - {New(2, 0, 1), true}, - {New(-1, 0, 0), false}, - {New(0, 0, -1), false}, - } - for _, c := range cases { - got := c.in.IsInOneDay() - if got != c.want { - t.Errorf("%v got %v, want %v", c.in, c.in.IsInOneDay(), c.want) + {New(0, 0, 0, 0), true}, + {New(24, 0, 0, 0), true}, + {New(-24, 0, 0, 0), false}, + {New(48, 0, 0, 0), false}, + {New(0, 0, 0, 1), true}, + {New(2, 0, 0, 1), true}, + {New(-1, 0, 0, 0), false}, + {New(0, 0, 0, -1), false}, + } + for _, x := range cases { + got := x.in.IsInOneDay() + if got != x.want { + t.Errorf("%v got %v, want %v", x.in, x.in.IsInOneDay(), x.want) } } } func TestClockAdd(t *testing.T) { cases := []struct { - h, m, s int - in, want Clock + h, m, s, ms int + in, want Clock }{ - {0, 0, 0, 2 * ClockHour, New(2, 0, 0)}, - {0, 0, 1, 2 * ClockHour, New(2, 0, 1)}, - {0, 0, -1, 2 * ClockHour, New(1, 59, 59)}, - {0, 1, 0, 2 * ClockHour, New(2, 1, 0)}, - {0, -1, 0, 2 * ClockHour, New(1, 59, 0)}, - {1, 0, 0, 2 * ClockHour, New(3, 0, 0)}, - {-1, 0, 0, 2 * ClockHour, New(1, 0, 0)}, - {-2, 0, 0, 2 * ClockHour, New(0, 0, 0)}, - {-2, 0, -1, 2 * ClockHour, New(0, 0, -1)}, - } - for _, c := range cases { - got := c.in.Add(c.h, c.m, c.s) - if got != c.want { - t.Errorf("%d %d %d: got %v, want %v", c.h, c.m, c.s, got, c.want) + {0, 0, 0, 0, 2 * ClockHour, New(2, 0, 0, 0)}, + {0, 0, 0, 1, 2 * ClockHour, New(2, 0, 0, 1)}, + {0, 0, 0, -1, 2 * ClockHour, New(1, 59, 59, 999)}, + {0, 0, 1, 0, 2 * ClockHour, New(2, 0, 1, 0)}, + {0, 0, -1, 0, 2 * ClockHour, New(1, 59, 59, 0)}, + {0, 1, 0, 0, 2 * ClockHour, New(2, 1, 0, 0)}, + {0, -1, 0, 0, 2 * ClockHour, New(1, 59, 0, 0)}, + {1, 0, 0, 0, 2 * ClockHour, New(3, 0, 0, 0)}, + {-1, 0, 0, 0, 2 * ClockHour, New(1, 0, 0, 0)}, + {-2, 0, 0, 0, 2 * ClockHour, New(0, 0, 0, 0)}, + {-2, 0, -1, -1, 2 * ClockHour, New(0, 0, -1, -1)}, + } + for i, x := range cases { + got := x.in.Add(x.h, x.m, x.s, x.ms) + if got != x.want { + t.Errorf("%d: %d %d %d.%d: got %v, want %v", i, x.h, x.m, x.s, x.ms, got, x.want) } } } @@ -79,22 +100,22 @@ func TestClockIsMidnight(t *testing.T) { in Clock want bool }{ - {New(0, 0, 0), true}, + {New(0, 0, 0, 0), true}, {ClockDay, true}, {24 * ClockHour, true}, - {New(24, 0, 0), true}, - {New(-24, 0, 0), true}, - {New(-48, 0, 0), true}, - {New(48, 0, 0), true}, - {New(0, 0, 1), false}, - {New(2, 0, 1), false}, - {New(-1, 0, 0), false}, - {New(0, 0, -1), false}, - } - for i, c := range cases { - got := c.in.IsMidnight() - if got != c.want { - t.Errorf("%d: %v got %v, want %v, %d", i, c.in, c.in.IsMidnight(), c.want, c.in.Mod24()) + {New(24, 0, 0, 0), true}, + {New(-24, 0, 0, 0), true}, + {New(-48, 0, 0, 0), true}, + {New(48, 0, 0, 0), true}, + {New(0, 0, 0, 1), false}, + {New(2, 0, 0, 1), false}, + {New(-1, 0, 0, 0), false}, + {New(0, 0, 0, -1), false}, + } + for i, x := range cases { + got := x.in.IsMidnight() + if got != x.want { + t.Errorf("%d: %v got %v, want %v, %d", i, x.in, x.in.IsMidnight(), x.want, x.in.Mod24()) } } } @@ -114,14 +135,15 @@ func TestClockMod(t *testing.T) { {49 * ClockHour, ClockHour}, {-1 * ClockHour, 23 * ClockHour}, {-23 * ClockHour, ClockHour}, - {New(0, 0, 1), ClockSecond}, - {New(0, 0, -1), New(23, 59, 59)}, + {New(0, 0, 0, 1), 1}, + {New(0, 0, 1, 0), ClockSecond}, + {New(0, 0, 0, -1), New(23, 59, 59, 999)}, } - for i, c := range cases { - clock := c.h + for i, x := range cases { + clock := x.h got := clock.Mod24() - if got != c.want { - t.Errorf("%d: %dh: got %#v, want %#v", i, c.h, got, c.want) + if got != x.want { + t.Errorf("%d: %dh: got %#v, want %#v", i, x.h, got, x.want) } } } @@ -141,40 +163,40 @@ func TestClockDays(t *testing.T) { {-23, -1}, {-24, -2}, } - for i, c := range cases { - clock := Clock(c.h) * ClockHour - if clock.Days() != c.days { - t.Errorf("%d: %dh: got %v, want %v", i, c.h, clock.Days(), c.days) + for i, x := range cases { + clock := Clock(x.h) * ClockHour + if clock.Days() != x.days { + t.Errorf("%d: %dh: got %v, want %v", i, x.h, clock.Days(), x.days) } } } func TestClockString(t *testing.T) { cases := []struct { - h, m, s, ns time.Duration + h, m, s, ms Clock hh, hhmm, hhmmss, str string }{ - {0, 0, 0, 0, "00", "00:00", "00:00:00", "00:00:00.000000000"}, - {0, 0, 0, 1, "00", "00:00", "00:00:00", "00:00:00.000000001"}, - {0, 0, 1, 0, "00", "00:00", "00:00:01", "00:00:01.000000000"}, - {0, 1, 0, 0, "00", "00:01", "00:01:00", "00:01:00.000000000"}, - {1, 0, 0, 0, "01", "01:00", "01:00:00", "01:00:00.000000000"}, - {1, 2, 3, 4, "01", "01:02", "01:02:03", "01:02:03.000000004"}, - {-1, -1, -1, -1, "22", "22:58", "22:58:58", "22:58:58.999999999"}, - } - for _, c := range cases { - d := Clock(c.h * time.Hour + c.m * time.Minute + c.s * time.Second + c.ns) - if d.Hh() != c.hh { - t.Errorf("%d, %d, %d, %d, got %v, want %v", c.h, c.m, c.s, c.ns, d.Hh(), c.hh) + {0, 0, 0, 0, "00", "00:00", "00:00:00", "00:00:00.000"}, + {0, 0, 0, 1, "00", "00:00", "00:00:00", "00:00:00.001"}, + {0, 0, 1, 0, "00", "00:00", "00:00:01", "00:00:01.000"}, + {0, 1, 0, 0, "00", "00:01", "00:01:00", "00:01:00.000"}, + {1, 0, 0, 0, "01", "01:00", "01:00:00", "01:00:00.000"}, + {1, 2, 3, 4, "01", "01:02", "01:02:03", "01:02:03.004"}, + {-1, -1, -1, -1, "22", "22:58", "22:58:58", "22:58:58.999"}, + } + for _, x := range cases { + d := Clock(x.h * ClockHour + x.m * ClockMinute + x.s * ClockSecond + x.ms) + if d.Hh() != x.hh { + t.Errorf("%d, %d, %d, %d, got %v, want %v (%d)", x.h, x.m, x.s, x.ms, d.Hh(), x.hh, d) } - if d.HhMm() != c.hhmm { - t.Errorf("%d, %d, %d, %d, got %v, want %v", c.h, c.m, c.s, c.ns, d.HhMm(), c.hhmm) + if d.HhMm() != x.hhmm { + t.Errorf("%d, %d, %d, %d, got %v, want %v (%d)", x.h, x.m, x.s, x.ms, d.HhMm(), x.hhmm, d) } - if d.HhMmSs() != c.hhmmss { - t.Errorf("%d, %d, %d, %d, got %v, want %v", c.h, c.m, c.s, c.ns, d.HhMmSs(), c.hhmmss) + if d.HhMmSs() != x.hhmmss { + t.Errorf("%d, %d, %d, %d, got %v, want %v (%d)", x.h, x.m, x.s, x.ms, d.HhMmSs(), x.hhmmss, d) } - if d.String() != c.str { - t.Errorf("%d, %d, %d, %d, got %v, want %v", c.h, c.m, c.s, c.ns, d.String(), c.str) + if d.String() != x.str { + t.Errorf("%d, %d, %d, %d, got %v, want %v (%d)", x.h, x.m, x.s, x.ms, d.String(), x.str, d) } } } @@ -184,35 +206,35 @@ func TestClockParse(t *testing.T) { str string want Clock }{ - {"00", New(0, 0, 0)}, - {"01", New(1, 0, 0)}, - {"23", New(23, 0, 0)}, - {"00:00", New(0, 0, 0)}, - {"00:01", New(0, 1, 0)}, - {"01:00", New(1, 0, 0)}, - {"01:02", New(1, 2, 0)}, - {"23:59", New(23, 59, 0)}, - {"00:00:00", New(0, 0, 0)}, - {"00:00:01", New(0, 0, 1)}, - {"00:01:00", New(0, 1, 0)}, - {"01:00:00", New(1, 0, 0)}, - {"01:02:03", New(1, 2, 3)}, - {"23:59:59", New(23, 59, 59)}, - {"00:00:00.000000000", New(0, 0, 0)}, - {"00:00:00.000000001", New(0, 0, 0) + 1}, - {"00:00:01.000000000", New(0, 0, 1)}, - {"00:01:00.000000000", New(0, 1, 0)}, - {"01:00:00.000000000", New(1, 0, 0)}, - {"01:02:03.000000004", New(1, 2, 3) + 4}, - {"23:59:59.999999999", New(23, 59, 59) + 999999999}, - } - for _, c := range cases { - str, err := Parse(c.str) + {"00", New(0, 0, 0, 0)}, + {"01", New(1, 0, 0, 0)}, + {"23", New(23, 0, 0, 0)}, + {"00:00", New(0, 0, 0, 0)}, + {"00:01", New(0, 1, 0, 0)}, + {"01:00", New(1, 0, 0, 0)}, + {"01:02", New(1, 2, 0, 0)}, + {"23:59", New(23, 59, 0, 0)}, + {"00:00:00", New(0, 0, 0, 0)}, + {"00:00:01", New(0, 0, 1, 0)}, + {"00:01:00", New(0, 1, 0, 0)}, + {"01:00:00", New(1, 0, 0, 0)}, + {"01:02:03", New(1, 2, 3, 0)}, + {"23:59:59", New(23, 59, 59, 0)}, + {"00:00:00.000", New(0, 0, 0, 0)}, + {"00:00:00.001", New(0, 0, 0, 1)}, + {"00:00:01.000", New(0, 0, 1, 0)}, + {"00:01:00.000", New(0, 1, 0, 0)}, + {"01:00:00.000", New(1, 0, 0, 0)}, + {"01:02:03.004", New(1, 2, 3, 4)}, + {"23:59:59.999", New(23, 59, 59, 999)}, + } + for _, x := range cases { + str, err := Parse(x.str) if err != nil { - t.Errorf("%s, error %v", c.str, err) + t.Errorf("%s, error %v", x.str, err) } - if str != c.want { - t.Errorf("%s, got %v, want %v", c.str, str, c.want) + if str != x.want { + t.Errorf("%s, got %v, want %v", x.str, str, x.want) } } } From 552f750c39dd28db8ecdefc62b2beb9f169b8a2a Mon Sep 17 00:00:00 2001 From: Rick Date: Sun, 6 Dec 2015 20:38:36 +0000 Subject: [PATCH 028/165] Added IsLeap and DaysIn functions for general-purpose date handling. --- date.go | 24 +++++++++++++++++++++ date_test.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/date.go b/date.go index 03e457d0..dd6048f9 100644 --- a/date.go +++ b/date.go @@ -40,6 +40,7 @@ package date import ( "math" "time" + "fmt" ) // A Date represents a date under the (proleptic) Gregorian calendar as @@ -242,3 +243,26 @@ func (d Date) AddDate(years, months, days int) Date { func (d Date) Sub(u Date) (days PeriodOfDays) { return PeriodOfDays(d.day - u.day) } + +// IsLeap simply tests whether a given year is a leap year, using the Gregorian calendar algorithm. +func IsLeap(year int) bool { + return year%4 == 0 && (year%100 != 0 || year%400 == 0) +} + +// DaysIn gives the number of days in a given month, according to the Gregorian calendar. +func DaysIn(year int, month time.Month) int { + switch month { + case time.January, time.March, time.May, time.July, time.August, time.October, time.December: + return 31 + + case time.September, time.April, time.June, time.November: + return 30 + + case time.February: + if IsLeap(year) { + return 29 + } + return 28 + } + panic(fmt.Sprintf("Not valid: year %d month %d", year, month)) +} diff --git a/date_test.go b/date_test.go index ff4b568f..d632a032 100644 --- a/date_test.go +++ b/date_test.go @@ -14,11 +14,11 @@ func same(d Date, t time.Time) bool { yd, wd := d.ISOWeek() yt, wt := t.ISOWeek() return d.Year() == t.Year() && - d.Month() == t.Month() && - d.Day() == t.Day() && - d.Weekday() == t.Weekday() && - d.YearDay() == t.YearDay() && - yd == yt && wd == wt + d.Month() == t.Month() && + d.Day() == t.Day() && + d.Weekday() == t.Weekday() && + d.YearDay() == t.YearDay() && + yd == yt && wd == wt } func TestNew(t *testing.T) { @@ -63,7 +63,7 @@ func TestToday(t *testing.T) { } cases := []int{-10, -5, -3, 0, 1, 4, 8, 12} for _, c := range cases { - location := time.FixedZone("zone", c*60*60) + location := time.FixedZone("zone", c * 60 * 60) today = TodayIn(location) now = time.Now().In(location) if !same(today, now) { @@ -105,7 +105,7 @@ func TestTime(t *testing.T) { t.Errorf("TimeLocal(%v) == %v, want %v", d, tLocal.Location(), time.Local) } for _, z := range zones { - location := time.FixedZone("zone", z*60*60) + location := time.FixedZone("zone", z * 60 * 60) tInLoc := d.In(location) if !same(d, tInLoc) { t.Errorf("TimeIn(%v) == %v, want date part %v", d, tInLoc, d) @@ -217,3 +217,49 @@ func max(a, b int32) int32 { } return b } + +func TestIsLeap(t *testing.T) { + cases := []struct { + year int + expected bool + }{ + {2000, true}, + {2400, true}, + {2001, false}, + {2002, false}, + {2003, false}, + {2003, false}, + {2004, true}, + {2005, false}, + {1800, false}, + {1900, false}, + {2200, false}, + {2300, false}, + {2500, false}, + } + for _, c := range cases { + got := IsLeap(c.year) + if got != c.expected { + t.Errorf("TestIsLeap(%d) == %v, want %v", c.year, got, c.expected) + } + } +} + +func TestDaysIn(t *testing.T) { + cases := []struct { + year int + month time.Month + expected int + }{ + {2000, time.January, 31}, + {2000, time.February, 29}, + {2001, time.February, 28}, + {2001, time.April, 30}, + } + for _, c := range cases { + got := DaysIn(c.year, c.month) + if got != c.expected { + t.Errorf("TestIsLeap(%d) == %v, want %v", c.year, got, c.expected) + } + } +} From 8550750370264dd83fba9c524e9332753e9a504e Mon Sep 17 00:00:00 2001 From: Rick Date: Sun, 6 Dec 2015 20:51:41 +0000 Subject: [PATCH 029/165] LastDayOfMonth added --- date.go | 7 +++++++ date_test.go | 11 ++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/date.go b/date.go index dd6048f9..ef38e444 100644 --- a/date.go +++ b/date.go @@ -147,6 +147,13 @@ func (d Date) Date() (year int, month time.Month, day int) { return t.Date() } +// LastDayOfMonth returns the last day of the month specified by d. +// The first day of the month is 1. +func (d Date) LastDayOfMonth() int { + y, m, _ := d.Date() + return DaysIn(y, m) +} + // Day returns the day of the month specified by d. // The first day of the month is 1. func (d Date) Day() int { diff --git a/date_test.go b/date_test.go index d632a032..1966a64f 100644 --- a/date_test.go +++ b/date_test.go @@ -257,9 +257,14 @@ func TestDaysIn(t *testing.T) { {2001, time.April, 30}, } for _, c := range cases { - got := DaysIn(c.year, c.month) - if got != c.expected { - t.Errorf("TestIsLeap(%d) == %v, want %v", c.year, got, c.expected) + got1 := DaysIn(c.year, c.month) + if got1 != c.expected { + t.Errorf("DaysIn(%d) == %v, want %v", c.year, got1, c.expected) + } + d := New(c.year, c.month, 1) + got2 := d.LastDayOfMonth() + if got2 != c.expected { + t.Errorf("DaysIn(%d) == %v, want %v", c.year, got2, c.expected) } } } From be2d89ad750f95f2601aa61a698deca76c2a82f9 Mon Sep 17 00:00:00 2001 From: Rick Date: Sat, 19 Dec 2015 11:48:13 +0000 Subject: [PATCH 030/165] Added MustParseXxx methods - mostly to simplify startup and test code. --- clock/clock.go | 9 +++++++++ clock/clock_test.go | 5 +---- format.go | 18 ++++++++++++++++++ format_test.go | 28 +++++----------------------- 4 files changed, 33 insertions(+), 27 deletions(-) diff --git a/clock/clock.go b/clock/clock.go index eb5d6b77..ed07ba06 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -70,6 +70,15 @@ func (c Clock) Add(h, m, s, ms int) Clock { return c + hx + mx + sx + Clock(ms) } +// MustParse is as per Parse except that it panics if the string cannot be parsed. +func MustParse(hms string) Clock { + t, err := Parse(hms) + if err != nil { + panic(err) + } + return t +} + // Parse converts a string representation to a Clock. Acceptable representations // are as per ISO-8601 - see https://en.wikipedia.org/wiki/ISO_8601#Times func Parse(hms string) (clock Clock, err error) { diff --git a/clock/clock_test.go b/clock/clock_test.go index 03abafcf..a3984a8b 100644 --- a/clock/clock_test.go +++ b/clock/clock_test.go @@ -229,10 +229,7 @@ func TestClockParse(t *testing.T) { {"23:59:59.999", New(23, 59, 59, 999)}, } for _, x := range cases { - str, err := Parse(x.str) - if err != nil { - t.Errorf("%s, error %v", x.str, err) - } + str := MustParse(x.str) if str != x.want { t.Errorf("%s, got %v, want %v", x.str, str, x.want) } diff --git a/format.go b/format.go index cef8f1a7..74f0b204 100644 --- a/format.go +++ b/format.go @@ -31,6 +31,15 @@ const ( RFC3339 = "2006-01-02" ) +// MustParseISO is as per ParseISO except that it panics if the string cannot be parsed. +func MustParseISO(value string) Date { + d, err := ParseISO(value) + if err != nil { + panic(err) + } + return d +} + // ParseISO parses an ISO 8601 formatted string and returns the date value it represents. // In addition to the common formats (e.g. 2006-01-02 and 20060102), this function // accepts date strings using the expanded year representation @@ -113,6 +122,15 @@ func parseField(value, field, name string, minLength, requiredLength int) (int, return number, nil } +// MustParse is as per Parse except that it panics if the string cannot be parsed. +func MustParse(layout, value string) Date { + d, err := Parse(layout, value) + if err != nil { + panic(err) + } + return d +} + // Parse parses a formatted string and returns the Date value it represents. // The layout defines the format by showing how the reference date, defined // to be diff --git a/format_test.go b/format_test.go index ad1edf70..a8d3801d 100644 --- a/format_test.go +++ b/format_test.go @@ -45,10 +45,7 @@ func TestParseISO(t *testing.T) { {"-00191012", -19, time.October, 12}, } for _, c := range cases { - d, err := ParseISO(c.value) - if err != nil { - t.Errorf("ParseISO(%v) == %v", c.value, err) - } + d := MustParseISO(c.value) year, month, day := d.Date() if year != c.year || month != c.month || day != c.day { t.Errorf("ParseISO(%v) == %v, want (%v, %v, %v)", c.value, d, c.year, c.month, c.day) @@ -126,10 +123,7 @@ func TestParse(t *testing.T) { {RFC3339, "2345-06-07", 2345, time.June, 7}, } for _, c := range cases { - d, err := Parse(c.layout, c.value) - if err != nil { - t.Errorf("Parse(%v) == %v", c.value, err) - } + d := MustParse(c.layout, c.value) year, month, day := d.Date() if year != c.year || month != c.month || day != c.day { t.Errorf("Parse(%v) == %v, want (%v, %v, %v)", c.value, d, c.year, c.month, c.day) @@ -190,11 +184,7 @@ func TestString(t *testing.T) { {"+10000-01-01"}, } for _, c := range cases { - d, err := ParseISO(c.value) - if err != nil { - t.Errorf("ParseISO(%v) cannot parse input: %v", c.value, err) - continue - } + d := MustParseISO(c.value) value := d.String() if value != c.value { t.Errorf("String() == %v, want %v", value, c.value) @@ -219,11 +209,7 @@ func TestFormatISO(t *testing.T) { {"+999999-12-31", 6}, } for _, c := range cases { - d, err := ParseISO(c.value) - if err != nil { - t.Errorf("ParseISO(%v) cannot parse input: %v", c.value, err) - continue - } + d := MustParseISO(c.value) value := d.FormatISO(c.n) if value != c.value { t.Errorf("FormatISO(%v) == %v, want %v", c, value, c.value) @@ -253,11 +239,7 @@ func TestFormat(t *testing.T) { {"2016-11-01", "2nd 2nd 2nd", "1st 1st 1st"}, } for _, c := range cases { - d, err := ParseISO(c.value) - if err != nil { - t.Errorf("ParseISO(%v) cannot parse input: %v", c.value, err) - continue - } + d := MustParseISO(c.value) actual := d.Format(c.format) if actual != c.expected { t.Errorf("Format(%v) == %v, want %v", c, actual, c.expected) From 6e2eda787b08a95f6fe309accd8163966525753f Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 29 Dec 2015 17:00:45 +0000 Subject: [PATCH 031/165] New date.AutoParse function; new tool for viewing dates/clocks as numbers and vice versa. --- date.go | 3 ++ datetool/main.go | 60 ++++++++++++++++++++++++++++++++++++++++ format.go | 65 ++++++++++++++++++++++++++++++++++++------- format_test.go | 72 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+), 10 deletions(-) create mode 100644 datetool/main.go diff --git a/date.go b/date.go index ef38e444..f92c0d06 100644 --- a/date.go +++ b/date.go @@ -75,6 +75,9 @@ type Date struct { // indicate days earlier than some mark. type PeriodOfDays int32 +// ZeroDays is the named zero value for PeriodOfDays. +const ZeroDays PeriodOfDays = 0 + // New returns the Date value corresponding to the given year, month, and day. // // The month and day may be outside their usual ranges and will be normalized diff --git a/datetool/main.go b/datetool/main.go new file mode 100644 index 00000000..b23d9034 --- /dev/null +++ b/datetool/main.go @@ -0,0 +1,60 @@ +// This tool prints equivalences between the string representation and the internal numerical +// representation for dates and clocks. + +package main + +import ( + "os" + "strings" + . "github.com/rickb777/date" + "github.com/rickb777/date/clock" + "fmt" + "strconv" +) + +func printPair(a string, b interface{}) { + fmt.Printf("%-12s %12v\n", a, b) +} + +func printOneDate(s string, d Date, err error) { + if err != nil { + printPair(s, err.Error()) + } else { + printPair(s, d.Sub(Date{})) + } +} + +func printOneClock(s string, c clock.Clock, err error) { + if err != nil { + printPair(s, err.Error()) + } else { + printPair(s, int32(c)) + } +} + +func printArg(arg string) { + d := Date{} + + d, e1 := AutoParse(arg) + if e1 == nil { + printPair(arg, d.Sub(Date{})) + } else if strings.Index(arg, ":") == 2 { + c, err := clock.Parse(arg) + printOneClock(arg, c, err) + } else { + i, err := strconv.Atoi(arg) + if err == nil { + d = d.Add(PeriodOfDays(i)) + fmt.Printf("%-12s %12s %s\n", arg, d, clock.Clock(i)) + } else { + printPair(arg, err) + } + } +} + +func main() { + argsWithoutProg := os.Args[1:] + for _, arg := range argsWithoutProg { + printArg(arg) + } +} diff --git a/format.go b/format.go index 74f0b204..88efae48 100644 --- a/format.go +++ b/format.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" "time" + "unicode" ) // These are predefined layouts for use in Date.Format and Date.Parse. @@ -21,14 +22,14 @@ import ( // so that the Parse function and Format method can apply the same // transformation to a general date value. const ( - ISO8601 = "2006-01-02" // ISO 8601 extended format + ISO8601 = "2006-01-02" // ISO 8601 extended format ISO8601B = "20060102" // ISO 8601 basic format - RFC822 = "02-Jan-06" - RFC822W = "Mon, 02-Jan-06" // RFC822 with day of the week - RFC850 = "Monday, 02-Jan-06" - RFC1123 = "02 Jan 2006" + RFC822 = "02-Jan-06" + RFC822W = "Mon, 02-Jan-06" // RFC822 with day of the week + RFC850 = "Monday, 02-Jan-06" + RFC1123 = "02 Jan 2006" RFC1123W = "Mon, 02 Jan 2006" // RFC1123 with day of the week - RFC3339 = "2006-01-02" + RFC3339 = "2006-01-02" ) // MustParseISO is as per ParseISO except that it panics if the string cannot be parsed. @@ -40,6 +41,50 @@ func MustParseISO(value string) Date { return d } +// AutoParse is like ParseISO, except that it automatically adapts to a variety of date formats +// provided that they can be detected unambiguously. The supported formats are: +// +// all formats supported by ParseISO +// +// yyyy/mm/dd | yyyy.mm.dd (or any similar pattern) +// +// dd/mm/yyyy | dd.mm.yyyy (or any similar pattern) +// +func AutoParse(value string) (Date, error) { + abs := strings.TrimSpace(value) + lead := "" + if value[0] == '+' || value[0] == '-' { + abs = value[1:] + lead = value[:1] + } + + if len(abs) >= 10 { + i1 := -1 + i2 := -1 + for i, r := range abs { + if unicode.IsPunct(r) { + if i1 < 0 { + i1 = i + } else { + i2 = i + } + } + } + if i1 >= 4 && i2 > i1 && abs[i1] == abs[i2] { + yyyy := abs[:i1] + mm := abs[i1 + 1:i2] + dd := abs[i2 + 1:] + abs = fmt.Sprintf("%s-%s-%s", yyyy, mm, dd) + } else if i1 >= 2 && i2 > i1 && abs[i1] == abs[i2] { + dd := abs[0:i1] + mm := abs[i1 + 1:i2] + yyyy := abs[i2 + 1:] + abs = fmt.Sprintf("%s-%s-%s", yyyy, mm, dd) + } + } + return ParseISO(lead + abs) +} + // ParseISO parses an ISO 8601 formatted string and returns the date value it represents. // In addition to the common formats (e.g. 2006-01-02 and 20060102), this function // accepts date strings using the expanded year representation @@ -50,7 +95,7 @@ func MustParseISO(value string) Date { // be happy to parse dates with a year longer in length than the four-digit minimum even // if they are missing the + sign prefix. // -// Function Date.Parse can be used to parse date strings in other formats, but it +// Function date.Parse can be used to parse date strings in other formats, but it // is currently not able to parse ISO 8601 formatted strings that use the // expanded year format. // @@ -131,7 +176,7 @@ func MustParse(layout, value string) Date { return d } -// Parse parses a formatted string and returns the Date value it represents. +// Parse parses a formatted string of a known layout and returns the Date value it represents. // The layout defines the format by showing how the reference date, defined // to be // Monday, Jan 2, 2006 @@ -219,10 +264,10 @@ func (d Date) FormatWithSuffixes(layout string, suffixes []string) string { return t.Format(layout) default: - a := make([]string, 0, 2*len(parts)-1) + a := make([]string, 0, 2 * len(parts) - 1) for i, p := range parts { if i > 0 { - a = append(a, suffixes[d.Day()-1]) + a = append(a, suffixes[d.Day() - 1]) } a = append(a, t.Format(p)) } diff --git a/format_test.go b/format_test.go index a8d3801d..34c6648b 100644 --- a/format_test.go +++ b/format_test.go @@ -9,6 +9,78 @@ import ( "time" ) +func TestAutoParse(t *testing.T) { + cases := []struct { + value string + year int + month time.Month + day int + }{ + {"31/12/1969", 1969, time.December, 31}, + {"1969/12/31", 1969, time.December, 31}, + {"1969.12.31", 1969, time.December, 31}, + {"1969-12-31", 1969, time.December, 31}, + {"+1970-01-01", 1970, time.January, 1}, + {"+01970-01-02", 1970, time.January, 2}, + {"2000-02-28", 2000, time.February, 28}, + {"+2000-02-29", 2000, time.February, 29}, + {"+02000-03-01", 2000, time.March, 1}, + {"+002004-02-28", 2004, time.February, 28}, + {"2004-02-29", 2004, time.February, 29}, + {"2004-03-01", 2004, time.March, 1}, + {"0000-01-01", 0, time.January, 1}, + {"+0001-02-03", 1, time.February, 3}, + {"+00019-03-04", 19, time.March, 4}, + {"0100-04-05", 100, time.April, 5}, + {"2000-05-06", 2000, time.May, 6}, + {"+5000000-08-09", 5000000, time.August, 9}, + {"-0001-09-11", -1, time.September, 11}, + {"-0019-10-12", -19, time.October, 12}, + {"-00100-11-13", -100, time.November, 13}, + {"-02000-12-14", -2000, time.December, 14}, + {"-30000-02-15", -30000, time.February, 15}, + {"-0400000-05-16", -400000, time.May, 16}, + {"-5000000-09-17", -5000000, time.September, 17}, + {"12340506", 1234, time.May, 6}, + {"+12340506", 1234, time.May, 6}, + {"-00191012", -19, time.October, 12}, + } + for _, c := range cases { + d, err := AutoParse(c.value) + if err != nil { + t.Errorf("FlexibleParse(%v) == %v", c.value, err) + } + year, month, day := d.Date() + if year != c.year || month != c.month || day != c.day { + t.Errorf("ParseISO(%v) == %v, want (%v, %v, %v)", c.value, d, c.year, c.month, c.day) + } + } + + badCases := []string{ + "1234-05", + "1234-5-6", + "1234-05-6", + "1234-5-06", + "1234-0A-06", + "1234-05-0B", + "1234-05-06trailing", + "padding1234-05-06", + "1-02-03", + "10-11-12", + "100-02-03", + "+1-02-03", + "+10-11-12", + "+100-02-03", + "-123-05-06", + } + for _, c := range badCases { + d, err := AutoParse(c) + if err == nil { + t.Errorf("ParseISO(%v) == %v", c, d) + } + } +} + func TestParseISO(t *testing.T) { cases := []struct { value string From 139383e835f88f4e326768829c28ebb8a283a5fe Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 29 Dec 2015 17:57:32 +0000 Subject: [PATCH 032/165] Added direct access methods NewOfDays and DaysSinceEpoch --- date.go | 13 ++++++++++++- date_test.go | 13 +++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/date.go b/date.go index f92c0d06..4cd69f2f 100644 --- a/date.go +++ b/date.go @@ -63,7 +63,7 @@ import ( // them. // The Add method adds a Date and a number of days, producing a Date. // -// The zero value of type Date is Thursday, January 1, 1970. +// The zero value of type Date is Thursday, January 1, 1970 (called 'the epoch'). // As this date is unlikely to come up in practice, the IsZero method gives // a simple way of detecting a date that has not been initialized explicitly. // @@ -94,6 +94,12 @@ func NewAt(t time.Time) Date { return Date{encode(t)} } +// NewOfDays returns the Date value corresponding to the given period since the +// epoch (1st January 1970), which may be negative. +func NewOfDays(p PeriodOfDays) Date { + return Date{int32(p)} +} + // Today returns today's date according to the current local time. func Today() Date { t := time.Now() @@ -254,6 +260,11 @@ func (d Date) Sub(u Date) (days PeriodOfDays) { return PeriodOfDays(d.day - u.day) } +// DaysSinceEpoch returns the number of days since the epoch (1st January 1970), which may be negative. +func (d Date) DaysSinceEpoch() (days PeriodOfDays) { + return PeriodOfDays(d.day) +} + // IsLeap simply tests whether a given year is a leap year, using the Gregorian calendar algorithm. func IsLeap(year int) bool { return year%4 == 0 && (year%100 != 0 || year%400 == 0) diff --git a/date_test.go b/date_test.go index 1966a64f..7102f92a 100644 --- a/date_test.go +++ b/date_test.go @@ -50,6 +50,19 @@ func TestNew(t *testing.T) { } } +func TestDaysSinceEpoch(t *testing.T) { + zero := Date{}.DaysSinceEpoch() + if zero != 0 { + t.Errorf("Non zero %v", zero) + } + today := Today() + days := today.DaysSinceEpoch() + copy := NewOfDays(days) + if today != copy || days == 0 { + t.Errorf("Today == %v, want date of %v", today, copy) + } +} + func TestToday(t *testing.T) { today := Today() now := time.Now() From 9cc3fbd3decfe01e141ad879b5145888bed328ae Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 29 Dec 2015 20:04:23 +0000 Subject: [PATCH 033/165] Improved documentation; new MustAutoParse function. --- clock/clock.go | 1 + format.go | 46 +++++++++++++++++++++++++++++++--------------- format_test.go | 5 +---- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/clock/clock.go b/clock/clock.go index ed07ba06..013051ed 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -71,6 +71,7 @@ func (c Clock) Add(h, m, s, ms int) Clock { } // MustParse is as per Parse except that it panics if the string cannot be parsed. +// This is intended for setup code; don't use it for user inputs. func MustParse(hms string) Clock { t, err := Parse(hms) if err != nil { diff --git a/format.go b/format.go index 88efae48..e1ed29bd 100644 --- a/format.go +++ b/format.go @@ -32,9 +32,10 @@ const ( RFC3339 = "2006-01-02" ) -// MustParseISO is as per ParseISO except that it panics if the string cannot be parsed. -func MustParseISO(value string) Date { - d, err := ParseISO(value) +// MustAutoParse is as per AutoParse except that it panics if the string cannot be parsed. +// This is intended for setup code; don't use it for user inputs. +func MustAutoParse(value string) Date { + d, err := AutoParse(value) if err != nil { panic(err) } @@ -42,20 +43,22 @@ func MustParseISO(value string) Date { } // AutoParse is like ParseISO, except that it automatically adapts to a variety of date formats -// provided that they can be detected unambiguously. The supported formats are: +// provided that they can be detected unambiguously. Specifically, this includes the "European" +// and "British" date formats but not the common US format. Surrounding whitespace is ignored. +// The supported formats are: // -// all formats supported by ParseISO +// * all formats supported by ParseISO // -// yyyy/mm/dd | yyyy.mm.dd (or any similar pattern) +// * yyyy/mm/dd | yyyy.mm.dd (or any similar pattern) // -// dd/mm/yyyy | dd.mm.yyyy (or any similar pattern) +// * dd/mm/yyyy | dd.mm.yyyy (or any similar pattern) // func AutoParse(value string) (Date, error) { abs := strings.TrimSpace(value) - lead := "" + sign := "" if value[0] == '+' || value[0] == '-' { abs = value[1:] - lead = value[:1] + sign = value[:1] } if len(abs) >= 10 { @@ -71,18 +74,30 @@ func AutoParse(value string) (Date, error) { } } if i1 >= 4 && i2 > i1 && abs[i1] == abs[i2] { - yyyy := abs[:i1] - mm := abs[i1 + 1:i2] - dd := abs[i2 + 1:] - abs = fmt.Sprintf("%s-%s-%s", yyyy, mm, dd) + // just normalise the punctuation + a := []byte(abs) + a[i1] = '-' + a[i2] = '-' + abs = string(a) } else if i1 >= 2 && i2 > i1 && abs[i1] == abs[i2] { + // harder case - need to swap the field order dd := abs[0:i1] mm := abs[i1 + 1:i2] yyyy := abs[i2 + 1:] abs = fmt.Sprintf("%s-%s-%s", yyyy, mm, dd) } } - return ParseISO(lead + abs) + return ParseISO(sign + abs) +} + +// MustParseISO is as per ParseISO except that it panics if the string cannot be parsed. +// This is intended for setup code; don't use it for user inputs. +func MustParseISO(value string) Date { + d, err := ParseISO(value) + if err != nil { + panic(err) + } + return d } // ParseISO parses an ISO 8601 formatted string and returns the date value it represents. @@ -168,6 +183,7 @@ func parseField(value, field, name string, minLength, requiredLength int) (int, } // MustParse is as per Parse except that it panics if the string cannot be parsed. +// This is intended for setup code; don't use it for user inputs. func MustParse(layout, value string) Date { d, err := Parse(layout, value) if err != nil { @@ -188,7 +204,7 @@ func MustParse(layout, value string) Date { // parsed Time value. // // This function cannot currently parse ISO 8601 strings that use the expanded -// year format; you should use Date.ParseISO to parse those strings correctly. +// year format; you should use date.ParseISO to parse those strings correctly. func Parse(layout, value string) (Date, error) { t, err := time.Parse(layout, value) if err != nil { diff --git a/format_test.go b/format_test.go index 34c6648b..22d5c82d 100644 --- a/format_test.go +++ b/format_test.go @@ -46,10 +46,7 @@ func TestAutoParse(t *testing.T) { {"-00191012", -19, time.October, 12}, } for _, c := range cases { - d, err := AutoParse(c.value) - if err != nil { - t.Errorf("FlexibleParse(%v) == %v", c.value, err) - } + d := MustAutoParse(c.value) year, month, day := d.Date() if year != c.year || month != c.month || day != c.day { t.Errorf("ParseISO(%v) == %v, want (%v, %v, %v)", c.value, d, c.year, c.month, c.day) From aa417a9466cfd9dd6c4f25a639586c022098a5a5 Mon Sep 17 00:00:00 2001 From: Rick Date: Wed, 30 Dec 2015 20:39:14 +0000 Subject: [PATCH 034/165] Comments --- date.go | 1 + 1 file changed, 1 insertion(+) diff --git a/date.go b/date.go index 4cd69f2f..fb02611a 100644 --- a/date.go +++ b/date.go @@ -151,6 +151,7 @@ func (d Date) In(loc *time.Location) time.Time { } // Date returns the year, month, and day of d. +// The first day of the month is 1. func (d Date) Date() (year int, month time.Month, day int) { t := decode(d.day) return t.Date() From 31c200e6d78373a45d84824a9f82b6bc02d92c2e Mon Sep 17 00:00:00 2001 From: Rick Date: Sun, 3 Jan 2016 10:29:49 +0000 Subject: [PATCH 035/165] Added Undefined constant and revised the documentation --- clock/clock.go | 28 ++++++++++++++++++++-------- clock/clock_test.go | 6 +++--- date.go | 2 +- date_test.go | 14 +++++++------- datetool/main.go | 6 +++--- format.go | 20 ++++++++++---------- 6 files changed, 44 insertions(+), 32 deletions(-) diff --git a/clock/clock.go b/clock/clock.go index 013051ed..46bd03a8 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -6,12 +6,11 @@ package clock import ( "fmt" + "math" "strconv" "time" ) -const zero time.Duration = 0 - // Clock specifies a time of day. It complements the existing time.Duration, applying // that to the time since midnight (on some arbitrary day in some arbitrary timezone). // The resolution is to the nearest millisecond, unlike time.Duration (which has nanosecond @@ -25,11 +24,21 @@ const zero time.Duration = 0 type Clock int32 const ( -// ClockDay is a fixed period of 24 hours. This does not take account of daylight savings, so is not fully general. - ClockDay Clock = Clock(time.Hour * 24 / time.Millisecond) - ClockHour Clock = Clock(time.Hour / time.Millisecond) + // ClockDay is a fixed period of 24 hours. This does not take account of daylight savings, so is not fully general. + ClockDay Clock = Clock(time.Hour * 24 / time.Millisecond) + + // ClockHour is one hour; it has a similar meaning to time.Hour. + ClockHour Clock = Clock(time.Hour / time.Millisecond) + + // ClockMinute is one minute; it has a similar meaning to time.Minute. ClockMinute Clock = Clock(time.Minute / time.Millisecond) + + // ClockSecond is one second; it has a similar meaning to time.Second. ClockSecond Clock = Clock(time.Second / time.Millisecond) + + // Undefined is provided because the zero value of a Clock is defined (i.e. midnight). + // A special value is chosen, which is math.MinInt32. + Undefined Clock = Clock(math.MinInt32) ) // New returns a new Clock with specified hour, minute, second and millisecond. @@ -154,19 +163,22 @@ func (c Clock) IsInOneDay() bool { } // IsMidnight tests whether a clock time is midnight. This is shorthand for c.Mod24() == 0. +// For large values, this assumes that every day has 24 hours. func (c Clock) IsMidnight() bool { return c.Mod24() == 0 } // Mod24 calculates the remainder vs 24 hours using Euclidean division, in which the result -// will be less than 24 hours and is never negative. +// will be less than 24 hours and is never negative. Note that this imposes the assumption that +// every day has 24 hours (not correct when daylight saving changes in any timezone). +// // https://en.wikipedia.org/wiki/Modulo_operation func (c Clock) Mod24() Clock { if 0 <= c && c < ClockDay { return c } if c < 0 { - q := 1 - c / ClockDay + q := 1 - c/ClockDay m := c + (q * ClockDay) if m == ClockDay { m = 0 @@ -183,7 +195,7 @@ func (c Clock) Mod24() Clock { // clock range 0s to 23h59m59s, for which IsInOneDay() returns true. func (c Clock) Days() int { if c < 0 { - return int(c / ClockDay) - 1 + return int(c/ClockDay) - 1 } else { return int(c / ClockDay) } diff --git a/clock/clock_test.go b/clock/clock_test.go index a3984a8b..63d2e369 100644 --- a/clock/clock_test.go +++ b/clock/clock_test.go @@ -36,8 +36,8 @@ func TestClockSinceMidnight(t *testing.T) { in Clock d time.Duration }{ - {New(1, 2, 3, 4), time.Hour + 2 * time.Minute + 3 * time.Second + 4}, - {New(23, 59, 59, 999), 23 * time.Hour + 59 * time.Minute + 59 * time.Second + 999}, + {New(1, 2, 3, 4), time.Hour + 2*time.Minute + 3*time.Second + 4}, + {New(23, 59, 59, 999), 23*time.Hour + 59*time.Minute + 59*time.Second + 999}, } for i, x := range cases { d := x.in.DurationSinceMidnight() @@ -185,7 +185,7 @@ func TestClockString(t *testing.T) { {-1, -1, -1, -1, "22", "22:58", "22:58:58", "22:58:58.999"}, } for _, x := range cases { - d := Clock(x.h * ClockHour + x.m * ClockMinute + x.s * ClockSecond + x.ms) + d := Clock(x.h*ClockHour + x.m*ClockMinute + x.s*ClockSecond + x.ms) if d.Hh() != x.hh { t.Errorf("%d, %d, %d, %d, got %v, want %v (%d)", x.h, x.m, x.s, x.ms, d.Hh(), x.hh, d) } diff --git a/date.go b/date.go index fb02611a..a5515a98 100644 --- a/date.go +++ b/date.go @@ -38,9 +38,9 @@ package date import ( + "fmt" "math" "time" - "fmt" ) // A Date represents a date under the (proleptic) Gregorian calendar as diff --git a/date_test.go b/date_test.go index 7102f92a..be9b6dc9 100644 --- a/date_test.go +++ b/date_test.go @@ -14,11 +14,11 @@ func same(d Date, t time.Time) bool { yd, wd := d.ISOWeek() yt, wt := t.ISOWeek() return d.Year() == t.Year() && - d.Month() == t.Month() && - d.Day() == t.Day() && - d.Weekday() == t.Weekday() && - d.YearDay() == t.YearDay() && - yd == yt && wd == wt + d.Month() == t.Month() && + d.Day() == t.Day() && + d.Weekday() == t.Weekday() && + d.YearDay() == t.YearDay() && + yd == yt && wd == wt } func TestNew(t *testing.T) { @@ -76,7 +76,7 @@ func TestToday(t *testing.T) { } cases := []int{-10, -5, -3, 0, 1, 4, 8, 12} for _, c := range cases { - location := time.FixedZone("zone", c * 60 * 60) + location := time.FixedZone("zone", c*60*60) today = TodayIn(location) now = time.Now().In(location) if !same(today, now) { @@ -118,7 +118,7 @@ func TestTime(t *testing.T) { t.Errorf("TimeLocal(%v) == %v, want %v", d, tLocal.Location(), time.Local) } for _, z := range zones { - location := time.FixedZone("zone", z * 60 * 60) + location := time.FixedZone("zone", z*60*60) tInLoc := d.In(location) if !same(d, tInLoc) { t.Errorf("TimeIn(%v) == %v, want date part %v", d, tInLoc, d) diff --git a/datetool/main.go b/datetool/main.go index b23d9034..555ddb37 100644 --- a/datetool/main.go +++ b/datetool/main.go @@ -4,12 +4,12 @@ package main import ( - "os" - "strings" + "fmt" . "github.com/rickb777/date" "github.com/rickb777/date/clock" - "fmt" + "os" "strconv" + "strings" ) func printPair(a string, b interface{}) { diff --git a/format.go b/format.go index e1ed29bd..33b71843 100644 --- a/format.go +++ b/format.go @@ -22,14 +22,14 @@ import ( // so that the Parse function and Format method can apply the same // transformation to a general date value. const ( - ISO8601 = "2006-01-02" // ISO 8601 extended format + ISO8601 = "2006-01-02" // ISO 8601 extended format ISO8601B = "20060102" // ISO 8601 basic format - RFC822 = "02-Jan-06" - RFC822W = "Mon, 02-Jan-06" // RFC822 with day of the week - RFC850 = "Monday, 02-Jan-06" - RFC1123 = "02 Jan 2006" + RFC822 = "02-Jan-06" + RFC822W = "Mon, 02-Jan-06" // RFC822 with day of the week + RFC850 = "Monday, 02-Jan-06" + RFC1123 = "02 Jan 2006" RFC1123W = "Mon, 02 Jan 2006" // RFC1123 with day of the week - RFC3339 = "2006-01-02" + RFC3339 = "2006-01-02" ) // MustAutoParse is as per AutoParse except that it panics if the string cannot be parsed. @@ -82,8 +82,8 @@ func AutoParse(value string) (Date, error) { } else if i1 >= 2 && i2 > i1 && abs[i1] == abs[i2] { // harder case - need to swap the field order dd := abs[0:i1] - mm := abs[i1 + 1:i2] - yyyy := abs[i2 + 1:] + mm := abs[i1+1 : i2] + yyyy := abs[i2+1:] abs = fmt.Sprintf("%s-%s-%s", yyyy, mm, dd) } } @@ -280,10 +280,10 @@ func (d Date) FormatWithSuffixes(layout string, suffixes []string) string { return t.Format(layout) default: - a := make([]string, 0, 2 * len(parts) - 1) + a := make([]string, 0, 2*len(parts)-1) for i, p := range parts { if i > 0 { - a = append(a, suffixes[d.Day() - 1]) + a = append(a, suffixes[d.Day()-1]) } a = append(a, t.Format(p)) } From c8a42df852ca75fb82641ba6b2847f7d14c4f26f Mon Sep 17 00:00:00 2001 From: Rick Date: Sun, 3 Jan 2016 13:08:15 +0000 Subject: [PATCH 036/165] Added parsing for am/pm times --- clock/clock.go | 86 ++-------------------- clock/clock_test.go | 63 ++++++++++++++++- clock/parse.go | 169 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 236 insertions(+), 82 deletions(-) create mode 100644 clock/parse.go diff --git a/clock/clock.go b/clock/clock.go index 46bd03a8..7abc17b5 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -7,7 +7,6 @@ package clock import ( "fmt" "math" - "strconv" "time" ) @@ -23,6 +22,7 @@ import ( // See https://en.wikipedia.org/wiki/ISO_8601#Times type Clock int32 +// Common durations. const ( // ClockDay is a fixed period of 24 hours. This does not take account of daylight savings, so is not fully general. ClockDay Clock = Clock(time.Hour * 24 / time.Millisecond) @@ -35,12 +35,12 @@ const ( // ClockSecond is one second; it has a similar meaning to time.Second. ClockSecond Clock = Clock(time.Second / time.Millisecond) - - // Undefined is provided because the zero value of a Clock is defined (i.e. midnight). - // A special value is chosen, which is math.MinInt32. - Undefined Clock = Clock(math.MinInt32) ) +// Undefined is provided because the zero value of a Clock *is* defined (i.e. midnight). +// A special value is chosen, which is math.MinInt32. +const Undefined Clock = Clock(math.MinInt32) + // New returns a new Clock with specified hour, minute, second and millisecond. func New(hour, minute, second, millisec int) Clock { hx := Clock(hour) * ClockHour @@ -79,82 +79,6 @@ func (c Clock) Add(h, m, s, ms int) Clock { return c + hx + mx + sx + Clock(ms) } -// MustParse is as per Parse except that it panics if the string cannot be parsed. -// This is intended for setup code; don't use it for user inputs. -func MustParse(hms string) Clock { - t, err := Parse(hms) - if err != nil { - panic(err) - } - return t -} - -// Parse converts a string representation to a Clock. Acceptable representations -// are as per ISO-8601 - see https://en.wikipedia.org/wiki/ISO_8601#Times -func Parse(hms string) (clock Clock, err error) { - switch len(hms) { - case 2: // HH - return parseClockParts(hms, hms, "", "", "") - - case 4: // HHMM - return parseClockParts(hms, hms[:2], hms[2:], "", "") - - case 5: // HH:MM - if hms[2] != ':' { - return 0, fmt.Errorf("date.ParseClock: cannot parse %s", hms) - } - return parseClockParts(hms, hms[:2], hms[3:], "", "") - - case 6: // HHMMSS - return parseClockParts(hms, hms[:2], hms[2:4], hms[4:], "") - - case 8: // HH:MM:SS - if hms[2] != ':' || hms[5] != ':' { - return 0, fmt.Errorf("date.ParseClock: cannot parse %s", hms) - } - return parseClockParts(hms, hms[:2], hms[3:5], hms[6:], "") - - default: - if hms[2] != ':' || hms[5] != ':' || hms[8] != '.' { - return 0, fmt.Errorf("date.ParseClock: cannot parse %s", hms) - } - return parseClockParts(hms, hms[:2], hms[3:5], hms[6:8], hms[9:]) - } - return 0, fmt.Errorf("date.ParseClock: cannot parse %s", hms) -} - -func parseClockParts(hms, hh, mm, ss, mmms string) (clock Clock, err error) { - h := 0 - m := 0 - s := 0 - ms := 0 - if hh != "" { - h, err = strconv.Atoi(hh) - if err != nil { - return 0, fmt.Errorf("date.ParseClock: cannot parse %s: %v", hms, err) - } - } - if mm != "" { - m, err = strconv.Atoi(mm) - if err != nil { - return 0, fmt.Errorf("date.ParseClock: cannot parse %s: %v", hms, err) - } - } - if ss != "" { - s, err = strconv.Atoi(ss) - if err != nil { - return 0, fmt.Errorf("date.ParseClock: cannot parse %s: %v", hms, err) - } - } - if mmms != "" { - ms, err = strconv.Atoi(mmms) - if err != nil { - return 0, fmt.Errorf("date.ParseClock: cannot parse %s: %v", hms, err) - } - } - return New(h, m, s, ms), nil -} - // IsInOneDay tests whether a clock time is in the range 0 to 24 hours, inclusive. Inside this // range, a Clock is generally well-behaved. But outside it, there may be errors due to daylight // savings. Note that 24:00:00 is included as a special case as per ISO-8601 definition of midnight. diff --git a/clock/clock_test.go b/clock/clock_test.go index 63d2e369..1f374d82 100644 --- a/clock/clock_test.go +++ b/clock/clock_test.go @@ -201,7 +201,7 @@ func TestClockString(t *testing.T) { } } -func TestClockParse(t *testing.T) { +func TestClockParseGoods(t *testing.T) { cases := []struct { str string want Clock @@ -226,7 +226,25 @@ func TestClockParse(t *testing.T) { {"00:01:00.000", New(0, 1, 0, 0)}, {"01:00:00.000", New(1, 0, 0, 0)}, {"01:02:03.004", New(1, 2, 3, 4)}, + {"01:02:03.04", New(1, 2, 3, 40)}, + {"01:02:03.4", New(1, 2, 3, 400)}, {"23:59:59.999", New(23, 59, 59, 999)}, + {"12am", New(0, 0, 0, 0)}, + {"12pm", New(12, 0, 0, 0)}, + {"12:01am", New(0, 1, 0, 0)}, + {"12:01pm", New(12, 1, 0, 0)}, + {"12:01:02am", New(0, 1, 2, 0)}, + {"12:01:02pm", New(12, 1, 2, 0)}, + {"1am", New(1, 0, 0, 0)}, + {"1pm", New(13, 0, 0, 0)}, + {"1:00am", New(1, 0, 0, 0)}, + {"1:00pm", New(13, 0, 0, 0)}, + {"1:00:00am", New(1, 0, 0, 0)}, + {"1:02:03pm", New(13, 2, 3, 0)}, + {"1:02:03.004pm", New(13, 2, 3, 4)}, + {"1:20:30.04pm", New(13, 20, 30, 40)}, + {"1:20:30.4pm", New(13, 20, 30, 400)}, + {"1:20:30.pm", New(13, 20, 30, 0)}, } for _, x := range cases { str := MustParse(x.str) @@ -235,3 +253,46 @@ func TestClockParse(t *testing.T) { } } } + +func TestClockParseBads(t *testing.T) { + cases := []struct { + str string + }{ + {"0"}, + {"0:01"}, + {"0:00:01"}, + {"hh"}, + {"00-00"}, + {"00:00-00"}, + {"00:00:00-"}, + {"00:00:00-0"}, + {"00:00:00-00"}, + {"00:00:00-000"}, + {"00:mm"}, + {"00:00:ss"}, + {"00:00:00.xxx"}, + {"01-02:03.004"}, + {"01:02-03.04"}, + {"01:02:03-4"}, + {"12xm"}, + {"12-01am"}, + {"12:01-02am"}, + {"ham"}, + {"hham"}, + {"1xm"}, + {"1-00am"}, + {"1:00-00am"}, + {"1:02:03-4pm"}, + {"1:02:03-04pm"}, + {"1:02:03-004pm"}, + {"1:02:03.0045pm"}, + } + for _, x := range cases { + c, err := Parse(x.str) + if err == nil { + t.Errorf("%s, got %#v, want err", x.str, c) + } else { + println(err.Error()) + } + } +} diff --git a/clock/parse.go b/clock/parse.go new file mode 100644 index 00000000..dd5e8d11 --- /dev/null +++ b/clock/parse.go @@ -0,0 +1,169 @@ +package clock + +import ( + "fmt" + "runtime" + "strconv" + "strings" +) + +// MustParse is as per Parse except that it panics if the string cannot be parsed. +// This is intended for setup code; don't use it for user inputs. +func MustParse(hms string) Clock { + t, err := Parse(hms) + if err != nil { + panic(err) + } + return t +} + +// Parse converts a string representation to a Clock. Acceptable representations +// are as per ISO-8601 - see https://en.wikipedia.org/wiki/ISO_8601#Times +// +// Also, conventional AM- and PM-based strings are parsed, such as "2am", "2:45pm". +// Remember that 12am is midnight and 12pm is noon. +func Parse(hms string) (clock Clock, err error) { + if strings.HasSuffix(hms, "am") || strings.HasSuffix(hms, "AM") { + return parseAmPm(hms, 0) + } else if strings.HasSuffix(hms, "pm") || strings.HasSuffix(hms, "PM") { + return parseAmPm(hms, 12) + } + return parseISO(hms) +} + +func parseISO(hms string) (clock Clock, err error) { + switch len(hms) { + case 2: // HH + return parseClockParts(hms, hms, "", "", "", 0, 0) + + case 4: // HHMM + return parseClockParts(hms, hms[:2], hms[2:], "", "", 0, 0) + + case 5: // HH:MM + if hms[2] != ':' { + return 0, parseError(hms, nil) + } + return parseClockParts(hms, hms[:2], hms[3:], "", "", 0, 0) + + case 6: // HHMMSS + return parseClockParts(hms, hms[:2], hms[2:4], hms[4:], "", 0, 0) + + case 8: // HH:MM:SS + if hms[2] != ':' || hms[5] != ':' { + return 0, parseError(hms, nil) + } + return parseClockParts(hms, hms[:2], hms[3:5], hms[6:], "", 0, 0) + + case 9, 10: // HH:MM:SS.0 + if hms[2] != ':' || hms[5] != ':' || hms[8] != '.' { + return 0, parseError(hms, nil) + } + return parseClockParts(hms, hms[:2], hms[3:5], hms[6:8], hms[9:]+"00", 0, 0) + + case 11: // HH:MM:SS.00 + if hms[2] != ':' || hms[5] != ':' || hms[8] != '.' { + return 0, parseError(hms, nil) + } + return parseClockParts(hms, hms[:2], hms[3:5], hms[6:8], hms[9:]+"0", 0, 0) + + case 12: // HH:MM:SS.000 + if hms[2] != ':' || hms[5] != ':' || hms[8] != '.' { + return 0, parseError(hms, nil) + } + return parseClockParts(hms, hms[:2], hms[3:5], hms[6:8], hms[9:], 0, 0) + } + return 0, parseError(hms, nil) +} + +func parseAmPm(hms string, offset int) (clock Clock, err error) { + n := len(hms) + + switch len(hms) { + case 3: // Ham + return parseClockParts(hms, "0"+hms[:1], "", "", "", 12, offset) + + case 4: // HHam + return parseClockParts(hms, hms[:2], "", "", "", 12, offset) + } + + colon := strings.IndexByte(hms, ':') + if colon < 0 { + return 0, parseError(hms, nil) + } + + h := hms[:colon] + rest := hms[colon+1 : n-2] + + switch len(rest) { + case 2: // MM + return parseClockParts(hms, h, rest, "", "", 12, offset) + + case 5: // MM:SS + if rest[2] != ':' { + return 0, parseError(hms, nil) + } + return parseClockParts(hms, h, rest[:2], rest[3:], "", 12, offset) + + case 6, 7: // MM:SS.0xm + if rest[2] != ':' || rest[5] != '.' { + return 0, parseError(hms, nil) + } + return parseClockParts(hms, h, rest[:2], rest[3:5], rest[6:]+"00", 12, offset) + + case 8: // MM:SS.00xm + if rest[2] != ':' || rest[5] != '.' { + return 0, parseError(hms, nil) + } + return parseClockParts(hms, h, rest[:2], rest[3:5], rest[6:]+"0", 12, offset) + + case 9: // MM:SS.000xm + if rest[2] != ':' || rest[5] != '.' { + return 0, parseError(hms, nil) + } + return parseClockParts(hms, h, rest[:2], rest[3:5], rest[6:], 12, offset) + } + return 0, parseError(hms, nil) +} + +func parseClockParts(hms, hh, mm, ss, mmms string, mod, offset int) (clock Clock, err error) { + h := 0 + m := 0 + s := 0 + ms := 0 + if hh != "" { + h, err = strconv.Atoi(hh) + if err != nil { + return 0, parseError(hms, err) + } + } + if mm != "" { + m, err = strconv.Atoi(mm) + if err != nil { + return 0, parseError(hms, err) + } + } + if ss != "" { + s, err = strconv.Atoi(ss) + if err != nil { + return 0, parseError(hms, err) + } + } + if mmms != "" { + ms, err = strconv.Atoi(mmms) + if err != nil { + return 0, parseError(hms, err) + } + } + if mod > 0 { + h = h % mod + } + return New(h+offset, m, s, ms), nil +} + +func parseError(hms string, err error) error { + _, _, line, _ := runtime.Caller(1) + if err != nil { + return fmt.Errorf("parse.go:%d: clock.Clock: cannot parse %s: %v", line, hms, err) + } + return fmt.Errorf("parse.go:%d: clock.Clock: cannot parse %s", line, hms) +} From 8fb35ed28b91b33939b0d3e6f873da50bf197a9f Mon Sep 17 00:00:00 2001 From: Rick Date: Sun, 3 Jan 2016 13:44:09 +0000 Subject: [PATCH 037/165] Added formatting methods for am/pm times --- clock/clock.go | 44 ----------------------- clock/clock_test.go | 37 +++++++++++++------ clock/format.go | 86 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 55 deletions(-) create mode 100644 clock/format.go diff --git a/clock/clock.go b/clock/clock.go index 7abc17b5..8cf218d2 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -5,7 +5,6 @@ package clock import ( - "fmt" "math" "time" ) @@ -147,46 +146,3 @@ func (c Clock) Seconds() int { func (c Clock) Millisec() int { return int(clockMillisec(c.Mod24())) } - -func clockHours(cm Clock) Clock { - return (cm / ClockHour) -} - -func clockMinutes(cm Clock) Clock { - return (cm % ClockHour) / ClockMinute -} - -func clockSeconds(cm Clock) Clock { - return (cm % ClockMinute) / ClockSecond -} - -func clockMillisec(cm Clock) Clock { - return cm % ClockSecond -} - -// Hh gets the clock-face number of hours as a two-digit string (calculated from the modulo time, see Mod24). -func (c Clock) Hh() string { - cm := c.Mod24() - return fmt.Sprintf("%02d", clockHours(cm)) -} - -// HhMm gets the clock-face number of hours and minutes as a five-character ISO-8601 time string (calculated -// from the modulo time, see Mod24). -func (c Clock) HhMm() string { - cm := c.Mod24() - return fmt.Sprintf("%02d:%02d", clockHours(cm), clockMinutes(cm)) -} - -// HhMmSs gets the clock-face number of hours, minutes, seconds as an eight-character ISO-8601 time string -// (calculated from the modulo time, see Mod24). -func (c Clock) HhMmSs() string { - cm := c.Mod24() - return fmt.Sprintf("%02d:%02d:%02d", clockHours(cm), clockMinutes(cm), clockSeconds(cm)) -} - -// String gets the clock-face number of hours, minutes, seconds and milliseconds as a 12-character ISO-8601 -// time string (calculated from the modulo time, see Mod24), specified to the nearest millisecond. -func (c Clock) String() string { - cm := c.Mod24() - return fmt.Sprintf("%02d:%02d:%02d.%03d", clockHours(cm), clockMinutes(cm), clockSeconds(cm), clockMillisec(cm)) -} diff --git a/clock/clock_test.go b/clock/clock_test.go index 1f374d82..5823f4cf 100644 --- a/clock/clock_test.go +++ b/clock/clock_test.go @@ -173,16 +173,20 @@ func TestClockDays(t *testing.T) { func TestClockString(t *testing.T) { cases := []struct { - h, m, s, ms Clock - hh, hhmm, hhmmss, str string + h, m, s, ms Clock + hh, hhmm, hhmmss, str, h12, hmm12, hmmss12 string }{ - {0, 0, 0, 0, "00", "00:00", "00:00:00", "00:00:00.000"}, - {0, 0, 0, 1, "00", "00:00", "00:00:00", "00:00:00.001"}, - {0, 0, 1, 0, "00", "00:00", "00:00:01", "00:00:01.000"}, - {0, 1, 0, 0, "00", "00:01", "00:01:00", "00:01:00.000"}, - {1, 0, 0, 0, "01", "01:00", "01:00:00", "01:00:00.000"}, - {1, 2, 3, 4, "01", "01:02", "01:02:03", "01:02:03.004"}, - {-1, -1, -1, -1, "22", "22:58", "22:58:58", "22:58:58.999"}, + {0, 0, 0, 0, "00", "00:00", "00:00:00", "00:00:00.000", "12am", "12:00am", "12:00:00am"}, + {0, 0, 0, 1, "00", "00:00", "00:00:00", "00:00:00.001", "12am", "12:00am", "12:00:00am"}, + {0, 0, 1, 0, "00", "00:00", "00:00:01", "00:00:01.000", "12am", "12:00am", "12:00:01am"}, + {0, 1, 0, 0, "00", "00:01", "00:01:00", "00:01:00.000", "12am", "12:01am", "12:01:00am"}, + {1, 0, 0, 0, "01", "01:00", "01:00:00", "01:00:00.000", "1am", "1:00am", "1:00:00am"}, + {1, 2, 3, 4, "01", "01:02", "01:02:03", "01:02:03.004", "1am", "1:02am", "1:02:03am"}, + {11, 0, 0, 0, "11", "11:00", "11:00:00", "11:00:00.000", "11am", "11:00am", "11:00:00am"}, + {12, 0, 0, 0, "12", "12:00", "12:00:00", "12:00:00.000", "12pm", "12:00pm", "12:00:00pm"}, + {13, 0, 0, 0, "13", "13:00", "13:00:00", "13:00:00.000", "1pm", "1:00pm", "1:00:00pm"}, + {-1, 0, 0, 0, "23", "23:00", "23:00:00", "23:00:00.000", "11pm", "11:00pm", "11:00:00pm"}, + {-1, -1, -1, -1, "22", "22:58", "22:58:58", "22:58:58.999", "10pm", "10:58pm", "10:58:58pm"}, } for _, x := range cases { d := Clock(x.h*ClockHour + x.m*ClockMinute + x.s*ClockSecond + x.ms) @@ -198,6 +202,15 @@ func TestClockString(t *testing.T) { if d.String() != x.str { t.Errorf("%d, %d, %d, %d, got %v, want %v (%d)", x.h, x.m, x.s, x.ms, d.String(), x.str, d) } + if d.Hh12() != x.h12 { + t.Errorf("%d, %d, %d, %d, got %v, want %v (%d)", x.h, x.m, x.s, x.ms, d.Hh12(), x.h12, d) + } + if d.HhMm12() != x.hmm12 { + t.Errorf("%d, %d, %d, %d, got %v, want %v (%d)", x.h, x.m, x.s, x.ms, d.HhMm12(), x.hmm12, d) + } + if d.HhMmSs12() != x.hmmss12 { + t.Errorf("%d, %d, %d, %d, got %v, want %v (%d)", x.h, x.m, x.s, x.ms, d.HhMmSs12(), x.hmmss12, d) + } } } @@ -229,6 +242,8 @@ func TestClockParseGoods(t *testing.T) { {"01:02:03.04", New(1, 2, 3, 40)}, {"01:02:03.4", New(1, 2, 3, 400)}, {"23:59:59.999", New(23, 59, 59, 999)}, + {"0am", New(0, 0, 0, 0)}, + {"00am", New(0, 0, 0, 0)}, {"12am", New(0, 0, 0, 0)}, {"12pm", New(12, 0, 0, 0)}, {"12:01am", New(0, 1, 0, 0)}, @@ -291,8 +306,8 @@ func TestClockParseBads(t *testing.T) { c, err := Parse(x.str) if err == nil { t.Errorf("%s, got %#v, want err", x.str, c) - } else { - println(err.Error()) + // } else { + // println(err.Error()) } } } diff --git a/clock/format.go b/clock/format.go new file mode 100644 index 00000000..05672a0a --- /dev/null +++ b/clock/format.go @@ -0,0 +1,86 @@ +package clock + +import "fmt" + +func clockHours(cm Clock) Clock { + return (cm / ClockHour) +} + +func clockHours12(cm Clock) (Clock, string) { + h := clockHours(cm) + if h < 1 { + return 12, "am" + } else if h > 12 { + return h - 12, "pm" + } else if h == 12 { + return 12, "pm" + } + return h, "am" +} + +func clockMinutes(cm Clock) Clock { + return (cm % ClockHour) / ClockMinute +} + +func clockSeconds(cm Clock) Clock { + return (cm % ClockMinute) / ClockSecond +} + +func clockMillisec(cm Clock) Clock { + return cm % ClockSecond +} + +// Hh gets the clock-face number of hours as a two-digit string. +// It is calculated from the modulo time; see Mod24. +func (c Clock) Hh() string { + cm := c.Mod24() + return fmt.Sprintf("%02d", clockHours(cm)) +} + +// HhMm gets the clock-face number of hours and minutes as a five-character ISO-8601 time string. +// It is calculated from the modulo time; see Mod24. +func (c Clock) HhMm() string { + cm := c.Mod24() + return fmt.Sprintf("%02d:%02d", clockHours(cm), clockMinutes(cm)) +} + +// HhMmSs gets the clock-face number of hours, minutes, seconds as an eight-character ISO-8601 time string. +// It is calculated from the modulo time; see Mod24. +func (c Clock) HhMmSs() string { + cm := c.Mod24() + return fmt.Sprintf("%02d:%02d:%02d", clockHours(cm), clockMinutes(cm), clockSeconds(cm)) +} + +// Hh12 gets the clock-face number of hours as a one- or two-digit string, followed by am or pm. +// Remember that midnight is 12am, noon is 12pm. +// It is calculated from the modulo time; see Mod24. +func (c Clock) Hh12() string { + cm := c.Mod24() + h, sfx := clockHours12(cm) + return fmt.Sprintf("%d%s", h, sfx) +} + +// HhMm12 gets the clock-face number of hours and minutes, followed by am or pm. +// Remember that midnight is 12am, noon is 12pm. +// It is calculated from the modulo time; see Mod24. +func (c Clock) HhMm12() string { + cm := c.Mod24() + h, sfx := clockHours12(cm) + return fmt.Sprintf("%d:%02d%s", h, clockMinutes(cm), sfx) +} + +// HhMm12 gets the clock-face number of hours, minutes and seconds, followed by am or pm. +// Remember that midnight is 12am, noon is 12pm. +// It is calculated from the modulo time; see Mod24. +func (c Clock) HhMmSs12() string { + cm := c.Mod24() + h, sfx := clockHours12(cm) + return fmt.Sprintf("%d:%02d:%02d%s", h, clockMinutes(cm), clockSeconds(cm), sfx) +} + +// String gets the clock-face number of hours, minutes, seconds and milliseconds as a 12-character ISO-8601 +// time string (calculated from the modulo time, see Mod24), specified to the nearest millisecond. +func (c Clock) String() string { + cm := c.Mod24() + return fmt.Sprintf("%02d:%02d:%02d.%03d", clockHours(cm), clockMinutes(cm), clockSeconds(cm), clockMillisec(cm)) +} From 60a8326bfd79f75d94bb4184d72484923ab37ff3 Mon Sep 17 00:00:00 2001 From: Rick Date: Sun, 3 Jan 2016 14:06:26 +0000 Subject: [PATCH 038/165] Better documentation --- clock/clock.go | 4 +++- clock/format.go | 4 ++++ clock/parse.go | 4 ++++ date.go | 33 --------------------------------- datetool/main.go | 6 +++++- doc.go | 39 +++++++++++++++++++++++++++++++++++++++ timespan/doc.go | 9 +++++++++ 7 files changed, 64 insertions(+), 35 deletions(-) create mode 100644 doc.go create mode 100644 timespan/doc.go diff --git a/clock/clock.go b/clock/clock.go index 8cf218d2..4fa09551 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Clock specifies a time of day with resolution to the nearest millisecond. +// package clock import ( @@ -70,7 +72,7 @@ func (c Clock) DurationSinceMidnight() time.Duration { // Add returns a new Clock offset from this clock specified hour, minute, second and millisecond. // The parameters can be negative. -// If required, use Mod() to correct any overflow or underflow. +// If required, use Mod24() to correct any overflow or underflow. func (c Clock) Add(h, m, s, ms int) Clock { hx := Clock(h) * ClockHour mx := Clock(m) * ClockMinute diff --git a/clock/format.go b/clock/format.go index 05672a0a..3a504da1 100644 --- a/clock/format.go +++ b/clock/format.go @@ -1,3 +1,7 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package clock import "fmt" diff --git a/clock/parse.go b/clock/parse.go index dd5e8d11..56f9a2a8 100644 --- a/clock/parse.go +++ b/clock/parse.go @@ -1,3 +1,7 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package clock import ( diff --git a/date.go b/date.go index a5515a98..a81ef0b2 100644 --- a/date.go +++ b/date.go @@ -2,39 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package date provides functionality for working with dates. -// -// This package introduces a light-weight Date type that is storage-efficient -// and covenient for calendrical calculations and date parsing and formatting -// (including years outside the [0,9999] interval). -// -// Credits -// -// This package follows very closely the design of package time -// (http://golang.org/pkg/time/) in the standard library, many of the Date -// methods are implemented using the corresponding methods of the time.Time -// type, and much of the documentation is copied directly from that package. -// -// References -// -// https://golang.org/src/time/time.go -// -// https://en.wikipedia.org/wiki/Gregorian_calendar -// -// https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar -// -// https://en.wikipedia.org/wiki/Astronomical_year_numbering -// -// https://en.wikipedia.org/wiki/ISO_8601 -// -// https://tools.ietf.org/html/rfc822 -// -// https://tools.ietf.org/html/rfc850 -// -// https://tools.ietf.org/html/rfc1123 -// -// https://tools.ietf.org/html/rfc3339 -// package date import ( diff --git a/datetool/main.go b/datetool/main.go index 555ddb37..bd6cb68c 100644 --- a/datetool/main.go +++ b/datetool/main.go @@ -1,6 +1,10 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + // This tool prints equivalences between the string representation and the internal numerical // representation for dates and clocks. - +// package main import ( diff --git a/doc.go b/doc.go new file mode 100644 index 00000000..69914bff --- /dev/null +++ b/doc.go @@ -0,0 +1,39 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package date provides functionality for working with dates. Subpackages support +// clock-face time, spans of time and ranges of dates. +// +// This package introduces a light-weight Date type that is storage-efficient +// and covenient for calendrical calculations and date parsing and formatting +// (including years outside the [0,9999] interval). +// +// Credits +// +// This package follows very closely the design of package time +// (http://golang.org/pkg/time/) in the standard library, many of the Date +// methods are implemented using the corresponding methods of the time.Time +// type, and much of the documentation is copied directly from that package. +// +// References +// +// https://golang.org/src/time/time.go +// +// https://en.wikipedia.org/wiki/Gregorian_calendar +// +// https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar +// +// https://en.wikipedia.org/wiki/Astronomical_year_numbering +// +// https://en.wikipedia.org/wiki/ISO_8601 +// +// https://tools.ietf.org/html/rfc822 +// +// https://tools.ietf.org/html/rfc850 +// +// https://tools.ietf.org/html/rfc1123 +// +// https://tools.ietf.org/html/rfc3339 +// +package date diff --git a/timespan/doc.go b/timespan/doc.go new file mode 100644 index 00000000..b5b571dc --- /dev/null +++ b/timespan/doc.go @@ -0,0 +1,9 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package timespan provides spans of time (TimeSpan), and ranges of dates (DateRange). +// Both are half-open intervals for which the start is included and the end is excluded. +// This allows for empty spans and also facilitates aggregating spans together. +// +package timespan From 559ebedf880bbf74e2564aa9e1d7f7dc7de29544 Mon Sep 17 00:00:00 2001 From: Rick Date: Mon, 4 Jan 2016 08:32:29 +0000 Subject: [PATCH 039/165] Some minor internal refactoring --- date.go | 30 +++++++++++++++--------------- date_test.go | 4 ++-- marshal.go | 2 +- rep.go | 8 ++++---- rep_test.go | 8 ++++---- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/date.go b/date.go index a81ef0b2..a7b662e1 100644 --- a/date.go +++ b/date.go @@ -10,6 +10,13 @@ import ( "time" ) +// PeriodOfDays describes a period of time measured in whole days. Negative values +// indicate days earlier than some mark. +type PeriodOfDays int32 + +// ZeroDays is the named zero value for PeriodOfDays. +const ZeroDays PeriodOfDays = 0 + // A Date represents a date under the (proleptic) Gregorian calendar as // used by ISO 8601. This calendar uses astronomical year numbering, // so it includes a year 0 and represents earlier years as negative numbers @@ -35,16 +42,9 @@ import ( // a simple way of detecting a date that has not been initialized explicitly. // type Date struct { - day int32 // day gives the number of days elapsed since date zero. + day PeriodOfDays // day gives the number of days elapsed since date zero. } -// PeriodOfDays describes a period of time measured in whole days. Negative values -// indicate days earlier than some mark. -type PeriodOfDays int32 - -// ZeroDays is the named zero value for PeriodOfDays. -const ZeroDays PeriodOfDays = 0 - // New returns the Date value corresponding to the given year, month, and day. // // The month and day may be outside their usual ranges and will be normalized @@ -64,7 +64,7 @@ func NewAt(t time.Time) Date { // NewOfDays returns the Date value corresponding to the given period since the // epoch (1st January 1970), which may be negative. func NewOfDays(p PeriodOfDays) Date { - return Date{int32(p)} + return Date{p} } // Today returns today's date according to the current local time. @@ -88,12 +88,12 @@ func TodayIn(loc *time.Location) Date { // Min returns the smallest representable date. func Min() Date { - return Date{day: math.MinInt32} + return Date{day: PeriodOfDays(math.MinInt32)} } // Max returns the largest representable date. func Max() Date { - return Date{day: math.MaxInt32} + return Date{day: PeriodOfDays(math.MaxInt32)} } // UTC returns a Time value corresponding to midnight on the given date, @@ -162,7 +162,7 @@ func (d Date) Weekday() time.Weekday { // Date zero, January 1, 1970, fell on a Thursday wdayZero := time.Thursday // Taking into account potential for overflow and negative offset - return time.Weekday((int32(wdayZero) + d.day%7 + 7) % 7) + return time.Weekday((int32(wdayZero) + int32(d.day)%7 + 7) % 7) } // ISOWeek returns the ISO 8601 year and week number in which d occurs. @@ -212,7 +212,7 @@ func (d Date) Max(u Date) Date { // Add returns the date d plus the given number of days. The parameter may be negative. func (d Date) Add(days PeriodOfDays) Date { - return Date{d.day + int32(days)} + return Date{d.day + days} } // AddDate returns the date corresponding to adding the given number of years, @@ -225,12 +225,12 @@ func (d Date) AddDate(years, months, days int) Date { // Sub returns d-u as the number of days between the two dates. func (d Date) Sub(u Date) (days PeriodOfDays) { - return PeriodOfDays(d.day - u.day) + return d.day - u.day } // DaysSinceEpoch returns the number of days since the epoch (1st January 1970), which may be negative. func (d Date) DaysSinceEpoch() (days PeriodOfDays) { - return PeriodOfDays(d.day) + return d.day } // IsLeap simply tests whether a given year is a leap year, using the Gregorian calendar algorithm. diff --git a/date_test.go b/date_test.go index be9b6dc9..e5785ee3 100644 --- a/date_test.go +++ b/date_test.go @@ -217,14 +217,14 @@ func TestArithmetic(t *testing.T) { } } -func min(a, b int32) int32 { +func min(a, b PeriodOfDays) PeriodOfDays { if a < b { return a } return b } -func max(a, b int32) int32 { +func max(a, b PeriodOfDays) PeriodOfDays { if a > b { return a } diff --git a/marshal.go b/marshal.go index 99ef338e..de616b45 100644 --- a/marshal.go +++ b/marshal.go @@ -29,7 +29,7 @@ func (d *Date) UnmarshalBinary(data []byte) error { return errors.New("Date.UnmarshalBinary: invalid length") } - d.day = int32(data[3]) | int32(data[2])<<8 | int32(data[1])<<16 | int32(data[0])<<24 + d.day = PeriodOfDays(data[3]) | PeriodOfDays(data[2])<<8 | PeriodOfDays(data[1])<<16 | PeriodOfDays(data[0])<<24 // d.decoded = time.Time{} return nil diff --git a/rep.go b/rep.go index 37adce88..911eee51 100644 --- a/rep.go +++ b/rep.go @@ -10,7 +10,7 @@ const secondsPerDay = 60 * 60 * 24 // encode returns the number of days elapsed from date zero to the date // corresponding to the given Time value. -func encode(t time.Time) int32 { +func encode(t time.Time) PeriodOfDays { // Compute the number of seconds elapsed since January 1, 1970 00:00:00 // in the location specified by t and not necessarily UTC. // A Time value is represented internally as an offset from a UTC base @@ -22,14 +22,14 @@ func encode(t time.Time) int32 { // Unfortunately operator / rounds towards 0, so negative values // must be handled differently if secs >= 0 { - return int32(secs / secondsPerDay) + return PeriodOfDays(secs / secondsPerDay) } - return -int32((secondsPerDay - 1 - secs) / secondsPerDay) + return -PeriodOfDays((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 int32) time.Time { +func decode(d PeriodOfDays) time.Time { secs := int64(d) * secondsPerDay return time.Unix(secs, 0).UTC() } diff --git a/rep_test.go b/rep_test.go index b9f93f6a..4ee31422 100644 --- a/rep_test.go +++ b/rep_test.go @@ -17,11 +17,11 @@ func TestEncode(t *testing.T) { tBase := time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC) for i, c := range cases { d := encode(tBase.AddDate(0, 0, c)) - if d != int32(c) { + if d != PeriodOfDays(c) { t.Errorf("Encode(%v) == %v, want %v", i, d, c) } d = encode(tBase.AddDate(0, 0, -c)) - if d != int32(-c) { + if d != PeriodOfDays(-c) { t.Errorf("Encode(%v) == %v, want %v", i, d, c) } } @@ -73,14 +73,14 @@ func TestEncodeDecode(t *testing.T) { func TestDecodeEncode(t *testing.T) { for i := 0; i < 1000; i++ { - c := int32(rand.Int31()) + c := PeriodOfDays(rand.Int31()) d := encode(decode(c)) if d != c { t.Errorf("DecodeEncode(%v) == %v, want %v", i, d, c) } } for i := 0; i < 1000; i++ { - c := -int32(rand.Int31()) + c := -PeriodOfDays(rand.Int31()) d := encode(decode(c)) if d != c { t.Errorf("DecodeEncode(%v) == %v, want %v", i, d, c) From d71630285ff111a7a333b85ba5a19691d7ffed25 Mon Sep 17 00:00:00 2001 From: Rick Date: Mon, 4 Jan 2016 17:17:15 +0000 Subject: [PATCH 040/165] New constants: Midnight, Noon --- clock/clock.go | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/clock/clock.go b/clock/clock.go index 4fa09551..46828764 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -23,23 +23,30 @@ import ( // See https://en.wikipedia.org/wiki/ISO_8601#Times type Clock int32 -// Common durations. +// Common durations - second, minute, hour and day. const ( - // ClockDay is a fixed period of 24 hours. This does not take account of daylight savings, so is not fully general. - ClockDay Clock = Clock(time.Hour * 24 / time.Millisecond) - - // ClockHour is one hour; it has a similar meaning to time.Hour. - ClockHour Clock = Clock(time.Hour / time.Millisecond) + // ClockSecond is one second; it has a similar meaning to time.Second. + ClockSecond Clock = Clock(time.Second / time.Millisecond) // ClockMinute is one minute; it has a similar meaning to time.Minute. ClockMinute Clock = Clock(time.Minute / time.Millisecond) - // ClockSecond is one second; it has a similar meaning to time.Second. - ClockSecond Clock = Clock(time.Second / time.Millisecond) + // ClockHour is one hour; it has a similar meaning to time.Hour. + ClockHour Clock = Clock(time.Hour / time.Millisecond) + + // ClockDay is a fixed period of 24 hours. This does not take account of daylight savings, + // so is not fully general. + ClockDay Clock = Clock(time.Hour * 24 / time.Millisecond) ) -// Undefined is provided because the zero value of a Clock *is* defined (i.e. midnight). -// A special value is chosen, which is math.MinInt32. +// Midnight is the zero value of a Clock. +const Midnight Clock = 0 + +// Noon is at 12pm. +const Noon Clock = ClockHour * 12 + +// Undefined is provided because the zero value of a Clock *is* defined (i.e. Midnight). +// So a special value is chosen, which is math.MinInt32. const Undefined Clock = Clock(math.MinInt32) // New returns a new Clock with specified hour, minute, second and millisecond. @@ -84,13 +91,13 @@ func (c Clock) Add(h, m, s, ms int) Clock { // range, a Clock is generally well-behaved. But outside it, there may be errors due to daylight // savings. Note that 24:00:00 is included as a special case as per ISO-8601 definition of midnight. func (c Clock) IsInOneDay() bool { - return 0 <= c && c <= ClockDay + return Midnight <= c && c <= ClockDay } // IsMidnight tests whether a clock time is midnight. This is shorthand for c.Mod24() == 0. // For large values, this assumes that every day has 24 hours. func (c Clock) IsMidnight() bool { - return c.Mod24() == 0 + return c.Mod24() == Midnight } // Mod24 calculates the remainder vs 24 hours using Euclidean division, in which the result @@ -99,14 +106,14 @@ func (c Clock) IsMidnight() bool { // // https://en.wikipedia.org/wiki/Modulo_operation func (c Clock) Mod24() Clock { - if 0 <= c && c < ClockDay { + if Midnight <= c && c < ClockDay { return c } - if c < 0 { + if c < Midnight { q := 1 - c/ClockDay m := c + (q * ClockDay) if m == ClockDay { - m = 0 + m = Midnight } return m } @@ -119,7 +126,7 @@ func (c Clock) Mod24() Clock { // enclosed in a day numbered -1, and so on. This means that the result is zero only for the // clock range 0s to 23h59m59s, for which IsInOneDay() returns true. func (c Clock) Days() int { - if c < 0 { + if c < Midnight { return int(c/ClockDay) - 1 } else { return int(c / ClockDay) From a00101df0f89e67f2e1bc5f530ac6d33495f5996 Mon Sep 17 00:00:00 2001 From: Rick Date: Mon, 4 Jan 2016 17:25:12 +0000 Subject: [PATCH 041/165] Renamed constants --- clock/clock.go | 59 ++++++++++++++++++++++++--------------------- clock/clock_test.go | 52 +++++++++++++++++++-------------------- clock/format.go | 8 +++--- 3 files changed, 62 insertions(+), 57 deletions(-) diff --git a/clock/clock.go b/clock/clock.go index 46828764..d3e86bc1 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -25,25 +25,30 @@ type Clock int32 // Common durations - second, minute, hour and day. const ( - // ClockSecond is one second; it has a similar meaning to time.Second. - ClockSecond Clock = Clock(time.Second / time.Millisecond) + // Second is one second; it has a similar meaning to time.Second. + Second Clock = Clock(time.Second / time.Millisecond) + // ClockSecond Clock = Clock(time.Second / time.Millisecond) - // ClockMinute is one minute; it has a similar meaning to time.Minute. - ClockMinute Clock = Clock(time.Minute / time.Millisecond) + // Minute is one minute; it has a similar meaning to time.Minute. + Minute Clock = Clock(time.Minute / time.Millisecond) + // ClockMinute Clock = Clock(time.Minute / time.Millisecond) - // ClockHour is one hour; it has a similar meaning to time.Hour. - ClockHour Clock = Clock(time.Hour / time.Millisecond) + // Hour is one hour; it has a similar meaning to time.Hour. + Hour Clock = Clock(time.Hour / time.Millisecond) + // ClockHour Clock = Clock(time.Hour / time.Millisecond) - // ClockDay is a fixed period of 24 hours. This does not take account of daylight savings, + // Day is a fixed period of 24 hours. This does not take account of daylight savings, // so is not fully general. - ClockDay Clock = Clock(time.Hour * 24 / time.Millisecond) + Day Clock = Clock(time.Hour * 24 / time.Millisecond) + +// ClockDay Clock = Clock(time.Hour * 24 / time.Millisecond) ) // Midnight is the zero value of a Clock. const Midnight Clock = 0 // Noon is at 12pm. -const Noon Clock = ClockHour * 12 +const Noon Clock = Hour * 12 // Undefined is provided because the zero value of a Clock *is* defined (i.e. Midnight). // So a special value is chosen, which is math.MinInt32. @@ -51,18 +56,18 @@ const Undefined Clock = Clock(math.MinInt32) // New returns a new Clock with specified hour, minute, second and millisecond. func New(hour, minute, second, millisec int) Clock { - hx := Clock(hour) * ClockHour - mx := Clock(minute) * ClockMinute - sx := Clock(second) * ClockSecond + hx := Clock(hour) * Hour + mx := Clock(minute) * Minute + sx := Clock(second) * Second return Clock(hx + mx + sx + Clock(millisec)) } // NewAt returns a new Clock with specified hour, minute, second and millisecond. func NewAt(t time.Time) Clock { hour, minute, second := t.Clock() - hx := Clock(hour) * ClockHour - mx := Clock(minute) * ClockMinute - sx := Clock(second) * ClockSecond + hx := Clock(hour) * Hour + mx := Clock(minute) * Minute + sx := Clock(second) * Second ms := Clock(t.Nanosecond() / int(time.Millisecond)) return Clock(hx + mx + sx + ms) } @@ -81,9 +86,9 @@ func (c Clock) DurationSinceMidnight() time.Duration { // The parameters can be negative. // If required, use Mod24() to correct any overflow or underflow. func (c Clock) Add(h, m, s, ms int) Clock { - hx := Clock(h) * ClockHour - mx := Clock(m) * ClockMinute - sx := Clock(s) * ClockSecond + hx := Clock(h) * Hour + mx := Clock(m) * Minute + sx := Clock(s) * Second return c + hx + mx + sx + Clock(ms) } @@ -91,7 +96,7 @@ func (c Clock) Add(h, m, s, ms int) Clock { // range, a Clock is generally well-behaved. But outside it, there may be errors due to daylight // savings. Note that 24:00:00 is included as a special case as per ISO-8601 definition of midnight. func (c Clock) IsInOneDay() bool { - return Midnight <= c && c <= ClockDay + return Midnight <= c && c <= Day } // IsMidnight tests whether a clock time is midnight. This is shorthand for c.Mod24() == 0. @@ -106,19 +111,19 @@ func (c Clock) IsMidnight() bool { // // https://en.wikipedia.org/wiki/Modulo_operation func (c Clock) Mod24() Clock { - if Midnight <= c && c < ClockDay { + if Midnight <= c && c < Day { return c } if c < Midnight { - q := 1 - c/ClockDay - m := c + (q * ClockDay) - if m == ClockDay { + q := 1 - c/Day + m := c + (q * Day) + if m == Day { m = Midnight } return m } - q := c / ClockDay - return c - (q * ClockDay) + q := c / Day + return c - (q * Day) } // Days gets the number of whole days represented by the Clock, assuming that each day is a fixed @@ -127,9 +132,9 @@ func (c Clock) Mod24() Clock { // clock range 0s to 23h59m59s, for which IsInOneDay() returns true. func (c Clock) Days() int { if c < Midnight { - return int(c/ClockDay) - 1 + return int(c/Day) - 1 } else { - return int(c / ClockDay) + return int(c / Day) } } diff --git a/clock/clock_test.go b/clock/clock_test.go index 5823f4cf..fe0ba8aa 100644 --- a/clock/clock_test.go +++ b/clock/clock_test.go @@ -75,17 +75,17 @@ func TestClockAdd(t *testing.T) { h, m, s, ms int in, want Clock }{ - {0, 0, 0, 0, 2 * ClockHour, New(2, 0, 0, 0)}, - {0, 0, 0, 1, 2 * ClockHour, New(2, 0, 0, 1)}, - {0, 0, 0, -1, 2 * ClockHour, New(1, 59, 59, 999)}, - {0, 0, 1, 0, 2 * ClockHour, New(2, 0, 1, 0)}, - {0, 0, -1, 0, 2 * ClockHour, New(1, 59, 59, 0)}, - {0, 1, 0, 0, 2 * ClockHour, New(2, 1, 0, 0)}, - {0, -1, 0, 0, 2 * ClockHour, New(1, 59, 0, 0)}, - {1, 0, 0, 0, 2 * ClockHour, New(3, 0, 0, 0)}, - {-1, 0, 0, 0, 2 * ClockHour, New(1, 0, 0, 0)}, - {-2, 0, 0, 0, 2 * ClockHour, New(0, 0, 0, 0)}, - {-2, 0, -1, -1, 2 * ClockHour, New(0, 0, -1, -1)}, + {0, 0, 0, 0, 2 * Hour, New(2, 0, 0, 0)}, + {0, 0, 0, 1, 2 * Hour, New(2, 0, 0, 1)}, + {0, 0, 0, -1, 2 * Hour, New(1, 59, 59, 999)}, + {0, 0, 1, 0, 2 * Hour, New(2, 0, 1, 0)}, + {0, 0, -1, 0, 2 * Hour, New(1, 59, 59, 0)}, + {0, 1, 0, 0, 2 * Hour, New(2, 1, 0, 0)}, + {0, -1, 0, 0, 2 * Hour, New(1, 59, 0, 0)}, + {1, 0, 0, 0, 2 * Hour, New(3, 0, 0, 0)}, + {-1, 0, 0, 0, 2 * Hour, New(1, 0, 0, 0)}, + {-2, 0, 0, 0, 2 * Hour, New(0, 0, 0, 0)}, + {-2, 0, -1, -1, 2 * Hour, New(0, 0, -1, -1)}, } for i, x := range cases { got := x.in.Add(x.h, x.m, x.s, x.ms) @@ -101,8 +101,8 @@ func TestClockIsMidnight(t *testing.T) { want bool }{ {New(0, 0, 0, 0), true}, - {ClockDay, true}, - {24 * ClockHour, true}, + {Day, true}, + {24 * Hour, true}, {New(24, 0, 0, 0), true}, {New(-24, 0, 0, 0), true}, {New(-48, 0, 0, 0), true}, @@ -125,18 +125,18 @@ func TestClockMod(t *testing.T) { h, want Clock }{ {0, 0}, - {1 * ClockHour, 1 * ClockHour}, - {2 * ClockHour, 2 * ClockHour}, - {23 * ClockHour, 23 * ClockHour}, - {24 * ClockHour, 0}, - {-24 * ClockHour, 0}, - {-48 * ClockHour, 0}, - {25 * ClockHour, ClockHour}, - {49 * ClockHour, ClockHour}, - {-1 * ClockHour, 23 * ClockHour}, - {-23 * ClockHour, ClockHour}, + {1 * Hour, 1 * Hour}, + {2 * Hour, 2 * Hour}, + {23 * Hour, 23 * Hour}, + {24 * Hour, 0}, + {-24 * Hour, 0}, + {-48 * Hour, 0}, + {25 * Hour, Hour}, + {49 * Hour, Hour}, + {-1 * Hour, 23 * Hour}, + {-23 * Hour, Hour}, {New(0, 0, 0, 1), 1}, - {New(0, 0, 1, 0), ClockSecond}, + {New(0, 0, 1, 0), Second}, {New(0, 0, 0, -1), New(23, 59, 59, 999)}, } for i, x := range cases { @@ -164,7 +164,7 @@ func TestClockDays(t *testing.T) { {-24, -2}, } for i, x := range cases { - clock := Clock(x.h) * ClockHour + clock := Clock(x.h) * Hour if clock.Days() != x.days { t.Errorf("%d: %dh: got %v, want %v", i, x.h, clock.Days(), x.days) } @@ -189,7 +189,7 @@ func TestClockString(t *testing.T) { {-1, -1, -1, -1, "22", "22:58", "22:58:58", "22:58:58.999", "10pm", "10:58pm", "10:58:58pm"}, } for _, x := range cases { - d := Clock(x.h*ClockHour + x.m*ClockMinute + x.s*ClockSecond + x.ms) + d := Clock(x.h*Hour + x.m*Minute + x.s*Second + x.ms) if d.Hh() != x.hh { t.Errorf("%d, %d, %d, %d, got %v, want %v (%d)", x.h, x.m, x.s, x.ms, d.Hh(), x.hh, d) } diff --git a/clock/format.go b/clock/format.go index 3a504da1..65b9fd65 100644 --- a/clock/format.go +++ b/clock/format.go @@ -7,7 +7,7 @@ package clock import "fmt" func clockHours(cm Clock) Clock { - return (cm / ClockHour) + return (cm / Hour) } func clockHours12(cm Clock) (Clock, string) { @@ -23,15 +23,15 @@ func clockHours12(cm Clock) (Clock, string) { } func clockMinutes(cm Clock) Clock { - return (cm % ClockHour) / ClockMinute + return (cm % Hour) / Minute } func clockSeconds(cm Clock) Clock { - return (cm % ClockMinute) / ClockSecond + return (cm % Minute) / Second } func clockMillisec(cm Clock) Clock { - return cm % ClockSecond + return cm % Second } // Hh gets the clock-face number of hours as a two-digit string. From 7e0c2e1bff0cd61bbfbff842e4e6b649831048e6 Mon Sep 17 00:00:00 2001 From: Rick Date: Mon, 4 Jan 2016 17:49:24 +0000 Subject: [PATCH 042/165] Renamed constants --- clock/clock.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/clock/clock.go b/clock/clock.go index d3e86bc1..c41f2311 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -26,22 +26,21 @@ type Clock int32 // Common durations - second, minute, hour and day. const ( // Second is one second; it has a similar meaning to time.Second. - Second Clock = Clock(time.Second / time.Millisecond) - // ClockSecond Clock = Clock(time.Second / time.Millisecond) + Second Clock = Clock(time.Second / time.Millisecond) + ClockSecond Clock = Clock(time.Second / time.Millisecond) // Minute is one minute; it has a similar meaning to time.Minute. - Minute Clock = Clock(time.Minute / time.Millisecond) - // ClockMinute Clock = Clock(time.Minute / time.Millisecond) + Minute Clock = Clock(time.Minute / time.Millisecond) + ClockMinute Clock = Clock(time.Minute / time.Millisecond) // Hour is one hour; it has a similar meaning to time.Hour. - Hour Clock = Clock(time.Hour / time.Millisecond) - // ClockHour Clock = Clock(time.Hour / time.Millisecond) + Hour Clock = Clock(time.Hour / time.Millisecond) + ClockHour Clock = Clock(time.Hour / time.Millisecond) // Day is a fixed period of 24 hours. This does not take account of daylight savings, // so is not fully general. - Day Clock = Clock(time.Hour * 24 / time.Millisecond) - -// ClockDay Clock = Clock(time.Hour * 24 / time.Millisecond) + Day Clock = Clock(time.Hour * 24 / time.Millisecond) + ClockDay Clock = Clock(time.Hour * 24 / time.Millisecond) ) // Midnight is the zero value of a Clock. From 8978bef2207e6e09272c23eabb7b5fa91e48f9ee Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 14 Jan 2016 19:24:12 +0000 Subject: [PATCH 043/165] Bug fix - using "Monday" in a format string clashed with "2nd" --- format.go | 195 +++------------------------------------ format_test.go | 235 +---------------------------------------------- parse.go | 194 +++++++++++++++++++++++++++++++++++++++ parse_test.go | 242 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 449 insertions(+), 417 deletions(-) create mode 100644 parse.go create mode 100644 parse_test.go diff --git a/format.go b/format.go index 33b71843..c1e59b74 100644 --- a/format.go +++ b/format.go @@ -6,10 +6,7 @@ package date import ( "fmt" - "strconv" "strings" - "time" - "unicode" ) // These are predefined layouts for use in Date.Format and Date.Parse. @@ -32,187 +29,6 @@ const ( RFC3339 = "2006-01-02" ) -// MustAutoParse is as per AutoParse except that it panics if the string cannot be parsed. -// This is intended for setup code; don't use it for user inputs. -func MustAutoParse(value string) Date { - d, err := AutoParse(value) - if err != nil { - panic(err) - } - return d -} - -// AutoParse is like ParseISO, except that it automatically adapts to a variety of date formats -// provided that they can be detected unambiguously. Specifically, this includes the "European" -// and "British" date formats but not the common US format. Surrounding whitespace is ignored. -// The supported formats are: -// -// * all formats supported by ParseISO -// -// * yyyy/mm/dd | yyyy.mm.dd (or any similar pattern) -// -// * dd/mm/yyyy | dd.mm.yyyy (or any similar pattern) -// -func AutoParse(value string) (Date, error) { - abs := strings.TrimSpace(value) - sign := "" - if value[0] == '+' || value[0] == '-' { - abs = value[1:] - sign = value[:1] - } - - if len(abs) >= 10 { - i1 := -1 - i2 := -1 - for i, r := range abs { - if unicode.IsPunct(r) { - if i1 < 0 { - i1 = i - } else { - i2 = i - } - } - } - if i1 >= 4 && i2 > i1 && abs[i1] == abs[i2] { - // just normalise the punctuation - a := []byte(abs) - a[i1] = '-' - a[i2] = '-' - abs = string(a) - } else if i1 >= 2 && i2 > i1 && abs[i1] == abs[i2] { - // harder case - need to swap the field order - dd := abs[0:i1] - mm := abs[i1+1 : i2] - yyyy := abs[i2+1:] - abs = fmt.Sprintf("%s-%s-%s", yyyy, mm, dd) - } - } - return ParseISO(sign + abs) -} - -// MustParseISO is as per ParseISO except that it panics if the string cannot be parsed. -// This is intended for setup code; don't use it for user inputs. -func MustParseISO(value string) Date { - d, err := ParseISO(value) - if err != nil { - panic(err) - } - return d -} - -// ParseISO parses an ISO 8601 formatted string and returns the date value it represents. -// In addition to the common formats (e.g. 2006-01-02 and 20060102), this function -// accepts date strings using the expanded year representation -// with possibly extra year digits beyond the prescribed four-digit minimum -// and with a + or - sign prefix (e.g. , "+12345-06-07", "-0987-06-05"). -// -// Note that ParseISO is a little looser than the ISO 8601 standard and will -// be happy to parse dates with a year longer in length than the four-digit minimum even -// if they are missing the + sign prefix. -// -// Function date.Parse can be used to parse date strings in other formats, but it -// is currently not able to parse ISO 8601 formatted strings that use the -// expanded year format. -// -// Background: https://en.wikipedia.org/wiki/ISO_8601#Dates -func ParseISO(value string) (Date, error) { - if len(value) < 8 { - return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %s: incorrect length", value) - } - - abs := value - if value[0] == '+' || value[0] == '-' { - abs = value[1:] - } - - dash1 := strings.IndexByte(abs, '-') - fm1 := dash1 + 1 - fm2 := dash1 + 3 - fd1 := dash1 + 4 - fd2 := dash1 + 6 - - if dash1 < 0 { - // switch to YYYYMMDD format - dash1 = 4 - fm1 = 4 - fm2 = 6 - fd1 = 6 - fd2 = 8 - } else if abs[fm2] != '-' { - return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %s: incorrect syntax", value) - } - //fmt.Printf("%s %d %d %d %d %d\n", value, dash1, fm1, fm2, fd1, fd2) - - if len(abs) != fd2 { - return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %s: incorrect length", value) - } - - year, err := parseField(value, abs[:dash1], "year", 4, -1) - if err != nil { - return Date{}, err - } - - month, err := parseField(value, abs[fm1:fm2], "month", -1, 2) - if err != nil { - return Date{}, err - } - - day, err := parseField(value, abs[fd1:], "day", -1, 2) - if err != nil { - return Date{}, err - } - - if value[0] == '-' { - year = -year - } - - t := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) - - return Date{encode(t)}, nil -} - -func parseField(value, field, name string, minLength, requiredLength int) (int, error) { - if (minLength > 0 && len(field) < minLength) || (requiredLength > 0 && len(field) != requiredLength) { - return 0, fmt.Errorf("Date.ParseISO: cannot parse %s: invalid %s", value, name) - } - number, err := strconv.Atoi(field) - if err != nil { - return 0, fmt.Errorf("Date.ParseISO: cannot parse %s: invalid %s", value, name) - } - return number, nil -} - -// MustParse is as per Parse except that it panics if the string cannot be parsed. -// This is intended for setup code; don't use it for user inputs. -func MustParse(layout, value string) Date { - d, err := Parse(layout, value) - if err != nil { - panic(err) - } - return d -} - -// Parse parses a formatted string of a known layout and returns the Date value it represents. -// The layout defines the format by showing how the reference date, defined -// to be -// Monday, Jan 2, 2006 -// would be interpreted if it were the value; it serves as an example of the -// input format. The same interpretation will then be made to the input string. -// -// This function actually uses time.Parse to parse the input and can use any -// layout accepted by time.Parse, but returns only the date part of the -// parsed Time value. -// -// This function cannot currently parse ISO 8601 strings that use the expanded -// year format; you should use date.ParseISO to parse those strings correctly. -func Parse(layout, value string) (Date, error) { - t, err := time.Parse(layout, value) - if err != nil { - return Date{}, err - } - return Date{encode(t)}, nil -} - // String returns the time formatted in ISO 8601 extended format // (e.g. "2006-01-02"). If the year of the date falls outside the // [0,9999] range, this format produces an expanded year representation @@ -280,6 +96,17 @@ func (d Date) FormatWithSuffixes(layout string, suffixes []string) string { return t.Format(layout) default: + // If the format contains "Monday", it has been split so repair it. + i := 1 + for i < len(parts) { + if i > 0 && strings.HasSuffix(parts[i-1], "Mo") && strings.HasPrefix(parts[i], "ay") { + parts[i-1] = parts[i-1] + "nd" + parts[i] + copy(parts[i:], parts[i+1:]) + parts = parts[:len(parts)-1] + } else { + i++ + } + } a := make([]string, 0, 2*len(parts)-1) for i, p := range parts { if i > 0 { diff --git a/format_test.go b/format_test.go index 22d5c82d..cf596aa3 100644 --- a/format_test.go +++ b/format_test.go @@ -6,241 +6,8 @@ package date import ( "testing" - "time" ) -func TestAutoParse(t *testing.T) { - cases := []struct { - value string - year int - month time.Month - day int - }{ - {"31/12/1969", 1969, time.December, 31}, - {"1969/12/31", 1969, time.December, 31}, - {"1969.12.31", 1969, time.December, 31}, - {"1969-12-31", 1969, time.December, 31}, - {"+1970-01-01", 1970, time.January, 1}, - {"+01970-01-02", 1970, time.January, 2}, - {"2000-02-28", 2000, time.February, 28}, - {"+2000-02-29", 2000, time.February, 29}, - {"+02000-03-01", 2000, time.March, 1}, - {"+002004-02-28", 2004, time.February, 28}, - {"2004-02-29", 2004, time.February, 29}, - {"2004-03-01", 2004, time.March, 1}, - {"0000-01-01", 0, time.January, 1}, - {"+0001-02-03", 1, time.February, 3}, - {"+00019-03-04", 19, time.March, 4}, - {"0100-04-05", 100, time.April, 5}, - {"2000-05-06", 2000, time.May, 6}, - {"+5000000-08-09", 5000000, time.August, 9}, - {"-0001-09-11", -1, time.September, 11}, - {"-0019-10-12", -19, time.October, 12}, - {"-00100-11-13", -100, time.November, 13}, - {"-02000-12-14", -2000, time.December, 14}, - {"-30000-02-15", -30000, time.February, 15}, - {"-0400000-05-16", -400000, time.May, 16}, - {"-5000000-09-17", -5000000, time.September, 17}, - {"12340506", 1234, time.May, 6}, - {"+12340506", 1234, time.May, 6}, - {"-00191012", -19, time.October, 12}, - } - for _, c := range cases { - d := MustAutoParse(c.value) - year, month, day := d.Date() - if year != c.year || month != c.month || day != c.day { - t.Errorf("ParseISO(%v) == %v, want (%v, %v, %v)", c.value, d, c.year, c.month, c.day) - } - } - - badCases := []string{ - "1234-05", - "1234-5-6", - "1234-05-6", - "1234-5-06", - "1234-0A-06", - "1234-05-0B", - "1234-05-06trailing", - "padding1234-05-06", - "1-02-03", - "10-11-12", - "100-02-03", - "+1-02-03", - "+10-11-12", - "+100-02-03", - "-123-05-06", - } - for _, c := range badCases { - d, err := AutoParse(c) - if err == nil { - t.Errorf("ParseISO(%v) == %v", c, d) - } - } -} - -func TestParseISO(t *testing.T) { - cases := []struct { - value string - year int - month time.Month - day int - }{ - {"1969-12-31", 1969, time.December, 31}, - {"+1970-01-01", 1970, time.January, 1}, - {"+01970-01-02", 1970, time.January, 2}, - {"2000-02-28", 2000, time.February, 28}, - {"+2000-02-29", 2000, time.February, 29}, - {"+02000-03-01", 2000, time.March, 1}, - {"+002004-02-28", 2004, time.February, 28}, - {"2004-02-29", 2004, time.February, 29}, - {"2004-03-01", 2004, time.March, 1}, - {"0000-01-01", 0, time.January, 1}, - {"+0001-02-03", 1, time.February, 3}, - {"+00019-03-04", 19, time.March, 4}, - {"0100-04-05", 100, time.April, 5}, - {"2000-05-06", 2000, time.May, 6}, - {"+30000-06-07", 30000, time.June, 7}, - {"+400000-07-08", 400000, time.July, 8}, - {"+5000000-08-09", 5000000, time.August, 9}, - {"-0001-09-11", -1, time.September, 11}, - {"-0019-10-12", -19, time.October, 12}, - {"-00100-11-13", -100, time.November, 13}, - {"-02000-12-14", -2000, time.December, 14}, - {"-30000-02-15", -30000, time.February, 15}, - {"-0400000-05-16", -400000, time.May, 16}, - {"-5000000-09-17", -5000000, time.September, 17}, - {"12340506", 1234, time.May, 6}, - {"+12340506", 1234, time.May, 6}, - {"-00191012", -19, time.October, 12}, - } - for _, c := range cases { - d := MustParseISO(c.value) - year, month, day := d.Date() - if year != c.year || month != c.month || day != c.day { - t.Errorf("ParseISO(%v) == %v, want (%v, %v, %v)", c.value, d, c.year, c.month, c.day) - } - } - - badCases := []string{ - "1234-05", - "1234-5-6", - "1234-05-6", - "1234-5-06", - "1234/05/06", - "1234-0A-06", - "1234-05-0B", - "1234-05-06trailing", - "padding1234-05-06", - "1-02-03", - "10-11-12", - "100-02-03", - "+1-02-03", - "+10-11-12", - "+100-02-03", - "-123-05-06", - } - for _, c := range badCases { - d, err := ParseISO(c) - if err == nil { - t.Errorf("ParseISO(%v) == %v", c, d) - } - } -} - -func BenchmarkParseISO(b *testing.B) { - cases := []struct { - layout string - value string - year int - month time.Month - day int - }{ - {ISO8601, "1969-12-31", 1969, time.December, 31}, - {ISO8601, "2000-02-28", 2000, time.February, 28}, - {ISO8601, "2004-02-29", 2004, time.February, 29}, - {ISO8601, "2004-03-01", 2004, time.March, 1}, - {ISO8601, "0000-01-01", 0, time.January, 1}, - {ISO8601, "0001-02-03", 1, time.February, 3}, - {ISO8601, "0100-04-05", 100, time.April, 5}, - {ISO8601, "2000-05-06", 2000, time.May, 6}, - } - for n := 0; n < b.N; n++ { - c := cases[n%len(cases)] - _, err := ParseISO(c.value) - if err != nil { - b.Errorf("ParseISO(%v) == %v", c.value, err) - } - } -} - -func TestParse(t *testing.T) { - // Test ability to parse a few common date formats - cases := []struct { - layout string - value string - year int - month time.Month - day int - }{ - {ISO8601, "1969-12-31", 1969, time.December, 31}, - {ISO8601B, "19700101", 1970, time.January, 1}, - {RFC822, "29-Feb-00", 2000, time.February, 29}, - {RFC822W, "Mon, 01-Mar-04", 2004, time.March, 1}, - {RFC850, "Wednesday, 12-Aug-15", 2015, time.August, 12}, - {RFC1123, "05 Dec 1928", 1928, time.December, 5}, - {RFC1123W, "Mon, 05 Dec 1928", 1928, time.December, 5}, - {RFC3339, "2345-06-07", 2345, time.June, 7}, - } - for _, c := range cases { - d := MustParse(c.layout, c.value) - year, month, day := d.Date() - if year != c.year || month != c.month || day != c.day { - t.Errorf("Parse(%v) == %v, want (%v, %v, %v)", c.value, d, c.year, c.month, c.day) - } - } - - // Test inability to parse ISO 8601 expanded year format - badCases := []string{ - "+1234-05-06", - "+12345-06-07", - "-1234-05-06", - "-12345-06-07", - } - for _, c := range badCases { - d, err := Parse(ISO8601, c) - if err == nil { - t.Errorf("Parse(%v) == %v", c, d) - } - } -} - -func BenchmarkParse(b *testing.B) { - // Test ability to parse a few common date formats - cases := []struct { - layout string - value string - year int - month time.Month - day int - }{ - {ISO8601, "1969-12-31", 1969, time.December, 31}, - {ISO8601, "2000-02-28", 2000, time.February, 28}, - {ISO8601, "2004-02-29", 2004, time.February, 29}, - {ISO8601, "2004-03-01", 2004, time.March, 1}, - {ISO8601, "0000-01-01", 0, time.January, 1}, - {ISO8601, "0001-02-03", 1, time.February, 3}, - {ISO8601, "0100-04-05", 100, time.April, 5}, - {ISO8601, "2000-05-06", 2000, time.May, 6}, - } - for n := 0; n < b.N; n++ { - c := cases[n%len(cases)] - _, err := Parse(c.layout, c.value) - if err != nil { - b.Errorf("Parse(%v) == %v", c.value, err) - } - } -} - func TestString(t *testing.T) { cases := []struct { value string @@ -305,6 +72,8 @@ func TestFormat(t *testing.T) { {"2016-08-23", "Jan 2nd 2006", "Aug 23rd 2016"}, {"2016-09-30", "Jan 2nd 2006", "Sep 30th 2016"}, {"2016-10-31", "Jan 2nd 2006", "Oct 31st 2016"}, + {"2016-01-07", "Monday January 2nd 2006", "Thursday January 7th 2016"}, + {"2016-01-07", "Monday 2nd Monday 2nd", "Thursday 7th Thursday 7th"}, {"2016-11-01", "2nd 2nd 2nd", "1st 1st 1st"}, } for _, c := range cases { diff --git a/parse.go b/parse.go new file mode 100644 index 00000000..efd3039f --- /dev/null +++ b/parse.go @@ -0,0 +1,194 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package date + +import ( + "fmt" + "strconv" + "strings" + "time" + "unicode" +) + +// MustAutoParse is as per AutoParse except that it panics if the string cannot be parsed. +// This is intended for setup code; don't use it for user inputs. +func MustAutoParse(value string) Date { + d, err := AutoParse(value) + if err != nil { + panic(err) + } + return d +} + +// AutoParse is like ParseISO, except that it automatically adapts to a variety of date formats +// provided that they can be detected unambiguously. Specifically, this includes the "European" +// and "British" date formats but not the common US format. Surrounding whitespace is ignored. +// The supported formats are: +// +// * all formats supported by ParseISO +// +// * yyyy/mm/dd | yyyy.mm.dd (or any similar pattern) +// +// * dd/mm/yyyy | dd.mm.yyyy (or any similar pattern) +// +func AutoParse(value string) (Date, error) { + abs := strings.TrimSpace(value) + sign := "" + if value[0] == '+' || value[0] == '-' { + abs = value[1:] + sign = value[:1] + } + + if len(abs) >= 10 { + i1 := -1 + i2 := -1 + for i, r := range abs { + if unicode.IsPunct(r) { + if i1 < 0 { + i1 = i + } else { + i2 = i + } + } + } + if i1 >= 4 && i2 > i1 && abs[i1] == abs[i2] { + // just normalise the punctuation + a := []byte(abs) + a[i1] = '-' + a[i2] = '-' + abs = string(a) + } else if i1 >= 2 && i2 > i1 && abs[i1] == abs[i2] { + // harder case - need to swap the field order + dd := abs[0:i1] + mm := abs[i1+1 : i2] + yyyy := abs[i2+1:] + abs = fmt.Sprintf("%s-%s-%s", yyyy, mm, dd) + } + } + return ParseISO(sign + abs) +} + +// MustParseISO is as per ParseISO except that it panics if the string cannot be parsed. +// This is intended for setup code; don't use it for user inputs. +func MustParseISO(value string) Date { + d, err := ParseISO(value) + if err != nil { + panic(err) + } + return d +} + +// ParseISO parses an ISO 8601 formatted string and returns the date value it represents. +// In addition to the common formats (e.g. 2006-01-02 and 20060102), this function +// accepts date strings using the expanded year representation +// with possibly extra year digits beyond the prescribed four-digit minimum +// and with a + or - sign prefix (e.g. , "+12345-06-07", "-0987-06-05"). +// +// Note that ParseISO is a little looser than the ISO 8601 standard and will +// be happy to parse dates with a year longer in length than the four-digit minimum even +// if they are missing the + sign prefix. +// +// Function date.Parse can be used to parse date strings in other formats, but it +// is currently not able to parse ISO 8601 formatted strings that use the +// expanded year format. +// +// Background: https://en.wikipedia.org/wiki/ISO_8601#Dates +func ParseISO(value string) (Date, error) { + if len(value) < 8 { + return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %s: incorrect length", value) + } + + abs := value + if value[0] == '+' || value[0] == '-' { + abs = value[1:] + } + + dash1 := strings.IndexByte(abs, '-') + fm1 := dash1 + 1 + fm2 := dash1 + 3 + fd1 := dash1 + 4 + fd2 := dash1 + 6 + + if dash1 < 0 { + // switch to YYYYMMDD format + dash1 = 4 + fm1 = 4 + fm2 = 6 + fd1 = 6 + fd2 = 8 + } else if abs[fm2] != '-' { + return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %s: incorrect syntax", value) + } + //fmt.Printf("%s %d %d %d %d %d\n", value, dash1, fm1, fm2, fd1, fd2) + + if len(abs) != fd2 { + return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %s: incorrect length", value) + } + + year, err := parseField(value, abs[:dash1], "year", 4, -1) + if err != nil { + return Date{}, err + } + + month, err := parseField(value, abs[fm1:fm2], "month", -1, 2) + if err != nil { + return Date{}, err + } + + day, err := parseField(value, abs[fd1:], "day", -1, 2) + if err != nil { + return Date{}, err + } + + if value[0] == '-' { + year = -year + } + + t := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) + + return Date{encode(t)}, nil +} + +func parseField(value, field, name string, minLength, requiredLength int) (int, error) { + if (minLength > 0 && len(field) < minLength) || (requiredLength > 0 && len(field) != requiredLength) { + return 0, fmt.Errorf("Date.ParseISO: cannot parse %s: invalid %s", value, name) + } + number, err := strconv.Atoi(field) + if err != nil { + return 0, fmt.Errorf("Date.ParseISO: cannot parse %s: invalid %s", value, name) + } + return number, nil +} + +// MustParse is as per Parse except that it panics if the string cannot be parsed. +// This is intended for setup code; don't use it for user inputs. +func MustParse(layout, value string) Date { + d, err := Parse(layout, value) + if err != nil { + panic(err) + } + return d +} + +// Parse parses a formatted string of a known layout and returns the Date value it represents. +// The layout defines the format by showing how the reference date, defined +// to be +// Monday, Jan 2, 2006 +// would be interpreted if it were the value; it serves as an example of the +// input format. The same interpretation will then be made to the input string. +// +// This function actually uses time.Parse to parse the input and can use any +// layout accepted by time.Parse, but returns only the date part of the +// parsed Time value. +// +// This function cannot currently parse ISO 8601 strings that use the expanded +// year format; you should use date.ParseISO to parse those strings correctly. +func Parse(layout, value string) (Date, error) { + t, err := time.Parse(layout, value) + if err != nil { + return Date{}, err + } + return Date{encode(t)}, nil +} diff --git a/parse_test.go b/parse_test.go new file mode 100644 index 00000000..12d013d4 --- /dev/null +++ b/parse_test.go @@ -0,0 +1,242 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package date + +import ( + "testing" + "time" +) + +func TestAutoParse(t *testing.T) { + cases := []struct { + value string + year int + month time.Month + day int + }{ + {"31/12/1969", 1969, time.December, 31}, + {"1969/12/31", 1969, time.December, 31}, + {"1969.12.31", 1969, time.December, 31}, + {"1969-12-31", 1969, time.December, 31}, + {"+1970-01-01", 1970, time.January, 1}, + {"+01970-01-02", 1970, time.January, 2}, + {"2000-02-28", 2000, time.February, 28}, + {"+2000-02-29", 2000, time.February, 29}, + {"+02000-03-01", 2000, time.March, 1}, + {"+002004-02-28", 2004, time.February, 28}, + {"2004-02-29", 2004, time.February, 29}, + {"2004-03-01", 2004, time.March, 1}, + {"0000-01-01", 0, time.January, 1}, + {"+0001-02-03", 1, time.February, 3}, + {"+00019-03-04", 19, time.March, 4}, + {"0100-04-05", 100, time.April, 5}, + {"2000-05-06", 2000, time.May, 6}, + {"+5000000-08-09", 5000000, time.August, 9}, + {"-0001-09-11", -1, time.September, 11}, + {"-0019-10-12", -19, time.October, 12}, + {"-00100-11-13", -100, time.November, 13}, + {"-02000-12-14", -2000, time.December, 14}, + {"-30000-02-15", -30000, time.February, 15}, + {"-0400000-05-16", -400000, time.May, 16}, + {"-5000000-09-17", -5000000, time.September, 17}, + {"12340506", 1234, time.May, 6}, + {"+12340506", 1234, time.May, 6}, + {"-00191012", -19, time.October, 12}, + } + for _, c := range cases { + d := MustAutoParse(c.value) + year, month, day := d.Date() + if year != c.year || month != c.month || day != c.day { + t.Errorf("ParseISO(%v) == %v, want (%v, %v, %v)", c.value, d, c.year, c.month, c.day) + } + } + + badCases := []string{ + "1234-05", + "1234-5-6", + "1234-05-6", + "1234-5-06", + "1234-0A-06", + "1234-05-0B", + "1234-05-06trailing", + "padding1234-05-06", + "1-02-03", + "10-11-12", + "100-02-03", + "+1-02-03", + "+10-11-12", + "+100-02-03", + "-123-05-06", + } + for _, c := range badCases { + d, err := AutoParse(c) + if err == nil { + t.Errorf("ParseISO(%v) == %v", c, d) + } + } +} + +func TestParseISO(t *testing.T) { + cases := []struct { + value string + year int + month time.Month + day int + }{ + {"1969-12-31", 1969, time.December, 31}, + {"+1970-01-01", 1970, time.January, 1}, + {"+01970-01-02", 1970, time.January, 2}, + {"2000-02-28", 2000, time.February, 28}, + {"+2000-02-29", 2000, time.February, 29}, + {"+02000-03-01", 2000, time.March, 1}, + {"+002004-02-28", 2004, time.February, 28}, + {"2004-02-29", 2004, time.February, 29}, + {"2004-03-01", 2004, time.March, 1}, + {"0000-01-01", 0, time.January, 1}, + {"+0001-02-03", 1, time.February, 3}, + {"+00019-03-04", 19, time.March, 4}, + {"0100-04-05", 100, time.April, 5}, + {"2000-05-06", 2000, time.May, 6}, + {"+30000-06-07", 30000, time.June, 7}, + {"+400000-07-08", 400000, time.July, 8}, + {"+5000000-08-09", 5000000, time.August, 9}, + {"-0001-09-11", -1, time.September, 11}, + {"-0019-10-12", -19, time.October, 12}, + {"-00100-11-13", -100, time.November, 13}, + {"-02000-12-14", -2000, time.December, 14}, + {"-30000-02-15", -30000, time.February, 15}, + {"-0400000-05-16", -400000, time.May, 16}, + {"-5000000-09-17", -5000000, time.September, 17}, + {"12340506", 1234, time.May, 6}, + {"+12340506", 1234, time.May, 6}, + {"-00191012", -19, time.October, 12}, + } + for _, c := range cases { + d := MustParseISO(c.value) + year, month, day := d.Date() + if year != c.year || month != c.month || day != c.day { + t.Errorf("ParseISO(%v) == %v, want (%v, %v, %v)", c.value, d, c.year, c.month, c.day) + } + } + + badCases := []string{ + "1234-05", + "1234-5-6", + "1234-05-6", + "1234-5-06", + "1234/05/06", + "1234-0A-06", + "1234-05-0B", + "1234-05-06trailing", + "padding1234-05-06", + "1-02-03", + "10-11-12", + "100-02-03", + "+1-02-03", + "+10-11-12", + "+100-02-03", + "-123-05-06", + } + for _, c := range badCases { + d, err := ParseISO(c) + if err == nil { + t.Errorf("ParseISO(%v) == %v", c, d) + } + } +} + +func BenchmarkParseISO(b *testing.B) { + cases := []struct { + layout string + value string + year int + month time.Month + day int + }{ + {ISO8601, "1969-12-31", 1969, time.December, 31}, + {ISO8601, "2000-02-28", 2000, time.February, 28}, + {ISO8601, "2004-02-29", 2004, time.February, 29}, + {ISO8601, "2004-03-01", 2004, time.March, 1}, + {ISO8601, "0000-01-01", 0, time.January, 1}, + {ISO8601, "0001-02-03", 1, time.February, 3}, + {ISO8601, "0100-04-05", 100, time.April, 5}, + {ISO8601, "2000-05-06", 2000, time.May, 6}, + } + for n := 0; n < b.N; n++ { + c := cases[n%len(cases)] + _, err := ParseISO(c.value) + if err != nil { + b.Errorf("ParseISO(%v) == %v", c.value, err) + } + } +} + +func TestParse(t *testing.T) { + // Test ability to parse a few common date formats + cases := []struct { + layout string + value string + year int + month time.Month + day int + }{ + {ISO8601, "1969-12-31", 1969, time.December, 31}, + {ISO8601B, "19700101", 1970, time.January, 1}, + {RFC822, "29-Feb-00", 2000, time.February, 29}, + {RFC822W, "Mon, 01-Mar-04", 2004, time.March, 1}, + {RFC850, "Wednesday, 12-Aug-15", 2015, time.August, 12}, + {RFC1123, "05 Dec 1928", 1928, time.December, 5}, + {RFC1123W, "Mon, 05 Dec 1928", 1928, time.December, 5}, + {RFC3339, "2345-06-07", 2345, time.June, 7}, + } + for _, c := range cases { + d := MustParse(c.layout, c.value) + year, month, day := d.Date() + if year != c.year || month != c.month || day != c.day { + t.Errorf("Parse(%v) == %v, want (%v, %v, %v)", c.value, d, c.year, c.month, c.day) + } + } + + // Test inability to parse ISO 8601 expanded year format + badCases := []string{ + "+1234-05-06", + "+12345-06-07", + "-1234-05-06", + "-12345-06-07", + } + for _, c := range badCases { + d, err := Parse(ISO8601, c) + if err == nil { + t.Errorf("Parse(%v) == %v", c, d) + } + } +} + +func BenchmarkParse(b *testing.B) { + // Test ability to parse a few common date formats + cases := []struct { + layout string + value string + year int + month time.Month + day int + }{ + {ISO8601, "1969-12-31", 1969, time.December, 31}, + {ISO8601, "2000-02-28", 2000, time.February, 28}, + {ISO8601, "2004-02-29", 2004, time.February, 29}, + {ISO8601, "2004-03-01", 2004, time.March, 1}, + {ISO8601, "0000-01-01", 0, time.January, 1}, + {ISO8601, "0001-02-03", 1, time.February, 3}, + {ISO8601, "0100-04-05", 100, time.April, 5}, + {ISO8601, "2000-05-06", 2000, time.May, 6}, + } + for n := 0; n < b.N; n++ { + c := cases[n%len(cases)] + _, err := Parse(c.layout, c.value) + if err != nil { + b.Errorf("Parse(%v) == %v", c.value, err) + } + } +} From 943b43291f9535d37ade044b8041cde72f94a11b Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 14 Jan 2016 19:48:40 +0000 Subject: [PATCH 044/165] New view package for fluent formatting of dates. --- view/vdate.go | 98 ++++++++++++++++++++++++++++++++++++++++++++++ view/vdate_test.go | 44 +++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 view/vdate.go create mode 100644 view/vdate_test.go diff --git a/view/vdate.go b/view/vdate.go new file mode 100644 index 00000000..38d74516 --- /dev/null +++ b/view/vdate.go @@ -0,0 +1,98 @@ +// This package provides a fluent API for formatting dates as strings. This is useful in view-models +// especially when using Go temapltes. +package view + +import ( + "github.com/rickb777/date" + "r3/roster3/util/r3date" +) + +const WebDateFormat = "02/01/2006" +const SqlDateFormat = "2006-01-02" + +type VDate struct { + d date.Date +} + +func NewVDate(d date.Date) VDate { + return VDate{d} +} + +func (d VDate) Next() VDateDelta { + return VDateDelta{d.d, 1} +} + +func (d VDate) Previous() VDateDelta { + return VDateDelta{d.d, -1} +} + +func (d VDate) String() string { + return d.d.Format(r3date.SqlDateFormat) +} + +func (d VDate) Web() string { + return d.d.Format(r3date.WebDateFormat) +} + +func (d VDate) Mon() string { + return d.d.Format("Mon") +} + +func (d VDate) Monday() string { + return d.d.Format("Monday") +} + +func (d VDate) Day2() string { + return d.d.Format("2") +} + +func (d VDate) Day02() string { + return d.d.Format("02") +} + +func (d VDate) Day02nd() string { + return d.d.Format("2nd") +} + +func (d VDate) Month1() string { + return d.d.Format("1") +} + +func (d VDate) Month01() string { + return d.d.Format("01") +} + +func (d VDate) MonthJan() string { + return d.d.Format("Jan") +} + +func (d VDate) MonthJanuary() string { + return d.d.Format("January") +} + +func (d VDate) Year() string { + return d.d.Format("2006") +} + +//------------------------------------------------------------------------------------------------- + +type VDateDelta struct { + d date.Date + sign date.PeriodOfDays +} + +func (dd VDateDelta) Day() VDate { + return VDate{dd.d.Add(dd.sign)} +} + +func (dd VDateDelta) Week() VDate { + return VDate{dd.d.Add(dd.sign * 7)} +} + +func (dd VDateDelta) Month() VDate { + return VDate{dd.d.AddDate(0, int(dd.sign), 0)} +} + +func (dd VDateDelta) Year() VDate { + return VDate{dd.d.AddDate(int(dd.sign), 0, 0)} +} diff --git a/view/vdate_test.go b/view/vdate_test.go new file mode 100644 index 00000000..1d8d4b08 --- /dev/null +++ b/view/vdate_test.go @@ -0,0 +1,44 @@ +package view + +import ( + "github.com/rickb777/date" + "testing" +) + +func TestBasicFormatting(t *testing.T) { + d := NewVDate(date.New(2016, 2, 7)) + is(t, d.String(), "2016-02-07") + is(t, d.Web(), "07/02/2016") + is(t, d.Mon(), "Sun") + is(t, d.Monday(), "Sunday") + is(t, d.Day2(), "7") + is(t, d.Day02(), "07") + is(t, d.Day02nd(), "7th") + is(t, d.Month1(), "2") + is(t, d.Month01(), "02") + is(t, d.MonthJan(), "Feb") + is(t, d.MonthJanuary(), "February") + is(t, d.Year(), "2016") +} + +func TestNext(t *testing.T) { + d := NewVDate(date.New(2016, 2, 7)) + is(t, d.Next().Day().String(), "2016-02-08") + is(t, d.Next().Week().String(), "2016-02-14") + is(t, d.Next().Month().String(), "2016-03-07") + is(t, d.Next().Year().String(), "2017-02-07") +} + +func TestPrevious(t *testing.T) { + d := NewVDate(date.New(2016, 2, 7)) + is(t, d.Previous().Day().String(), "2016-02-06") + is(t, d.Previous().Week().String(), "2016-01-31") + is(t, d.Previous().Month().String(), "2016-01-07") + is(t, d.Previous().Year().String(), "2015-02-07") +} + +func is(t *testing.T, s1, s2 string) { + if s1 != s2 { + t.Error("%s != %s", s1, s2) + } +} From 00b595c09ce0d06617e45b855fe86dd0aa763eb4 Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 14 Jan 2016 20:43:19 +0000 Subject: [PATCH 045/165] Revised to removed incorrect dependencies. Improved the API and documentation. --- view/vdate.go | 80 +++++++++++++++++++++++++++++++++------------- view/vdate_test.go | 9 +++--- 2 files changed, 62 insertions(+), 27 deletions(-) diff --git a/view/vdate.go b/view/vdate.go index 38d74516..66f28e21 100644 --- a/view/vdate.go +++ b/view/vdate.go @@ -1,98 +1,132 @@ -// This package provides a fluent API for formatting dates as strings. This is useful in view-models -// especially when using Go temapltes. +// Package view provides a simple API for formatting dates as strings in a manner that is easy to use in view-models, +// especially when using Go templates. package view import ( "github.com/rickb777/date" - "r3/roster3/util/r3date" ) -const WebDateFormat = "02/01/2006" -const SqlDateFormat = "2006-01-02" +const ( + // DMYFormat is a typical British representation. + DMYFormat = "02/01/2006" + // MDYFormat is a typical American representation. + MDYFormat = "01/02/2006" + // DefaultFormat is used by Format() unless a different format is set. + DefaultFormat = DMYFormat +) +// A VDate holds a Date and provides easy ways to render it, e.g. in Go templates. type VDate struct { d date.Date + f string } +// NewVDate wraps a Date. func NewVDate(d date.Date) VDate { - return VDate{d} -} - -func (d VDate) Next() VDateDelta { - return VDateDelta{d.d, 1} + return VDate{d, DefaultFormat} } -func (d VDate) Previous() VDateDelta { - return VDateDelta{d.d, -1} +// String formats the date in basic ISO8601 format YYYY-MM-DD. +func (d VDate) String() string { + return d.d.String() } -func (d VDate) String() string { - return d.d.Format(r3date.SqlDateFormat) +// WithFormat creates a new instance containing the specified format string. +func (d VDate) WithFormat(f string) VDate { + return VDate{d.d, f} } -func (d VDate) Web() string { - return d.d.Format(r3date.WebDateFormat) +// Format formats the date using the specified format string, or "02/01/2006" by default. +// Use WithFormat to set this up. +func (d VDate) Format() string { + return d.d.Format(d.f) } +// Mon returns the day name as three letters. func (d VDate) Mon() string { return d.d.Format("Mon") } +// Monday returns the full day name. func (d VDate) Monday() string { return d.d.Format("Monday") } +// Day2 returns the day number without a leading zero. func (d VDate) Day2() string { return d.d.Format("2") } +// Day02 returns the day number with a leading zero if necessary. func (d VDate) Day02() string { return d.d.Format("02") } -func (d VDate) Day02nd() string { +// Day2nd returns the day number without a leading zero but with the appropriate +// "st", "nd", "rd", "th" suffix. +func (d VDate) Day2nd() string { return d.d.Format("2nd") } +// Month1 returns the month number without a leading zero. func (d VDate) Month1() string { return d.d.Format("1") } +// Month01 returns the month number with a leading zero if necessary. func (d VDate) Month01() string { return d.d.Format("01") } -func (d VDate) MonthJan() string { +// Jan returns the month name abbreviated to three letters. +func (d VDate) Jan() string { return d.d.Format("Jan") } -func (d VDate) MonthJanuary() string { +// January returns the full month name. +func (d VDate) January() string { return d.d.Format("January") } +// Year returns the four-digit year. func (d VDate) Year() string { return d.d.Format("2006") } +// Next returns a fluent generator for later dates. +func (d VDate) Next() VDateDelta { + return VDateDelta{d.d, d.f, 1} +} + +// Previous returns a fluent generator for earlier dates. +func (d VDate) Previous() VDateDelta { + return VDateDelta{d.d, d.f, -1} +} + //------------------------------------------------------------------------------------------------- type VDateDelta struct { d date.Date + f string sign date.PeriodOfDays } +// Day adds or subtracts one day. func (dd VDateDelta) Day() VDate { - return VDate{dd.d.Add(dd.sign)} + return VDate{dd.d.Add(dd.sign), dd.f} } +// Week adds or subtracts one week. func (dd VDateDelta) Week() VDate { - return VDate{dd.d.Add(dd.sign * 7)} + return VDate{dd.d.Add(dd.sign * 7), dd.f} } +// Month adds or subtracts one month. func (dd VDateDelta) Month() VDate { - return VDate{dd.d.AddDate(0, int(dd.sign), 0)} + return VDate{dd.d.AddDate(0, int(dd.sign), 0), dd.f} } +// Year adds or subtracts one year. func (dd VDateDelta) Year() VDate { - return VDate{dd.d.AddDate(int(dd.sign), 0, 0)} + return VDate{dd.d.AddDate(int(dd.sign), 0, 0), dd.f} } diff --git a/view/vdate_test.go b/view/vdate_test.go index 1d8d4b08..7c81c80c 100644 --- a/view/vdate_test.go +++ b/view/vdate_test.go @@ -8,16 +8,17 @@ import ( func TestBasicFormatting(t *testing.T) { d := NewVDate(date.New(2016, 2, 7)) is(t, d.String(), "2016-02-07") - is(t, d.Web(), "07/02/2016") + is(t, d.Format(), "07/02/2016") + is(t, d.WithFormat(MDYFormat).Format(), "02/07/2016") is(t, d.Mon(), "Sun") is(t, d.Monday(), "Sunday") is(t, d.Day2(), "7") is(t, d.Day02(), "07") - is(t, d.Day02nd(), "7th") + is(t, d.Day2nd(), "7th") is(t, d.Month1(), "2") is(t, d.Month01(), "02") - is(t, d.MonthJan(), "Feb") - is(t, d.MonthJanuary(), "February") + is(t, d.Jan(), "Feb") + is(t, d.January(), "February") is(t, d.Year(), "2016") } From 0177733477a4a219d3bdc04b4ddd707139140dfb Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 15 Jan 2016 23:34:06 +0000 Subject: [PATCH 046/165] Added lossy support for marshalling (a.k.a. marshaling) for JSON and Text variants only. --- marshal.go | 26 +++++++-------------- marshal_test.go | 10 +++++++-- view/vdate.go | 38 +++++++++++++++++++++++++++++++ view/vdate_test.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 20 deletions(-) diff --git a/marshal.go b/marshal.go index de616b45..913f7b5d 100644 --- a/marshal.go +++ b/marshal.go @@ -30,7 +30,6 @@ func (d *Date) UnmarshalBinary(data []byte) error { } d.day = PeriodOfDays(data[3]) | PeriodOfDays(data[2])<<8 | PeriodOfDays(data[1])<<16 | PeriodOfDays(data[0])<<24 - // d.decoded = time.Time{} return nil } @@ -60,19 +59,12 @@ func (d Date) MarshalJSON() ([]byte, error) { // (e.g. "2006-01-02", "+12345-06-07", "-0987-06-05"); // the year must use at least 4 digits and if outside the [0,9999] range // must be prefixed with a + or - sign. -func (d *Date) UnmarshalJSON(data []byte) (err error) { - value := string(data) - n := len(value) - if n < 2 || value[0] != '"' || value[n-1] != '"' { - return fmt.Errorf("Date.UnmarshalJSON: missing double quotes (%s)", value) +func (d *Date) UnmarshalJSON(data []byte) error { + n := len(data) + if n < 2 || data[0] != '"' || data[n-1] != '"' { + return fmt.Errorf("Date.UnmarshalJSON: missing double quotes (%s)", string(data)) } - u, err := ParseISO(value[1 : n-1]) - if err != nil { - return err - } - d.day = u.day - // d.decoded = time.Time{} - return nil + return d.UnmarshalText(data[1 : n-1]) } // MarshalText implements the encoding.TextMarshaler interface. @@ -92,10 +84,8 @@ func (d Date) MarshalText() ([]byte, error) { // must be prefixed with a + or - sign. func (d *Date) UnmarshalText(data []byte) (err error) { u, err := ParseISO(string(data)) - if err != nil { - return err + if err == nil { + d.day = u.day } - d.day = u.day - // d.decoded = time.Time{} - return nil + return err } diff --git a/marshal_test.go b/marshal_test.go index a1e7e967..4e804821 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -34,6 +34,8 @@ func TestGobEncoding(t *testing.T) { err = decoder.Decode(&d) if err != nil { t.Errorf("Gob(%v) decode error %v", c, err) + } else if d != c { + t.Errorf("Gob(%v) decode got %v", c, d) } } } @@ -61,7 +63,6 @@ func TestInvalidGob(t *testing.T) { } func TestJSONMarshalling(t *testing.T) { - var d Date cases := []struct { value Date want string @@ -75,6 +76,7 @@ func TestJSONMarshalling(t *testing.T) { {New(12345, time.June, 7), `"+12345-06-07"`}, } for _, c := range cases { + var d Date bytes, err := json.Marshal(c.value) if err != nil { t.Errorf("JSON(%v) marshal error %v", c, err) @@ -84,6 +86,8 @@ func TestJSONMarshalling(t *testing.T) { err = json.Unmarshal(bytes, &d) if err != nil { t.Errorf("JSON(%v) unmarshal error %v", c.value, err) + } else if d != c.value { + t.Errorf("JSON(%v) unmarshal got %v", c.value, d) } } } @@ -109,7 +113,6 @@ func TestInvalidJSON(t *testing.T) { } func TestTextMarshalling(t *testing.T) { - var d Date cases := []struct { value Date want string @@ -123,6 +126,7 @@ func TestTextMarshalling(t *testing.T) { {New(12345, time.June, 7), "+12345-06-07"}, } for _, c := range cases { + var d Date bytes, err := c.value.MarshalText() if err != nil { t.Errorf("Text(%v) marshal error %v", c, err) @@ -132,6 +136,8 @@ func TestTextMarshalling(t *testing.T) { err = d.UnmarshalText(bytes) if err != nil { t.Errorf("Text(%v) unmarshal error %v", c.value, err) + } else if d != c.value { + t.Errorf("Text(%v) unmarshal got %v", c.value, d) } } } diff --git a/view/vdate.go b/view/vdate.go index 66f28e21..39b63656 100644 --- a/view/vdate.go +++ b/view/vdate.go @@ -103,6 +103,44 @@ func (d VDate) Previous() VDateDelta { return VDateDelta{d.d, d.f, -1} } +//------------------------------------------------------------------------------------------------- +// Only lossy transcoding is supported here because the intention is that data exchange should be +// via the main Date type; VDate is only intended for output through view layers. + +// MarshalJSON implements the json.Marshaler interface. +func (v VDate) MarshalJSON() ([]byte, error) { + return v.d.MarshalJSON() +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +// Note that the format value gets lost. +func (v *VDate) UnmarshalJSON(data []byte) (err error) { + u := &date.Date{} + err = u.UnmarshalJSON(data) + if err == nil { + v.d = *u + v.f = DefaultFormat + } + return err +} + +// MarshalText implements the encoding.TextMarshaler interface. +func (v VDate) MarshalText() ([]byte, error) { + return v.d.MarshalText() +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +// Note that the format value gets lost. +func (v *VDate) UnmarshalText(data []byte) (err error) { + u := &date.Date{} + err = u.UnmarshalText(data) + if err == nil { + v.d = *u + v.f = DefaultFormat + } + return err +} + //------------------------------------------------------------------------------------------------- type VDateDelta struct { diff --git a/view/vdate_test.go b/view/vdate_test.go index 7c81c80c..5326a630 100644 --- a/view/vdate_test.go +++ b/view/vdate_test.go @@ -1,8 +1,10 @@ package view import ( + "encoding/json" "github.com/rickb777/date" "testing" + "time" ) func TestBasicFormatting(t *testing.T) { @@ -43,3 +45,57 @@ func is(t *testing.T, s1, s2 string) { t.Error("%s != %s", s1, s2) } } + +func TestJSONMarshalling(t *testing.T) { + cases := []struct { + value VDate + want string + }{ + {NewVDate(date.New(-1, time.December, 31)), `"-0001-12-31"`}, + {NewVDate(date.New(2012, time.June, 25)), `"2012-06-25"`}, + {NewVDate(date.New(12345, time.June, 7)), `"+12345-06-07"`}, + } + for _, c := range cases { + var d VDate + bytes, err := json.Marshal(c.value) + if err != nil { + t.Errorf("JSON(%v) marshal error %v", c, err) + } else if string(bytes) != c.want { + t.Errorf("JSON(%v) == %v, want %v", c.value, string(bytes), c.want) + } else { + err = json.Unmarshal(bytes, &d) + if err != nil { + t.Errorf("JSON(%v) unmarshal error %v", c.value, err) + } else if d != c.value { + t.Errorf("JSON(%#v) unmarshal got %#v", c.value, d) + } + } + } +} + +func TestTextMarshalling(t *testing.T) { + cases := []struct { + value VDate + want string + }{ + {NewVDate(date.New(-1, time.December, 31)), "-0001-12-31"}, + {NewVDate(date.New(2012, time.June, 25)), "2012-06-25"}, + {NewVDate(date.New(12345, time.June, 7)), "+12345-06-07"}, + } + for _, c := range cases { + var d VDate + bytes, err := c.value.MarshalText() + if err != nil { + t.Errorf("Text(%v) marshal error %v", c, err) + } else if string(bytes) != c.want { + t.Errorf("Text(%v) == %v, want %v", c.value, string(bytes), c.want) + } else { + err = d.UnmarshalText(bytes) + if err != nil { + t.Errorf("Text(%v) unmarshal error %v", c.value, err) + } else if d != c.value { + t.Errorf("Text(%#v) unmarshal got %#v", c.value, d) + } + } + } +} From 2408229b1970b979126a3e21b76718875bfe271f Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 26 Jan 2016 21:34:07 +0000 Subject: [PATCH 047/165] DateRange - added EmptyRange and IsZero; deprecated ZeroRange --- timespan/daterange.go | 15 +++++++++++++-- timespan/daterange_test.go | 26 +++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/timespan/daterange.go b/timespan/daterange.go index 887b1593..13ea9119 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -50,8 +50,13 @@ func NewMonthOf(year int, month time.Month) DateRange { return DateRange{start, PeriodOfDays(end.Sub(start))} } -// ZeroRange constructs an empty range. This is often a useful basis for +// 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) DateRange { + return DateRange{day, 0} +} + +// Deprecated - ZeroRange constructs an empty range. Use EmptyRange instead. func ZeroRange(day Date) DateRange { return DateRange{day, 0} } @@ -67,7 +72,13 @@ func (dateRange DateRange) Days() PeriodOfDays { return dateRange.days } -// IsEmpty returns true if this is an empty range (zero 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.IsZero() +} + +// IsEmpty returns true if this has a starting date but the range is empty (zero days). func (dateRange DateRange) IsEmpty() bool { return dateRange.days == 0 } diff --git a/timespan/daterange_test.go b/timespan/daterange_test.go index 215351b6..d4d6e44e 100644 --- a/timespan/daterange_test.go +++ b/timespan/daterange_test.go @@ -62,9 +62,10 @@ func TestNewDateRangeWithNormalise(t *testing.T) { isEq(t, r2.End(), d0402) } -func TestOneDayRange(t *testing.T) { +func TestEmptyRange(t *testing.T) { drN0 := DateRange{d0327, -1} isEq(t, drN0.Days(), PeriodOfDays(-1)) + isEq(t, drN0.IsZero(), false) isEq(t, drN0.IsEmpty(), false) isEq(t, drN0.Start(), d0327) isEq(t, drN0.Last(), d0327) @@ -72,10 +73,29 @@ func TestOneDayRange(t *testing.T) { dr0 := DateRange{} isEq(t, dr0.Days(), PeriodOfDays(0)) + isEq(t, dr0.IsZero(), true) isEq(t, dr0.IsEmpty(), true) isEq(t, dr0.String(), "0 days from 1970-01-01") + dr1 := EmptyRange(Date{}) + isEq(t, dr1.IsZero(), true) + isEq(t, dr1.IsEmpty(), true) + isEq(t, dr1.Days(), PeriodOfDays(0)) + + dr2 := EmptyRange(d0327) + isEq(t, dr2.IsZero(), false) + isEq(t, dr2.IsEmpty(), true) + isEq(t, dr2.Start(), d0327) + isEq(t, dr2.Last().IsZero(), true) + isEq(t, dr2.End(), d0327) + isEq(t, dr2.Days(), PeriodOfDays(0)) + isEq(t, dr2.String(), "0 days from 2015-03-27") +} + +func TestOneDayRange(t *testing.T) { dr1 := OneDayRange(Date{}) + isEq(t, dr1.IsZero(), false) + isEq(t, dr1.IsEmpty(), false) isEq(t, dr1.Days(), PeriodOfDays(1)) dr2 := OneDayRange(d0327) @@ -217,8 +237,8 @@ func TestMergeNonOverlapping(t *testing.T) { } func TestMergeEmpties(t *testing.T) { - dr1 := ZeroRange(d0320) - dr2 := ZeroRange(d0408) // curiously, this is *not* included because it has no size. + dr1 := EmptyRange(d0320) + dr2 := EmptyRange(d0408) // curiously, this is *not* included because it has no size. m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) isEq(t, m1.Start(), d0320) From e34666d816f3db463caa96af5076352d54f8941e Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 26 Jan 2016 21:48:38 +0000 Subject: [PATCH 048/165] DateRange.Merge now excludes zero-valued ranges as a special case. --- timespan/daterange.go | 17 ++++++++++++++--- timespan/daterange_test.go | 21 ++++++++++++++++----- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/timespan/daterange.go b/timespan/daterange.go index 13ea9119..047d1d7c 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -199,10 +199,21 @@ func (dateRange DateRange) ContainsTime(t time.Time) bool { } // Merge combines two date ranges by calculating a date range that just encompasses them both. -// As a special case, if one range is entirely contained within the other range, the larger of -// the two is returned. Otherwise, the result is from the start of the earlier one to the end of -// the later one, even if the two ranges don't overlap. +// There are two special cases. +// +// Firstly, if one range is entirely contained within the other range, the larger of the two is +// returned. Otherwise, the result is from the start of the earlier one to the end of the later +// one, even if the two ranges don't overlap. +// +// Secondly, if either range is the zero value (see IsZero), it is excluded from the merge and +// the other range is returned unchanged. func (thisRange DateRange) Merge(thatRange DateRange) DateRange { + if thatRange.IsZero() { + return thisRange + } + if thisRange.IsZero() { + return thatRange + } minStart := thisRange.Start().Min(thatRange.Start()) maxEnd := thisRange.End().Max(thatRange.End()) return NewDateRange(minStart, maxEnd) diff --git a/timespan/daterange_test.go b/timespan/daterange_test.go index d4d6e44e..46a49372 100644 --- a/timespan/daterange_test.go +++ b/timespan/daterange_test.go @@ -24,6 +24,7 @@ var d0331 = New(2015, time.March, 31) var d0401 = New(2015, time.April, 1) var d0402 = New(2015, time.April, 2) var d0403 = New(2015, time.April, 3) +var d0404 = New(2015, time.April, 4) var d0407 = New(2015, time.April, 7) var d0408 = New(2015, time.April, 8) var d0410 = New(2015, time.April, 10) @@ -200,7 +201,7 @@ func TestMerge1(t *testing.T) { m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) isEq(t, m1.Start(), d0327) - isEq(t, m1.Last(), d0403) + isEq(t, m1.End(), d0404) isEq(t, m1, m2) } @@ -210,7 +211,7 @@ func TestMerge2(t *testing.T) { m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) isEq(t, m1.Start(), d0327) - isEq(t, m1.Last(), d0403) + isEq(t, m1.End(), d0404) isEq(t, m1, m2) } @@ -220,7 +221,6 @@ func TestMergeOverlapping(t *testing.T) { m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) isEq(t, m1.Start(), d0320) - isEq(t, m1.Last(), d0407) isEq(t, m1.End(), d0408) isEq(t, m1, m2) } @@ -231,7 +231,6 @@ func TestMergeNonOverlapping(t *testing.T) { m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) isEq(t, m1.Start(), d0320) - isEq(t, m1.Last(), d0407) isEq(t, m1.End(), d0408) isEq(t, m1, m2) } @@ -242,11 +241,23 @@ func TestMergeEmpties(t *testing.T) { m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) isEq(t, m1.Start(), d0320) - isEq(t, m1.Last(), d0407) isEq(t, m1.End(), d0408) isEq(t, m1, m2) } +func TestMergeZeroes(t *testing.T) { + dr0 := DateRange{} + dr1 := OneDayRange(d0401).ExtendBy(6) + m1 := dr1.Merge(dr0) + m2 := dr0.Merge(dr1) + m3 := dr0.Merge(dr0) + isEq(t, m1.Start(), d0401) + isEq(t, m1.End(), d0408) + isEq(t, m1, m2) + isEq(t, m3.IsZero(), true) + isEq(t, m3, dr0) +} + func TestDurationNormalUTC(t *testing.T) { dr := OneDayRange(d0329) isEq(t, dr.Duration(), time.Hour*24) From f30c14d2719b8afd711ec1dc42204394d8a6ffa6 Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 2 Feb 2016 21:49:22 +0000 Subject: [PATCH 049/165] New Period type represents ISO-8601 periods such as P2Y3M --- date.go | 16 ++ date_test.go | 124 ++++++++++----- isoperiod.go | 318 +++++++++++++++++++++++++++++++++++++ isoperiod_test.go | 108 +++++++++++++ marshal.go | 14 ++ marshal_test.go | 52 +++++- timespan/daterange.go | 44 +++-- timespan/daterange_test.go | 48 +++++- 8 files changed, 667 insertions(+), 57 deletions(-) create mode 100644 isoperiod.go create mode 100644 isoperiod_test.go diff --git a/date.go b/date.go index a7b662e1..25bd47c8 100644 --- a/date.go +++ b/date.go @@ -218,11 +218,27 @@ func (d Date) Add(days PeriodOfDays) Date { // AddDate returns the date corresponding to adding the given number of years, // months, and days to d. For example, AddData(-1, 2, 3) applied to // January 1, 2011 returns March 4, 2010. +// +// AddDate normalizes its result in the same way that Date does, +// so, for example, adding one month to October 31 yields +// December 1, the normalized form for November 31. +// +// The addition of all fields is performed before normalisation of any; this can affect +// the result. For example, adding 0y 1m 3d to September 28 gives October 31 (not +// November 1). func (d Date) AddDate(years, months, days int) Date { t := decode(d.day).AddDate(years, months, days) return Date{encode(t)} } +// AddPeriod returns the date corresponding to adding the given period. If the +// period's fields are be negative, this results in an earlier date. +// +// See the description for AddDate. +func (d Date) AddPeriod(period Period) Date { + return d.AddDate(period.Years(), period.Months(), period.Days()) +} + // Sub returns d-u as the number of days between the two dates. func (d Date) Sub(u Date) (days PeriodOfDays) { return d.day - u.day diff --git a/date_test.go b/date_test.go index e5785ee3..7c9ea8ff 100644 --- a/date_test.go +++ b/date_test.go @@ -87,22 +87,20 @@ func TestToday(t *testing.T) { func TestTime(t *testing.T) { cases := []struct { - year int - month time.Month - day int + d Date }{ - {-1234, time.February, 5}, - {0, time.April, 12}, - {1, time.January, 1}, - {1946, time.February, 4}, - {1970, time.January, 1}, - {1976, time.April, 1}, - {1999, time.December, 1}, - {1111111, time.June, 21}, + {New(-1234, time.February, 5)}, + {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)}, } zones := []int{-12, -10, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 8, 12} for _, c := range cases { - d := New(c.year, c.month, c.day) + d := c.d tUTC := d.UTC() if !same(d, tUTC) { t.Errorf("TimeUTC(%v) == %v, want date part %v", d, tUTC, d) @@ -133,23 +131,21 @@ func TestTime(t *testing.T) { func TestPredicates(t *testing.T) { // The list of case dates must be sorted in ascending order cases := []struct { - year int - month time.Month - day int + d Date }{ - {-1234, time.February, 5}, - {0, time.April, 12}, - {1, time.January, 1}, - {1946, time.February, 4}, - {1970, time.January, 1}, - {1976, time.April, 1}, - {1999, time.December, 1}, - {1111111, time.June, 21}, + {New(-1234, time.February, 5)}, + {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)}, } for i, ci := range cases { - di := New(ci.year, ci.month, ci.day) + di := ci.d for j, cj := range cases { - dj := New(cj.year, cj.month, cj.day) + dj := cj.d testPredicate(t, di, dj, di.Equal(dj), i == j, "Equal") testPredicate(t, di, dj, di.Before(dj), i < j, "Before") testPredicate(t, di, dj, di.After(dj), i > j, "After") @@ -177,22 +173,20 @@ func testPredicate(t *testing.T, di, dj Date, p, q bool, m string) { func TestArithmetic(t *testing.T) { cases := []struct { - year int - month time.Month - day int + d Date }{ - {-1234, time.February, 5}, - {0, time.April, 12}, - {1, time.January, 1}, - {1946, time.February, 4}, - {1970, time.January, 1}, - {1976, time.April, 1}, - {1999, time.December, 1}, - {1111111, time.June, 21}, + {New(-1234, time.February, 5)}, + {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)}, } offsets := []PeriodOfDays{-1000000, -9999, -555, -99, -22, -1, 0, 1, 22, 99, 555, 9999, 1000000} for _, c := range cases { - di := New(c.year, c.month, c.day) + di := c.d for _, days := range offsets { dj := di.Add(days) days2 := dj.Sub(di) @@ -217,6 +211,62 @@ func TestArithmetic(t *testing.T) { } } +func TestAddDate(t *testing.T) { + cases := []struct { + d Date + 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)}, + } + 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) + } + } +} + +func xTestAddDuration(t *testing.T) { + cases := []struct { + year int + month time.Month + day int + }{ + {-1234, time.February, 5}, + {0, time.April, 12}, + {1, time.January, 1}, + {1946, time.February, 4}, + {1970, time.January, 1}, + {1976, time.April, 1}, + {1999, time.December, 1}, + {1111111, time.June, 21}, + } + offsets := []PeriodOfDays{-1000000, -9999, -555, -99, -22, -1, 0, 1, 22, 99, 555, 9999, 1000000} + for _, c := range cases { + di := New(c.year, c.month, c.day) + for _, days := range offsets { + dj := di.AddPeriod(NewPeriod(0, 0, int(days))) + days2 := dj.Sub(di) + if days2 != days { + t.Errorf("AddSub(%v,%v) == %v, want %v", di, days, days2, days) + } + dk := dj.AddPeriod(NewPeriod(0, 0, -int(days))) + if dk != di { + t.Errorf("AddNeg(%v,%v) == %v, want %v", di, days, dk, di) + } + } + } +} + func min(a, b PeriodOfDays) PeriodOfDays { if a < b { return a diff --git a/isoperiod.go b/isoperiod.go new file mode 100644 index 00000000..81f6e02b --- /dev/null +++ b/isoperiod.go @@ -0,0 +1,318 @@ +package date + +import ( + "fmt" + "strconv" + "strings" +) + +// Period holds a period of time and provides conversion to/from +// ISO-8601 representations. Because of the vagaries of calendar systems, the meaning of +// year lengths, month lengths and even day lengths depends on context. So a period is +// not necessarily a fixed duration of time in terms of seconds. +// +// See https://en.wikipedia.org/wiki/ISO_8601#Periods +// +// Example representations: "P4D" is four days; "P3Y6M4W1D" is three years, 6 months, +// 4 weeks and one day. +// +// In the ISO representation, decimal fractions are supported, although only the last non-zero +// component is allowed to have a fraction according to the Standard. For example "P2.5Y" +// is 2.5 years. +// +// Internally, fractions are expressed using fixed-point arithmetic to three +// decimal places only. This avoids using float32 in the struct, so there are no problems +// testing equality using ==. +// +// The concept of weeks exists in string representations of periods, but otherwise weeks +// are unimportant. The period contains a number of days from which the number of weeks can +// be calculated when needed. +// Note that although fractional weeks can be parsed, they will never be returned. This is +// because the number of weeks is always computed as an integer from the number of days. +// +// IMPLENTATION NOTE: THE TIME COMPONENT OF ISO-8601 IS NOT YET SUPPORTED. +type Period struct { + years, months, days int32 +} + +// NewPeriod creates a simple period without any fractional parts. All the parameters +// must have the same sign (otherwise a panic occurs). +func NewPeriod(years, months, days int) Period { + if (years >= 0 && months >= 0 && days >= 0) || + (years <= 0 && months <= 0 && days <= 0) { + return Period{int32(years) * 1000, int32(months) * 1000, int32(days) * 1000} + } + panic(fmt.Sprintf("Periods must have homogeneous signs; got P%dY%dM%dD", years, months, days)) +} + +// MustParsePeriod is as per ParsePeriod except that it panics if the string cannot be parsed. +// This is intended for setup code; don't use it for user inputs. +func MustParsePeriod(value string) Period { + d, err := ParsePeriod(value) + if err != nil { + panic(err) + } + return d +} + +// ParsePeriod parses strings that specify periods using ISO-8601 rules. +// +// In addition, a plus or minus sign can precede the period, e.g. "-P10D" +// +// The zero value can be represented in several ways: all of the following +// are equivalent: "P0Y", "P0M", "P0W", "P0D", and "P0". +func ParsePeriod(period string) (Period, error) { + if period == "" { + return Period{}, fmt.Errorf("Cannot parse a blank string as a period.") + } + + if period == "P0" { + return Period{}, nil + } + + dur := period + sign := int32(1) + if dur[0] == '-' { + sign = -1 + dur = dur[1:] + } else if dur[0] == '+' { + dur = dur[1:] + } + + ok := false + result := Period{} + t := strings.IndexByte(dur, 'T') + if t > 0 { + // NOY YET IMPLEMENTED + dur = dur[:t] + } + + if dur[0] != 'P' { + return Period{}, fmt.Errorf("Expected 'P' period mark at the start: %s", period) + } + dur = dur[1:] + + y := strings.IndexByte(dur, 'Y') + if y > 0 { + t, err := parseDecimalFixedPoint(dur[:y], period) + if err != nil { + return Period{}, err + } + dur = dur[y+1:] + result.years = sign * t + ok = true + } + + m := strings.IndexByte(dur, 'M') + if m > 0 { + t, err := parseDecimalFixedPoint(dur[:m], period) + if err != nil { + return Period{}, err + } + dur = dur[m+1:] + result.months = sign * t + ok = true + } + + weeks := int32(0) + w := strings.IndexByte(dur, 'W') + if w > 0 { + var err error + weeks, err = parseDecimalFixedPoint(dur[:w], period) + if err != nil { + return Period{}, err + } + dur = dur[w+1:] + ok = true + } + + days := int32(0) + d := strings.IndexByte(dur, 'D') + if d > 0 { + var err error + days, err = parseDecimalFixedPoint(dur[:d], period) + if err != nil { + return Period{}, err + } + dur = dur[d+1:] + ok = true + } + result.days = sign * (weeks*7 + days) + + if !ok { + return Period{}, fmt.Errorf("Expected 'Y', 'M', 'W' or 'D' marker: %s", period) + } + return result, nil + //P, Y, M, W, D, T, H, M, and S +} + +// Fixed-point three decimal places +func parseDecimalFixedPoint(s, original string) (int32, error) { + //was := s + dec := strings.IndexByte(s, '.') + if dec < 0 { + dec = strings.IndexByte(s, ',') + } + + if dec >= 0 { + dp := len(s) - dec + if dp > 3 { + s = s[:dec] + s[dec+1:dec+4] + } else { + switch dp { + case 3: + s = s[:dec] + s[dec+1:] + "0" + case 2: + s = s[:dec] + s[dec+1:] + "00" + case 1: + s = s[:dec] + s[dec+1:] + "000" + } + } + } else { + s = s + "000" + } + + n, e := strconv.ParseInt(s, 10, 32) + //fmt.Printf("ParseInt(%s) = %d -- from %s in %s %d\n", s, n, was, original, dec) + return int32(n), e +} + +// IsZero returns true if applied to a zero-length period. +func (period Period) IsZero() bool { + return period == Period{} +} + +// IsNegative returns true if any field is negative. By design, this implies that +// all the fields are negative. +func (period Period) IsNegative() bool { + return period.years < 0 || period.months < 0 || period.days < 0 +} + +// IsPrecise returns true for all values with no year or month component. This holds +// true even if the week or days component is large. +// +// For values where this method returns false, the imprecision arises because the +// number of days per month varies in the Gregorian calendar and the number of +// days per year is different for leap years. +//func (d Period) IsPrecise() bool { +// return d.years == 0 && d.months == 0 +//} + +// String converts the period to -8601 form. +func (period Period) String() string { + if period.IsZero() { + return "P0D" + } + + s := "" + if period.years < 0 || period.months < 0 || period.days < 0 { + s = "-" + } + + y, m, w, d := "", "", "", "" + + if period.years != 0 { + y = fmt.Sprintf("%gY", absFloat1000(period.years)) + } + if period.months != 0 { + m = fmt.Sprintf("%gM", absFloat1000(period.months)) + } + if period.days != 0 { + //days := absInt32(period.days) + //weeks := days / 7 + //if (weeks >= 1000) { + // w = fmt.Sprintf("%gW", absFloat(weeks)) + //} + //mdays := days % 7 + if period.days != 0 { + d = fmt.Sprintf("%gD", absFloat1000(period.days)) + } + } + + return fmt.Sprintf("%sP%s%s%s%s", s, y, m, w, d) +} + +func absFloat1000(v int32) float32 { + f := float32(v) / 1000 + if v < 0 { + return -f + } + return f +} + +// Abs converts a negative period to a positive one. +func (period Period) Abs() Period { + return Period{absInt32(period.years), absInt32(period.months), absInt32(period.days)} +} + +func absInt32(v int32) int32 { + if v < 0 { + return -v + } + return v +} + +// Negate changes the sign of the period. +func (period Period) Negate() Period { + return Period{-period.years, -period.months, -period.days} +} + +// Sign returns +1 for positive periods and -1 for negative periods. +func (period Period) Sign() int { + if period.years < 0 { + return -1 + } + return 1 +} + +// Years gets the whole number of years in the period. +func (period Period) Years() int { + return int(period.YearsFloat()) +} + +// YearsFloat gets the number of years in the period, including a fraction if any is present. +func (period Period) YearsFloat() float32 { + return float32(period.years) / 1000 +} + +// Months gets the whole number of months in the period. +func (period Period) Months() int { + return int(period.MonthsFloat()) +} + +// MonthsFloat gets the number of months in the period. +func (period Period) MonthsFloat() float32 { + return float32(period.months) / 1000 +} + +// Days gets the whole number of days in the period. This includes the implied +// number of weeks. +func (period Period) Days() int { + return int(period.DaysFloat()) +} + +// DaysFloat gets the number of days in the period. This includes the implied +// number of weeks. +func (period Period) DaysFloat() float32 { + return float32(period.days) / 1000 +} + +// Weeks calculates the number of whole weeks from the number of days. If the result +// would contain a fraction, it is truncated. +func (period Period) Weeks() int { + return int(period.days) / 7000 +} + +// ModuloDays calculates the whole number of days remaining after the whole number of weeks +// has been excluded. +func (period Period) ModuloDays() int { + days := absInt32(period.days) % 7000 + f := int(days / 1000) + if period.days < 0 { + return -f + } + return f +} + +//TODO marshalling support +//TODO gobencode diff --git a/isoperiod_test.go b/isoperiod_test.go new file mode 100644 index 00000000..2b985a27 --- /dev/null +++ b/isoperiod_test.go @@ -0,0 +1,108 @@ +package date + +import ( + "testing" +) + +func TestParsePeriod(t *testing.T) { + cases := []struct { + value string + period Period + }{ + {"P0", Period{}}, + {"P0D", Period{}}, + {"P3Y", Period{3000, 0, 0}}, + {"P6M", Period{0, 6000, 0}}, + {"P5W", Period{0, 0, 35000}}, + {"P4D", Period{0, 0, 4000}}, + //{"PT12H", Period{}}, + //{"PT30M", Period{}}, + //{"PT5S", Period{}}, + {"P3Y6M5W4DT12H30M5S", Period{3000, 6000, 39000}}, + {"+P3Y6M5W4DT12H30M5S", Period{3000, 6000, 39000}}, + {"-P3Y6M5W4DT12H30M5S", Period{-3000, -6000, -39000}}, + {"P2.Y", Period{2000, 0, 0}}, + {"P2.5Y", Period{2500, 0, 0}}, + {"P2.15Y", Period{2150, 0, 0}}, + {"P2.125Y", Period{2125, 0, 0}}, + } + for _, c := range cases { + d := MustParsePeriod(c.value) + if d != c.period { + t.Errorf("MustParsePeriod(%v) == %#v, want (%#v)", c.value, d, c.period) + } + } + + badCases := []string{ + "13M", + "P", + } + for _, c := range badCases { + d, err := ParsePeriod(c) + if err == nil { + t.Errorf("ParsePeriod(%v) == %v", c, d) + } + } +} + +func TestPeriodString(t *testing.T) { + cases := []struct { + value string + period Period + }{ + {"P0D", Period{}}, + {"P3Y", Period{3000, 0, 0}}, + {"-P3Y", Period{-3000, 0, 0}}, + {"P6M", Period{0, 6000, 0}}, + {"-P6M", Period{0, -6000, 0}}, + {"P35D", Period{0, 0, 35000}}, + {"-P35D", Period{0, 0, -35000}}, + {"P4D", Period{0, 0, 4000}}, + {"-P4D", Period{0, 0, -4000}}, + //{"PT12H", Period{}}, + //{"PT30M", Period{}}, + //{"PT5S", Period{}}, + {"P3Y6M39D", Period{3000, 6000, 39000}}, + {"-P3Y6M39D", Period{-3000, -6000, -39000}}, + {"P2.5Y", Period{2500, 0, 0}}, + {"P2.15Y", Period{2150, 0, 0}}, + {"P2.125Y", Period{2125, 0, 0}}, + } + for _, c := range cases { + s := c.period.String() + if s != c.value { + t.Errorf("String() == %s, want %s for %+v", s, c.value, c.period) + } + } +} + +func TestNewPeriod(t *testing.T) { + cases := []struct { + years, months, days int + period Period + }{ + {0, 0, 0, Period{0, 0, 0}}, + {0, 0, 1, Period{0, 0, 1000}}, + {0, 1, 0, Period{0, 1000, 0}}, + {1, 0, 0, Period{1000, 0, 0}}, + {100, 222, 700, Period{100000, 222000, 700000}}, + {0, 0, -1, Period{0, 0, -1000}}, + {0, -1, 0, Period{0, -1000, 0}}, + {-1, 0, 0, Period{-1000, 0, 0}}, + } + for _, c := range cases { + p := NewPeriod(c.years, c.months, c.days) + if p != c.period { + t.Errorf("%d,%d,%d gives %#v, want %#v", c.years, c.months, c.days, p, c.period) + } + if p.Years() != c.years { + t.Errorf("%#v, got %d want %d", p, p.Years(), c.years) + } + if p.Months() != c.months { + t.Errorf("%#v, got %d want %d", p, p.Months(), c.months) + } + if p.Days() != c.days { + t.Errorf("%#v, got %d want %d", p, p.Days(), c.days) + } + } +} diff --git a/marshal.go b/marshal.go index 913f7b5d..031cbb1c 100644 --- a/marshal.go +++ b/marshal.go @@ -89,3 +89,17 @@ func (d *Date) UnmarshalText(data []byte) (err error) { } return err } + +// MarshalText implements the encoding.TextMarshaler interface for Periods. +func (period Period) MarshalText() ([]byte, error) { + return []byte(period.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for Periods. +func (period *Period) UnmarshalText(data []byte) (err error) { + u, err := ParsePeriod(string(data)) + if err == nil { + *period = u + } + return err +} diff --git a/marshal_test.go b/marshal_test.go index 4e804821..a39d0b5b 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -112,7 +112,7 @@ func TestInvalidJSON(t *testing.T) { } } -func TestTextMarshalling(t *testing.T) { +func TestDateTextMarshalling(t *testing.T) { cases := []struct { value Date want string @@ -143,7 +143,7 @@ func TestTextMarshalling(t *testing.T) { } } -func TestInvalidText(t *testing.T) { +func TestInvalidDateText(t *testing.T) { cases := []struct { value string want string @@ -159,3 +159,51 @@ func TestInvalidText(t *testing.T) { } } } + +func TestPeriodTextMarshalling(t *testing.T) { + cases := []struct { + value Period + want string + }{ + {NewPeriod(-11111, -123, -3), "-P11111Y123M3D"}, + {NewPeriod(-1, -12, -31), "-P1Y12M31D"}, + {NewPeriod(0, 0, 0), "P0D"}, + {NewPeriod(0, 0, 1), "P1D"}, + {NewPeriod(0, 1, 0), "P1M"}, + {NewPeriod(1, 0, 0), "P1Y"}, + } + for _, c := range cases { + var p Period + bytes, err := c.value.MarshalText() + if err != nil { + t.Errorf("Text(%v) marshal error %v", c, err) + } else if string(bytes) != c.want { + t.Errorf("Text(%v) == %v, want %v", c.value, string(bytes), c.want) + } else { + err = p.UnmarshalText(bytes) + if err != nil { + t.Errorf("Text(%v) unmarshal error %v", c.value, err) + } else if p != c.value { + t.Errorf("Text(%v) unmarshal got %v", c.value, p) + } + } + } +} + +func TestInvalidPeriodText(t *testing.T) { + cases := []struct { + value string + want string + }{ + {``, `Cannot parse a blank string as a period.`}, + {`not-a-period`, `Expected 'P' period mark at the start: not-a-period`}, + {`P000`, `Expected 'Y', 'M', 'W' or 'D' marker: P000`}, + } + for _, c := range cases { + var p Period + err := p.UnmarshalText([]byte(c.value)) + if err == nil || err.Error() != c.want { + t.Errorf("InvalidText(%v) == %v, want %v", c.value, err, c.want) + } + } +} diff --git a/timespan/daterange.go b/timespan/daterange.go index 047d1d7c..3eb50f47 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -67,8 +67,11 @@ func OneDayRange(day Date) DateRange { return DateRange{day, 1} } -// Days returns the period represented by this range. +// 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 } @@ -144,21 +147,42 @@ func (dateRange DateRange) ExtendBy(days PeriodOfDays) DateRange { if days == 0 { return dateRange } - return DateRange{dateRange.mark, dateRange.days + days} + return DateRange{dateRange.mark, dateRange.days + days}.Normalise() +} + +// ShiftByPeriod moves the date range by moving both the start and end dates similarly. +// A negative parameter is allowed. +func (dateRange DateRange) ShiftByPeriod(period Period) DateRange { + if period.IsZero() { + return dateRange + } + newMark := dateRange.mark.AddPeriod(period) + //fmt.Printf("mark + %v : %v -> %v", period, dateRange.mark, newMark) + return DateRange{newMark, 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). +func (dateRange DateRange) ExtendByPeriod(period Period) DateRange { + if period.IsZero() { + return dateRange + } + newEnd := dateRange.End().AddPeriod(period) + //fmt.Printf("%v, end + %v : %v -> %v", dateRange.mark, period, dateRange.End(), newEnd) + return NewDateRange(dateRange.Start(), newEnd) } // String describes the date range in human-readable form. func (dateRange DateRange) String() string { - switch dateRange.days { + norm := dateRange.Normalise() + switch norm.days { case 0: - return fmt.Sprintf("0 days from %s", dateRange.mark) - case 1, -1: - return fmt.Sprintf("1 day on %s", dateRange.mark) + return fmt.Sprintf("0 days at %s", norm.mark) + case 1: + return fmt.Sprintf("1 day on %s", norm.mark) default: - if dateRange.days < 0 { - return fmt.Sprintf("%d days from %s to %s", -dateRange.days, dateRange.Start(), dateRange.Last()) - } - return fmt.Sprintf("%d days from %s to %s", dateRange.days, dateRange.Start(), dateRange.Last()) + return fmt.Sprintf("%d days from %s to %s", norm.days, norm.Start(), norm.Last()) } } diff --git a/timespan/daterange_test.go b/timespan/daterange_test.go index 46a49372..4aa280d7 100644 --- a/timespan/daterange_test.go +++ b/timespan/daterange_test.go @@ -65,18 +65,18 @@ func TestNewDateRangeWithNormalise(t *testing.T) { func TestEmptyRange(t *testing.T) { drN0 := DateRange{d0327, -1} - isEq(t, drN0.Days(), PeriodOfDays(-1)) + isEq(t, drN0.Days(), PeriodOfDays(1)) isEq(t, drN0.IsZero(), false) isEq(t, drN0.IsEmpty(), false) isEq(t, drN0.Start(), d0327) isEq(t, drN0.Last(), d0327) - isEq(t, drN0.String(), "1 day on 2015-03-27") + isEq(t, drN0.String(), "1 day on 2015-03-26") dr0 := DateRange{} isEq(t, dr0.Days(), PeriodOfDays(0)) isEq(t, dr0.IsZero(), true) isEq(t, dr0.IsEmpty(), true) - isEq(t, dr0.String(), "0 days from 1970-01-01") + isEq(t, dr0.String(), "0 days at 1970-01-01") dr1 := EmptyRange(Date{}) isEq(t, dr1.IsZero(), true) @@ -90,7 +90,7 @@ func TestEmptyRange(t *testing.T) { isEq(t, dr2.Last().IsZero(), true) isEq(t, dr2.End(), d0327) isEq(t, dr2.Days(), PeriodOfDays(0)) - isEq(t, dr2.String(), "0 days from 2015-03-27") + isEq(t, dr2.String(), "0 days at 2015-03-27") } func TestOneDayRange(t *testing.T) { @@ -147,11 +147,43 @@ func TestExtendByPos(t *testing.T) { } func TestExtendByNeg(t *testing.T) { - dr := OneDayRange(d0327).ExtendBy(-9) - isEq(t, dr.Days(), PeriodOfDays(-8)) + dr := OneDayRange(d0327).ExtendBy(-8) + isEq(t, dr.Days(), PeriodOfDays(7)) + isEq(t, dr.Start(), d0320) + isEq(t, dr.Last(), d0326) + isEq(t, dr.String(), "7 days from 2015-03-20 to 2015-03-26") +} + +func TestShiftByPosPeriod(t *testing.T) { + dr := NewDateRange(d0327, d0402).ShiftByPeriod(NewPeriod(0, 0, 7)) + isEq(t, dr.Days(), PeriodOfDays(6)) + isEq(t, dr.Start(), d0403) + isEq(t, dr.Last(), d0408) +} + +func TestShiftByNegPeriod(t *testing.T) { + dr := NewDateRange(d0403, d0408).ShiftByPeriod(NewPeriod(0, 0, -7)) + isEq(t, dr.Days(), PeriodOfDays(5)) + isEq(t, dr.Start(), d0327) + isEq(t, dr.Last(), d0331) +} + +func TestExtendByPosPeriod(t *testing.T) { + dr := OneDayRange(d0327).ExtendByPeriod(NewPeriod(0, 0, 6)) + isEq(t, dr.Days(), PeriodOfDays(7)) + isEq(t, dr.Start(), d0327) + isEq(t, dr.Last(), d0402) + isEq(t, dr.End(), d0403) + isEq(t, dr.String(), "7 days from 2015-03-27 to 2015-04-02") +} + +func TestExtendByNegPeriod(t *testing.T) { + dr := OneDayRange(d0327).ExtendByPeriod(NewPeriod(0, 0, -8)) + fmt.Printf("\ndr=%#v\n", dr) + isEq(t, dr.Days(), PeriodOfDays(7)) isEq(t, dr.Start(), d0320) - isEq(t, dr.Last(), d0327) - isEq(t, dr.String(), "8 days from 2015-03-20 to 2015-03-27") + isEq(t, dr.Last(), d0326) + isEq(t, dr.String(), "7 days from 2015-03-20 to 2015-03-26") } func TestContains1(t *testing.T) { From 55b61340ae19193ded4da707e81e291776c15178 Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 2 Feb 2016 21:58:04 +0000 Subject: [PATCH 050/165] Added support for JSON marshalling of Period --- marshal.go | 14 +++++++++++++ marshal_test.go | 52 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/marshal.go b/marshal.go index 031cbb1c..17f305cb 100644 --- a/marshal.go +++ b/marshal.go @@ -67,6 +67,20 @@ func (d *Date) UnmarshalJSON(data []byte) error { return d.UnmarshalText(data[1 : n-1]) } +// MarshalJSON implements the json.Marshaler interface for Period. +func (period Period) MarshalJSON() ([]byte, error) { + return []byte(`"` + period.String() + `"`), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface for Period. +func (period *Period) UnmarshalJSON(data []byte) error { + n := len(data) + if n < 2 || data[0] != '"' || data[n-1] != '"' { + return fmt.Errorf("Period.UnmarshalJSON: missing double quotes (%s)", string(data)) + } + return period.UnmarshalText(data[1 : n-1]) +} + // MarshalText implements the encoding.TextMarshaler interface. // The date is given in ISO 8601 extended format (e.g. "2006-01-02"). // If the year of the date falls outside the [0,9999] range, this format diff --git a/marshal_test.go b/marshal_test.go index a39d0b5b..554988a7 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -62,7 +62,7 @@ func TestInvalidGob(t *testing.T) { } } -func TestJSONMarshalling(t *testing.T) { +func TestDateJSONMarshalling(t *testing.T) { cases := []struct { value Date want string @@ -93,7 +93,7 @@ func TestJSONMarshalling(t *testing.T) { } } -func TestInvalidJSON(t *testing.T) { +func TestInvalidDateJSON(t *testing.T) { cases := []struct { value string want string @@ -112,6 +112,54 @@ func TestInvalidJSON(t *testing.T) { } } +func TestPeriodJSONMarshalling(t *testing.T) { + cases := []struct { + value Period + want string + }{ + {NewPeriod(-11111, -123, -3), `"-P11111Y123M3D"`}, + {NewPeriod(-1, -12, -31), `"-P1Y12M31D"`}, + {NewPeriod(0, 0, 0), `"P0D"`}, + {NewPeriod(0, 0, 1), `"P1D"`}, + {NewPeriod(0, 1, 0), `"P1M"`}, + {NewPeriod(1, 0, 0), `"P1Y"`}, + } + for _, c := range cases { + var p Period + bytes, err := json.Marshal(c.value) + if err != nil { + t.Errorf("JSON(%v) marshal error %v", c, err) + } else if string(bytes) != c.want { + t.Errorf("JSON(%v) == %v, want %v", c.value, string(bytes), c.want) + } else { + err = json.Unmarshal(bytes, &p) + if err != nil { + t.Errorf("JSON(%v) unmarshal error %v", c.value, err) + } else if p != c.value { + t.Errorf("JSON(%v) unmarshal got %v", c.value, p) + } + } + } +} + +func TestInvalidPeriodJSON(t *testing.T) { + cases := []struct { + value string + want string + }{ + {`""`, `Cannot parse a blank string as a period.`}, + {`"not-a-period"`, `Expected 'P' period mark at the start: not-a-period`}, + {`"P000"`, `Expected 'Y', 'M', 'W' or 'D' marker: P000`}, + } + for _, c := range cases { + var p Period + err := p.UnmarshalJSON([]byte(c.value)) + if err == nil || err.Error() != c.want { + t.Errorf("InvalidJSON(%v) == %v, want %v", c.value, err, c.want) + } + } +} + func TestDateTextMarshalling(t *testing.T) { cases := []struct { value Date From 1176065c95087ffdf15081857c8dc6be7f972309 Mon Sep 17 00:00:00 2001 From: Rick Date: Sat, 6 Feb 2016 09:01:26 +0000 Subject: [PATCH 051/165] Moved Period to sub-package to reduce API clutter. Added string formatting to Period. --- date.go | 3 +- date_test.go | 5 +- marshal.go | 28 ----- marshal_test.go | 96 ---------------- isoperiod.go => period/isoperiod.go | 59 +++++++++- isoperiod_test.go => period/isoperiod_test.go | 71 +++++++++++- period/marshal.go | 72 ++++++++++++ period/marshal_test.go | 106 ++++++++++++++++++ timespan/daterange.go | 5 +- timespan/daterange_test.go | 11 +- 10 files changed, 318 insertions(+), 138 deletions(-) rename isoperiod.go => period/isoperiod.go (78%) rename isoperiod_test.go => period/isoperiod_test.go (60%) create mode 100644 period/marshal.go create mode 100644 period/marshal_test.go diff --git a/date.go b/date.go index 25bd47c8..9ed85a48 100644 --- a/date.go +++ b/date.go @@ -6,6 +6,7 @@ package date import ( "fmt" + "github.com/rickb777/date/period" "math" "time" ) @@ -235,7 +236,7 @@ func (d Date) AddDate(years, months, days int) Date { // period's fields are be negative, this results in an earlier date. // // See the description for AddDate. -func (d Date) AddPeriod(period Period) Date { +func (d Date) AddPeriod(period period.Period) Date { return d.AddDate(period.Years(), period.Months(), period.Days()) } diff --git a/date_test.go b/date_test.go index 7c9ea8ff..2034b873 100644 --- a/date_test.go +++ b/date_test.go @@ -5,6 +5,7 @@ package date import ( + "github.com/rickb777/date/period" "runtime/debug" "testing" "time" @@ -254,12 +255,12 @@ func xTestAddDuration(t *testing.T) { for _, c := range cases { di := New(c.year, c.month, c.day) for _, days := range offsets { - dj := di.AddPeriod(NewPeriod(0, 0, int(days))) + dj := di.AddPeriod(period.NewPeriod(0, 0, int(days))) days2 := dj.Sub(di) if days2 != days { t.Errorf("AddSub(%v,%v) == %v, want %v", di, days, days2, days) } - dk := dj.AddPeriod(NewPeriod(0, 0, -int(days))) + dk := dj.AddPeriod(period.NewPeriod(0, 0, -int(days))) if dk != di { t.Errorf("AddNeg(%v,%v) == %v, want %v", di, days, dk, di) } diff --git a/marshal.go b/marshal.go index 17f305cb..913f7b5d 100644 --- a/marshal.go +++ b/marshal.go @@ -67,20 +67,6 @@ func (d *Date) UnmarshalJSON(data []byte) error { return d.UnmarshalText(data[1 : n-1]) } -// MarshalJSON implements the json.Marshaler interface for Period. -func (period Period) MarshalJSON() ([]byte, error) { - return []byte(`"` + period.String() + `"`), nil -} - -// UnmarshalJSON implements the json.Unmarshaler interface for Period. -func (period *Period) UnmarshalJSON(data []byte) error { - n := len(data) - if n < 2 || data[0] != '"' || data[n-1] != '"' { - return fmt.Errorf("Period.UnmarshalJSON: missing double quotes (%s)", string(data)) - } - return period.UnmarshalText(data[1 : n-1]) -} - // MarshalText implements the encoding.TextMarshaler interface. // The date is given in ISO 8601 extended format (e.g. "2006-01-02"). // If the year of the date falls outside the [0,9999] range, this format @@ -103,17 +89,3 @@ func (d *Date) UnmarshalText(data []byte) (err error) { } return err } - -// MarshalText implements the encoding.TextMarshaler interface for Periods. -func (period Period) MarshalText() ([]byte, error) { - return []byte(period.String()), nil -} - -// UnmarshalText implements the encoding.TextUnmarshaler interface for Periods. -func (period *Period) UnmarshalText(data []byte) (err error) { - u, err := ParsePeriod(string(data)) - if err == nil { - *period = u - } - return err -} diff --git a/marshal_test.go b/marshal_test.go index 554988a7..d8176a62 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -112,54 +112,6 @@ func TestInvalidDateJSON(t *testing.T) { } } -func TestPeriodJSONMarshalling(t *testing.T) { - cases := []struct { - value Period - want string - }{ - {NewPeriod(-11111, -123, -3), `"-P11111Y123M3D"`}, - {NewPeriod(-1, -12, -31), `"-P1Y12M31D"`}, - {NewPeriod(0, 0, 0), `"P0D"`}, - {NewPeriod(0, 0, 1), `"P1D"`}, - {NewPeriod(0, 1, 0), `"P1M"`}, - {NewPeriod(1, 0, 0), `"P1Y"`}, - } - for _, c := range cases { - var p Period - bytes, err := json.Marshal(c.value) - if err != nil { - t.Errorf("JSON(%v) marshal error %v", c, err) - } else if string(bytes) != c.want { - t.Errorf("JSON(%v) == %v, want %v", c.value, string(bytes), c.want) - } else { - err = json.Unmarshal(bytes, &p) - if err != nil { - t.Errorf("JSON(%v) unmarshal error %v", c.value, err) - } else if p != c.value { - t.Errorf("JSON(%v) unmarshal got %v", c.value, p) - } - } - } -} - -func TestInvalidPeriodJSON(t *testing.T) { - cases := []struct { - value string - want string - }{ - {`""`, `Cannot parse a blank string as a period.`}, - {`"not-a-period"`, `Expected 'P' period mark at the start: not-a-period`}, - {`"P000"`, `Expected 'Y', 'M', 'W' or 'D' marker: P000`}, - } - for _, c := range cases { - var p Period - err := p.UnmarshalJSON([]byte(c.value)) - if err == nil || err.Error() != c.want { - t.Errorf("InvalidJSON(%v) == %v, want %v", c.value, err, c.want) - } - } -} - func TestDateTextMarshalling(t *testing.T) { cases := []struct { value Date @@ -207,51 +159,3 @@ func TestInvalidDateText(t *testing.T) { } } } - -func TestPeriodTextMarshalling(t *testing.T) { - cases := []struct { - value Period - want string - }{ - {NewPeriod(-11111, -123, -3), "-P11111Y123M3D"}, - {NewPeriod(-1, -12, -31), "-P1Y12M31D"}, - {NewPeriod(0, 0, 0), "P0D"}, - {NewPeriod(0, 0, 1), "P1D"}, - {NewPeriod(0, 1, 0), "P1M"}, - {NewPeriod(1, 0, 0), "P1Y"}, - } - for _, c := range cases { - var p Period - bytes, err := c.value.MarshalText() - if err != nil { - t.Errorf("Text(%v) marshal error %v", c, err) - } else if string(bytes) != c.want { - t.Errorf("Text(%v) == %v, want %v", c.value, string(bytes), c.want) - } else { - err = p.UnmarshalText(bytes) - if err != nil { - t.Errorf("Text(%v) unmarshal error %v", c.value, err) - } else if p != c.value { - t.Errorf("Text(%v) unmarshal got %v", c.value, p) - } - } - } -} - -func TestInvalidPeriodText(t *testing.T) { - cases := []struct { - value string - want string - }{ - {``, `Cannot parse a blank string as a period.`}, - {`not-a-period`, `Expected 'P' period mark at the start: not-a-period`}, - {`P000`, `Expected 'Y', 'M', 'W' or 'D' marker: P000`}, - } - for _, c := range cases { - var p Period - err := p.UnmarshalText([]byte(c.value)) - if err == nil || err.Error() != c.want { - t.Errorf("InvalidText(%v) == %v, want %v", c.value, err, c.want) - } - } -} diff --git a/isoperiod.go b/period/isoperiod.go similarity index 78% rename from isoperiod.go rename to period/isoperiod.go index 81f6e02b..1a400479 100644 --- a/isoperiod.go +++ b/period/isoperiod.go @@ -1,7 +1,8 @@ -package date +package period import ( "fmt" + . "github.com/rickb777/plural" "strconv" "strings" ) @@ -198,6 +199,59 @@ func (period Period) IsNegative() bool { // return d.years == 0 && d.months == 0 //} +// Format converts the period to human-readable form using the default localisation. +func (period Period) Format() string { + return period.FormatWithPeriodNames(PeriodYearNames, PeriodMonthNames, PeriodWeekNames, PeriodDayNames) +} + +// FormatWithPeriodNames converts the period to human-readable form in a localisable way. +func (period Period) FormatWithPeriodNames(yearNames Plurals, monthNames Plurals, weekNames Plurals, dayNames Plurals) string { + period = period.Abs() + + parts := make([]string, 0) + parts = appendNonBlank(parts, yearNames.FormatFloat(absFloat1000(period.years))) + parts = appendNonBlank(parts, monthNames.FormatFloat(absFloat1000(period.months))) + + if (period.years == 0 && period.months == 0) || period.days > 0 { + if len(weekNames) > 0 { + weeks := period.days / 7000 + mdays := period.days % 7000 + //fmt.Printf("%v %#v - %d %d\n", period, period, weeks, mdays) + if weeks > 0 { + parts = appendNonBlank(parts, weekNames.FormatInt(int(weeks))) + } + if mdays > 0 || weeks == 0 { + parts = appendNonBlank(parts, dayNames.FormatFloat(absFloat1000(mdays))) + } + } else { + parts = appendNonBlank(parts, dayNames.FormatFloat(absFloat1000(period.days))) + } + } + + return strings.Join(parts, ", ") +} + +func appendNonBlank(parts []string, s string) []string { + if s == "" { + return parts + } + return append(parts, s) +} + +// PeriodDayNames provides the English default format names for the days part of the period. +// This is a sequence of plurals where the first match is used, otherwise the last one is used. +// The last one must include a "%g" placeholder for the number. +var PeriodDayNames = Plurals{Case{0, "%v days"}, Case{1, "%v day"}, Case{2, "%v days"}} + +// PeriodWeekNames is as for PeriodDayNames but for weeks. +var PeriodWeekNames = Plurals{Case{0, ""}, Case{1, "%v week"}, Case{2, "%v weeks"}} + +// PeriodMonthNames is as for PeriodDayNames but for months. +var PeriodMonthNames = Plurals{Case{0, ""}, Case{1, "%g month"}, Case{2, "%g months"}} + +// PeriodYearNames is as for PeriodDayNames but for years. +var PeriodYearNames = Plurals{Case{0, ""}, Case{1, "%g year"}, Case{2, "%g years"}} + // String converts the period to -8601 form. func (period Period) String() string { if period.IsZero() { @@ -286,7 +340,7 @@ func (period Period) MonthsFloat() float32 { } // Days gets the whole number of days in the period. This includes the implied -// number of weeks. +// number of weeks but excludes the specified years and months. func (period Period) Days() int { return int(period.DaysFloat()) } @@ -314,5 +368,4 @@ func (period Period) ModuloDays() int { return f } -//TODO marshalling support //TODO gobencode diff --git a/isoperiod_test.go b/period/isoperiod_test.go similarity index 60% rename from isoperiod_test.go rename to period/isoperiod_test.go index 2b985a27..c354b8a0 100644 --- a/isoperiod_test.go +++ b/period/isoperiod_test.go @@ -1,6 +1,7 @@ -package date +package period import ( + "github.com/rickb777/plural" "testing" ) @@ -106,3 +107,71 @@ func TestNewPeriod(t *testing.T) { } } } + +func TestPeriodFormat(t *testing.T) { + cases := []struct { + period string + expect string + }{ + {"P0D", "0 days"}, + {"P1Y", "1 year"}, + {"P3Y", "3 years"}, + {"-P3Y", "3 years"}, + {"P1M", "1 month"}, + {"P6M", "6 months"}, + {"-P6M", "6 months"}, + {"P7D", "1 week"}, + {"P35D", "5 weeks"}, + {"-P35D", "5 weeks"}, + {"P1D", "1 day"}, + {"P4D", "4 days"}, + {"-P4D", "4 days"}, + {"P1Y1M8D", "1 year, 1 month, 1 week, 1 day"}, + {"P3Y6M39D", "3 years, 6 months, 5 weeks, 4 days"}, + {"-P3Y6M39D", "3 years, 6 months, 5 weeks, 4 days"}, + {"P1.1Y", "1.1 years"}, + {"P2.5Y", "2.5 years"}, + {"P2.15Y", "2.15 years"}, + {"P2.125Y", "2.125 years"}, + } + for _, c := range cases { + s := MustParsePeriod(c.period).Format() + if s != c.expect { + t.Errorf("Format() == %s, want %s for %+v", s, c.expect, c.period) + } + } +} + +func TestPeriodFormatWithoutWeeks(t *testing.T) { + cases := []struct { + period string + expect string + }{ + {"P0D", "0 days"}, + {"P1Y", "1 year"}, + {"P3Y", "3 years"}, + {"-P3Y", "3 years"}, + {"P1M", "1 month"}, + {"P6M", "6 months"}, + {"-P6M", "6 months"}, + {"P7D", "7 days"}, + {"P35D", "35 days"}, + {"-P35D", "35 days"}, + {"P1D", "1 day"}, + {"P4D", "4 days"}, + {"-P4D", "4 days"}, + {"P1Y1M1D", "1 year, 1 month, 1 day"}, + {"P3Y6M39D", "3 years, 6 months, 39 days"}, + {"-P3Y6M39D", "3 years, 6 months, 39 days"}, + {"P1.1Y", "1.1 years"}, + {"P2.5Y", "2.5 years"}, + {"P2.15Y", "2.15 years"}, + {"P2.125Y", "2.125 years"}, + } + for _, c := range cases { + s := MustParsePeriod(c.period).FormatWithPeriodNames(PeriodYearNames, PeriodMonthNames, plural.Plurals{}, PeriodDayNames) + if s != c.expect { + t.Errorf("Format() == %s, want %s for %+v", s, c.expect, c.period) + } + } +} diff --git a/period/marshal.go b/period/marshal.go new file mode 100644 index 00000000..079cecfd --- /dev/null +++ b/period/marshal.go @@ -0,0 +1,72 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package period + +import ( + "fmt" +) + +//// MarshalBinary implements the encoding.BinaryMarshaler interface. +//func (d Date) MarshalBinary() ([]byte, error) { +// enc := []byte{ +// byte(d.day >> 24), +// byte(d.day >> 16), +// byte(d.day >> 8), +// byte(d.day), +// } +// return enc, nil +//} +// +//// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +//func (d *Date) UnmarshalBinary(data []byte) error { +// if len(data) == 0 { +// return errors.New("Date.UnmarshalBinary: no data") +// } +// if len(data) != 4 { +// return errors.New("Date.UnmarshalBinary: invalid length") +// } +// +// d.day = PeriodOfDays(data[3]) | PeriodOfDays(data[2])<<8 | PeriodOfDays(data[1])<<16 | PeriodOfDays(data[0])<<24 +// +// return nil +//} +// +//// GobEncode implements the gob.GobEncoder interface. +//func (d Date) GobEncode() ([]byte, error) { +// return d.MarshalBinary() +//} +// +//// GobDecode implements the gob.GobDecoder interface. +//func (d *Date) GobDecode(data []byte) error { +// return d.UnmarshalBinary(data) +//} + +// MarshalJSON implements the json.Marshaler interface for Period. +func (period Period) MarshalJSON() ([]byte, error) { + return []byte(`"` + period.String() + `"`), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface for Period. +func (period *Period) UnmarshalJSON(data []byte) error { + n := len(data) + if n < 2 || data[0] != '"' || data[n-1] != '"' { + return fmt.Errorf("Period.UnmarshalJSON: missing double quotes (%s)", string(data)) + } + return period.UnmarshalText(data[1 : n-1]) +} + +// MarshalText implements the encoding.TextMarshaler interface for Periods. +func (period Period) MarshalText() ([]byte, error) { + return []byte(period.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for Periods. +func (period *Period) UnmarshalText(data []byte) (err error) { + u, err := ParsePeriod(string(data)) + if err == nil { + *period = u + } + return err +} diff --git a/period/marshal_test.go b/period/marshal_test.go new file mode 100644 index 00000000..5fc961db --- /dev/null +++ b/period/marshal_test.go @@ -0,0 +1,106 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package period + +import ( + "encoding/json" + "testing" +) + +func TestPeriodJSONMarshalling(t *testing.T) { + cases := []struct { + value Period + want string + }{ + {NewPeriod(-11111, -123, -3), `"-P11111Y123M3D"`}, + {NewPeriod(-1, -12, -31), `"-P1Y12M31D"`}, + {NewPeriod(0, 0, 0), `"P0D"`}, + {NewPeriod(0, 0, 1), `"P1D"`}, + {NewPeriod(0, 1, 0), `"P1M"`}, + {NewPeriod(1, 0, 0), `"P1Y"`}, + } + for _, c := range cases { + var p Period + bytes, err := json.Marshal(c.value) + if err != nil { + t.Errorf("JSON(%v) marshal error %v", c, err) + } else if string(bytes) != c.want { + t.Errorf("JSON(%v) == %v, want %v", c.value, string(bytes), c.want) + } else { + err = json.Unmarshal(bytes, &p) + if err != nil { + t.Errorf("JSON(%v) unmarshal error %v", c.value, err) + } else if p != c.value { + t.Errorf("JSON(%v) unmarshal got %v", c.value, p) + } + } + } +} + +func TestInvalidPeriodJSON(t *testing.T) { + cases := []struct { + value string + want string + }{ + {`""`, `Cannot parse a blank string as a period.`}, + {`"not-a-period"`, `Expected 'P' period mark at the start: not-a-period`}, + {`"P000"`, `Expected 'Y', 'M', 'W' or 'D' marker: P000`}, + } + for _, c := range cases { + var p Period + err := p.UnmarshalJSON([]byte(c.value)) + if err == nil || err.Error() != c.want { + t.Errorf("InvalidJSON(%v) == %v, want %v", c.value, err, c.want) + } + } +} + +func TestPeriodTextMarshalling(t *testing.T) { + cases := []struct { + value Period + want string + }{ + {NewPeriod(-11111, -123, -3), "-P11111Y123M3D"}, + {NewPeriod(-1, -12, -31), "-P1Y12M31D"}, + {NewPeriod(0, 0, 0), "P0D"}, + {NewPeriod(0, 0, 1), "P1D"}, + {NewPeriod(0, 1, 0), "P1M"}, + {NewPeriod(1, 0, 0), "P1Y"}, + } + for _, c := range cases { + var p Period + bytes, err := c.value.MarshalText() + if err != nil { + t.Errorf("Text(%v) marshal error %v", c, err) + } else if string(bytes) != c.want { + t.Errorf("Text(%v) == %v, want %v", c.value, string(bytes), c.want) + } else { + err = p.UnmarshalText(bytes) + if err != nil { + t.Errorf("Text(%v) unmarshal error %v", c.value, err) + } else if p != c.value { + t.Errorf("Text(%v) unmarshal got %v", c.value, p) + } + } + } +} + +func TestInvalidPeriodText(t *testing.T) { + cases := []struct { + value string + want string + }{ + {``, `Cannot parse a blank string as a period.`}, + {`not-a-period`, `Expected 'P' period mark at the start: not-a-period`}, + {`P000`, `Expected 'Y', 'M', 'W' or 'D' marker: P000`}, + } + for _, c := range cases { + var p Period + err := p.UnmarshalText([]byte(c.value)) + if err == nil || err.Error() != c.want { + t.Errorf("InvalidText(%v) == %v, want %v", c.value, err, c.want) + } + } +} diff --git a/timespan/daterange.go b/timespan/daterange.go index 3eb50f47..984970db 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -7,6 +7,7 @@ package timespan import ( "fmt" . "github.com/rickb777/date" + "github.com/rickb777/date/period" "time" ) @@ -152,7 +153,7 @@ func (dateRange DateRange) ExtendBy(days PeriodOfDays) DateRange { // ShiftByPeriod moves the date range by moving both the start and end dates similarly. // A negative parameter is allowed. -func (dateRange DateRange) ShiftByPeriod(period Period) DateRange { +func (dateRange DateRange) ShiftByPeriod(period period.Period) DateRange { if period.IsZero() { return dateRange } @@ -164,7 +165,7 @@ func (dateRange DateRange) ShiftByPeriod(period Period) DateRange { // 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). -func (dateRange DateRange) ExtendByPeriod(period Period) DateRange { +func (dateRange DateRange) ExtendByPeriod(period period.Period) DateRange { if period.IsZero() { return dateRange } diff --git a/timespan/daterange_test.go b/timespan/daterange_test.go index 4aa280d7..ff43e125 100644 --- a/timespan/daterange_test.go +++ b/timespan/daterange_test.go @@ -7,6 +7,7 @@ package timespan import ( "fmt" . "github.com/rickb777/date" + "github.com/rickb777/date/period" "runtime/debug" "strings" "testing" @@ -155,21 +156,21 @@ func TestExtendByNeg(t *testing.T) { } func TestShiftByPosPeriod(t *testing.T) { - dr := NewDateRange(d0327, d0402).ShiftByPeriod(NewPeriod(0, 0, 7)) + dr := NewDateRange(d0327, d0402).ShiftByPeriod(period.NewPeriod(0, 0, 7)) isEq(t, dr.Days(), PeriodOfDays(6)) isEq(t, dr.Start(), d0403) isEq(t, dr.Last(), d0408) } func TestShiftByNegPeriod(t *testing.T) { - dr := NewDateRange(d0403, d0408).ShiftByPeriod(NewPeriod(0, 0, -7)) + dr := NewDateRange(d0403, d0408).ShiftByPeriod(period.NewPeriod(0, 0, -7)) isEq(t, dr.Days(), PeriodOfDays(5)) isEq(t, dr.Start(), d0327) isEq(t, dr.Last(), d0331) } func TestExtendByPosPeriod(t *testing.T) { - dr := OneDayRange(d0327).ExtendByPeriod(NewPeriod(0, 0, 6)) + dr := OneDayRange(d0327).ExtendByPeriod(period.NewPeriod(0, 0, 6)) isEq(t, dr.Days(), PeriodOfDays(7)) isEq(t, dr.Start(), d0327) isEq(t, dr.Last(), d0402) @@ -178,8 +179,8 @@ func TestExtendByPosPeriod(t *testing.T) { } func TestExtendByNegPeriod(t *testing.T) { - dr := OneDayRange(d0327).ExtendByPeriod(NewPeriod(0, 0, -8)) - fmt.Printf("\ndr=%#v\n", dr) + dr := OneDayRange(d0327).ExtendByPeriod(period.NewPeriod(0, 0, -8)) + //fmt.Printf("\ndr=%#v\n", dr) isEq(t, dr.Days(), PeriodOfDays(7)) isEq(t, dr.Start(), d0320) isEq(t, dr.Last(), d0326) From b651586e61d28a763ac4f4cda317df15119f9631 Mon Sep 17 00:00:00 2001 From: Rick Date: Sat, 6 Feb 2016 09:39:53 +0000 Subject: [PATCH 052/165] Finished the binary encoding/decoding. Obsolete encoders/decoders were pruned out. --- marshal.go | 34 ------------------ marshal_test.go | 40 --------------------- period/marshal.go | 80 +++++++++++++++++------------------------- period/marshal_test.go | 50 ++++++++++++++++---------- view/vdate.go | 24 ++++++------- 5 files changed, 77 insertions(+), 151 deletions(-) diff --git a/marshal.go b/marshal.go index 913f7b5d..8a2d347c 100644 --- a/marshal.go +++ b/marshal.go @@ -6,7 +6,6 @@ package date import ( "errors" - "fmt" ) // MarshalBinary implements the encoding.BinaryMarshaler interface. @@ -34,39 +33,6 @@ func (d *Date) UnmarshalBinary(data []byte) error { return nil } -// GobEncode implements the gob.GobEncoder interface. -func (d Date) GobEncode() ([]byte, error) { - return d.MarshalBinary() -} - -// GobDecode implements the gob.GobDecoder interface. -func (d *Date) GobDecode(data []byte) error { - return d.UnmarshalBinary(data) -} - -// MarshalJSON implements the json.Marshaler interface. -// The date is a quoted string in ISO 8601 extended format (e.g. "2006-01-02"). -// If the year of the date falls outside the [0,9999] range, this format -// produces an expanded year representation with possibly extra year digits -// beyond the prescribed four-digit minimum and with a + or - sign prefix -// (e.g. , "+12345-06-07", "-0987-06-05"). -func (d Date) MarshalJSON() ([]byte, error) { - return []byte(`"` + d.String() + `"`), nil -} - -// UnmarshalJSON implements the json.Unmarshaler interface. -// The date is expected to be a quoted string in ISO 8601 extended format -// (e.g. "2006-01-02", "+12345-06-07", "-0987-06-05"); -// the year must use at least 4 digits and if outside the [0,9999] range -// must be prefixed with a + or - sign. -func (d *Date) UnmarshalJSON(data []byte) error { - n := len(data) - if n < 2 || data[0] != '"' || data[n-1] != '"' { - return fmt.Errorf("Date.UnmarshalJSON: missing double quotes (%s)", string(data)) - } - return d.UnmarshalText(data[1 : n-1]) -} - // MarshalText implements the encoding.TextMarshaler interface. // The date is given in ISO 8601 extended format (e.g. "2006-01-02"). // If the year of the date falls outside the [0,9999] range, this format diff --git a/marshal_test.go b/marshal_test.go index d8176a62..1fe80614 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -41,27 +41,6 @@ func TestGobEncoding(t *testing.T) { } } -func TestInvalidGob(t *testing.T) { - cases := []struct { - bytes []byte - want string - }{ - {[]byte{}, "Date.UnmarshalBinary: no data"}, - {[]byte{1, 2, 3}, "Date.UnmarshalBinary: invalid length"}, - } - for _, c := range cases { - var ignored Date - err := ignored.GobDecode(c.bytes) - if err == nil || err.Error() != c.want { - t.Errorf("InvalidGobDecode(%v) == %v, want %v", c.bytes, err, c.want) - } - err = ignored.UnmarshalBinary(c.bytes) - if err == nil || err.Error() != c.want { - t.Errorf("InvalidUnmarshalBinary(%v) == %v, want %v", c.bytes, err, c.want) - } - } -} - func TestDateJSONMarshalling(t *testing.T) { cases := []struct { value Date @@ -93,25 +72,6 @@ func TestDateJSONMarshalling(t *testing.T) { } } -func TestInvalidDateJSON(t *testing.T) { - cases := []struct { - value string - want string - }{ - {`"not-a-date"`, `Date.ParseISO: cannot parse not-a-date: incorrect syntax`}, - {`2015-08-15"`, `Date.UnmarshalJSON: missing double quotes (2015-08-15")`}, - {`"2015-08-15`, `Date.UnmarshalJSON: missing double quotes ("2015-08-15)`}, - {`"215-08-15"`, `Date.ParseISO: cannot parse 215-08-15: invalid year`}, - } - for _, c := range cases { - var d Date - err := d.UnmarshalJSON([]byte(c.value)) - if err == nil || err.Error() != c.want { - t.Errorf("InvalidJSON(%v) == %v, want %v", c.value, err, c.want) - } - } -} - func TestDateTextMarshalling(t *testing.T) { cases := []struct { value Date diff --git a/period/marshal.go b/period/marshal.go index 079cecfd..eaa50524 100644 --- a/period/marshal.go +++ b/period/marshal.go @@ -4,57 +4,43 @@ package period -import ( - "fmt" -) +import "errors" -//// MarshalBinary implements the encoding.BinaryMarshaler interface. -//func (d Date) MarshalBinary() ([]byte, error) { -// enc := []byte{ -// byte(d.day >> 24), -// byte(d.day >> 16), -// byte(d.day >> 8), -// byte(d.day), -// } -// return enc, nil -//} -// -//// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. -//func (d *Date) UnmarshalBinary(data []byte) error { -// if len(data) == 0 { -// return errors.New("Date.UnmarshalBinary: no data") -// } -// if len(data) != 4 { -// return errors.New("Date.UnmarshalBinary: invalid length") -// } -// -// d.day = PeriodOfDays(data[3]) | PeriodOfDays(data[2])<<8 | PeriodOfDays(data[1])<<16 | PeriodOfDays(data[0])<<24 -// -// return nil -//} -// -//// GobEncode implements the gob.GobEncoder interface. -//func (d Date) GobEncode() ([]byte, error) { -// return d.MarshalBinary() -//} -// -//// GobDecode implements the gob.GobDecoder interface. -//func (d *Date) GobDecode(data []byte) error { -// return d.UnmarshalBinary(data) -//} - -// MarshalJSON implements the json.Marshaler interface for Period. -func (period Period) MarshalJSON() ([]byte, error) { - return []byte(`"` + period.String() + `"`), nil +// MarshalBinary implements the encoding.BinaryMarshaler interface. +// This also provides support for gob encoding. +func (p Period) MarshalBinary() ([]byte, error) { + enc := []byte{ + byte(p.years >> 24), + byte(p.years >> 16), + byte(p.years >> 8), + byte(p.years), + byte(p.months >> 24), + byte(p.months >> 16), + byte(p.months >> 8), + byte(p.months), + byte(p.days >> 24), + byte(p.days >> 16), + byte(p.days >> 8), + byte(p.days), + } + return enc, nil } -// UnmarshalJSON implements the json.Unmarshaler interface for Period. -func (period *Period) UnmarshalJSON(data []byte) error { - n := len(data) - if n < 2 || data[0] != '"' || data[n-1] != '"' { - return fmt.Errorf("Period.UnmarshalJSON: missing double quotes (%s)", string(data)) +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +// This also provides support for gob encoding. +func (p *Period) UnmarshalBinary(data []byte) error { + if len(data) == 0 { + return errors.New("Date.UnmarshalBinary: no data") + } + if len(data) != 12 { + return errors.New("Date.UnmarshalBinary: invalid length") } - return period.UnmarshalText(data[1 : n-1]) + + p.years = int32(data[3]) | int32(data[2])<<8 | int32(data[1])<<16 | int32(data[0])<<24 + p.months = int32(data[7]) | int32(data[6])<<8 | int32(data[5])<<16 | int32(data[4])<<24 + p.days = int32(data[11]) | int32(data[10])<<8 | int32(data[9])<<16 | int32(data[8])<<24 + + return nil } // MarshalText implements the encoding.TextMarshaler interface for Periods. diff --git a/period/marshal_test.go b/period/marshal_test.go index 5fc961db..5843b79d 100644 --- a/period/marshal_test.go +++ b/period/marshal_test.go @@ -5,10 +5,42 @@ package period import ( + "bytes" + "encoding/gob" "encoding/json" "testing" ) +func TestGobEncoding(t *testing.T) { + var b bytes.Buffer + encoder := gob.NewEncoder(&b) + decoder := gob.NewDecoder(&b) + cases := []string{ + "P0D", + "P1D", + "P1W", + "P1M", + "P1Y", + "P2Y3M4W5D", + "-P2Y3M4W5D", + } + for _, c := range cases { + period := MustParsePeriod(c) + var p Period + err := encoder.Encode(&period) + if err != nil { + t.Errorf("Gob(%v) encode error %v", c, err) + } else { + err = decoder.Decode(&p) + if err != nil { + t.Errorf("Gob(%v) decode error %v", c, err) + } else if p != period { + t.Errorf("Gob(%v) decode got %v", c, p) + } + } + } +} + func TestPeriodJSONMarshalling(t *testing.T) { cases := []struct { value Period @@ -39,24 +71,6 @@ func TestPeriodJSONMarshalling(t *testing.T) { } } -func TestInvalidPeriodJSON(t *testing.T) { - cases := []struct { - value string - want string - }{ - {`""`, `Cannot parse a blank string as a period.`}, - {`"not-a-period"`, `Expected 'P' period mark at the start: not-a-period`}, - {`"P000"`, `Expected 'Y', 'M', 'W' or 'D' marker: P000`}, - } - for _, c := range cases { - var p Period - err := p.UnmarshalJSON([]byte(c.value)) - if err == nil || err.Error() != c.want { - t.Errorf("InvalidJSON(%v) == %v, want %v", c.value, err, c.want) - } - } -} - func TestPeriodTextMarshalling(t *testing.T) { cases := []struct { value Period diff --git a/view/vdate.go b/view/vdate.go index 39b63656..e33ee9c4 100644 --- a/view/vdate.go +++ b/view/vdate.go @@ -108,21 +108,21 @@ func (d VDate) Previous() VDateDelta { // via the main Date type; VDate is only intended for output through view layers. // MarshalJSON implements the json.Marshaler interface. -func (v VDate) MarshalJSON() ([]byte, error) { - return v.d.MarshalJSON() -} +//func (v VDate) MarshalJSON() ([]byte, error) { +// return v.d.MarshalJSON() +//} // UnmarshalJSON implements the json.Unmarshaler interface. // Note that the format value gets lost. -func (v *VDate) UnmarshalJSON(data []byte) (err error) { - u := &date.Date{} - err = u.UnmarshalJSON(data) - if err == nil { - v.d = *u - v.f = DefaultFormat - } - return err -} +//func (v *VDate) UnmarshalJSON(data []byte) (err error) { +// u := &date.Date{} +// err = u.UnmarshalJSON(data) +// if err == nil { +// v.d = *u +// v.f = DefaultFormat +// } +// return err +//} // MarshalText implements the encoding.TextMarshaler interface. func (v VDate) MarshalText() ([]byte, error) { From b74feb26576f1834e4895e3c820ec46021d10f19 Mon Sep 17 00:00:00 2001 From: Rick Date: Sat, 6 Feb 2016 10:01:05 +0000 Subject: [PATCH 053/165] Documentation --- period/doc.go | 22 ++++++++++++++++++++++ period/isoperiod.go | 5 ----- 2 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 period/doc.go diff --git a/period/doc.go b/period/doc.go new file mode 100644 index 00000000..b478d764 --- /dev/null +++ b/period/doc.go @@ -0,0 +1,22 @@ +// Copyright 2016 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package period provides functionality for periods of time using ISO-8601 conventions. +// This deals with years, months, weeks and days. +// Because of the vagaries of calendar systems, the meaning of year lengths, month lengths +// and even day lengths depends on context. So a period is not necessarily a fixed duration +// of time in terms of seconds. +// +// See https://en.wikipedia.org/wiki/ISO_8601#Periods +// +// Example representations: +// +// * "P4D" is four days; +// +// * "P3Y6M4W1D" is three years, 6 months, 4 weeks and one day. +// +// Note that ISO-8601 periods can also express periods of time in terms of hours, minutes and seconds, +// as well as years/months/weeks/days, but the hour/minute/second parts are not (yet) supported. +// +package period diff --git a/period/isoperiod.go b/period/isoperiod.go index 1a400479..114796f7 100644 --- a/period/isoperiod.go +++ b/period/isoperiod.go @@ -12,11 +12,6 @@ import ( // year lengths, month lengths and even day lengths depends on context. So a period is // not necessarily a fixed duration of time in terms of seconds. // -// See https://en.wikipedia.org/wiki/ISO_8601#Periods -// -// Example representations: "P4D" is four days; "P3Y6M4W1D" is three years, 6 months, -// 4 weeks and one day. -// // In the ISO representation, decimal fractions are supported, although only the last non-zero // component is allowed to have a fraction according to the Standard. For example "P2.5Y" // is 2.5 years. From 67f93839ff7cfeb0e2d6f2cf3057f9a586878d61 Mon Sep 17 00:00:00 2001 From: Rick Date: Sat, 6 Feb 2016 10:03:51 +0000 Subject: [PATCH 054/165] Documentation --- doc.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc.go b/doc.go index 69914bff..f0db1fdb 100644 --- a/doc.go +++ b/doc.go @@ -3,7 +3,8 @@ // license that can be found in the LICENSE file. // Package date provides functionality for working with dates. Subpackages support -// clock-face time, spans of time and ranges of dates. +// clock-face time, spans of time, ranges of dates, and periods (as years, months, +// weeks and days). // // This package introduces a light-weight Date type that is storage-efficient // and covenient for calendrical calculations and date parsing and formatting From 8836972c497d04eb204a7bbaf58d2efe8cf5af63 Mon Sep 17 00:00:00 2001 From: Rick Date: Sat, 6 Feb 2016 13:11:21 +0000 Subject: [PATCH 055/165] Period now supports time-based and day-based components, i.e. the complete ISO-8601 field set. The precision has been reduced to int16-based fields to match better the expected use cases. --- date_test.go | 4 +- period/doc.go | 7 +- period/isoperiod.go | 346 ++++++++++++++++++++++++------------- period/isoperiod_test.go | 193 +++++++++++++++------ period/marshal.go | 38 +--- period/marshal_test.go | 39 +++-- timespan/daterange_test.go | 8 +- 7 files changed, 406 insertions(+), 229 deletions(-) diff --git a/date_test.go b/date_test.go index 2034b873..5014be8c 100644 --- a/date_test.go +++ b/date_test.go @@ -255,12 +255,12 @@ func xTestAddDuration(t *testing.T) { for _, c := range cases { di := New(c.year, c.month, c.day) for _, days := range offsets { - dj := di.AddPeriod(period.NewPeriod(0, 0, int(days))) + dj := di.AddPeriod(period.New(0, 0, int(days), 0, 0, 0)) days2 := dj.Sub(di) if days2 != days { t.Errorf("AddSub(%v,%v) == %v, want %v", di, days, days2, days) } - dk := dj.AddPeriod(period.NewPeriod(0, 0, -int(days))) + dk := dj.AddPeriod(period.New(0, 0, -int(days), 0, 0, 0)) if dk != di { t.Errorf("AddNeg(%v,%v) == %v, want %v", di, days, dk, di) } diff --git a/period/doc.go b/period/doc.go index b478d764..745ca4a6 100644 --- a/period/doc.go +++ b/period/doc.go @@ -16,7 +16,10 @@ // // * "P3Y6M4W1D" is three years, 6 months, 4 weeks and one day. // -// Note that ISO-8601 periods can also express periods of time in terms of hours, minutes and seconds, -// as well as years/months/weeks/days, but the hour/minute/second parts are not (yet) supported. +// * "P2DT12H" is 2 days and 12 hours. +// +// * "PT30S" is 30 seconds. +// +// * "P2.5Y" is 2.5 years. // package period diff --git a/period/isoperiod.go b/period/isoperiod.go index 114796f7..5133b970 100644 --- a/period/isoperiod.go +++ b/period/isoperiod.go @@ -5,20 +5,20 @@ import ( . "github.com/rickb777/plural" "strconv" "strings" + "time" ) -// Period holds a period of time and provides conversion to/from -// ISO-8601 representations. Because of the vagaries of calendar systems, the meaning of -// year lengths, month lengths and even day lengths depends on context. So a period is -// not necessarily a fixed duration of time in terms of seconds. -// +// Period holds a period of time and provides conversion to/from ISO-8601 representations. // In the ISO representation, decimal fractions are supported, although only the last non-zero // component is allowed to have a fraction according to the Standard. For example "P2.5Y" // is 2.5 years. // -// Internally, fractions are expressed using fixed-point arithmetic to three -// decimal places only. This avoids using float32 in the struct, so there are no problems -// testing equality using ==. +// In this implementation, the precision is limited to one decimal place only, by means +// of integers with fixed point arithmetic. This avoids using float32 in the struct, so +// there are no problems testing equality using ==. +// +// The implementation limits the range of possible values to +/- 2^16 / 10. Note in +// particular that the range of years is limited to approximately +/- 3276. // // The concept of weeks exists in string representations of periods, but otherwise weeks // are unimportant. The period contains a number of days from which the number of weeks can @@ -26,38 +26,51 @@ import ( // Note that although fractional weeks can be parsed, they will never be returned. This is // because the number of weeks is always computed as an integer from the number of days. // -// IMPLENTATION NOTE: THE TIME COMPONENT OF ISO-8601 IS NOT YET SUPPORTED. type Period struct { - years, months, days int32 + years, months, days, hours, minutes, seconds int16 +} + +// NewYMD creates a simple period without any fractional parts. All the parameters +// must have the same sign (otherwise a panic occurs). +func NewYMD(years, months, days int) Period { + return New(years, months, days, 0, 0, 0) +} + +// NewHMS creates a simple period without any fractional parts. All the parameters +// must have the same sign (otherwise a panic occurs). +func NewHMS(hours, minutes, seconds int) Period { + return New(0, 0, 0, hours, minutes, seconds) } // NewPeriod creates a simple period without any fractional parts. All the parameters // must have the same sign (otherwise a panic occurs). -func NewPeriod(years, months, days int) Period { - if (years >= 0 && months >= 0 && days >= 0) || - (years <= 0 && months <= 0 && days <= 0) { - return Period{int32(years) * 1000, int32(months) * 1000, int32(days) * 1000} +func New(years, months, days, hours, minutes, seconds int) Period { + if (years >= 0 && months >= 0 && days >= 0 && hours >= 0 && minutes >= 0 && seconds >= 0) || + (years <= 0 && months <= 0 && days <= 0 && hours <= 0 && minutes <= 0 && seconds <= 0) { + return Period{int16(years) * 10, int16(months) * 10, int16(days) * 10, + int16(hours) * 10, int16(minutes) * 10, int16(seconds) * 10} } - panic(fmt.Sprintf("Periods must have homogeneous signs; got P%dY%dM%dD", years, months, days)) + panic(fmt.Sprintf("Periods must have homogeneous signs; got P%dY%dM%dD%%dH%dM%dS", + years, months, days, hours, minutes, seconds)) } -// MustParsePeriod is as per ParsePeriod except that it panics if the string cannot be parsed. +// MustParse is as per Parse except that it panics if the string cannot be parsed. // This is intended for setup code; don't use it for user inputs. -func MustParsePeriod(value string) Period { - d, err := ParsePeriod(value) +func MustParse(value string) Period { + d, err := Parse(value) if err != nil { panic(err) } return d } -// ParsePeriod parses strings that specify periods using ISO-8601 rules. +// Parse parses strings that specify periods using ISO-8601 rules. // // In addition, a plus or minus sign can precede the period, e.g. "-P10D" // // The zero value can be represented in several ways: all of the following // are equivalent: "P0Y", "P0M", "P0W", "P0D", and "P0". -func ParsePeriod(period string) (Period, error) { +func Parse(period string) (Period, error) { if period == "" { return Period{}, fmt.Errorf("Cannot parse a blank string as a period.") } @@ -66,84 +79,100 @@ func ParsePeriod(period string) (Period, error) { return Period{}, nil } - dur := period - sign := int32(1) - if dur[0] == '-' { - sign = -1 - dur = dur[1:] - } else if dur[0] == '+' { - dur = dur[1:] - } - - ok := false - result := Period{} - t := strings.IndexByte(dur, 'T') - if t > 0 { - // NOY YET IMPLEMENTED - dur = dur[:t] + pcopy := period + negate := false + if pcopy[0] == '-' { + negate = true + pcopy = pcopy[1:] + } else if pcopy[0] == '+' { + pcopy = pcopy[1:] } - if dur[0] != 'P' { + if pcopy[0] != 'P' { return Period{}, fmt.Errorf("Expected 'P' period mark at the start: %s", period) } - dur = dur[1:] + pcopy = pcopy[1:] + + result := Period{} + + st := parseState{period, pcopy, false, nil} + t := strings.IndexByte(pcopy, 'T') + if t >= 0 { + st.pcopy = pcopy[t+1:] - y := strings.IndexByte(dur, 'Y') - if y > 0 { - t, err := parseDecimalFixedPoint(dur[:y], period) - if err != nil { - return Period{}, err + result.hours, st = parseField(st, 'H') + if st.err != nil { + return Period{}, st.err } - dur = dur[y+1:] - result.years = sign * t - ok = true - } - m := strings.IndexByte(dur, 'M') - if m > 0 { - t, err := parseDecimalFixedPoint(dur[:m], period) - if err != nil { - return Period{}, err + result.minutes, st = parseField(st, 'M') + if st.err != nil { + return Period{}, st.err } - dur = dur[m+1:] - result.months = sign * t - ok = true - } - weeks := int32(0) - w := strings.IndexByte(dur, 'W') - if w > 0 { - var err error - weeks, err = parseDecimalFixedPoint(dur[:w], period) - if err != nil { - return Period{}, err + result.seconds, st = parseField(st, 'S') + if st.err != nil { + return Period{}, st.err } - dur = dur[w+1:] - ok = true + + st.pcopy = pcopy[:t] } - days := int32(0) - d := strings.IndexByte(dur, 'D') - if d > 0 { - var err error - days, err = parseDecimalFixedPoint(dur[:d], period) - if err != nil { - return Period{}, err - } - dur = dur[d+1:] - ok = true + result.years, st = parseField(st, 'Y') + if st.err != nil { + return Period{}, st.err + } + + result.months, st = parseField(st, 'M') + if st.err != nil { + return Period{}, st.err } - result.days = sign * (weeks*7 + days) - if !ok { - return Period{}, fmt.Errorf("Expected 'Y', 'M', 'W' or 'D' marker: %s", period) + weeks, st := parseField(st, 'W') + if st.err != nil { + return Period{}, st.err + } + + days, st := parseField(st, 'D') + if st.err != nil { + return Period{}, st.err + } + + result.days = weeks*7 + days + //fmt.Printf("%#v\n", st) + + if !st.ok { + return Period{}, fmt.Errorf("Expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' marker: %s", period) + } + if negate { + return result.Negate(), nil } return result, nil - //P, Y, M, W, D, T, H, M, and S +} + +type parseState struct { + period, pcopy string + ok bool + err error +} + +func parseField(st parseState, mark byte) (int16, parseState) { + //fmt.Printf("%c %#v\n", mark, st) + r := int16(0) + m := strings.IndexByte(st.pcopy, mark) + if m > 0 { + r, st.err = parseDecimalFixedPoint(st.pcopy[:m], st.period) + if st.err != nil { + return 0, st + } + st.pcopy = st.pcopy[m+1:] + st.ok = true + } + return r, st } // Fixed-point three decimal places -func parseDecimalFixedPoint(s, original string) (int32, error) { +func parseDecimalFixedPoint(s, original string) (int16, error) { //was := s dec := strings.IndexByte(s, '.') if dec < 0 { @@ -152,25 +181,18 @@ func parseDecimalFixedPoint(s, original string) (int32, error) { if dec >= 0 { dp := len(s) - dec - if dp > 3 { - s = s[:dec] + s[dec+1:dec+4] + if dp > 1 { + s = s[:dec] + s[dec+1:dec+2] } else { - switch dp { - case 3: - s = s[:dec] + s[dec+1:] + "0" - case 2: - s = s[:dec] + s[dec+1:] + "00" - case 1: - s = s[:dec] + s[dec+1:] + "000" - } + s = s[:dec] + s[dec+1:] + "0" } } else { - s = s + "000" + s = s + "0" } - n, e := strconv.ParseInt(s, 10, 32) + n, e := strconv.ParseInt(s, 10, 16) //fmt.Printf("ParseInt(%s) = %d -- from %s in %s %d\n", s, n, was, original, dec) - return int32(n), e + return int16(n), e } // IsZero returns true if applied to a zero-length period. @@ -196,32 +218,35 @@ func (period Period) IsNegative() bool { // Format converts the period to human-readable form using the default localisation. func (period Period) Format() string { - return period.FormatWithPeriodNames(PeriodYearNames, PeriodMonthNames, PeriodWeekNames, PeriodDayNames) + return period.FormatWithPeriodNames(PeriodYearNames, PeriodMonthNames, PeriodWeekNames, PeriodDayNames, PeriodHourNames, PeriodMinuteNames, PeriodSecondNames) } // FormatWithPeriodNames converts the period to human-readable form in a localisable way. -func (period Period) FormatWithPeriodNames(yearNames Plurals, monthNames Plurals, weekNames Plurals, dayNames Plurals) string { +func (period Period) FormatWithPeriodNames(yearNames, monthNames, weekNames, dayNames, hourNames, minNames, secNames Plurals) string { period = period.Abs() parts := make([]string, 0) - parts = appendNonBlank(parts, yearNames.FormatFloat(absFloat1000(period.years))) - parts = appendNonBlank(parts, monthNames.FormatFloat(absFloat1000(period.months))) + parts = appendNonBlank(parts, yearNames.FormatFloat(absFloat10(period.years))) + parts = appendNonBlank(parts, monthNames.FormatFloat(absFloat10(period.months))) - if (period.years == 0 && period.months == 0) || period.days > 0 { + if period.days > 0 || (period.IsZero()) { if len(weekNames) > 0 { - weeks := period.days / 7000 - mdays := period.days % 7000 + weeks := period.days / 70 + mdays := period.days % 70 //fmt.Printf("%v %#v - %d %d\n", period, period, weeks, mdays) if weeks > 0 { parts = appendNonBlank(parts, weekNames.FormatInt(int(weeks))) } if mdays > 0 || weeks == 0 { - parts = appendNonBlank(parts, dayNames.FormatFloat(absFloat1000(mdays))) + parts = appendNonBlank(parts, dayNames.FormatFloat(absFloat10(mdays))) } } else { - parts = appendNonBlank(parts, dayNames.FormatFloat(absFloat1000(period.days))) + parts = appendNonBlank(parts, dayNames.FormatFloat(absFloat10(period.days))) } } + parts = appendNonBlank(parts, hourNames.FormatFloat(absFloat10(period.hours))) + parts = appendNonBlank(parts, minNames.FormatFloat(absFloat10(period.minutes))) + parts = appendNonBlank(parts, secNames.FormatFloat(absFloat10(period.seconds))) return strings.Join(parts, ", ") } @@ -247,6 +272,15 @@ var PeriodMonthNames = Plurals{Case{0, ""}, Case{1, "%g month"}, Case{2, "%g mon // PeriodYearNames is as for PeriodDayNames but for years. var PeriodYearNames = Plurals{Case{0, ""}, Case{1, "%g year"}, Case{2, "%g years"}} +// PeriodHourNames is as for PeriodDayNames but for hours. +var PeriodHourNames = Plurals{Case{0, ""}, Case{1, "%v hour"}, Case{2, "%v hours"}} + +// PeriodMinuteNames is as for PeriodDayNames but for minutes. +var PeriodMinuteNames = Plurals{Case{0, ""}, Case{1, "%g minute"}, Case{2, "%g minutes"}} + +// PeriodSecondNames is as for PeriodDayNames but for seconds. +var PeriodSecondNames = Plurals{Case{0, ""}, Case{1, "%g second"}, Case{2, "%g seconds"}} + // String converts the period to -8601 form. func (period Period) String() string { if period.IsZero() { @@ -254,35 +288,47 @@ func (period Period) String() string { } s := "" - if period.years < 0 || period.months < 0 || period.days < 0 { + if period.Sign() < 0 { s = "-" } - y, m, w, d := "", "", "", "" + y, m, w, d, t, hh, mm, ss := "", "", "", "", "", "", "", "" if period.years != 0 { - y = fmt.Sprintf("%gY", absFloat1000(period.years)) + y = fmt.Sprintf("%gY", absFloat10(period.years)) } if period.months != 0 { - m = fmt.Sprintf("%gM", absFloat1000(period.months)) + m = fmt.Sprintf("%gM", absFloat10(period.months)) } if period.days != 0 { //days := absInt32(period.days) //weeks := days / 7 - //if (weeks >= 1000) { + //if (weeks >= 10) { // w = fmt.Sprintf("%gW", absFloat(weeks)) //} //mdays := days % 7 if period.days != 0 { - d = fmt.Sprintf("%gD", absFloat1000(period.days)) + d = fmt.Sprintf("%gD", absFloat10(period.days)) } } + if period.hours != 0 || period.minutes != 0 || period.seconds != 0 { + t = "T" + } + if period.hours != 0 { + hh = fmt.Sprintf("%gH", absFloat10(period.hours)) + } + if period.minutes != 0 { + mm = fmt.Sprintf("%gM", absFloat10(period.minutes)) + } + if period.seconds != 0 { + ss = fmt.Sprintf("%gS", absFloat10(period.seconds)) + } - return fmt.Sprintf("%sP%s%s%s%s", s, y, m, w, d) + return fmt.Sprintf("%sP%s%s%s%s%s%s%s%s", s, y, m, w, d, t, hh, mm, ss) } -func absFloat1000(v int32) float32 { - f := float32(v) / 1000 +func absFloat10(v int16) float32 { + f := float32(v) / 10 if v < 0 { return -f } @@ -291,10 +337,11 @@ func absFloat1000(v int32) float32 { // Abs converts a negative period to a positive one. func (period Period) Abs() Period { - return Period{absInt32(period.years), absInt32(period.months), absInt32(period.days)} + return Period{absInt16(period.years), absInt16(period.months), absInt16(period.days), + absInt16(period.hours), absInt16(period.minutes), absInt16(period.seconds)} } -func absInt32(v int32) int32 { +func absInt16(v int16) int16 { if v < 0 { return -v } @@ -303,35 +350,39 @@ func absInt32(v int32) int32 { // Negate changes the sign of the period. func (period Period) Negate() Period { - return Period{-period.years, -period.months, -period.days} + return Period{-period.years, -period.months, -period.days, -period.hours, -period.minutes, -period.seconds} } // Sign returns +1 for positive periods and -1 for negative periods. func (period Period) Sign() int { - if period.years < 0 { + if period.years < 0 || period.months < 0 || period.days < 0 || period.hours < 0 || period.minutes < 0 || period.seconds < 0 { return -1 } return 1 } // Years gets the whole number of years in the period. +// The result does not include any other field. func (period Period) Years() int { return int(period.YearsFloat()) } // YearsFloat gets the number of years in the period, including a fraction if any is present. +// The result does not include any other field. func (period Period) YearsFloat() float32 { - return float32(period.years) / 1000 + return float32(period.years) / 10 } // Months gets the whole number of months in the period. +// The result does not include any other field. func (period Period) Months() int { return int(period.MonthsFloat()) } // MonthsFloat gets the number of months in the period. +// The result does not include any other field. func (period Period) MonthsFloat() float32 { - return float32(period.months) / 1000 + return float32(period.months) / 10 } // Days gets the whole number of days in the period. This includes the implied @@ -343,24 +394,77 @@ func (period Period) Days() int { // DaysFloat gets the number of days in the period. This includes the implied // number of weeks. func (period Period) DaysFloat() float32 { - return float32(period.days) / 1000 + return float32(period.days) / 10 } // Weeks calculates the number of whole weeks from the number of days. If the result // would contain a fraction, it is truncated. func (period Period) Weeks() int { - return int(period.days) / 7000 + return int(period.days) / 70 } // ModuloDays calculates the whole number of days remaining after the whole number of weeks // has been excluded. func (period Period) ModuloDays() int { - days := absInt32(period.days) % 7000 - f := int(days / 1000) + days := absInt16(period.days) % 70 + f := int(days / 10) if period.days < 0 { return -f } return f } -//TODO gobencode +// Hours gets the whole number of hours in the period. +// The result does not include any other field. +func (period Period) Hours() int { + return int(period.HoursFloat()) +} + +// HoursFloat gets the number of hours in the period. +// The result does not include any other field. +func (period Period) HoursFloat() float32 { + return float32(period.hours) / 10 +} + +// Minutes gets the whole number of minutes in the period. +// The result does not include any other field. +func (period Period) Minutes() int { + return int(period.MinutesFloat()) +} + +// MinutesFloat gets the number of minutes in the period. +// The result does not include any other field. +func (period Period) MinutesFloat() float32 { + return float32(period.minutes) / 10 +} + +// Seconds gets the whole number of seconds in the period. +// The result does not include any other field. +func (period Period) Seconds() int { + return int(period.SecondsFloat()) +} + +// SecondsFloat gets the number of seconds in the period. +// The result does not include any other field. +func (period Period) SecondsFloat() float32 { + return float32(period.seconds) / 10 +} + +// Duration converts a period to the equivalent duration in nanoseconds. +// A flag is also returned that is true when the conversion was precise and false otherwise. +// When the period specifies years, months and days, it is impossible to be precise, so +// the duration is calculated on the basis of a year being 365.2 days and a month being +// 1/12 of a year; days are all 24 hours long. +func (period Period) Duration() (time.Duration, bool) { + // remember that the fields are all fixed-point 1E1 + ydE6 := time.Duration(period.years) * 36525000 // 365.25 days + mdE6 := time.Duration(period.months) * 3043750 // 30.437 days + ddE6 := time.Duration(period.days) * 100000 + tdE6 := (ydE6 + mdE6 + ddE6) * 86400 + hhE3 := time.Duration(period.hours) * 360000 + mmE3 := time.Duration(period.minutes) * 6000 + ssE3 := time.Duration(period.seconds) * 100 + //fmt.Printf("y %d, m %d, d %d, hh %d, mm %d, ss %d\n", ydE6, mdE6, ddE6, hhE3, mmE3, ssE3) + stE3 := hhE3 + mmE3 + ssE3 + return tdE6*time.Microsecond + stE3*time.Millisecond, tdE6 == 0 +} diff --git a/period/isoperiod_test.go b/period/isoperiod_test.go index c354b8a0..21e96630 100644 --- a/period/isoperiod_test.go +++ b/period/isoperiod_test.go @@ -3,6 +3,7 @@ package period import ( "github.com/rickb777/plural" "testing" + "time" ) func TestParsePeriod(t *testing.T) { @@ -12,23 +13,27 @@ func TestParsePeriod(t *testing.T) { }{ {"P0", Period{}}, {"P0D", Period{}}, - {"P3Y", Period{3000, 0, 0}}, - {"P6M", Period{0, 6000, 0}}, - {"P5W", Period{0, 0, 35000}}, - {"P4D", Period{0, 0, 4000}}, - //{"PT12H", Period{}}, - //{"PT30M", Period{}}, - //{"PT5S", Period{}}, - {"P3Y6M5W4DT12H30M5S", Period{3000, 6000, 39000}}, - {"+P3Y6M5W4DT12H30M5S", Period{3000, 6000, 39000}}, - {"-P3Y6M5W4DT12H30M5S", Period{-3000, -6000, -39000}}, - {"P2.Y", Period{2000, 0, 0}}, - {"P2.5Y", Period{2500, 0, 0}}, - {"P2.15Y", Period{2150, 0, 0}}, - {"P2.125Y", Period{2125, 0, 0}}, + {"P3Y", Period{30, 0, 0, 0, 0, 0}}, + {"P6M", Period{0, 60, 0, 0, 0, 0}}, + {"P5W", Period{0, 0, 350, 0, 0, 0}}, + {"P4D", Period{0, 0, 40, 0, 0, 0}}, + {"PT12H", Period{0, 0, 0, 120, 0, 0}}, + {"PT30M", Period{0, 0, 0, 0, 300, 0}}, + {"PT25S", Period{0, 0, 0, 0, 0, 250}}, + {"P3Y6M5W4DT12H40M5S", Period{30, 60, 390, 120, 400, 50}}, + {"+P3Y6M5W4DT12H40M5S", Period{30, 60, 390, 120, 400, 50}}, + {"-P3Y6M5W4DT12H40M5S", Period{-30, -60, -390, -120, -400, -50}}, + {"P2.Y", Period{20, 0, 0, 0, 0, 0}}, + {"P2.5Y", Period{25, 0, 0, 0, 0, 0}}, + {"P2.15Y", Period{21, 0, 0, 0, 0, 0}}, + {"P2.125Y", Period{21, 0, 0, 0, 0, 0}}, + {"P1Y2.M", Period{10, 20, 0, 0, 0, 0}}, + {"P1Y2.5M", Period{10, 25, 0, 0, 0, 0}}, + {"P1Y2.15M", Period{10, 21, 0, 0, 0, 0}}, + {"P1Y2.125M", Period{10, 21, 0, 0, 0, 0}}, } for _, c := range cases { - d := MustParsePeriod(c.value) + d := MustParse(c.value) if d != c.period { t.Errorf("MustParsePeriod(%v) == %#v, want (%#v)", c.value, d, c.period) } @@ -39,7 +44,7 @@ func TestParsePeriod(t *testing.T) { "P", } for _, c := range badCases { - d, err := ParsePeriod(c) + d, err := Parse(c) if err == nil { t.Errorf("ParsePeriod(%v) == %v", c, d) } @@ -52,22 +57,20 @@ func TestPeriodString(t *testing.T) { period Period }{ {"P0D", Period{}}, - {"P3Y", Period{3000, 0, 0}}, - {"-P3Y", Period{-3000, 0, 0}}, - {"P6M", Period{0, 6000, 0}}, - {"-P6M", Period{0, -6000, 0}}, - {"P35D", Period{0, 0, 35000}}, - {"-P35D", Period{0, 0, -35000}}, - {"P4D", Period{0, 0, 4000}}, - {"-P4D", Period{0, 0, -4000}}, - //{"PT12H", Period{}}, - //{"PT30M", Period{}}, - //{"PT5S", Period{}}, - {"P3Y6M39D", Period{3000, 6000, 39000}}, - {"-P3Y6M39D", Period{-3000, -6000, -39000}}, - {"P2.5Y", Period{2500, 0, 0}}, - {"P2.15Y", Period{2150, 0, 0}}, - {"P2.125Y", Period{2125, 0, 0}}, + {"P3Y", Period{30, 0, 0, 0, 0, 0}}, + {"-P3Y", Period{-30, 0, 0, 0, 0, 0}}, + {"P6M", Period{0, 60, 0, 0, 0, 0}}, + {"-P6M", Period{0, -60, 0, 0, 0, 0}}, + {"P35D", Period{0, 0, 350, 0, 0, 0}}, + {"-P35D", Period{0, 0, -350, 0, 0, 0}}, + {"P4D", Period{0, 0, 40, 0, 0, 0}}, + {"-P4D", Period{0, 0, -40, 0, 0, 0}}, + {"PT12H", Period{0, 0, 0, 120, 0, 0}}, + {"PT30M", Period{0, 0, 0, 0, 300, 0}}, + {"PT5S", Period{0, 0, 0, 0, 0, 50}}, + {"P3Y6M39DT1H2M4S", Period{30, 60, 390, 10, 20, 40}}, + {"-P3Y6M39DT1H2M4S", Period{-30, -60, -390, 10, 20, 40}}, + {"P2.5Y", Period{25, 0, 0, 0, 0, 0}}, } for _, c := range cases { s := c.period.String() @@ -77,22 +80,101 @@ func TestPeriodString(t *testing.T) { } } +func TestPeriodComponents(t *testing.T) { + cases := []struct { + value string + y, m, w, d, dx, hh, mm, ss int + }{ + {"P0D", 0, 0, 0, 0, 0, 0, 0, 0}, + {"P1Y", 1, 0, 0, 0, 0, 0, 0, 0}, + {"-P1Y", -1, 0, 0, 0, 0, 0, 0, 0}, + {"P6M", 0, 6, 0, 0, 0, 0, 0, 0}, + {"-P6M", 0, -6, 0, 0, 0, 0, 0, 0}, + {"P39D", 0, 0, 5, 39, 4, 0, 0, 0}, + {"-P39D", 0, 0, -5, -39, -4, 0, 0, 0}, + {"P4D", 0, 0, 0, 4, 4, 0, 0, 0}, + {"-P4D", 0, 0, 0, -4, -4, 0, 0, 0}, + {"PT12H", 0, 0, 0, 0, 0, 12, 0, 0}, + {"PT30M", 0, 0, 0, 0, 0, 0, 30, 0}, + {"PT5S", 0, 0, 0, 0, 0, 0, 0, 5}, + } + for _, c := range cases { + p := MustParse(c.value) + if p.Years() != c.y { + t.Errorf("%s.Years() == %d, want %d", c.value, p.Years(), c.y) + } + if p.Months() != c.m { + t.Errorf("%s.Months() == %d, want %d", c.value, p.Months(), c.m) + } + if p.Weeks() != c.w { + t.Errorf("%s.Weeks() == %d, want %d", c.value, p.Weeks(), c.w) + } + if p.Days() != c.d { + t.Errorf("%s.Days() == %d, want %d", c.value, p.Days(), c.d) + } + if p.ModuloDays() != c.dx { + t.Errorf("%s.ModuloDays() == %d, want %d", c.value, p.ModuloDays(), c.dx) + } + if p.Hours() != c.hh { + t.Errorf("%s.Hours() == %d, want %d", c.value, p.Hours(), c.hh) + } + if p.Minutes() != c.mm { + t.Errorf("%s.Minutes() == %d, want %d", c.value, p.Minutes(), c.mm) + } + if p.Seconds() != c.ss { + t.Errorf("%s.Seconds() == %d, want %d", c.value, p.Seconds(), c.ss) + } + } +} + +func TestPeriodToDuration(t *testing.T) { + cases := []struct { + value string + duration time.Duration + precise bool + }{ + {"P0D", time.Duration(0), true}, + {"PT1S", time.Duration(1 * time.Second), true}, + {"PT1M", time.Duration(60 * time.Second), true}, + {"PT1H", time.Duration(3600 * time.Second), true}, + {"P1D", time.Duration(24 * time.Hour), false}, + {"P1M", time.Duration(2629800 * time.Second), false}, + {"P1Y", time.Duration(31557600 * time.Second), false}, + {"-P1Y", -time.Duration(31557600 * time.Second), false}, + } + for _, c := range cases { + s, p := MustParse(c.value).Duration() + if s != c.duration { + t.Errorf("Duration() == %s %v, want %s for %+v", s, p, c.duration, c.value) + } + if p != c.precise { + t.Errorf("Duration() == %s %v, want %v for %+v", s, p, c.precise, c.value) + } + } +} + func TestNewPeriod(t *testing.T) { cases := []struct { - years, months, days int - period Period + years, months, days, hours, minutes, seconds int + period Period }{ - {0, 0, 0, Period{0, 0, 0}}, - {0, 0, 1, Period{0, 0, 1000}}, - {0, 1, 0, Period{0, 1000, 0}}, - {1, 0, 0, Period{1000, 0, 0}}, - {100, 222, 700, Period{100000, 222000, 700000}}, - {0, 0, -1, Period{0, 0, -1000}}, - {0, -1, 0, Period{0, -1000, 0}}, - {-1, 0, 0, Period{-1000, 0, 0}}, + {0, 0, 0, 0, 0, 0, Period{0, 0, 0, 0, 0, 0}}, + {0, 0, 0, 0, 0, 1, Period{0, 0, 0, 0, 0, 10}}, + {0, 0, 0, 0, 1, 0, Period{0, 0, 0, 0, 10, 0}}, + {0, 0, 0, 1, 0, 0, Period{0, 0, 0, 10, 0, 0}}, + {0, 0, 1, 0, 0, 0, Period{0, 0, 10, 0, 0, 0}}, + {0, 1, 0, 0, 0, 0, Period{0, 10, 0, 0, 0, 0}}, + {1, 0, 0, 0, 0, 0, Period{10, 0, 0, 0, 0, 0}}, + {100, 222, 700, 0, 0, 0, Period{1000, 2220, 7000, 0, 0, 0}}, + {0, 0, 0, 0, 0, -1, Period{0, 0, 0, 0, 0, -10}}, + {0, 0, 0, 0, -1, 0, Period{0, 0, 0, 0, -10, 0}}, + {0, 0, 0, -1, 0, 0, Period{0, 0, 0, -10, 0, 0}}, + {0, 0, -1, 0, 0, 0, Period{0, 0, -10, 0, 0, 0}}, + {0, -1, 0, 0, 0, 0, Period{0, -10, 0, 0, 0, 0}}, + {-1, 0, 0, 0, 0, 0, Period{-10, 0, 0, 0, 0, 0}}, } for _, c := range cases { - p := NewPeriod(c.years, c.months, c.days) + p := New(c.years, c.months, c.days, c.hours, c.minutes, c.seconds) if p != c.period { t.Errorf("%d,%d,%d gives %#v, want %#v", c.years, c.months, c.days, p, c.period) } @@ -127,15 +209,17 @@ func TestPeriodFormat(t *testing.T) { {"P4D", "4 days"}, {"-P4D", "4 days"}, {"P1Y1M8D", "1 year, 1 month, 1 week, 1 day"}, - {"P3Y6M39D", "3 years, 6 months, 5 weeks, 4 days"}, - {"-P3Y6M39D", "3 years, 6 months, 5 weeks, 4 days"}, + {"PT1H1M1S", "1 hour, 1 minute, 1 second"}, + {"P1Y1M8DT1H1M1S", "1 year, 1 month, 1 week, 1 day, 1 hour, 1 minute, 1 second"}, + {"P3Y6M39DT2H7M9S", "3 years, 6 months, 5 weeks, 4 days, 2 hours, 7 minutes, 9 seconds"}, + {"-P3Y6M39DT2H7M9S", "3 years, 6 months, 5 weeks, 4 days, 2 hours, 7 minutes, 9 seconds"}, {"P1.1Y", "1.1 years"}, {"P2.5Y", "2.5 years"}, - {"P2.15Y", "2.15 years"}, - {"P2.125Y", "2.125 years"}, + {"P2.15Y", "2.1 years"}, + {"P2.125Y", "2.1 years"}, } for _, c := range cases { - s := MustParsePeriod(c.period).Format() + s := MustParse(c.period).Format() if s != c.expect { t.Errorf("Format() == %s, want %s for %+v", s, c.expect, c.period) } @@ -160,16 +244,17 @@ func TestPeriodFormatWithoutWeeks(t *testing.T) { {"P1D", "1 day"}, {"P4D", "4 days"}, {"-P4D", "4 days"}, - {"P1Y1M1D", "1 year, 1 month, 1 day"}, - {"P3Y6M39D", "3 years, 6 months, 39 days"}, - {"-P3Y6M39D", "3 years, 6 months, 39 days"}, + {"P1Y1M1DT1H1M1S", "1 year, 1 month, 1 day, 1 hour, 1 minute, 1 second"}, + {"P3Y6M39DT2H7M9S", "3 years, 6 months, 39 days, 2 hours, 7 minutes, 9 seconds"}, + {"-P3Y6M39DT2H7M9S", "3 years, 6 months, 39 days, 2 hours, 7 minutes, 9 seconds"}, {"P1.1Y", "1.1 years"}, {"P2.5Y", "2.5 years"}, - {"P2.15Y", "2.15 years"}, - {"P2.125Y", "2.125 years"}, + {"P2.15Y", "2.1 years"}, + {"P2.125Y", "2.1 years"}, } for _, c := range cases { - s := MustParsePeriod(c.period).FormatWithPeriodNames(PeriodYearNames, PeriodMonthNames, plural.Plurals{}, PeriodDayNames) + s := MustParse(c.period).FormatWithPeriodNames(PeriodYearNames, PeriodMonthNames, plural.Plurals{}, PeriodDayNames, + PeriodHourNames, PeriodMinuteNames, PeriodSecondNames) if s != c.expect { t.Errorf("Format() == %s, want %s for %+v", s, c.expect, c.period) } diff --git a/period/marshal.go b/period/marshal.go index eaa50524..c87ad5f6 100644 --- a/period/marshal.go +++ b/period/marshal.go @@ -4,43 +4,17 @@ package period -import "errors" - // MarshalBinary implements the encoding.BinaryMarshaler interface. // This also provides support for gob encoding. -func (p Period) MarshalBinary() ([]byte, error) { - enc := []byte{ - byte(p.years >> 24), - byte(p.years >> 16), - byte(p.years >> 8), - byte(p.years), - byte(p.months >> 24), - byte(p.months >> 16), - byte(p.months >> 8), - byte(p.months), - byte(p.days >> 24), - byte(p.days >> 16), - byte(p.days >> 8), - byte(p.days), - } - return enc, nil +func (period Period) MarshalBinary() ([]byte, error) { + // binary method would take more space in many cases, so we simply use text + return period.MarshalText() } // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. // This also provides support for gob encoding. -func (p *Period) UnmarshalBinary(data []byte) error { - if len(data) == 0 { - return errors.New("Date.UnmarshalBinary: no data") - } - if len(data) != 12 { - return errors.New("Date.UnmarshalBinary: invalid length") - } - - p.years = int32(data[3]) | int32(data[2])<<8 | int32(data[1])<<16 | int32(data[0])<<24 - p.months = int32(data[7]) | int32(data[6])<<8 | int32(data[5])<<16 | int32(data[4])<<24 - p.days = int32(data[11]) | int32(data[10])<<8 | int32(data[9])<<16 | int32(data[8])<<24 - - return nil +func (period *Period) UnmarshalBinary(data []byte) error { + return period.UnmarshalText(data) } // MarshalText implements the encoding.TextMarshaler interface for Periods. @@ -50,7 +24,7 @@ func (period Period) MarshalText() ([]byte, error) { // UnmarshalText implements the encoding.TextUnmarshaler interface for Periods. func (period *Period) UnmarshalText(data []byte) (err error) { - u, err := ParsePeriod(string(data)) + u, err := Parse(string(data)) if err == nil { *period = u } diff --git a/period/marshal_test.go b/period/marshal_test.go index 5843b79d..f07b3f9d 100644 --- a/period/marshal_test.go +++ b/period/marshal_test.go @@ -21,11 +21,16 @@ func TestGobEncoding(t *testing.T) { "P1W", "P1M", "P1Y", + "PT1H", + "PT1M", + "PT1S", "P2Y3M4W5D", "-P2Y3M4W5D", + "P2Y3M4W5DT1H7M9S", + "-P2Y3M4W5DT1H7M9S", } for _, c := range cases { - period := MustParsePeriod(c) + period := MustParse(c) var p Period err := encoder.Encode(&period) if err != nil { @@ -46,12 +51,15 @@ func TestPeriodJSONMarshalling(t *testing.T) { value Period want string }{ - {NewPeriod(-11111, -123, -3), `"-P11111Y123M3D"`}, - {NewPeriod(-1, -12, -31), `"-P1Y12M31D"`}, - {NewPeriod(0, 0, 0), `"P0D"`}, - {NewPeriod(0, 0, 1), `"P1D"`}, - {NewPeriod(0, 1, 0), `"P1M"`}, - {NewPeriod(1, 0, 0), `"P1Y"`}, + {New(-1111, -123, -3, -11, -59, -59), `"-P1111Y123M3DT11H59M59S"`}, + {New(-1, -12, -31, -5, -4, -20), `"-P1Y12M31DT5H4M20S"`}, + {New(0, 0, 0, 0, 0, 0), `"P0D"`}, + {New(0, 0, 0, 0, 0, 1), `"PT1S"`}, + {New(0, 0, 0, 0, 1, 0), `"PT1M"`}, + {New(0, 0, 0, 1, 0, 0), `"PT1H"`}, + {New(0, 0, 1, 0, 0, 0), `"P1D"`}, + {New(0, 1, 0, 0, 0, 0), `"P1M"`}, + {New(1, 0, 0, 0, 0, 0), `"P1Y"`}, } for _, c := range cases { var p Period @@ -76,12 +84,15 @@ func TestPeriodTextMarshalling(t *testing.T) { value Period want string }{ - {NewPeriod(-11111, -123, -3), "-P11111Y123M3D"}, - {NewPeriod(-1, -12, -31), "-P1Y12M31D"}, - {NewPeriod(0, 0, 0), "P0D"}, - {NewPeriod(0, 0, 1), "P1D"}, - {NewPeriod(0, 1, 0), "P1M"}, - {NewPeriod(1, 0, 0), "P1Y"}, + {New(-1111, -123, -3, -11, -59, -59), "-P1111Y123M3DT11H59M59S"}, + {New(-1, -12, -31, -5, -4, -20), "-P1Y12M31DT5H4M20S"}, + {New(0, 0, 0, 0, 0, 0), "P0D"}, + {New(0, 0, 0, 0, 0, 1), "PT1S"}, + {New(0, 0, 0, 0, 1, 0), "PT1M"}, + {New(0, 0, 0, 1, 0, 0), "PT1H"}, + {New(0, 0, 1, 0, 0, 0), "P1D"}, + {New(0, 1, 0, 0, 0, 0), "P1M"}, + {New(1, 0, 0, 0, 0, 0), "P1Y"}, } for _, c := range cases { var p Period @@ -108,7 +119,7 @@ func TestInvalidPeriodText(t *testing.T) { }{ {``, `Cannot parse a blank string as a period.`}, {`not-a-period`, `Expected 'P' period mark at the start: not-a-period`}, - {`P000`, `Expected 'Y', 'M', 'W' or 'D' marker: P000`}, + {`P000`, `Expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' marker: P000`}, } for _, c := range cases { var p Period diff --git a/timespan/daterange_test.go b/timespan/daterange_test.go index ff43e125..40f17034 100644 --- a/timespan/daterange_test.go +++ b/timespan/daterange_test.go @@ -156,21 +156,21 @@ func TestExtendByNeg(t *testing.T) { } func TestShiftByPosPeriod(t *testing.T) { - dr := NewDateRange(d0327, d0402).ShiftByPeriod(period.NewPeriod(0, 0, 7)) + dr := NewDateRange(d0327, d0402).ShiftByPeriod(period.New(0, 0, 7, 0, 0, 0)) isEq(t, dr.Days(), PeriodOfDays(6)) isEq(t, dr.Start(), d0403) isEq(t, dr.Last(), d0408) } func TestShiftByNegPeriod(t *testing.T) { - dr := NewDateRange(d0403, d0408).ShiftByPeriod(period.NewPeriod(0, 0, -7)) + dr := NewDateRange(d0403, d0408).ShiftByPeriod(period.New(0, 0, -7, 0, 0, 0)) isEq(t, dr.Days(), PeriodOfDays(5)) isEq(t, dr.Start(), d0327) isEq(t, dr.Last(), d0331) } func TestExtendByPosPeriod(t *testing.T) { - dr := OneDayRange(d0327).ExtendByPeriod(period.NewPeriod(0, 0, 6)) + dr := OneDayRange(d0327).ExtendByPeriod(period.New(0, 0, 6, 0, 0, 0)) isEq(t, dr.Days(), PeriodOfDays(7)) isEq(t, dr.Start(), d0327) isEq(t, dr.Last(), d0402) @@ -179,7 +179,7 @@ func TestExtendByPosPeriod(t *testing.T) { } func TestExtendByNegPeriod(t *testing.T) { - dr := OneDayRange(d0327).ExtendByPeriod(period.NewPeriod(0, 0, -8)) + dr := OneDayRange(d0327).ExtendByPeriod(period.New(0, 0, -8, 0, 0, 0)) //fmt.Printf("\ndr=%#v\n", dr) isEq(t, dr.Days(), PeriodOfDays(7)) isEq(t, dr.Start(), d0320) From 8b5c29de6bd4d9b8882593b6f25754eff6294207 Mon Sep 17 00:00:00 2001 From: Rick Date: Sat, 6 Feb 2016 13:44:38 +0000 Subject: [PATCH 056/165] Period now has Add and Scale methods --- period/isoperiod.go | 27 ++++++++++++++++++++ period/isoperiod_test.go | 53 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/period/isoperiod.go b/period/isoperiod.go index 5133b970..aeda0f1f 100644 --- a/period/isoperiod.go +++ b/period/isoperiod.go @@ -353,6 +353,33 @@ func (period Period) Negate() Period { return Period{-period.years, -period.months, -period.days, -period.hours, -period.minutes, -period.seconds} } +// Add adds two periods together. +func (this Period) Add(that Period) Period { + return Period{ + this.years + that.years, + this.months + that.months, + this.days + that.days, + this.hours + that.hours, + this.minutes + that.minutes, + this.seconds + that.seconds, + } +} + +// Scale a period by a multiplication factor. Obviously, this can both enlarge and shrink it, +// and change the sign if negative. +// Bear in mind that the internal representation is limited by fixed-point arithmetic with one +// decimal place; each field only is int16. +func (this Period) Scale(factor float32) Period { + return Period{ + int16(float32(this.years) * factor), + int16(float32(this.months) * factor), + int16(float32(this.days) * factor), + int16(float32(this.hours) * factor), + int16(float32(this.minutes) * factor), + int16(float32(this.seconds) * factor), + } +} + // Sign returns +1 for positive periods and -1 for negative periods. func (period Period) Sign() int { if period.years < 0 || period.months < 0 || period.days < 0 || period.hours < 0 || period.minutes < 0 || period.seconds < 0 { diff --git a/period/isoperiod_test.go b/period/isoperiod_test.go index 21e96630..f8583543 100644 --- a/period/isoperiod_test.go +++ b/period/isoperiod_test.go @@ -260,3 +260,56 @@ func TestPeriodFormatWithoutWeeks(t *testing.T) { } } } + +func TestPeriodAdd(t *testing.T) { + cases := []struct { + one, two string + expect string + }{ + {"P0D", "P0D", "P0D"}, + {"P1D", "P1D", "P2D"}, + {"P1M", "P1M", "P2M"}, + {"P1Y", "P1Y", "P2Y"}, + {"PT1H", "PT1H", "PT2H"}, + {"PT1M", "PT1M", "PT2M"}, + {"PT1S", "PT1S", "PT2S"}, + {"P1Y2M3DT4H5M6S", "P6Y5M4DT3H2M1S", "P7Y7M7DT7H7M7S"}, + {"P7Y7M7DT7H7M7S", "-P7Y7M7DT7H7M7S", "P0D"}, + } + for _, c := range cases { + s := MustParse(c.one).Add(MustParse(c.two)) + if s != MustParse(c.expect) { + t.Errorf("%s.Add(%s) == %v, want %s", c.one, c.two, s, c.expect) + } + } +} + +func TestPeriodScale(t *testing.T) { + cases := []struct { + one string + m float32 + expect string + }{ + {"P0D", 2, "P0D"}, + {"P1D", 2, "P2D"}, + {"P1M", 2, "P2M"}, + {"P1Y", 2, "P2Y"}, + {"PT1H", 2, "PT2H"}, + {"PT1M", 2, "PT2M"}, + {"PT1S", 2, "PT2S"}, + {"P1D", 0.5, "P0.5D"}, + {"P1M", 0.5, "P0.5M"}, + {"P1Y", 0.5, "P0.5Y"}, + {"PT1H", 0.5, "PT0.5H"}, + {"PT1M", 0.5, "PT0.5M"}, + {"PT1S", 0.5, "PT0.5S"}, + {"P1Y2M3DT4H5M6S", 2, "P2Y4M6DT8H10M12S"}, + {"P2Y4M6DT8H10M12S", -0.5, "-P1Y2M3DT4H5M6S"}, + } + for _, c := range cases { + s := MustParse(c.one).Scale(c.m) + if s != MustParse(c.expect) { + t.Errorf("%s.Scale(%g) == %v, want %s", c.one, c.m, s, c.expect) + } + } +} From 058f15ca715bc67c8d0ca6bcd084faf67919e7c8 Mon Sep 17 00:00:00 2001 From: Rick Date: Sat, 6 Feb 2016 14:57:51 +0000 Subject: [PATCH 057/165] Split source code across several files --- period/format.go | 126 +++++++++ period/parse.go | 149 ++++++++++ period/{isoperiod.go => period.go} | 279 +------------------ period/{isoperiod_test.go => period_test.go} | 5 + 4 files changed, 283 insertions(+), 276 deletions(-) create mode 100644 period/format.go create mode 100644 period/parse.go rename period/{isoperiod.go => period.go} (51%) rename period/{isoperiod_test.go => period_test.go} (98%) diff --git a/period/format.go b/period/format.go new file mode 100644 index 00000000..1c604a49 --- /dev/null +++ b/period/format.go @@ -0,0 +1,126 @@ +package period + +import ( + "fmt" + . "github.com/rickb777/plural" + "strings" +) + +// Format converts the period to human-readable form using the default localisation. +func (period Period) Format() string { + return period.FormatWithPeriodNames(PeriodYearNames, PeriodMonthNames, PeriodWeekNames, PeriodDayNames, PeriodHourNames, PeriodMinuteNames, PeriodSecondNames) +} + +// FormatWithPeriodNames converts the period to human-readable form in a localisable way. +func (period Period) FormatWithPeriodNames(yearNames, monthNames, weekNames, dayNames, hourNames, minNames, secNames Plurals) string { + period = period.Abs() + + parts := make([]string, 0) + parts = appendNonBlank(parts, yearNames.FormatFloat(absFloat10(period.years))) + parts = appendNonBlank(parts, monthNames.FormatFloat(absFloat10(period.months))) + + if period.days > 0 || (period.IsZero()) { + if len(weekNames) > 0 { + weeks := period.days / 70 + mdays := period.days % 70 + //fmt.Printf("%v %#v - %d %d\n", period, period, weeks, mdays) + if weeks > 0 { + parts = appendNonBlank(parts, weekNames.FormatInt(int(weeks))) + } + if mdays > 0 || weeks == 0 { + parts = appendNonBlank(parts, dayNames.FormatFloat(absFloat10(mdays))) + } + } else { + parts = appendNonBlank(parts, dayNames.FormatFloat(absFloat10(period.days))) + } + } + parts = appendNonBlank(parts, hourNames.FormatFloat(absFloat10(period.hours))) + parts = appendNonBlank(parts, minNames.FormatFloat(absFloat10(period.minutes))) + parts = appendNonBlank(parts, secNames.FormatFloat(absFloat10(period.seconds))) + + return strings.Join(parts, ", ") +} + +func appendNonBlank(parts []string, s string) []string { + if s == "" { + return parts + } + return append(parts, s) +} + +// PeriodDayNames provides the English default format names for the days part of the period. +// This is a sequence of plurals where the first match is used, otherwise the last one is used. +// The last one must include a "%v" placeholder for the number. +var PeriodDayNames = Plurals{Case{0, "%v days"}, Case{1, "%v day"}, Case{2, "%v days"}} + +// PeriodWeekNames is as for PeriodDayNames but for weeks. +var PeriodWeekNames = Plurals{Case{0, ""}, Case{1, "%v week"}, Case{2, "%v weeks"}} + +// PeriodMonthNames is as for PeriodDayNames but for months. +var PeriodMonthNames = Plurals{Case{0, ""}, Case{1, "%v month"}, Case{2, "%g months"}} + +// PeriodYearNames is as for PeriodDayNames but for years. +var PeriodYearNames = Plurals{Case{0, ""}, Case{1, "%v year"}, Case{2, "%v years"}} + +// PeriodHourNames is as for PeriodDayNames but for hours. +var PeriodHourNames = Plurals{Case{0, ""}, Case{1, "%v hour"}, Case{2, "%v hours"}} + +// PeriodMinuteNames is as for PeriodDayNames but for minutes. +var PeriodMinuteNames = Plurals{Case{0, ""}, Case{1, "%v minute"}, Case{2, "%v minutes"}} + +// PeriodSecondNames is as for PeriodDayNames but for seconds. +var PeriodSecondNames = Plurals{Case{0, ""}, Case{1, "%v second"}, Case{2, "%v seconds"}} + +// String converts the period to -8601 form. +func (period Period) String() string { + if period.IsZero() { + return "P0D" + } + + s := "" + if period.Sign() < 0 { + s = "-" + } + + y, m, w, d, t, hh, mm, ss := "", "", "", "", "", "", "", "" + + if period.years != 0 { + y = fmt.Sprintf("%gY", absFloat10(period.years)) + } + if period.months != 0 { + m = fmt.Sprintf("%gM", absFloat10(period.months)) + } + if period.days != 0 { + //days := absInt32(period.days) + //weeks := days / 7 + //if (weeks >= 10) { + // w = fmt.Sprintf("%gW", absFloat(weeks)) + //} + //mdays := days % 7 + if period.days != 0 { + d = fmt.Sprintf("%gD", absFloat10(period.days)) + } + } + if period.hours != 0 || period.minutes != 0 || period.seconds != 0 { + t = "T" + } + if period.hours != 0 { + hh = fmt.Sprintf("%gH", absFloat10(period.hours)) + } + if period.minutes != 0 { + mm = fmt.Sprintf("%gM", absFloat10(period.minutes)) + } + if period.seconds != 0 { + ss = fmt.Sprintf("%gS", absFloat10(period.seconds)) + } + + return fmt.Sprintf("%sP%s%s%s%s%s%s%s%s", s, y, m, w, d, t, hh, mm, ss) +} + +func absFloat10(v int16) float32 { + f := float32(v) / 10 + if v < 0 { + return -f + } + return f +} diff --git a/period/parse.go b/period/parse.go new file mode 100644 index 00000000..98c78c18 --- /dev/null +++ b/period/parse.go @@ -0,0 +1,149 @@ +package period + +import ( + "fmt" + "strconv" + "strings" +) + +// MustParse is as per Parse except that it panics if the string cannot be parsed. +// This is intended for setup code; don't use it for user inputs. +func MustParse(value string) Period { + d, err := Parse(value) + if err != nil { + panic(err) + } + return d +} + +// Parse parses strings that specify periods using ISO-8601 rules. +// +// In addition, a plus or minus sign can precede the period, e.g. "-P10D" +// +// The zero value can be represented in several ways: all of the following +// are equivalent: "P0Y", "P0M", "P0W", "P0D", "PT0H", PT0M", PT0S", and "P0". +// The canonical zero is "P0D". +func Parse(period string) (Period, error) { + if period == "" { + return Period{}, fmt.Errorf("Cannot parse a blank string as a period.") + } + + if period == "P0" { + return Period{}, nil + } + + pcopy := period + negate := false + if pcopy[0] == '-' { + negate = true + pcopy = pcopy[1:] + } else if pcopy[0] == '+' { + pcopy = pcopy[1:] + } + + if pcopy[0] != 'P' { + return Period{}, fmt.Errorf("Expected 'P' period mark at the start: %s", period) + } + pcopy = pcopy[1:] + + result := Period{} + + st := parseState{period, pcopy, false, nil} + t := strings.IndexByte(pcopy, 'T') + if t >= 0 { + st.pcopy = pcopy[t+1:] + + result.hours, st = parseField(st, 'H') + if st.err != nil { + return Period{}, st.err + } + + result.minutes, st = parseField(st, 'M') + if st.err != nil { + return Period{}, st.err + } + + result.seconds, st = parseField(st, 'S') + if st.err != nil { + return Period{}, st.err + } + + st.pcopy = pcopy[:t] + } + + result.years, st = parseField(st, 'Y') + if st.err != nil { + return Period{}, st.err + } + + result.months, st = parseField(st, 'M') + if st.err != nil { + return Period{}, st.err + } + + weeks, st := parseField(st, 'W') + if st.err != nil { + return Period{}, st.err + } + + days, st := parseField(st, 'D') + if st.err != nil { + return Period{}, st.err + } + + result.days = weeks*7 + days + //fmt.Printf("%#v\n", st) + + if !st.ok { + return Period{}, fmt.Errorf("Expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' marker: %s", period) + } + if negate { + return result.Negate(), nil + } + return result, nil +} + +type parseState struct { + period, pcopy string + ok bool + err error +} + +func parseField(st parseState, mark byte) (int16, parseState) { + //fmt.Printf("%c %#v\n", mark, st) + r := int16(0) + m := strings.IndexByte(st.pcopy, mark) + if m > 0 { + r, st.err = parseDecimalFixedPoint(st.pcopy[:m], st.period) + if st.err != nil { + return 0, st + } + st.pcopy = st.pcopy[m+1:] + st.ok = true + } + return r, st +} + +// Fixed-point three decimal places +func parseDecimalFixedPoint(s, original string) (int16, error) { + //was := s + dec := strings.IndexByte(s, '.') + if dec < 0 { + dec = strings.IndexByte(s, ',') + } + + if dec >= 0 { + dp := len(s) - dec + if dp > 1 { + s = s[:dec] + s[dec+1:dec+2] + } else { + s = s[:dec] + s[dec+1:] + "0" + } + } else { + s = s + "0" + } + + n, e := strconv.ParseInt(s, 10, 16) + //fmt.Printf("ParseInt(%s) = %d -- from %s in %s %d\n", s, n, was, original, dec) + return int16(n), e +} diff --git a/period/isoperiod.go b/period/period.go similarity index 51% rename from period/isoperiod.go rename to period/period.go index aeda0f1f..f3a954bd 100644 --- a/period/isoperiod.go +++ b/period/period.go @@ -2,9 +2,6 @@ package period import ( "fmt" - . "github.com/rickb777/plural" - "strconv" - "strings" "time" ) @@ -54,147 +51,6 @@ func New(years, months, days, hours, minutes, seconds int) Period { years, months, days, hours, minutes, seconds)) } -// MustParse is as per Parse except that it panics if the string cannot be parsed. -// This is intended for setup code; don't use it for user inputs. -func MustParse(value string) Period { - d, err := Parse(value) - if err != nil { - panic(err) - } - return d -} - -// Parse parses strings that specify periods using ISO-8601 rules. -// -// In addition, a plus or minus sign can precede the period, e.g. "-P10D" -// -// The zero value can be represented in several ways: all of the following -// are equivalent: "P0Y", "P0M", "P0W", "P0D", and "P0". -func Parse(period string) (Period, error) { - if period == "" { - return Period{}, fmt.Errorf("Cannot parse a blank string as a period.") - } - - if period == "P0" { - return Period{}, nil - } - - pcopy := period - negate := false - if pcopy[0] == '-' { - negate = true - pcopy = pcopy[1:] - } else if pcopy[0] == '+' { - pcopy = pcopy[1:] - } - - if pcopy[0] != 'P' { - return Period{}, fmt.Errorf("Expected 'P' period mark at the start: %s", period) - } - pcopy = pcopy[1:] - - result := Period{} - - st := parseState{period, pcopy, false, nil} - t := strings.IndexByte(pcopy, 'T') - if t >= 0 { - st.pcopy = pcopy[t+1:] - - result.hours, st = parseField(st, 'H') - if st.err != nil { - return Period{}, st.err - } - - result.minutes, st = parseField(st, 'M') - if st.err != nil { - return Period{}, st.err - } - - result.seconds, st = parseField(st, 'S') - if st.err != nil { - return Period{}, st.err - } - - st.pcopy = pcopy[:t] - } - - result.years, st = parseField(st, 'Y') - if st.err != nil { - return Period{}, st.err - } - - result.months, st = parseField(st, 'M') - if st.err != nil { - return Period{}, st.err - } - - weeks, st := parseField(st, 'W') - if st.err != nil { - return Period{}, st.err - } - - days, st := parseField(st, 'D') - if st.err != nil { - return Period{}, st.err - } - - result.days = weeks*7 + days - //fmt.Printf("%#v\n", st) - - if !st.ok { - return Period{}, fmt.Errorf("Expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' marker: %s", period) - } - if negate { - return result.Negate(), nil - } - return result, nil -} - -type parseState struct { - period, pcopy string - ok bool - err error -} - -func parseField(st parseState, mark byte) (int16, parseState) { - //fmt.Printf("%c %#v\n", mark, st) - r := int16(0) - m := strings.IndexByte(st.pcopy, mark) - if m > 0 { - r, st.err = parseDecimalFixedPoint(st.pcopy[:m], st.period) - if st.err != nil { - return 0, st - } - st.pcopy = st.pcopy[m+1:] - st.ok = true - } - return r, st -} - -// Fixed-point three decimal places -func parseDecimalFixedPoint(s, original string) (int16, error) { - //was := s - dec := strings.IndexByte(s, '.') - if dec < 0 { - dec = strings.IndexByte(s, ',') - } - - if dec >= 0 { - dp := len(s) - dec - if dp > 1 { - s = s[:dec] + s[dec+1:dec+2] - } else { - s = s[:dec] + s[dec+1:] + "0" - } - } else { - s = s + "0" - } - - n, e := strconv.ParseInt(s, 10, 16) - //fmt.Printf("ParseInt(%s) = %d -- from %s in %s %d\n", s, n, was, original, dec) - return int16(n), e -} - // IsZero returns true if applied to a zero-length period. func (period Period) IsZero() bool { return period == Period{} @@ -206,135 +62,6 @@ func (period Period) IsNegative() bool { return period.years < 0 || period.months < 0 || period.days < 0 } -// IsPrecise returns true for all values with no year or month component. This holds -// true even if the week or days component is large. -// -// For values where this method returns false, the imprecision arises because the -// number of days per month varies in the Gregorian calendar and the number of -// days per year is different for leap years. -//func (d Period) IsPrecise() bool { -// return d.years == 0 && d.months == 0 -//} - -// Format converts the period to human-readable form using the default localisation. -func (period Period) Format() string { - return period.FormatWithPeriodNames(PeriodYearNames, PeriodMonthNames, PeriodWeekNames, PeriodDayNames, PeriodHourNames, PeriodMinuteNames, PeriodSecondNames) -} - -// FormatWithPeriodNames converts the period to human-readable form in a localisable way. -func (period Period) FormatWithPeriodNames(yearNames, monthNames, weekNames, dayNames, hourNames, minNames, secNames Plurals) string { - period = period.Abs() - - parts := make([]string, 0) - parts = appendNonBlank(parts, yearNames.FormatFloat(absFloat10(period.years))) - parts = appendNonBlank(parts, monthNames.FormatFloat(absFloat10(period.months))) - - if period.days > 0 || (period.IsZero()) { - if len(weekNames) > 0 { - weeks := period.days / 70 - mdays := period.days % 70 - //fmt.Printf("%v %#v - %d %d\n", period, period, weeks, mdays) - if weeks > 0 { - parts = appendNonBlank(parts, weekNames.FormatInt(int(weeks))) - } - if mdays > 0 || weeks == 0 { - parts = appendNonBlank(parts, dayNames.FormatFloat(absFloat10(mdays))) - } - } else { - parts = appendNonBlank(parts, dayNames.FormatFloat(absFloat10(period.days))) - } - } - parts = appendNonBlank(parts, hourNames.FormatFloat(absFloat10(period.hours))) - parts = appendNonBlank(parts, minNames.FormatFloat(absFloat10(period.minutes))) - parts = appendNonBlank(parts, secNames.FormatFloat(absFloat10(period.seconds))) - - return strings.Join(parts, ", ") -} - -func appendNonBlank(parts []string, s string) []string { - if s == "" { - return parts - } - return append(parts, s) -} - -// PeriodDayNames provides the English default format names for the days part of the period. -// This is a sequence of plurals where the first match is used, otherwise the last one is used. -// The last one must include a "%g" placeholder for the number. -var PeriodDayNames = Plurals{Case{0, "%v days"}, Case{1, "%v day"}, Case{2, "%v days"}} - -// PeriodWeekNames is as for PeriodDayNames but for weeks. -var PeriodWeekNames = Plurals{Case{0, ""}, Case{1, "%v week"}, Case{2, "%v weeks"}} - -// PeriodMonthNames is as for PeriodDayNames but for months. -var PeriodMonthNames = Plurals{Case{0, ""}, Case{1, "%g month"}, Case{2, "%g months"}} - -// PeriodYearNames is as for PeriodDayNames but for years. -var PeriodYearNames = Plurals{Case{0, ""}, Case{1, "%g year"}, Case{2, "%g years"}} - -// PeriodHourNames is as for PeriodDayNames but for hours. -var PeriodHourNames = Plurals{Case{0, ""}, Case{1, "%v hour"}, Case{2, "%v hours"}} - -// PeriodMinuteNames is as for PeriodDayNames but for minutes. -var PeriodMinuteNames = Plurals{Case{0, ""}, Case{1, "%g minute"}, Case{2, "%g minutes"}} - -// PeriodSecondNames is as for PeriodDayNames but for seconds. -var PeriodSecondNames = Plurals{Case{0, ""}, Case{1, "%g second"}, Case{2, "%g seconds"}} - -// String converts the period to -8601 form. -func (period Period) String() string { - if period.IsZero() { - return "P0D" - } - - s := "" - if period.Sign() < 0 { - s = "-" - } - - y, m, w, d, t, hh, mm, ss := "", "", "", "", "", "", "", "" - - if period.years != 0 { - y = fmt.Sprintf("%gY", absFloat10(period.years)) - } - if period.months != 0 { - m = fmt.Sprintf("%gM", absFloat10(period.months)) - } - if period.days != 0 { - //days := absInt32(period.days) - //weeks := days / 7 - //if (weeks >= 10) { - // w = fmt.Sprintf("%gW", absFloat(weeks)) - //} - //mdays := days % 7 - if period.days != 0 { - d = fmt.Sprintf("%gD", absFloat10(period.days)) - } - } - if period.hours != 0 || period.minutes != 0 || period.seconds != 0 { - t = "T" - } - if period.hours != 0 { - hh = fmt.Sprintf("%gH", absFloat10(period.hours)) - } - if period.minutes != 0 { - mm = fmt.Sprintf("%gM", absFloat10(period.minutes)) - } - if period.seconds != 0 { - ss = fmt.Sprintf("%gS", absFloat10(period.seconds)) - } - - return fmt.Sprintf("%sP%s%s%s%s%s%s%s%s", s, y, m, w, d, t, hh, mm, ss) -} - -func absFloat10(v int16) float32 { - f := float32(v) / 10 - if v < 0 { - return -f - } - return f -} - // Abs converts a negative period to a positive one. func (period Period) Abs() Period { return Period{absInt16(period.years), absInt16(period.months), absInt16(period.days), @@ -368,7 +95,7 @@ func (this Period) Add(that Period) Period { // Scale a period by a multiplication factor. Obviously, this can both enlarge and shrink it, // and change the sign if negative. // Bear in mind that the internal representation is limited by fixed-point arithmetic with one -// decimal place; each field only is int16. +// decimal place; each field is only int16. func (this Period) Scale(factor float32) Period { return Period{ int16(float32(this.years) * factor), @@ -480,8 +207,8 @@ func (period Period) SecondsFloat() float32 { // Duration converts a period to the equivalent duration in nanoseconds. // A flag is also returned that is true when the conversion was precise and false otherwise. // When the period specifies years, months and days, it is impossible to be precise, so -// the duration is calculated on the basis of a year being 365.2 days and a month being -// 1/12 of a year; days are all 24 hours long. +// the duration is calculated on the basis of a year being 365.25 days and a month being +// 1/12 of a that; days are all 24 hours long. func (period Period) Duration() (time.Duration, bool) { // remember that the fields are all fixed-point 1E1 ydE6 := time.Duration(period.years) * 36525000 // 365.25 days diff --git a/period/isoperiod_test.go b/period/period_test.go similarity index 98% rename from period/isoperiod_test.go rename to period/period_test.go index f8583543..8f78ec72 100644 --- a/period/isoperiod_test.go +++ b/period/period_test.go @@ -12,7 +12,12 @@ func TestParsePeriod(t *testing.T) { period Period }{ {"P0", Period{}}, + {"P0Y", Period{}}, + {"P0M", Period{}}, {"P0D", Period{}}, + {"PT0H", Period{}}, + {"PT0M", Period{}}, + {"PT0S", Period{}}, {"P3Y", Period{30, 0, 0, 0, 0, 0}}, {"P6M", Period{0, 60, 0, 0, 0, 0}}, {"P5W", Period{0, 0, 350, 0, 0, 0}}, From 5beca004fbb6dc8e34016f1cef5b5dba1f954a5c Mon Sep 17 00:00:00 2001 From: Rick Date: Sat, 6 Feb 2016 15:22:45 +0000 Subject: [PATCH 058/165] More conversion and interfacing methods --- README.md | 2 ++ clock/clock.go | 19 +++++++++++-------- clock/clock_test.go | 19 +++++++++++++++++++ date.go | 3 ++- period/period.go | 12 ++++++++++++ period/period_test.go | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 78 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7fa173ff..d08dd9af 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ It also provides * `TimeSpan` which expresses a duration of time between two instants, and * `DateRange` which expresses a period between two dats. + * `Period` which expresses a period corresponding to the ISO-8601 form. + * `Clock` which expresses a wall-clock style hours-minutes-seconds. See [package documentation](https://godoc.org/github.com/rickb777/date) for full documentation and examples. diff --git a/clock/clock.go b/clock/clock.go index c41f2311..34328603 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -26,21 +26,17 @@ type Clock int32 // Common durations - second, minute, hour and day. const ( // Second is one second; it has a similar meaning to time.Second. - Second Clock = Clock(time.Second / time.Millisecond) - ClockSecond Clock = Clock(time.Second / time.Millisecond) + Second Clock = Clock(time.Second / time.Millisecond) // Minute is one minute; it has a similar meaning to time.Minute. - Minute Clock = Clock(time.Minute / time.Millisecond) - ClockMinute Clock = Clock(time.Minute / time.Millisecond) + Minute Clock = Clock(time.Minute / time.Millisecond) // Hour is one hour; it has a similar meaning to time.Hour. - Hour Clock = Clock(time.Hour / time.Millisecond) - ClockHour Clock = Clock(time.Hour / time.Millisecond) + Hour Clock = Clock(time.Hour / time.Millisecond) // Day is a fixed period of 24 hours. This does not take account of daylight savings, // so is not fully general. - Day Clock = Clock(time.Hour * 24 / time.Millisecond) - ClockDay Clock = Clock(time.Hour * 24 / time.Millisecond) + Day Clock = Clock(time.Hour * 24 / time.Millisecond) ) // Midnight is the zero value of a Clock. @@ -91,6 +87,13 @@ func (c Clock) Add(h, m, s, ms int) Clock { return c + hx + mx + sx + Clock(ms) } +// AddDuration returns a new Clock offset from this clock by a duration. +// The parameters can be negative. +// If required, use Mod24() to correct any overflow or underflow. +func (c Clock) AddDuration(d time.Duration) Clock { + return c + Clock(d/time.Millisecond) +} + // IsInOneDay tests whether a clock time is in the range 0 to 24 hours, inclusive. Inside this // range, a Clock is generally well-behaved. But outside it, there may be errors due to daylight // savings. Note that 24:00:00 is included as a special case as per ISO-8601 definition of midnight. diff --git a/clock/clock_test.go b/clock/clock_test.go index fe0ba8aa..8affc1f3 100644 --- a/clock/clock_test.go +++ b/clock/clock_test.go @@ -95,6 +95,25 @@ func TestClockAdd(t *testing.T) { } } +func TestClockAddDuration(t *testing.T) { + cases := []struct { + d time.Duration + in, want Clock + }{ + {0, 2 * Hour, New(2, 0, 0, 0)}, + {1, 2 * Hour, New(2, 0, 0, 0)}, + {time.Millisecond, 2 * Hour, New(2, 0, 0, 1)}, + {-time.Second, 2 * Hour, New(1, 59, 59, 0)}, + {7 * time.Minute, 2 * Hour, New(2, 7, 0, 0)}, + } + for i, x := range cases { + got := x.in.AddDuration(x.d) + if got != x.want { + t.Errorf("%d: %d: got %v, want %v", i, x.d, got, x.want) + } + } +} + func TestClockIsMidnight(t *testing.T) { cases := []struct { in Clock diff --git a/date.go b/date.go index 9ed85a48..84922746 100644 --- a/date.go +++ b/date.go @@ -233,7 +233,8 @@ func (d Date) AddDate(years, months, days int) Date { } // AddPeriod returns the date corresponding to adding the given period. If the -// period's fields are be negative, this results in an earlier date. +// period's fields are be negative, this results in an earlier date. Any time +// component is ignored. // // See the description for AddDate. func (d Date) AddPeriod(period period.Period) Date { diff --git a/period/period.go b/period/period.go index f3a954bd..93166abe 100644 --- a/period/period.go +++ b/period/period.go @@ -62,6 +62,18 @@ func (period Period) IsNegative() bool { return period.years < 0 || period.months < 0 || period.days < 0 } +// OnlyYMD returns a new Period with only the year, month and day fields. The hour, +// minute and second fields are zeroed. +func (period Period) OnlyYMD() Period { + return Period{period.years, period.months, period.days, 0, 0, 0} +} + +// OnlyHMS returns a new Period with only the hour, minute and second fields. The year, +// month and day fields are zeroed. +func (period Period) OnlyHMS() Period { + return Period{0, 0, 0, period.hours, period.minutes, period.seconds} +} + // Abs converts a negative period to a positive one. func (period Period) Abs() Period { return Period{absInt16(period.years), absInt16(period.months), absInt16(period.days), diff --git a/period/period_test.go b/period/period_test.go index 8f78ec72..b6e48c8f 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -266,6 +266,38 @@ func TestPeriodFormatWithoutWeeks(t *testing.T) { } } +func TestPeriodOnlyYMD(t *testing.T) { + cases := []struct { + one string + expect string + }{ + {"P1Y2M3DT4H5M6S", "P1Y2M3D"}, + {"-P6Y5M4DT3H2M1S", "-P6Y5M4D"}, + } + for _, c := range cases { + s := MustParse(c.one).OnlyYMD() + if s != MustParse(c.expect) { + t.Errorf("%s.OnlyYMD() == %v, want %s", c.one, s, c.expect) + } + } +} + +func TestPeriodOnlyHMS(t *testing.T) { + cases := []struct { + one string + expect string + }{ + {"P1Y2M3DT4H5M6S", "PT4H5M6S"}, + {"-P6Y5M4DT3H2M1S", "-PT3H2M1S"}, + } + for _, c := range cases { + s := MustParse(c.one).OnlyHMS() + if s != MustParse(c.expect) { + t.Errorf("%s.OnlyHMS() == %v, want %s", c.one, s, c.expect) + } + } +} + func TestPeriodAdd(t *testing.T) { cases := []struct { one, two string From ce7b491a106cc7054165d085d9e4005063286e5f Mon Sep 17 00:00:00 2001 From: Rick Date: Sat, 6 Feb 2016 15:27:30 +0000 Subject: [PATCH 059/165] Documentation --- README.md | 4 ++-- doc.go | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d08dd9af..1680984e 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ and convenient for calendrical calculations and date parsing and formatting It also provides - * `TimeSpan` which expresses a duration of time between two instants, and - * `DateRange` which expresses a period between two dats. + * `DateRange` which expresses a period between two dates. + * `TimeSpan` which expresses a duration of time between two instants. * `Period` which expresses a period corresponding to the ISO-8601 form. * `Clock` which expresses a wall-clock style hours-minutes-seconds. diff --git a/doc.go b/doc.go index f0db1fdb..ba35b628 100644 --- a/doc.go +++ b/doc.go @@ -2,14 +2,21 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package date provides functionality for working with dates. Subpackages support -// clock-face time, spans of time, ranges of dates, and periods (as years, months, -// weeks and days). -// -// This package introduces a light-weight Date type that is storage-efficient -// and covenient for calendrical calculations and date parsing and formatting +// Package date provides functionality for working with dates. +// It implements a light-weight Date type that is storage-efficient +// and convenient for calendrical calculations and date parsing and formatting // (including years outside the [0,9999] interval). // +// Subpackages provide: +// +// * `DateRange` which expresses a period between two dates. +// +// * `TimeSpan` which expresses a duration of time between two instants. +// +// * `Period` which expresses a period corresponding to the ISO-8601 form. +// +// * `Clock` which expresses a wall-clock style hours-minutes-seconds. +// // Credits // // This package follows very closely the design of package time From 701aea3a0297c1fd9f3d2508edfb398935625366 Mon Sep 17 00:00:00 2001 From: Rick Date: Sat, 6 Feb 2016 20:57:41 +0000 Subject: [PATCH 060/165] Period has new Normalise and TotalDaysApprox methods --- period/period.go | 62 ++++++++++++++++++++++++++++++++++++++++--- period/period_test.go | 54 ++++++++++++++++++++++++++++++++++--- 2 files changed, 108 insertions(+), 8 deletions(-) diff --git a/period/period.go b/period/period.go index 93166abe..c0e5b11a 100644 --- a/period/period.go +++ b/period/period.go @@ -223,10 +223,7 @@ func (period Period) SecondsFloat() float32 { // 1/12 of a that; days are all 24 hours long. func (period Period) Duration() (time.Duration, bool) { // remember that the fields are all fixed-point 1E1 - ydE6 := time.Duration(period.years) * 36525000 // 365.25 days - mdE6 := time.Duration(period.months) * 3043750 // 30.437 days - ddE6 := time.Duration(period.days) * 100000 - tdE6 := (ydE6 + mdE6 + ddE6) * 86400 + tdE6 := time.Duration(totalDaysApproxE6(period)) * 86400 hhE3 := time.Duration(period.hours) * 360000 mmE3 := time.Duration(period.minutes) * 6000 ssE3 := time.Duration(period.seconds) * 100 @@ -234,3 +231,60 @@ func (period Period) Duration() (time.Duration, bool) { stE3 := hhE3 + mmE3 + ssE3 return tdE6*time.Microsecond + stE3*time.Millisecond, tdE6 == 0 } + +// TotalDaysApprox gets the approximate total number of days in the period. The approximation assumes +// a year is 365.25 days and a month is 1/12 of that. +// The result does not include any time field. +func totalDaysApproxE6(period Period) int64 { + // remember that the fields are all fixed-point 1E1 + ydE6 := int64(period.years) * 36525000 // 365.25 days + mdE6 := int64(period.months) * 3043750 // 30.437 days + ddE6 := int64(period.days) * 100000 + return ydE6 + mdE6 + ddE6 +} + +// TotalDaysApprox gets the approximate total number of days in the period. The approximation assumes +// a year is 365.25 days and a month is 1/12 of that. +// The result does not include any time field. +func (period Period) TotalDaysApprox() int { + tdE6 := totalDaysApproxE6(period.Normalise(false)) + return int(tdE6 / 1000000) +} + +// Normalise attempts to simplify the fields. It operates in either precise or imprecise mode. +// +// In precise mode: +// Multiples of 60 seconds become minutes. +// Multiples of 60 minutes become hours. +// Multiples of 12 months become years. + +// Addtionally, in imprecise mode: +// Multiples of 24 hours become days. +// Multiples of 30.4 days become months. +func (period Period) Normalise(precise bool) Period { + // remember that the fields are all fixed-point 1E1 + s := period.Sign() + p := period.Abs() + + p.minutes += (p.seconds / 600) * 10 + p.seconds = p.seconds % 600 + + p.hours += (p.minutes / 600) * 10 + p.minutes = p.minutes % 600 + + if !precise { + p.days += (p.hours / 240) * 10 + p.hours = p.hours % 240 + + p.months += (p.days / 304) * 10 + p.days = p.days % 304 + } + + p.years += (p.months / 120) * 10 + p.months = p.months % 120 + + if s < 0 { + return p.Negate() + } + return p +} diff --git a/period/period_test.go b/period/period_test.go index b6e48c8f..ab9ce478 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -148,12 +148,35 @@ func TestPeriodToDuration(t *testing.T) { {"-P1Y", -time.Duration(31557600 * time.Second), false}, } for _, c := range cases { - s, p := MustParse(c.value).Duration() + p := MustParse(c.value) + s, prec := p.Duration() if s != c.duration { - t.Errorf("Duration() == %s %v, want %s for %+v", s, p, c.duration, c.value) + t.Errorf("Duration() == %s %v, want %s for %+v", s, prec, c.duration, c.value) + } + if prec != c.precise { + t.Errorf("Duration() == %s %v, want %v for %+v", s, prec, c.precise, c.value) } - if p != c.precise { - t.Errorf("Duration() == %s %v, want %v for %+v", s, p, c.precise, c.value) + } +} + +func TestPeriodApproxDays(t *testing.T) { + cases := []struct { + value string + approxDays int + }{ + {"P0D", 0}, + {"PT24H", 1}, + {"PT49H", 2}, + {"P1D", 1}, + {"P1M", 30}, + {"P1Y", 365}, + {"-P1Y", -365}, + } + for _, c := range cases { + p := MustParse(c.value) + td := p.TotalDaysApprox() + if td != c.approxDays { + t.Errorf("%v.TotalDaysApprox() == %v, want %v", p, td, c.approxDays) } } } @@ -195,6 +218,29 @@ func TestNewPeriod(t *testing.T) { } } +func TestNormalise(t *testing.T) { + cases := []struct { + source, expected Period + precise bool + }{ + {New(0, 0, 0, 0, 0, 0), New(0, 0, 0, 0, 0, 0), true}, + {New(0, 12, 0, 0, 0, 60), New(1, 0, 0, 0, 1, 0), true}, + {New(0, 25, 0, 0, 61, 65), New(2, 1, 0, 1, 2, 5), true}, + {New(0, 0, 31, 0, 0, 0), New(0, 0, 31, 0, 0, 0), true}, + {New(0, 0, 29, 0, 0, 0), New(0, 0, 29, 0, 0, 0), false}, + {New(0, 0, 31, 0, 0, 0), Period{0, 10, 6, 0, 0, 0}, false}, + {New(0, 0, 61, 0, 0, 0), Period{0, 20, 2, 0, 0, 0}, false}, + {New(0, 11, 30, 23, 59, 60), Period{10, 0, 6, 0, 0, 0}, false}, + {New(0, 11, 30, 23, 59, 60).Negate(), Period{10, 0, 6, 0, 0, 0}.Negate(), false}, + } + for _, c := range cases { + n := c.source.Normalise(c.precise) + if n != c.expected { + t.Errorf("%v.Normalise(%v) gives %v %#v, want %v", c.source, c.precise, n, n, c.expected) + } + } +} + func TestPeriodFormat(t *testing.T) { cases := []struct { period string From 659d28c1b41def4edbff17df8c4077038f0dccaa Mon Sep 17 00:00:00 2001 From: Rick Date: Sat, 13 Feb 2016 14:04:28 +0000 Subject: [PATCH 061/165] Added TotalMonthsApprox method --- period/period.go | 17 ++++++++++++----- period/period_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/period/period.go b/period/period.go index c0e5b11a..358062e0 100644 --- a/period/period.go +++ b/period/period.go @@ -232,9 +232,6 @@ func (period Period) Duration() (time.Duration, bool) { return tdE6*time.Microsecond + stE3*time.Millisecond, tdE6 == 0 } -// TotalDaysApprox gets the approximate total number of days in the period. The approximation assumes -// a year is 365.25 days and a month is 1/12 of that. -// The result does not include any time field. func totalDaysApproxE6(period Period) int64 { // remember that the fields are all fixed-point 1E1 ydE6 := int64(period.years) * 36525000 // 365.25 days @@ -244,13 +241,23 @@ func totalDaysApproxE6(period Period) int64 { } // TotalDaysApprox gets the approximate total number of days in the period. The approximation assumes -// a year is 365.25 days and a month is 1/12 of that. -// The result does not include any time field. +// a year is 365.25 days and a month is 1/12 of that. Whole multiples of 24 hours are also included +// in the calculation. func (period Period) TotalDaysApprox() int { tdE6 := totalDaysApproxE6(period.Normalise(false)) return int(tdE6 / 1000000) } +// TotalMonthsApprox gets the approximate total number of months in the period. The days component +// is included by approximately assumes a year is 365.25 days and a month is 1/12 of that. +// Whole multiples of 24 hours are also included in the calculation. +func (period Period) TotalMonthsApprox() int { + p := period.Normalise(false) + mE1 := int(p.years)*12 + int(p.months) + dE6 := int64(p.days) * 100000 / 3043750 // 30.437 days per month + return (mE1 + int(dE6)) / 10 +} + // Normalise attempts to simplify the fields. It operates in either precise or imprecise mode. // // In precise mode: diff --git a/period/period_test.go b/period/period_test.go index ab9ce478..9a3307a4 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -181,6 +181,32 @@ func TestPeriodApproxDays(t *testing.T) { } } +func TestPeriodApproxMonths(t *testing.T) { + cases := []struct { + value string + approxMonths int + }{ + {"P0D", 0}, + {"P1D", 0}, + {"P30D", 0}, + {"P31D", 1}, + {"P1M", 1}, + {"P2M31D", 3}, + {"P1Y", 12}, + {"P2Y3M", 27}, + {"-P1Y", -12}, + {"PT24H", 0}, + {"PT744H", 1}, + } + for _, c := range cases { + p := MustParse(c.value) + td := p.TotalMonthsApprox() + if td != c.approxMonths { + t.Errorf("%v.TotalMonthsApprox() == %v, want %v", p, td, c.approxMonths) + } + } +} + func TestNewPeriod(t *testing.T) { cases := []struct { years, months, days, hours, minutes, seconds int From 920c6041f9394365051faf42f35f1ad8d5bc8731 Mon Sep 17 00:00:00 2001 From: Rick Date: Mon, 11 Apr 2016 21:14:50 +0100 Subject: [PATCH 062/165] Added NewOf(Duration) method to allow easier construction from the time package. --- period/format.go | 6 --- period/period.go | 87 ++++++++++++++++++++++---------- period/period_test.go | 115 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 169 insertions(+), 39 deletions(-) diff --git a/period/format.go b/period/format.go index 1c604a49..68205853 100644 --- a/period/format.go +++ b/period/format.go @@ -91,12 +91,6 @@ func (period Period) String() string { m = fmt.Sprintf("%gM", absFloat10(period.months)) } if period.days != 0 { - //days := absInt32(period.days) - //weeks := days / 7 - //if (weeks >= 10) { - // w = fmt.Sprintf("%gW", absFloat(weeks)) - //} - //mdays := days % 7 if period.days != 0 { d = fmt.Sprintf("%gD", absFloat10(period.days)) } diff --git a/period/period.go b/period/period.go index 358062e0..c36e4f47 100644 --- a/period/period.go +++ b/period/period.go @@ -5,6 +5,11 @@ import ( "time" ) +const daysPerYearApproxE3 = 365250 // 365.25 days +const daysPerMonthApproxE4 = 304375 // 30.437 days per month +const oneE5 = 100000 +const oneE6 = 1000000 + // Period holds a period of time and provides conversion to/from ISO-8601 representations. // In the ISO representation, decimal fractions are supported, although only the last non-zero // component is allowed to have a fraction according to the Standard. For example "P2.5Y" @@ -14,14 +19,15 @@ import ( // of integers with fixed point arithmetic. This avoids using float32 in the struct, so // there are no problems testing equality using ==. // -// The implementation limits the range of possible values to +/- 2^16 / 10. Note in -// particular that the range of years is limited to approximately +/- 3276. +// The implementation limits the range of possible values to ± 2^16 / 10. Note in +// particular that the range of years is limited to approximately ± 3276. // // The concept of weeks exists in string representations of periods, but otherwise weeks // are unimportant. The period contains a number of days from which the number of weeks can // be calculated when needed. -// Note that although fractional weeks can be parsed, they will never be returned. This is -// because the number of weeks is always computed as an integer from the number of days. +// +// Note that although fractional weeks can be parsed, they will never be returned via String(). +// This is because the number of weeks is always inferred from the number of days. // type Period struct { years, months, days, hours, minutes, seconds int16 @@ -47,10 +53,39 @@ func New(years, months, days, hours, minutes, seconds int) Period { return Period{int16(years) * 10, int16(months) * 10, int16(days) * 10, int16(hours) * 10, int16(minutes) * 10, int16(seconds) * 10} } - panic(fmt.Sprintf("Periods must have homogeneous signs; got P%dY%dM%dD%%dH%dM%dS", + panic(fmt.Sprintf("Periods must have homogeneous signs; got P%dY%dM%dDT%dH%dM%dS", years, months, days, hours, minutes, seconds)) } +// NewOf converts a time duration to a Period, and also indicates whether the conversion is precise. +// Any time duration that spans more than ± 3276 hours will be approximated by assuming that there +// are 24 hours per day, 30.4375 per month and 365.25 days per year. +func NewOf(duration time.Duration) (p Period, precise bool) { + sign := 1 + d := duration + if duration < 0 { + sign = -1 + d = -duration + } + + hours := int64(d / time.Hour) + + // check for 16-bit overflow + if hours > 3276 { + days := hours / 24 + years := (1000 * days) / daysPerYearApproxE3 + months := ((10000 * days) / daysPerMonthApproxE4) - (12 * years) + hours -= days * 24 + days = ((days * 10000) - (daysPerMonthApproxE4 * months) - (10 * daysPerYearApproxE3 * years)) / 10000 + return New(sign*int(years), sign*int(months), sign*int(days), sign*int(hours), 0, 0), false + } + + minutes := int64(d % time.Hour / time.Minute) + seconds := int64(d % time.Minute / time.Second) + + return New(0, 0, 0, sign*int(hours), sign*int(minutes), sign*int(seconds)), true +} + // IsZero returns true if applied to a zero-length period. func (period Period) IsZero() bool { return period == Period{} @@ -93,14 +128,14 @@ func (period Period) Negate() Period { } // Add adds two periods together. -func (this Period) Add(that Period) Period { +func (period Period) Add(that Period) Period { return Period{ - this.years + that.years, - this.months + that.months, - this.days + that.days, - this.hours + that.hours, - this.minutes + that.minutes, - this.seconds + that.seconds, + period.years + that.years, + period.months + that.months, + period.days + that.days, + period.hours + that.hours, + period.minutes + that.minutes, + period.seconds + that.seconds, } } @@ -108,14 +143,14 @@ func (this Period) Add(that Period) Period { // and change the sign if negative. // Bear in mind that the internal representation is limited by fixed-point arithmetic with one // decimal place; each field is only int16. -func (this Period) Scale(factor float32) Period { +func (period Period) Scale(factor float32) Period { return Period{ - int16(float32(this.years) * factor), - int16(float32(this.months) * factor), - int16(float32(this.days) * factor), - int16(float32(this.hours) * factor), - int16(float32(this.minutes) * factor), - int16(float32(this.seconds) * factor), + int16(float32(period.years) * factor), + int16(float32(period.months) * factor), + int16(float32(period.days) * factor), + int16(float32(period.hours) * factor), + int16(float32(period.minutes) * factor), + int16(float32(period.seconds) * factor), } } @@ -234,9 +269,9 @@ func (period Period) Duration() (time.Duration, bool) { func totalDaysApproxE6(period Period) int64 { // remember that the fields are all fixed-point 1E1 - ydE6 := int64(period.years) * 36525000 // 365.25 days - mdE6 := int64(period.months) * 3043750 // 30.437 days - ddE6 := int64(period.days) * 100000 + ydE6 := int64(period.years) * (daysPerYearApproxE3 * 100) + mdE6 := int64(period.months) * (daysPerMonthApproxE4 * 10) + ddE6 := int64(period.days) * oneE5 return ydE6 + mdE6 + ddE6 } @@ -245,7 +280,7 @@ func totalDaysApproxE6(period Period) int64 { // in the calculation. func (period Period) TotalDaysApprox() int { tdE6 := totalDaysApproxE6(period.Normalise(false)) - return int(tdE6 / 1000000) + return int(tdE6 / oneE6) } // TotalMonthsApprox gets the approximate total number of months in the period. The days component @@ -254,7 +289,7 @@ func (period Period) TotalDaysApprox() int { func (period Period) TotalMonthsApprox() int { p := period.Normalise(false) mE1 := int(p.years)*12 + int(p.months) - dE6 := int64(p.days) * 100000 / 3043750 // 30.437 days per month + dE6 := int64(p.days) * 1000 / daysPerMonthApproxE4 return (mE1 + int(dE6)) / 10 } @@ -264,8 +299,8 @@ func (period Period) TotalMonthsApprox() int { // Multiples of 60 seconds become minutes. // Multiples of 60 minutes become hours. // Multiples of 12 months become years. - -// Addtionally, in imprecise mode: +// +// Additionally, in imprecise mode: // Multiples of 24 hours become days. // Multiples of 30.4 days become months. func (period Period) Normalise(precise bool) Period { diff --git a/period/period_test.go b/period/period_test.go index 9a3307a4..539c418c 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -6,6 +6,10 @@ import ( "time" ) +var oneDayApprox = 24 * time.Hour +var oneMonthApprox = 2629800 * time.Second +var oneYearApprox = 31557600 * time.Second + func TestParsePeriod(t *testing.T) { cases := []struct { value string @@ -139,13 +143,13 @@ func TestPeriodToDuration(t *testing.T) { precise bool }{ {"P0D", time.Duration(0), true}, - {"PT1S", time.Duration(1 * time.Second), true}, - {"PT1M", time.Duration(60 * time.Second), true}, - {"PT1H", time.Duration(3600 * time.Second), true}, - {"P1D", time.Duration(24 * time.Hour), false}, - {"P1M", time.Duration(2629800 * time.Second), false}, - {"P1Y", time.Duration(31557600 * time.Second), false}, - {"-P1Y", -time.Duration(31557600 * time.Second), false}, + {"PT1S", 1 * time.Second, true}, + {"PT1M", 60 * time.Second, true}, + {"PT1H", 3600 * time.Second, true}, + {"P1D", 24 * time.Hour, false}, + {"P1M", oneMonthApprox, false}, + {"P1Y", oneYearApprox, false}, + {"-P1Y", -oneYearApprox, false}, } for _, c := range cases { p := MustParse(c.value) @@ -244,6 +248,103 @@ func TestNewPeriod(t *testing.T) { } } +func TestNewHMS(t *testing.T) { + cases := []struct { + hours, minutes, seconds int + period Period + }{ + {0, 0, 0, Period{0, 0, 0, 0, 0, 0}}, + {0, 0, 1, Period{0, 0, 0, 0, 0, 10}}, + {0, 1, 0, Period{0, 0, 0, 0, 10, 0}}, + {1, 0, 0, Period{0, 0, 0, 10, 0, 0}}, + {0, 0, -1, Period{0, 0, 0, 0, 0, -10}}, + {0, -1, 0, Period{0, 0, 0, 0, -10, 0}}, + {-1, 0, 0, Period{0, 0, 0, -10, 0, 0}}, + } + for _, c := range cases { + p := NewHMS(c.hours, c.minutes, c.seconds) + if p != c.period { + t.Errorf("gives %#v, want %#v", p, c.period) + } + if p.Hours() != c.hours { + t.Errorf("%#v, got %d want %d", p, p.Years(), c.hours) + } + if p.Minutes() != c.minutes { + t.Errorf("%#v, got %d want %d", p, p.Months(), c.minutes) + } + if p.Seconds() != c.seconds { + t.Errorf("%#v, got %d want %d", p, p.Days(), c.seconds) + } + } +} + +func TestNewYMD(t *testing.T) { + cases := []struct { + years, months, days int + period Period + }{ + {0, 0, 0, Period{0, 0, 0, 0, 0, 0}}, + {0, 0, 1, Period{0, 0, 10, 0, 0, 0}}, + {0, 1, 0, Period{0, 10, 0, 0, 0, 0}}, + {1, 0, 0, Period{10, 0, 0, 0, 0, 0}}, + {100, 222, 700, Period{1000, 2220, 7000, 0, 0, 0}}, + {0, 0, -1, Period{0, 0, -10, 0, 0, 0}}, + {0, -1, 0, Period{0, -10, 0, 0, 0, 0}}, + {-1, 0, 0, Period{-10, 0, 0, 0, 0, 0}}, + } + for _, c := range cases { + p := NewYMD(c.years, c.months, c.days) + if p != c.period { + t.Errorf("%d,%d,%d gives %#v, want %#v", c.years, c.months, c.days, p, c.period) + } + if p.Years() != c.years { + t.Errorf("%#v, got %d want %d", p, p.Years(), c.years) + } + if p.Months() != c.months { + t.Errorf("%#v, got %d want %d", p, p.Months(), c.months) + } + if p.Days() != c.days { + t.Errorf("%#v, got %d want %d", p, p.Days(), c.days) + } + } +} + +func TestNewOf(t *testing.T) { + cases := []struct { + source time.Duration + expected Period + precise bool + }{ + {time.Second, Period{0, 0, 0, 0, 0, 10}, true}, + {time.Minute, Period{0, 0, 0, 0, 10, 0}, true}, + {time.Hour, Period{0, 0, 0, 10, 0, 0}, true}, + {time.Hour + time.Minute + time.Second, Period{0, 0, 0, 10, 10, 10}, true}, + {24*time.Hour + time.Minute + time.Second, Period{0, 0, 0, 240, 10, 10}, true}, + {300 * oneDayApprox, Period{0, 90, 260, 0, 0, 0}, false}, + {305 * oneDayApprox, Period{0, 100, 0, 0, 0, 0}, false}, + {305*oneDayApprox - time.Hour, Period{0, 90, 300, 230, 0, 0}, false}, + {36525 * oneDayApprox, Period{1000, 0, 0, 0, 0, 0}, false}, + {36525*oneDayApprox - time.Hour, Period{990, 110, 290, 230, 0, 0}, false}, + + {-time.Second, Period{0, 0, 0, 0, 0, -10}, true}, + {-time.Minute, Period{0, 0, 0, 0, -10, 0}, true}, + {-time.Hour, Period{0, 0, 0, -10, 0, 0}, true}, + {-time.Hour - time.Minute - time.Second, Period{0, 0, 0, -10, -10, -10}, true}, + {-300 * oneDayApprox, Period{0, -90, -260, 0, 0, 0}, false}, + {-305 * oneDayApprox, Period{0, -100, 0, 0, 0, 0}, false}, + {-36525 * oneDayApprox, Period{-1000, 0, 0, 0, 0, 0}, false}, + } + for _, c := range cases { + n, p := NewOf(c.source) + if n != c.expected { + t.Errorf("NewOf(%v) gives %v %#v, want %v", c.source, n, n, c.expected) + } + if p != c.precise { + t.Errorf("NewOf(%v) gives %v, want %v for %v", c.source, p, c.precise, c.expected) + } + } +} + func TestNormalise(t *testing.T) { cases := []struct { source, expected Period From fa25087c6c32a2d6e8a888c0d8b44680d41ff2b8 Mon Sep 17 00:00:00 2001 From: Rick Date: Mon, 11 Apr 2016 23:28:03 +0100 Subject: [PATCH 063/165] Added Between function to calculate the difference between two time.Time values. --- period/period.go | 76 +++++++++++++++++++++++++++++++++++++++++++ period/period_test.go | 23 +++++++++++++ 2 files changed, 99 insertions(+) diff --git a/period/period.go b/period/period.go index c36e4f47..bf0b0b2b 100644 --- a/period/period.go +++ b/period/period.go @@ -86,6 +86,82 @@ func NewOf(duration time.Duration) (p Period, precise bool) { return New(0, 0, 0, sign*int(hours), sign*int(minutes), sign*int(seconds)), true } +// Between converts the span between two times to a period. Based on the Gregorian conversion algorithms +// of `time.Time`, the resultant period is precise. +// +// Remember that the resultant period does not retain any knowledge of the calendar, so any subsequent +// computations applied to the period can only be precise if they concern either the date (year, month, +// day) part, or the clock (hour, minute, second) part, but not both. +func Between(t1, t2 time.Time) Period { + if t1.Location() != t2.Location() { + t2 = t2.In(t1.Location()) + } + + sign := 1 + if t2.Before(t1) { + t1, t2, sign = t2, t1, -1 + } + + year, month, day, hour, min, sec := timeDiff(t1, t2) + if sign < 0 { + return New(year, month, day, hour, min, sec).Negate() + } + return New(year, month, day, hour, min, sec) +} + +//func TimeDiff(t1, t2 time.Time) (year, month, day, hour, min, sec int) { +// if t1.Location() != t2.Location() { +// t2 = t2.In(t1.Location()) +// } +// if t1.After(t2) { +// t1, t2 = t2, t1 +// } +// return timeDiff(t1, t2) +//} + +func timeDiff(t1, t2 time.Time) (year, month, day, hour, min, sec int) { + y1, m1, d1 := t1.Date() + y2, m2, d2 := t2.Date() + + hh1, mm1, ss1 := t1.Clock() + hh2, mm2, ss2 := t2.Clock() + + year = int(y2 - y1) + month = int(m2 - m1) + day = int(d2 - d1) + hour = int(hh2 - hh1) + min = int(mm2 - mm1) + sec = int(ss2 - ss1) + //fmt.Printf("A) %d %d %d, %d %d %d\n", year, month, day, hour, min, sec) + + // Normalize negative values + if sec < 0 { + sec += 60 + min-- + } + if min < 0 { + min += 60 + hour-- + } + if hour < 0 { + hour += 24 + day-- + } + if day < 0 { + // days in month: + t := time.Date(y1, m1, 32, 0, 0, 0, 0, time.UTC) + day += 32 - t.Day() + month-- + } + if month < 0 { + month += 12 + year-- + } + + //fmt.Printf("B) %d %d %d, %d %d %d\n", year, month, day, hour, min, sec) + return +} + // IsZero returns true if applied to a zero-length period. func (period Period) IsZero() bool { return period == Period{} diff --git a/period/period_test.go b/period/period_test.go index 539c418c..a47b9791 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -345,6 +345,29 @@ func TestNewOf(t *testing.T) { } } +func TestBetween(t *testing.T) { + cases := []struct { + a, b time.Time + expected Period + }{ + {time.Now(), time.Now(), Period{0, 0, 0, 0, 0, 0}}, + {time.Date(2015, 5, 1, 0, 0, 0, 0, time.UTC), time.Date(2016, 6, 2, 1, 1, 1, 1, time.UTC), Period{10, 10, 10, 10, 10, 10}}, + {time.Date(2016, 1, 2, 0, 0, 0, 0, time.UTC), time.Date(2016, 2, 1, 0, 0, 0, 0, time.UTC), Period{0, 0, 300, 0, 0, 0}}, + {time.Date(2015, 2, 1, 0, 0, 0, 0, time.UTC), time.Date(2015, 3, 1, 0, 0, 0, 0, time.UTC), Period{0, 10, 0, 0, 0, 0}}, + {time.Date(2016, 2, 1, 0, 0, 0, 0, time.UTC), time.Date(2016, 3, 1, 0, 0, 0, 0, time.UTC), Period{0, 10, 0, 0, 0, 0}}, + {time.Date(2015, 2, 2, 0, 0, 0, 0, time.UTC), time.Date(2015, 3, 1, 0, 0, 0, 0, time.UTC), Period{0, 0, 270, 0, 0, 0}}, + {time.Date(2016, 2, 2, 0, 0, 0, 0, time.UTC), time.Date(2016, 3, 1, 0, 0, 0, 0, time.UTC), Period{0, 0, 280, 0, 0, 0}}, + {time.Date(2015, 2, 11, 0, 0, 0, 0, time.UTC), time.Date(2016, 1, 12, 0, 0, 0, 0, time.UTC), Period{0, 110, 10, 0, 0, 0}}, + {time.Date(2016, 1, 12, 0, 0, 0, 0, time.UTC), time.Date(2015, 2, 11, 0, 0, 0, 0, time.UTC), Period{0, -110, -10, 0, 0, 0}}, + } + for _, c := range cases { + n := Between(c.a, c.b) + if n != c.expected { + t.Errorf("Between(%v, %v) gives %v %#v, want %v", c.a, c.b, n, n, c.expected) + } + } +} + func TestNormalise(t *testing.T) { cases := []struct { source, expected Period From 5f241351ca1f0795d86e7816ad8a93d2b73fddd7 Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 21 Apr 2016 20:44:20 +0100 Subject: [PATCH 064/165] Formatting time now follows the special-case convention that midnight at the end of a day is shown as 24:00 (or similar) instead of 00:00. This is the only case in which the number of hours can reach 24. --- clock/clock_test.go | 2 ++ clock/format.go | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/clock/clock_test.go b/clock/clock_test.go index 8affc1f3..d83d24fc 100644 --- a/clock/clock_test.go +++ b/clock/clock_test.go @@ -204,6 +204,8 @@ func TestClockString(t *testing.T) { {11, 0, 0, 0, "11", "11:00", "11:00:00", "11:00:00.000", "11am", "11:00am", "11:00:00am"}, {12, 0, 0, 0, "12", "12:00", "12:00:00", "12:00:00.000", "12pm", "12:00pm", "12:00:00pm"}, {13, 0, 0, 0, "13", "13:00", "13:00:00", "13:00:00.000", "1pm", "1:00pm", "1:00:00pm"}, + {24, 0, 0, 0, "24", "24:00", "24:00:00", "24:00:00.000", "12am", "12:00am", "12:00:00am"}, + {24, 0, 0, 1, "00", "00:00", "00:00:00", "00:00:00.001", "12am", "12:00am", "12:00:00am"}, {-1, 0, 0, 0, "23", "23:00", "23:00:00", "23:00:00.000", "11pm", "11:00pm", "11:00:00pm"}, {-1, -1, -1, -1, "22", "22:58", "22:58:58", "22:58:58.999", "10pm", "10:58pm", "10:58:58pm"}, } diff --git a/clock/format.go b/clock/format.go index 65b9fd65..5975a535 100644 --- a/clock/format.go +++ b/clock/format.go @@ -36,21 +36,33 @@ func clockMillisec(cm Clock) Clock { // Hh gets the clock-face number of hours as a two-digit string. // It is calculated from the modulo time; see Mod24. +// Note the special case of midnight at the end of a day is "24". func (c Clock) Hh() string { + if c == Day { + return "24" + } cm := c.Mod24() return fmt.Sprintf("%02d", clockHours(cm)) } // HhMm gets the clock-face number of hours and minutes as a five-character ISO-8601 time string. // It is calculated from the modulo time; see Mod24. +// Note the special case of midnight at the end of a day is "24:00". func (c Clock) HhMm() string { + if c == Day { + return "24:00" + } cm := c.Mod24() return fmt.Sprintf("%02d:%02d", clockHours(cm), clockMinutes(cm)) } // HhMmSs gets the clock-face number of hours, minutes, seconds as an eight-character ISO-8601 time string. // It is calculated from the modulo time; see Mod24. +// Note the special case of midnight at the end of a day is "24:00:00". func (c Clock) HhMmSs() string { + if c == Day { + return "24:00:00" + } cm := c.Mod24() return fmt.Sprintf("%02d:%02d:%02d", clockHours(cm), clockMinutes(cm), clockSeconds(cm)) } @@ -84,7 +96,11 @@ func (c Clock) HhMmSs12() string { // String gets the clock-face number of hours, minutes, seconds and milliseconds as a 12-character ISO-8601 // time string (calculated from the modulo time, see Mod24), specified to the nearest millisecond. +// Note the special case of midnight at the end of a day is "24:00:00.000". func (c Clock) String() string { + if c == Day { + return "24:00:00.000" + } cm := c.Mod24() return fmt.Sprintf("%02d:%02d:%02d.%03d", clockHours(cm), clockMinutes(cm), clockSeconds(cm), clockMillisec(cm)) } From f56ed5052171a83cee09b76d51f1e008c9278cc2 Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 6 Dec 2016 22:11:41 +0000 Subject: [PATCH 065/165] Added ModSubtract method --- clock/clock.go | 12 ++++++++++++ clock/clock_test.go | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/clock/clock.go b/clock/clock.go index 34328603..9c366178 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -94,6 +94,18 @@ func (c Clock) AddDuration(d time.Duration) Clock { return c + Clock(d/time.Millisecond) } +// ModSubtract returns the duration between two clock times. +// +// If c2 is before c1 (i.e. c2 < c1), the result is the duration computed from c1 - c2. +// +// But if c1 is before c2, it is assumed that c1 is after midnight and c2 is before midnight. The +// result is the sum of the evening time from c2 to midnight with the morning time from midnight to c1. +// This is the same as Mod24(c1 - c2). +func (c1 Clock) ModSubtract(c2 Clock) time.Duration { + ms := c1 - c2 + return ms.Mod24().DurationSinceMidnight() +} + // IsInOneDay tests whether a clock time is in the range 0 to 24 hours, inclusive. Inside this // range, a Clock is generally well-behaved. But outside it, there may be errors due to daylight // savings. Note that 24:00:00 is included as a special case as per ISO-8601 definition of midnight. diff --git a/clock/clock_test.go b/clock/clock_test.go index d83d24fc..93ae4c01 100644 --- a/clock/clock_test.go +++ b/clock/clock_test.go @@ -114,6 +114,27 @@ func TestClockAddDuration(t *testing.T) { } } +func TestClockSubtract(t *testing.T) { + cases := []struct { + c1, c2 Clock + want time.Duration + }{ + {New(1, 2, 3, 4), New(1, 2, 3, 4), 0 * time.Hour}, + {New(2, 0, 0, 0), New(0, 0, 0, 0), 2 * time.Hour}, + {New(0, 0, 0, 0), New(2, 0, 0, 0), 22 * time.Hour}, + {New(1, 0, 0, 0), New(23, 0, 0, 0), 2 * time.Hour}, + {New(23, 0, 0, 0), New(1, 0, 0, 0), 22 * time.Hour}, + {New(1, 2, 3, 5), New(1, 2, 3, 4), 1 * time.Millisecond}, + {New(1, 2, 3, 4), New(1, 2, 3, 5), 24*time.Hour - 1*time.Millisecond}, + } + for i, x := range cases { + got := x.c1.ModSubtract(x.c2) + if got != x.want { + t.Errorf("%d: %v - %v: got %v, want %v", i, x.c1, x.c2, got, x.want) + } + } +} + func TestClockIsMidnight(t *testing.T) { cases := []struct { in Clock From c769905eed70758e5be26d75b4025cfff01aa414 Mon Sep 17 00:00:00 2001 From: Rick Date: Sat, 20 May 2017 17:57:12 +0100 Subject: [PATCH 066/165] Date now implements Scanner and Valuer interfaces to allow direct storage in an SQL database via an integer column. --- .travis.yml | 22 +++++++++++++++++++--- sql.go | 29 +++++++++++++++++++++++++++++ sql_test.go | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 sql.go create mode 100644 sql_test.go diff --git a/.travis.yml b/.travis.yml index dc879936..7ed6db71 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,25 @@ install: - go get github.com/mattn/goveralls script: - - go test -v -covermode=count -coverprofile=coverage.out . - - go tool cover -func=coverage.out - - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN + - go test -v -covermode=count -coverprofile=date.out . + - go tool cover -func=date.out + - $HOME/gopath/bin/goveralls -coverprofile=date.out -service=travis-ci -repotoken $COVERALLS_TOKEN + + - go test -v -covermode=count -coverprofile=clock.out clock + - go tool cover -func=clock.out + - $HOME/gopath/bin/goveralls -coverprofile=clock.out -service=travis-ci -repotoken $COVERALLS_TOKEN + + - go test -v -covermode=count -coverprofile=period.out period + - go tool cover -func=period.out + - $HOME/gopath/bin/goveralls -coverprofile=period.out -service=travis-ci -repotoken $COVERALLS_TOKEN + + - go test -v -covermode=count -coverprofile=timespan.out timespan + - go tool cover -func=timespan.out + - $HOME/gopath/bin/goveralls -coverprofile=timespan.out -service=travis-ci -repotoken $COVERALLS_TOKEN + + - go test -v -covermode=count -coverprofile=view.out view + - go tool cover -func=view.out + - $HOME/gopath/bin/goveralls -coverprofile=view.out -service=travis-ci -repotoken $COVERALLS_TOKEN #env: # secure: "kcksCWXVeZKmFUWcyi2S/j87iwUmXMxZXxA2DG9ymc11QP43QoPNSG9pBjA/DDjvzt4WdKIFphTrxVfvawii/9j3oXA1aPmAcHGu87i4iOVg4IIZ4bPZLfUo0e7s6XP5FakzegYvPP6HWV5Xr5h+Q6osrjq3czOnPY+rVII6MRrxXMOfsqo8HEER+YIOOD6vj5LV2/quY8d0XHtThqgGvQ1cz4OB3vbd4KFBl48kmfXKefTrRG1NoqoQMMpwUVzU395JIEAg1eWbGkquhWU5v13gRwk3VMVWF75jZna8TSiqWha0P5iQdaED30kNCz3poIaBI1MLdxktJxwUQJZ5AaYIMCxh7ZCiW0FXTYCRu3EoeYusTPMLqy1ghK+gIlA46sNd26cKk5/OngXRrHo/J0aF5NWjydlk5FLHfKm9ih/Y426M9nV2zYNQAcVKgO8zVNb2IkJ3e7aTB2NH4DpkvjSV4D4hlnmW9xxmo14TKF+gXJ9Hw9ssKbigRHoL6S92aQHcpkdjGGnI5YSTy1fZh/nIE3HDmx+hcK4/ZtPHj9KnXopKYxBGyNswN5Eko+q6h3BB/Q7LwALtEexdDbznwsRmcZXJsOU4chvcjAKgIAi6cbeTwq8kG5E/w8TTY2wGeRm2ZFysQu6Jf8hgTZDQTV343RG80STWeUbiD0o7/WY=" diff --git a/sql.go b/sql.go new file mode 100644 index 00000000..8ef43db7 --- /dev/null +++ b/sql.go @@ -0,0 +1,29 @@ +package date + +import ( + "database/sql/driver" + "fmt" +) + +// These methods allow Date and PeriodOfDays to be fields stored in an +// SQL database by implementing the database/sql/driver interfaces. +// The underlying column type is simply an integer. + +// Scan parses some value. It implements sql.Scanner, +// https://golang.org/pkg/database/sql/#Scanner +func (d *Date) Scan(value interface{}) (err error) { + err = nil + switch value.(type) { + case int64: + *d = Date{PeriodOfDays(value.(int64))} + default: + err = fmt.Errorf("%#v", value) + } + return +} + +// Value converts the value to an int64. It implements driver.Valuer, +// https://golang.org/pkg/database/sql/driver/#Valuer +func (d Date) Value() (driver.Value, error) { + return int64(d.day), nil +} diff --git a/sql_test.go b/sql_test.go new file mode 100644 index 00000000..91ef34c9 --- /dev/null +++ b/sql_test.go @@ -0,0 +1,36 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package date + +import ( + "database/sql/driver" + "testing" +) + +func TestDateScan(t *testing.T) { + cases := []PeriodOfDays{ + 0, 1, 28, 30, 31, 32, 364, 365, 366, 367, 500, 1000, 10000, 100000, + } + for _, c := range cases { + var d driver.Valuer = NewOfDays(c) + + v, e := d.Value() + if e != nil { + t.Errorf("Got %v for %d", e, c) + } + if v.(int64) != int64(c) { + t.Errorf("Got %v, want %d", v, c) + } + + r := new(Date) + e = r.Scan(v) + if e != nil { + t.Errorf("Got %v for %d", e, c) + } + if *r != d { + t.Errorf("Got %v, want %d", *r, d) + } + } +} From 52be6ff76b926f96f6f830610d3267ae06c15fdc Mon Sep 17 00:00:00 2001 From: Rick Date: Sun, 21 May 2017 14:24:23 +0100 Subject: [PATCH 067/165] SQL serialisation extended to allow string and []byte values. --- date.go | 6 ++++++ date_test.go | 10 ++++++--- period/format.go | 4 ++++ period/parse.go | 4 ++++ period/period.go | 4 ++++ period/period_test.go | 4 ++++ sql.go | 35 +++++++++++++++++++++++++++++++ sql_test.go | 48 ++++++++++++++++++++++++++++++------------- view/vdate.go | 4 ++++ view/vdate_test.go | 4 ++++ 10 files changed, 106 insertions(+), 17 deletions(-) diff --git a/date.go b/date.go index 84922746..1408a676 100644 --- a/date.go +++ b/date.go @@ -68,6 +68,12 @@ func NewOfDays(p PeriodOfDays) Date { return Date{p} } +// Date returns the Date value corresponding to the given period since the +// epoch (1st January 1970), which may be negative. +func (p PeriodOfDays) Date() Date { + return Date{p} +} + // Today returns today's date according to the current local time. func Today() Date { t := time.Now() diff --git a/date_test.go b/date_test.go index 5014be8c..18f89fbe 100644 --- a/date_test.go +++ b/date_test.go @@ -58,9 +58,13 @@ func TestDaysSinceEpoch(t *testing.T) { } today := Today() days := today.DaysSinceEpoch() - copy := NewOfDays(days) - if today != copy || days == 0 { - t.Errorf("Today == %v, want date of %v", today, copy) + copy1 := NewOfDays(days) + copy2 := days.Date() + if today != copy1 || days == 0 { + t.Errorf("Today == %v, want date of %v", today, copy1) + } + if today != copy2 || days == 0 { + t.Errorf("Today == %v, want date of %v", today, copy2) } } diff --git a/period/format.go b/period/format.go index 68205853..38562156 100644 --- a/period/format.go +++ b/period/format.go @@ -1,3 +1,7 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package period import ( diff --git a/period/parse.go b/period/parse.go index 98c78c18..df71d98f 100644 --- a/period/parse.go +++ b/period/parse.go @@ -1,3 +1,7 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package period import ( diff --git a/period/period.go b/period/period.go index bf0b0b2b..e5236cc3 100644 --- a/period/period.go +++ b/period/period.go @@ -1,3 +1,7 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package period import ( diff --git a/period/period_test.go b/period/period_test.go index a47b9791..ebe86e63 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -1,3 +1,7 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package period import ( diff --git a/sql.go b/sql.go index 8ef43db7..85ed0d30 100644 --- a/sql.go +++ b/sql.go @@ -1,8 +1,14 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package date import ( "database/sql/driver" "fmt" + "strconv" + "time" ) // These methods allow Date and PeriodOfDays to be fields stored in an @@ -12,6 +18,29 @@ import ( // Scan parses some value. It implements sql.Scanner, // https://golang.org/pkg/database/sql/#Scanner func (d *Date) Scan(value interface{}) (err error) { + if DisableTextStorage { + return d.scanInt(value) + } + var n int64 + err = nil + switch value.(type) { + case int64: + *d = Date{PeriodOfDays(value.(int64))} + case []byte: + n, err = strconv.ParseInt(string(value.([]byte)), 10, 64) + *d = Date{PeriodOfDays(n)} + case string: + n, err = strconv.ParseInt(value.(string), 10, 64) + *d = Date{PeriodOfDays(n)} + case time.Time: + *d = NewAt(value.(time.Time)) + default: + err = fmt.Errorf("%#v", value) + } + return +} + +func (d *Date) scanInt(value interface{}) (err error) { err = nil switch value.(type) { case int64: @@ -27,3 +56,9 @@ func (d *Date) Scan(value interface{}) (err error) { func (d Date) Value() (driver.Value, error) { return int64(d.day), nil } + +// DisableTextStorage reduces the Scan method so that only integers are handled. +// Normally, database types int64, []byte, string and time.Time are supported. +// When set true, only int64 is supported; this mode allows optimisation of SQL +// result processing and would only be used during development. +var DisableTextStorage = false diff --git a/sql_test.go b/sql_test.go index 91ef34c9..7287d787 100644 --- a/sql_test.go +++ b/sql_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 The Go Authors. All rights reserved. +// Copyright 2015 Rick Beton. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. @@ -10,27 +10,47 @@ import ( ) func TestDateScan(t *testing.T) { - cases := []PeriodOfDays{ - 0, 1, 28, 30, 31, 32, 364, 365, 366, 367, 500, 1000, 10000, 100000, + cases := []struct { + v interface{} + disallow bool + expected PeriodOfDays + }{ + {int64(0), false, 0}, + {int64(1000), false, 1000}, + {int64(10000), false, 10000}, + {int64(0), true, 0}, + {int64(1000), true, 1000}, + {int64(10000), true, 10000}, + {"0", false, 0}, + {"1000", false, 1000}, + {"10000", false, 10000}, + {[]byte("10000"), false, 10000}, + {PeriodOfDays(10000).Date().Local(), false, 10000}, } - for _, c := range cases { - var d driver.Valuer = NewOfDays(c) - v, e := d.Value() + prior := DisableTextStorage + + for i, c := range cases { + DisableTextStorage = c.disallow + r := new(Date) + e := r.Scan(c.v) if e != nil { - t.Errorf("Got %v for %d", e, c) + t.Errorf("%d: Got %v for %d", i, e, c.expected) } - if v.(int64) != int64(c) { - t.Errorf("Got %v, want %d", v, c) + if r.DaysSinceEpoch() != c.expected { + t.Errorf("%d: Got %v, want %d", i, *r, c.expected) } - r := new(Date) - e = r.Scan(v) + var d driver.Valuer = *r + + q, e := d.Value() if e != nil { - t.Errorf("Got %v for %d", e, c) + t.Errorf("%d: Got %v for %d", i, e, c.expected) } - if *r != d { - t.Errorf("Got %v, want %d", *r, d) + if q.(int64) != int64(c.expected) { + t.Errorf("%d: Got %v, want %d", i, q, c.expected) } } + + DisableTextStorage = prior } diff --git a/view/vdate.go b/view/vdate.go index e33ee9c4..70249c6a 100644 --- a/view/vdate.go +++ b/view/vdate.go @@ -1,3 +1,7 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + // Package view provides a simple API for formatting dates as strings in a manner that is easy to use in view-models, // especially when using Go templates. package view diff --git a/view/vdate_test.go b/view/vdate_test.go index 5326a630..fd716396 100644 --- a/view/vdate_test.go +++ b/view/vdate_test.go @@ -1,3 +1,7 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package view import ( From 7566b02712a71251e914680b727da782118dab37 Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 8 Jun 2017 13:41:14 +0100 Subject: [PATCH 068/165] Fixed the build scripts --- .gitignore | 1 + .travis.yml | 20 +------------------- build+test.sh | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+), 19 deletions(-) create mode 100755 build+test.sh diff --git a/.gitignore b/.gitignore index daf913b1..ffaff0d8 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ _testmain.go *.exe *.test *.prof +*.out diff --git a/.travis.yml b/.travis.yml index 7ed6db71..8ab3016f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,25 +8,7 @@ install: - go get github.com/mattn/goveralls script: - - go test -v -covermode=count -coverprofile=date.out . - - go tool cover -func=date.out - - $HOME/gopath/bin/goveralls -coverprofile=date.out -service=travis-ci -repotoken $COVERALLS_TOKEN - - - go test -v -covermode=count -coverprofile=clock.out clock - - go tool cover -func=clock.out - - $HOME/gopath/bin/goveralls -coverprofile=clock.out -service=travis-ci -repotoken $COVERALLS_TOKEN - - - go test -v -covermode=count -coverprofile=period.out period - - go tool cover -func=period.out - - $HOME/gopath/bin/goveralls -coverprofile=period.out -service=travis-ci -repotoken $COVERALLS_TOKEN - - - go test -v -covermode=count -coverprofile=timespan.out timespan - - go tool cover -func=timespan.out - - $HOME/gopath/bin/goveralls -coverprofile=timespan.out -service=travis-ci -repotoken $COVERALLS_TOKEN - - - go test -v -covermode=count -coverprofile=view.out view - - go tool cover -func=view.out - - $HOME/gopath/bin/goveralls -coverprofile=view.out -service=travis-ci -repotoken $COVERALLS_TOKEN + - ./build+test.sh #env: # secure: "kcksCWXVeZKmFUWcyi2S/j87iwUmXMxZXxA2DG9ymc11QP43QoPNSG9pBjA/DDjvzt4WdKIFphTrxVfvawii/9j3oXA1aPmAcHGu87i4iOVg4IIZ4bPZLfUo0e7s6XP5FakzegYvPP6HWV5Xr5h+Q6osrjq3czOnPY+rVII6MRrxXMOfsqo8HEER+YIOOD6vj5LV2/quY8d0XHtThqgGvQ1cz4OB3vbd4KFBl48kmfXKefTrRG1NoqoQMMpwUVzU395JIEAg1eWbGkquhWU5v13gRwk3VMVWF75jZna8TSiqWha0P5iQdaED30kNCz3poIaBI1MLdxktJxwUQJZ5AaYIMCxh7ZCiW0FXTYCRu3EoeYusTPMLqy1ghK+gIlA46sNd26cKk5/OngXRrHo/J0aF5NWjydlk5FLHfKm9ih/Y426M9nV2zYNQAcVKgO8zVNb2IkJ3e7aTB2NH4DpkvjSV4D4hlnmW9xxmo14TKF+gXJ9Hw9ssKbigRHoL6S92aQHcpkdjGGnI5YSTy1fZh/nIE3HDmx+hcK4/ZtPHj9KnXopKYxBGyNswN5Eko+q6h3BB/Q7LwALtEexdDbznwsRmcZXJsOU4chvcjAKgIAi6cbeTwq8kG5E/w8TTY2wGeRm2ZFysQu6Jf8hgTZDQTV343RG80STWeUbiD0o7/WY=" diff --git a/build+test.sh b/build+test.sh new file mode 100755 index 00000000..75e59170 --- /dev/null +++ b/build+test.sh @@ -0,0 +1,19 @@ +#!/bin/bash -e +PATH=$HOME/gopath/bin:$GOPATH/bin:$PATH + +if ! type -p goveralls; then + echo go get github.com/mattn/goveralls + go get github.com/mattn/goveralls +fi + +echo date... +go test -v -covermode=count -coverprofile=date.out . +go tool cover -func=date.out +[ -z "$COVERALLS_TOKEN" ] || goveralls -coverprofile=date.out -service=travis-ci -repotoken $COVERALLS_TOKEN + +for d in clock period timespan view; do + echo $d... + go test -v -covermode=count -coverprofile=$d.out ./$d + go tool cover -func=$d.out + [ -z "$COVERALLS_TOKEN" ] || goveralls -coverprofile=$d.out -service=travis-ci -repotoken $COVERALLS_TOKEN +done From a6d27bd49712532ae36940e162a53d700f8a551c Mon Sep 17 00:00:00 2001 From: Rick Date: Mon, 21 Aug 2017 18:57:28 +0100 Subject: [PATCH 069/165] Preparation for v1 --- .gitignore | 5 +++-- Gopkg.lock | 15 +++++++++++++++ Gopkg.toml | 5 +++++ README.md | 4 ++++ 4 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 Gopkg.lock create mode 100644 Gopkg.toml diff --git a/.gitignore b/.gitignore index ffaff0d8..49ef8df4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,9 @@ *.so # Folders -_obj -_test +_obj/ +_test/ +vendor/ # Architecture specific extensions/prefixes *.[568vq] diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 00000000..6b39ce0f --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,15 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/rickb777/plural" + packages = ["."] + revision = "fb23b1a142e9fd02ad69a10fb69ff25b5bcfe079" + version = "v1.0.0" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "db0a171068f0d4c1006d9df3c91278c67ddd77351c97a73786d90b2b432d7f3d" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 00000000..d390c279 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,5 @@ +# dep: see https://github.com/golang/dep + +[[constraint]] + name = "github.com/rickb777/plural" + version = "1.0.0" diff --git a/README.md b/README.md index 1680984e..176c9799 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ full documentation and examples. go get -u github.com/rickb777/date +## Status + +This library has been in reliable production use for some time. Versioning follows the well-known semantic version pattern. + ## Credits This package follows very closely the design of package From 16ffe3a5c7a3147b6524bcf686909dfcd2533475 Mon Sep 17 00:00:00 2001 From: Rick Date: Mon, 21 Aug 2017 19:05:21 +0100 Subject: [PATCH 070/165] minor --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8ab3016f..7dcf53ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,5 +10,3 @@ install: script: - ./build+test.sh -#env: -# secure: "kcksCWXVeZKmFUWcyi2S/j87iwUmXMxZXxA2DG9ymc11QP43QoPNSG9pBjA/DDjvzt4WdKIFphTrxVfvawii/9j3oXA1aPmAcHGu87i4iOVg4IIZ4bPZLfUo0e7s6XP5FakzegYvPP6HWV5Xr5h+Q6osrjq3czOnPY+rVII6MRrxXMOfsqo8HEER+YIOOD6vj5LV2/quY8d0XHtThqgGvQ1cz4OB3vbd4KFBl48kmfXKefTrRG1NoqoQMMpwUVzU395JIEAg1eWbGkquhWU5v13gRwk3VMVWF75jZna8TSiqWha0P5iQdaED30kNCz3poIaBI1MLdxktJxwUQJZ5AaYIMCxh7ZCiW0FXTYCRu3EoeYusTPMLqy1ghK+gIlA46sNd26cKk5/OngXRrHo/J0aF5NWjydlk5FLHfKm9ih/Y426M9nV2zYNQAcVKgO8zVNb2IkJ3e7aTB2NH4DpkvjSV4D4hlnmW9xxmo14TKF+gXJ9Hw9ssKbigRHoL6S92aQHcpkdjGGnI5YSTy1fZh/nIE3HDmx+hcK4/ZtPHj9KnXopKYxBGyNswN5Eko+q6h3BB/Q7LwALtEexdDbznwsRmcZXJsOU4chvcjAKgIAi6cbeTwq8kG5E/w8TTY2wGeRm2ZFysQu6Jf8hgTZDQTV343RG80STWeUbiD0o7/WY=" From 3861b515db11d3bf740fe235397aeeb765709873 Mon Sep 17 00:00:00 2001 From: Rick Date: Mon, 21 Aug 2017 19:54:44 +0100 Subject: [PATCH 071/165] Fixed minor doc issues etc; removed an item that was to be deleted earlier --- Gopkg.lock | 4 +-- README.md | 1 + clock/clock.go | 5 ++-- clock/format.go | 2 +- date.go | 2 +- period/period.go | 2 +- timespan/daterange.go | 5 ---- timespan/timespan.go | 1 + view/vdate.go | 65 ++++++++++++++++++++++--------------------- view/vdate_test.go | 2 +- 10 files changed, 43 insertions(+), 46 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 6b39ce0f..afd0f5d1 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -4,8 +4,8 @@ [[projects]] name = "github.com/rickb777/plural" packages = ["."] - revision = "fb23b1a142e9fd02ad69a10fb69ff25b5bcfe079" - version = "v1.0.0" + revision = "a4d8479dc7ebd80a2cbe436c908b3e8ce28d7b22" + version = "v1.0.1" [solve-meta] analyzer-name = "dep" diff --git a/README.md b/README.md index 176c9799..7e3276cb 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![GoDoc](https://img.shields.io/badge/api-Godoc-blue.svg?style=flat-square)](https://godoc.org/github.com/rickb777/date) [![Build Status](https://api.travis-ci.org/rickb777/date.svg?branch=master)](https://travis-ci.org/rickb777/date) [![Coverage Status](https://coveralls.io/repos/rickb777/date/badge.svg?branch=master&service=github)](https://coveralls.io/github/rickb777/date?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/rickb777/date)](https://goreportcard.com/report/github.com/rickb777/date) Package `date` provides functionality for working with dates. diff --git a/clock/clock.go b/clock/clock.go index 9c366178..c4b3b726 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Clock specifies a time of day with resolution to the nearest millisecond. +// Package clock specifies a time of day with resolution to the nearest millisecond. // package clock @@ -147,9 +147,8 @@ func (c Clock) Mod24() Clock { func (c Clock) Days() int { if c < Midnight { return int(c/Day) - 1 - } else { - return int(c / Day) } + return int(c / Day) } // Hours gets the clock-face number of hours (calculated from the modulo time, see Mod24). diff --git a/clock/format.go b/clock/format.go index 5975a535..8db9eda6 100644 --- a/clock/format.go +++ b/clock/format.go @@ -85,7 +85,7 @@ func (c Clock) HhMm12() string { return fmt.Sprintf("%d:%02d%s", h, clockMinutes(cm), sfx) } -// HhMm12 gets the clock-face number of hours, minutes and seconds, followed by am or pm. +// HhMmSs12 gets the clock-face number of hours, minutes and seconds, followed by am or pm. // Remember that midnight is 12am, noon is 12pm. // It is calculated from the modulo time; see Mod24. func (c Clock) HhMmSs12() string { diff --git a/date.go b/date.go index 1408a676..35ed8352 100644 --- a/date.go +++ b/date.go @@ -201,7 +201,7 @@ func (d Date) After(u Date) bool { return d.day > u.day } -// Max returns the earlier of two dates. +// Min returns the earlier of two dates. func (d Date) Min(u Date) Date { if d.day > u.day { return u diff --git a/period/period.go b/period/period.go index e5236cc3..a2f72a6b 100644 --- a/period/period.go +++ b/period/period.go @@ -49,7 +49,7 @@ func NewHMS(hours, minutes, seconds int) Period { return New(0, 0, 0, hours, minutes, seconds) } -// NewPeriod creates a simple period without any fractional parts. All the parameters +// New creates a simple period without any fractional parts. All the parameters // must have the same sign (otherwise a panic occurs). func New(years, months, days, hours, minutes, seconds int) Period { if (years >= 0 && months >= 0 && days >= 0 && hours >= 0 && minutes >= 0 && seconds >= 0) || diff --git a/timespan/daterange.go b/timespan/daterange.go index 984970db..9e677e12 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -57,11 +57,6 @@ func EmptyRange(day Date) DateRange { return DateRange{day, 0} } -// Deprecated - ZeroRange constructs an empty range. Use EmptyRange instead. -func ZeroRange(day Date) DateRange { - return DateRange{day, 0} -} - // OneDayRange constructs a range of exactly one day. This is often a useful basis for // further operations. Note that the end date is the same as the start date. func OneDayRange(day Date) DateRange { diff --git a/timespan/timespan.go b/timespan/timespan.go index 7fa6e0f5..77cfeb8e 100644 --- a/timespan/timespan.go +++ b/timespan/timespan.go @@ -10,6 +10,7 @@ import ( "time" ) +// TimestampFormat is a simple format for date & time, "2006-01-02 15:04:05". const TimestampFormat = "2006-01-02 15:04:05" //const ISOFormat = "2006-01-02T15:04:05" diff --git a/view/vdate.go b/view/vdate.go index 70249c6a..4a71df90 100644 --- a/view/vdate.go +++ b/view/vdate.go @@ -31,80 +31,80 @@ func NewVDate(d date.Date) VDate { } // String formats the date in basic ISO8601 format YYYY-MM-DD. -func (d VDate) String() string { - return d.d.String() +func (v VDate) String() string { + return v.d.String() } // WithFormat creates a new instance containing the specified format string. -func (d VDate) WithFormat(f string) VDate { - return VDate{d.d, f} +func (v VDate) WithFormat(f string) VDate { + return VDate{v.d, f} } // Format formats the date using the specified format string, or "02/01/2006" by default. // Use WithFormat to set this up. -func (d VDate) Format() string { - return d.d.Format(d.f) +func (v VDate) Format() string { + return v.d.Format(v.f) } // Mon returns the day name as three letters. -func (d VDate) Mon() string { - return d.d.Format("Mon") +func (v VDate) Mon() string { + return v.d.Format("Mon") } // Monday returns the full day name. -func (d VDate) Monday() string { - return d.d.Format("Monday") +func (v VDate) Monday() string { + return v.d.Format("Monday") } // Day2 returns the day number without a leading zero. -func (d VDate) Day2() string { - return d.d.Format("2") +func (v VDate) Day2() string { + return v.d.Format("2") } // Day02 returns the day number with a leading zero if necessary. -func (d VDate) Day02() string { - return d.d.Format("02") +func (v VDate) Day02() string { + return v.d.Format("02") } // Day2nd returns the day number without a leading zero but with the appropriate // "st", "nd", "rd", "th" suffix. -func (d VDate) Day2nd() string { - return d.d.Format("2nd") +func (v VDate) Day2nd() string { + return v.d.Format("2nd") } // Month1 returns the month number without a leading zero. -func (d VDate) Month1() string { - return d.d.Format("1") +func (v VDate) Month1() string { + return v.d.Format("1") } // Month01 returns the month number with a leading zero if necessary. -func (d VDate) Month01() string { - return d.d.Format("01") +func (v VDate) Month01() string { + return v.d.Format("01") } // Jan returns the month name abbreviated to three letters. -func (d VDate) Jan() string { - return d.d.Format("Jan") +func (v VDate) Jan() string { + return v.d.Format("Jan") } // January returns the full month name. -func (d VDate) January() string { - return d.d.Format("January") +func (v VDate) January() string { + return v.d.Format("January") } // Year returns the four-digit year. -func (d VDate) Year() string { - return d.d.Format("2006") +func (v VDate) Year() string { + return v.d.Format("2006") } // Next returns a fluent generator for later dates. -func (d VDate) Next() VDateDelta { - return VDateDelta{d.d, d.f, 1} +func (v VDate) Next() VDateDelta { + return VDateDelta{v.d, v.f, 1} } // Previous returns a fluent generator for earlier dates. -func (d VDate) Previous() VDateDelta { - return VDateDelta{d.d, d.f, -1} +func (v VDate) Previous() VDateDelta { + return VDateDelta{v.d, v.f, -1} } //------------------------------------------------------------------------------------------------- @@ -113,7 +113,7 @@ func (d VDate) Previous() VDateDelta { // MarshalJSON implements the json.Marshaler interface. //func (v VDate) MarshalJSON() ([]byte, error) { -// return v.d.MarshalJSON() +// return v.v.MarshalJSON() //} // UnmarshalJSON implements the json.Unmarshaler interface. @@ -122,7 +122,7 @@ func (d VDate) Previous() VDateDelta { // u := &date.Date{} // err = u.UnmarshalJSON(data) // if err == nil { -// v.d = *u +// v.v = *u // v.f = DefaultFormat // } // return err @@ -147,6 +147,7 @@ func (v *VDate) UnmarshalText(data []byte) (err error) { //------------------------------------------------------------------------------------------------- +// VDateDelta is a VDate with the ability to add or subtract days, weeks, months or years. type VDateDelta struct { d date.Date f string diff --git a/view/vdate_test.go b/view/vdate_test.go index fd716396..35114b25 100644 --- a/view/vdate_test.go +++ b/view/vdate_test.go @@ -46,7 +46,7 @@ func TestPrevious(t *testing.T) { func is(t *testing.T, s1, s2 string) { if s1 != s2 { - t.Error("%s != %s", s1, s2) + t.Errorf("%s != %s", s1, s2) } } From 6c2251facea9730ccb4b2a7aaff75ac1c6a982ae Mon Sep 17 00:00:00 2001 From: Rick Date: Wed, 23 Aug 2017 10:41:57 +0100 Subject: [PATCH 072/165] switched from glock to dep; removed gomock --- Gopkg.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gopkg.toml b/Gopkg.toml index d390c279..2dd6c702 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -2,4 +2,4 @@ [[constraint]] name = "github.com/rickb777/plural" - version = "1.0.0" + version = "^1.0.0" From ca6968fff53327f2b7a823b91fe7ab5f421024f0 Mon Sep 17 00:00:00 2001 From: Rick Date: Sat, 26 Aug 2017 22:52:52 +0100 Subject: [PATCH 073/165] docs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7e3276cb..d2c23d4f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![Build Status](https://api.travis-ci.org/rickb777/date.svg?branch=master)](https://travis-ci.org/rickb777/date) [![Coverage Status](https://coveralls.io/repos/rickb777/date/badge.svg?branch=master&service=github)](https://coveralls.io/github/rickb777/date?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/rickb777/date)](https://goreportcard.com/report/github.com/rickb777/date) +[![Issues](https://img.shields.io/github/issues/rickb777/date.svg)](https://github.com/rickb777/date/issues) Package `date` provides functionality for working with dates. From 511bb2b4a749f0b66e36c9b2e1f7bf63cd9f4f69 Mon Sep 17 00:00:00 2001 From: Rick Date: Thu, 31 Aug 2017 13:59:16 +0100 Subject: [PATCH 074/165] Added several new methods to VDate: IsYesterday, IsToday, IsTomorrow, Date --- view/vdate.go | 37 ++++++++++++++++++++----------------- view/vdate_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/view/vdate.go b/view/vdate.go index 4a71df90..9fc9889a 100644 --- a/view/vdate.go +++ b/view/vdate.go @@ -30,6 +30,26 @@ func NewVDate(d date.Date) VDate { return VDate{d, DefaultFormat} } +// Date returns the underlying date. +func (v VDate) Date() date.Date { + return v.d +} + +// IsYesterday returns true if the date is yesterday's date. +func (v VDate) IsYesterday() bool { + return v.d.DaysSinceEpoch() + 1 == date.Today().DaysSinceEpoch() +} + +// IsToday returns true if the date is today's date. +func (v VDate) IsToday() bool { + return v.d.DaysSinceEpoch() == date.Today().DaysSinceEpoch() +} + +// IsTomorrow returns true if the date is tomorrow's date. +func (v VDate) IsTomorrow() bool { + return v.d.DaysSinceEpoch() - 1 == date.Today().DaysSinceEpoch() +} + // String formats the date in basic ISO8601 format YYYY-MM-DD. func (v VDate) String() string { return v.d.String() @@ -111,23 +131,6 @@ func (v VDate) Previous() VDateDelta { // Only lossy transcoding is supported here because the intention is that data exchange should be // via the main Date type; VDate is only intended for output through view layers. -// MarshalJSON implements the json.Marshaler interface. -//func (v VDate) MarshalJSON() ([]byte, error) { -// return v.v.MarshalJSON() -//} - -// UnmarshalJSON implements the json.Unmarshaler interface. -// Note that the format value gets lost. -//func (v *VDate) UnmarshalJSON(data []byte) (err error) { -// u := &date.Date{} -// err = u.UnmarshalJSON(data) -// if err == nil { -// v.v = *u -// v.f = DefaultFormat -// } -// return err -//} - // MarshalText implements the encoding.TextMarshaler interface. func (v VDate) MarshalText() ([]byte, error) { return v.d.MarshalText() diff --git a/view/vdate_test.go b/view/vdate_test.go index 35114b25..5b62d11e 100644 --- a/view/vdate_test.go +++ b/view/vdate_test.go @@ -28,6 +28,44 @@ func TestBasicFormatting(t *testing.T) { is(t, d.Year(), "2016") } +func TestDate(t *testing.T) { + d := date.New(2016, 2, 7) + vd := NewVDate(d) + if vd.Date() != d { + t.Errorf("%v != %v", vd.Date(), d) + } +} + +func TestIsToday(t *testing.T) { + today := date.Today() + + cases := []struct { + value VDate + expectYesterday bool + expectToday bool + expectTomorrow bool + }{ + {NewVDate(date.New(2012, time.June, 25)), false, false, false}, + {NewVDate(today.Add(-2)), false, false, false}, + {NewVDate(today.Add(-1)), true, false, false}, + {NewVDate(today.Add(0)), false, true, false}, + {NewVDate(today.Add(1)), false, false, true}, + {NewVDate(today.Add(2)), false, false, false}, + } + for _, c := range cases { + if c.value.IsYesterday() != c.expectYesterday { + t.Errorf("%s should be 'yesterday': %v", c.value, c.expectYesterday) + } + if c.value.IsToday() != c.expectToday { + t.Errorf("%s should be 'today': %v", c.value, c.expectToday) + } + if c.value.IsTomorrow() != c.expectTomorrow { + t.Errorf("%s should be 'tomorrow': %v", c.value, c.expectTomorrow) + } + } + +} + func TestNext(t *testing.T) { d := NewVDate(date.New(2016, 2, 7)) is(t, d.Next().Day().String(), "2016-02-08") @@ -45,6 +83,7 @@ func TestPrevious(t *testing.T) { } func is(t *testing.T, s1, s2 string) { + t.Helper() if s1 != s2 { t.Errorf("%s != %s", s1, s2) } From 6a81950173d5c2d79151abb000ccd84f6f060949 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 22 Sep 2017 10:33:45 +0100 Subject: [PATCH 075/165] Reduced the number of lint warnings --- clock/clock.go | 12 +++++------ datetool/main.go | 14 ++++++------ period/format.go | 18 ++++++++-------- timespan/daterange.go | 50 +++++++++++++++++++++---------------------- view/vdate.go | 4 ++-- 5 files changed, 49 insertions(+), 49 deletions(-) diff --git a/clock/clock.go b/clock/clock.go index c4b3b726..591563a1 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -96,13 +96,13 @@ func (c Clock) AddDuration(d time.Duration) Clock { // ModSubtract returns the duration between two clock times. // -// If c2 is before c1 (i.e. c2 < c1), the result is the duration computed from c1 - c2. +// If c2 is before c (i.e. c2 < c), the result is the duration computed from c - c2. // -// But if c1 is before c2, it is assumed that c1 is after midnight and c2 is before midnight. The -// result is the sum of the evening time from c2 to midnight with the morning time from midnight to c1. -// This is the same as Mod24(c1 - c2). -func (c1 Clock) ModSubtract(c2 Clock) time.Duration { - ms := c1 - c2 +// But if c is before c2, it is assumed that c is after midnight and c2 is before midnight. The +// result is the sum of the evening time from c2 to midnight with the morning time from midnight to c. +// This is the same as Mod24(c - c2). +func (c Clock) ModSubtract(c2 Clock) time.Duration { + ms := c - c2 return ms.Mod24().DurationSinceMidnight() } diff --git a/datetool/main.go b/datetool/main.go index bd6cb68c..05ffdf5d 100644 --- a/datetool/main.go +++ b/datetool/main.go @@ -9,7 +9,7 @@ package main import ( "fmt" - . "github.com/rickb777/date" + "github.com/rickb777/date" "github.com/rickb777/date/clock" "os" "strconv" @@ -20,11 +20,11 @@ func printPair(a string, b interface{}) { fmt.Printf("%-12s %12v\n", a, b) } -func printOneDate(s string, d Date, err error) { +func printOneDate(s string, d date.Date, err error) { if err != nil { printPair(s, err.Error()) } else { - printPair(s, d.Sub(Date{})) + printPair(s, d.Sub(date.Date{})) } } @@ -37,18 +37,18 @@ func printOneClock(s string, c clock.Clock, err error) { } func printArg(arg string) { - d := Date{} + d := date.Date{} - d, e1 := AutoParse(arg) + d, e1 := date.AutoParse(arg) if e1 == nil { - printPair(arg, d.Sub(Date{})) + printPair(arg, d.Sub(date.Date{})) } else if strings.Index(arg, ":") == 2 { c, err := clock.Parse(arg) printOneClock(arg, c, err) } else { i, err := strconv.Atoi(arg) if err == nil { - d = d.Add(PeriodOfDays(i)) + d = d.Add(date.PeriodOfDays(i)) fmt.Printf("%-12s %12s %s\n", arg, d, clock.Clock(i)) } else { printPair(arg, err) diff --git a/period/format.go b/period/format.go index 38562156..3d0bee9c 100644 --- a/period/format.go +++ b/period/format.go @@ -6,7 +6,7 @@ package period import ( "fmt" - . "github.com/rickb777/plural" + "github.com/rickb777/plural" "strings" ) @@ -16,7 +16,7 @@ func (period Period) Format() string { } // FormatWithPeriodNames converts the period to human-readable form in a localisable way. -func (period Period) FormatWithPeriodNames(yearNames, monthNames, weekNames, dayNames, hourNames, minNames, secNames Plurals) string { +func (period Period) FormatWithPeriodNames(yearNames, monthNames, weekNames, dayNames, hourNames, minNames, secNames plural.Plurals) string { period = period.Abs() parts := make([]string, 0) @@ -55,25 +55,25 @@ func appendNonBlank(parts []string, s string) []string { // PeriodDayNames provides the English default format names for the days part of the period. // This is a sequence of plurals where the first match is used, otherwise the last one is used. // The last one must include a "%v" placeholder for the number. -var PeriodDayNames = Plurals{Case{0, "%v days"}, Case{1, "%v day"}, Case{2, "%v days"}} +var PeriodDayNames = plural.Plurals{plural.Case{0, "%v days"}, plural.Case{1, "%v day"}, plural.Case{2, "%v days"}} // PeriodWeekNames is as for PeriodDayNames but for weeks. -var PeriodWeekNames = Plurals{Case{0, ""}, Case{1, "%v week"}, Case{2, "%v weeks"}} +var PeriodWeekNames = plural.Plurals{plural.Case{0, ""}, plural.Case{1, "%v week"}, plural.Case{2, "%v weeks"}} // PeriodMonthNames is as for PeriodDayNames but for months. -var PeriodMonthNames = Plurals{Case{0, ""}, Case{1, "%v month"}, Case{2, "%g months"}} +var PeriodMonthNames = plural.Plurals{plural.Case{0, ""}, plural.Case{1, "%v month"}, plural.Case{2, "%g months"}} // PeriodYearNames is as for PeriodDayNames but for years. -var PeriodYearNames = Plurals{Case{0, ""}, Case{1, "%v year"}, Case{2, "%v years"}} +var PeriodYearNames = plural.Plurals{plural.Case{0, ""}, plural.Case{1, "%v year"}, plural.Case{2, "%v years"}} // PeriodHourNames is as for PeriodDayNames but for hours. -var PeriodHourNames = Plurals{Case{0, ""}, Case{1, "%v hour"}, Case{2, "%v hours"}} +var PeriodHourNames = plural.Plurals{plural.Case{0, ""}, plural.Case{1, "%v hour"}, plural.Case{2, "%v hours"}} // PeriodMinuteNames is as for PeriodDayNames but for minutes. -var PeriodMinuteNames = Plurals{Case{0, ""}, Case{1, "%v minute"}, Case{2, "%v minutes"}} +var PeriodMinuteNames = plural.Plurals{plural.Case{0, ""}, plural.Case{1, "%v minute"}, plural.Case{2, "%v minutes"}} // PeriodSecondNames is as for PeriodDayNames but for seconds. -var PeriodSecondNames = Plurals{Case{0, ""}, Case{1, "%v second"}, Case{2, "%v seconds"}} +var PeriodSecondNames = plural.Plurals{plural.Case{0, ""}, plural.Case{1, "%v second"}, plural.Case{2, "%v seconds"}} // String converts the period to -8601 form. func (period Period) String() string { diff --git a/timespan/daterange.go b/timespan/daterange.go index 9e677e12..3cf73cb6 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -6,7 +6,7 @@ package timespan import ( "fmt" - . "github.com/rickb777/date" + "github.com/rickb777/date" "github.com/rickb777/date/period" "time" ) @@ -15,56 +15,56 @@ const minusOneNano time.Duration = -1 // DateRange carries a date and a number of days and describes a range between two dates. type DateRange struct { - mark Date - days PeriodOfDays + mark date.Date + days date.PeriodOfDays } // 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 := NewAt(start) - ed := NewAt(start.Add(duration)) - return DateRange{sd, PeriodOfDays(ed.Sub(sd))} + sd := date.NewAt(start) + ed := date.NewAt(start.Add(duration)) + return DateRange{sd, date.PeriodOfDays(ed.Sub(sd))} } // NewDateRange 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). -func NewDateRange(start, end Date) DateRange { - return DateRange{start, PeriodOfDays(end.Sub(start))}.Normalise() +func NewDateRange(start, end date.Date) DateRange { + return DateRange{start, date.PeriodOfDays(end.Sub(start))}.Normalise() } // NewYearOf constructs the range encompassing the whole year specified. func NewYearOf(year int) DateRange { - start := New(year, time.January, 1) - end := New(year+1, time.January, 1) - return DateRange{start, PeriodOfDays(end.Sub(start))} + start := date.New(year, time.January, 1) + end := date.New(year+1, time.January, 1) + return DateRange{start, date.PeriodOfDays(end.Sub(start))} } // NewMonthOf constructs the range encompassing the whole month specified for a given year. // It handles leap years correctly. func NewMonthOf(year int, month time.Month) DateRange { - start := New(year, month, 1) + start := date.New(year, month, 1) endT := time.Date(year, month+1, 1, 0, 0, 0, 0, time.UTC) - end := NewAt(endT) - return DateRange{start, PeriodOfDays(end.Sub(start))} + end := date.NewAt(endT) + return DateRange{start, date.PeriodOfDays(end.Sub(start))} } // 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) DateRange { +func EmptyRange(day date.Date) DateRange { return DateRange{day, 0} } // OneDayRange constructs a range of exactly one day. This is often a useful basis for // further operations. Note that the end date is the same as the start date. -func OneDayRange(day Date) DateRange { +func OneDayRange(day date.Date) DateRange { return DateRange{day, 1} } // Days returns the period represented by this range. This will never be negative. -func (dateRange DateRange) Days() PeriodOfDays { +func (dateRange DateRange) Days() date.PeriodOfDays { if dateRange.days < 0 { return -dateRange.days } @@ -83,9 +83,9 @@ func (dateRange DateRange) IsEmpty() bool { } // Start returns the earliest date represented by this range. -func (dateRange DateRange) Start() Date { +func (dateRange DateRange) Start() date.Date { if dateRange.days < 0 { - return dateRange.mark.Add(PeriodOfDays(1 + dateRange.days)) + return dateRange.mark.Add(date.PeriodOfDays(1 + dateRange.days)) } return dateRange.mark } @@ -94,11 +94,11 @@ func (dateRange DateRange) Start() Date { // 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(). // See also IsEmpty(). -func (dateRange DateRange) Last() Date { +func (dateRange DateRange) Last() date.Date { if dateRange.days < 0 { return dateRange.mark // because mark is at the end } else if dateRange.days == 0 { - return Date{} + return date.Date{} } return dateRange.mark.Add(dateRange.days - 1) } @@ -109,7 +109,7 @@ func (dateRange DateRange) Last() Date { // 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. -func (dateRange DateRange) End() Date { +func (dateRange DateRange) End() date.Date { if dateRange.days < 0 { return dateRange.mark.Add(1) // because mark is at the end } @@ -128,7 +128,7 @@ func (dateRange DateRange) Normalise() DateRange { // ShiftBy moves the date range by moving both the start and end dates similarly. // A negative parameter is allowed. -func (dateRange DateRange) ShiftBy(days PeriodOfDays) DateRange { +func (dateRange DateRange) ShiftBy(days date.PeriodOfDays) DateRange { if days == 0 { return dateRange } @@ -139,7 +139,7 @@ func (dateRange DateRange) ShiftBy(days PeriodOfDays) DateRange { // 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). -func (dateRange DateRange) ExtendBy(days PeriodOfDays) DateRange { +func (dateRange DateRange) ExtendBy(days date.PeriodOfDays) DateRange { if days == 0 { return dateRange } @@ -184,7 +184,7 @@ func (dateRange DateRange) String() string { // Contains tests whether the date range contains a specified date. // Empty date ranges (i.e. zero days) never contain anything. -func (dateRange DateRange) Contains(d Date) bool { +func (dateRange DateRange) Contains(d date.Date) bool { if dateRange.days == 0 { return false } diff --git a/view/vdate.go b/view/vdate.go index 9fc9889a..dbdb2a42 100644 --- a/view/vdate.go +++ b/view/vdate.go @@ -37,7 +37,7 @@ func (v VDate) Date() date.Date { // IsYesterday returns true if the date is yesterday's date. func (v VDate) IsYesterday() bool { - return v.d.DaysSinceEpoch() + 1 == date.Today().DaysSinceEpoch() + return v.d.DaysSinceEpoch()+1 == date.Today().DaysSinceEpoch() } // IsToday returns true if the date is today's date. @@ -47,7 +47,7 @@ func (v VDate) IsToday() bool { // IsTomorrow returns true if the date is tomorrow's date. func (v VDate) IsTomorrow() bool { - return v.d.DaysSinceEpoch() - 1 == date.Today().DaysSinceEpoch() + return v.d.DaysSinceEpoch()-1 == date.Today().DaysSinceEpoch() } // String formats the date in basic ISO8601 format YYYY-MM-DD. From 65dd7e686c17986b83e55b48b8cbbeeabd31e1bb Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 22 Sep 2017 11:12:32 +0100 Subject: [PATCH 076/165] Reduced the number of lint warnings again --- timespan/daterange.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/timespan/daterange.go b/timespan/daterange.go index 3cf73cb6..d6e54907 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -227,15 +227,15 @@ func (dateRange DateRange) ContainsTime(t time.Time) bool { // // Secondly, if either range is the zero value (see IsZero), it is excluded from the merge and // the other range is returned unchanged. -func (thisRange DateRange) Merge(thatRange DateRange) DateRange { - if thatRange.IsZero() { - return thisRange +func (dateRange DateRange) Merge(otherRange DateRange) DateRange { + if otherRange.IsZero() { + return dateRange } - if thisRange.IsZero() { - return thatRange + if dateRange.IsZero() { + return otherRange } - minStart := thisRange.Start().Min(thatRange.Start()) - maxEnd := thisRange.End().Max(thatRange.End()) + minStart := dateRange.Start().Min(otherRange.Start()) + maxEnd := dateRange.End().Max(otherRange.End()) return NewDateRange(minStart, maxEnd) } From 4db6498108f7fab631c131c3d11797805f708978 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 29 Dec 2017 12:38:40 +0000 Subject: [PATCH 077/165] tweaked the build script --- build+test.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/build+test.sh b/build+test.sh index 75e59170..1bf8afce 100755 --- a/build+test.sh +++ b/build+test.sh @@ -1,4 +1,5 @@ #!/bin/bash -e +cd $(dirname $0) PATH=$HOME/gopath/bin:$GOPATH/bin:$PATH if ! type -p goveralls; then From e1655561972ad25fd12ae4a64cb6cda9b0b974bd Mon Sep 17 00:00:00 2001 From: rbeton Date: Tue, 16 Jan 2018 17:44:02 +0000 Subject: [PATCH 078/165] Bug fixed in Scan method - now handles nil correctly --- sql.go | 13 +++++++++++-- sql_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/sql.go b/sql.go index 85ed0d30..310795e7 100644 --- a/sql.go +++ b/sql.go @@ -18,9 +18,17 @@ import ( // Scan parses some value. It implements sql.Scanner, // https://golang.org/pkg/database/sql/#Scanner func (d *Date) Scan(value interface{}) (err error) { + if value == nil { + return nil + } + if DisableTextStorage { return d.scanInt(value) } + return d.scanAny(value) +} + +func (d *Date) scanAny(value interface{}) (err error) { var n int64 err = nil switch value.(type) { @@ -35,8 +43,9 @@ func (d *Date) Scan(value interface{}) (err error) { case time.Time: *d = NewAt(value.(time.Time)) default: - err = fmt.Errorf("%#v", value) + err = fmt.Errorf("%T %+v is not a meaningful date", value, value) } + return } @@ -46,7 +55,7 @@ func (d *Date) scanInt(value interface{}) (err error) { case int64: *d = Date{PeriodOfDays(value.(int64))} default: - err = fmt.Errorf("%#v", value) + err = fmt.Errorf("%T %+v is not a meaningful date", value, value) } return } diff --git a/sql_test.go b/sql_test.go index 7287d787..e62d944d 100644 --- a/sql_test.go +++ b/sql_test.go @@ -54,3 +54,38 @@ func TestDateScan(t *testing.T) { DisableTextStorage = prior } + +func TestDateScanWithJunk(t *testing.T) { + cases := []struct { + v interface{} + disallow bool + expected string + }{ + {true, false, "bool true is not a meaningful date"}, + {true, true, "bool true is not a meaningful date"}, + } + + prior := DisableTextStorage + + for i, c := range cases { + DisableTextStorage = c.disallow + r := new(Date) + e := r.Scan(c.v) + if e.Error() != c.expected { + t.Errorf("%d: Got %q, want %q", i, e.Error(), c.expected) + } + } + + DisableTextStorage = prior +} + +func TestDateScanWithNil(t *testing.T) { + var r *Date + e := r.Scan(nil) + if e != nil { + t.Errorf("Got %v", e) + } + if r != nil { + t.Errorf("Got %v", r) + } +} From bcf2cbc42c2e6ccb6b1569dae6896637afbab1ba Mon Sep 17 00:00:00 2001 From: rbeton Date: Mon, 19 Feb 2018 22:30:10 +0000 Subject: [PATCH 079/165] New IsOdd method added to VDate --- view/vdate.go | 6 ++++++ view/vdate_test.go | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/view/vdate.go b/view/vdate.go index dbdb2a42..2b28abc4 100644 --- a/view/vdate.go +++ b/view/vdate.go @@ -50,6 +50,12 @@ func (v VDate) IsTomorrow() bool { return v.d.DaysSinceEpoch()-1 == date.Today().DaysSinceEpoch() } +// IsOdd returns true if the date is an odd number. This is useful for +// zebra striping etc. +func (v VDate) IsOdd() bool { + return v.d.DaysSinceEpoch() % 2 == 0 +} + // String formats the date in basic ISO8601 format YYYY-MM-DD. func (v VDate) String() string { return v.d.String() diff --git a/view/vdate_test.go b/view/vdate_test.go index 5b62d11e..eee1c90b 100644 --- a/view/vdate_test.go +++ b/view/vdate_test.go @@ -66,6 +66,28 @@ func TestIsToday(t *testing.T) { } +func TestIsOdd(t *testing.T) { + d25 := date.New(2012, time.June, 25) + + cases := []struct { + value VDate + expectOdd bool + }{ + {NewVDate(d25), true}, + {NewVDate(d25.Add(-2)), true}, + {NewVDate(d25.Add(-1)), false}, + {NewVDate(d25.Add(0)), true}, + {NewVDate(d25.Add(1)), false}, + {NewVDate(d25.Add(2)), true}, + } + for _, c := range cases { + if c.value.IsOdd() != c.expectOdd { + t.Errorf("%s should be odd: %v", c.value, c.expectOdd) + } + } + +} + func TestNext(t *testing.T) { d := NewVDate(date.New(2016, 2, 7)) is(t, d.Next().Day().String(), "2016-02-08") From b004bed477d4ef6a7d4fc72eb48f500cd3b0ffb1 Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Tue, 17 Jul 2018 17:19:14 +0100 Subject: [PATCH 080/165] Documentation updates --- README.md | 4 ++++ period/doc.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d2c23d4f..5bd1e27d 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ full documentation and examples. go get -u github.com/rickb777/date +or + + dep ensure -add github.com/rickb777/date + ## Status This library has been in reliable production use for some time. Versioning follows the well-known semantic version pattern. diff --git a/period/doc.go b/period/doc.go index 745ca4a6..4e0eaa05 100644 --- a/period/doc.go +++ b/period/doc.go @@ -8,7 +8,7 @@ // and even day lengths depends on context. So a period is not necessarily a fixed duration // of time in terms of seconds. // -// See https://en.wikipedia.org/wiki/ISO_8601#Periods +// See https://en.wikipedia.org/wiki/ISO_8601#Durations // // Example representations: // From a2b74d58c1f74ebf28422bf0a808fcc3aa57586f Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Tue, 17 Jul 2018 18:11:26 +0100 Subject: [PATCH 081/165] Added TimeSpan.Format and MarshalText methods, compatible with RFC5545. --- .gitignore | 1 + timespan/daterange_test.go | 1 + timespan/timespan.go | 23 +++++++++++++++++++++++ timespan/timespan_test.go | 23 +++++++++++++++++++++++ 4 files changed, 48 insertions(+) diff --git a/.gitignore b/.gitignore index 49ef8df4..4d166d37 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ *.so # Folders +.idea/ _obj/ _test/ vendor/ diff --git a/timespan/daterange_test.go b/timespan/daterange_test.go index 40f17034..2d916ce6 100644 --- a/timespan/daterange_test.go +++ b/timespan/daterange_test.go @@ -304,6 +304,7 @@ func TestDurationInZoneWithDaylightSaving(t *testing.T) { } func isEq(t *testing.T, a, b interface{}, msg ...interface{}) { + t.Helper() if a != b { sa := make([]string, len(msg)) for i, m := range msg { diff --git a/timespan/timespan.go b/timespan/timespan.go index 77cfeb8e..9033a1dc 100644 --- a/timespan/timespan.go +++ b/timespan/timespan.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/rickb777/date" "time" + "github.com/rickb777/date/period" ) // TimestampFormat is a simple format for date & time, "2006-01-02 15:04:05". @@ -144,3 +145,25 @@ func (ts TimeSpan) Merge(other TimeSpan) TimeSpan { return NewTimeSpan(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. +const RFC5545DateTimeLayout = "20060102T150405" + +func (ts TimeSpan) Format() string { + format := RFC5545DateTimeLayout + if ts.mark.Location().String() == "UTC" { + format = RFC5545DateTimeLayout + "Z" + } + s := ts.Start() + e := ts.End() + p := period.Between(s, e) + return fmt.Sprintf("%s/%s", s.Format(format), p) +} + +// MarshalText formats the timespan as a string. This implements +// then encoding.TextMarshaler interface. +func (ts TimeSpan) MarshalText() (text []byte, err error) { + s := ts.Format() + return []byte(s), nil +} diff --git a/timespan/timespan_test.go b/timespan/timespan_test.go index d3705dfe..4659a614 100644 --- a/timespan/timespan_test.go +++ b/timespan/timespan_test.go @@ -96,6 +96,29 @@ func TestTSString(t *testing.T) { isEq(t, s, "24h0m0s from 2015-03-27 00:00:00 to 2015-03-28 00:00:00") } +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) + + cases := []struct{ + start time.Time + duration time.Duration + exp string + }{ + {t0, time.Hour, "20150327T101314Z/PT1H"}, + {t0.In(berlin), time.Minute, "20150327T111314/PT1M"}, + } + + for _, c := range cases { + ts := TimeSpan{c.start, c.duration} + b, err := ts.MarshalText() + isEq(t, ts.Format(), c.exp) + isEq(t, err, nil) + isEq(t, string(b), c.exp) + } +} + func TestTSContains(t *testing.T) { ts := NewTimeSpan(t0327, t0329) isEq(t, ts.Contains(t0327.Add(minusOneNano)), false) From 2f7dc21b41ee48a05bfa04aef17ff750910d9cc6 Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Tue, 17 Jul 2018 23:07:31 +0100 Subject: [PATCH 082/165] Enhanced timespan Format method to give more configuration options. --- timespan/timespan.go | 61 +++++++++++++++++++++++++++++++++------ timespan/timespan_test.go | 45 ++++++++++++++++++++++++++--- 2 files changed, 93 insertions(+), 13 deletions(-) diff --git a/timespan/timespan.go b/timespan/timespan.go index 9033a1dc..3a7ad7ac 100644 --- a/timespan/timespan.go +++ b/timespan/timespan.go @@ -9,6 +9,7 @@ import ( "github.com/rickb777/date" "time" "github.com/rickb777/date/period" + "strings" ) // TimestampFormat is a simple format for date & time, "2006-01-02 15:04:05". @@ -150,20 +151,62 @@ func (ts TimeSpan) Merge(other TimeSpan) TimeSpan { // that "Z" is to be appended when the time is UTC. const RFC5545DateTimeLayout = "20060102T150405" -func (ts TimeSpan) Format() string { - format := RFC5545DateTimeLayout - if ts.mark.Location().String() == "UTC" { - format = RFC5545DateTimeLayout + "Z" +func layoutHasTimezone(layout string) bool { + return strings.IndexByte(layout, 'Z') >= 0 || strings.Contains(layout, "-07") +} + +// Format returns a textual representation of the time value formatted according to layout. +// It produces a string containing the start and end time separated by a slash. Or, if +// useDuration is true, it returns a string containing the start time and the duration, +// separated by a slash. +// +// The layout string is as specified for time.Format. If it doesn't have a timezone element +// ("07" or "Z") and the times in the timespan are UTC, the "Z" zulu indicator is added. +// THis is as required by RFC5545. Also, if the layout is blank, it defaults to +// RFC5545DateTimeLayout. +func (ts TimeSpan) Format(layout, separator string, useDuration bool) string { + if layout == "" { + layout = RFC5545DateTimeLayout + } + + // if the time is UTC and the format doesn't contain zulu field ("Z") or timezone field ("07") + if ts.mark.Location().String() == "UTC" && !layoutHasTimezone(layout) { + layout = RFC5545DateTimeLayout + "Z" } s := ts.Start() e := ts.End() - p := period.Between(s, e) - return fmt.Sprintf("%s/%s", s.Format(format), p) + if useDuration { + p := period.Between(s, e) + return fmt.Sprintf("%s%s%s", s.Format(layout), separator, p) + } + + return fmt.Sprintf("%s%s%s", s.Format(layout), separator, e.Format(layout)) } -// MarshalText formats the timespan as a string. This implements -// then encoding.TextMarshaler interface. +func (ts TimeSpan) FormatRFC5545(useDuration bool) string { + return ts.Format(RFC5545DateTimeLayout, "/", useDuration) +} + +// MarshalText formats the timespan as a string using, using RFC5545 layout. +// This implements the encoding.TextMarshaler interface. func (ts TimeSpan) MarshalText() (text []byte, err error) { - s := ts.Format() + s := ts.Format(RFC5545DateTimeLayout, "/", true) return []byte(s), nil } + +// ParseInLocation parses a string as a timespan. The string must contain either of +// +// time "/" time +// time "/" period +// +// The RFC5545 format is expected. +//func ParseInLocation(format, text string, loc *time.Location) (TimeSpan, error) { +// return TimeSpan{}, nil +//} + +// UnmarshalText parses a string as a timespan. It expects RFC5545 layout. +// This implements the encoding.TextUnmarshaler interface. +//func (ts *TimeSpan) UnmarshalText(text []byte) (err error) { +// *ts, err = ParseInLocation(RFC5545DateTimeLayout, string(text), time.UTC) +// return +//} diff --git a/timespan/timespan_test.go b/timespan/timespan_test.go index 4659a614..e222fbc7 100644 --- a/timespan/timespan_test.go +++ b/timespan/timespan_test.go @@ -101,10 +101,44 @@ func TestTSFormat(t *testing.T) { berlin, _ := time.LoadLocation("Europe/Berlin") t0 := time.Date(2015, 3, 27, 10, 13, 14, 0, time.UTC) - cases := []struct{ - start time.Time + cases := []struct { + start time.Time + duration time.Duration + useDuration bool + layout, separator, exp string + }{ + {t0, time.Hour, true, "", " for ", "20150327T101314Z for PT1H"}, + {t0, time.Hour, true, "", "/", "20150327T101314Z/PT1H"}, + {t0.In(berlin), time.Minute, true, "", "/","20150327T111314/PT1M"}, + {t0.In(berlin), time.Hour, true, "2006-01-02T15:04:05", "/","2015-03-27T11:13:14/PT1H"}, + {t0.In(berlin), time.Hour, true, "2006-01-02T15:04:05-07", "/","2015-03-27T11:13:14+01/PT1H"}, + {t0, time.Hour, true, "2006-01-02T15:04:05-07", "/","2015-03-27T10:13:14+00/PT1H"}, + {t0, time.Hour, true, "2006-01-02T15:04:05Z07", "/","2015-03-27T10:13:14Z/PT1H"}, + + {t0, time.Hour, false, "", " to ","20150327T101314Z to 20150327T111314Z"}, + {t0, time.Hour, false, "", "/","20150327T101314Z/20150327T111314Z"}, + {t0.In(berlin), time.Minute, false, "", "/","20150327T111314/20150327T111414"}, + {t0.In(berlin), time.Hour, false, "2006-01-02T15:04:05", "/","2015-03-27T11:13:14/2015-03-27T12:13:14"}, + {t0.In(berlin), time.Hour, false, "2006-01-02T15:04:05-07", "/","2015-03-27T11:13:14+01/2015-03-27T12:13:14+01"}, + {t0, time.Hour, false, "2006-01-02T15:04:05-07", "/","2015-03-27T10:13:14+00/2015-03-27T11:13:14+00"}, + {t0, time.Hour, false, "2006-01-02T15:04:05Z07", "/","2015-03-27T10:13:14Z/2015-03-27T11:13:14Z"}, + } + + for _, c := range cases { + ts := TimeSpan{c.start, c.duration} + isEq(t, ts.Format(c.layout, c.separator, c.useDuration), c.exp) + } +} + +func TestTSMarshalText(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) + + cases := []struct { + start time.Time duration time.Duration - exp string + exp string }{ {t0, time.Hour, "20150327T101314Z/PT1H"}, {t0.In(berlin), time.Minute, "20150327T111314/PT1M"}, @@ -112,8 +146,11 @@ func TestTSFormat(t *testing.T) { for _, c := range cases { ts := TimeSpan{c.start, c.duration} + + s := ts.FormatRFC5545(true) + isEq(t, s, c.exp) + b, err := ts.MarshalText() - isEq(t, ts.Format(), c.exp) isEq(t, err, nil) isEq(t, string(b), c.exp) } From 28d9e9ff80c141967ffe0bb81b671353da0555cf Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Tue, 17 Jul 2018 23:57:11 +0100 Subject: [PATCH 083/165] Added timespan.ParseRFC5545InLocation function --- timespan/timespan.go | 52 ++++++++++++++++++++++--- timespan/timespan_test.go | 80 ++++++++++++++++++++++++++++++++------- 2 files changed, 114 insertions(+), 18 deletions(-) diff --git a/timespan/timespan.go b/timespan/timespan.go index 3a7ad7ac..7a7c1b78 100644 --- a/timespan/timespan.go +++ b/timespan/timespan.go @@ -194,15 +194,57 @@ func (ts TimeSpan) MarshalText() (text []byte, err error) { return []byte(s), nil } -// ParseInLocation parses a string as a timespan. The string must contain either of +// ParseRFC5545InLocation parses a string as a timespan. The string must contain either of // // time "/" time // time "/" period // -// The RFC5545 format is expected. -//func ParseInLocation(format, text string, loc *time.Location) (TimeSpan, error) { -// return TimeSpan{}, nil -//} +// 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) { + 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:] + + st, err := parseTimeInLocation(start, loc) + if err != nil { + return TimeSpan{}, fmt.Errorf("cannot parse start time in %q: %s", text, err.Error()) + } + + if rest == "" { + return TimeSpan{}, fmt.Errorf("cannot parse %q because there is end time or duration", text) + } + + if rest[0] == 'P' { + pe, err := period.Parse(rest) + if err != nil { + return TimeSpan{}, fmt.Errorf("cannot parse period in %q: %s", text, err.Error()) + } + + du, precise := pe.Duration() + if precise { + return TimeSpan{st, du}, nil + } + + et := st.AddDate(pe.Years(), pe.Months(), pe.Days()) + return NewTimeSpan(st, et), nil + } + + et, err := parseTimeInLocation(rest, loc) + return NewTimeSpan(st, et), err +} + +func parseTimeInLocation(text string, loc *time.Location) (time.Time, error) { + if strings.HasSuffix(text, "Z") { + text = text[:len(text)-1] + return time.ParseInLocation(RFC5545DateTimeLayout, text, time.UTC) + } + return time.ParseInLocation(RFC5545DateTimeLayout, text, loc) +} // UnmarshalText parses a string as a timespan. It expects RFC5545 layout. // This implements the encoding.TextUnmarshaler interface. diff --git a/timespan/timespan_test.go b/timespan/timespan_test.go index e222fbc7..453458ab 100644 --- a/timespan/timespan_test.go +++ b/timespan/timespan_test.go @@ -109,19 +109,19 @@ func TestTSFormat(t *testing.T) { }{ {t0, time.Hour, true, "", " for ", "20150327T101314Z for PT1H"}, {t0, time.Hour, true, "", "/", "20150327T101314Z/PT1H"}, - {t0.In(berlin), time.Minute, true, "", "/","20150327T111314/PT1M"}, - {t0.In(berlin), time.Hour, true, "2006-01-02T15:04:05", "/","2015-03-27T11:13:14/PT1H"}, - {t0.In(berlin), time.Hour, true, "2006-01-02T15:04:05-07", "/","2015-03-27T11:13:14+01/PT1H"}, - {t0, time.Hour, true, "2006-01-02T15:04:05-07", "/","2015-03-27T10:13:14+00/PT1H"}, - {t0, time.Hour, true, "2006-01-02T15:04:05Z07", "/","2015-03-27T10:13:14Z/PT1H"}, - - {t0, time.Hour, false, "", " to ","20150327T101314Z to 20150327T111314Z"}, - {t0, time.Hour, false, "", "/","20150327T101314Z/20150327T111314Z"}, - {t0.In(berlin), time.Minute, false, "", "/","20150327T111314/20150327T111414"}, - {t0.In(berlin), time.Hour, false, "2006-01-02T15:04:05", "/","2015-03-27T11:13:14/2015-03-27T12:13:14"}, - {t0.In(berlin), time.Hour, false, "2006-01-02T15:04:05-07", "/","2015-03-27T11:13:14+01/2015-03-27T12:13:14+01"}, - {t0, time.Hour, false, "2006-01-02T15:04:05-07", "/","2015-03-27T10:13:14+00/2015-03-27T11:13:14+00"}, - {t0, time.Hour, false, "2006-01-02T15:04:05Z07", "/","2015-03-27T10:13:14Z/2015-03-27T11:13:14Z"}, + {t0.In(berlin), time.Minute, true, "", "/", "20150327T111314/PT1M"}, + {t0.In(berlin), time.Hour, true, "2006-01-02T15:04:05", "/", "2015-03-27T11:13:14/PT1H"}, + {t0.In(berlin), time.Hour, true, "2006-01-02T15:04:05-07", "/", "2015-03-27T11:13:14+01/PT1H"}, + {t0, time.Hour, true, "2006-01-02T15:04:05-07", "/", "2015-03-27T10:13:14+00/PT1H"}, + {t0, time.Hour, true, "2006-01-02T15:04:05Z07", "/", "2015-03-27T10:13:14Z/PT1H"}, + + {t0, time.Hour, false, "", " to ", "20150327T101314Z to 20150327T111314Z"}, + {t0, time.Hour, false, "", "/", "20150327T101314Z/20150327T111314Z"}, + {t0.In(berlin), time.Minute, false, "", "/", "20150327T111314/20150327T111414"}, + {t0.In(berlin), time.Hour, false, "2006-01-02T15:04:05", "/", "2015-03-27T11:13:14/2015-03-27T12:13:14"}, + {t0.In(berlin), time.Hour, false, "2006-01-02T15:04:05-07", "/", "2015-03-27T11:13:14+01/2015-03-27T12:13:14+01"}, + {t0, time.Hour, false, "2006-01-02T15:04:05-07", "/", "2015-03-27T10:13:14+00/2015-03-27T11:13:14+00"}, + {t0, time.Hour, false, "2006-01-02T15:04:05Z07", "/", "2015-03-27T10:13:14Z/2015-03-27T11:13:14Z"}, } for _, c := range cases { @@ -156,6 +156,60 @@ func TestTSMarshalText(t *testing.T) { } } +func TestTSParseInLocation(t *testing.T) { + // use Berlin, which is UTC-1 + berlin, _ := time.LoadLocation("Europe/Berlin") + t0120 := time.Date(2015, 1, 20, 10, 13, 14, 0, time.UTC) + t0327 := time.Date(2015, 3, 27, 10, 13, 14, 0, time.UTC) + + cases := []struct { + start time.Time + duration time.Duration + text string + }{ + {t0327, time.Hour, "20150327T101314Z/PT1H"}, + {t0327, 2*time.Second, "20150327T101314Z/PT2S"}, + {t0327.In(berlin), time.Minute, "20150327T111314/PT1M"}, + {t0327, 168*time.Hour, "20150327T101314Z/P1W"}, + {t0120.In(berlin), 168*time.Hour, "20150120T111314/P1W"}, + // This case has the daylight-savings clock shift + {t0327.In(berlin), 167*time.Hour, "20150327T111314/P1W"}, + } + + for _, c := range cases { + ts, err := ParseRFC5545InLocation(c.text, c.start.Location()) + isEq(t, err, nil) + + if !ts.Start().Equal(c.start) { + t.Errorf(ts.String()) + } + + if ts.Duration() != c.duration { + t.Errorf(ts.String()) + } + } +} + +func TestTSParseInLocationErrors(t *testing.T) { + cases := []struct { + text string + }{ + {"20150327T101314Z PT1H"}, + {"2015XX27T101314/PT1H"}, + {"20150127T101314/2016XX27T101314"}, + {"20150127T101314/P1Z"}, + {"20150327T101314Z/"}, + {"/PT1H"}, + } + + for _, c := range cases { + ts, err := ParseRFC5545InLocation(c.text, time.UTC) + if err == nil { + t.Errorf(ts.String()) + } + } +} + func TestTSContains(t *testing.T) { ts := NewTimeSpan(t0327, t0329) isEq(t, ts.Contains(t0327.Add(minusOneNano)), false) From a1faafb3ed9c53307314e8e28072c8b590266450 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 18 Jul 2018 11:39:03 +0100 Subject: [PATCH 084/165] Added functions & methods to timespan: TimeSpanOf, Equal, Format, FormatRFC5545, MarshalText, ParseRFC5545InLocation, UnmarshalText. Added more tests/test cases. --- .gitignore | 1 + README.md | 3 +- clock/clock_test.go | 2 + coverage.sh | 41 ++++++++++++++++++ date.go | 16 +++++-- date_test.go | 42 ++++++++---------- marshal_test.go | 57 +++++++++++++++++++++---- timespan/daterange.go | 1 + timespan/daterange_test.go | 14 ++++-- timespan/timespan.go | 48 ++++++++++++++++----- timespan/timespan_test.go | 87 +++++++++++++++++++++++++++++++------- 11 files changed, 245 insertions(+), 67 deletions(-) create mode 100755 coverage.sh diff --git a/.gitignore b/.gitignore index 4d166d37..0d58f8ee 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ .idea/ _obj/ _test/ +reports/ vendor/ # Architecture specific extensions/prefixes diff --git a/README.md b/README.md index 5bd1e27d..3a917bf8 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,9 @@ It also provides * `DateRange` which expresses a period between two dates. * `TimeSpan` which expresses a duration of time between two instants. - * `Period` which expresses a period corresponding to the ISO-8601 form. + * `Period` which expresses a period corresponding to the ISO-8601 form (e.g. PT1H) * `Clock` which expresses a wall-clock style hours-minutes-seconds. + * `VDate` which wraps a Date to make it easy to use in Go templates and similar view tiers. See [package documentation](https://godoc.org/github.com/rickb777/date) for full documentation and examples. diff --git a/clock/clock_test.go b/clock/clock_test.go index 93ae4c01..e9bdc675 100644 --- a/clock/clock_test.go +++ b/clock/clock_test.go @@ -269,12 +269,14 @@ func TestClockParseGoods(t *testing.T) { {"01:00", New(1, 0, 0, 0)}, {"01:02", New(1, 2, 0, 0)}, {"23:59", New(23, 59, 0, 0)}, + {"2359", New(23, 59, 0, 0)}, {"00:00:00", New(0, 0, 0, 0)}, {"00:00:01", New(0, 0, 1, 0)}, {"00:01:00", New(0, 1, 0, 0)}, {"01:00:00", New(1, 0, 0, 0)}, {"01:02:03", New(1, 2, 3, 0)}, {"23:59:59", New(23, 59, 59, 0)}, + {"235959", New(23, 59, 59, 0)}, {"00:00:00.000", New(0, 0, 0, 0)}, {"00:00:00.001", New(0, 0, 0, 1)}, {"00:00:01.000", New(0, 0, 1, 0)}, diff --git a/coverage.sh b/coverage.sh new file mode 100755 index 00000000..b8b78321 --- /dev/null +++ b/coverage.sh @@ -0,0 +1,41 @@ +#!/bin/bash -e +# Developer tool to run the tests and obtain HTML coverage reports. + +DIR=$PWD +DOT=$(dirname $0) +cd $DOT +TOP=$PWD + +# install Goveralls if absent +if ! type -p goveralls; then + echo go get github.com/mattn/goveralls + go get github.com/mattn/goveralls +fi + +mkdir -p reports +rm -f reports/*.html coverage?*.* + +for file in $(find . -type f -name \*_test.go | fgrep -v vendor/); do + dirname $file >> coverage$$.tmp +done + +sort coverage$$.tmp | uniq | tee coverage$$.dirs + +for pkg in $(cat coverage$$.dirs); do + name=$(echo $pkg | sed 's#^./##' | sed 's#/#-#g') + [ "$pkg" = "." ] && name=$(basename $PWD) + echo $pkg becomes $name + go test -v -coverprofile coverage$$.data $pkg + if [ -f coverage$$.data ]; then + go tool cover -html coverage$$.data -o reports/$name.html + unlink coverage$$.data + fi +done + +rm -f coverage$$.tmp coverage$$.dirs + +if [ -n "$(type -p chromium-browser)" ]; then + chromium-browser reports/*.html >/dev/null & +else + ls -lh reports/ +fi diff --git a/date.go b/date.go index 35ed8352..3893228b 100644 --- a/date.go +++ b/date.go @@ -239,12 +239,20 @@ func (d Date) AddDate(years, months, days int) Date { } // AddPeriod returns the date corresponding to adding the given period. If the -// period's fields are be negative, this results in an earlier date. Any time -// component is ignored. +// period's fields are be negative, this results in an earlier date. +// +// Any time component is ignored. Therefore, be careful with periods containing +// more that 24 hours in the hours/minutes/seconds fields. These will not be +// normalised for you; if you want this behaviour, call delta.Normalise(false) +// on the input parameter. +// +// For example, PT24H adds nothing, whereas P1D adds one day as expected. To +// convert a period such as PT24H to its equivalent P1D, use +// delta.Normalise(false) as the input. // // See the description for AddDate. -func (d Date) AddPeriod(period period.Period) Date { - return d.AddDate(period.Years(), period.Months(), period.Days()) +func (d Date) AddPeriod(delta period.Period) Date { + return d.AddDate(delta.Years(), delta.Months(), delta.Days()) } // Sub returns d-u as the number of days between the two dates. diff --git a/date_test.go b/date_test.go index 18f89fbe..23a0e608 100644 --- a/date_test.go +++ b/date_test.go @@ -240,34 +240,26 @@ func TestAddDate(t *testing.T) { } } -func xTestAddDuration(t *testing.T) { +func TestAddPeriod(t *testing.T) { cases := []struct { - year int - month time.Month - day int + in Date + delta period.Period + expected Date }{ - {-1234, time.February, 5}, - {0, time.April, 12}, - {1, time.January, 1}, - {1946, time.February, 4}, - {1970, time.January, 1}, - {1976, time.April, 1}, - {1999, time.December, 1}, - {1111111, time.June, 21}, + {New(1970, time.January, 1), period.NewYMD(0, 0, 0), New(1970, time.January, 1)}, + {New(1971, time.January, 1), period.NewYMD(10, 0, 0), New(1981, time.January, 1)}, + {New(1972, time.January, 1), period.NewYMD(0, 10, 0), New(1972, time.November, 1)}, + {New(1972, time.January, 1), period.NewYMD(0, 24, 0), New(1974, time.January, 1)}, + {New(1973, time.January, 1), period.NewYMD(0, 0, 10), New(1973, time.January, 11)}, + {New(1973, time.January, 1), period.NewYMD(0, 0, 365), New(1974, time.January, 1)}, + {New(1974, time.January, 1), period.NewHMS(1, 2, 3), New(1974, time.January, 1)}, + // note: the period is not normalised so the HMS is ignored even though it's more than one day + {New(1975, time.January, 1), period.NewHMS(24, 2, 3), New(1975, time.January, 1)}, } - offsets := []PeriodOfDays{-1000000, -9999, -555, -99, -22, -1, 0, 1, 22, 99, 555, 9999, 1000000} - for _, c := range cases { - di := New(c.year, c.month, c.day) - for _, days := range offsets { - dj := di.AddPeriod(period.New(0, 0, int(days), 0, 0, 0)) - days2 := dj.Sub(di) - if days2 != days { - t.Errorf("AddSub(%v,%v) == %v, want %v", di, days, days2, days) - } - dk := dj.AddPeriod(period.New(0, 0, -int(days), 0, 0, 0)) - if dk != di { - t.Errorf("AddNeg(%v,%v) == %v, want %v", di, days, dk, di) - } + 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) } } } diff --git a/marshal_test.go b/marshal_test.go index 1fe80614..153c4d4f 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -56,13 +56,13 @@ func TestDateJSONMarshalling(t *testing.T) { } for _, c := range cases { var d Date - bytes, err := json.Marshal(c.value) + bb, err := json.Marshal(c.value) if err != nil { t.Errorf("JSON(%v) marshal error %v", c, err) - } else if string(bytes) != c.want { - t.Errorf("JSON(%v) == %v, want %v", c.value, string(bytes), c.want) + } else if string(bb) != c.want { + t.Errorf("JSON(%v) == %v, want %v", c.value, string(bb), c.want) } else { - err = json.Unmarshal(bytes, &d) + err = json.Unmarshal(bb, &d) if err != nil { t.Errorf("JSON(%v) unmarshal error %v", c.value, err) } else if d != c.value { @@ -87,13 +87,13 @@ func TestDateTextMarshalling(t *testing.T) { } for _, c := range cases { var d Date - bytes, err := c.value.MarshalText() + bb, err := c.value.MarshalText() if err != nil { t.Errorf("Text(%v) marshal error %v", c, err) - } else if string(bytes) != c.want { - t.Errorf("Text(%v) == %v, want %v", c.value, string(bytes), c.want) + } else if string(bb) != c.want { + t.Errorf("Text(%v) == %v, want %v", c.value, string(bb), c.want) } else { - err = d.UnmarshalText(bytes) + err = d.UnmarshalText(bb) if err != nil { t.Errorf("Text(%v) unmarshal error %v", c.value, err) } else if d != c.value { @@ -103,6 +103,47 @@ func TestDateTextMarshalling(t *testing.T) { } } +func TestDateBinaryMarshalling(t *testing.T) { + cases := []struct { + value Date + }{ + {New(-11111, time.February, 3)}, + {New(-1, time.December, 31)}, + {New(0, time.January, 1)}, + {New(1, time.January, 1)}, + {New(1970, time.January, 1)}, + {New(2012, time.June, 25)}, + {New(12345, time.June, 7)}, + } + for _, c := range cases { + bb, err := c.value.MarshalBinary() + if err != nil { + t.Errorf("Binary(%v) marshal error %v", c, err) + } else { + var d Date + err = d.UnmarshalBinary(bb) + if err != nil { + t.Errorf("Binary(%v) unmarshal error %v", c.value, err) + } else if d != c.value { + t.Errorf("Binary(%v) unmarshal got %v", c.value, d) + } + } + } +} + +func TestDateBinaryUnmarshallingErrors(t *testing.T) { + var d Date + err1 := d.UnmarshalBinary([]byte{}) + if err1 == nil { + t.Errorf("unmarshal no empty data error") + } + + err2 := d.UnmarshalBinary([]byte("12345")) + if err2 == nil { + t.Errorf("unmarshal no wrong length error") + } +} + func TestInvalidDateText(t *testing.T) { cases := []struct { value string diff --git a/timespan/daterange.go b/timespan/daterange.go index d6e54907..ac755697 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -31,6 +31,7 @@ func NewDateRangeOf(start time.Time, duration time.Duration) DateRange { // NewDateRange 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 { return DateRange{start, date.PeriodOfDays(end.Sub(start))}.Normalise() } diff --git a/timespan/daterange_test.go b/timespan/daterange_test.go index 2d916ce6..3fa220f4 100644 --- a/timespan/daterange_test.go +++ b/timespan/daterange_test.go @@ -8,13 +8,13 @@ import ( "fmt" . "github.com/rickb777/date" "github.com/rickb777/date/period" - "runtime/debug" "strings" "testing" "time" ) var d0320 = New(2015, time.March, 20) +var d0321 = New(2015, time.March, 21) var d0325 = New(2015, time.March, 25) var d0326 = New(2015, time.March, 26) var d0327 = New(2015, time.March, 27) @@ -43,13 +43,21 @@ func mustLoadLocation(name string) *time.Location { } func TestNewDateRangeOf(t *testing.T) { - dr := NewDateRangeOf(t0327, time.Duration(7*24*60*60*1e9)) + dr := NewDateRangeOf(t0327, 7*24*time.Hour) isEq(t, dr.mark, d0327) isEq(t, dr.Days(), PeriodOfDays(7)) isEq(t, dr.IsEmpty(), false) isEq(t, dr.Start(), d0327) isEq(t, dr.Last(), d0402) isEq(t, dr.End(), d0403) + + dr2 := NewDateRangeOf(t0327, -7*24*time.Hour) + isEq(t, dr2.mark, d0327) + isEq(t, dr2.Days(), PeriodOfDays(7)) + isEq(t, dr2.IsEmpty(), false) + isEq(t, dr2.Start(), d0321) + isEq(t, dr2.Last(), d0327) + isEq(t, dr2.End(), d0328) } func TestNewDateRangeWithNormalise(t *testing.T) { @@ -310,6 +318,6 @@ func isEq(t *testing.T, a, b interface{}, msg ...interface{}) { for i, m := range msg { sa[i] = fmt.Sprintf(", %v", m) } - t.Errorf("%v (%#v) is not equal to %v (%#v)%s\n%s", a, a, b, b, strings.Join(sa, ""), debug.Stack()) + t.Errorf("%+v is not equal to %+v%s", a, b, strings.Join(sa, "")) } } diff --git a/timespan/timespan.go b/timespan/timespan.go index 7a7c1b78..f37c2d4a 100644 --- a/timespan/timespan.go +++ b/timespan/timespan.go @@ -29,6 +29,11 @@ func ZeroTimeSpan(start time.Time) TimeSpan { return TimeSpan{start, 0} } +// TimeSpanOf creates a new time span at a specified time and duration. +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 // order; the result will be normalised. The inputs are half-open: the start is included and // the end is excluded. @@ -155,15 +160,25 @@ func layoutHasTimezone(layout string) bool { return strings.IndexByte(layout, 'Z') >= 0 || strings.Contains(layout, "-07") } +// Equal reports whether ts and us represent the same time start and duration. +// Two times can be equal even if they are in different locations. +// For example, 6:00 +0200 CEST and 4:00 UTC are Equal. +func (ts TimeSpan) Equal(us TimeSpan) bool { + return ts.Duration() == us.Duration() && ts.Start().Equal(us.Start()) +} + // Format returns a textual representation of the time value formatted according to layout. -// It produces a string containing the start and end time separated by a slash. Or, if -// useDuration is true, it returns a string containing the start time and the duration, -// separated by a slash. +// It produces a string containing the start and end time. Or, if useDuration is true, +// it returns a string containing the start time and the duration. // // The layout string is as specified for time.Format. If it doesn't have a timezone element // ("07" or "Z") and the times in the timespan are UTC, the "Z" zulu indicator is added. -// THis is as required by RFC5545. Also, if the layout is blank, it defaults to -// RFC5545DateTimeLayout. +// This is as required by iCalendar (RFC5545). +// +// Also, if the layout is blank, it defaults to RFC5545DateTimeLayout. +// +// The separator between the two parts of the result would be "/" for RFC5545, but can be +// anything. func (ts TimeSpan) Format(layout, separator string, useDuration bool) string { if layout == "" { layout = RFC5545DateTimeLayout @@ -199,8 +214,9 @@ func (ts TimeSpan) MarshalText() (text []byte, err error) { // time "/" time // time "/" period // -// The specified location will be used for the resulting times; this behaves the same -// as time.ParseInLocation. +// 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) { slash := strings.IndexByte(text, '/') if slash < 0 { @@ -215,6 +231,8 @@ func ParseRFC5545InLocation(text string, loc *time.Location) (TimeSpan, error) { return TimeSpan{}, fmt.Errorf("cannot parse start time in %q: %s", text, err.Error()) } + //fmt.Printf("got %20s %s\n", st.Location(), st.Format(RFC5545DateTimeLayout)) + if rest == "" { return TimeSpan{}, fmt.Errorf("cannot parse %q because there is end time or duration", text) } @@ -247,8 +265,16 @@ 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 implements the encoding.TextUnmarshaler interface. -//func (ts *TimeSpan) UnmarshalText(text []byte) (err error) { -// *ts, err = ParseInLocation(RFC5545DateTimeLayout, string(text), time.UTC) -// return -//} +func (ts *TimeSpan) UnmarshalText(text []byte) (err error) { + loc := time.Local + if ts != nil { + loc = ts.mark.Location() + } + *ts, err = ParseRFC5545InLocation(string(text), loc) + return +} diff --git a/timespan/timespan_test.go b/timespan/timespan_test.go index 453458ab..e7a0f78a 100644 --- a/timespan/timespan_test.go +++ b/timespan/timespan_test.go @@ -96,6 +96,50 @@ func TestTSString(t *testing.T) { isEq(t, s, "24h0m0s from 2015-03-27 00:00:00 to 2015-03-28 00:00:00") } +func TestTSEqual(t *testing.T) { + // use Berlin, which is UTC+1/+2 + berlin, _ := time.LoadLocation("Europe/Berlin") + t0 := time.Date(2015, 2, 20, 10, 13, 25, 0, time.UTC) + t1 := t0.Add(time.Hour) + z0 := ZeroTimeSpan(t0) + ts1 := z0.ExtendBy(time.Hour) + + cases := []struct { + a, b TimeSpan + }{ + {z0, NewTimeSpan(t0, t0)}, + {z0, z0.In(berlin)}, + {ts1, ts1}, + {ts1, NewTimeSpan(t0, t1)}, + {ts1, ts1.In(berlin)}, + {ts1, 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) + } + } +} + +func TestTSNotEqual(t *testing.T) { + t0 := time.Date(2015, 2, 20, 10, 13, 25, 0, time.UTC) + t1 := t0.Add(time.Hour) + + cases := []struct { + a, b TimeSpan + }{ + {ZeroTimeSpan(t0), TimeSpanOf(t0, time.Hour)}, + {ZeroTimeSpan(t0), ZeroTimeSpan(t1)}, + } + + 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) + } + } +} + func TestTSFormat(t *testing.T) { // use Berlin, which is UTC-1 berlin, _ := time.LoadLocation("Europe/Berlin") @@ -160,39 +204,52 @@ func TestTSParseInLocation(t *testing.T) { // use Berlin, which is UTC-1 berlin, _ := time.LoadLocation("Europe/Berlin") t0120 := time.Date(2015, 1, 20, 10, 13, 14, 0, time.UTC) - t0327 := time.Date(2015, 3, 27, 10, 13, 14, 0, time.UTC) + // just before start of daylight savings + t0325 := time.Date(2015, 3, 25, 10, 13, 14, 0, time.UTC) cases := []struct { start time.Time duration time.Duration text string }{ - {t0327, time.Hour, "20150327T101314Z/PT1H"}, - {t0327, 2*time.Second, "20150327T101314Z/PT2S"}, - {t0327.In(berlin), time.Minute, "20150327T111314/PT1M"}, - {t0327, 168*time.Hour, "20150327T101314Z/P1W"}, - {t0120.In(berlin), 168*time.Hour, "20150120T111314/P1W"}, + {t0325, time.Hour, "20150325T101314Z/PT1H"}, + {t0325, 2 * time.Second, "20150325T101314Z/PT2S"}, + {t0120.In(berlin), time.Minute, "20150120T111314/PT1M"}, + {t0325, 336 * time.Hour, "20150325T101314Z/P2W"}, + {t0120.In(berlin), 72*time.Hour, "20150120T111314/P3D"}, // This case has the daylight-savings clock shift - {t0327.In(berlin), 167*time.Hour, "20150327T111314/P1W"}, + {t0325.In(berlin), 167*time.Hour, "20150325T111314/P1W"}, } - for _, c := range cases { - ts, err := ParseRFC5545InLocation(c.text, c.start.Location()) - isEq(t, err, nil) + for i, c := range cases { + ts1, err := ParseRFC5545InLocation(c.text, c.start.Location()) + if err != nil { + t.Errorf("%d: %s %v %v", i, c.text, ts1.String(), err) + } - if !ts.Start().Equal(c.start) { - t.Errorf(ts.String()) + if !ts1.Start().Equal(c.start) { + t.Errorf("%d: %s", i, ts1) } - if ts.Duration() != c.duration { - t.Errorf(ts.String()) + if ts1.Duration() != c.duration { + t.Errorf("%d: %s", i, ts1) + } + + ts2 := TimeSpan{}.In(c.start.Location()) + err = ts2.UnmarshalText([]byte(c.text)) + if err != nil { + t.Errorf("%d: %s: %v %v", i, c.text, ts2.String(), err) + } + + if !ts1.Equal(ts2) { + t.Errorf("%d: %s: %v is not equal to %v", i, c.text, ts1, ts2) } } } func TestTSParseInLocationErrors(t *testing.T) { cases := []struct { - text string + text string }{ {"20150327T101314Z PT1H"}, {"2015XX27T101314/PT1H"}, From de3500f13f718ce74b1a227e0bca39fa20b2bdee Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 18 Jul 2018 12:17:19 +0100 Subject: [PATCH 085/165] Upgraded to plural v1.2.0. Added more tests/test cases. --- Gopkg.lock | 6 +- Gopkg.toml | 2 +- period/format.go | 43 +++++++------- period/period.go | 12 ++-- period/period_test.go | 128 +++++++++++++++++++++--------------------- timespan/daterange.go | 25 ++++++--- 6 files changed, 116 insertions(+), 100 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index afd0f5d1..384f2550 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -4,12 +4,12 @@ [[projects]] name = "github.com/rickb777/plural" packages = ["."] - revision = "a4d8479dc7ebd80a2cbe436c908b3e8ce28d7b22" - version = "v1.0.1" + revision = "7589705ae1b0a218b5389c04b1505e0c8defbc1f" + version = "v1.2.0" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "db0a171068f0d4c1006d9df3c91278c67ddd77351c97a73786d90b2b432d7f3d" + inputs-digest = "1f971ef25906ec07445b5e0dbbb90e964612f80d60ae1ab8db48337a5fed448d" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 2dd6c702..b13cc562 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -2,4 +2,4 @@ [[constraint]] name = "github.com/rickb777/plural" - version = "^1.0.0" + version = "^1.2.0" diff --git a/period/format.go b/period/format.go index 3d0bee9c..8f1236a2 100644 --- a/period/format.go +++ b/period/format.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/rickb777/plural" "strings" + "bytes" ) // Format converts the period to human-readable form using the default localisation. @@ -55,64 +56,66 @@ func appendNonBlank(parts []string, s string) []string { // PeriodDayNames provides the English default format names for the days part of the period. // This is a sequence of plurals where the first match is used, otherwise the last one is used. // The last one must include a "%v" placeholder for the number. -var PeriodDayNames = plural.Plurals{plural.Case{0, "%v days"}, plural.Case{1, "%v day"}, plural.Case{2, "%v days"}} +var PeriodDayNames = plural.FromZero("%v days", "%v day", "%v days") // PeriodWeekNames is as for PeriodDayNames but for weeks. -var PeriodWeekNames = plural.Plurals{plural.Case{0, ""}, plural.Case{1, "%v week"}, plural.Case{2, "%v weeks"}} +var PeriodWeekNames = plural.FromZero("", "%v week", "%v weeks") // PeriodMonthNames is as for PeriodDayNames but for months. -var PeriodMonthNames = plural.Plurals{plural.Case{0, ""}, plural.Case{1, "%v month"}, plural.Case{2, "%g months"}} +var PeriodMonthNames = plural.FromZero("", "%v month", "%v months") // PeriodYearNames is as for PeriodDayNames but for years. -var PeriodYearNames = plural.Plurals{plural.Case{0, ""}, plural.Case{1, "%v year"}, plural.Case{2, "%v years"}} +var PeriodYearNames = plural.FromZero("", "%v year", "%v years") // PeriodHourNames is as for PeriodDayNames but for hours. -var PeriodHourNames = plural.Plurals{plural.Case{0, ""}, plural.Case{1, "%v hour"}, plural.Case{2, "%v hours"}} +var PeriodHourNames = plural.FromZero("", "%v hour", "%v hours") // PeriodMinuteNames is as for PeriodDayNames but for minutes. -var PeriodMinuteNames = plural.Plurals{plural.Case{0, ""}, plural.Case{1, "%v minute"}, plural.Case{2, "%v minutes"}} +var PeriodMinuteNames = plural.FromZero("", "%v minute", "%v minutes") // PeriodSecondNames is as for PeriodDayNames but for seconds. -var PeriodSecondNames = plural.Plurals{plural.Case{0, ""}, plural.Case{1, "%v second"}, plural.Case{2, "%v seconds"}} +var PeriodSecondNames = plural.FromZero("", "%v second", "%v seconds") -// String converts the period to -8601 form. +// String converts the period to ISO-8601 form. func (period Period) String() string { if period.IsZero() { return "P0D" } - s := "" + buf := &bytes.Buffer{} if period.Sign() < 0 { - s = "-" + buf.WriteByte('-') } - y, m, w, d, t, hh, mm, ss := "", "", "", "", "", "", "", "" + buf.WriteByte('P') if period.years != 0 { - y = fmt.Sprintf("%gY", absFloat10(period.years)) + fmt.Fprintf(buf, "%gY", absFloat10(period.years)) } if period.months != 0 { - m = fmt.Sprintf("%gM", absFloat10(period.months)) + fmt.Fprintf(buf, "%gM", absFloat10(period.months)) } if period.days != 0 { - if period.days != 0 { - d = fmt.Sprintf("%gD", absFloat10(period.days)) + if period.days%70 == 0 { + fmt.Fprintf(buf, "%gW", absFloat10(period.days/7)) + } else { + fmt.Fprintf(buf, "%gD", absFloat10(period.days)) } } if period.hours != 0 || period.minutes != 0 || period.seconds != 0 { - t = "T" + buf.WriteByte('T') } if period.hours != 0 { - hh = fmt.Sprintf("%gH", absFloat10(period.hours)) + fmt.Fprintf(buf, "%gH", absFloat10(period.hours)) } if period.minutes != 0 { - mm = fmt.Sprintf("%gM", absFloat10(period.minutes)) + fmt.Fprintf(buf, "%gM", absFloat10(period.minutes)) } if period.seconds != 0 { - ss = fmt.Sprintf("%gS", absFloat10(period.seconds)) + fmt.Fprintf(buf, "%gS", absFloat10(period.seconds)) } - return fmt.Sprintf("%sP%s%s%s%s%s%s%s%s", s, y, m, w, d, t, hh, mm, ss) + return buf.String() } func absFloat10(v int16) float32 { diff --git a/period/period.go b/period/period.go index a2f72a6b..6c836c97 100644 --- a/period/period.go +++ b/period/period.go @@ -15,16 +15,18 @@ const oneE5 = 100000 const oneE6 = 1000000 // Period holds a period of time and provides conversion to/from ISO-8601 representations. +// Therefore there are six fields: years, months, days, hours, minutes, and seconds. +// // In the ISO representation, decimal fractions are supported, although only the last non-zero // component is allowed to have a fraction according to the Standard. For example "P2.5Y" // is 2.5 years. // -// In this implementation, the precision is limited to one decimal place only, by means -// of integers with fixed point arithmetic. This avoids using float32 in the struct, so -// there are no problems testing equality using ==. +// However, in this implementation, the precision is limited to one decimal place only, by +// means of integers with fixed point arithmetic. (This avoids using float32 in the struct, +// so there are no problems testing equality using ==.) // -// The implementation limits the range of possible values to ± 2^16 / 10. Note in -// particular that the range of years is limited to approximately ± 3276. +// The implementation limits the range of possible values to ± 2^16 / 10 in each field. +// Note in particular that the range of years is limited to approximately ± 3276. // // The concept of weeks exists in string representations of periods, but otherwise weeks // are unimportant. The period contains a number of days from which the number of weeks can diff --git a/period/period_test.go b/period/period_test.go index ebe86e63..bd0e17da 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -45,10 +45,10 @@ func TestParsePeriod(t *testing.T) { {"P1Y2.15M", Period{10, 21, 0, 0, 0, 0}}, {"P1Y2.125M", Period{10, 21, 0, 0, 0, 0}}, } - for _, c := range cases { + for i, c := range cases { d := MustParse(c.value) if d != c.period { - t.Errorf("MustParsePeriod(%v) == %#v, want (%#v)", c.value, d, c.period) + t.Errorf("%d: MustParsePeriod(%v) == %#v, want (%#v)", i, c.value, d, c.period) } } @@ -56,10 +56,10 @@ func TestParsePeriod(t *testing.T) { "13M", "P", } - for _, c := range badCases { + for i, c := range badCases { d, err := Parse(c) if err == nil { - t.Errorf("ParsePeriod(%v) == %v", c, d) + t.Errorf("%d: ParsePeriod(%v) == %v", i, c, d) } } } @@ -69,13 +69,15 @@ func TestPeriodString(t *testing.T) { value string period Period }{ - {"P0D", Period{}}, - {"P3Y", Period{30, 0, 0, 0, 0, 0}}, - {"-P3Y", Period{-30, 0, 0, 0, 0, 0}}, - {"P6M", Period{0, 60, 0, 0, 0, 0}}, - {"-P6M", Period{0, -60, 0, 0, 0, 0}}, - {"P35D", Period{0, 0, 350, 0, 0, 0}}, - {"-P35D", Period{0, 0, -350, 0, 0, 0}}, + //{"P0D", Period{}}, + //{"P3Y", Period{30, 0, 0, 0, 0, 0}}, + //{"-P3Y", Period{-30, 0, 0, 0, 0, 0}}, + //{"P6M", Period{0, 60, 0, 0, 0, 0}}, + //{"-P6M", Period{0, -60, 0, 0, 0, 0}}, + //{"P35D", Period{0, 0, 350, 0, 0, 0}}, + //{"-P35D", Period{0, 0, -350, 0, 0, 0}}, + {"P4W", Period{0, 0, 280, 0, 0, 0}}, + {"-P4W", Period{0, 0, -280, 0, 0, 0}}, {"P4D", Period{0, 0, 40, 0, 0, 0}}, {"-P4D", Period{0, 0, -40, 0, 0, 0}}, {"PT12H", Period{0, 0, 0, 120, 0, 0}}, @@ -85,10 +87,10 @@ func TestPeriodString(t *testing.T) { {"-P3Y6M39DT1H2M4S", Period{-30, -60, -390, 10, 20, 40}}, {"P2.5Y", Period{25, 0, 0, 0, 0, 0}}, } - for _, c := range cases { + for i, c := range cases { s := c.period.String() if s != c.value { - t.Errorf("String() == %s, want %s for %+v", s, c.value, c.period) + t.Errorf("%d: String() == %s, want %s for %+v", i, s, c.value, c.period) } } } @@ -111,31 +113,31 @@ func TestPeriodComponents(t *testing.T) { {"PT30M", 0, 0, 0, 0, 0, 0, 30, 0}, {"PT5S", 0, 0, 0, 0, 0, 0, 0, 5}, } - for _, c := range cases { + for i, c := range cases { p := MustParse(c.value) if p.Years() != c.y { - t.Errorf("%s.Years() == %d, want %d", c.value, p.Years(), c.y) + t.Errorf("%d: %s.Years() == %d, want %d", i, c.value, p.Years(), c.y) } if p.Months() != c.m { - t.Errorf("%s.Months() == %d, want %d", c.value, p.Months(), c.m) + t.Errorf("%d: %s.Months() == %d, want %d", i, c.value, p.Months(), c.m) } if p.Weeks() != c.w { - t.Errorf("%s.Weeks() == %d, want %d", c.value, p.Weeks(), c.w) + t.Errorf("%d: %s.Weeks() == %d, want %d", i, c.value, p.Weeks(), c.w) } if p.Days() != c.d { - t.Errorf("%s.Days() == %d, want %d", c.value, p.Days(), c.d) + t.Errorf("%d: %s.Days() == %d, want %d", i, c.value, p.Days(), c.d) } if p.ModuloDays() != c.dx { - t.Errorf("%s.ModuloDays() == %d, want %d", c.value, p.ModuloDays(), c.dx) + t.Errorf("%d: %s.ModuloDays() == %d, want %d", i, c.value, p.ModuloDays(), c.dx) } if p.Hours() != c.hh { - t.Errorf("%s.Hours() == %d, want %d", c.value, p.Hours(), c.hh) + t.Errorf("%d: %s.Hours() == %d, want %d", i, c.value, p.Hours(), c.hh) } if p.Minutes() != c.mm { - t.Errorf("%s.Minutes() == %d, want %d", c.value, p.Minutes(), c.mm) + t.Errorf("%d: %s.Minutes() == %d, want %d", i, c.value, p.Minutes(), c.mm) } if p.Seconds() != c.ss { - t.Errorf("%s.Seconds() == %d, want %d", c.value, p.Seconds(), c.ss) + t.Errorf("%d: %s.Seconds() == %d, want %d", i, c.value, p.Seconds(), c.ss) } } } @@ -155,14 +157,14 @@ func TestPeriodToDuration(t *testing.T) { {"P1Y", oneYearApprox, false}, {"-P1Y", -oneYearApprox, false}, } - for _, c := range cases { + for i, c := range cases { p := MustParse(c.value) s, prec := p.Duration() if s != c.duration { - t.Errorf("Duration() == %s %v, want %s for %+v", s, prec, c.duration, c.value) + t.Errorf("%d: Duration() == %s %v, want %s for %+v", i, s, prec, c.duration, c.value) } if prec != c.precise { - t.Errorf("Duration() == %s %v, want %v for %+v", s, prec, c.precise, c.value) + t.Errorf("%d: Duration() == %s %v, want %v for %+v", i, s, prec, c.precise, c.value) } } } @@ -180,11 +182,11 @@ func TestPeriodApproxDays(t *testing.T) { {"P1Y", 365}, {"-P1Y", -365}, } - for _, c := range cases { + for i, c := range cases { p := MustParse(c.value) td := p.TotalDaysApprox() if td != c.approxDays { - t.Errorf("%v.TotalDaysApprox() == %v, want %v", p, td, c.approxDays) + t.Errorf("%d: %v.TotalDaysApprox() == %v, want %v", i, p, td, c.approxDays) } } } @@ -206,11 +208,11 @@ func TestPeriodApproxMonths(t *testing.T) { {"PT24H", 0}, {"PT744H", 1}, } - for _, c := range cases { + for i, c := range cases { p := MustParse(c.value) td := p.TotalMonthsApprox() if td != c.approxMonths { - t.Errorf("%v.TotalMonthsApprox() == %v, want %v", p, td, c.approxMonths) + t.Errorf("%d: %v.TotalMonthsApprox() == %v, want %v", i, p, td, c.approxMonths) } } } @@ -235,19 +237,19 @@ func TestNewPeriod(t *testing.T) { {0, -1, 0, 0, 0, 0, Period{0, -10, 0, 0, 0, 0}}, {-1, 0, 0, 0, 0, 0, Period{-10, 0, 0, 0, 0, 0}}, } - for _, c := range cases { + for i, c := range cases { p := New(c.years, c.months, c.days, c.hours, c.minutes, c.seconds) if p != c.period { - t.Errorf("%d,%d,%d gives %#v, want %#v", c.years, c.months, c.days, p, c.period) + t.Errorf("%d: %d,%d,%d gives %#v, want %#v", i, c.years, c.months, c.days, p, c.period) } if p.Years() != c.years { - t.Errorf("%#v, got %d want %d", p, p.Years(), c.years) + t.Errorf("%d: %#v, got %d want %d", i, p, p.Years(), c.years) } if p.Months() != c.months { - t.Errorf("%#v, got %d want %d", p, p.Months(), c.months) + t.Errorf("%d: %#v, got %d want %d", i, p, p.Months(), c.months) } if p.Days() != c.days { - t.Errorf("%#v, got %d want %d", p, p.Days(), c.days) + t.Errorf("%d: %#v, got %d want %d", i, p, p.Days(), c.days) } } } @@ -265,19 +267,19 @@ func TestNewHMS(t *testing.T) { {0, -1, 0, Period{0, 0, 0, 0, -10, 0}}, {-1, 0, 0, Period{0, 0, 0, -10, 0, 0}}, } - for _, c := range cases { + for i, c := range cases { p := NewHMS(c.hours, c.minutes, c.seconds) if p != c.period { - t.Errorf("gives %#v, want %#v", p, c.period) + t.Errorf("%d: gives %#v, want %#v", i, p, c.period) } if p.Hours() != c.hours { - t.Errorf("%#v, got %d want %d", p, p.Years(), c.hours) + t.Errorf("%d: %#v, got %d want %d", i, p, p.Years(), c.hours) } if p.Minutes() != c.minutes { - t.Errorf("%#v, got %d want %d", p, p.Months(), c.minutes) + t.Errorf("%d: %#v, got %d want %d", i, p, p.Months(), c.minutes) } if p.Seconds() != c.seconds { - t.Errorf("%#v, got %d want %d", p, p.Days(), c.seconds) + t.Errorf("%d: %#v, got %d want %d", i, p, p.Days(), c.seconds) } } } @@ -296,19 +298,19 @@ func TestNewYMD(t *testing.T) { {0, -1, 0, Period{0, -10, 0, 0, 0, 0}}, {-1, 0, 0, Period{-10, 0, 0, 0, 0, 0}}, } - for _, c := range cases { + for i, c := range cases { p := NewYMD(c.years, c.months, c.days) if p != c.period { - t.Errorf("%d,%d,%d gives %#v, want %#v", c.years, c.months, c.days, p, c.period) + t.Errorf("%d: %d,%d,%d gives %#v, want %#v", i, c.years, c.months, c.days, p, c.period) } if p.Years() != c.years { - t.Errorf("%#v, got %d want %d", p, p.Years(), c.years) + t.Errorf("%d: %#v, got %d want %d", i, p, p.Years(), c.years) } if p.Months() != c.months { - t.Errorf("%#v, got %d want %d", p, p.Months(), c.months) + t.Errorf("%d: %#v, got %d want %d", i, p, p.Months(), c.months) } if p.Days() != c.days { - t.Errorf("%#v, got %d want %d", p, p.Days(), c.days) + t.Errorf("%d: %#v, got %d want %d", i, p, p.Days(), c.days) } } } @@ -338,13 +340,13 @@ func TestNewOf(t *testing.T) { {-305 * oneDayApprox, Period{0, -100, 0, 0, 0, 0}, false}, {-36525 * oneDayApprox, Period{-1000, 0, 0, 0, 0, 0}, false}, } - for _, c := range cases { + for i, c := range cases { n, p := NewOf(c.source) if n != c.expected { - t.Errorf("NewOf(%v) gives %v %#v, want %v", c.source, n, n, c.expected) + t.Errorf("%d: NewOf(%v) gives %v %#v, want %v", i, c.source, n, n, c.expected) } if p != c.precise { - t.Errorf("NewOf(%v) gives %v, want %v for %v", c.source, p, c.precise, c.expected) + t.Errorf("%d: NewOf(%v) gives %v, want %v for %v", i, c.source, p, c.precise, c.expected) } } } @@ -364,10 +366,10 @@ func TestBetween(t *testing.T) { {time.Date(2015, 2, 11, 0, 0, 0, 0, time.UTC), time.Date(2016, 1, 12, 0, 0, 0, 0, time.UTC), Period{0, 110, 10, 0, 0, 0}}, {time.Date(2016, 1, 12, 0, 0, 0, 0, time.UTC), time.Date(2015, 2, 11, 0, 0, 0, 0, time.UTC), Period{0, -110, -10, 0, 0, 0}}, } - for _, c := range cases { + for i, c := range cases { n := Between(c.a, c.b) if n != c.expected { - t.Errorf("Between(%v, %v) gives %v %#v, want %v", c.a, c.b, n, n, c.expected) + t.Errorf("%d: Between(%v, %v) gives %v %#v, want %v", i, c.a, c.b, n, n, c.expected) } } } @@ -387,10 +389,10 @@ func TestNormalise(t *testing.T) { {New(0, 11, 30, 23, 59, 60), Period{10, 0, 6, 0, 0, 0}, false}, {New(0, 11, 30, 23, 59, 60).Negate(), Period{10, 0, 6, 0, 0, 0}.Negate(), false}, } - for _, c := range cases { + for i, c := range cases { n := c.source.Normalise(c.precise) if n != c.expected { - t.Errorf("%v.Normalise(%v) gives %v %#v, want %v", c.source, c.precise, n, n, c.expected) + t.Errorf("%d: %v.Normalise(%v) gives %v %#v, want %v", i, c.source, c.precise, n, n, c.expected) } } } @@ -423,10 +425,10 @@ func TestPeriodFormat(t *testing.T) { {"P2.15Y", "2.1 years"}, {"P2.125Y", "2.1 years"}, } - for _, c := range cases { + for i, c := range cases { s := MustParse(c.period).Format() if s != c.expect { - t.Errorf("Format() == %s, want %s for %+v", s, c.expect, c.period) + t.Errorf("%d: Format() == %s, want %s for %+v", i, s, c.expect, c.period) } } } @@ -457,11 +459,11 @@ func TestPeriodFormatWithoutWeeks(t *testing.T) { {"P2.15Y", "2.1 years"}, {"P2.125Y", "2.1 years"}, } - for _, c := range cases { + for i, c := range cases { s := MustParse(c.period).FormatWithPeriodNames(PeriodYearNames, PeriodMonthNames, plural.Plurals{}, PeriodDayNames, PeriodHourNames, PeriodMinuteNames, PeriodSecondNames) if s != c.expect { - t.Errorf("Format() == %s, want %s for %+v", s, c.expect, c.period) + t.Errorf("%d: Format() == %s, want %s for %+v", i, s, c.expect, c.period) } } } @@ -474,10 +476,10 @@ func TestPeriodOnlyYMD(t *testing.T) { {"P1Y2M3DT4H5M6S", "P1Y2M3D"}, {"-P6Y5M4DT3H2M1S", "-P6Y5M4D"}, } - for _, c := range cases { + for i, c := range cases { s := MustParse(c.one).OnlyYMD() if s != MustParse(c.expect) { - t.Errorf("%s.OnlyYMD() == %v, want %s", c.one, s, c.expect) + t.Errorf("%d: %s.OnlyYMD() == %v, want %s", i, c.one, s, c.expect) } } } @@ -490,10 +492,10 @@ func TestPeriodOnlyHMS(t *testing.T) { {"P1Y2M3DT4H5M6S", "PT4H5M6S"}, {"-P6Y5M4DT3H2M1S", "-PT3H2M1S"}, } - for _, c := range cases { + for i, c := range cases { s := MustParse(c.one).OnlyHMS() if s != MustParse(c.expect) { - t.Errorf("%s.OnlyHMS() == %v, want %s", c.one, s, c.expect) + t.Errorf("%d: %s.OnlyHMS() == %v, want %s", i, c.one, s, c.expect) } } } @@ -513,10 +515,10 @@ func TestPeriodAdd(t *testing.T) { {"P1Y2M3DT4H5M6S", "P6Y5M4DT3H2M1S", "P7Y7M7DT7H7M7S"}, {"P7Y7M7DT7H7M7S", "-P7Y7M7DT7H7M7S", "P0D"}, } - for _, c := range cases { + for i, c := range cases { s := MustParse(c.one).Add(MustParse(c.two)) if s != MustParse(c.expect) { - t.Errorf("%s.Add(%s) == %v, want %s", c.one, c.two, s, c.expect) + t.Errorf("%d: %s.Add(%s) == %v, want %s", i, c.one, c.two, s, c.expect) } } } @@ -543,10 +545,10 @@ func TestPeriodScale(t *testing.T) { {"P1Y2M3DT4H5M6S", 2, "P2Y4M6DT8H10M12S"}, {"P2Y4M6DT8H10M12S", -0.5, "-P1Y2M3DT4H5M6S"}, } - for _, c := range cases { + for i, c := range cases { s := MustParse(c.one).Scale(c.m) if s != MustParse(c.expect) { - t.Errorf("%s.Scale(%g) == %v, want %s", c.one, c.m, s, c.expect) + t.Errorf("%d: %s.Scale(%g) == %v, want %s", i, c.one, c.m, s, c.expect) } } } diff --git a/timespan/daterange.go b/timespan/daterange.go index ac755697..2b2370f3 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -149,24 +149,33 @@ func (dateRange DateRange) ExtendBy(days date.PeriodOfDays) DateRange { // ShiftByPeriod moves the date range by moving both the start and end dates similarly. // A negative parameter is allowed. -func (dateRange DateRange) ShiftByPeriod(period period.Period) DateRange { - if period.IsZero() { +// +// Any time component is ignored. Therefore, be careful with periods containing +// more that 24 hours in the hours/minutes/seconds fields. These will not be +// normalised for you; if you want this behaviour, call delta.Normalise(false) +// on the input parameter. +// +// For example, PT24H adds nothing, whereas P1D adds one day as expected. To +// convert a period such as PT24H to its equivalent P1D, use +// delta.Normalise(false) as the input. +func (dateRange DateRange) ShiftByPeriod(delta period.Period) DateRange { + if delta.IsZero() { return dateRange } - newMark := dateRange.mark.AddPeriod(period) - //fmt.Printf("mark + %v : %v -> %v", period, dateRange.mark, newMark) + newMark := dateRange.mark.AddPeriod(delta) + //fmt.Printf("mark + %v : %v -> %v", delta, dateRange.mark, newMark) return DateRange{newMark, 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). -func (dateRange DateRange) ExtendByPeriod(period period.Period) DateRange { - if period.IsZero() { +func (dateRange DateRange) ExtendByPeriod(delta period.Period) DateRange { + if delta.IsZero() { return dateRange } - newEnd := dateRange.End().AddPeriod(period) - //fmt.Printf("%v, end + %v : %v -> %v", dateRange.mark, period, dateRange.End(), newEnd) + newEnd := dateRange.End().AddPeriod(delta) + //fmt.Printf("%v, end + %v : %v -> %v", dateRange.mark, delta, dateRange.End(), newEnd) return NewDateRange(dateRange.Start(), newEnd) } From faad512c073dcc948069a6f6512305e3f8e03804 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 18 Jul 2018 13:31:47 +0100 Subject: [PATCH 086/165] Added more tests/test cases. --- period/format.go | 2 +- timespan/daterange.go | 19 +- timespan/daterange_test.go | 347 ++++++++++++++++++++----------------- timespan/timespan.go | 2 +- timespan/timespan_test.go | 166 +++++++++--------- view/vdate.go | 2 +- 6 files changed, 288 insertions(+), 250 deletions(-) diff --git a/period/format.go b/period/format.go index 8f1236a2..3bb7e98f 100644 --- a/period/format.go +++ b/period/format.go @@ -5,10 +5,10 @@ package period import ( + "bytes" "fmt" "github.com/rickb777/plural" "strings" - "bytes" ) // Format converts the period to human-readable form using the default localisation. diff --git a/timespan/daterange.go b/timespan/daterange.go index 2b2370f3..f7abf1a1 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -33,7 +33,10 @@ func NewDateRangeOf(start time.Time, duration time.Duration) DateRange { // are on subsequent days, the range is one date (not two). // The result is normalised. func NewDateRange(start, end date.Date) DateRange { - return DateRange{start, date.PeriodOfDays(end.Sub(start))}.Normalise() + if end.Before(start) { + return DateRange{end, date.PeriodOfDays(start.Sub(end))} + } + return DateRange{start, date.PeriodOfDays(end.Sub(start))} } // NewYearOf constructs the range encompassing the whole year specified. @@ -59,11 +62,23 @@ func EmptyRange(day date.Date) DateRange { } // OneDayRange constructs a range of exactly one day. This is often a useful basis for -// further operations. Note that the end date is the same as the start date. +// further operations. Note that the last date is the same as the start date. func OneDayRange(day date.Date) DateRange { return DateRange{day, 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. +func DayRange(day date.Date, n date.PeriodOfDays) DateRange { + if n < 0 { + return DateRange{day.Add(n), -n} + } + return DateRange{day, n} +} + // Days returns the period represented by this range. This will never be negative. func (dateRange DateRange) Days() date.PeriodOfDays { if dateRange.days < 0 { diff --git a/timespan/daterange_test.go b/timespan/daterange_test.go index 3fa220f4..7a4db30a 100644 --- a/timespan/daterange_test.go +++ b/timespan/daterange_test.go @@ -28,6 +28,7 @@ var d0403 = New(2015, time.April, 3) var d0404 = New(2015, time.April, 4) var d0407 = New(2015, time.April, 7) var d0408 = New(2015, time.April, 8) +var d0409 = New(2015, time.April, 9) var d0410 = New(2015, time.April, 10) var d0501 = New(2015, time.May, 1) var d1025 = New(2015, time.October, 25) @@ -44,169 +45,175 @@ func mustLoadLocation(name string) *time.Location { func TestNewDateRangeOf(t *testing.T) { dr := NewDateRangeOf(t0327, 7*24*time.Hour) - isEq(t, dr.mark, d0327) - isEq(t, dr.Days(), PeriodOfDays(7)) - isEq(t, dr.IsEmpty(), false) - isEq(t, dr.Start(), d0327) - isEq(t, dr.Last(), d0402) - isEq(t, dr.End(), d0403) + 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) dr2 := NewDateRangeOf(t0327, -7*24*time.Hour) - isEq(t, dr2.mark, d0327) - isEq(t, dr2.Days(), PeriodOfDays(7)) - isEq(t, dr2.IsEmpty(), false) - isEq(t, dr2.Start(), d0321) - isEq(t, dr2.Last(), d0327) - isEq(t, dr2.End(), d0328) + isEq(t, 0, dr2.mark, d0327) + 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) } func TestNewDateRangeWithNormalise(t *testing.T) { r1 := NewDateRange(d0327, d0402) - isEq(t, r1.Start(), d0327) - isEq(t, r1.Last(), d0401) - isEq(t, r1.End(), d0402) + isEq(t, 0, r1.Start(), d0327) + isEq(t, 0, r1.Last(), d0401) + isEq(t, 0, r1.End(), d0402) r2 := NewDateRange(d0402, d0327) - isEq(t, r2.Start(), d0327) - isEq(t, r2.Last(), d0401) - isEq(t, r2.End(), d0402) + 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, drN0.Days(), PeriodOfDays(1)) - isEq(t, drN0.IsZero(), false) - isEq(t, drN0.IsEmpty(), false) - isEq(t, drN0.Start(), d0327) - isEq(t, drN0.Last(), d0327) - isEq(t, drN0.String(), "1 day on 2015-03-26") + 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, dr0.Days(), PeriodOfDays(0)) - isEq(t, dr0.IsZero(), true) - isEq(t, dr0.IsEmpty(), true) - isEq(t, dr0.String(), "0 days at 1970-01-01") + isEq(t, 0, dr0.Days(), PeriodOfDays(0)) + isEq(t, 0, dr0.IsZero(), true) + isEq(t, 0, dr0.IsEmpty(), true) + isEq(t, 0, dr0.String(), "0 days at 1970-01-01") dr1 := EmptyRange(Date{}) - isEq(t, dr1.IsZero(), true) - isEq(t, dr1.IsEmpty(), true) - isEq(t, dr1.Days(), PeriodOfDays(0)) + isEq(t, 0, dr1.IsZero(), true) + isEq(t, 0, dr1.IsEmpty(), true) + isEq(t, 0, dr1.Days(), PeriodOfDays(0)) dr2 := EmptyRange(d0327) - isEq(t, dr2.IsZero(), false) - isEq(t, dr2.IsEmpty(), true) - isEq(t, dr2.Start(), d0327) - isEq(t, dr2.Last().IsZero(), true) - isEq(t, dr2.End(), d0327) - isEq(t, dr2.Days(), PeriodOfDays(0)) - isEq(t, dr2.String(), "0 days at 2015-03-27") + isEq(t, 0, dr2.IsZero(), false) + isEq(t, 0, dr2.IsEmpty(), true) + isEq(t, 0, dr2.Start(), d0327) + isEq(t, 0, dr2.Last().IsZero(), true) + isEq(t, 0, dr2.End(), d0327) + isEq(t, 0, dr2.Days(), PeriodOfDays(0)) + isEq(t, 0, dr2.String(), "0 days at 2015-03-27") } func TestOneDayRange(t *testing.T) { dr1 := OneDayRange(Date{}) - isEq(t, dr1.IsZero(), false) - isEq(t, dr1.IsEmpty(), false) - isEq(t, dr1.Days(), PeriodOfDays(1)) + isEq(t, 0, dr1.IsZero(), false) + isEq(t, 0, dr1.IsEmpty(), false) + isEq(t, 0, dr1.Days(), PeriodOfDays(1)) dr2 := OneDayRange(d0327) - isEq(t, dr2.Start(), d0327) - isEq(t, dr2.Last(), d0327) - isEq(t, dr2.End(), d0328) - isEq(t, dr2.Days(), PeriodOfDays(1)) - isEq(t, dr2.String(), "1 day on 2015-03-27") + isEq(t, 0, dr2.Start(), d0327) + isEq(t, 0, dr2.Last(), d0327) + isEq(t, 0, dr2.End(), d0328) + isEq(t, 0, dr2.Days(), PeriodOfDays(1)) + isEq(t, 0, dr2.String(), "1 day on 2015-03-27") +} + +func TestDayRange(t *testing.T) { + dr1 := DayRange(Date{}, 0) + isEq(t, 0, dr1.IsZero(), true) + isEq(t, 0, dr1.IsEmpty(), true) + isEq(t, 0, dr1.Days(), PeriodOfDays(0)) + + dr2 := DayRange(d0327, 2) + isEq(t, 0, dr2.Start(), d0327) + isEq(t, 0, dr2.Last(), d0328) + isEq(t, 0, dr2.End(), d0329) + isEq(t, 0, dr2.Days(), PeriodOfDays(2)) + isEq(t, 0, dr2.String(), "2 days from 2015-03-27 to 2015-03-28") + + dr3 := DayRange(d0327, -2) + isEq(t, 0, dr3.Start(), d0325) + isEq(t, 0, dr3.Last(), d0326) + isEq(t, 0, dr3.End(), d0327) + isEq(t, 0, dr3.Days(), PeriodOfDays(2)) + isEq(t, 0, dr3.String(), "2 days from 2015-03-25 to 2015-03-26") } func TestNewYearOf(t *testing.T) { dr := NewYearOf(2015) - isEq(t, dr.Days(), PeriodOfDays(365)) - isEq(t, dr.Start(), New(2015, time.January, 1)) - isEq(t, dr.Last(), New(2015, time.December, 31)) - isEq(t, dr.End(), New(2016, time.January, 1)) + isEq(t, 0, dr.Days(), PeriodOfDays(365)) + isEq(t, 0, dr.Start(), New(2015, time.January, 1)) + isEq(t, 0, dr.Last(), New(2015, time.December, 31)) + isEq(t, 0, dr.End(), New(2016, time.January, 1)) } func TestNewMonthOf(t *testing.T) { dr := NewMonthOf(2015, time.February) - isEq(t, dr.Days(), PeriodOfDays(28)) - isEq(t, dr.Start(), New(2015, time.February, 1)) - isEq(t, dr.Last(), New(2015, time.February, 28)) - isEq(t, dr.End(), New(2015, time.March, 1)) -} - -func TestShiftByPos(t *testing.T) { - dr := NewDateRange(d0327, d0402).ShiftBy(7) - isEq(t, dr.Days(), PeriodOfDays(6)) - isEq(t, dr.Start(), d0403) - isEq(t, dr.Last(), d0408) + isEq(t, 0, dr.Days(), PeriodOfDays(28)) + isEq(t, 0, dr.Start(), New(2015, time.February, 1)) + isEq(t, 0, dr.Last(), New(2015, time.February, 28)) + isEq(t, 0, dr.End(), New(2015, time.March, 1)) } -func TestShiftByNeg(t *testing.T) { - dr := NewDateRange(d0403, d0408).ShiftBy(-7) - isEq(t, dr.Days(), PeriodOfDays(5)) - isEq(t, dr.Start(), d0327) - isEq(t, dr.Last(), d0331) -} - -func TestExtendByPos(t *testing.T) { - dr := OneDayRange(d0327).ExtendBy(6) - isEq(t, dr.Days(), PeriodOfDays(7)) - isEq(t, dr.Start(), d0327) - isEq(t, dr.Last(), d0402) - isEq(t, dr.End(), d0403) - isEq(t, dr.String(), "7 days from 2015-03-27 to 2015-04-02") -} - -func TestExtendByNeg(t *testing.T) { - dr := OneDayRange(d0327).ExtendBy(-8) - isEq(t, dr.Days(), PeriodOfDays(7)) - isEq(t, dr.Start(), d0320) - isEq(t, dr.Last(), d0326) - isEq(t, dr.String(), "7 days from 2015-03-20 to 2015-03-26") -} - -func TestShiftByPosPeriod(t *testing.T) { - dr := NewDateRange(d0327, d0402).ShiftByPeriod(period.New(0, 0, 7, 0, 0, 0)) - isEq(t, dr.Days(), PeriodOfDays(6)) - isEq(t, dr.Start(), d0403) - isEq(t, dr.Last(), d0408) -} - -func TestShiftByNegPeriod(t *testing.T) { - dr := NewDateRange(d0403, d0408).ShiftByPeriod(period.New(0, 0, -7, 0, 0, 0)) - isEq(t, dr.Days(), PeriodOfDays(5)) - isEq(t, dr.Start(), d0327) - isEq(t, dr.Last(), d0331) -} +func TestShiftAndExtend(t *testing.T) { + cases := []struct { + dr DateRange + n PeriodOfDays + start Date + end Date + s string + }{ + {DayRange(d0327, 6).ShiftBy(0), 6, d0327, d0402, "6 days from 2015-03-27 to 2015-04-01"}, + {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"}, + + {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"}, + {DayRange(d0327, 6).ExtendBy(0), 6, d0327, d0402, "6 days from 2015-03-27 to 2015-04-01"}, + {DayRange(d0327, 6).ExtendBy(7), 13, d0327, d0409, "13 days from 2015-03-27 to 2015-04-08"}, + {DayRange(d0327, 6).ExtendBy(-6), 0, d0327, d0327, "0 days at 2015-03-27"}, + {DayRange(d0327, 6).ExtendBy(-8), 2, d0325, d0327, "2 days from 2015-03-25 to 2015-03-26"}, + + {DayRange(d0327, 6).ShiftByPeriod(period.NewYMD(0, 0, 0)), 6, d0327, d0402, "6 days from 2015-03-27 to 2015-04-01"}, + {DayRange(d0327, 6).ShiftByPeriod(period.NewYMD(0, 0, 7)), 6, d0403, d0409, "6 days from 2015-04-03 to 2015-04-08"}, + {DayRange(d0327, 6).ShiftByPeriod(period.NewYMD(0, 0, -7)), 6, d0320, d0326, "6 days from 2015-03-20 to 2015-03-25"}, + + {DayRange(d0327, 6).ExtendByPeriod(period.NewYMD(0, 0, 0)), 6, d0327, d0402, "6 days from 2015-03-27 to 2015-04-01"}, + {DayRange(d0327, 6).ExtendByPeriod(period.NewYMD(0, 0, 7)), 13, d0327, d0409, "13 days from 2015-03-27 to 2015-04-08"}, + {DayRange(d0327, 6).ExtendByPeriod(period.NewYMD(0, 0, -5)), 1, d0327, d0328, "1 day on 2015-03-27"}, + {DayRange(d0327, 6).ExtendByPeriod(period.NewYMD(0, 0, -6)), 0, d0327, d0327, "0 days at 2015-03-27"}, + {DayRange(d0327, 6).ExtendByPeriod(period.NewYMD(0, 0, -7)), 1, d0326, d0327, "1 day on 2015-03-26"}, + } -func TestExtendByPosPeriod(t *testing.T) { - dr := OneDayRange(d0327).ExtendByPeriod(period.New(0, 0, 6, 0, 0, 0)) - isEq(t, dr.Days(), PeriodOfDays(7)) - isEq(t, dr.Start(), d0327) - isEq(t, dr.Last(), d0402) - isEq(t, dr.End(), d0403) - isEq(t, dr.String(), "7 days from 2015-03-27 to 2015-04-02") + for i, c := range cases { + isEq(t, i, c.dr.Days(), c.n) + isEq(t, i, c.dr.Start(), c.start) + isEq(t, i, c.dr.End(), c.end) + isEq(t, i, c.dr.String(), c.s) + } } -func TestExtendByNegPeriod(t *testing.T) { - dr := OneDayRange(d0327).ExtendByPeriod(period.New(0, 0, -8, 0, 0, 0)) - //fmt.Printf("\ndr=%#v\n", dr) - isEq(t, dr.Days(), PeriodOfDays(7)) - isEq(t, dr.Start(), d0320) - isEq(t, dr.Last(), d0326) - isEq(t, dr.String(), "7 days from 2015-03-20 to 2015-03-26") +func TestContains0(t *testing.T) { + old := time.Local + time.Local = time.FixedZone("Test", 7200) + dr := EmptyRange(d0326) + isEq(t, 0, dr.Contains(d0320), false, dr, d0320) + time.Local = old } func TestContains1(t *testing.T) { old := time.Local time.Local = time.FixedZone("Test", 7200) - dr := OneDayRange(d0326).ExtendBy(1) - isEq(t, dr.Contains(d0320), false, dr, d0320) - isEq(t, dr.Contains(d0325), false, dr, d0325) - isEq(t, dr.Contains(d0326), true, dr, d0326) - isEq(t, dr.Contains(d0327), true, dr, d0327) - isEq(t, dr.Contains(d0328), false, dr, d0328) - isEq(t, dr.Contains(d0401), false, dr, d0401) - isEq(t, dr.Contains(d0410), false, dr, d0410) - isEq(t, dr.Contains(d0501), false, dr, d0501) + dr := DayRange(d0326, 2) + isEq(t, 0, dr.Contains(d0320), false, dr, d0320) + isEq(t, 0, dr.Contains(d0325), false, dr, d0325) + isEq(t, 0, dr.Contains(d0326), true, dr, d0326) + isEq(t, 0, dr.Contains(d0327), true, dr, d0327) + isEq(t, 0, dr.Contains(d0328), false, dr, d0328) + isEq(t, 0, dr.Contains(d0401), false, dr, d0401) + isEq(t, 0, dr.Contains(d0410), false, dr, d0410) + isEq(t, 0, dr.Contains(d0501), false, dr, d0501) time.Local = old } @@ -214,9 +221,25 @@ func TestContains2(t *testing.T) { old := time.Local time.Local = time.FixedZone("Test", 7200) dr := OneDayRange(d0326) - isEq(t, dr.Contains(d0325), false, dr, d0325) - isEq(t, dr.Contains(d0326), true, dr, d0326) - isEq(t, dr.Contains(d0327), false, dr, d0327) + isEq(t, 0, dr.Contains(d0325), false, dr, d0325) + isEq(t, 0, dr.Contains(d0326), true, dr, d0326) + isEq(t, 0, dr.Contains(d0327), false, dr, d0327) + time.Local = old +} + +func TestContainsTime0(t *testing.T) { + old := time.Local + time.Local = time.FixedZone("Test", 7200) + t0328e := time.Date(2015, 3, 28, 23, 59, 59, 999999999, time.UTC) + t0329 := time.Date(2015, 3, 29, 0, 0, 0, 0, time.UTC) + + dr := EmptyRange(d0327) + isEq(t, 0, dr.StartUTC(), t0327, dr, t0327) + isEq(t, 0, dr.EndUTC(), t0327, dr, t0327) + isEq(t, 0, dr.ContainsTime(t0327), false, dr, t0327) + isEq(t, 0, dr.ContainsTime(t0328), false, dr, t0328) + isEq(t, 0, dr.ContainsTime(t0328e), false, dr, t0328e) + isEq(t, 0, dr.ContainsTime(t0329), false, dr, t0329) time.Local = old } @@ -226,34 +249,34 @@ func TestContainsTimeUTC(t *testing.T) { t0328e := time.Date(2015, 3, 28, 23, 59, 59, 999999999, time.UTC) t0329 := time.Date(2015, 3, 29, 0, 0, 0, 0, time.UTC) - dr := OneDayRange(d0327).ExtendBy(1) - isEq(t, dr.StartUTC(), t0327, dr, t0327) - isEq(t, dr.EndUTC(), t0329, dr, t0329) - isEq(t, dr.ContainsTime(t0327), true, dr, t0327) - isEq(t, dr.ContainsTime(t0328), true, dr, t0328) - isEq(t, dr.ContainsTime(t0328e), true, dr, t0328e) - isEq(t, dr.ContainsTime(t0329), false, dr, t0329) + dr := DayRange(d0327, 2) + isEq(t, 0, dr.StartUTC(), t0327, dr, t0327) + isEq(t, 0, dr.EndUTC(), t0329, dr, t0329) + isEq(t, 0, dr.ContainsTime(t0327), true, dr, t0327) + isEq(t, 0, dr.ContainsTime(t0328), true, dr, t0328) + isEq(t, 0, dr.ContainsTime(t0328e), true, dr, t0328e) + isEq(t, 0, dr.ContainsTime(t0329), false, dr, t0329) time.Local = old } func TestMerge1(t *testing.T) { - dr1 := OneDayRange(d0327).ExtendBy(1) - dr2 := OneDayRange(d0327).ExtendBy(7) + dr1 := DayRange(d0327, 2) + dr2 := DayRange(d0327, 8) m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) - isEq(t, m1.Start(), d0327) - isEq(t, m1.End(), d0404) - isEq(t, m1, m2) + isEq(t, 0, m1.Start(), d0327) + isEq(t, 0, m1.End(), d0404) + isEq(t, 0, m1, m2) } func TestMerge2(t *testing.T) { - dr1 := OneDayRange(d0327).ExtendBy(1).ShiftBy(1) - dr2 := OneDayRange(d0327).ExtendBy(7) + dr1 := DayRange(d0328, 2) + dr2 := DayRange(d0327, 8) m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) - isEq(t, m1.Start(), d0327) - isEq(t, m1.End(), d0404) - isEq(t, m1, m2) + isEq(t, 0, m1.Start(), d0327) + isEq(t, 0, m1.End(), d0404) + isEq(t, 0, m1, m2) } func TestMergeOverlapping(t *testing.T) { @@ -261,9 +284,9 @@ func TestMergeOverlapping(t *testing.T) { dr2 := OneDayRange(d0401).ExtendBy(6) m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) - isEq(t, m1.Start(), d0320) - isEq(t, m1.End(), d0408) - isEq(t, m1, m2) + isEq(t, 0, m1.Start(), d0320) + isEq(t, 0, m1.End(), d0408) + isEq(t, 0, m1, m2) } func TestMergeNonOverlapping(t *testing.T) { @@ -271,9 +294,9 @@ func TestMergeNonOverlapping(t *testing.T) { dr2 := OneDayRange(d0401).ExtendBy(6) m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) - isEq(t, m1.Start(), d0320) - isEq(t, m1.End(), d0408) - isEq(t, m1, m2) + isEq(t, 0, m1.Start(), d0320) + isEq(t, 0, m1.End(), d0408) + isEq(t, 0, m1, m2) } func TestMergeEmpties(t *testing.T) { @@ -281,9 +304,9 @@ func TestMergeEmpties(t *testing.T) { dr2 := EmptyRange(d0408) // curiously, this is *not* included because it has no size. m1 := dr1.Merge(dr2) m2 := dr2.Merge(dr1) - isEq(t, m1.Start(), d0320) - isEq(t, m1.End(), d0408) - isEq(t, m1, m2) + isEq(t, 0, m1.Start(), d0320) + isEq(t, 0, m1.End(), d0408) + isEq(t, 0, m1, m2) } func TestMergeZeroes(t *testing.T) { @@ -292,32 +315,32 @@ func TestMergeZeroes(t *testing.T) { m1 := dr1.Merge(dr0) m2 := dr0.Merge(dr1) m3 := dr0.Merge(dr0) - isEq(t, m1.Start(), d0401) - isEq(t, m1.End(), d0408) - isEq(t, m1, m2) - isEq(t, m3.IsZero(), true) - isEq(t, m3, dr0) + isEq(t, 0, m1.Start(), d0401) + isEq(t, 0, m1.End(), d0408) + isEq(t, 0, m1, m2) + isEq(t, 0, m3.IsZero(), true) + isEq(t, 0, m3, dr0) } func TestDurationNormalUTC(t *testing.T) { dr := OneDayRange(d0329) - isEq(t, dr.Duration(), time.Hour*24) + isEq(t, 0, dr.Duration(), time.Hour*24) } func TestDurationInZoneWithDaylightSaving(t *testing.T) { - isEq(t, OneDayRange(d0328).DurationIn(london), time.Hour*24) - isEq(t, OneDayRange(d0329).DurationIn(london), time.Hour*23) - isEq(t, OneDayRange(d1025).DurationIn(london), time.Hour*25) - isEq(t, NewDateRange(d0328, d0331).DurationIn(london), time.Hour*71) + 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) } -func isEq(t *testing.T, a, b interface{}, msg ...interface{}) { +func isEq(t *testing.T, i int, a, b interface{}, msg ...interface{}) { t.Helper() if a != b { sa := make([]string, len(msg)) for i, m := range msg { sa[i] = fmt.Sprintf(", %v", m) } - t.Errorf("%+v is not equal to %+v%s", a, b, strings.Join(sa, "")) + t.Errorf("%d: %+v is not equal to %+v%s", i, a, b, strings.Join(sa, "")) } } diff --git a/timespan/timespan.go b/timespan/timespan.go index f37c2d4a..34b1ea30 100644 --- a/timespan/timespan.go +++ b/timespan/timespan.go @@ -7,9 +7,9 @@ package timespan import ( "fmt" "github.com/rickb777/date" - "time" "github.com/rickb777/date/period" "strings" + "time" ) // TimestampFormat is a simple format for date & time, "2006-01-02 15:04:05". diff --git a/timespan/timespan_test.go b/timespan/timespan_test.go index e7a0f78a..9426efec 100644 --- a/timespan/timespan_test.go +++ b/timespan/timespan_test.go @@ -19,81 +19,81 @@ var t0330 = time.Date(2015, 3, 30, 0, 0, 0, 0, time.UTC) func TestZeroTimeSpan(t *testing.T) { ts := ZeroTimeSpan(t0327) - isEq(t, ts.mark, t0327) - isEq(t, ts.Duration(), zero) - isEq(t, ts.End(), 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, ts1.mark, t0327) - isEq(t, ts1.Duration(), zero) - isEq(t, ts1.IsEmpty(), true) - isEq(t, ts1.End(), 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, ts2.mark, t0327) - isEq(t, ts2.Duration(), time.Hour*24) - isEq(t, ts2.IsEmpty(), false) - isEq(t, ts2.End(), 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, ts3.mark, t0327) - isEq(t, ts3.Duration(), time.Hour*48) - isEq(t, ts3.IsEmpty(), false) - isEq(t, ts3.End(), t0329) + 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) } func TestTSEnd(t *testing.T) { ts1 := TimeSpan{t0328, time.Hour * 24} - isEq(t, ts1.Start(), t0328) - isEq(t, ts1.End(), t0329) + isEq(t, 0, ts1.Start(), t0328) + isEq(t, 0, ts1.End(), t0329) // not normalised, deliberately ts2 := TimeSpan{t0328, -time.Hour * 24} - isEq(t, ts2.Start(), t0327) - isEq(t, ts2.End(), t0328) + isEq(t, 0, ts2.Start(), t0327) + isEq(t, 0, ts2.End(), t0328) } func TestTSShiftBy(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328).ShiftBy(time.Hour * 24) - isEq(t, ts1.mark, t0328) - isEq(t, ts1.Duration(), time.Hour*24) - isEq(t, ts1.End(), t0329) + 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, ts2.mark, t0327) - isEq(t, ts2.Duration(), time.Hour*24) - isEq(t, ts2.End(), t0328) + 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, ts1.mark, t0327) - isEq(t, ts1.Duration(), time.Hour*48) - isEq(t, ts1.End(), t0329) + 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, ts2.mark, t0327) - isEq(t, ts2.Duration(), time.Hour*24) - isEq(t, ts2.End(), t0328) + 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, ts1.mark, t0327) - isEq(t, ts1.Duration(), time.Hour*48) - isEq(t, ts1.End(), t0329) + 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, ts2.mark, t0328) - isEq(t, ts2.Duration(), zero) - isEq(t, ts2.End(), t0328) + 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() - isEq(t, s, "24h0m0s from 2015-03-27 00:00:00 to 2015-03-28 00:00:00") + isEq(t, 0, s, "24h0m0s from 2015-03-27 00:00:00 to 2015-03-28 00:00:00") } func TestTSEqual(t *testing.T) { @@ -170,7 +170,7 @@ func TestTSFormat(t *testing.T) { for _, c := range cases { ts := TimeSpan{c.start, c.duration} - isEq(t, ts.Format(c.layout, c.separator, c.useDuration), c.exp) + isEq(t, 0, ts.Format(c.layout, c.separator, c.useDuration), c.exp) } } @@ -192,11 +192,11 @@ func TestTSMarshalText(t *testing.T) { ts := TimeSpan{c.start, c.duration} s := ts.FormatRFC5545(true) - isEq(t, s, c.exp) + isEq(t, 0, s, c.exp) b, err := ts.MarshalText() - isEq(t, err, nil) - isEq(t, string(b), c.exp) + isEq(t, 0, err, nil) + isEq(t, 0, string(b), c.exp) } } @@ -216,9 +216,9 @@ func TestTSParseInLocation(t *testing.T) { {t0325, 2 * time.Second, "20150325T101314Z/PT2S"}, {t0120.In(berlin), time.Minute, "20150120T111314/PT1M"}, {t0325, 336 * time.Hour, "20150325T101314Z/P2W"}, - {t0120.In(berlin), 72*time.Hour, "20150120T111314/P3D"}, + {t0120.In(berlin), 72 * time.Hour, "20150120T111314/P3D"}, // This case has the daylight-savings clock shift - {t0325.In(berlin), 167*time.Hour, "20150325T111314/P1W"}, + {t0325.In(berlin), 167 * time.Hour, "20150325T111314/P1W"}, } for i, c := range cases { @@ -269,18 +269,18 @@ func TestTSParseInLocationErrors(t *testing.T) { func TestTSContains(t *testing.T) { ts := NewTimeSpan(t0327, t0329) - isEq(t, ts.Contains(t0327.Add(minusOneNano)), false) - isEq(t, ts.Contains(t0327), true) - isEq(t, ts.Contains(t0328), true) - isEq(t, ts.Contains(t0329.Add(minusOneNano)), true) - isEq(t, ts.Contains(t0329), false) + isEq(t, 0, ts.Contains(t0327.Add(minusOneNano)), false) + isEq(t, 0, ts.Contains(t0327), true) + isEq(t, 0, ts.Contains(t0328), true) + isEq(t, 0, ts.Contains(t0329.Add(minusOneNano)), true) + isEq(t, 0, ts.Contains(t0329), false) } func TestTSIn(t *testing.T) { ts := ZeroTimeSpan(t0327).In(time.FixedZone("Test", 7200)) - isEq(t, ts.mark.Equal(t0327), true) - isEq(t, ts.Duration(), zero) - isEq(t, ts.End().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) { @@ -288,9 +288,9 @@ func TestTSMerge1(t *testing.T) { ts2 := NewTimeSpan(t0327, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) - isEq(t, m1.mark, t0327) - isEq(t, m1.End(), t0330) - isEq(t, m1, m2) + isEq(t, 0, m1.mark, t0327) + isEq(t, 0, m1.End(), t0330) + isEq(t, 0, m1, m2) } func TestTSMerge2(t *testing.T) { @@ -298,9 +298,9 @@ func TestTSMerge2(t *testing.T) { ts2 := NewTimeSpan(t0327, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) - isEq(t, m1.mark, t0327) - isEq(t, m1.End(), t0330) - isEq(t, m1, m2) + isEq(t, 0, m1.mark, t0327) + isEq(t, 0, m1.End(), t0330) + isEq(t, 0, m1, m2) } func TestTSMerge3(t *testing.T) { @@ -308,9 +308,9 @@ func TestTSMerge3(t *testing.T) { ts2 := NewTimeSpan(t0327, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) - isEq(t, m1.mark, t0327) - isEq(t, m1.End(), t0330) - isEq(t, m1, m2) + isEq(t, 0, m1.mark, t0327) + isEq(t, 0, m1.End(), t0330) + isEq(t, 0, m1, m2) } func TestTSMergeOverlapping(t *testing.T) { @@ -318,9 +318,9 @@ func TestTSMergeOverlapping(t *testing.T) { ts2 := NewTimeSpan(t0328, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) - isEq(t, m1.mark, t0327) - isEq(t, m1.End(), t0330) - isEq(t, m1, m2) + isEq(t, 0, m1.mark, t0327) + isEq(t, 0, m1.End(), t0330) + isEq(t, 0, m1, m2) } func TestTSMergeNonOverlapping(t *testing.T) { @@ -328,32 +328,32 @@ func TestTSMergeNonOverlapping(t *testing.T) { ts2 := NewTimeSpan(t0329, t0330) m1 := ts1.Merge(ts2) m2 := ts2.Merge(ts1) - isEq(t, m1.mark, t0327) - isEq(t, m1.End(), t0330) - isEq(t, m1, m2) + isEq(t, 0, m1.mark, t0327) + isEq(t, 0, m1.End(), t0330) + isEq(t, 0, m1, m2) } func TestConversion1(t *testing.T) { ts1 := ZeroTimeSpan(t0327) dr := ts1.DateRangeIn(time.UTC) ts2 := dr.TimeSpanIn(time.UTC) - isEq(t, dr.Start(), d0327) - isEq(t, dr.IsEmpty(), true) - isEq(t, ts1.Start(), ts1.End()) - isEq(t, ts1.Duration(), zero) - isEq(t, dr.Days(), date.PeriodOfDays(0)) - isEq(t, ts2.Duration(), zero) - isEq(t, ts1, ts2) + isEq(t, 0, dr.Start(), d0327) + isEq(t, 0, dr.IsEmpty(), true) + isEq(t, 0, ts1.Start(), ts1.End()) + isEq(t, 0, ts1.Duration(), zero) + isEq(t, 0, dr.Days(), date.PeriodOfDays(0)) + isEq(t, 0, ts2.Duration(), zero) + isEq(t, 0, ts1, ts2) } func TestConversion2(t *testing.T) { ts1 := NewTimeSpan(t0327, t0328) dr := ts1.DateRangeIn(time.UTC) ts2 := dr.TimeSpanIn(time.UTC) - isEq(t, dr.Start(), d0327) - isEq(t, dr.End(), d0328) - isEq(t, ts1, ts2) - isEq(t, ts1.Duration(), time.Hour*24) + isEq(t, 0, dr.Start(), d0327) + isEq(t, 0, dr.End(), d0328) + isEq(t, 0, ts1, ts2) + isEq(t, 0, ts1.Duration(), time.Hour*24) } func TestConversion3(t *testing.T) { @@ -361,9 +361,9 @@ func TestConversion3(t *testing.T) { ts1 := dr1.TimeSpanIn(london) dr2 := ts1.DateRangeIn(london) ts2 := dr2.TimeSpanIn(london) - isEq(t, dr1.Start(), d0327) - isEq(t, dr1.End(), d0330) - isEq(t, dr1, dr2) - isEq(t, ts1, ts2) - isEq(t, ts1.Duration(), time.Hour*71) + isEq(t, 0, dr1.Start(), d0327) + isEq(t, 0, dr1.End(), d0330) + isEq(t, 0, dr1, dr2) + isEq(t, 0, ts1, ts2) + isEq(t, 0, ts1.Duration(), time.Hour*71) } diff --git a/view/vdate.go b/view/vdate.go index 2b28abc4..d3911b49 100644 --- a/view/vdate.go +++ b/view/vdate.go @@ -53,7 +53,7 @@ func (v VDate) IsTomorrow() bool { // IsOdd returns true if the date is an odd number. This is useful for // zebra striping etc. func (v VDate) IsOdd() bool { - return v.d.DaysSinceEpoch() % 2 == 0 + return v.d.DaysSinceEpoch()%2 == 0 } // String formats the date in basic ISO8601 format YYYY-MM-DD. From b6289759623f14ea664800a991d88383c6b48d1d Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 18 Jul 2018 16:06:52 +0100 Subject: [PATCH 087/165] Correction for RFC5545 - always output in zulu time. --- timespan/timespan.go | 16 +++++++++++++--- timespan/timespan_test.go | 19 +++++++++++-------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/timespan/timespan.go b/timespan/timespan.go index 34b1ea30..1da7b1fd 100644 --- a/timespan/timespan.go +++ b/timespan/timespan.go @@ -156,6 +156,10 @@ func (ts TimeSpan) Merge(other TimeSpan) TimeSpan { // that "Z" is to be appended when the time is UTC. const RFC5545DateTimeLayout = "20060102T150405" +// RFC5545DateTimeZulu is the UTC format string used by iCalendar (RFC5545). Note +// that this cannot be used for parsing with time.Parse. +const RFC5545DateTimeZulu = RFC5545DateTimeLayout + "Z" + func layoutHasTimezone(layout string) bool { return strings.IndexByte(layout, 'Z') >= 0 || strings.Contains(layout, "-07") } @@ -186,10 +190,12 @@ func (ts TimeSpan) Format(layout, separator string, useDuration bool) string { // if the time is UTC and the format doesn't contain zulu field ("Z") or timezone field ("07") if ts.mark.Location().String() == "UTC" && !layoutHasTimezone(layout) { - layout = RFC5545DateTimeLayout + "Z" + layout = RFC5545DateTimeZulu } + s := ts.Start() e := ts.End() + if useDuration { p := period.Between(s, e) return fmt.Sprintf("%s%s%s", s.Format(layout), separator, p) @@ -198,14 +204,18 @@ func (ts TimeSpan) Format(layout, separator string, useDuration bool) string { return fmt.Sprintf("%s%s%s", s.Format(layout), separator, e.Format(layout)) } +// FormatRFC5545 formats the timespan as a string containing the start time and end time, or the +// start time and duration, if useDuration is true. The two parts are separated by slash. +// The time(s) is expressed as UTC zulu. +// This is as required by iCalendar (RFC5545). func (ts TimeSpan) FormatRFC5545(useDuration bool) string { - return ts.Format(RFC5545DateTimeLayout, "/", useDuration) + return ts.Format(RFC5545DateTimeZulu, "/", useDuration) } // MarshalText formats the timespan as a string using, using RFC5545 layout. // This implements the encoding.TextMarshaler interface. func (ts TimeSpan) MarshalText() (text []byte, err error) { - s := ts.Format(RFC5545DateTimeLayout, "/", true) + s := ts.Format(RFC5545DateTimeZulu, "/", true) return []byte(s), nil } diff --git a/timespan/timespan_test.go b/timespan/timespan_test.go index 9426efec..1060980f 100644 --- a/timespan/timespan_test.go +++ b/timespan/timespan_test.go @@ -175,28 +175,31 @@ func TestTSFormat(t *testing.T) { } func TestTSMarshalText(t *testing.T) { - // use Berlin, which is UTC-1 + // use Berlin, which is UTC+1 or +2 in summer berlin, _ := time.LoadLocation("Europe/Berlin") - t0 := time.Date(2015, 3, 27, 10, 13, 14, 0, time.UTC) + t0 := time.Date(2015, 2, 14, 10, 13, 14, 0, time.UTC) + t1 := time.Date(2015, 6, 27, 10, 13, 15, 0, time.UTC) cases := []struct { start time.Time duration time.Duration exp string }{ - {t0, time.Hour, "20150327T101314Z/PT1H"}, - {t0.In(berlin), time.Minute, "20150327T111314/PT1M"}, + {t0, time.Hour, "20150214T101314Z/PT1H"}, + {t1, 2*time.Hour, "20150627T101315Z/PT2H"}, + {t0.In(berlin), time.Minute, "20150214T111314Z/PT1M"}, // UTC+1 + {t1.In(berlin), time.Second, "20150627T121315Z/PT1S"}, // UTC+2 } - for _, c := range cases { + for i, c := range cases { ts := TimeSpan{c.start, c.duration} s := ts.FormatRFC5545(true) - isEq(t, 0, s, c.exp) + isEq(t, i, s, c.exp) b, err := ts.MarshalText() - isEq(t, 0, err, nil) - isEq(t, 0, string(b), c.exp) + isEq(t, i, err, nil) + isEq(t, i, string(b), c.exp) } } From 891e142739aafeed44ad672a7f434bcd801f9caa Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Tue, 24 Jul 2018 18:29:59 +0100 Subject: [PATCH 088/165] Revised Period to handle hudredth of a second better; added WeeksFloat method. --- date.go | 19 +--- date_test.go | 14 +-- gregorian/util.go | 42 ++++++++ gregorian/util_test.go | 77 ++++++++++++++ period/period.go | 189 ++++++++++++++++++++++++++-------- period/period_test.go | 226 +++++++++++++++++++++++++++++++++++++---- 6 files changed, 474 insertions(+), 93 deletions(-) create mode 100644 gregorian/util.go create mode 100644 gregorian/util_test.go diff --git a/date.go b/date.go index 3893228b..16fd00fb 100644 --- a/date.go +++ b/date.go @@ -5,7 +5,7 @@ package date import ( - "fmt" + "github.com/rickb777/date/gregorian" "github.com/rickb777/date/period" "math" "time" @@ -267,23 +267,10 @@ func (d Date) DaysSinceEpoch() (days PeriodOfDays) { // IsLeap simply tests whether a given year is a leap year, using the Gregorian calendar algorithm. func IsLeap(year int) bool { - return year%4 == 0 && (year%100 != 0 || year%400 == 0) + return gregorian.IsLeap(year) } // DaysIn gives the number of days in a given month, according to the Gregorian calendar. func DaysIn(year int, month time.Month) int { - switch month { - case time.January, time.March, time.May, time.July, time.August, time.October, time.December: - return 31 - - case time.September, time.April, time.June, time.November: - return 30 - - case time.February: - if IsLeap(year) { - return 29 - } - return 28 - } - panic(fmt.Sprintf("Not valid: year %d month %d", year, month)) + return gregorian.DaysIn(year, month) } diff --git a/date_test.go b/date_test.go index 23a0e608..bf64a0f8 100644 --- a/date_test.go +++ b/date_test.go @@ -278,24 +278,14 @@ func max(a, b PeriodOfDays) PeriodOfDays { return b } +// See main testin in period_test.go func TestIsLeap(t *testing.T) { cases := []struct { year int expected bool }{ {2000, true}, - {2400, true}, {2001, false}, - {2002, false}, - {2003, false}, - {2003, false}, - {2004, true}, - {2005, false}, - {1800, false}, - {1900, false}, - {2200, false}, - {2300, false}, - {2500, false}, } for _, c := range cases { got := IsLeap(c.year) @@ -319,7 +309,7 @@ func TestDaysIn(t *testing.T) { for _, c := range cases { got1 := DaysIn(c.year, c.month) if got1 != c.expected { - t.Errorf("DaysIn(%d) == %v, want %v", c.year, got1, c.expected) + t.Errorf("DaysIn(%d, %d) == %v, want %v", c.year, c.month, got1, c.expected) } d := New(c.year, c.month, 1) got2 := d.LastDayOfMonth() diff --git a/gregorian/util.go b/gregorian/util.go new file mode 100644 index 00000000..67df3ab4 --- /dev/null +++ b/gregorian/util.go @@ -0,0 +1,42 @@ +package gregorian + +import ( + "time" +) + +// IsLeap simply tests whether a given year is a leap year, using the Gregorian calendar algorithm. +func IsLeap(year int) bool { + return year%4 == 0 && (year%100 != 0 || year%400 == 0) +} + +// DaysInYear gives the number of days in a given year, according to the Gregorian calendar. +func DaysInYear(year int) int { + if IsLeap(year) { + return 366 + } + return 365 +} + +// DaysIn gives the number of days in a given month, according to the Gregorian calendar. +func DaysIn(year int, month time.Month) int { + if month == time.February && IsLeap(year) { + return 29 + } + return daysInMonth[month] +} + +var daysInMonth = []int{ + 0, + 31, // January + 28, + 31, // March + 30, + 31, // May + 30, + 31, // July + 31, + 30, // September + 31, + 30, // November + 31, +} diff --git a/gregorian/util_test.go b/gregorian/util_test.go new file mode 100644 index 00000000..1890b728 --- /dev/null +++ b/gregorian/util_test.go @@ -0,0 +1,77 @@ +package gregorian + +import ( + "testing" + "time" +) + +func TestIsLeap(t *testing.T) { + cases := []struct { + year int + expected bool + }{ + {2000, true}, + {2400, true}, + {2001, false}, + {2002, false}, + {2003, false}, + {2003, false}, + {2004, true}, + {2005, false}, + {1800, false}, + {1900, false}, + {2200, false}, + {2300, false}, + {2500, false}, + } + for _, c := range cases { + got := IsLeap(c.year) + if got != c.expected { + t.Errorf("TestIsLeap(%d) == %v, want %v", c.year, got, c.expected) + } + } +} + +func TestDaysInYear(t *testing.T) { + cases := []struct { + year int + expected int + }{ + {2000, 366}, + {2001, 365}, + } + for _, c := range cases { + got1 := DaysInYear(c.year) + if got1 != c.expected { + t.Errorf("DaysInYear(%d) == %v, want %v", c.year, got1, c.expected) + } + } +} + +func TestDaysIn(t *testing.T) { + cases := []struct { + year int + month time.Month + expected int + }{ + {2000, time.January, 31}, + {2000, time.February, 29}, + {2001, time.February, 28}, + {2001, time.April, 30}, + {2001, time.May, 31}, + {2001, time.June, 30}, + {2001, time.July, 31}, + {2001, time.August, 31}, + {2001, time.September, 30}, + {2001, time.October, 31}, + {2001, time.November, 30}, + {2001, time.December, 31}, + } + for _, c := range cases { + got1 := DaysIn(c.year, c.month) + if got1 != c.expected { + t.Errorf("DaysIn(%d, %d) == %v, want %v", c.year, c.month, got1, c.expected) + } + } +} + diff --git a/period/period.go b/period/period.go index 6c836c97..6ed2176e 100644 --- a/period/period.go +++ b/period/period.go @@ -7,10 +7,11 @@ package period import ( "fmt" "time" + "github.com/rickb777/date/gregorian" ) -const daysPerYearApproxE3 = 365250 // 365.25 days -const daysPerMonthApproxE4 = 304375 // 30.437 days per month +const daysPerYearApproxE3 = 365250 // 365.25 days (TODO should be 365.2425) +const daysPerMonthApproxE4 = 304375 // 30.4375 days per month const oneE5 = 100000 const oneE6 = 1000000 @@ -39,20 +40,29 @@ type Period struct { years, months, days, hours, minutes, seconds int16 } -// NewYMD creates a simple period without any fractional parts. All the parameters -// must have the same sign (otherwise a panic occurs). +// NewYMD creates a simple period without any fractional parts. The fields are initialised verbatim +// without any normalisation; e.g. 12 months will not become 1 year. Use the Normalise method if you +// need to. +// +// All the parameters must have the same sign (otherwise a panic occurs). func NewYMD(years, months, days int) Period { return New(years, months, days, 0, 0, 0) } -// NewHMS creates a simple period without any fractional parts. All the parameters -// must have the same sign (otherwise a panic occurs). +// NewHMS creates a simple period without any fractional parts. The fields are initialised verbatim +// without any normalisation; e.g. 120 seconds will not become 2 minutes. Use the Normalise method +// if you need to. +// +// All the parameters must have the same sign (otherwise a panic occurs). func NewHMS(hours, minutes, seconds int) Period { return New(0, 0, 0, hours, minutes, seconds) } -// New creates a simple period without any fractional parts. All the parameters -// must have the same sign (otherwise a panic occurs). +// New creates a simple period without any fractional parts. The fields are initialised verbatim +// without any normalisation; e.g. 120 seconds will not become 2 minutes. Use the Normalise method +// if you need to. +// +// All the parameters must have the same sign (otherwise a panic occurs). func New(years, months, days, hours, minutes, seconds int) Period { if (years >= 0 && months >= 0 && days >= 0 && hours >= 0 && minutes >= 0 && seconds >= 0) || (years <= 0 && months <= 0 && days <= 0 && hours <= 0 && minutes <= 0 && seconds <= 0) { @@ -63,11 +73,14 @@ func New(years, months, days, hours, minutes, seconds int) Period { years, months, days, hours, minutes, seconds)) } +// TODO normalise (precise) +// TODO NewFloat + // NewOf converts a time duration to a Period, and also indicates whether the conversion is precise. // Any time duration that spans more than ± 3276 hours will be approximated by assuming that there // are 24 hours per day, 30.4375 per month and 365.25 days per year. func NewOf(duration time.Duration) (p Period, precise bool) { - sign := 1 + var sign int16 = 1 d := duration if duration < 0 { sign = -1 @@ -83,22 +96,23 @@ func NewOf(duration time.Duration) (p Period, precise bool) { months := ((10000 * days) / daysPerMonthApproxE4) - (12 * years) hours -= days * 24 days = ((days * 10000) - (daysPerMonthApproxE4 * months) - (10 * daysPerYearApproxE3 * years)) / 10000 - return New(sign*int(years), sign*int(months), sign*int(days), sign*int(hours), 0, 0), false + return Period{10*sign*int16(years), 10*sign*int16(months), 10*sign*int16(days), 10*sign*int16(hours), 0, 0}, false } minutes := int64(d % time.Hour / time.Minute) - seconds := int64(d % time.Minute / time.Second) + seconds := int64(d % time.Minute / (100*time.Millisecond)) - return New(0, 0, 0, sign*int(hours), sign*int(minutes), sign*int(seconds)), true + return Period{0, 0, 0, 10*sign*int16(hours), 10*sign*int16(minutes), sign*int16(seconds)}, true } // Between converts the span between two times to a period. Based on the Gregorian conversion algorithms -// of `time.Time`, the resultant period is precise. +// of `time.Time`, the resultant period is precise. It is normalised based on the calendar: the whole +// number of years and months are calculated and the number of days obtained from what's left over. // // Remember that the resultant period does not retain any knowledge of the calendar, so any subsequent // computations applied to the period can only be precise if they concern either the date (year, month, // day) part, or the clock (hour, minute, second) part, but not both. -func Between(t1, t2 time.Time) Period { +func Between(t1, t2 time.Time) (p Period) { if t1.Location() != t2.Location() { t2 = t2.In(t1.Location()) } @@ -108,11 +122,15 @@ func Between(t1, t2 time.Time) Period { t1, t2, sign = t2, t1, -1 } - year, month, day, hour, min, sec := timeDiff(t1, t2) + year, month, day, hour, min, sec, hundredth := timeDiff(t1, t2) if sign < 0 { - return New(year, month, day, hour, min, sec).Negate() + p = New(-year, -month, -day, -hour, -min, -sec) + p.seconds -= int16(hundredth) + } else { + p = New(year, month, day, hour, min, sec) + p.seconds += int16(hundredth) } - return New(year, month, day, hour, min, sec) + return } //func TimeDiff(t1, t2 time.Time) (year, month, day, hour, min, sec int) { @@ -125,7 +143,7 @@ func Between(t1, t2 time.Time) Period { // return timeDiff(t1, t2) //} -func timeDiff(t1, t2 time.Time) (year, month, day, hour, min, sec int) { +func timeDiff(t1, t2 time.Time) (year, month, day, hour, min, sec, hundredth int) { y1, m1, d1 := t1.Date() y2, m2, d2 := t2.Date() @@ -138,6 +156,7 @@ func timeDiff(t1, t2 time.Time) (year, month, day, hour, min, sec int) { hour = int(hh2 - hh1) min = int(mm2 - mm1) sec = int(ss2 - ss1) + hundredth = (t2.Nanosecond() - t1.Nanosecond()) / int(100*time.Millisecond) //fmt.Printf("A) %d %d %d, %d %d %d\n", year, month, day, hour, min, sec) // Normalize negative values @@ -145,26 +164,102 @@ func timeDiff(t1, t2 time.Time) (year, month, day, hour, min, sec int) { sec += 60 min-- } + if min < 0 { min += 60 hour-- } + + //delta := 0 if hour < 0 { hour += 24 - day-- + //delta = -1 + } + + if month < 0 { // second month is earlier in the year than the first month + //if month > 0 { + // end := gregorian.DaysIn(y1, m1) - d1 + // day = end + d2 + delta + // month-- + //} + //for month < -12 { + month += 12 + year-- + //} + + //} + //if day < 0 { + end := gregorian.DaysIn(y2, m2) - d2 + day = d1 + end + //} + } else { + if d1 > 1 { + + } } - if day < 0 { - // days in month: - t := time.Date(y1, m1, 32, 0, 0, 0, 0, time.UTC) - day += 32 - t.Day() - month-- + + fmt.Printf("B) %d %d %d, %d %d %d %d\n", year, month, day, hour, min, sec, hundredth) + return +} + +// DaysBetween converts the span between two times to a period. Based on the Gregorian conversion +// algorithms of `time.Time`, the resultant period is precise. It is not normalised; the result will +// contain zero in the years and months fields, but the number of days may be large. +// +// Remember that the resultant period does not retain any knowledge of the calendar, so any subsequent +// computations applied to the period can only be precise if they concern either the date (year, month, +// day) part, or the clock (hour, minute, second) part, but not both. +func DaysBetween(t1, t2 time.Time) (p Period) { + if t1.Location() != t2.Location() { + t2 = t2.In(t1.Location()) } - if month < 0 { - month += 12 - year-- + + sign := 1 + if t2.Before(t1) { + t1, t2, sign = t2, t1, -1 } - //fmt.Printf("B) %d %d %d, %d %d %d\n", year, month, day, hour, min, sec) + day, hour, min, sec, hundredth := daysDiff(t1, t2) + if sign < 0 { + p = New(0, 0, -day, -hour, -min, -sec) + p.seconds -= int16(hundredth) + } else { + p = New(0, 0, day, hour, min, sec) + p.seconds += int16(hundredth) + } + return +} + +func daysDiff(t1, t2 time.Time) (day, hour, min, sec, hundredth int) { + duration := t2.Sub(t1) + + hh1, mm1, ss1 := t1.Clock() + hh2, mm2, ss2 := t2.Clock() + + //year = int(y2 - y1) + //month = int(m2 - m1) + day = int(duration / (24*time.Hour)) + hour = int(hh2 - hh1) + min = int(mm2 - mm1) + sec = int(ss2 - ss1) + hundredth = (t2.Nanosecond() - t1.Nanosecond()) / 100000000 + //fmt.Printf("A) %d %d %d, %d %d %d\n", year, month, day, hour, min, sec) + + // Normalize negative values + if sec < 0 { + sec += 60 + min-- + } + if min < 0 { + min += 60 + hour-- + } + if hour < 0 { + hour += 24 + day-- + } + + fmt.Printf("B) %d, %d %d %d %d\n", day, hour, min, sec, hundredth) return } @@ -245,47 +340,54 @@ func (period Period) Sign() int { } // Years gets the whole number of years in the period. -// The result does not include any other field. +// The result is the number of years and does not include any other field. func (period Period) Years() int { return int(period.YearsFloat()) } // YearsFloat gets the number of years in the period, including a fraction if any is present. -// The result does not include any other field. +// The result is the number of years and does not include any other field. func (period Period) YearsFloat() float32 { return float32(period.years) / 10 } // Months gets the whole number of months in the period. -// The result does not include any other field. +// The result is the number of months and does not include any other field. func (period Period) Months() int { return int(period.MonthsFloat()) } // MonthsFloat gets the number of months in the period. -// The result does not include any other field. +// The result is the number of months and does not include any other field. func (period Period) MonthsFloat() float32 { return float32(period.months) / 10 } // Days gets the whole number of days in the period. This includes the implied -// number of weeks but excludes the specified years and months. +// number of weeks but does not include any other field. func (period Period) Days() int { return int(period.DaysFloat()) } // DaysFloat gets the number of days in the period. This includes the implied -// number of weeks. +// number of weeks but does not include any other field. func (period Period) DaysFloat() float32 { return float32(period.days) / 10 } // Weeks calculates the number of whole weeks from the number of days. If the result // would contain a fraction, it is truncated. +// The result is the number of weeks and does not include any other field. func (period Period) Weeks() int { return int(period.days) / 70 } +// WeeksFloat calculates the number of weeks from the number of days. +// The result is the number of weeks and does not include any other field. +func (period Period) WeeksFloat() float32 { + return float32(period.days) / 70 +} + // ModuloDays calculates the whole number of days remaining after the whole number of weeks // has been excluded. func (period Period) ModuloDays() int { @@ -298,46 +400,47 @@ func (period Period) ModuloDays() int { } // Hours gets the whole number of hours in the period. -// The result does not include any other field. +// The result is the number of hours and does not include any other field. func (period Period) Hours() int { return int(period.HoursFloat()) } // HoursFloat gets the number of hours in the period. -// The result does not include any other field. +// The result is the number of hours and does not include any other field. func (period Period) HoursFloat() float32 { return float32(period.hours) / 10 } // Minutes gets the whole number of minutes in the period. -// The result does not include any other field. +// The result is the number of minutes and does not include any other field. func (period Period) Minutes() int { return int(period.MinutesFloat()) } // MinutesFloat gets the number of minutes in the period. -// The result does not include any other field. +// The result is the number of minutes and does not include any other field. func (period Period) MinutesFloat() float32 { return float32(period.minutes) / 10 } // Seconds gets the whole number of seconds in the period. -// The result does not include any other field. +// The result is the number of seconds and does not include any other field. func (period Period) Seconds() int { return int(period.SecondsFloat()) } // SecondsFloat gets the number of seconds in the period. -// The result does not include any other field. +// The result is the number of seconds and does not include any other field. func (period Period) SecondsFloat() float32 { return float32(period.seconds) / 10 } // Duration converts a period to the equivalent duration in nanoseconds. // A flag is also returned that is true when the conversion was precise and false otherwise. -// When the period specifies years, months and days, it is impossible to be precise, so -// the duration is calculated on the basis of a year being 365.25 days and a month being -// 1/12 of a that; days are all 24 hours long. +// When the period specifies years, months and days, it is impossible to be precise because +// the result would depend on knowing date and timezone information, so the duration is +// estimated on the basis of a year being 365.25 days and a month being +// 1/12 of a that; days are all assumed to be 24 hours long. func (period Period) Duration() (time.Duration, bool) { // remember that the fields are all fixed-point 1E1 tdE6 := time.Duration(totalDaysApproxE6(period)) * 86400 diff --git a/period/period_test.go b/period/period_test.go index bd0e17da..0410b451 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -11,8 +11,8 @@ import ( ) var oneDayApprox = 24 * time.Hour -var oneMonthApprox = 2629800 * time.Second -var oneYearApprox = 31557600 * time.Second +var oneMonthApprox = 2629800 * time.Second // 30.4375 days +var oneYearApprox = 31557600 * time.Second // 365.25 days func TestParsePeriod(t *testing.T) { cases := []struct { @@ -95,7 +95,7 @@ func TestPeriodString(t *testing.T) { } } -func TestPeriodComponents(t *testing.T) { +func TestPeriodIntComponents(t *testing.T) { cases := []struct { value string y, m, w, d, dx, hh, mm, ss int @@ -103,6 +103,8 @@ func TestPeriodComponents(t *testing.T) { {"P0D", 0, 0, 0, 0, 0, 0, 0, 0}, {"P1Y", 1, 0, 0, 0, 0, 0, 0, 0}, {"-P1Y", -1, 0, 0, 0, 0, 0, 0, 0}, + {"P1W", 0, 0, 1, 7, 0, 0, 0, 0}, + {"-P1W", 0, 0, -1, -7, 0, 0, 0, 0}, {"P6M", 0, 6, 0, 0, 0, 0, 0, 0}, {"-P6M", 0, -6, 0, 0, 0, 0, 0, 0}, {"P39D", 0, 0, 5, 39, 4, 0, 0, 0}, @@ -142,6 +144,62 @@ func TestPeriodComponents(t *testing.T) { } } +func TestPeriodFloatComponents(t *testing.T) { + cases := []struct { + value string + y, m, w, d, dx, hh, mm, ss float32 + }{ + {"P0D", 0, 0, 0, 0, 0, 0, 0, 0}, + + // YMD cases + {"P1Y", 1, 0, 0, 0, 0, 0, 0, 0}, + {"P1.1Y", 1.1, 0, 0, 0, 0, 0, 0, 0}, + {"-P1Y", -1, 0, 0, 0, 0, 0, 0, 0}, + {"P1W", 0, 0, 1, 7, 0, 0, 0, 0}, + {"P1.1W", 0, 0, 1.1, 7.7, 0, 0, 0, 0}, + {"-P1W", 0, 0, -1, -7, 0, 0, 0, 0}, + {"P1.1M", 0, 1.1, 0, 0, 0, 0, 0, 0}, + {"P6M", 0, 6, 0, 0, 0, 0, 0, 0}, + {"-P6M", 0, -6, 0, 0, 0, 0, 0, 0}, + {"P39D", 0, 0, 5.571429, 39, 4, 0, 0, 0}, + {"-P39D", 0, 0, -5.571429, -39, -4, 0, 0, 0}, + {"P4D", 0, 0, 0.5714286, 4, 4, 0, 0, 0}, + {"-P4D", 0, 0, -0.5714286, -4, -4, 0, 0, 0}, + + // HMS cases + {"PT1.1H", 0, 0, 0, 0, 0, 1.1, 0, 0}, + {"PT12H", 0, 0, 0, 0, 0, 12, 0, 0}, + {"PT1.1M", 0, 0, 0, 0, 0, 0, 1.1, 0}, + {"PT30M", 0, 0, 0, 0, 0, 0, 30, 0}, + {"PT1.1S", 0, 0, 0, 0, 0, 0, 0, 1.1}, + {"PT5S", 0, 0, 0, 0, 0, 0, 0, 5}, + } + for i, c := range cases { + p := MustParse(c.value) + if p.YearsFloat() != c.y { + t.Errorf("%d: %s.YearsFloat() == %g, want %g", i, c.value, p.YearsFloat(), c.y) + } + if p.MonthsFloat() != c.m { + t.Errorf("%d: %s.MonthsFloat() == %g, want %g", i, c.value, p.MonthsFloat(), c.m) + } + if p.WeeksFloat() != c.w { + t.Errorf("%d: %s.WeeksFloat() == %g, want %g", i, c.value, p.WeeksFloat(), c.w) + } + if p.DaysFloat() != c.d { + t.Errorf("%d: %s.DaysFloat() == %g, want %g", i, c.value, p.DaysFloat(), c.d) + } + if p.HoursFloat() != c.hh { + t.Errorf("%d: %s.HoursFloat() == %g, want %g", i, c.value, p.HoursFloat(), c.hh) + } + if p.MinutesFloat() != c.mm { + t.Errorf("%d: %s.MinutesFloat() == %g, want %g", i, c.value, p.MinutesFloat(), c.mm) + } + if p.SecondsFloat() != c.ss { + t.Errorf("%d: %s.SecondsFloat() == %g, want %g", i, c.value, p.SecondsFloat(), c.ss) + } + } +} + func TestPeriodToDuration(t *testing.T) { cases := []struct { value string @@ -150,8 +208,11 @@ func TestPeriodToDuration(t *testing.T) { }{ {"P0D", time.Duration(0), true}, {"PT1S", 1 * time.Second, true}, + {"PT0.1S", 100 * time.Millisecond, true}, {"PT1M", 60 * time.Second, true}, + {"PT0.1M", 6 * time.Second, true}, {"PT1H", 3600 * time.Second, true}, + {"PT0.1H", 360 * time.Second, true}, {"P1D", 24 * time.Hour, false}, {"P1M", oneMonthApprox, false}, {"P1Y", oneYearApprox, false}, @@ -321,6 +382,7 @@ func TestNewOf(t *testing.T) { expected Period precise bool }{ + {100 * time.Millisecond, Period{0, 0, 0, 0, 0, 1}, true}, {time.Second, Period{0, 0, 0, 0, 0, 10}, true}, {time.Minute, Period{0, 0, 0, 0, 10, 0}, true}, {time.Hour, Period{0, 0, 0, 10, 0, 0}, true}, @@ -332,6 +394,7 @@ func TestNewOf(t *testing.T) { {36525 * oneDayApprox, Period{1000, 0, 0, 0, 0, 0}, false}, {36525*oneDayApprox - time.Hour, Period{990, 110, 290, 230, 0, 0}, false}, + {-100 * time.Millisecond, Period{0, 0, 0, 0, 0, -1}, true}, {-time.Second, Period{0, 0, 0, 0, 0, -10}, true}, {-time.Minute, Period{0, 0, 0, 0, -10, 0}, true}, {-time.Hour, Period{0, 0, 0, -10, 0, 0}, true}, @@ -352,24 +415,106 @@ func TestNewOf(t *testing.T) { } func TestBetween(t *testing.T) { + //halfSec := int(500 * time.Millisecond) + now := time.Now() + cases := []struct { a, b time.Time expected Period }{ - {time.Now(), time.Now(), Period{0, 0, 0, 0, 0, 0}}, - {time.Date(2015, 5, 1, 0, 0, 0, 0, time.UTC), time.Date(2016, 6, 2, 1, 1, 1, 1, time.UTC), Period{10, 10, 10, 10, 10, 10}}, - {time.Date(2016, 1, 2, 0, 0, 0, 0, time.UTC), time.Date(2016, 2, 1, 0, 0, 0, 0, time.UTC), Period{0, 0, 300, 0, 0, 0}}, - {time.Date(2015, 2, 1, 0, 0, 0, 0, time.UTC), time.Date(2015, 3, 1, 0, 0, 0, 0, time.UTC), Period{0, 10, 0, 0, 0, 0}}, - {time.Date(2016, 2, 1, 0, 0, 0, 0, time.UTC), time.Date(2016, 3, 1, 0, 0, 0, 0, time.UTC), Period{0, 10, 0, 0, 0, 0}}, - {time.Date(2015, 2, 2, 0, 0, 0, 0, time.UTC), time.Date(2015, 3, 1, 0, 0, 0, 0, time.UTC), Period{0, 0, 270, 0, 0, 0}}, - {time.Date(2016, 2, 2, 0, 0, 0, 0, time.UTC), time.Date(2016, 3, 1, 0, 0, 0, 0, time.UTC), Period{0, 0, 280, 0, 0, 0}}, - {time.Date(2015, 2, 11, 0, 0, 0, 0, time.UTC), time.Date(2016, 1, 12, 0, 0, 0, 0, time.UTC), Period{0, 110, 10, 0, 0, 0}}, - {time.Date(2016, 1, 12, 0, 0, 0, 0, time.UTC), time.Date(2015, 2, 11, 0, 0, 0, 0, time.UTC), Period{0, -110, -10, 0, 0, 0}}, + {now, now, Period{0, 0, 0, 0, 0, 0}}, + + //// simple positive date calculations + {utc(2015, 1, 1, 0, 0, 0, 0), utc(2016, 2, 2, 1, 1, 1, 1), Period{10, 10, 10, 10, 10, 10}}, + {utc(2015, 2, 1, 0, 0, 0, 0), utc(2016, 3, 2, 1, 1, 1, 1), Period{10, 10, 10, 10, 10, 10}}, + {utc(2015, 3, 1, 0, 0, 0, 0), utc(2016, 4, 2, 1, 1, 1, 1), Period{10, 10, 10, 10, 10, 10}}, + {utc(2015, 4, 1, 0, 0, 0, 0), utc(2016, 5, 2, 1, 1, 1, 1), Period{10, 10, 10, 10, 10, 10}}, + {utc(2015, 5, 1, 0, 0, 0, 0), utc(2016, 6, 2, 1, 1, 1, 1), Period{10, 10, 10, 10, 10, 10}}, + {utc(2015, 6, 1, 0, 0, 0, 0), utc(2016, 7, 2, 1, 1, 1, 1), Period{10, 10, 10, 10, 10, 10}}, + + // negative date calculation + {utc(2016, 6, 2, 1, 1, 1, 1), utc(2015, 5, 1, 0, 0, 0, 0), Period{-10, -10, -10, -10, -10, -10}}, + + // less than one month + //{utc(2016, 1, 2, 0, 0, 0, 0), utc(2016, 2, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, + //{utc(2015, 2, 2, 0, 0, 0, 0), utc(2015, 3, 1, 0, 0, 0, 0), Period{0, 0, 270, 0, 0, 0}}, // non-leap + //{utc(2016, 2, 2, 0, 0, 0, 0), utc(2016, 3, 1, 0, 0, 0, 0), Period{0, 0, 280, 0, 0, 0}}, // leap year + //{utc(2016, 3, 2, 0, 0, 0, 0), utc(2016, 4, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, + //{utc(2016, 4, 2, 0, 0, 0, 0), utc(2016, 5, 1, 0, 0, 0, 0), Period{0, 0, 290, 0, 0, 0}}, + //{utc(2016, 5, 2, 0, 0, 0, 0), utc(2016, 6, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, + //{utc(2016, 6, 2, 0, 0, 0, 0), utc(2016, 7, 1, 0, 0, 0, 0), Period{0, 0, 290, 0, 0, 0}}, + + //// daytime only + //{utc(2015, 1, 1, 2, 3, 4, 0), utc(2015, 1, 1, 4, 4, 7, halfSec), Period{0, 0, 0, 20, 10, 35}}, + //{utc(2015, 1, 1, 2, 3, 4, halfSec), utc(2015, 1, 1, 4, 4, 7, 0), Period{0, 0, 0, 20, 10, 25}}, + + // different dates and times + //{utc(2015, 2, 1, 0, 0, 0, 0), utc(2015, 4, 30, 5, 6, 7, 0), Period{0, 10, 260, 50, 60, 70}}, + //{utc(2015, 2, 12, 0, 0, 0, 0), utc(2015, 4, 10, 5, 6, 7, 0), Period{0, 10, 260, 50, 60, 70}}, + + // earlier month in later year + //{utc(2015, 12, 22, 0, 0, 0, 0), utc(2016, 1, 10, 5, 6, 7, 0), Period{0, 0, 200, 50, 60, 70}}, + //{utc(2015, 2, 11, 5, 6, 7, halfSec), utc(2016, 1, 10, 0, 0, 0, 0), Period{0, 100, 290, 220, 570, 565}}, } for i, c := range cases { n := Between(c.a, c.b) if n != c.expected { - t.Errorf("%d: Between(%v, %v) gives %v %#v, want %v", i, c.a, c.b, n, n, c.expected) + t.Errorf("%d: Between(%v, %v)\n gives %-20s %#v,\n want %-20s %#v", i, c.a, c.b, n, n, c.expected, c.expected) + } + } +} + +func TestDaysBetween(t *testing.T) { + //halfSec := int(500 * time.Millisecond) + now := time.Now() + + cases := []struct { + a, b time.Time + expected Period + }{ + {now, now, Period{0, 0, 0, 0, 0, 0}}, + + //// simple positive date calculations + {utc(2015, 1, 1, 0, 0, 0, 0), utc(2015, 2, 2, 1, 1, 1, 1), Period{0, 0, 320, 10, 10, 10}}, + {utc(2015, 2, 1, 0, 0, 0, 0), utc(2015, 3, 2, 1, 1, 1, 1), Period{0, 0, 290, 10, 10, 10}}, + {utc(2015, 3, 1, 0, 0, 0, 0), utc(2015, 4, 2, 1, 1, 1, 1), Period{0, 0, 320, 10, 10, 10}}, + {utc(2015, 4, 1, 0, 0, 0, 0), utc(2015, 5, 2, 1, 1, 1, 1), Period{0, 0, 310, 10, 10, 10}}, + {utc(2015, 5, 1, 0, 0, 0, 0), utc(2015, 6, 2, 1, 1, 1, 1), Period{0, 0, 320, 10, 10, 10}}, + {utc(2015, 6, 1, 0, 0, 0, 0), utc(2015, 7, 2, 1, 1, 1, 1), Period{0, 0, 310, 10, 10, 10}}, + {utc(2015, 1, 1, 0, 0, 0, 0), utc(2015, 7, 2, 1, 1, 1, 1), Period{0, 0, 1820, 10, 10, 10}}, + + // BST drops an hour at the daylight-saving transition + {utc(2015, 1, 1, 0, 0, 0, 0), bst(2015, 7, 2, 1, 1, 1, 1), Period{0, 0, 1820, 0, 10, 10}}, + + // negative date calculation + {utc(2015, 6, 2, 0, 0, 0, 0), utc(2015, 5, 1, 0, 0, 0, 0), Period{0, 0, -320, 0, 0, 0}}, + {utc(2015, 6, 2, 1, 1, 1, 1), utc(2015, 5, 1, 0, 0, 0, 0), Period{0, 0, -320, -10, -10, -10}}, + + //// less than one month + //{utc(2016, 1, 2, 0, 0, 0, 0), utc(2016, 2, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, + //{utc(2015, 2, 2, 0, 0, 0, 0), utc(2015, 3, 1, 0, 0, 0, 0), Period{0, 0, 270, 0, 0, 0}}, // non-leap + //{utc(2016, 2, 2, 0, 0, 0, 0), utc(2016, 3, 1, 0, 0, 0, 0), Period{0, 0, 280, 0, 0, 0}}, // leap year + //{utc(2016, 3, 2, 0, 0, 0, 0), utc(2016, 4, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, + //{utc(2016, 4, 2, 0, 0, 0, 0), utc(2016, 5, 1, 0, 0, 0, 0), Period{0, 0, 290, 0, 0, 0}}, + //{utc(2016, 5, 2, 0, 0, 0, 0), utc(2016, 6, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, + //{utc(2016, 6, 2, 0, 0, 0, 0), utc(2016, 7, 1, 0, 0, 0, 0), Period{0, 0, 290, 0, 0, 0}}, + // + //// daytime only + //{utc(2015, 1, 1, 2, 3, 4, 0), utc(2015, 1, 1, 4, 4, 7, halfSec), Period{0, 0, 0, 20, 10, 35}}, + //{utc(2015, 1, 1, 2, 3, 4, halfSec), utc(2015, 1, 1, 4, 4, 7, 0), Period{0, 0, 0, 20, 10, 25}}, + + // different dates and times + //{utc(2015, 2, 1, 0, 0, 0, 0), utc(2015, 4, 30, 5, 6, 7, 0), Period{0, 10, 260, 50, 60, 70}}, + //{utc(2015, 2, 12, 0, 0, 0, 0), utc(2015, 4, 10, 5, 6, 7, 0), Period{0, 10, 260, 50, 60, 70}}, + + // earlier month in later year + //{utc(2015, 12, 22, 0, 0, 0, 0), utc(2016, 1, 10, 5, 6, 7, 0), Period{0, 0, 200, 50, 60, 70}}, + //{utc(2015, 2, 11, 5, 6, 7, halfSec), utc(2016, 1, 10, 0, 0, 0, 0), Period{0, 100, 290, 220, 570, 565}}, + } + for i, c := range cases { + n := DaysBetween(c.a, c.b) + if n != c.expected { + t.Errorf("%d: Between(%v, %v)\n gives %-20s %#v,\n want %-20s %#v", i, c.a, c.b, n, n, c.expected, c.expected) } } } @@ -379,20 +524,41 @@ func TestNormalise(t *testing.T) { source, expected Period precise bool }{ + // zero cases {New(0, 0, 0, 0, 0, 0), New(0, 0, 0, 0, 0, 0), true}, - {New(0, 12, 0, 0, 0, 60), New(1, 0, 0, 0, 1, 0), true}, - {New(0, 25, 0, 0, 61, 65), New(2, 1, 0, 1, 2, 5), true}, - {New(0, 0, 31, 0, 0, 0), New(0, 0, 31, 0, 0, 0), true}, - {New(0, 0, 29, 0, 0, 0), New(0, 0, 29, 0, 0, 0), false}, - {New(0, 0, 31, 0, 0, 0), Period{0, 10, 6, 0, 0, 0}, false}, - {New(0, 0, 61, 0, 0, 0), Period{0, 20, 2, 0, 0, 0}, false}, - {New(0, 11, 30, 23, 59, 60), Period{10, 0, 6, 0, 0, 0}, false}, - {New(0, 11, 30, 23, 59, 60).Negate(), Period{10, 0, 6, 0, 0, 0}.Negate(), false}, + {New(0, 0, 0, 0, 0, 0), New(0, 0, 0, 0, 0, 0), false}, + + // carry seconds to minutes + {Period{0, 0, 0, 0, 0, 699}, Period{0, 0, 0, 0, 10, 99}, true}, + {Period{0, 0, 0, 0, 0, -699}, Period{0, 0, 0, 0, -10, -99}, true}, + + // carry minutes to hours + {Period{0, 0, 0, 0, 699, 0}, Period{0, 0, 0, 10, 99, 0}, true}, + //{Period{0, 0, 0, 0, -699, 0}, Period{0, 0, 0, -10, -99, 0}, true}, + + // carry hours to days - two cases + {Period{0, 0, 0, 249, 0, 0}, Period{0, 0, 0, 249, 0, 0}, true}, + {Period{0, 0, 0, 249, 0, 0}, Period{0, 0, 10, 9, 0, 0}, false}, + + // carry days to months - two cases + {Period{0, 0, 323, 0, 0, 0}, Period{0, 0, 323, 0, 0, 0}, true}, + {Period{0, 0, 323, 0, 0, 0}, Period{0, 10, 19, 0, 0, 0}, false}, + + // carry months to years + {Period{0, 129, 0, 0, 0, 0}, Period{10, 9, 0, 0, 0, 0}, true}, + + // full ripple - two cases + {Period{0, 121, 305, 239, 591, 601}, Period{10, 1, 305, 249, 1, 1}, true}, + {Period{0, 119, 300, 239, 591, 601}, Period{10, 9, 6, 9, 1, 1}, false}, + + // full ripple - negative cases + {Period{0, -121, -305, -239, -591, -601}, Period{-10, -1, -305, -249, -1, -1}, true}, + {Period{0, -119, -300, -239, -591, -601}, Period{-10, -9, -6, -9, -1, -1}, false}, } for i, c := range cases { n := c.source.Normalise(c.precise) if n != c.expected { - t.Errorf("%d: %v.Normalise(%v) gives %v %#v, want %v", i, c.source, c.precise, n, n, c.expected) + t.Errorf("%3d: %v.Normalise(%v)\n gives %-20s %#v,\n want %-20s %#v", i, c.source, c.precise, n, n, c.expected, c.expected) } } } @@ -409,6 +575,8 @@ func TestPeriodFormat(t *testing.T) { {"P1M", "1 month"}, {"P6M", "6 months"}, {"-P6M", "6 months"}, + {"P1W", "1 week"}, + {"-P1W", "1 week"}, {"P7D", "1 week"}, {"P35D", "5 weeks"}, {"-P35D", "5 weeks"}, @@ -552,3 +720,17 @@ func TestPeriodScale(t *testing.T) { } } } + +func utc(year int, month time.Month, day, hour, min, sec, msec int) time.Time { + return time.Date(year, month, day, hour, min, sec, msec*int(time.Millisecond), time.UTC) +} + +func bst(year int, month time.Month, day, hour, min, sec, msec int) time.Time { + return time.Date(year, month, day, hour, min, sec, msec*int(time.Millisecond), london) +} + +var london *time.Location // UTC + 1 hour during summer + +func init() { + london, _ = time.LoadLocation("Europe/London") +} From 22a539e61e25939730d51b425dbd80ca3839c1d2 Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Wed, 25 Jul 2018 22:13:13 +0100 Subject: [PATCH 089/165] Improved NewOf function, plus a few other minor tweaks --- period/marshal_test.go | 6 ++-- period/parse.go | 6 ++-- period/period.go | 73 ++++++++++++++++++++++++++---------------- period/period_test.go | 60 +++++++++++++++++++++++++--------- 4 files changed, 97 insertions(+), 48 deletions(-) diff --git a/period/marshal_test.go b/period/marshal_test.go index f07b3f9d..89377f88 100644 --- a/period/marshal_test.go +++ b/period/marshal_test.go @@ -117,9 +117,9 @@ func TestInvalidPeriodText(t *testing.T) { value string want string }{ - {``, `Cannot parse a blank string as a period.`}, - {`not-a-period`, `Expected 'P' period mark at the start: not-a-period`}, - {`P000`, `Expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' marker: P000`}, + {``, `cannot parse a blank string as a period`}, + {`not-a-period`, `expected 'P' period mark at the start: not-a-period`}, + {`P000`, `expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' marker: P000`}, } for _, c := range cases { var p Period diff --git a/period/parse.go b/period/parse.go index df71d98f..f513b03d 100644 --- a/period/parse.go +++ b/period/parse.go @@ -29,7 +29,7 @@ func MustParse(value string) Period { // The canonical zero is "P0D". func Parse(period string) (Period, error) { if period == "" { - return Period{}, fmt.Errorf("Cannot parse a blank string as a period.") + return Period{}, fmt.Errorf("cannot parse a blank string as a period") } if period == "P0" { @@ -46,7 +46,7 @@ func Parse(period string) (Period, error) { } if pcopy[0] != 'P' { - return Period{}, fmt.Errorf("Expected 'P' period mark at the start: %s", period) + return Period{}, fmt.Errorf("expected 'P' period mark at the start: %s", period) } pcopy = pcopy[1:] @@ -99,7 +99,7 @@ func Parse(period string) (Period, error) { //fmt.Printf("%#v\n", st) if !st.ok { - return Period{}, fmt.Errorf("Expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' marker: %s", period) + return Period{}, fmt.Errorf("expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' marker: %s", period) } if negate { return result.Negate(), nil diff --git a/period/period.go b/period/period.go index 6ed2176e..c037c531 100644 --- a/period/period.go +++ b/period/period.go @@ -10,10 +10,18 @@ import ( "github.com/rickb777/date/gregorian" ) -const daysPerYearApproxE3 = 365250 // 365.25 days (TODO should be 365.2425) -const daysPerMonthApproxE4 = 304375 // 30.4375 days per month +const daysPerYearApproxE4 time.Duration = 3652425 // 365.2425 days by the Gregorian rule +const daysPerMonthApproxE4 time.Duration = 304375 // 30.4375 days per month +const daysPerMonthApproxE6 time.Duration = 30436875 // 30.436875 days per month + +const oneE4 = 10000 const oneE5 = 100000 const oneE6 = 1000000 +const oneE7 = 10000000 + +const hundredMs = 100 * time.Millisecond + +// reminder: int64 overflow is after 9,223,372,036,854,775,807 (math.MaxInt64) // Period holds a period of time and provides conversion to/from ISO-8601 representations. // Therefore there are six fields: years, months, days, hours, minutes, and seconds. @@ -87,22 +95,33 @@ func NewOf(duration time.Duration) (p Period, precise bool) { d = -duration } - hours := int64(d / time.Hour) + sign10 := sign * 10 - // check for 16-bit overflow - if hours > 3276 { - days := hours / 24 - years := (1000 * days) / daysPerYearApproxE3 - months := ((10000 * days) / daysPerMonthApproxE4) - (12 * years) - hours -= days * 24 - days = ((days * 10000) - (daysPerMonthApproxE4 * months) - (10 * daysPerYearApproxE3 * years)) / 10000 - return Period{10*sign*int16(years), 10*sign*int16(months), 10*sign*int16(days), 10*sign*int16(hours), 0, 0}, false + totalHours := d / time.Hour + + // check for 16-bit overflow - occurs near the 4.5 month mark + if totalHours < 3277 { + // simple HMS case + minutes := d % time.Hour / time.Minute + seconds := d % time.Minute / hundredMs + return Period{0, 0, 0, sign10 * int16(totalHours), sign10 * int16(minutes), sign * int16(seconds)}, true } - minutes := int64(d % time.Hour / time.Minute) - seconds := int64(d % time.Minute / (100*time.Millisecond)) + totalDays := totalHours / 24 // ignoring daylight savings adjustments + + if totalDays < 3277 { + hours := totalHours - totalDays*24 + minutes := d % time.Hour / time.Minute + seconds := d % time.Minute / hundredMs + return Period{0, 0, sign10 * int16(totalDays), sign10 * int16(hours), sign10 * int16(minutes), sign * int16(seconds)}, false + } - return Period{0, 0, 0, 10*sign*int16(hours), 10*sign*int16(minutes), sign*int16(seconds)}, true + // TODO it is uncertain whether this is too imprecise and should be improved + years := (oneE4 * totalDays) / daysPerYearApproxE4 + months := ((oneE4 * totalDays) / daysPerMonthApproxE4) - (12 * years) + hours := totalHours - totalDays*24 + totalDays = ((totalDays * oneE4) - (daysPerMonthApproxE4 * months) - (daysPerYearApproxE4 * years)) / oneE4 + return Period{sign10 * int16(years), sign10 * int16(months), sign10 * int16(totalDays), sign10 * int16(hours), 0, 0}, false } // Between converts the span between two times to a period. Based on the Gregorian conversion algorithms @@ -183,8 +202,8 @@ func timeDiff(t1, t2 time.Time) (year, month, day, hour, min, sec, hundredth int // month-- //} //for month < -12 { - month += 12 - year-- + month += 12 + year-- //} //} @@ -238,7 +257,7 @@ func daysDiff(t1, t2 time.Time) (day, hour, min, sec, hundredth int) { //year = int(y2 - y1) //month = int(m2 - m1) - day = int(duration / (24*time.Hour)) + day = int(duration / (24 * time.Hour)) hour = int(hh2 - hh1) min = int(mm2 - mm1) sec = int(ss2 - ss1) @@ -443,7 +462,7 @@ func (period Period) SecondsFloat() float32 { // 1/12 of a that; days are all assumed to be 24 hours long. func (period Period) Duration() (time.Duration, bool) { // remember that the fields are all fixed-point 1E1 - tdE6 := time.Duration(totalDaysApproxE6(period)) * 86400 + tdE6 := totalDaysApproxE7(period) * 8640 hhE3 := time.Duration(period.hours) * 360000 mmE3 := time.Duration(period.minutes) * 6000 ssE3 := time.Duration(period.seconds) * 100 @@ -452,11 +471,11 @@ func (period Period) Duration() (time.Duration, bool) { return tdE6*time.Microsecond + stE3*time.Millisecond, tdE6 == 0 } -func totalDaysApproxE6(period Period) int64 { +func totalDaysApproxE7(period Period) time.Duration { // remember that the fields are all fixed-point 1E1 - ydE6 := int64(period.years) * (daysPerYearApproxE3 * 100) - mdE6 := int64(period.months) * (daysPerMonthApproxE4 * 10) - ddE6 := int64(period.days) * oneE5 + ydE6 := time.Duration(period.years) * (daysPerYearApproxE4 * 100) + mdE6 := time.Duration(period.months) * daysPerMonthApproxE6 + ddE6 := time.Duration(period.days) * oneE6 return ydE6 + mdE6 + ddE6 } @@ -464,8 +483,8 @@ func totalDaysApproxE6(period Period) int64 { // a year is 365.25 days and a month is 1/12 of that. Whole multiples of 24 hours are also included // in the calculation. func (period Period) TotalDaysApprox() int { - tdE6 := totalDaysApproxE6(period.Normalise(false)) - return int(tdE6 / oneE6) + tdE6 := totalDaysApproxE7(period.Normalise(false)) + return int(tdE6 / oneE7) } // TotalMonthsApprox gets the approximate total number of months in the period. The days component @@ -473,9 +492,9 @@ func (period Period) TotalDaysApprox() int { // Whole multiples of 24 hours are also included in the calculation. func (period Period) TotalMonthsApprox() int { p := period.Normalise(false) - mE1 := int(p.years)*12 + int(p.months) - dE6 := int64(p.days) * 1000 / daysPerMonthApproxE4 - return (mE1 + int(dE6)) / 10 + mE1 := time.Duration(p.years)*12 + time.Duration(p.months) + dE6 := time.Duration(p.days) * 100 / daysPerMonthApproxE6 + return int((mE1 + dE6) / 10) } // Normalise attempts to simplify the fields. It operates in either precise or imprecise mode. diff --git a/period/period_test.go b/period/period_test.go index 0410b451..9b68952c 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -10,9 +10,9 @@ import ( "time" ) -var oneDayApprox = 24 * time.Hour -var oneMonthApprox = 2629800 * time.Second // 30.4375 days -var oneYearApprox = 31557600 * time.Second // 365.25 days +var oneDay = 24 * time.Hour +var oneMonthApprox = 2629746 * time.Second // 30.436875 days +var oneYearApprox = 31556952 * time.Second // 365.2425 days func TestParsePeriod(t *testing.T) { cases := []struct { @@ -209,23 +209,32 @@ func TestPeriodToDuration(t *testing.T) { {"P0D", time.Duration(0), true}, {"PT1S", 1 * time.Second, true}, {"PT0.1S", 100 * time.Millisecond, true}, + {"PT3276S", 3276 * time.Second, true}, {"PT1M", 60 * time.Second, true}, {"PT0.1M", 6 * time.Second, true}, + {"PT3276M", 3276 * time.Minute, true}, {"PT1H", 3600 * time.Second, true}, {"PT0.1H", 360 * time.Second, true}, + {"PT3276H", 3276 * time.Hour, true}, {"P1D", 24 * time.Hour, false}, + {"P0.1D", 144 * time.Minute, false}, + {"P3276D", 3276 * 24 * time.Hour, false}, {"P1M", oneMonthApprox, false}, + {"P0.1M", oneMonthApprox / 10, false}, + {"P3276M", 3276 * oneMonthApprox, false}, {"P1Y", oneYearApprox, false}, {"-P1Y", -oneYearApprox, false}, + {"P3276Y", 3276 * oneYearApprox, false}, // near the upper limit of range + {"-P3276Y", -3276 * oneYearApprox, false}, // near the lower limit of range } for i, c := range cases { p := MustParse(c.value) s, prec := p.Duration() if s != c.duration { - t.Errorf("%d: Duration() == %s %v, want %s for %+v", i, s, prec, c.duration, c.value) + t.Errorf("%d: Duration() == %s %v, want %s for %s", i, s, prec, c.duration, c.value) } if prec != c.precise { - t.Errorf("%d: Duration() == %s %v, want %v for %+v", i, s, prec, c.precise, c.value) + t.Errorf("%d: Duration() == %s %v, want %v for %s", i, s, prec, c.precise, c.value) } } } @@ -376,41 +385,62 @@ func TestNewYMD(t *testing.T) { } } +func durationOf(p Period) time.Duration { + d, _ := p.Duration() + return d +} + func TestNewOf(t *testing.T) { + ms := time.Millisecond + cases := []struct { source time.Duration expected Period precise bool }{ + // HMS tests {100 * time.Millisecond, Period{0, 0, 0, 0, 0, 1}, true}, {time.Second, Period{0, 0, 0, 0, 0, 10}, true}, {time.Minute, Period{0, 0, 0, 0, 10, 0}, true}, {time.Hour, Period{0, 0, 0, 10, 0, 0}, true}, {time.Hour + time.Minute + time.Second, Period{0, 0, 0, 10, 10, 10}, true}, {24*time.Hour + time.Minute + time.Second, Period{0, 0, 0, 240, 10, 10}, true}, - {300 * oneDayApprox, Period{0, 90, 260, 0, 0, 0}, false}, - {305 * oneDayApprox, Period{0, 100, 0, 0, 0, 0}, false}, - {305*oneDayApprox - time.Hour, Period{0, 90, 300, 230, 0, 0}, false}, - {36525 * oneDayApprox, Period{1000, 0, 0, 0, 0, 0}, false}, - {36525*oneDayApprox - time.Hour, Period{990, 110, 290, 230, 0, 0}, false}, + {3276*time.Hour + 59*time.Minute + 59*time.Second, Period{0, 0, 0, 32760, 590, 590}, true}, + // YMD tests: must be over 3276 hours (approx 4.5 months), otherwise HMS will take care of it + // first rollover: 3276 hours + {3288 * time.Hour, Period{0, 0, 1370, 0, 0, 0}, false}, + {3289 * time.Hour, Period{0, 0, 1370, 10, 0, 0}, false}, + {3277 * time.Hour, Period{0, 0, 1360, 130, 0, 0}, false}, + + // second rollover: 3276 days + {3277 * oneDay, Period{80, 110, 200, 0, 0, 0}, false}, + {3277*oneDay + time.Hour + time.Minute + time.Second, Period{80, 110, 200, 10, 0, 0}, false}, + {36525 * oneDay, Period{1000, 0, 0, 0, 0, 0}, false}, + + // negative cases too {-100 * time.Millisecond, Period{0, 0, 0, 0, 0, -1}, true}, {-time.Second, Period{0, 0, 0, 0, 0, -10}, true}, {-time.Minute, Period{0, 0, 0, 0, -10, 0}, true}, {-time.Hour, Period{0, 0, 0, -10, 0, 0}, true}, {-time.Hour - time.Minute - time.Second, Period{0, 0, 0, -10, -10, -10}, true}, - {-300 * oneDayApprox, Period{0, -90, -260, 0, 0, 0}, false}, - {-305 * oneDayApprox, Period{0, -100, 0, 0, 0, 0}, false}, - {-36525 * oneDayApprox, Period{-1000, 0, 0, 0, 0, 0}, false}, + {-oneDay, Period{0, 0, 0, -240, 0, 0}, true}, + {-305 * oneDay, Period{0, 0, -3050, 0, 0, 0}, false}, + {-36525 * oneDay, Period{-1000, 0, 0, 0, 0, 0}, false}, } + for i, c := range cases { n, p := NewOf(c.source) + rev, _ := c.expected.Duration() if n != c.expected { - t.Errorf("%d: NewOf(%v) gives %v %#v, want %v", i, c.source, n, n, c.expected) + t.Errorf("%d: NewOf(%s) (%dms)\n gives %-20s %#v,\n want %-20s (%dms)", i, c.source, c.source/ms, n, n, c.expected, rev/ms) } if p != c.precise { - t.Errorf("%d: NewOf(%v) gives %v, want %v for %v", i, c.source, p, c.precise, c.expected) + t.Errorf("%d: NewOf(%s) (%dms)\n gives %v,\n want %v for %v (%dms)", i, c.source, c.source/ms, p, c.precise, c.expected, rev/ms) } + //if rev != c.source { + // t.Logf("%d: NewOf(%s) input %dms differs from expected %dms", i, c.source, c.source/ms, rev/ms) + //} } } From 7269504ed7ad3cd4d9bb617e54efb2282b326021 Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Wed, 25 Jul 2018 23:05:43 +0100 Subject: [PATCH 090/165] Further work done on Between and DaysBetween --- period/period_test.go | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/period/period_test.go b/period/period_test.go index 9b68952c..0188d95e 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -445,7 +445,6 @@ func TestNewOf(t *testing.T) { } func TestBetween(t *testing.T) { - //halfSec := int(500 * time.Millisecond) now := time.Now() cases := []struct { @@ -474,9 +473,9 @@ func TestBetween(t *testing.T) { //{utc(2016, 5, 2, 0, 0, 0, 0), utc(2016, 6, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, //{utc(2016, 6, 2, 0, 0, 0, 0), utc(2016, 7, 1, 0, 0, 0, 0), Period{0, 0, 290, 0, 0, 0}}, - //// daytime only - //{utc(2015, 1, 1, 2, 3, 4, 0), utc(2015, 1, 1, 4, 4, 7, halfSec), Period{0, 0, 0, 20, 10, 35}}, - //{utc(2015, 1, 1, 2, 3, 4, halfSec), utc(2015, 1, 1, 4, 4, 7, 0), Period{0, 0, 0, 20, 10, 25}}, + // daytime only + {utc(2015, 1, 1, 2, 3, 4, 0), utc(2015, 1, 1, 4, 4, 7, 500), Period{0, 0, 0, 20, 10, 35}}, + {utc(2015, 1, 1, 2, 3, 4, 500), utc(2015, 1, 1, 4, 4, 7, 0), Period{0, 0, 0, 20, 10, 25}}, // different dates and times //{utc(2015, 2, 1, 0, 0, 0, 0), utc(2015, 4, 30, 5, 6, 7, 0), Period{0, 10, 260, 50, 60, 70}}, @@ -495,7 +494,6 @@ func TestBetween(t *testing.T) { } func TestDaysBetween(t *testing.T) { - //halfSec := int(500 * time.Millisecond) now := time.Now() cases := []struct { @@ -504,7 +502,7 @@ func TestDaysBetween(t *testing.T) { }{ {now, now, Period{0, 0, 0, 0, 0, 0}}, - //// simple positive date calculations + // simple positive date calculations {utc(2015, 1, 1, 0, 0, 0, 0), utc(2015, 2, 2, 1, 1, 1, 1), Period{0, 0, 320, 10, 10, 10}}, {utc(2015, 2, 1, 0, 0, 0, 0), utc(2015, 3, 2, 1, 1, 1, 1), Period{0, 0, 290, 10, 10, 10}}, {utc(2015, 3, 1, 0, 0, 0, 0), utc(2015, 4, 2, 1, 1, 1, 1), Period{0, 0, 320, 10, 10, 10}}, @@ -520,26 +518,18 @@ func TestDaysBetween(t *testing.T) { {utc(2015, 6, 2, 0, 0, 0, 0), utc(2015, 5, 1, 0, 0, 0, 0), Period{0, 0, -320, 0, 0, 0}}, {utc(2015, 6, 2, 1, 1, 1, 1), utc(2015, 5, 1, 0, 0, 0, 0), Period{0, 0, -320, -10, -10, -10}}, - //// less than one month - //{utc(2016, 1, 2, 0, 0, 0, 0), utc(2016, 2, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, - //{utc(2015, 2, 2, 0, 0, 0, 0), utc(2015, 3, 1, 0, 0, 0, 0), Period{0, 0, 270, 0, 0, 0}}, // non-leap - //{utc(2016, 2, 2, 0, 0, 0, 0), utc(2016, 3, 1, 0, 0, 0, 0), Period{0, 0, 280, 0, 0, 0}}, // leap year - //{utc(2016, 3, 2, 0, 0, 0, 0), utc(2016, 4, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, - //{utc(2016, 4, 2, 0, 0, 0, 0), utc(2016, 5, 1, 0, 0, 0, 0), Period{0, 0, 290, 0, 0, 0}}, - //{utc(2016, 5, 2, 0, 0, 0, 0), utc(2016, 6, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, - //{utc(2016, 6, 2, 0, 0, 0, 0), utc(2016, 7, 1, 0, 0, 0, 0), Period{0, 0, 290, 0, 0, 0}}, - // - //// daytime only - //{utc(2015, 1, 1, 2, 3, 4, 0), utc(2015, 1, 1, 4, 4, 7, halfSec), Period{0, 0, 0, 20, 10, 35}}, - //{utc(2015, 1, 1, 2, 3, 4, halfSec), utc(2015, 1, 1, 4, 4, 7, 0), Period{0, 0, 0, 20, 10, 25}}, + // daytime only + {utc(2015, 1, 1, 2, 3, 4, 0), utc(2015, 1, 1, 2, 3, 4, 500), Period{0, 0, 0, 0, 0, 5}}, + {utc(2015, 1, 1, 2, 3, 4, 0), utc(2015, 1, 1, 4, 4, 7, 500), Period{0, 0, 0, 20, 10, 35}}, + {utc(2015, 1, 1, 2, 3, 4, 500), utc(2015, 1, 1, 4, 4, 7, 0), Period{0, 0, 0, 20, 10, 25}}, // different dates and times - //{utc(2015, 2, 1, 0, 0, 0, 0), utc(2015, 4, 30, 5, 6, 7, 0), Period{0, 10, 260, 50, 60, 70}}, - //{utc(2015, 2, 12, 0, 0, 0, 0), utc(2015, 4, 10, 5, 6, 7, 0), Period{0, 10, 260, 50, 60, 70}}, + {utc(2015, 2, 1, 1, 0, 0, 0), utc(2015, 5, 30, 5, 6, 7, 0), Period{0, 0, 1180, 40, 60, 70}}, + {utc(2015, 2, 1, 1, 0, 0, 0), bst(2015, 5, 30, 5, 6, 7, 0), Period{0, 0, 1180, 30, 60, 70}}, // earlier month in later year - //{utc(2015, 12, 22, 0, 0, 0, 0), utc(2016, 1, 10, 5, 6, 7, 0), Period{0, 0, 200, 50, 60, 70}}, - //{utc(2015, 2, 11, 5, 6, 7, halfSec), utc(2016, 1, 10, 0, 0, 0, 0), Period{0, 100, 290, 220, 570, 565}}, + {utc(2015, 12, 22, 0, 0, 0, 0), utc(2016, 1, 10, 5, 6, 7, 0), Period{0, 0, 190, 50, 60, 70}}, + //{utc(2015, 2, 11, 5, 6, 7, 500), utc(2016, 1, 10, 0, 0, 0, 0), Period{0, 0, 3320, 180, 530, 525}}, } for i, c := range cases { n := DaysBetween(c.a, c.b) From 0447a7ced045032bd9a3c2d3355a93de2a220557 Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Fri, 27 Jul 2018 23:05:02 +0100 Subject: [PATCH 091/165] Between method now works in a wide range of use-cases (DaysBetween method was dropped). Improvements to normalisation. --- period/period.go | 294 +++++++++++++++++++++--------------------- period/period_test.go | 101 +++++---------- 2 files changed, 180 insertions(+), 215 deletions(-) diff --git a/period/period.go b/period/period.go index c037c531..1aee1d72 100644 --- a/period/period.go +++ b/period/period.go @@ -7,12 +7,11 @@ package period import ( "fmt" "time" - "github.com/rickb777/date/gregorian" ) -const daysPerYearApproxE4 time.Duration = 3652425 // 365.2425 days by the Gregorian rule -const daysPerMonthApproxE4 time.Duration = 304375 // 30.4375 days per month -const daysPerMonthApproxE6 time.Duration = 30436875 // 30.436875 days per month +const daysPerYearE4 int64 = 3652425 // 365.2425 days by the Gregorian rule +const daysPerMonthE4 int64 = 304375 // 30.4375 days per month +const daysPerMonthE6 int64 = 30436875 // 30.436875 days per month const oneE4 = 10000 const oneE5 = 100000 @@ -74,14 +73,22 @@ func NewHMS(hours, minutes, seconds int) Period { func New(years, months, days, hours, minutes, seconds int) Period { if (years >= 0 && months >= 0 && days >= 0 && hours >= 0 && minutes >= 0 && seconds >= 0) || (years <= 0 && months <= 0 && days <= 0 && hours <= 0 && minutes <= 0 && seconds <= 0) { - return Period{int16(years) * 10, int16(months) * 10, int16(days) * 10, - int16(hours) * 10, int16(minutes) * 10, int16(seconds) * 10} + return Period{ + int16(years) * 10, int16(months) * 10, int16(days) * 10, + int16(hours) * 10, int16(minutes) * 10, int16(seconds) * 10, + } } panic(fmt.Sprintf("Periods must have homogeneous signs; got P%dY%dM%dDT%dH%dM%dS", years, months, days, hours, minutes, seconds)) } -// TODO normalise (precise) +func newE1(years, months, days, hours, minutes, seconds int64) Period { + return Period{ + int16(years), int16(months), int16(days), + int16(hours), int16(minutes), int16(seconds), + } +} + // TODO NewFloat // NewOf converts a time duration to a Period, and also indicates whether the conversion is precise. @@ -97,7 +104,7 @@ func NewOf(duration time.Duration) (p Period, precise bool) { sign10 := sign * 10 - totalHours := d / time.Hour + totalHours := int64(d / time.Hour) // check for 16-bit overflow - occurs near the 4.5 month mark if totalHours < 3277 { @@ -117,16 +124,20 @@ func NewOf(duration time.Duration) (p Period, precise bool) { } // TODO it is uncertain whether this is too imprecise and should be improved - years := (oneE4 * totalDays) / daysPerYearApproxE4 - months := ((oneE4 * totalDays) / daysPerMonthApproxE4) - (12 * years) + years := (oneE4 * totalDays) / daysPerYearE4 + months := ((oneE4 * totalDays) / daysPerMonthE4) - (12 * years) hours := totalHours - totalDays*24 - totalDays = ((totalDays * oneE4) - (daysPerMonthApproxE4 * months) - (daysPerYearApproxE4 * years)) / oneE4 + totalDays = ((totalDays * oneE4) - (daysPerMonthE4 * months) - (daysPerYearE4 * years)) / oneE4 return Period{sign10 * int16(years), sign10 * int16(months), sign10 * int16(totalDays), sign10 * int16(hours), 0, 0}, false } -// Between converts the span between two times to a period. Based on the Gregorian conversion algorithms -// of `time.Time`, the resultant period is precise. It is normalised based on the calendar: the whole -// number of years and months are calculated and the number of days obtained from what's left over. +// Between converts the span between two times to a period. Based on the Gregorian conversion +// algorithms of `time.Time`, the resultant period is precise. +// +// The result is not normalised; for time differences less than 3276 days, it will contain zero in the +// years and months fields but the number of days may be up to 3275; this reduces errors arising from +// the variable lengths of months. For larger time differences, greater than 3276 days, the months and +// years fields are used as well. // // Remember that the resultant period does not retain any knowledge of the calendar, so any subsequent // computations applied to the period can only be precise if they concern either the date (year, month, @@ -141,7 +152,8 @@ func Between(t1, t2 time.Time) (p Period) { t1, t2, sign = t2, t1, -1 } - year, month, day, hour, min, sec, hundredth := timeDiff(t1, t2) + year, month, day, hour, min, sec, hundredth := daysDiff(t1, t2) + if sign < 0 { p = New(-year, -month, -day, -hour, -min, -sec) p.seconds -= int16(hundredth) @@ -152,31 +164,18 @@ func Between(t1, t2 time.Time) (p Period) { return } -//func TimeDiff(t1, t2 time.Time) (year, month, day, hour, min, sec int) { -// if t1.Location() != t2.Location() { -// t2 = t2.In(t1.Location()) -// } -// if t1.After(t2) { -// t1, t2 = t2, t1 -// } -// return timeDiff(t1, t2) -//} - -func timeDiff(t1, t2 time.Time) (year, month, day, hour, min, sec, hundredth int) { - y1, m1, d1 := t1.Date() - y2, m2, d2 := t2.Date() +func daysDiff(t1, t2 time.Time) (year, month, day, hour, min, sec, hundredth int) { + duration := t2.Sub(t1) hh1, mm1, ss1 := t1.Clock() hh2, mm2, ss2 := t2.Clock() - year = int(y2 - y1) - month = int(m2 - m1) - day = int(d2 - d1) + day = int(duration / (24 * time.Hour)) + hour = int(hh2 - hh1) min = int(mm2 - mm1) sec = int(ss2 - ss1) - hundredth = (t2.Nanosecond() - t1.Nanosecond()) / int(100*time.Millisecond) - //fmt.Printf("A) %d %d %d, %d %d %d\n", year, month, day, hour, min, sec) + hundredth = (t2.Nanosecond() - t1.Nanosecond()) / 100000000 // Normalize negative values if sec < 0 { @@ -189,93 +188,18 @@ func timeDiff(t1, t2 time.Time) (year, month, day, hour, min, sec, hundredth int hour-- } - //delta := 0 if hour < 0 { hour += 24 - //delta = -1 + // no need to reduce day - it's calculated differently. } - if month < 0 { // second month is earlier in the year than the first month - //if month > 0 { - // end := gregorian.DaysIn(y1, m1) - d1 - // day = end + d2 + delta - // month-- - //} - //for month < -12 { - month += 12 - year-- - //} - - //} - //if day < 0 { - end := gregorian.DaysIn(y2, m2) - d2 - day = d1 + end - //} - } else { - if d1 > 1 { - - } - } - - fmt.Printf("B) %d %d %d, %d %d %d %d\n", year, month, day, hour, min, sec, hundredth) - return -} - -// DaysBetween converts the span between two times to a period. Based on the Gregorian conversion -// algorithms of `time.Time`, the resultant period is precise. It is not normalised; the result will -// contain zero in the years and months fields, but the number of days may be large. -// -// Remember that the resultant period does not retain any knowledge of the calendar, so any subsequent -// computations applied to the period can only be precise if they concern either the date (year, month, -// day) part, or the clock (hour, minute, second) part, but not both. -func DaysBetween(t1, t2 time.Time) (p Period) { - if t1.Location() != t2.Location() { - t2 = t2.In(t1.Location()) - } - - sign := 1 - if t2.Before(t1) { - t1, t2, sign = t2, t1, -1 - } - - day, hour, min, sec, hundredth := daysDiff(t1, t2) - if sign < 0 { - p = New(0, 0, -day, -hour, -min, -sec) - p.seconds -= int16(hundredth) - } else { - p = New(0, 0, day, hour, min, sec) - p.seconds += int16(hundredth) - } - return -} - -func daysDiff(t1, t2 time.Time) (day, hour, min, sec, hundredth int) { - duration := t2.Sub(t1) - - hh1, mm1, ss1 := t1.Clock() - hh2, mm2, ss2 := t2.Clock() - - //year = int(y2 - y1) - //month = int(m2 - m1) - day = int(duration / (24 * time.Hour)) - hour = int(hh2 - hh1) - min = int(mm2 - mm1) - sec = int(ss2 - ss1) - hundredth = (t2.Nanosecond() - t1.Nanosecond()) / 100000000 - //fmt.Printf("A) %d %d %d, %d %d %d\n", year, month, day, hour, min, sec) - - // Normalize negative values - if sec < 0 { - sec += 60 - min-- - } - if min < 0 { - min += 60 - hour-- - } - if hour < 0 { - hour += 24 - day-- + // test 16bit storage limit (with 1 fixed decimal place) + if day > 3276 { + y1, m1, d1 := t1.Date() + y2, m2, d2 := t2.Date() + year = y2 - y1 + month = int(m2 - m1) + day = d2 - d1 } fmt.Printf("B) %d, %d %d %d %d\n", day, hour, min, sec, hundredth) @@ -336,18 +260,21 @@ func (period Period) Add(that Period) Period { } // Scale a period by a multiplication factor. Obviously, this can both enlarge and shrink it, -// and change the sign if negative. +// and change the sign if negative. The result is normalised. +// // Bear in mind that the internal representation is limited by fixed-point arithmetic with one // decimal place; each field is only int16. func (period Period) Scale(factor float32) Period { - return Period{ - int16(float32(period.years) * factor), - int16(float32(period.months) * factor), - int16(float32(period.days) * factor), - int16(float32(period.hours) * factor), - int16(float32(period.minutes) * factor), - int16(float32(period.seconds) * factor), - } + + y := int64(float32(period.years) * factor) + m := int64(float32(period.months) * factor) + d := int64(float32(period.days) * factor) + hh := int64(float32(period.hours) * factor) + mm := int64(float32(period.minutes) * factor) + ss := int64(float32(period.seconds) * factor) + + y, m, d, hh, mm, ss, _ = normalise64(y, m, d, hh, mm, ss, true) + return newE1(y, m, d, hh, mm, ss) } // Sign returns +1 for positive periods and -1 for negative periods. @@ -462,7 +389,7 @@ func (period Period) SecondsFloat() float32 { // 1/12 of a that; days are all assumed to be 24 hours long. func (period Period) Duration() (time.Duration, bool) { // remember that the fields are all fixed-point 1E1 - tdE6 := totalDaysApproxE7(period) * 8640 + tdE6 := time.Duration(totalDaysApproxE7(period) * 8640) hhE3 := time.Duration(period.hours) * 360000 mmE3 := time.Duration(period.minutes) * 6000 ssE3 := time.Duration(period.seconds) * 100 @@ -471,11 +398,11 @@ func (period Period) Duration() (time.Duration, bool) { return tdE6*time.Microsecond + stE3*time.Millisecond, tdE6 == 0 } -func totalDaysApproxE7(period Period) time.Duration { +func totalDaysApproxE7(period Period) int64 { // remember that the fields are all fixed-point 1E1 - ydE6 := time.Duration(period.years) * (daysPerYearApproxE4 * 100) - mdE6 := time.Duration(period.months) * daysPerMonthApproxE6 - ddE6 := time.Duration(period.days) * oneE6 + ydE6 := int64(period.years) * (daysPerYearE4 * 100) + mdE6 := int64(period.months) * daysPerMonthE6 + ddE6 := int64(period.days) * oneE6 return ydE6 + mdE6 + ddE6 } @@ -492,8 +419,8 @@ func (period Period) TotalDaysApprox() int { // Whole multiples of 24 hours are also included in the calculation. func (period Period) TotalMonthsApprox() int { p := period.Normalise(false) - mE1 := time.Duration(p.years)*12 + time.Duration(p.months) - dE6 := time.Duration(p.days) * 100 / daysPerMonthApproxE6 + mE1 := int64(p.years)*12 + int64(p.months) + dE6 := int64(p.days) * 100 / daysPerMonthE6 return int((mE1 + dE6) / 10) } @@ -508,29 +435,100 @@ func (period Period) TotalMonthsApprox() int { // Multiples of 24 hours become days. // Multiples of 30.4 days become months. func (period Period) Normalise(precise bool) Period { + const limit = 32670 - (32670/24) + + // can we use a quicker algorithm with int16 arithmetic? + if precise && period.days == 0 && period.hours > -limit && period.hours < limit { + s := period.Sign() + ap := period.Abs() + + // remember that the fields are all fixed-point 1E1 + ap.minutes += (ap.seconds / 600) * 10 + ap.seconds = ap.seconds % 600 + + ap.hours += (ap.minutes / 600) * 10 + ap.minutes = ap.minutes % 600 + + // can't touch days because of precision issues + + ap.years += (ap.months / 120) * 10 + ap.months = ap.months % 120 + + if s < 0 { + return ap.Negate() + } + return ap + } + + // do things the no-nonsense way using int64 arithmetic + y, m, d, hh, mm, ss, _ := normalise64(int64(period.years), int64(period.months), int64(period.days), + int64(period.hours), int64(period.minutes), int64(period.seconds), precise) + return newE1(y, m, d, hh, mm, ss) +} + +func normalise64(yearsE1, monthsE1, daysE1, hoursE1, minutesE1, secondsE1 int64, precise bool) (y, m, d, hh, mm, ss int64, imprecise bool) { + // first sort out sign and absolute values + neg := false + + if yearsE1 < 0 { + yearsE1 = -yearsE1 + neg = true + } + + if monthsE1 < 0 { + monthsE1 = -monthsE1 + neg = true + } + + if daysE1 < 0 { + daysE1 = -daysE1 + neg = true + } + + if hoursE1 < 0 { + hoursE1 = -hoursE1 + neg = true + } + + if minutesE1 < 0 { + minutesE1 = -minutesE1 + neg = true + } + + if secondsE1 < 0 { + secondsE1 = -secondsE1 + neg = true + } + // remember that the fields are all fixed-point 1E1 - s := period.Sign() - p := period.Abs() + mm = minutesE1 + (secondsE1/600)*10 + ss = secondsE1 % 600 - p.minutes += (p.seconds / 600) * 10 - p.seconds = p.seconds % 600 + hh = hoursE1 + (mm/600)*10 + mm = mm % 600 - p.hours += (p.minutes / 600) * 10 - p.minutes = p.minutes % 600 + d = daysE1 + m = monthsE1 + y = yearsE1 - if !precise { - p.days += (p.hours / 240) * 10 - p.hours = p.hours % 240 + if !precise || hh > 32670-(32670/60)-(32670/3600) { + d += (hh / 240) * 10 + hh = hh % 240 + imprecise = true + } - p.months += (p.days / 304) * 10 - p.days = p.days % 304 + if !precise || d > 32760 { + dE6 := d * oneE6 + m += dE6 / daysPerMonthE6 + d = (dE6 % daysPerMonthE6) / oneE6 + imprecise = true } - p.years += (p.months / 120) * 10 - p.months = p.months % 120 + y = yearsE1 + (m/120)*10 + m = m % 120 - if s < 0 { - return p.Negate() + if neg { + return -y, -m, -d, -hh, -mm, -ss, imprecise } - return p + return } diff --git a/period/period_test.go b/period/period_test.go index 0188d95e..5ebc3440 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -69,13 +69,13 @@ func TestPeriodString(t *testing.T) { value string period Period }{ - //{"P0D", Period{}}, - //{"P3Y", Period{30, 0, 0, 0, 0, 0}}, - //{"-P3Y", Period{-30, 0, 0, 0, 0, 0}}, - //{"P6M", Period{0, 60, 0, 0, 0, 0}}, - //{"-P6M", Period{0, -60, 0, 0, 0, 0}}, - //{"P35D", Period{0, 0, 350, 0, 0, 0}}, - //{"-P35D", Period{0, 0, -350, 0, 0, 0}}, + {"P0D", Period{}}, + {"P3Y", Period{30, 0, 0, 0, 0, 0}}, + {"-P3Y", Period{-30, 0, 0, 0, 0, 0}}, + {"P6M", Period{0, 60, 0, 0, 0, 0}}, + {"-P6M", Period{0, -60, 0, 0, 0, 0}}, + {"P5W", Period{0, 0, 350, 0, 0, 0}}, + {"-P5W", Period{0, 0, -350, 0, 0, 0}}, {"P4W", Period{0, 0, 280, 0, 0, 0}}, {"-P4W", Period{0, 0, -280, 0, 0, 0}}, {"P4D", Period{0, 0, 40, 0, 0, 0}}, @@ -385,11 +385,6 @@ func TestNewYMD(t *testing.T) { } } -func durationOf(p Period) time.Duration { - d, _ := p.Duration() - return d -} - func TestNewOf(t *testing.T) { ms := time.Millisecond @@ -447,55 +442,6 @@ func TestNewOf(t *testing.T) { func TestBetween(t *testing.T) { now := time.Now() - cases := []struct { - a, b time.Time - expected Period - }{ - {now, now, Period{0, 0, 0, 0, 0, 0}}, - - //// simple positive date calculations - {utc(2015, 1, 1, 0, 0, 0, 0), utc(2016, 2, 2, 1, 1, 1, 1), Period{10, 10, 10, 10, 10, 10}}, - {utc(2015, 2, 1, 0, 0, 0, 0), utc(2016, 3, 2, 1, 1, 1, 1), Period{10, 10, 10, 10, 10, 10}}, - {utc(2015, 3, 1, 0, 0, 0, 0), utc(2016, 4, 2, 1, 1, 1, 1), Period{10, 10, 10, 10, 10, 10}}, - {utc(2015, 4, 1, 0, 0, 0, 0), utc(2016, 5, 2, 1, 1, 1, 1), Period{10, 10, 10, 10, 10, 10}}, - {utc(2015, 5, 1, 0, 0, 0, 0), utc(2016, 6, 2, 1, 1, 1, 1), Period{10, 10, 10, 10, 10, 10}}, - {utc(2015, 6, 1, 0, 0, 0, 0), utc(2016, 7, 2, 1, 1, 1, 1), Period{10, 10, 10, 10, 10, 10}}, - - // negative date calculation - {utc(2016, 6, 2, 1, 1, 1, 1), utc(2015, 5, 1, 0, 0, 0, 0), Period{-10, -10, -10, -10, -10, -10}}, - - // less than one month - //{utc(2016, 1, 2, 0, 0, 0, 0), utc(2016, 2, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, - //{utc(2015, 2, 2, 0, 0, 0, 0), utc(2015, 3, 1, 0, 0, 0, 0), Period{0, 0, 270, 0, 0, 0}}, // non-leap - //{utc(2016, 2, 2, 0, 0, 0, 0), utc(2016, 3, 1, 0, 0, 0, 0), Period{0, 0, 280, 0, 0, 0}}, // leap year - //{utc(2016, 3, 2, 0, 0, 0, 0), utc(2016, 4, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, - //{utc(2016, 4, 2, 0, 0, 0, 0), utc(2016, 5, 1, 0, 0, 0, 0), Period{0, 0, 290, 0, 0, 0}}, - //{utc(2016, 5, 2, 0, 0, 0, 0), utc(2016, 6, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, - //{utc(2016, 6, 2, 0, 0, 0, 0), utc(2016, 7, 1, 0, 0, 0, 0), Period{0, 0, 290, 0, 0, 0}}, - - // daytime only - {utc(2015, 1, 1, 2, 3, 4, 0), utc(2015, 1, 1, 4, 4, 7, 500), Period{0, 0, 0, 20, 10, 35}}, - {utc(2015, 1, 1, 2, 3, 4, 500), utc(2015, 1, 1, 4, 4, 7, 0), Period{0, 0, 0, 20, 10, 25}}, - - // different dates and times - //{utc(2015, 2, 1, 0, 0, 0, 0), utc(2015, 4, 30, 5, 6, 7, 0), Period{0, 10, 260, 50, 60, 70}}, - //{utc(2015, 2, 12, 0, 0, 0, 0), utc(2015, 4, 10, 5, 6, 7, 0), Period{0, 10, 260, 50, 60, 70}}, - - // earlier month in later year - //{utc(2015, 12, 22, 0, 0, 0, 0), utc(2016, 1, 10, 5, 6, 7, 0), Period{0, 0, 200, 50, 60, 70}}, - //{utc(2015, 2, 11, 5, 6, 7, halfSec), utc(2016, 1, 10, 0, 0, 0, 0), Period{0, 100, 290, 220, 570, 565}}, - } - for i, c := range cases { - n := Between(c.a, c.b) - if n != c.expected { - t.Errorf("%d: Between(%v, %v)\n gives %-20s %#v,\n want %-20s %#v", i, c.a, c.b, n, n, c.expected, c.expected) - } - } -} - -func TestDaysBetween(t *testing.T) { - now := time.Now() - cases := []struct { a, b time.Time expected Period @@ -503,6 +449,7 @@ func TestDaysBetween(t *testing.T) { {now, now, Period{0, 0, 0, 0, 0, 0}}, // simple positive date calculations + {utc(2015, 1, 1, 0, 0, 0, 0), utc(2015, 1, 1, 0, 0, 0, 100), Period{0, 0, 0, 0, 0, 1}}, {utc(2015, 1, 1, 0, 0, 0, 0), utc(2015, 2, 2, 1, 1, 1, 1), Period{0, 0, 320, 10, 10, 10}}, {utc(2015, 2, 1, 0, 0, 0, 0), utc(2015, 3, 2, 1, 1, 1, 1), Period{0, 0, 290, 10, 10, 10}}, {utc(2015, 3, 1, 0, 0, 0, 0), utc(2015, 4, 2, 1, 1, 1, 1), Period{0, 0, 320, 10, 10, 10}}, @@ -511,10 +458,20 @@ func TestDaysBetween(t *testing.T) { {utc(2015, 6, 1, 0, 0, 0, 0), utc(2015, 7, 2, 1, 1, 1, 1), Period{0, 0, 310, 10, 10, 10}}, {utc(2015, 1, 1, 0, 0, 0, 0), utc(2015, 7, 2, 1, 1, 1, 1), Period{0, 0, 1820, 10, 10, 10}}, + // less than one month + {utc(2016, 1, 2, 0, 0, 0, 0), utc(2016, 2, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, + {utc(2015, 2, 2, 0, 0, 0, 0), utc(2015, 3, 1, 0, 0, 0, 0), Period{0, 0, 270, 0, 0, 0}}, // non-leap + {utc(2016, 2, 2, 0, 0, 0, 0), utc(2016, 3, 1, 0, 0, 0, 0), Period{0, 0, 280, 0, 0, 0}}, // leap year + {utc(2016, 3, 2, 0, 0, 0, 0), utc(2016, 4, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, + {utc(2016, 4, 2, 0, 0, 0, 0), utc(2016, 5, 1, 0, 0, 0, 0), Period{0, 0, 290, 0, 0, 0}}, + {utc(2016, 5, 2, 0, 0, 0, 0), utc(2016, 6, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, + {utc(2016, 6, 2, 0, 0, 0, 0), utc(2016, 7, 1, 0, 0, 0, 0), Period{0, 0, 290, 0, 0, 0}}, + // BST drops an hour at the daylight-saving transition {utc(2015, 1, 1, 0, 0, 0, 0), bst(2015, 7, 2, 1, 1, 1, 1), Period{0, 0, 1820, 0, 10, 10}}, // negative date calculation + {utc(2015, 1, 1, 0, 0, 0, 100), utc(2015, 1, 1, 0, 0, 0, 0), Period{0, 0, 0, 0, 0, -1}}, {utc(2015, 6, 2, 0, 0, 0, 0), utc(2015, 5, 1, 0, 0, 0, 0), Period{0, 0, -320, 0, 0, 0}}, {utc(2015, 6, 2, 1, 1, 1, 1), utc(2015, 5, 1, 0, 0, 0, 0), Period{0, 0, -320, -10, -10, -10}}, @@ -529,10 +486,14 @@ func TestDaysBetween(t *testing.T) { // earlier month in later year {utc(2015, 12, 22, 0, 0, 0, 0), utc(2016, 1, 10, 5, 6, 7, 0), Period{0, 0, 190, 50, 60, 70}}, - //{utc(2015, 2, 11, 5, 6, 7, 500), utc(2016, 1, 10, 0, 0, 0, 0), Period{0, 0, 3320, 180, 530, 525}}, + {utc(2015, 2, 11, 5, 6, 7, 500), utc(2016, 1, 10, 0, 0, 0, 0), Period{0, 0, 3320, 180, 530, 525}}, + + // larger ranges + {utc(2009, 1, 1, 0, 0, 1, 0), utc(2016, 12, 31, 0, 0, 2, 0), Period{0, 0, 29210, 0, 0, 10}}, + {utc(2008, 1, 1, 0, 0, 1, 0), utc(2016, 12, 31, 0, 0, 2, 0), Period{80, 110, 300, 0, 0, 10}}, } for i, c := range cases { - n := DaysBetween(c.a, c.b) + n := Between(c.a, c.b) if n != c.expected { t.Errorf("%d: Between(%v, %v)\n gives %-20s %#v,\n want %-20s %#v", i, c.a, c.b, n, n, c.expected, c.expected) } @@ -554,7 +515,7 @@ func TestNormalise(t *testing.T) { // carry minutes to hours {Period{0, 0, 0, 0, 699, 0}, Period{0, 0, 0, 10, 99, 0}, true}, - //{Period{0, 0, 0, 0, -699, 0}, Period{0, 0, 0, -10, -99, 0}, true}, + {Period{0, 0, 0, 0, -699, 0}, Period{0, 0, 0, -10, -99, 0}, true}, // carry hours to days - two cases {Period{0, 0, 0, 249, 0, 0}, Period{0, 0, 0, 249, 0, 0}, true}, @@ -562,18 +523,18 @@ func TestNormalise(t *testing.T) { // carry days to months - two cases {Period{0, 0, 323, 0, 0, 0}, Period{0, 0, 323, 0, 0, 0}, true}, - {Period{0, 0, 323, 0, 0, 0}, Period{0, 10, 19, 0, 0, 0}, false}, + {Period{0, 0, 323, 0, 0, 0}, Period{0, 10, 18, 0, 0, 0}, false}, // carry months to years {Period{0, 129, 0, 0, 0, 0}, Period{10, 9, 0, 0, 0, 0}, true}, // full ripple - two cases {Period{0, 121, 305, 239, 591, 601}, Period{10, 1, 305, 249, 1, 1}, true}, - {Period{0, 119, 300, 239, 591, 601}, Period{10, 9, 6, 9, 1, 1}, false}, + {Period{0, 119, 300, 239, 591, 601}, Period{10, 9, 5, 9, 1, 1}, false}, // full ripple - negative cases {Period{0, -121, -305, -239, -591, -601}, Period{-10, -1, -305, -249, -1, -1}, true}, - {Period{0, -119, -300, -239, -591, -601}, Period{-10, -9, -6, -9, -1, -1}, false}, + {Period{0, -119, -300, -239, -591, -601}, Period{-10, -9, -5, -9, -1, -1}, false}, } for i, c := range cases { n := c.source.Normalise(c.precise) @@ -732,6 +693,12 @@ func TestPeriodScale(t *testing.T) { {"PT1S", 0.5, "PT0.5S"}, {"P1Y2M3DT4H5M6S", 2, "P2Y4M6DT8H10M12S"}, {"P2Y4M6DT8H10M12S", -0.5, "-P1Y2M3DT4H5M6S"}, + {"-P2Y4M6DT8H10M12S", 0.5, "-P1Y2M3DT4H5M6S"}, + {"-P2Y4M6DT8H10M12S", -0.5, "P1Y2M3DT4H5M6S"}, + {"PT1M", 60, "PT1H"}, + {"PT1S", 60, "PT1M"}, + {"PT1S", 36000, "PT10H"}, + {"P365.5D", 10, "P10Y2.5D"}, } for i, c := range cases { s := MustParse(c.one).Scale(c.m) From 33e1bef4631659772901322d5091e9bfbc32b1b2 Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Fri, 27 Jul 2018 23:37:10 +0100 Subject: [PATCH 092/165] Minor simplification & documentation; uncovered some shortcomings in scaling down and ripple-down normalisation. --- period/period.go | 27 ++++++++++++++++----------- period/period_test.go | 23 ++++++++++++++++++++--- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/period/period.go b/period/period.go index 1aee1d72..af79c44a 100644 --- a/period/period.go +++ b/period/period.go @@ -247,7 +247,10 @@ func (period Period) Negate() Period { return Period{-period.years, -period.months, -period.days, -period.hours, -period.minutes, -period.seconds} } -// Add adds two periods together. +// Add adds two periods together. Use this method along with Negate in order to subtract periods. +// +// The result is not normalised and may overflow arithmetically (to make this unlikely, use Normalise on +// the inputs before adding them). func (period Period) Add(that Period) Period { return Period{ period.years + that.years, @@ -264,6 +267,8 @@ func (period Period) Add(that Period) Period { // // Bear in mind that the internal representation is limited by fixed-point arithmetic with one // decimal place; each field is only int16. +// +// Known issue: scaling by a large reduction factor (i.e. much less than one) doesn't work properly. func (period Period) Scale(factor float32) Period { y := int64(float32(period.years) * factor) @@ -273,8 +278,7 @@ func (period Period) Scale(factor float32) Period { mm := int64(float32(period.minutes) * factor) ss := int64(float32(period.seconds) * factor) - y, m, d, hh, mm, ss, _ = normalise64(y, m, d, hh, mm, ss, true) - return newE1(y, m, d, hh, mm, ss) + return newE1(normalise64(y, m, d, hh, mm, ss, true)) } // Sign returns +1 for positive periods and -1 for negative periods. @@ -426,6 +430,10 @@ func (period Period) TotalMonthsApprox() int { // Normalise attempts to simplify the fields. It operates in either precise or imprecise mode. // +// Because the number of hours per day is imprecise (due to daylight savings etc), and because +// the number of days per month is variable in the Gregorian calendar, there is a reluctance +// to transfer time too or from the days element. To give control over this, there are two modes. +// // In precise mode: // Multiples of 60 seconds become minutes. // Multiples of 60 minutes become hours. @@ -433,7 +441,7 @@ func (period Period) TotalMonthsApprox() int { // // Additionally, in imprecise mode: // Multiples of 24 hours become days. -// Multiples of 30.4 days become months. +// Multiples of approx. 30.4 days become months. func (period Period) Normalise(precise bool) Period { const limit = 32670 - (32670/24) @@ -461,12 +469,11 @@ func (period Period) Normalise(precise bool) Period { } // do things the no-nonsense way using int64 arithmetic - y, m, d, hh, mm, ss, _ := normalise64(int64(period.years), int64(period.months), int64(period.days), - int64(period.hours), int64(period.minutes), int64(period.seconds), precise) - return newE1(y, m, d, hh, mm, ss) + return newE1(normalise64(int64(period.years), int64(period.months), int64(period.days), + int64(period.hours), int64(period.minutes), int64(period.seconds), precise)) } -func normalise64(yearsE1, monthsE1, daysE1, hoursE1, minutesE1, secondsE1 int64, precise bool) (y, m, d, hh, mm, ss int64, imprecise bool) { +func normalise64(yearsE1, monthsE1, daysE1, hoursE1, minutesE1, secondsE1 int64, precise bool) (y, m, d, hh, mm, ss int64) { // first sort out sign and absolute values neg := false @@ -514,21 +521,19 @@ func normalise64(yearsE1, monthsE1, daysE1, hoursE1, minutesE1, secondsE1 int64, if !precise || hh > 32670-(32670/60)-(32670/3600) { d += (hh / 240) * 10 hh = hh % 240 - imprecise = true } if !precise || d > 32760 { dE6 := d * oneE6 m += dE6 / daysPerMonthE6 d = (dE6 % daysPerMonthE6) / oneE6 - imprecise = true } y = yearsE1 + (m/120)*10 m = m % 120 if neg { - return -y, -m, -d, -hh, -mm, -ss, imprecise + return -y, -m, -d, -hh, -mm, -ss } return } diff --git a/period/period_test.go b/period/period_test.go index 5ebc3440..a01e0e62 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -528,13 +528,26 @@ func TestNormalise(t *testing.T) { // carry months to years {Period{0, 129, 0, 0, 0, 0}, Period{10, 9, 0, 0, 0, 0}, true}, - // full ripple - two cases + // full ripple up - two cases {Period{0, 121, 305, 239, 591, 601}, Period{10, 1, 305, 249, 1, 1}, true}, {Period{0, 119, 300, 239, 591, 601}, Period{10, 9, 5, 9, 1, 1}, false}, - // full ripple - negative cases + // full ripple up - negative cases {Period{0, -121, -305, -239, -591, -601}, Period{-10, -1, -305, -249, -1, -1}, true}, {Period{0, -119, -300, -239, -591, -601}, Period{-10, -9, -5, -9, -1, -1}, false}, + + //TODO fix ripple-down normalisation + //// carry minutes to seconds + //{Period{0, 0, 0, 0, 5, 0}, Period{0, 0, 0, 0, 0, 30}, true}, + //{Period{0, 0, 0, 0, -5, 0}, Period{0, 0, 0, 0, 0, -30}, true}, + // + //// carry hours to minutes + //{Period{0, 0, 0, 5, 0, 0}, Period{0, 0, 0, 0, 30, 0}, true}, + //{Period{0, 0, 0, -5, 0, 0}, Period{0, 0, 0, 0, -30, 0}, true}, + // + //// carry yesr to months + //{Period{5, 0, 0, 0, 0, 0}, Period{0, 60, 0, 0, 0, 0}, true}, + //{Period{-5, 0, 0, 0, 0, 0}, Period{0, -60, 0, 0, 0, 0}, true}, } for i, c := range cases { n := c.source.Normalise(c.precise) @@ -680,7 +693,9 @@ func TestPeriodScale(t *testing.T) { }{ {"P0D", 2, "P0D"}, {"P1D", 2, "P2D"}, + {"P1D", 365, "P365D"}, {"P1M", 2, "P2M"}, + {"P1M", 12, "P1Y"}, {"P1Y", 2, "P2Y"}, {"PT1H", 2, "PT2H"}, {"PT1M", 2, "PT2M"}, @@ -691,13 +706,15 @@ func TestPeriodScale(t *testing.T) { {"PT1H", 0.5, "PT0.5H"}, {"PT1M", 0.5, "PT0.5M"}, {"PT1S", 0.5, "PT0.5S"}, + //TODO large reductions don't work {"PT1H", 1/3600, "PT1S"}, {"P1Y2M3DT4H5M6S", 2, "P2Y4M6DT8H10M12S"}, {"P2Y4M6DT8H10M12S", -0.5, "-P1Y2M3DT4H5M6S"}, {"-P2Y4M6DT8H10M12S", 0.5, "-P1Y2M3DT4H5M6S"}, {"-P2Y4M6DT8H10M12S", -0.5, "P1Y2M3DT4H5M6S"}, {"PT1M", 60, "PT1H"}, {"PT1S", 60, "PT1M"}, - {"PT1S", 36000, "PT10H"}, + {"PT1S", 86400, "PT24H"}, + {"PT1S", 86400000, "P1000D"}, {"P365.5D", 10, "P10Y2.5D"}, } for i, c := range cases { From 3e38f129a43ec550cae6d8424e61d92556a2ed0e Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Sat, 28 Jul 2018 22:57:38 +0100 Subject: [PATCH 093/165] Period: fixed Scale issue - now using Duration to handle reductions. Big rewrite of period normalisation. --- period/period.go | 289 +++++++++++++++++++++++++++++++----------- period/period_test.go | 252 +++++++++++++++++++----------------- 2 files changed, 348 insertions(+), 193 deletions(-) diff --git a/period/period.go b/period/period.go index af79c44a..7815ba69 100644 --- a/period/period.go +++ b/period/period.go @@ -82,13 +82,6 @@ func New(years, months, days, hours, minutes, seconds int) Period { years, months, days, hours, minutes, seconds)) } -func newE1(years, months, days, hours, minutes, seconds int64) Period { - return Period{ - int16(years), int16(months), int16(days), - int16(hours), int16(minutes), int16(seconds), - } -} - // TODO NewFloat // NewOf converts a time duration to a Period, and also indicates whether the conversion is precise. @@ -271,6 +264,13 @@ func (period Period) Add(that Period) Period { // Known issue: scaling by a large reduction factor (i.e. much less than one) doesn't work properly. func (period Period) Scale(factor float32) Period { + if -0.5 < factor && factor < 0.5 { + d, pr1 := period.Duration() + mul := float64(d) * float64(factor) + p2, pr2 := NewOf(time.Duration(mul)) + return p2.Normalise(pr1 && pr2) + } + y := int64(float32(period.years) * factor) m := int64(float32(period.months) * factor) d := int64(float32(period.days) * factor) @@ -278,7 +278,7 @@ func (period Period) Scale(factor float32) Period { mm := int64(float32(period.minutes) * factor) ss := int64(float32(period.seconds) * factor) - return newE1(normalise64(y, m, d, hh, mm, ss, true)) + return (&period64{y, m, d, hh, mm, ss, false}).normalise64(true).toPeriod() } // Sign returns +1 for positive periods and -1 for negative periods. @@ -385,11 +385,23 @@ func (period Period) SecondsFloat() float32 { return float32(period.seconds) / 10 } +// DurationApprox converts a period to the equivalent duration in nanoseconds. +// When the period specifies hours, minutes and seconds only, the result is precise. +// however, when the period specifies years, months and days, it is impossible to be precise +// because the result may depend on knowing date and timezone information, so the duration +// is estimated on the basis of a year being 365.25 days and a month being +// 1/12 of a that; days are all assumed to be 24 hours long. +func (period Period) DurationApprox() time.Duration { + d, _ := period.Duration() + return d +} + // Duration converts a period to the equivalent duration in nanoseconds. // A flag is also returned that is true when the conversion was precise and false otherwise. -// When the period specifies years, months and days, it is impossible to be precise because -// the result would depend on knowing date and timezone information, so the duration is -// estimated on the basis of a year being 365.25 days and a month being +// When the period specifies hours, minutes and seconds only, the result is precise. +// however, when the period specifies years, months and days, it is impossible to be precise +// because the result may depend on knowing date and timezone information, so the duration +// is estimated on the basis of a year being 365.25 days and a month being // 1/12 of a that; days are all assumed to be 24 hours long. func (period Period) Duration() (time.Duration, bool) { // remember that the fields are all fixed-point 1E1 @@ -414,18 +426,21 @@ func totalDaysApproxE7(period Period) int64 { // a year is 365.25 days and a month is 1/12 of that. Whole multiples of 24 hours are also included // in the calculation. func (period Period) TotalDaysApprox() int { - tdE6 := totalDaysApproxE7(period.Normalise(false)) - return int(tdE6 / oneE7) + pn := period.Normalise(false) + tdE6 := totalDaysApproxE7(pn) + hE6 := (int64(pn.hours) * oneE6) / 24 + return int((tdE6 + hE6) / oneE7) } // TotalMonthsApprox gets the approximate total number of months in the period. The days component // is included by approximately assumes a year is 365.25 days and a month is 1/12 of that. // Whole multiples of 24 hours are also included in the calculation. func (period Period) TotalMonthsApprox() int { - p := period.Normalise(false) - mE1 := int64(p.years)*12 + int64(p.months) - dE6 := int64(p.days) * 100 / daysPerMonthE6 - return int((mE1 + dE6) / 10) + pn := period.Normalise(false) + mE1 := int64(pn.years)*12 + int64(pn.months) + hE1 := int64(pn.hours) / 24 + dE1 := ((int64(pn.days) + hE1) * oneE6) / daysPerMonthE6 + return int((mE1 + dE1) / 10) } // Normalise attempts to simplify the fields. It operates in either precise or imprecise mode. @@ -443,97 +458,219 @@ func (period Period) TotalMonthsApprox() int { // Multiples of 24 hours become days. // Multiples of approx. 30.4 days become months. func (period Period) Normalise(precise bool) Period { - const limit = 32670 - (32670/24) + const limit = 32670 - (32670 / 60) - // can we use a quicker algorithm with int16 arithmetic? - if precise && period.days == 0 && period.hours > -limit && period.hours < limit { - s := period.Sign() - ap := period.Abs() + // can we use a quicker algorithm for HHMMSS with int16 arithmetic? + if period.years == 0 && period.months == 0 && + (!precise || period.days == 0) && + period.hours > -limit && period.hours < limit { - // remember that the fields are all fixed-point 1E1 - ap.minutes += (ap.seconds / 600) * 10 - ap.seconds = ap.seconds % 600 + return period.normaliseHHMMSS(precise) + } + + // can we use a quicker algorithm for YYMM with int16 arithmetic? + if (period.years != 0 || period.months != 0) && //period.months%10 == 0 && + period.days == 0 && period.hours == 0 && period.minutes == 0 && period.seconds == 0 { - ap.hours += (ap.minutes / 600) * 10 - ap.minutes = ap.minutes % 600 + return period.normaliseYYMM() + } + + // do things the no-nonsense way using int64 arithmetic + return period.toPeriod64().normalise64(precise).toPeriod() +} + +func (period Period) normaliseHHMMSS(precise bool) Period { + s := period.Sign() + ap := period.Abs() + + // remember that the fields are all fixed-point 1E1 + ap.minutes += (ap.seconds / 600) * 10 + ap.seconds = ap.seconds % 600 - // can't touch days because of precision issues + ap.hours += (ap.minutes / 600) * 10 + ap.minutes = ap.minutes % 600 + // up to 36 hours stays as hours + if !precise && ap.hours > 360 { + ap.days += (ap.hours / 240) * 10 + ap.hours = ap.hours % 240 + } + + d10 := ap.days % 10 + if d10 != 0 && (ap.hours != 0 || ap.minutes != 0 || ap.seconds != 0) { + ap.hours += d10 * 24 + ap.days -= d10 + } + + hh10 := ap.hours % 10 + if hh10 != 0 { + ap.minutes += hh10 * 60 + ap.hours -= hh10 + } + + mm10 := ap.minutes % 10 + if mm10 != 0 { + ap.seconds += mm10 * 60 + ap.minutes -= mm10 + } + + if s < 0 { + return ap.Negate() + } + return ap +} + +func (period Period) normaliseYYMM() Period { + s := period.Sign() + ap := period.Abs() + + // remember that the fields are all fixed-point 1E1 + if ap.months > 129 { ap.years += (ap.months / 120) * 10 ap.months = ap.months % 120 + } - if s < 0 { - return ap.Negate() - } - return ap + y10 := ap.years % 10 + if y10 != 0 && (ap.years < 10 || ap.months != 0) { + ap.months += y10 * 12 + ap.years -= y10 } - // do things the no-nonsense way using int64 arithmetic - return newE1(normalise64(int64(period.years), int64(period.months), int64(period.days), - int64(period.hours), int64(period.minutes), int64(period.seconds), precise)) + if s < 0 { + return ap.Negate() + } + return ap } -func normalise64(yearsE1, monthsE1, daysE1, hoursE1, minutesE1, secondsE1 int64, precise bool) (y, m, d, hh, mm, ss int64) { - // first sort out sign and absolute values - neg := false +//------------------------------------------------------------------------------------------------- - if yearsE1 < 0 { - yearsE1 = -yearsE1 - neg = true +// used for stages in arithmetic +type period64 struct { + years, months, days, hours, minutes, seconds int64 + neg bool +} + +func (period Period) toPeriod64() *period64 { + return &period64{ + int64(period.years), int64(period.months), int64(period.days), + int64(period.hours), int64(period.minutes), int64(period.seconds), + false, } +} - if monthsE1 < 0 { - monthsE1 = -monthsE1 - neg = true +func (p *period64) toPeriod() Period { + if p.neg { + return Period{ + int16(-p.years), int16(-p.months), int16(-p.days), + int16(-p.hours), int16(-p.minutes), int16(-p.seconds), + } } - if daysE1 < 0 { - daysE1 = -daysE1 - neg = true + return Period{ + int16(p.years), int16(p.months), int16(p.days), + int16(p.hours), int16(p.minutes), int16(p.seconds), } +} + +func (p *period64) normalise64(precise bool) *period64 { + return p.abs().rippleUp(precise).moveFractionToRight() +} + +func (p *period64) abs() *period64 { + + if !p.neg { + if p.years < 0 { + p.years = -p.years + p.neg = true + } + + if p.months < 0 { + p.months = -p.months + p.neg = true + } + + if p.days < 0 { + p.days = -p.days + p.neg = true + } + + if p.hours < 0 { + p.hours = -p.hours + p.neg = true + } + + if p.minutes < 0 { + p.minutes = -p.minutes + p.neg = true + } - if hoursE1 < 0 { - hoursE1 = -hoursE1 - neg = true + if p.seconds < 0 { + p.seconds = -p.seconds + p.neg = true + } } + return p +} + +func (p *period64) rippleUp(precise bool) *period64 { + // remember that the fields are all fixed-point 1E1 + + p.minutes = p.minutes + (p.seconds/600)*10 + p.seconds = p.seconds % 600 + + p.hours = p.hours + (p.minutes/600)*10 + p.minutes = p.minutes % 600 - if minutesE1 < 0 { - minutesE1 = -minutesE1 - neg = true + if !precise || p.hours > 32670-(32670/60)-(32670/3600) { + p.days += (p.hours / 240) * 10 + p.hours = p.hours % 240 } - if secondsE1 < 0 { - secondsE1 = -secondsE1 - neg = true + if !precise || p.days > 32760 { + dE6 := p.days * oneE6 + p.months += dE6 / daysPerMonthE6 + p.days = (dE6 % daysPerMonthE6) / oneE6 } - // remember that the fields are all fixed-point 1E1 - mm = minutesE1 + (secondsE1/600)*10 - ss = secondsE1 % 600 + p.years = p.years + (p.months/120)*10 + p.months = p.months % 120 + + return p +} - hh = hoursE1 + (mm/600)*10 - mm = mm % 600 +// moveFractionToRight applies the rule that only the smallest field is permitted to have a decimal fraction. +func (p *period64) moveFractionToRight() *period64 { + // remember that the fields are all fixed-point 1E1 - d = daysE1 - m = monthsE1 - y = yearsE1 + y10 := p.years % 10 + if y10 != 0 && (p.months != 0 || p.days != 0 || p.hours != 0 || p.minutes != 0 || p.seconds != 0) { + p.months += y10 * 24 + p.years = (p.years / 10) * 10 + } - if !precise || hh > 32670-(32670/60)-(32670/3600) { - d += (hh / 240) * 10 - hh = hh % 240 + m10 := p.months % 10 + if m10 != 0 && (p.days != 0 || p.hours != 0 || p.minutes != 0 || p.seconds != 0) { + p.days += (m10 * daysPerMonthE6) / oneE6 + p.months = (p.months / 10) * 10 } - if !precise || d > 32760 { - dE6 := d * oneE6 - m += dE6 / daysPerMonthE6 - d = (dE6 % daysPerMonthE6) / oneE6 + d10 := p.days % 10 + if d10 != 0 && (p.hours != 0 || p.minutes != 0 || p.seconds != 0) { + p.hours += d10 * 24 + p.days = (p.days / 10) * 10 } - y = yearsE1 + (m/120)*10 - m = m % 120 + hh10 := p.hours % 10 + if hh10 != 0 && (p.minutes != 0 || p.seconds != 0) { + p.minutes += hh10 * 60 + p.hours = (p.hours / 10) * 10 + } - if neg { - return -y, -m, -d, -hh, -mm, -ss + mm10 := p.minutes % 10 + if mm10 != 0 && p.seconds != 0 { + p.seconds += mm10 * 60 + p.minutes = (p.minutes / 10) * 10 } - return + + return p } diff --git a/period/period_test.go b/period/period_test.go index a01e0e62..84d2f0df 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -270,7 +270,10 @@ func TestPeriodApproxMonths(t *testing.T) { {"P1D", 0}, {"P30D", 0}, {"P31D", 1}, + {"P60D", 1}, + {"P62D", 2}, {"P1M", 1}, + {"P12M", 12}, {"P2M31D", 3}, {"P1Y", 12}, {"P2Y3M", 27}, @@ -501,59 +504,69 @@ func TestBetween(t *testing.T) { } func TestNormalise(t *testing.T) { - cases := []struct { - source, expected Period - precise bool - }{ - // zero cases - {New(0, 0, 0, 0, 0, 0), New(0, 0, 0, 0, 0, 0), true}, - {New(0, 0, 0, 0, 0, 0), New(0, 0, 0, 0, 0, 0), false}, - - // carry seconds to minutes - {Period{0, 0, 0, 0, 0, 699}, Period{0, 0, 0, 0, 10, 99}, true}, - {Period{0, 0, 0, 0, 0, -699}, Period{0, 0, 0, 0, -10, -99}, true}, - - // carry minutes to hours - {Period{0, 0, 0, 0, 699, 0}, Period{0, 0, 0, 10, 99, 0}, true}, - {Period{0, 0, 0, 0, -699, 0}, Period{0, 0, 0, -10, -99, 0}, true}, - - // carry hours to days - two cases - {Period{0, 0, 0, 249, 0, 0}, Period{0, 0, 0, 249, 0, 0}, true}, - {Period{0, 0, 0, 249, 0, 0}, Period{0, 0, 10, 9, 0, 0}, false}, - - // carry days to months - two cases - {Period{0, 0, 323, 0, 0, 0}, Period{0, 0, 323, 0, 0, 0}, true}, - {Period{0, 0, 323, 0, 0, 0}, Period{0, 10, 18, 0, 0, 0}, false}, - - // carry months to years - {Period{0, 129, 0, 0, 0, 0}, Period{10, 9, 0, 0, 0, 0}, true}, - - // full ripple up - two cases - {Period{0, 121, 305, 239, 591, 601}, Period{10, 1, 305, 249, 1, 1}, true}, - {Period{0, 119, 300, 239, 591, 601}, Period{10, 9, 5, 9, 1, 1}, false}, - - // full ripple up - negative cases - {Period{0, -121, -305, -239, -591, -601}, Period{-10, -1, -305, -249, -1, -1}, true}, - {Period{0, -119, -300, -239, -591, -601}, Period{-10, -9, -5, -9, -1, -1}, false}, - - //TODO fix ripple-down normalisation - //// carry minutes to seconds - //{Period{0, 0, 0, 0, 5, 0}, Period{0, 0, 0, 0, 0, 30}, true}, - //{Period{0, 0, 0, 0, -5, 0}, Period{0, 0, 0, 0, 0, -30}, true}, - // - //// carry hours to minutes - //{Period{0, 0, 0, 5, 0, 0}, Period{0, 0, 0, 0, 30, 0}, true}, - //{Period{0, 0, 0, -5, 0, 0}, Period{0, 0, 0, 0, -30, 0}, true}, - // - //// carry yesr to months - //{Period{5, 0, 0, 0, 0, 0}, Period{0, 60, 0, 0, 0, 0}, true}, - //{Period{-5, 0, 0, 0, 0, 0}, Period{0, -60, 0, 0, 0, 0}, true}, + // zero cases + testNormalise(t, New(0, 0, 0, 0, 0, 0), New(0, 0, 0, 0, 0, 0), New(0, 0, 0, 0, 0, 0)) + + // carry seconds to minutes + testNormalise(t, Period{0, 0, 0, 0, 0, 699}, Period{0, 0, 0, 0, 10, 99}, Period{0, 0, 0, 0, 10, 99}) + + // carry minutes to seconds + testNormalise(t, Period{0, 0, 0, 0, 5, 0}, Period{0, 0, 0, 0, 0, 300}, Period{0, 0, 0, 0, 0, 300}) + testNormalise(t, Period{0, 0, 0, 0, 1, 0}, Period{0, 0, 0, 0, 0, 60}, Period{0, 0, 0, 0, 0, 60}) + testNormalise(t, Period{0, 0, 0, 0, 55, 0}, Period{0, 0, 0, 0, 50, 300}, Period{0, 0, 0, 0, 50, 300}) + + // carry minutes to hours + testNormalise(t, Period{0, 0, 0, 0, 699, 0}, Period{0, 0, 0, 10, 90, 540}, Period{0, 0, 0, 10, 90, 540}) + + // carry hours to minutes + testNormalise(t, Period{0, 0, 0, 5, 0, 0}, Period{0, 0, 0, 0, 300, 0}, Period{0, 0, 0, 0, 300, 0}) + + // carry hours to days + testNormalise(t, Period{0, 0, 0, 249, 0, 0}, Period{0, 0, 0, 240, 540, 0}, Period{0, 0, 0, 240, 540, 0}) + testNormalise(t, Period{0, 0, 0, 249, 0, 0}, Period{0, 0, 0, 240, 540, 0}, Period{0, 0, 0, 240, 540, 0}) + testNormalise(t, Period{0, 0, 0, 369, 0, 0}, Period{0, 0, 0, 360, 540, 0}, Period{0, 0, 10, 120, 540, 0}) + testNormalise(t, Period{0, 0, 0, 249, 0, 10}, Period{0, 0, 0, 240, 540, 10}, Period{0, 0, 0, 240, 540, 10}) + + // carry months to years + testNormalise(t, Period{0, 125, 0, 0, 0, 0}, Period{0, 125, 0, 0, 0, 0}, Period{0, 125, 0, 0, 0, 0}) + testNormalise(t, Period{0, 131, 0, 0, 0, 0}, Period{10, 11, 0, 0, 0, 0}, Period{10, 11, 0, 0, 0, 0}) + + // carry days to months + testNormalise(t, Period{0, 0, 323, 0, 0, 0}, Period{0, 0, 323, 0, 0, 0}, Period{0, 0, 323, 0, 0, 0}) + + // full ripple up - two cases + testNormalise(t, Period{0, 121, 305, 239, 591, 601}, Period{10, 0, 330, 360, 540, 61}, Period{10, 10, 40, 0, 540, 61}) + + // carry year to months + testNormalise(t, Period{5, 0, 0, 0, 0, 0}, Period{0, 60, 0, 0, 0, 0}, Period{0, 60, 0, 0, 0, 0}) +} + +func testNormalise(t *testing.T, source, precise, approx Period) { + t.Helper() + + testNormaliseBothSigns(t, source, precise, true) + testNormaliseBothSigns(t, source, approx, false) +} + +func testNormaliseBothSigns(t *testing.T, source, expected Period, precise bool) { + t.Helper() + + n1 := source.Normalise(precise) + if n1 != expected { + t.Errorf("%v.Normalise(%v) %s\n gives %-22s %#v %s,\n want %-22s %#v %s", + source, precise, source.DurationApprox(), + n1, n1, n1.DurationApprox(), + expected, expected, expected.DurationApprox()) } - for i, c := range cases { - n := c.source.Normalise(c.precise) - if n != c.expected { - t.Errorf("%3d: %v.Normalise(%v)\n gives %-20s %#v,\n want %-20s %#v", i, c.source, c.precise, n, n, c.expected, c.expected) - } + + sneg := source.Negate() + eneg := expected.Negate() + n2 := sneg.Normalise(precise) + if n2 != eneg { + t.Errorf("%v.Normalise(%v) %s\n gives %-22s %#v %s,\n want %-22s %#v %s", + sneg, precise, sneg.DurationApprox(), + n2, n2, n2.DurationApprox(), + eneg, eneg, eneg.DurationApprox()) } } @@ -595,6 +608,74 @@ func TestPeriodFormat(t *testing.T) { } } +func TestPeriodScale(t *testing.T) { + cases := []struct { + one string + m float32 + expect string + }{ + {"P0D", 2, "P0D"}, + {"P1D", 2, "P2D"}, + {"P1D", 0, "P0D"}, + {"P1D", 365, "P365D"}, + {"P1M", 2, "P2M"}, + {"P1M", 12, "P1Y"}, + //TODO {"P1Y3M", 1.0/15, "P1M"}, + {"P1Y", 2, "P2Y"}, + {"PT1H", 2, "PT2H"}, + {"PT1M", 2, "PT2M"}, + {"PT1S", 2, "PT2S"}, + {"P1D", 0.5, "P0.5D"}, + {"P1M", 0.5, "P0.5M"}, + {"P1Y", 0.5, "P0.5Y"}, + {"PT1H", 0.5, "PT0.5H"}, + {"PT1H", 0.1, "PT6M"}, + //TODO {"PT1H", 0.01, "PT36S"}, + {"PT1M", 0.5, "PT0.5M"}, + {"PT1S", 0.5, "PT0.5S"}, + {"PT1H", 1.0 / 3600, "PT1S"}, + {"P1Y2M3DT4H5M6S", 2, "P2Y4M6DT8H10M12S"}, + {"P2Y4M6DT8H10M12S", -0.5, "-P1Y2M3DT4H5M6S"}, + {"-P2Y4M6DT8H10M12S", 0.5, "-P1Y2M3DT4H5M6S"}, + {"-P2Y4M6DT8H10M12S", -0.5, "P1Y2M3DT4H5M6S"}, + {"PT1M", 60, "PT1H"}, + {"PT1S", 60, "PT1M"}, + {"PT1S", 86400, "PT24H"}, + {"PT1S", 86400000, "P1000D"}, + {"P365.5D", 10, "P10Y2.5D"}, + //{"P365.5D", 0.1, "P36DT12H"}, + } + for i, c := range cases { + s := MustParse(c.one).Scale(c.m) + if s != MustParse(c.expect) { + t.Errorf("%d: %s.Scale(%g) == %v, want %s", i, c.one, c.m, s, c.expect) + } + } +} + +func TestPeriodAdd(t *testing.T) { + cases := []struct { + one, two string + expect string + }{ + {"P0D", "P0D", "P0D"}, + {"P1D", "P1D", "P2D"}, + {"P1M", "P1M", "P2M"}, + {"P1Y", "P1Y", "P2Y"}, + {"PT1H", "PT1H", "PT2H"}, + {"PT1M", "PT1M", "PT2M"}, + {"PT1S", "PT1S", "PT2S"}, + {"P1Y2M3DT4H5M6S", "P6Y5M4DT3H2M1S", "P7Y7M7DT7H7M7S"}, + {"P7Y7M7DT7H7M7S", "-P7Y7M7DT7H7M7S", "P0D"}, + } + for i, c := range cases { + s := MustParse(c.one).Add(MustParse(c.two)) + if s != MustParse(c.expect) { + t.Errorf("%d: %s.Add(%s) == %v, want %s", i, c.one, c.two, s, c.expect) + } + } +} + func TestPeriodFormatWithoutWeeks(t *testing.T) { cases := []struct { period string @@ -630,7 +711,7 @@ func TestPeriodFormatWithoutWeeks(t *testing.T) { } } -func TestPeriodOnlyYMD(t *testing.T) { +func TestPeriodParseOnlyYMD(t *testing.T) { cases := []struct { one string expect string @@ -646,7 +727,7 @@ func TestPeriodOnlyYMD(t *testing.T) { } } -func TestPeriodOnlyHMS(t *testing.T) { +func TestPeriodParseOnlyHMS(t *testing.T) { cases := []struct { one string expect string @@ -662,69 +743,6 @@ func TestPeriodOnlyHMS(t *testing.T) { } } -func TestPeriodAdd(t *testing.T) { - cases := []struct { - one, two string - expect string - }{ - {"P0D", "P0D", "P0D"}, - {"P1D", "P1D", "P2D"}, - {"P1M", "P1M", "P2M"}, - {"P1Y", "P1Y", "P2Y"}, - {"PT1H", "PT1H", "PT2H"}, - {"PT1M", "PT1M", "PT2M"}, - {"PT1S", "PT1S", "PT2S"}, - {"P1Y2M3DT4H5M6S", "P6Y5M4DT3H2M1S", "P7Y7M7DT7H7M7S"}, - {"P7Y7M7DT7H7M7S", "-P7Y7M7DT7H7M7S", "P0D"}, - } - for i, c := range cases { - s := MustParse(c.one).Add(MustParse(c.two)) - if s != MustParse(c.expect) { - t.Errorf("%d: %s.Add(%s) == %v, want %s", i, c.one, c.two, s, c.expect) - } - } -} - -func TestPeriodScale(t *testing.T) { - cases := []struct { - one string - m float32 - expect string - }{ - {"P0D", 2, "P0D"}, - {"P1D", 2, "P2D"}, - {"P1D", 365, "P365D"}, - {"P1M", 2, "P2M"}, - {"P1M", 12, "P1Y"}, - {"P1Y", 2, "P2Y"}, - {"PT1H", 2, "PT2H"}, - {"PT1M", 2, "PT2M"}, - {"PT1S", 2, "PT2S"}, - {"P1D", 0.5, "P0.5D"}, - {"P1M", 0.5, "P0.5M"}, - {"P1Y", 0.5, "P0.5Y"}, - {"PT1H", 0.5, "PT0.5H"}, - {"PT1M", 0.5, "PT0.5M"}, - {"PT1S", 0.5, "PT0.5S"}, - //TODO large reductions don't work {"PT1H", 1/3600, "PT1S"}, - {"P1Y2M3DT4H5M6S", 2, "P2Y4M6DT8H10M12S"}, - {"P2Y4M6DT8H10M12S", -0.5, "-P1Y2M3DT4H5M6S"}, - {"-P2Y4M6DT8H10M12S", 0.5, "-P1Y2M3DT4H5M6S"}, - {"-P2Y4M6DT8H10M12S", -0.5, "P1Y2M3DT4H5M6S"}, - {"PT1M", 60, "PT1H"}, - {"PT1S", 60, "PT1M"}, - {"PT1S", 86400, "PT24H"}, - {"PT1S", 86400000, "P1000D"}, - {"P365.5D", 10, "P10Y2.5D"}, - } - for i, c := range cases { - s := MustParse(c.one).Scale(c.m) - if s != MustParse(c.expect) { - t.Errorf("%d: %s.Scale(%g) == %v, want %s", i, c.one, c.m, s, c.expect) - } - } -} - func utc(year int, month time.Month, day, hour, min, sec, msec int) time.Time { return time.Date(year, month, day, hour, min, sec, msec*int(time.Millisecond), time.UTC) } From 062bfeba0df465d693ceb6e1c2026ef6875567b0 Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Sat, 28 Jul 2018 23:08:03 +0100 Subject: [PATCH 094/165] Fixed bug: Period.IsNegative --- period/period.go | 7 +-- period/period_test.go | 113 ++++++++++++++++++++++++------------------ 2 files changed, 70 insertions(+), 50 deletions(-) diff --git a/period/period.go b/period/period.go index 7815ba69..9f8affeb 100644 --- a/period/period.go +++ b/period/period.go @@ -204,10 +204,11 @@ func (period Period) IsZero() bool { return period == Period{} } -// IsNegative returns true if any field is negative. By design, this implies that -// all the fields are negative. +// IsNegative returns true if any field is negative. By design, this also implies that +// all the fields are negative or zero. func (period Period) IsNegative() bool { - return period.years < 0 || period.months < 0 || period.days < 0 + return period.years < 0 || period.months < 0 || period.days < 0 || + period.hours < 0 || period.minutes < 0 || period.seconds < 0 } // OnlyYMD returns a new Period with only the year, month and day fields. The hour, diff --git a/period/period_test.go b/period/period_test.go index 84d2f0df..ca4c0c98 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -239,6 +239,34 @@ func TestPeriodToDuration(t *testing.T) { } } +func TestIsNegative(t *testing.T) { + cases := []struct { + value string + expected bool + }{ + {"P0D", false}, + {"PT1S", false}, + {"-PT1S", true}, + {"PT1M", false}, + {"-PT1M", true}, + {"PT1H", false}, + {"-PT1H", true}, + {"P1D", false}, + {"-P1D", true}, + {"P1M", false}, + {"-P1M", true}, + {"P1Y", false}, + {"-P1Y", true}, + } + for i, c := range cases { + p := MustParse(c.value) + got := p.IsNegative() + if got != c.expected { + t.Errorf("%d: %v.IsNegative() == %v, want %v", i, p, got, c.expected) + } + } +} + func TestPeriodApproxDays(t *testing.T) { cases := []struct { value string @@ -389,57 +417,48 @@ func TestNewYMD(t *testing.T) { } func TestNewOf(t *testing.T) { + // HMS tests + testNewOf(t, 100*time.Millisecond, Period{0, 0, 0, 0, 0, 1}, true) + testNewOf(t, time.Second, Period{0, 0, 0, 0, 0, 10}, true) + testNewOf(t, time.Minute, Period{0, 0, 0, 0, 10, 0}, true) + testNewOf(t, time.Hour, Period{0, 0, 0, 10, 0, 0}, true) + testNewOf(t, time.Hour+time.Minute+time.Second, Period{0, 0, 0, 10, 10, 10}, true) + testNewOf(t, 24*time.Hour+time.Minute+time.Second, Period{0, 0, 0, 240, 10, 10}, true) + testNewOf(t, 3276*time.Hour+59*time.Minute+59*time.Second, Period{0, 0, 0, 32760, 590, 590}, true) + + // YMD tests: must be over 3276 hours (approx 4.5 months), otherwise HMS will take care of it + // first rollover: 3276 hours + testNewOf(t, 3288*time.Hour, Period{0, 0, 1370, 0, 0, 0}, false) + testNewOf(t, 3289*time.Hour, Period{0, 0, 1370, 10, 0, 0}, false) + testNewOf(t, 3277*time.Hour, Period{0, 0, 1360, 130, 0, 0}, false) + + // second rollover: 3276 days + testNewOf(t, 3277*oneDay, Period{80, 110, 200, 0, 0, 0}, false) + testNewOf(t, 3277*oneDay+time.Hour+time.Minute+time.Second, Period{80, 110, 200, 10, 0, 0}, false) + testNewOf(t, 36525*oneDay, Period{1000, 0, 0, 0, 0, 0}, false) +} + +func testNewOf(t *testing.T, source time.Duration, expected Period, precise bool) { + t.Helper() + testNewOf1(t, source, expected, precise) + testNewOf1(t, -source, expected.Negate(), precise) +} + +func testNewOf1(t *testing.T, source time.Duration, expected Period, precise bool) { + t.Helper() ms := time.Millisecond - cases := []struct { - source time.Duration - expected Period - precise bool - }{ - // HMS tests - {100 * time.Millisecond, Period{0, 0, 0, 0, 0, 1}, true}, - {time.Second, Period{0, 0, 0, 0, 0, 10}, true}, - {time.Minute, Period{0, 0, 0, 0, 10, 0}, true}, - {time.Hour, Period{0, 0, 0, 10, 0, 0}, true}, - {time.Hour + time.Minute + time.Second, Period{0, 0, 0, 10, 10, 10}, true}, - {24*time.Hour + time.Minute + time.Second, Period{0, 0, 0, 240, 10, 10}, true}, - {3276*time.Hour + 59*time.Minute + 59*time.Second, Period{0, 0, 0, 32760, 590, 590}, true}, - - // YMD tests: must be over 3276 hours (approx 4.5 months), otherwise HMS will take care of it - // first rollover: 3276 hours - {3288 * time.Hour, Period{0, 0, 1370, 0, 0, 0}, false}, - {3289 * time.Hour, Period{0, 0, 1370, 10, 0, 0}, false}, - {3277 * time.Hour, Period{0, 0, 1360, 130, 0, 0}, false}, - - // second rollover: 3276 days - {3277 * oneDay, Period{80, 110, 200, 0, 0, 0}, false}, - {3277*oneDay + time.Hour + time.Minute + time.Second, Period{80, 110, 200, 10, 0, 0}, false}, - {36525 * oneDay, Period{1000, 0, 0, 0, 0, 0}, false}, - - // negative cases too - {-100 * time.Millisecond, Period{0, 0, 0, 0, 0, -1}, true}, - {-time.Second, Period{0, 0, 0, 0, 0, -10}, true}, - {-time.Minute, Period{0, 0, 0, 0, -10, 0}, true}, - {-time.Hour, Period{0, 0, 0, -10, 0, 0}, true}, - {-time.Hour - time.Minute - time.Second, Period{0, 0, 0, -10, -10, -10}, true}, - {-oneDay, Period{0, 0, 0, -240, 0, 0}, true}, - {-305 * oneDay, Period{0, 0, -3050, 0, 0, 0}, false}, - {-36525 * oneDay, Period{-1000, 0, 0, 0, 0, 0}, false}, + n, p := NewOf(source) + rev, _ := expected.Duration() + if n != expected { + t.Errorf("NewOf(%s) (%dms)\n gives %-20s %#v,\n want %-20s (%dms)", source, source/ms, n, n, expected, rev/ms) } - - for i, c := range cases { - n, p := NewOf(c.source) - rev, _ := c.expected.Duration() - if n != c.expected { - t.Errorf("%d: NewOf(%s) (%dms)\n gives %-20s %#v,\n want %-20s (%dms)", i, c.source, c.source/ms, n, n, c.expected, rev/ms) - } - if p != c.precise { - t.Errorf("%d: NewOf(%s) (%dms)\n gives %v,\n want %v for %v (%dms)", i, c.source, c.source/ms, p, c.precise, c.expected, rev/ms) - } - //if rev != c.source { - // t.Logf("%d: NewOf(%s) input %dms differs from expected %dms", i, c.source, c.source/ms, rev/ms) - //} + if p != precise { + t.Errorf("NewOf(%s) (%dms)\n gives %v,\n want %v for %v (%dms)", source, source/ms, p, precise, expected, rev/ms) } + //if rev != source { + // t.Logf("%d: NewOf(%s) input %dms differs from expected %dms", i, source, source/ms, rev/ms) + //} } func TestBetween(t *testing.T) { From 041306ec22dc8373db5cf1c53bc656100652ac02 Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Sun, 29 Jul 2018 22:55:30 +0100 Subject: [PATCH 095/165] Fixed bug: Period.Normalise edge cases --- period/period.go | 10 +++++----- period/period_test.go | 24 ++++++++++++++++++------ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/period/period.go b/period/period.go index 9f8affeb..edf057ae 100644 --- a/period/period.go +++ b/period/period.go @@ -13,10 +13,10 @@ const daysPerYearE4 int64 = 3652425 // 365.2425 days by the Gregorian rule const daysPerMonthE4 int64 = 304375 // 30.4375 days per month const daysPerMonthE6 int64 = 30436875 // 30.436875 days per month -const oneE4 = 10000 -const oneE5 = 100000 -const oneE6 = 1000000 -const oneE7 = 10000000 +const oneE4 int64 = 10000 +const oneE5 int64 = 100000 +const oneE6 int64 = 1000000 +const oneE7 int64 = 10000000 const hundredMs = 100 * time.Millisecond @@ -645,7 +645,7 @@ func (p *period64) moveFractionToRight() *period64 { y10 := p.years % 10 if y10 != 0 && (p.months != 0 || p.days != 0 || p.hours != 0 || p.minutes != 0 || p.seconds != 0) { - p.months += y10 * 24 + p.months += y10 * 12 p.years = (p.years / 10) * 10 } diff --git a/period/period_test.go b/period/period_test.go index ca4c0c98..e60841c2 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -229,12 +229,16 @@ func TestPeriodToDuration(t *testing.T) { } for i, c := range cases { p := MustParse(c.value) - s, prec := p.Duration() - if s != c.duration { - t.Errorf("%d: Duration() == %s %v, want %s for %s", i, s, prec, c.duration, c.value) + d1, prec := p.Duration() + if d1 != c.duration { + t.Errorf("%d: Duration() == %s %v, want %s for %s", i, d1, prec, c.duration, c.value) } if prec != c.precise { - t.Errorf("%d: Duration() == %s %v, want %v for %s", i, s, prec, c.precise, c.value) + t.Errorf("%d: Duration() == %s %v, want %v for %s", i, d1, prec, c.precise, c.value) + } + d2 := p.DurationApprox() + if c.precise && d2 != c.duration { + t.Errorf("%d: DurationApprox() == %s %v, want %s for %s", i, d2, prec, c.duration, c.value) } } } @@ -546,6 +550,9 @@ func TestNormalise(t *testing.T) { testNormalise(t, Period{0, 0, 0, 369, 0, 0}, Period{0, 0, 0, 360, 540, 0}, Period{0, 0, 10, 120, 540, 0}) testNormalise(t, Period{0, 0, 0, 249, 0, 10}, Period{0, 0, 0, 240, 540, 10}, Period{0, 0, 0, 240, 540, 10}) + // carry days to hours + testNormalise(t, Period{0, 0, 5, 30, 0, 0}, Period{0, 0, 0, 150, 00, 0}, Period{0, 0, 0, 150, 0, 0}) + // carry months to years testNormalise(t, Period{0, 125, 0, 0, 0, 0}, Period{0, 125, 0, 0, 0, 0}, Period{0, 125, 0, 0, 0, 0}) testNormalise(t, Period{0, 131, 0, 0, 0, 0}, Period{10, 11, 0, 0, 0, 0}, Period{10, 11, 0, 0, 0, 0}) @@ -553,11 +560,16 @@ func TestNormalise(t *testing.T) { // carry days to months testNormalise(t, Period{0, 0, 323, 0, 0, 0}, Period{0, 0, 323, 0, 0, 0}, Period{0, 0, 323, 0, 0, 0}) - // full ripple up - two cases + // carry months to days + testNormalise(t, Period{0, 5, 203, 0, 0, 0}, Period{0, 0, 355, 0, 0, 0}, Period{0, 10, 50, 0, 0, 0}) + + // full ripple up testNormalise(t, Period{0, 121, 305, 239, 591, 601}, Period{10, 0, 330, 360, 540, 61}, Period{10, 10, 40, 0, 540, 61}) - // carry year to months + // carry years to months testNormalise(t, Period{5, 0, 0, 0, 0, 0}, Period{0, 60, 0, 0, 0, 0}, Period{0, 60, 0, 0, 0, 0}) + testNormalise(t, Period{5, 25, 0, 0, 0, 0}, Period{0, 85, 0, 0, 0, 0}, Period{0, 85, 0, 0, 0, 0}) + testNormalise(t, Period{5, 20, 10, 0, 0, 0}, Period{0, 80, 10, 0, 0, 0}, Period{0, 80, 10, 0, 0, 0}) } func testNormalise(t *testing.T, source, precise, approx Period) { From eae054510823319ea4f4dcf08675a8319194be68 Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Thu, 23 Aug 2018 23:09:53 +0100 Subject: [PATCH 096/165] Clarified VDate.String zero-value case is now blank --- view/vdate.go | 3 +++ view/vdate_test.go | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/view/vdate.go b/view/vdate.go index d3911b49..fe308bfc 100644 --- a/view/vdate.go +++ b/view/vdate.go @@ -58,6 +58,9 @@ func (v VDate) IsOdd() bool { // String formats the date in basic ISO8601 format YYYY-MM-DD. func (v VDate) String() string { + if v.d.IsZero() { + return "" + } return v.d.String() } diff --git a/view/vdate_test.go b/view/vdate_test.go index eee1c90b..9b208f44 100644 --- a/view/vdate_test.go +++ b/view/vdate_test.go @@ -28,6 +28,23 @@ func TestBasicFormatting(t *testing.T) { is(t, d.Year(), "2016") } +func TestZeroFormatting(t *testing.T) { + d := NewVDate(date.Date{}) + is(t, d.String(), "") + is(t, d.Format(), "01/01/1970") + is(t, d.WithFormat(MDYFormat).Format(), "01/01/1970") + is(t, d.Mon(), "Thu") + is(t, d.Monday(), "Thursday") + is(t, d.Day2(), "1") + is(t, d.Day02(), "01") + is(t, d.Day2nd(), "1st") + is(t, d.Month1(), "1") + is(t, d.Month01(), "01") + is(t, d.Jan(), "Jan") + is(t, d.January(), "January") + is(t, d.Year(), "1970") +} + func TestDate(t *testing.T) { d := date.New(2016, 2, 7) vd := NewVDate(d) From 6bcbdd54477390eed1c8a1f8d39d5ea76edda1ee Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Sun, 2 Sep 2018 19:08:35 +0100 Subject: [PATCH 097/165] this package is now a go module --- Gopkg.lock | 15 --------------- Gopkg.toml | 5 ----- go.mod | 3 +++ go.sum | 2 ++ 4 files changed, 5 insertions(+), 20 deletions(-) delete mode 100644 Gopkg.lock delete mode 100644 Gopkg.toml create mode 100644 go.mod create mode 100644 go.sum diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index 384f2550..00000000 --- a/Gopkg.lock +++ /dev/null @@ -1,15 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - name = "github.com/rickb777/plural" - packages = ["."] - revision = "7589705ae1b0a218b5389c04b1505e0c8defbc1f" - version = "v1.2.0" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - inputs-digest = "1f971ef25906ec07445b5e0dbbb90e964612f80d60ae1ab8db48337a5fed448d" - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml deleted file mode 100644 index b13cc562..00000000 --- a/Gopkg.toml +++ /dev/null @@ -1,5 +0,0 @@ -# dep: see https://github.com/golang/dep - -[[constraint]] - name = "github.com/rickb777/plural" - version = "^1.2.0" diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..3e0bbeed --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/rickb777/date + +require github.com/rickb777/plural v1.2.0 diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..50b0d6f2 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/rickb777/plural v1.2.0 h1:5tvEc7UBCZ7l8h/2UeybSkt/uu1DQsZFOFdNevmUhlE= +github.com/rickb777/plural v1.2.0/go.mod h1:UdpyWFCGbo3mvK3f/PfZOAOrkjzJlYN/sD46XNWJ+Es= From ee30139c88e657c21e64e98ec536029f6fe1f50c Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Mon, 10 Sep 2018 13:32:32 +0100 Subject: [PATCH 098/165] Removed a rogue fmt.Printf --- period/period.go | 1 - 1 file changed, 1 deletion(-) diff --git a/period/period.go b/period/period.go index edf057ae..dd84a7e6 100644 --- a/period/period.go +++ b/period/period.go @@ -195,7 +195,6 @@ func daysDiff(t1, t2 time.Time) (year, month, day, hour, min, sec, hundredth int day = d2 - d1 } - fmt.Printf("B) %d, %d %d %d %d\n", day, hour, min, sec, hundredth) return } From f81515179829583c4535a52adda984ae07980507 Mon Sep 17 00:00:00 2001 From: avalchev94 Date: Tue, 18 Sep 2018 15:50:54 +0300 Subject: [PATCH 099/165] Added implementation for the following interfaces: encoding.BinaryMarshaler, sql.Scanner and driver.Valuer. The design of the implementations follow closely the Date's implementation. Furthermore, added tests for the new methods. --- clock/marshal.go | 43 ++++++++++++ clock/marshal_test.go | 149 ++++++++++++++++++++++++++++++++++++++++++ clock/sql.go | 41 ++++++++++++ clock/sql_test.go | 73 +++++++++++++++++++++ 4 files changed, 306 insertions(+) create mode 100644 clock/marshal.go create mode 100644 clock/marshal_test.go create mode 100644 clock/sql.go create mode 100644 clock/sql_test.go diff --git a/clock/marshal.go b/clock/marshal.go new file mode 100644 index 00000000..6121b71a --- /dev/null +++ b/clock/marshal.go @@ -0,0 +1,43 @@ +package clock + +import ( + "errors" +) + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (c Clock) MarshalBinary() ([]byte, error) { + enc := []byte{ + byte(c >> 24), + byte(c >> 16), + byte(c >> 8), + byte(c), + } + return enc, nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (c *Clock) UnmarshalBinary(data []byte) error { + if len(data) == 0 { + return errors.New("Clock.UnmarshalBinary: no data") + } + if len(data) != 4 { + return errors.New("Clock.UnmarshalBinary: invalid length") + } + + *c = Clock(data[3]) | Clock(data[2])<<8 | Clock(data[1])<<16 | Clock(data[0])<<24 + return nil +} + +// MarshalText implements the encoding.TextMarshaler interface. +func (c Clock) MarshalText() ([]byte, error) { + return []byte(c.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +func (c *Clock) UnmarshalText(data []byte) (err error) { + clock, err := Parse(string(data)) + if err == nil { + *c = clock + } + return err +} diff --git a/clock/marshal_test.go b/clock/marshal_test.go new file mode 100644 index 00000000..1a9fa655 --- /dev/null +++ b/clock/marshal_test.go @@ -0,0 +1,149 @@ +package clock + +import ( + "bytes" + "encoding/gob" + "encoding/json" + "strings" + "testing" +) + +func TestGobEncoding(t *testing.T) { + var b bytes.Buffer + encoder := gob.NewEncoder(&b) + decoder := gob.NewDecoder(&b) + cases := []Clock{ + New(-1, -1, -1, -1), + New(0, 0, 0, 0), + New(12, 40, 40, 80), + New(13, 55, 0, 20), + New(16, 20, 0, 0), + New(20, 60, 59, 59), + New(24, 0, 0, 0), + New(24, 0, 0, 1), + } + for _, c := range cases { + var clock Clock + err := encoder.Encode(&c) + if err != nil { + t.Errorf("Gob(%v) encode error %v", c, err) + } else { + err = decoder.Decode(&clock) + if err != nil { + t.Errorf("Gob(%v) decode error %v", c, err) + } else if clock != c { + t.Errorf("Gob(%v) decode got %v", c, clock) + } + } + } +} + +func TestJSONMarshalling(t *testing.T) { + cases := []struct { + value Clock + want string + }{ + {New(-1, -1, -1, -1), `"22:58:58.999"`}, + {New(0, 0, 0, 0), `"00:00:00.000"`}, + {New(12, 40, 40, 80), `"12:40:40.080"`}, + {New(13, 55, 0, 20), `"13:55:00.020"`}, + {New(16, 20, 0, 0), `"16:20:00.000"`}, + {New(20, 60, 59, 59), `"21:00:59.059"`}, + {New(24, 0, 0, 0), `"24:00:00.000"`}, + {New(24, 0, 0, 1), `"00:00:00.001"`}, + } + for _, c := range cases { + bb, err := json.Marshal(c.value) + if err != nil { + t.Errorf("JSON(%v) marshal error %v", c, err) + } else if string(bb) != c.want { + t.Errorf("JSON(%v) == %v, want %v", c.value, string(bb), c.want) + } + } +} + +func TestJSONUnmarshalling(t *testing.T) { + cases := []struct { + values []string + want Clock + }{ + {[]string{`"22:58:58.999"`, `"10:58:58.999pm"`}, New(-1, -1, -1, -1)}, + {[]string{`"00:00:00.000"`, `"00:00:00.000AM"`}, New(0, 0, 0, 0)}, + {[]string{`"12:40:40.080"`, `"12:40:40.080PM"`}, New(12, 40, 40, 80)}, + {[]string{`"13:55:00.020"`, `"01:55:00.020PM"`}, New(13, 55, 0, 20)}, + {[]string{`"16:20:00.000"`, `"04:20:00.000pm"`}, New(16, 20, 0, 0)}, + {[]string{`"21:00:59.059"`, `"09:00:59.059PM"`}, New(20, 60, 59, 59)}, + {[]string{`"24:00:00.000"`, `"00:00:00.000am"`}, New(24, 0, 0, 0)}, + {[]string{`"00:00:00.001"`, `"00:00:00.001AM"`}, New(24, 0, 0, 1)}, + } + + for _, c := range cases { + for _, v := range c.values { + var clock Clock + err := json.Unmarshal([]byte(v), &clock) + if err != nil { + t.Errorf("JSON(%v) unmarshal error %v", v, err) + } else if c.want.Mod24() != clock.Mod24() { + t.Errorf("JSON(%v) == %v, want %v", v, clock, c.want) + } + } + } +} + +func TestBinaryMarshalling(t *testing.T) { + cases := []Clock{ + New(-1, -1, -1, -1), + New(0, 0, 0, 0), + New(12, 40, 40, 80), + New(13, 55, 0, 20), + New(16, 20, 0, 0), + New(20, 60, 59, 59), + New(24, 0, 0, 0), + New(24, 0, 0, 1), + } + for _, c := range cases { + bb, err := c.MarshalBinary() + if err != nil { + t.Errorf("Binary(%v) marshal error %v", c, err) + } else { + var clock Clock + err = clock.UnmarshalBinary(bb) + if err != nil { + t.Errorf("Binary(% v) unmarshal error %v", c, err) + } else if clock.Mod24() != c.Mod24() { + t.Errorf("Binary(%v) unmarshal got %v", c, clock) + } + } + } +} + +func TestBinaryUnmarshallingErrors(t *testing.T) { + var c Clock + err1 := c.UnmarshalBinary([]byte{}) + if err1 == nil { + t.Errorf("unmarshal no empty data error") + } + + err2 := c.UnmarshalBinary([]byte("12345")) + if err2 == nil { + t.Errorf("unmarshal no wrong length error") + } +} + +func TestInvalidClockText(t *testing.T) { + cases := []struct { + value string + want string + }{ + {`not-a-clock`, `clock.Clock: cannot parse not-a-clock`}, + {`00:50:100.0`, `clock.Clock: cannot parse 00:50:100.0`}, + {`24:00:00.0pM`, `clock.Clock: cannot parse 24:00:00.0pM: strconv.Atoi: parsing "0pM": invalid syntax`}, + } + for _, c := range cases { + var clock Clock + err := clock.UnmarshalText([]byte(c.value)) + if err == nil || !strings.Contains(err.Error(), c.want) { + t.Errorf("InvalidText(%v) == %v, want %v", c.value, err, c.want) + } + } +} diff --git a/clock/sql.go b/clock/sql.go new file mode 100644 index 00000000..87903c2e --- /dev/null +++ b/clock/sql.go @@ -0,0 +1,41 @@ +package clock + +import ( + "database/sql/driver" + "fmt" + "time" +) + +// Scan parses some value. It implements sql.Scanner, +// https://golang.org/pkg/database/sql/#Scanner +func (c *Clock) Scan(value interface{}) (err error) { + if value == nil { + return nil + } + + return c.scanAny(value) +} + +func (c *Clock) scanAny(value interface{}) (err error) { + err = nil + switch value.(type) { + case int64: + *c = Clock(value.(int64)) + case []byte: + *c, err = Parse(string(value.([]byte))) + case string: + *c, err = Parse(value.(string)) + case time.Time: + *c = NewAt(value.(time.Time)) + default: + err = fmt.Errorf("%T %+v is not a meaningful clock", value, value) + } + return +} + +// Value converts the value to an int64. It implements driver.Valuer, +// https://golang.org/pkg/database/sql/driver/#Valuer +func (c Clock) Value() (driver.Value, error) { + + return int64(c), nil +} diff --git a/clock/sql_test.go b/clock/sql_test.go new file mode 100644 index 00000000..88a27812 --- /dev/null +++ b/clock/sql_test.go @@ -0,0 +1,73 @@ +package clock + +import ( + "database/sql/driver" + "testing" + "time" +) + +func TestClockScan(t *testing.T) { + now := time.Now() + + cases := []struct { + v interface{} + expected Clock + }{ + {int64(New(-1, -1, -1, -1)), New(-1, -1, -1, -1)}, + {int64(New(10, 60, 10, 0)), New(10, 60, 10, 0)}, + {int64(New(24, 10, 0, 10)), New(0, 10, 0, 10)}, + {"12:00:00.400", New(12, 0, 0, 400)}, + {"01:40:50.000pm", New(13, 40, 50, 0)}, + {"4:20:00.000pm", New(16, 20, 0, 0)}, + {[]byte("23:60:60.000"), New(0, 1, 0, 0)}, + {now, NewAt(now)}, + } + + for i, c := range cases { + var clock Clock + e := clock.Scan(c.v) + if e != nil { + t.Errorf("%d: Got %v for %d", i, e, c.expected) + } else if clock.Mod24() != c.expected.Mod24() { + t.Errorf("%d: Got %v, want %d", i, clock, c.expected) + } + + var d driver.Valuer = clock + + q, e := d.Value() + if e != nil { + t.Errorf("%d: Got %v for %d", i, e, c.expected) + } else if Clock(q.(int64)).Mod24() != c.expected.Mod24() { + t.Errorf("%d: Got %v, want %d", i, q, c.expected) + } + } +} + +func TestClockScanWithJunk(t *testing.T) { + cases := []struct { + v interface{} + expected string + }{ + {true, "bool true is not a meaningful clock"}, + {false, "bool false is not a meaningful clock"}, + } + + for i, c := range cases { + var clock Clock + e := clock.Scan(c.v) + if e.Error() != c.expected { + t.Errorf("%d: Got %q, want %q", i, e.Error(), c.expected) + } + } +} + +func TestClockScanWithNil(t *testing.T) { + var r *Clock + e := r.Scan(nil) + if e != nil { + t.Errorf("Got %v", e) + } + if r != nil { + t.Errorf("Got %v", r) + } +} From 9f6145faf24f363bf0bf89f0a1a731cadfa6ca24 Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Thu, 20 Sep 2018 23:01:33 +0100 Subject: [PATCH 100/165] Improved error reporting in period.Parse. Added more documentation. --- doc.go | 6 ++++-- gregorian/doc.go | 6 ++++++ gregorian/util_test.go | 2 +- period/parse.go | 15 +++++++-------- period/period_test.go | 34 ++++++++++++++++++++++------------ view/vdate.go | 2 ++ view/vdate_test.go | 2 +- 7 files changed, 43 insertions(+), 24 deletions(-) create mode 100644 gregorian/doc.go diff --git a/doc.go b/doc.go index ba35b628..92e736ad 100644 --- a/doc.go +++ b/doc.go @@ -13,9 +13,11 @@ // // * `TimeSpan` which expresses a duration of time between two instants. // -// * `Period` which expresses a period corresponding to the ISO-8601 form. +// * `Period` which expresses a period corresponding to the ISO-8601 form (e.g. "PT30S"). // -// * `Clock` which expresses a wall-clock style hours-minutes-seconds. +// * `Clock` which expresses a wall-clock style hours-minutes-seconds with millisecond precision. +// +// * `View` which wraps `Date` for use in templates etc. // // Credits // diff --git a/gregorian/doc.go b/gregorian/doc.go new file mode 100644 index 00000000..435274ba --- /dev/null +++ b/gregorian/doc.go @@ -0,0 +1,6 @@ +// Copyright 2016 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package gregorian provides utility functions for the Gregorian calendar calculations. +package gregorian diff --git a/gregorian/util_test.go b/gregorian/util_test.go index 1890b728..680a118f 100644 --- a/gregorian/util_test.go +++ b/gregorian/util_test.go @@ -10,6 +10,7 @@ func TestIsLeap(t *testing.T) { year int expected bool }{ + {0, true}, // year zero is not defined under some conventions but is in ISO8601 {2000, true}, {2400, true}, {2001, false}, @@ -74,4 +75,3 @@ func TestDaysIn(t *testing.T) { } } } - diff --git a/period/parse.go b/period/parse.go index f513b03d..61c9a650 100644 --- a/period/parse.go +++ b/period/parse.go @@ -59,17 +59,17 @@ func Parse(period string) (Period, error) { result.hours, st = parseField(st, 'H') if st.err != nil { - return Period{}, st.err + return Period{}, fmt.Errorf("expected a number before the 'H' marker: %s", period) } result.minutes, st = parseField(st, 'M') if st.err != nil { - return Period{}, st.err + return Period{}, fmt.Errorf("expected a number before the 'M' marker: %s", period) } result.seconds, st = parseField(st, 'S') if st.err != nil { - return Period{}, st.err + return Period{}, fmt.Errorf("expected a number before the 'S' marker: %s", period) } st.pcopy = pcopy[:t] @@ -77,22 +77,22 @@ func Parse(period string) (Period, error) { result.years, st = parseField(st, 'Y') if st.err != nil { - return Period{}, st.err + return Period{}, fmt.Errorf("expected a number before the 'Y' marker: %s", period) } result.months, st = parseField(st, 'M') if st.err != nil { - return Period{}, st.err + return Period{}, fmt.Errorf("expected a number before the 'M' marker: %s", period) } weeks, st := parseField(st, 'W') if st.err != nil { - return Period{}, st.err + return Period{}, fmt.Errorf("expected a number before the 'W' marker: %s", period) } days, st := parseField(st, 'D') if st.err != nil { - return Period{}, st.err + return Period{}, fmt.Errorf("expected a number before the 'D' marker: %s", period) } result.days = weeks*7 + days @@ -148,6 +148,5 @@ func parseDecimalFixedPoint(s, original string) (int16, error) { } n, e := strconv.ParseInt(s, 10, 16) - //fmt.Printf("ParseInt(%s) = %d -- from %s in %s %d\n", s, n, was, original, dec) return int16(n), e } diff --git a/period/period_test.go b/period/period_test.go index e60841c2..e1cf7040 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -14,6 +14,27 @@ var oneDay = 24 * time.Hour var oneMonthApprox = 2629746 * time.Second // 30.436875 days var oneYearApprox = 31556952 * time.Second // 365.2425 days +func TestParseErrors(t *testing.T) { + cases := []struct { + value string + expected string + }{ + {"", "cannot parse a blank string as a period"}, + {"XY", "expected 'P' period mark at the start: XY"}, + {"PxY", "expected a number before the 'Y' marker: PxY"}, + {"PxW", "expected a number before the 'W' marker: PxW"}, + {"PxD", "expected a number before the 'D' marker: PxD"}, + {"PTxH", "expected a number before the 'H' marker: PTxH"}, + {"PTxS", "expected a number before the 'S' marker: PTxS"}, + } + for i, c := range cases { + _, err := Parse(c.value) + if err.Error() != c.expected { + t.Errorf("%d: Parse(%q) == %#v, want (%#v)", i, c.value, err.Error(), c.expected) + } + } +} + func TestParsePeriod(t *testing.T) { cases := []struct { value string @@ -48,18 +69,7 @@ func TestParsePeriod(t *testing.T) { for i, c := range cases { d := MustParse(c.value) if d != c.period { - t.Errorf("%d: MustParsePeriod(%v) == %#v, want (%#v)", i, c.value, d, c.period) - } - } - - badCases := []string{ - "13M", - "P", - } - for i, c := range badCases { - d, err := Parse(c) - if err == nil { - t.Errorf("%d: ParsePeriod(%v) == %v", i, c, d) + t.Errorf("%d: MustParse(%v) == %#v, want (%#v)", i, c.value, d, c.period) } } } diff --git a/view/vdate.go b/view/vdate.go index fe308bfc..96ff23d4 100644 --- a/view/vdate.go +++ b/view/vdate.go @@ -15,6 +15,8 @@ const ( DMYFormat = "02/01/2006" // MDYFormat is a typical American representation. MDYFormat = "01/02/2006" + // ISOFormat is ISO-8601 YYYY-MM-DD. + ISOFormat = "2006-02-01" // DefaultFormat is used by Format() unless a different format is set. DefaultFormat = DMYFormat ) diff --git a/view/vdate_test.go b/view/vdate_test.go index 9b208f44..7657dfbd 100644 --- a/view/vdate_test.go +++ b/view/vdate_test.go @@ -32,7 +32,7 @@ func TestZeroFormatting(t *testing.T) { d := NewVDate(date.Date{}) is(t, d.String(), "") is(t, d.Format(), "01/01/1970") - is(t, d.WithFormat(MDYFormat).Format(), "01/01/1970") + is(t, d.WithFormat(ISOFormat).Format(), "1970-01-01") is(t, d.Mon(), "Thu") is(t, d.Monday(), "Thursday") is(t, d.Day2(), "1") From 8571bfe5c81fbade73a615f9ba791c4a1f1b79a3 Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Thu, 20 Sep 2018 23:16:40 +0100 Subject: [PATCH 101/165] documentation --- README.md | 10 +++++----- doc.go | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3a917bf8..b2d92197 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,11 @@ and convenient for calendrical calculations and date parsing and formatting It also provides - * `DateRange` which expresses a period between two dates. - * `TimeSpan` which expresses a duration of time between two instants. - * `Period` which expresses a period corresponding to the ISO-8601 form (e.g. PT1H) - * `Clock` which expresses a wall-clock style hours-minutes-seconds. - * `VDate` which wraps a Date to make it easy to use in Go templates and similar view tiers. + * `clock.Clock` which expresses a wall-clock style hours-minutes-seconds with millisecond precision. + * `period.Period` which expresses a period corresponding to the ISO-8601 form (e.g. "PT30S"). + * `timespan.DateRange` which expresses a period between two dates. + * `timespan.TimeSpan` which expresses a duration of time between two instants. + * `view.VDate` which wraps `Date` for use in templates etc. See [package documentation](https://godoc.org/github.com/rickb777/date) for full documentation and examples. diff --git a/doc.go b/doc.go index 92e736ad..9cf72db0 100644 --- a/doc.go +++ b/doc.go @@ -9,15 +9,15 @@ // // Subpackages provide: // -// * `DateRange` which expresses a period between two dates. +// * `clock.Clock` which expresses a wall-clock style hours-minutes-seconds with millisecond precision. // -// * `TimeSpan` which expresses a duration of time between two instants. +// * `period.Period` which expresses a period corresponding to the ISO-8601 form (e.g. "PT30S"). // -// * `Period` which expresses a period corresponding to the ISO-8601 form (e.g. "PT30S"). +// * `timespan.DateRange` which expresses a period between two dates. // -// * `Clock` which expresses a wall-clock style hours-minutes-seconds with millisecond precision. +// * `timespan.TimeSpan` which expresses a duration of time between two instants. // -// * `View` which wraps `Date` for use in templates etc. +// * `view.VDate` which wraps `Date` for use in templates etc. // // Credits // From 9fcad742cdd1ad96f480853c6ca0cedbdd414ad0 Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Fri, 5 Oct 2018 23:34:57 +0100 Subject: [PATCH 102/165] comments & test cases --- clock/clock.go | 4 ++++ clock/clock_test.go | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/clock/clock.go b/clock/clock.go index 591563a1..a2083019 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -20,6 +20,10 @@ import ( // negative values. However, for such lengths of time, a fixed 24 hours per day // is assumed and a modulo operation Mod24 is provided to discard whole multiples of 24 hours. // +// Clock is a type of integer (actually int32), so values can be compared and sorted as +// per other integers. The constants Second, Minute, Hour and Day can be added and subtracted +// with obvious outcomes. +// // See https://en.wikipedia.org/wiki/ISO_8601#Times type Clock int32 diff --git a/clock/clock_test.go b/clock/clock_test.go index e9bdc675..0d629167 100644 --- a/clock/clock_test.go +++ b/clock/clock_test.go @@ -269,6 +269,8 @@ func TestClockParseGoods(t *testing.T) { {"01:00", New(1, 0, 0, 0)}, {"01:02", New(1, 2, 0, 0)}, {"23:59", New(23, 59, 0, 0)}, + {"0911", New(9, 11, 0, 0)}, + {"1024", New(10, 24, 0, 0)}, {"2359", New(23, 59, 0, 0)}, {"00:00:00", New(0, 0, 0, 0)}, {"00:00:01", New(0, 0, 1, 0)}, @@ -297,10 +299,14 @@ func TestClockParseGoods(t *testing.T) { {"1am", New(1, 0, 0, 0)}, {"1pm", New(13, 0, 0, 0)}, {"1:00am", New(1, 0, 0, 0)}, - {"1:00pm", New(13, 0, 0, 0)}, + {"1:23am", New(1, 23, 0, 0)}, + {"1:23pm", New(13, 23, 0, 0)}, + {"01:23pm", New(13, 23, 0, 0)}, {"1:00:00am", New(1, 0, 0, 0)}, {"1:02:03pm", New(13, 2, 3, 0)}, + {"01:02:03pm", New(13, 2, 3, 0)}, {"1:02:03.004pm", New(13, 2, 3, 4)}, + {"01:02:03.004pm", New(13, 2, 3, 4)}, {"1:20:30.04pm", New(13, 20, 30, 40)}, {"1:20:30.4pm", New(13, 20, 30, 400)}, {"1:20:30.pm", New(13, 20, 30, 0)}, From 93168f6141f541552d32309f0f1ec5f2ac9c4097 Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Thu, 15 Nov 2018 11:31:41 +0000 Subject: [PATCH 103/165] more comments --- gregorian/doc.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/gregorian/doc.go b/gregorian/doc.go index 435274ba..7c657b0f 100644 --- a/gregorian/doc.go +++ b/gregorian/doc.go @@ -3,4 +3,14 @@ // license that can be found in the LICENSE file. // Package gregorian provides utility functions for the Gregorian calendar calculations. +// The Gregorian calendar was officially introduced in October 1582 so, strictly speaking, +// it only applies after that date. Some countries did not switch to the Gregorian calendar +// for many years after (such as Great Britain in 1782). +// +// Extending the Gregorian calendar backwards to dates preceding its official introduction +// produces a proleptic calendar that should be used with some caution for historic dates +// because it can lead to confusion. +// +// See https://en.wikipedia.org/wiki/Gregorian_calendar +// https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar package gregorian From 5959d0b2f6a98ff42db7851d34dd4de17f84b32a Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Tue, 27 Nov 2018 13:07:25 +0000 Subject: [PATCH 104/165] Augmented Period with AddTo(time) method. Added Period.IsPositive and clarified that Sign returns 0 for zero periods. --- date.go | 5 +- date_test.go | 3 +- datetool/main.go | 5 +- period/format.go | 3 +- period/period.go | 67 ++++++++++++++++++++------- period/period_test.go | 95 ++++++++++++++++++++++++++++++-------- timespan/daterange.go | 3 +- timespan/daterange_test.go | 5 +- timespan/timespan.go | 5 +- timespan/timespan_test.go | 5 +- view/vdate_test.go | 3 +- 11 files changed, 149 insertions(+), 50 deletions(-) diff --git a/date.go b/date.go index 16fd00fb..0aa43db5 100644 --- a/date.go +++ b/date.go @@ -5,10 +5,11 @@ package date import ( - "github.com/rickb777/date/gregorian" - "github.com/rickb777/date/period" "math" "time" + + "github.com/rickb777/date/gregorian" + "github.com/rickb777/date/period" ) // PeriodOfDays describes a period of time measured in whole days. Negative values diff --git a/date_test.go b/date_test.go index bf64a0f8..b75e8575 100644 --- a/date_test.go +++ b/date_test.go @@ -5,10 +5,11 @@ package date import ( - "github.com/rickb777/date/period" "runtime/debug" "testing" "time" + + "github.com/rickb777/date/period" ) func same(d Date, t time.Time) bool { diff --git a/datetool/main.go b/datetool/main.go index 05ffdf5d..2ace4000 100644 --- a/datetool/main.go +++ b/datetool/main.go @@ -9,11 +9,12 @@ package main import ( "fmt" - "github.com/rickb777/date" - "github.com/rickb777/date/clock" "os" "strconv" "strings" + + "github.com/rickb777/date" + "github.com/rickb777/date/clock" ) func printPair(a string, b interface{}) { diff --git a/period/format.go b/period/format.go index 3bb7e98f..32cb6a83 100644 --- a/period/format.go +++ b/period/format.go @@ -7,8 +7,9 @@ package period import ( "bytes" "fmt" - "github.com/rickb777/plural" "strings" + + "github.com/rickb777/plural" ) // Format converts the period to human-readable form using the default localisation. diff --git a/period/period.go b/period/period.go index dd84a7e6..cb726aab 100644 --- a/period/period.go +++ b/period/period.go @@ -86,7 +86,7 @@ func New(years, months, days, hours, minutes, seconds int) Period { // NewOf converts a time duration to a Period, and also indicates whether the conversion is precise. // Any time duration that spans more than ± 3276 hours will be approximated by assuming that there -// are 24 hours per day, 30.4375 per month and 365.25 days per year. +// are 24 hours per day, 30.4375 per month and 365.2425 days per year. func NewOf(duration time.Duration) (p Period, precise bool) { var sign int16 = 1 d := duration @@ -203,13 +203,31 @@ func (period Period) IsZero() bool { return period == Period{} } +// IsPositive returns true if any field is greater than zero. By design, this also implies that +// all the other fields are greater than or equal to zero. +func (period Period) IsPositive() bool { + return period.years > 0 || period.months > 0 || period.days > 0 || + period.hours > 0 || period.minutes > 0 || period.seconds > 0 +} + // IsNegative returns true if any field is negative. By design, this also implies that -// all the fields are negative or zero. +// all the other fields are negative or zero. func (period Period) IsNegative() bool { return period.years < 0 || period.months < 0 || period.days < 0 || period.hours < 0 || period.minutes < 0 || period.seconds < 0 } +// Sign returns +1 for positive periods and -1 for negative periods. If the period is zero, it returns zero. +func (period Period) Sign() int { + if period.IsZero() { + return 0 + } + if period.IsNegative() { + return -1 + } + return 1 +} + // OnlyYMD returns a new Period with only the year, month and day fields. The hour, // minute and second fields are zeroed. func (period Period) OnlyYMD() Period { @@ -281,14 +299,6 @@ func (period Period) Scale(factor float32) Period { return (&period64{y, m, d, hh, mm, ss, false}).normalise64(true).toPeriod() } -// Sign returns +1 for positive periods and -1 for negative periods. -func (period Period) Sign() int { - if period.years < 0 || period.months < 0 || period.days < 0 || period.hours < 0 || period.minutes < 0 || period.seconds < 0 { - return -1 - } - return 1 -} - // Years gets the whole number of years in the period. // The result is the number of years and does not include any other field. func (period Period) Years() int { @@ -385,11 +395,28 @@ func (period Period) SecondsFloat() float32 { return float32(period.seconds) / 10 } +// AddTo adds the period to a time, returning the result. +// A flag is also returned that is true when the conversion was precise and false otherwise. +// +// When the period specifies hours, minutes and seconds only, the result is precise. +// Also, when the period specifies whole years, months and days (i.e. without fractions), the +// result is precise. However, when years, months or days contains fractions, the result +// is only an approximation (it assumes that all days are 24 hours and every year is 365.2425 days). +func (period Period) AddTo(t time.Time) (time.Time, bool) { + d, precise := period.Duration() + if !precise { + return t.Add(d), false + } + + stE3 := totalSecondsE3(period) + return t.AddDate(period.Years(), period.Months(), period.Days()).Add(stE3 * time.Millisecond), true +} + // DurationApprox converts a period to the equivalent duration in nanoseconds. // When the period specifies hours, minutes and seconds only, the result is precise. // however, when the period specifies years, months and days, it is impossible to be precise // because the result may depend on knowing date and timezone information, so the duration -// is estimated on the basis of a year being 365.25 days and a month being +// is estimated on the basis of a year being 365.2425 days and a month being // 1/12 of a that; days are all assumed to be 24 hours long. func (period Period) DurationApprox() time.Duration { d, _ := period.Duration() @@ -398,20 +425,26 @@ func (period Period) DurationApprox() time.Duration { // Duration converts a period to the equivalent duration in nanoseconds. // A flag is also returned that is true when the conversion was precise and false otherwise. +// // When the period specifies hours, minutes and seconds only, the result is precise. // however, when the period specifies years, months and days, it is impossible to be precise // because the result may depend on knowing date and timezone information, so the duration -// is estimated on the basis of a year being 365.25 days and a month being +// is estimated on the basis of a year being 365.2425 days and a month being // 1/12 of a that; days are all assumed to be 24 hours long. func (period Period) Duration() (time.Duration, bool) { // remember that the fields are all fixed-point 1E1 tdE6 := time.Duration(totalDaysApproxE7(period) * 8640) + stE3 := totalSecondsE3(period) + return tdE6*time.Microsecond + stE3*time.Millisecond, tdE6 == 0 +} + +func totalSecondsE3(period Period) time.Duration { + // remember that the fields are all fixed-point 1E1 + // and these are divided by 1E1 hhE3 := time.Duration(period.hours) * 360000 mmE3 := time.Duration(period.minutes) * 6000 ssE3 := time.Duration(period.seconds) * 100 - //fmt.Printf("y %d, m %d, d %d, hh %d, mm %d, ss %d\n", ydE6, mdE6, ddE6, hhE3, mmE3, ssE3) - stE3 := hhE3 + mmE3 + ssE3 - return tdE6*time.Microsecond + stE3*time.Millisecond, tdE6 == 0 + return hhE3 + mmE3 + ssE3 } func totalDaysApproxE7(period Period) int64 { @@ -423,7 +456,7 @@ func totalDaysApproxE7(period Period) int64 { } // TotalDaysApprox gets the approximate total number of days in the period. The approximation assumes -// a year is 365.25 days and a month is 1/12 of that. Whole multiples of 24 hours are also included +// a year is 365.2425 days and a month is 1/12 of that. Whole multiples of 24 hours are also included // in the calculation. func (period Period) TotalDaysApprox() int { pn := period.Normalise(false) @@ -433,7 +466,7 @@ func (period Period) TotalDaysApprox() int { } // TotalMonthsApprox gets the approximate total number of months in the period. The days component -// is included by approximately assumes a year is 365.25 days and a month is 1/12 of that. +// is included by approximation, assuming a year is 365.2425 days and a month is 1/12 of that. // Whole multiples of 24 hours are also included in the calculation. func (period Period) TotalMonthsApprox() int { pn := period.Normalise(false) diff --git a/period/period_test.go b/period/period_test.go index e1cf7040..d5a275f0 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -5,9 +5,10 @@ package period import ( - "github.com/rickb777/plural" "testing" "time" + + "github.com/rickb777/plural" ) var oneDay = 24 * time.Hour @@ -210,6 +211,54 @@ func TestPeriodFloatComponents(t *testing.T) { } } +func TestPeriodAddToTime(t *testing.T) { + const ms = 1000000 + const sec = 1000 * ms + const min = 60 * sec + const hr = 60 * min + + // A conveniently round number (14 July 2017 @ 2:40am UTC) + var t0 = time.Unix(1500000000, 0) + + cases := []struct { + value string + result time.Time + precise bool + }{ + {"P0D", t0, true}, + {"PT1S", t0.Add(sec), true}, + {"PT0.1S", t0.Add(100 * ms), true}, + {"-PT0.1S", t0.Add(-100 * ms), true}, + {"PT3276S", t0.Add(3276 * sec), true}, + {"PT1M", t0.Add(60 * sec), true}, + {"PT0.1M", t0.Add(6 * sec), true}, + {"PT3276M", t0.Add(3276 * min), true}, + {"PT1H", t0.Add(hr), true}, + {"PT0.1H", t0.Add(6 * min), true}, + {"PT3276H", t0.Add(3276 * hr), true}, + {"P1D", t0.Add(24 * hr), false}, + {"P0.1D", t0.Add(144 * min), false}, + {"P3276D", t0.Add(3276 * 24 * hr), false}, + {"P1M", t0.Add(oneMonthApprox), false}, + {"P0.1M", t0.Add(oneMonthApprox / 10), false}, + {"P3276M", t0.Add(3276 * oneMonthApprox), false}, + {"P1Y", t0.Add(oneYearApprox), false}, + {"-P1Y", t0.Add(-oneYearApprox), false}, + {"P3276Y", t0.Add(3276 * oneYearApprox), false}, // near the upper limit of range + {"-P3276Y", t0.Add(-3276 * oneYearApprox), false}, // near the lower limit of range + } + for i, c := range cases { + p := MustParse(c.value) + t1, prec := p.AddTo(t0) + if t1 != c.result { + t.Errorf("%d: AddTo(t) == %s %v, want %s for %s", i, t1, prec, c.result, c.value) + } + if prec != c.precise { + t.Errorf("%d: Duration() == %s %v, want %v for %s", i, t1, prec, c.precise, c.value) + } + } +} + func TestPeriodToDuration(t *testing.T) { cases := []struct { value string @@ -219,6 +268,7 @@ func TestPeriodToDuration(t *testing.T) { {"P0D", time.Duration(0), true}, {"PT1S", 1 * time.Second, true}, {"PT0.1S", 100 * time.Millisecond, true}, + {"-PT0.1S", -100 * time.Millisecond, true}, {"PT3276S", 3276 * time.Second, true}, {"PT1M", 60 * time.Second, true}, {"PT0.1M", 6 * time.Second, true}, @@ -253,30 +303,37 @@ func TestPeriodToDuration(t *testing.T) { } } -func TestIsNegative(t *testing.T) { +func TestSignPotisitveNegative(t *testing.T) { cases := []struct { value string - expected bool + positive bool + negative bool + sign int }{ - {"P0D", false}, - {"PT1S", false}, - {"-PT1S", true}, - {"PT1M", false}, - {"-PT1M", true}, - {"PT1H", false}, - {"-PT1H", true}, - {"P1D", false}, - {"-P1D", true}, - {"P1M", false}, - {"-P1M", true}, - {"P1Y", false}, - {"-P1Y", true}, + {"P0D", false, false, 0}, + {"PT1S", true, false, 1}, + {"-PT1S", false, true, -1}, + {"PT1M", true, false, 1}, + {"-PT1M", false, true, -1}, + {"PT1H", true, false, 1}, + {"-PT1H", false, true, -1}, + {"P1D", true, false, 1}, + {"-P1D", false, true, -1}, + {"P1M", true, false, 1}, + {"-P1M", false, true, -1}, + {"P1Y", true, false, 1}, + {"-P1Y", false, true, -1}, } for i, c := range cases { p := MustParse(c.value) - got := p.IsNegative() - if got != c.expected { - t.Errorf("%d: %v.IsNegative() == %v, want %v", i, p, got, c.expected) + if p.IsPositive() != c.positive { + t.Errorf("%d: %v.IsPositive() == %v, want %v", i, p, p.IsPositive(), c.positive) + } + if p.IsNegative() != c.negative { + t.Errorf("%d: %v.IsNegative() == %v, want %v", i, p, p.IsNegative(), c.negative) + } + if p.Sign() != c.sign { + t.Errorf("%d: %v.Sign() == %d, want %d", i, p, p.Sign(), c.sign) } } } diff --git a/timespan/daterange.go b/timespan/daterange.go index f7abf1a1..dae51aee 100644 --- a/timespan/daterange.go +++ b/timespan/daterange.go @@ -6,9 +6,10 @@ package timespan import ( "fmt" + "time" + "github.com/rickb777/date" "github.com/rickb777/date/period" - "time" ) const minusOneNano time.Duration = -1 diff --git a/timespan/daterange_test.go b/timespan/daterange_test.go index 7a4db30a..a7211db1 100644 --- a/timespan/daterange_test.go +++ b/timespan/daterange_test.go @@ -6,11 +6,12 @@ package timespan import ( "fmt" - . "github.com/rickb777/date" - "github.com/rickb777/date/period" "strings" "testing" "time" + + . "github.com/rickb777/date" + "github.com/rickb777/date/period" ) var d0320 = New(2015, time.March, 20) diff --git a/timespan/timespan.go b/timespan/timespan.go index 1da7b1fd..6b22a840 100644 --- a/timespan/timespan.go +++ b/timespan/timespan.go @@ -6,10 +6,11 @@ package timespan import ( "fmt" - "github.com/rickb777/date" - "github.com/rickb777/date/period" "strings" "time" + + "github.com/rickb777/date" + "github.com/rickb777/date/period" ) // TimestampFormat is a simple format for date & time, "2006-01-02 15:04:05". diff --git a/timespan/timespan_test.go b/timespan/timespan_test.go index 1060980f..a40056c9 100644 --- a/timespan/timespan_test.go +++ b/timespan/timespan_test.go @@ -5,9 +5,10 @@ package timespan import ( - "github.com/rickb777/date" "testing" "time" + + "github.com/rickb777/date" ) const zero time.Duration = 0 @@ -186,7 +187,7 @@ func TestTSMarshalText(t *testing.T) { exp string }{ {t0, time.Hour, "20150214T101314Z/PT1H"}, - {t1, 2*time.Hour, "20150627T101315Z/PT2H"}, + {t1, 2 * time.Hour, "20150627T101315Z/PT2H"}, {t0.In(berlin), time.Minute, "20150214T111314Z/PT1M"}, // UTC+1 {t1.In(berlin), time.Second, "20150627T121315Z/PT1S"}, // UTC+2 } diff --git a/view/vdate_test.go b/view/vdate_test.go index 7657dfbd..2eca7489 100644 --- a/view/vdate_test.go +++ b/view/vdate_test.go @@ -6,9 +6,10 @@ package view import ( "encoding/json" - "github.com/rickb777/date" "testing" "time" + + "github.com/rickb777/date" ) func TestBasicFormatting(t *testing.T) { From 75ffca56eb37e0f55737ff4509e0fea543d239da Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Tue, 27 Nov 2018 14:31:48 +0000 Subject: [PATCH 105/165] Bug fixed in Period.AddTo calculation: a more accurate solution is possible --- period/period.go | 16 +++++++++++----- period/period_test.go | 23 ++++++++++++++--------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/period/period.go b/period/period.go index cb726aab..cd4dd9ed 100644 --- a/period/period.go +++ b/period/period.go @@ -403,13 +403,19 @@ func (period Period) SecondsFloat() float32 { // result is precise. However, when years, months or days contains fractions, the result // is only an approximation (it assumes that all days are 24 hours and every year is 365.2425 days). func (period Period) AddTo(t time.Time) (time.Time, bool) { - d, precise := period.Duration() - if !precise { - return t.Add(d), false + wholeYears := (period.years % 10) == 0 + wholeMonths := (period.months % 10) == 0 + wholeDays := (period.days % 10) == 0 + + if wholeYears && wholeMonths && wholeDays { + // in this case, time.AddDate provides an exact solution + stE3 := totalSecondsE3(period) + t1 := t.AddDate(int(period.years/10), int(period.months/10), int(period.days/10)) + return t1.Add(stE3 * time.Millisecond), true } - stE3 := totalSecondsE3(period) - return t.AddDate(period.Years(), period.Months(), period.Days()).Add(stE3 * time.Millisecond), true + d, precise := period.Duration() + return t.Add(d), precise } // DurationApprox converts a period to the equivalent duration in nanoseconds. diff --git a/period/period_test.go b/period/period_test.go index d5a275f0..3051c817 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -225,6 +225,7 @@ func TestPeriodAddToTime(t *testing.T) { result time.Time precise bool }{ + // precise cases {"P0D", t0, true}, {"PT1S", t0.Add(sec), true}, {"PT0.1S", t0.Add(100 * ms), true}, @@ -236,16 +237,20 @@ func TestPeriodAddToTime(t *testing.T) { {"PT1H", t0.Add(hr), true}, {"PT0.1H", t0.Add(6 * min), true}, {"PT3276H", t0.Add(3276 * hr), true}, - {"P1D", t0.Add(24 * hr), false}, + {"P1D", t0.AddDate(0, 0, 1), true}, + {"P3276D", t0.AddDate(0, 0, 3276), true}, + {"P1M", t0.AddDate(0, 1, 0), true}, + {"P3276M", t0.AddDate(0, 3276, 0), true}, + {"P1Y", t0.AddDate(1, 0, 0), true}, + {"-P1Y", t0.AddDate(-1, 0, 0), true}, + {"P3276Y", t0.AddDate(3276, 0, 0), true}, // near the upper limit of range + {"-P3276Y", t0.AddDate(-3276, 0, 0), true}, // near the lower limit of range + // approximate cases {"P0.1D", t0.Add(144 * min), false}, - {"P3276D", t0.Add(3276 * 24 * hr), false}, - {"P1M", t0.Add(oneMonthApprox), false}, + {"-P0.1D", t0.Add(-144 * min), false}, {"P0.1M", t0.Add(oneMonthApprox / 10), false}, - {"P3276M", t0.Add(3276 * oneMonthApprox), false}, - {"P1Y", t0.Add(oneYearApprox), false}, - {"-P1Y", t0.Add(-oneYearApprox), false}, - {"P3276Y", t0.Add(3276 * oneYearApprox), false}, // near the upper limit of range - {"-P3276Y", t0.Add(-3276 * oneYearApprox), false}, // near the lower limit of range + {"P0.1Y", t0.Add(oneYearApprox / 10), false}, + {"-P0.1Y0.1M0.1D", t0.Add(-(oneYearApprox / 10) - (oneMonthApprox / 10) - (144 * min)), false}, } for i, c := range cases { p := MustParse(c.value) @@ -254,7 +259,7 @@ func TestPeriodAddToTime(t *testing.T) { t.Errorf("%d: AddTo(t) == %s %v, want %s for %s", i, t1, prec, c.result, c.value) } if prec != c.precise { - t.Errorf("%d: Duration() == %s %v, want %v for %s", i, t1, prec, c.precise, c.value) + t.Errorf("%d: AddTo(t) == %s %v, want %v for %s", i, t1, prec, c.precise, c.value) } } } From 5d32ee77f1453b18d99e34b451c406ec3db04019 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 20 Mar 2019 13:41:18 +0000 Subject: [PATCH 106/165] Much improved 'datetool' --- build+test.sh | 2 ++ datetool/main.go | 89 +++++++++++++++++++++++++++++++++--------------- go.mod | 5 ++- go.sum | 2 ++ 4 files changed, 69 insertions(+), 29 deletions(-) diff --git a/build+test.sh b/build+test.sh index 1bf8afce..0e600c97 100755 --- a/build+test.sh +++ b/build+test.sh @@ -18,3 +18,5 @@ for d in clock period timespan view; do go tool cover -func=$d.out [ -z "$COVERALLS_TOKEN" ] || goveralls -coverprofile=$d.out -service=travis-ci -repotoken $COVERALLS_TOKEN done + +go install ./datetool diff --git a/datetool/main.go b/datetool/main.go index 2ace4000..1cc75279 100644 --- a/datetool/main.go +++ b/datetool/main.go @@ -9,57 +9,90 @@ package main import ( "fmt" - "os" - "strconv" - "strings" - "github.com/rickb777/date" "github.com/rickb777/date/clock" + "golang.org/x/text/language" + "golang.org/x/text/message" + "os" + "strconv" ) -func printPair(a string, b interface{}) { - fmt.Printf("%-12s %12v\n", a, b) +func usage() { + fmt.Printf("Usage: %s [-t] number | date | time\n\n", os.Args[0]) + fmt.Printf(" -t: terse output\n") + fmt.Printf(" date: [+-]yyyy/mm/dd | yyyy.mm.dd | dd/mm/yyyy | dd.mm.yyyy\n") + fmt.Printf(" time: e.g. 11:15:20 | 2:45pm | 1:15:10.101\n") + os.Exit(0) } -func printOneDate(s string, d date.Date, err error) { - if err != nil { - printPair(s, err.Error()) +var titled = false +var terse = false +var success = false +var printer = message.NewPrinter(language.English) + +func sprintf(num interface{}) string { + if terse { + return fmt.Sprintf("%d", num) } else { - printPair(s, d.Sub(date.Date{})) + return printer.Sprintf("%d", num) } } -func printOneClock(s string, c clock.Clock, err error) { - if err != nil { - printPair(s, err.Error()) - } else { - printPair(s, int32(c)) +func title() { + if !terse && !titled { + titled = true + fmt.Printf("%-15s %-15s %-15s %s\n", "input", "number", "date", "clock") + fmt.Printf("%-15s %-15s %-15s %s\n", "-----", "------", "----", "-----") } } func printArg(arg string) { - d := date.Date{} + + i, err := strconv.ParseInt(arg, 10, 64) + if err == nil { + title() + d := date.NewOfDays(date.PeriodOfDays(i)) + c := clock.Clock(i) + fmt.Printf("%-15s %-15s %-15s %s\n", arg, sprintf(i), d, c) + return + } d, e1 := date.AutoParse(arg) if e1 == nil { - printPair(arg, d.Sub(date.Date{})) - } else if strings.Index(arg, ":") == 2 { - c, err := clock.Parse(arg) - printOneClock(arg, c, err) - } else { - i, err := strconv.Atoi(arg) - if err == nil { - d = d.Add(date.PeriodOfDays(i)) - fmt.Printf("%-12s %12s %s\n", arg, d, clock.Clock(i)) - } else { - printPair(arg, err) - } + title() + fmt.Printf("%-15s %-15s %s\n", arg, sprintf(d.DaysSinceEpoch()), d) + success = true + } + + c, err := clock.Parse(arg) + if err == nil { + title() + fmt.Printf("%-15s %-15s %-15s %s\n", arg, sprintf(c), "", c) + success = true } } func main() { argsWithoutProg := os.Args[1:] + if len(argsWithoutProg) == 0 { + usage() + } + + if len(argsWithoutProg) > 0 && argsWithoutProg[0] == "-t" { + terse = true + argsWithoutProg = argsWithoutProg[1:] + } + for _, arg := range argsWithoutProg { printArg(arg) } + + if !success { + usage() + } + + if titled { + fmt.Printf("\n# dates are counted using days since 1st Jan 1970\n") + fmt.Printf("# clock operates via milliseconds since midnight\n") + } } diff --git a/go.mod b/go.mod index 3e0bbeed..b2894f59 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,6 @@ module github.com/rickb777/date -require github.com/rickb777/plural v1.2.0 +require ( + github.com/rickb777/plural v1.2.0 + golang.org/x/text v0.3.0 +) diff --git a/go.sum b/go.sum index 50b0d6f2..1023909a 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ github.com/rickb777/plural v1.2.0 h1:5tvEc7UBCZ7l8h/2UeybSkt/uu1DQsZFOFdNevmUhlE= github.com/rickb777/plural v1.2.0/go.mod h1:UdpyWFCGbo3mvK3f/PfZOAOrkjzJlYN/sD46XNWJ+Es= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 338cbdfe6f5fb85f390a0895b9e93153642fa1f8 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 20 Mar 2019 13:47:25 +0000 Subject: [PATCH 107/165] correction --- datetool/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/datetool/main.go b/datetool/main.go index 1cc75279..dabb4251 100644 --- a/datetool/main.go +++ b/datetool/main.go @@ -54,6 +54,7 @@ func printArg(arg string) { d := date.NewOfDays(date.PeriodOfDays(i)) c := clock.Clock(i) fmt.Printf("%-15s %-15s %-15s %s\n", arg, sprintf(i), d, c) + success = true return } From 6d588f000b9b6867bc4faf1c566958efa31b29a2 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Tue, 2 Apr 2019 10:35:03 +0100 Subject: [PATCH 108/165] datetool now shows day-of-week name --- datetool/main.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/datetool/main.go b/datetool/main.go index dabb4251..d7b7af16 100644 --- a/datetool/main.go +++ b/datetool/main.go @@ -41,8 +41,8 @@ func sprintf(num interface{}) string { func title() { if !terse && !titled { titled = true - fmt.Printf("%-15s %-15s %-15s %s\n", "input", "number", "date", "clock") - fmt.Printf("%-15s %-15s %-15s %s\n", "-----", "------", "----", "-----") + fmt.Printf("%-15s %-15s %-15s %s\n", "input", "number", "clock", "date") + fmt.Printf("%-15s %-15s %-15s %s\n", "-----", "------", "-----", "----") } } @@ -53,7 +53,7 @@ func printArg(arg string) { title() d := date.NewOfDays(date.PeriodOfDays(i)) c := clock.Clock(i) - fmt.Printf("%-15s %-15s %-15s %s\n", arg, sprintf(i), d, c) + fmt.Printf("%-15s %-15s %-15s %-12s %s\n", arg, sprintf(i), c, d, d.Weekday()) success = true return } @@ -61,14 +61,14 @@ func printArg(arg string) { d, e1 := date.AutoParse(arg) if e1 == nil { title() - fmt.Printf("%-15s %-15s %s\n", arg, sprintf(d.DaysSinceEpoch()), d) + fmt.Printf("%-15s %-15s %15s %-12s %s\n", arg, sprintf(d.DaysSinceEpoch()), "", d, d.Weekday()) success = true } c, err := clock.Parse(arg) if err == nil { title() - fmt.Printf("%-15s %-15s %-15s %s\n", arg, sprintf(c), "", c) + fmt.Printf("%-15s %-15s %s\n", arg, sprintf(c), c) success = true } } @@ -93,7 +93,7 @@ func main() { } if titled { - fmt.Printf("\n# dates are counted using days since 1st Jan 1970\n") + fmt.Printf("\n# dates are counted using days since Thursday 1st Jan 1970\n") fmt.Printf("# clock operates via milliseconds since midnight\n") } } From 64df88010e4e5ad1f446fa61887800099522ee4e Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 24 Apr 2019 22:58:50 +0100 Subject: [PATCH 109/165] experimental ValueConverter implementation --- sql.go | 42 +++++++++++++++++++++++++++++++----------- sql_test.go | 13 ++++++++----- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/sql.go b/sql.go index 310795e7..811c3c5d 100644 --- a/sql.go +++ b/sql.go @@ -13,7 +13,8 @@ import ( // These methods allow Date and PeriodOfDays to be fields stored in an // SQL database by implementing the database/sql/driver interfaces. -// The underlying column type is simply an integer. +// The underlying column type can be an integer (period of days since the epoch), +// a string, or a DATE. // Scan parses some value. It implements sql.Scanner, // https://golang.org/pkg/database/sql/#Scanner @@ -29,24 +30,31 @@ func (d *Date) Scan(value interface{}) (err error) { } func (d *Date) scanAny(value interface{}) (err error) { - var n int64 err = nil - switch value.(type) { + switch v := value.(type) { case int64: - *d = Date{PeriodOfDays(value.(int64))} + *d = Date{PeriodOfDays(v)} case []byte: - n, err = strconv.ParseInt(string(value.([]byte)), 10, 64) - *d = Date{PeriodOfDays(n)} + return d.scanString(string(v)) case string: - n, err = strconv.ParseInt(value.(string), 10, 64) - *d = Date{PeriodOfDays(n)} + return d.scanString(v) case time.Time: - *d = NewAt(value.(time.Time)) + *d = NewAt(v) default: err = fmt.Errorf("%T %+v is not a meaningful date", value, value) } - return + return err +} + +func (d *Date) scanString(value string) (err error) { + n, err := strconv.ParseInt(value, 10, 64) + if err == nil { + *d = Date{PeriodOfDays(n)} + return nil + } + *d, err = AutoParse(value) + return err } func (d *Date) scanInt(value interface{}) (err error) { @@ -62,10 +70,22 @@ func (d *Date) scanInt(value interface{}) (err error) { // Value converts the value to an int64. It implements driver.Valuer, // https://golang.org/pkg/database/sql/driver/#Valuer -func (d Date) Value() (driver.Value, error) { +func (d Date) ConvertValue(v interface{}) (driver.Value, error) { + switch v.(type) { + case string, []byte: + return d.String(), nil + case time.Time: + return d.UTC(), nil + } return int64(d.day), nil } +// Value converts the value to an int64. It implements driver.Valuer, +// https://golang.org/pkg/database/sql/driver/#Valuer +//func (d Date) Value() (driver.Value, error) { +// return int64(d.day), nil +//} + // DisableTextStorage reduces the Scan method so that only integers are handled. // Normally, database types int64, []byte, string and time.Time are supported. // When set true, only int64 is supported; this mode allows optimisation of SQL diff --git a/sql_test.go b/sql_test.go index e62d944d..3cdbf62c 100644 --- a/sql_test.go +++ b/sql_test.go @@ -24,6 +24,8 @@ func TestDateScan(t *testing.T) { {"0", false, 0}, {"1000", false, 1000}, {"10000", false, 10000}, + {"2018-12-31", false, 17896}, + {"31/12/2018", false, 17896}, {[]byte("10000"), false, 10000}, {PeriodOfDays(10000).Date().Local(), false, 10000}, } @@ -41,15 +43,16 @@ func TestDateScan(t *testing.T) { t.Errorf("%d: Got %v, want %d", i, *r, c.expected) } - var d driver.Valuer = *r + var d driver.ValueConverter = *r + //var d driver.Valuer = *r - q, e := d.Value() + _, e = d.ConvertValue(c.v) if e != nil { t.Errorf("%d: Got %v for %d", i, e, c.expected) } - if q.(int64) != int64(c.expected) { - t.Errorf("%d: Got %v, want %d", i, q, c.expected) - } + //if q.(int64) != int64(c.expected) { + // t.Errorf("%d: Got %v, want %d", i, q, c.expected) + //} } DisableTextStorage = prior From 6062b863584867437cd0d65106ff8e50b3c63e87 Mon Sep 17 00:00:00 2001 From: Andrew LeFevre Date: Thu, 25 Apr 2019 12:57:48 -0400 Subject: [PATCH 110/165] Increased the bit size from 16 to 32 when parsing Period strings to allow for parsing larger integers --- period/parse.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/period/parse.go b/period/parse.go index 61c9a650..30a0ff2b 100644 --- a/period/parse.go +++ b/period/parse.go @@ -147,6 +147,6 @@ func parseDecimalFixedPoint(s, original string) (int16, error) { s = s + "0" } - n, e := strconv.ParseInt(s, 10, 16) + n, e := strconv.ParseInt(s, 10, 32) return int16(n), e } From bd8f90e8621ed6c86e9b6cd979180065f42e8998 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Thu, 25 Apr 2019 21:16:34 +0100 Subject: [PATCH 111/165] new DateString variant of Date allows DB storage using TEXT or DATE columns --- sql.go | 58 ++++++++++++++++----------- sql_test.go | 113 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 114 insertions(+), 57 deletions(-) diff --git a/sql.go b/sql.go index 811c3c5d..2cd796ad 100644 --- a/sql.go +++ b/sql.go @@ -23,9 +23,6 @@ func (d *Date) Scan(value interface{}) (err error) { return nil } - if DisableTextStorage { - return d.scanInt(value) - } return d.scanAny(value) } @@ -57,37 +54,50 @@ func (d *Date) scanString(value string) (err error) { return err } -func (d *Date) scanInt(value interface{}) (err error) { - err = nil - switch value.(type) { - case int64: - *d = Date{PeriodOfDays(value.(int64))} - default: - err = fmt.Errorf("%T %+v is not a meaningful date", value, value) - } - return -} - // Value converts the value to an int64. It implements driver.Valuer, // https://golang.org/pkg/database/sql/driver/#Valuer -func (d Date) ConvertValue(v interface{}) (driver.Value, error) { - switch v.(type) { - case string, []byte: - return d.String(), nil - case time.Time: - return d.UTC(), nil - } +func (d Date) Value() (driver.Value, error) { return int64(d.day), nil } +//------------------------------------------------------------------------------------------------- + +// DateString alters Date to make database storage use a string column, or +// a similar derived column such as SQL DATE. (Otherwise, Date is stored as +// an integer). +type DateString Date + +// Date provides a simple fluent type conversion to the underlying type. +func (d DateString) Date() Date { + return Date(d) +} + +// DateString provides a simple fluent type conversion from the underlying type. +func (d Date) DateString() DateString { + return DateString(d) +} + +// Scan parses some value. It implements sql.Scanner, +// https://golang.org/pkg/database/sql/#Scanner +func (d *DateString) Scan(value interface{}) (err error) { + if value == nil { + return nil + } + return (*Date)(d).Scan(value) +} + // Value converts the value to an int64. It implements driver.Valuer, // https://golang.org/pkg/database/sql/driver/#Valuer -//func (d Date) Value() (driver.Value, error) { -// return int64(d.day), nil -//} +func (d DateString) Value() (driver.Value, error) { + return d.Date().String(), nil +} + +//------------------------------------------------------------------------------------------------- // DisableTextStorage reduces the Scan method so that only integers are handled. // Normally, database types int64, []byte, string and time.Time are supported. // When set true, only int64 is supported; this mode allows optimisation of SQL // result processing and would only be used during development. +// +// Deprecated: this is no longer used. var DisableTextStorage = false diff --git a/sql_test.go b/sql_test.go index 3cdbf62c..d07f8de5 100644 --- a/sql_test.go +++ b/sql_test.go @@ -12,28 +12,24 @@ import ( func TestDateScan(t *testing.T) { cases := []struct { v interface{} - disallow bool expected PeriodOfDays }{ - {int64(0), false, 0}, - {int64(1000), false, 1000}, - {int64(10000), false, 10000}, - {int64(0), true, 0}, - {int64(1000), true, 1000}, - {int64(10000), true, 10000}, - {"0", false, 0}, - {"1000", false, 1000}, - {"10000", false, 10000}, - {"2018-12-31", false, 17896}, - {"31/12/2018", false, 17896}, - {[]byte("10000"), false, 10000}, - {PeriodOfDays(10000).Date().Local(), false, 10000}, + {int64(0), 0}, + {int64(1000), 1000}, + {int64(10000), 10000}, + {int64(0), 0}, + {int64(1000), 1000}, + {int64(10000), 10000}, + {"0", 0}, + {"1000", 1000}, + {"10000", 10000}, + {"2018-12-31", 17896}, + {"31/12/2018", 17896}, + {[]byte("10000"), 10000}, + {PeriodOfDays(10000).Date().Local(), 10000}, } - prior := DisableTextStorage - for i, c := range cases { - DisableTextStorage = c.disallow r := new(Date) e := r.Scan(c.v) if e != nil { @@ -43,43 +39,89 @@ func TestDateScan(t *testing.T) { t.Errorf("%d: Got %v, want %d", i, *r, c.expected) } - var d driver.ValueConverter = *r - //var d driver.Valuer = *r + var d driver.Valuer = *r - _, e = d.ConvertValue(c.v) + q, e := d.Value() if e != nil { t.Errorf("%d: Got %v for %d", i, e, c.expected) } - //if q.(int64) != int64(c.expected) { - // t.Errorf("%d: Got %v, want %d", i, q, c.expected) - //} + if q.(int64) != int64(c.expected) { + t.Errorf("%d: Got %v, want %d", i, q, c.expected) + } + } +} + +func TestDateStringScan(t *testing.T) { + cases := []struct { + v interface{} + expected string + }{ + {int64(0), "1970-01-01"}, + {int64(15000), "2011-01-26"}, + {"0", "1970-01-01"}, + {"15000", "2011-01-26"}, + {"2018-12-31", "2018-12-31"}, + {"31/12/2018", "2018-12-31"}, + //{[]byte("10000"), ""}, + //{PeriodOfDays(10000).Date().Local(), ""}, } - DisableTextStorage = prior + for i, c := range cases { + r := new(DateString) + e := r.Scan(c.v) + if e != nil { + t.Errorf("%d: Got %v for %s", i, e, c.expected) + } + if r.Date().String() != c.expected { + t.Errorf("%d: Got %v, want %s", i, r.Date(), c.expected) + } + + var d driver.Valuer = *r + + q, e := d.Value() + if e != nil { + t.Errorf("%d: Got %v for %s", i, e, c.expected) + } + if q.(string) != c.expected { + t.Errorf("%d: Got %v, want %s", i, q, c.expected) + } + } } func TestDateScanWithJunk(t *testing.T) { cases := []struct { v interface{} - disallow bool expected string }{ - {true, false, "bool true is not a meaningful date"}, - {true, true, "bool true is not a meaningful date"}, + {true, "bool true is not a meaningful date"}, + {true, "bool true is not a meaningful date"}, } - prior := DisableTextStorage - for i, c := range cases { - DisableTextStorage = c.disallow r := new(Date) e := r.Scan(c.v) if e.Error() != c.expected { t.Errorf("%d: Got %q, want %q", i, e.Error(), c.expected) } } +} + +func TestDateStringScanWithJunk(t *testing.T) { + cases := []struct { + v interface{} + expected string + }{ + {true, "bool true is not a meaningful date"}, + {true, "bool true is not a meaningful date"}, + } - DisableTextStorage = prior + for i, c := range cases { + r := new(DateString) + e := r.Scan(c.v) + if e.Error() != c.expected { + t.Errorf("%d: Got %q, want %q", i, e.Error(), c.expected) + } + } } func TestDateScanWithNil(t *testing.T) { @@ -88,7 +130,12 @@ func TestDateScanWithNil(t *testing.T) { if e != nil { t.Errorf("Got %v", e) } - if r != nil { - t.Errorf("Got %v", r) +} + +func TestDateAsStringScanWithNil(t *testing.T) { + var r *Date + e := r.Scan(nil) + if e != nil { + t.Errorf("Got %v", e) } } From a4bb3f7f97427156166ac60fdd5f2de5234fbc1f Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Sat, 27 Apr 2019 09:54:06 +0100 Subject: [PATCH 112/165] parsing now uses 64-bit arithmetic for its calculations, allowing the correct parsing of, say, large numbers of seconds --- period/marshal_test.go | 8 ++++---- period/parse.go | 21 ++++++++------------- period/period.go | 3 ++- period/period_test.go | 28 ++++++++++++++++++++++------ 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/period/marshal_test.go b/period/marshal_test.go index 89377f88..b158723d 100644 --- a/period/marshal_test.go +++ b/period/marshal_test.go @@ -51,8 +51,8 @@ func TestPeriodJSONMarshalling(t *testing.T) { value Period want string }{ - {New(-1111, -123, -3, -11, -59, -59), `"-P1111Y123M3DT11H59M59S"`}, - {New(-1, -12, -31, -5, -4, -20), `"-P1Y12M31DT5H4M20S"`}, + {New(-1111, -4, -3, -11, -59, -59), `"-P1111Y4M3DT11H59M59S"`}, + {New(-1, -10, -31, -5, -4, -20), `"-P1Y10M31DT5H4M20S"`}, {New(0, 0, 0, 0, 0, 0), `"P0D"`}, {New(0, 0, 0, 0, 0, 1), `"PT1S"`}, {New(0, 0, 0, 0, 1, 0), `"PT1M"`}, @@ -84,8 +84,8 @@ func TestPeriodTextMarshalling(t *testing.T) { value Period want string }{ - {New(-1111, -123, -3, -11, -59, -59), "-P1111Y123M3DT11H59M59S"}, - {New(-1, -12, -31, -5, -4, -20), "-P1Y12M31DT5H4M20S"}, + {New(-1111, -4, -3, -11, -59, -59), "-P1111Y4M3DT11H59M59S"}, + {New(-1, -9, -31, -5, -4, -20), "-P1Y9M31DT5H4M20S"}, {New(0, 0, 0, 0, 0, 0), "P0D"}, {New(0, 0, 0, 0, 0, 1), "PT1S"}, {New(0, 0, 0, 0, 1, 0), "PT1M"}, diff --git a/period/parse.go b/period/parse.go index 30a0ff2b..16e6ec78 100644 --- a/period/parse.go +++ b/period/parse.go @@ -36,10 +36,10 @@ func Parse(period string) (Period, error) { return Period{}, nil } + result := period64{} pcopy := period - negate := false if pcopy[0] == '-' { - negate = true + result.neg = true pcopy = pcopy[1:] } else if pcopy[0] == '+' { pcopy = pcopy[1:] @@ -50,8 +50,6 @@ func Parse(period string) (Period, error) { } pcopy = pcopy[1:] - result := Period{} - st := parseState{period, pcopy, false, nil} t := strings.IndexByte(pcopy, 'T') if t >= 0 { @@ -101,10 +99,8 @@ func Parse(period string) (Period, error) { if !st.ok { return Period{}, fmt.Errorf("expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' marker: %s", period) } - if negate { - return result.Negate(), nil - } - return result, nil + + return result.normalise64(true).toPeriod(), nil } type parseState struct { @@ -113,9 +109,9 @@ type parseState struct { err error } -func parseField(st parseState, mark byte) (int16, parseState) { +func parseField(st parseState, mark byte) (int64, parseState) { //fmt.Printf("%c %#v\n", mark, st) - r := int16(0) + r := int64(0) m := strings.IndexByte(st.pcopy, mark) if m > 0 { r, st.err = parseDecimalFixedPoint(st.pcopy[:m], st.period) @@ -129,7 +125,7 @@ func parseField(st parseState, mark byte) (int16, parseState) { } // Fixed-point three decimal places -func parseDecimalFixedPoint(s, original string) (int16, error) { +func parseDecimalFixedPoint(s, original string) (int64, error) { //was := s dec := strings.IndexByte(s, '.') if dec < 0 { @@ -147,6 +143,5 @@ func parseDecimalFixedPoint(s, original string) (int16, error) { s = s + "0" } - n, e := strconv.ParseInt(s, 10, 32) - return int16(n), e + return strconv.ParseInt(s, 10, 64) } diff --git a/period/period.go b/period/period.go index cd4dd9ed..bf722c9d 100644 --- a/period/period.go +++ b/period/period.go @@ -660,7 +660,8 @@ func (p *period64) rippleUp(precise bool) *period64 { p.hours = p.hours + (p.minutes/600)*10 p.minutes = p.minutes % 600 - if !precise || p.hours > 32670-(32670/60)-(32670/3600) { + // 32670-(32670/60)-(32670/3600) = 32760 - 546 - 9.1 = 32204.9 + if !precise || p.hours > 32204 { p.days += (p.hours / 240) * 10 p.hours = p.hours % 240 } diff --git a/period/period_test.go b/period/period_test.go index 3051c817..abffd75d 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -66,6 +66,19 @@ func TestParsePeriod(t *testing.T) { {"P1Y2.5M", Period{10, 25, 0, 0, 0, 0}}, {"P1Y2.15M", Period{10, 21, 0, 0, 0, 0}}, {"P1Y2.125M", Period{10, 21, 0, 0, 0, 0}}, + {"P3276.7Y", Period{32767, 0, 0, 0, 0, 0}}, + {"-P3276.7Y", Period{-32767, 0, 0, 0, 0, 0}}, + // largest possible number of seconds normalised only in hours, mins, sec + {"PT11592000S", Period{0, 0, 0, 32200, 0, 0}}, + {"-PT11592000S", Period{0, 0, 0, -32200, 0, 0}}, + {"PT11595599S", Period{0, 0, 0, 32200, 590, 590}}, + // largest possible number of seconds normalised only in days, hours, mins, sec + {"PT283046400S", Period{0, 0, 32760, 0, 0, 0}}, + {"-PT283046400S", Period{0, 0, -32760, 0, 0, 0}}, + {"PT283132799S", Period{0, 0, 32760, 230, 590, 590}}, + // largest possible number of months + {"P39312M", Period{32760, 0, 0, 0, 0, 0}}, + {"-P39312M", Period{-32760, 0, 0, 0, 0, 0}}, } for i, c := range cases { d := MustParse(c.value) @@ -218,7 +231,7 @@ func TestPeriodAddToTime(t *testing.T) { const hr = 60 * min // A conveniently round number (14 July 2017 @ 2:40am UTC) - var t0 = time.Unix(1500000000, 0) + var t0 = time.Unix(1500000000, 0).UTC() cases := []struct { value string @@ -250,16 +263,17 @@ func TestPeriodAddToTime(t *testing.T) { {"-P0.1D", t0.Add(-144 * min), false}, {"P0.1M", t0.Add(oneMonthApprox / 10), false}, {"P0.1Y", t0.Add(oneYearApprox / 10), false}, - {"-P0.1Y0.1M0.1D", t0.Add(-(oneYearApprox / 10) - (oneMonthApprox / 10) - (144 * min)), false}, + // after normalisation, this period is one month and 9.2 days + {"-P0.1Y0.1M0.1D", t0.Add(-oneMonthApprox - (13248 * min)), false}, } for i, c := range cases { p := MustParse(c.value) t1, prec := p.AddTo(t0) - if t1 != c.result { - t.Errorf("%d: AddTo(t) == %s %v, want %s for %s", i, t1, prec, c.result, c.value) + if !t1.Equal(c.result) { + t.Errorf("%d: %s.AddTo(t) == %s %v, want %s", i, c.value, t1, prec, c.result) } if prec != c.precise { - t.Errorf("%d: AddTo(t) == %s %v, want %v for %s", i, t1, prec, c.precise, c.value) + t.Errorf("%d: %s.AddTo(t) == %s %v, want %v", i, c.value, t1, prec, c.precise) } } } @@ -280,7 +294,9 @@ func TestPeriodToDuration(t *testing.T) { {"PT3276M", 3276 * time.Minute, true}, {"PT1H", 3600 * time.Second, true}, {"PT0.1H", 360 * time.Second, true}, - {"PT3276H", 3276 * time.Hour, true}, + {"PT3220H", 3220 * time.Hour, true}, + {"PT3221H", 3221 * time.Hour, false}, // threshold of normalisation wrapping + // days, months and years conversions are never precise {"P1D", 24 * time.Hour, false}, {"P0.1D", 144 * time.Minute, false}, {"P3276D", 3276 * 24 * time.Hour, false}, From 9434d2c1334c56fd4d3fe28eea7c89d1d4758b10 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Mon, 13 May 2019 23:32:04 +0100 Subject: [PATCH 113/165] DateString now implements binary & text marshalling interfaces, so is compatible with the standard encoders (e.g. JSON) --- go.mod | 2 +- go.sum | 5 +-- marshal.go | 29 +++++++++++++++++ marshal_test.go | 83 +++++++++++++++++++++++++++++++++++++++++-------- sql.go | 12 +++---- 5 files changed, 109 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index b2894f59..9b7e613f 100644 --- a/go.mod +++ b/go.mod @@ -2,5 +2,5 @@ module github.com/rickb777/date require ( github.com/rickb777/plural v1.2.0 - golang.org/x/text v0.3.0 + golang.org/x/text v0.3.2 ) diff --git a/go.sum b/go.sum index 1023909a..1b495fb7 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ github.com/rickb777/plural v1.2.0 h1:5tvEc7UBCZ7l8h/2UeybSkt/uu1DQsZFOFdNevmUhlE= github.com/rickb777/plural v1.2.0/go.mod h1:UdpyWFCGbo3mvK3f/PfZOAOrkjzJlYN/sD46XNWJ+Es= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/marshal.go b/marshal.go index 8a2d347c..377a64ff 100644 --- a/marshal.go +++ b/marshal.go @@ -33,6 +33,16 @@ func (d *Date) UnmarshalBinary(data []byte) error { return nil } +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (ds DateString) MarshalBinary() ([]byte, error) { + return Date(ds).MarshalBinary() +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (ds *DateString) UnmarshalBinary(data []byte) error { + return (*Date)(ds).UnmarshalBinary(data) +} + // MarshalText implements the encoding.TextMarshaler interface. // The date is given in ISO 8601 extended format (e.g. "2006-01-02"). // If the year of the date falls outside the [0,9999] range, this format @@ -55,3 +65,22 @@ func (d *Date) UnmarshalText(data []byte) (err error) { } return err } + +// MarshalText implements the encoding.TextMarshaler interface. +// The date is given in ISO 8601 extended format (e.g. "2006-01-02"). +// If the year of the date falls outside the [0,9999] range, this format +// produces an expanded year representation with possibly extra year digits +// beyond the prescribed four-digit minimum and with a + or - sign prefix +// (e.g. , "+12345-06-07", "-0987-06-05"). +func (ds DateString) MarshalText() ([]byte, error) { + return Date(ds).MarshalText() +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +// The date is expected to be in ISO 8601 extended format +// (e.g. "2006-01-02", "+12345-06-07", "-0987-06-05"); +// the year must use at least 4 digits and if outside the [0,9999] range +// must be prefixed with a + or - sign. +func (ds *DateString) UnmarshalText(data []byte) (err error) { + return (*Date)(ds).UnmarshalText(data) +} diff --git a/marshal_test.go b/marshal_test.go index 153c4d4f..94d2957a 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -13,9 +13,6 @@ import ( ) func TestGobEncoding(t *testing.T) { - var b bytes.Buffer - encoder := gob.NewEncoder(&b) - decoder := gob.NewDecoder(&b) cases := []Date{ New(-11111, time.February, 3), New(-1, time.December, 31), @@ -26,6 +23,10 @@ func TestGobEncoding(t *testing.T) { New(12345, time.June, 7), } for _, c := range cases { + var b bytes.Buffer + encoder := gob.NewEncoder(&b) + decoder := gob.NewDecoder(&b) + var d Date err := encoder.Encode(&c) if err != nil { @@ -38,6 +39,19 @@ func TestGobEncoding(t *testing.T) { t.Errorf("Gob(%v) decode got %v", c, d) } } + + ds := c.DateString() + err = encoder.Encode(&ds) + if err != nil { + t.Errorf("Gob(%v) encode error %v", c, err) + } else { + err = decoder.Decode(&ds) + if err != nil { + t.Errorf("Gob(%v) decode error %v", c, err) + } else if ds != c.DateString() { + t.Errorf("Gob(%v) decode got %v", c, ds) + } + } } } @@ -56,19 +70,34 @@ func TestDateJSONMarshalling(t *testing.T) { } for _, c := range cases { var d Date - bb, err := json.Marshal(c.value) + bb1, err := json.Marshal(c.value) if err != nil { t.Errorf("JSON(%v) marshal error %v", c, err) - } else if string(bb) != c.want { - t.Errorf("JSON(%v) == %v, want %v", c.value, string(bb), c.want) + } else if string(bb1) != c.want { + t.Errorf("JSON(%v) == %v, want %v", c.value, string(bb1), c.want) } else { - err = json.Unmarshal(bb, &d) + err = json.Unmarshal(bb1, &d) if err != nil { t.Errorf("JSON(%v) unmarshal error %v", c.value, err) } else if d != c.value { t.Errorf("JSON(%v) unmarshal got %v", c.value, d) } } + + var ds DateString + bb2, err := json.Marshal(c.value.DateString()) + if err != nil { + t.Errorf("JSON(%v) marshal error %v", c, err) + } else if string(bb2) != c.want { + t.Errorf("JSON(%v) == %v, want %v", c.value.DateString(), string(bb2), c.want) + } else { + err = json.Unmarshal(bb2, &ds) + if err != nil { + t.Errorf("JSON(%v) unmarshal error %v", c.value.DateString(), err) + } else if ds != c.value.DateString() { + t.Errorf("JSON(%v) unmarshal got %v", c.value.DateString(), ds) + } + } } } @@ -87,19 +116,34 @@ func TestDateTextMarshalling(t *testing.T) { } for _, c := range cases { var d Date - bb, err := c.value.MarshalText() + bb1, err := c.value.MarshalText() if err != nil { t.Errorf("Text(%v) marshal error %v", c, err) - } else if string(bb) != c.want { - t.Errorf("Text(%v) == %v, want %v", c.value, string(bb), c.want) + } else if string(bb1) != c.want { + t.Errorf("Text(%v) == %v, want %v", c.value, string(bb1), c.want) } else { - err = d.UnmarshalText(bb) + err = d.UnmarshalText(bb1) if err != nil { t.Errorf("Text(%v) unmarshal error %v", c.value, err) } else if d != c.value { t.Errorf("Text(%v) unmarshal got %v", c.value, d) } } + + var ds DateString + bb2, err := c.value.DateString().MarshalText() + if err != nil { + t.Errorf("Text(%v) marshal error %v", c, err) + } else if string(bb2) != c.want { + t.Errorf("Text(%v) == %v, want %v", c.value, string(bb2), c.want) + } else { + err = ds.UnmarshalText(bb2) + if err != nil { + t.Errorf("Text(%v) unmarshal error %v", c.value, err) + } else if ds != c.value.DateString() { + t.Errorf("Text(%v) unmarshal got %v", c.value, ds) + } + } } } @@ -116,18 +160,31 @@ func TestDateBinaryMarshalling(t *testing.T) { {New(12345, time.June, 7)}, } for _, c := range cases { - bb, err := c.value.MarshalBinary() + bb1, err := c.value.MarshalBinary() if err != nil { t.Errorf("Binary(%v) marshal error %v", c, err) } else { var d Date - err = d.UnmarshalBinary(bb) + err = d.UnmarshalBinary(bb1) if err != nil { t.Errorf("Binary(%v) unmarshal error %v", c.value, err) } else if d != c.value { t.Errorf("Binary(%v) unmarshal got %v", c.value, d) } } + + bb2, err := c.value.MarshalBinary() + if err != nil { + t.Errorf("Binary(%v) marshal error %v", c, err) + } else { + var ds DateString + err = ds.UnmarshalBinary(bb2) + if err != nil { + t.Errorf("Binary(%v) unmarshal error %v", c.value, err) + } else if ds != c.value.DateString() { + t.Errorf("Binary(%v) unmarshal got %v", c.value, ds) + } + } } } diff --git a/sql.go b/sql.go index 2cd796ad..54642fc8 100644 --- a/sql.go +++ b/sql.go @@ -68,8 +68,8 @@ func (d Date) Value() (driver.Value, error) { type DateString Date // Date provides a simple fluent type conversion to the underlying type. -func (d DateString) Date() Date { - return Date(d) +func (ds DateString) Date() Date { + return Date(ds) } // DateString provides a simple fluent type conversion from the underlying type. @@ -79,17 +79,17 @@ func (d Date) DateString() DateString { // Scan parses some value. It implements sql.Scanner, // https://golang.org/pkg/database/sql/#Scanner -func (d *DateString) Scan(value interface{}) (err error) { +func (ds *DateString) Scan(value interface{}) (err error) { if value == nil { return nil } - return (*Date)(d).Scan(value) + return (*Date)(ds).Scan(value) } // Value converts the value to an int64. It implements driver.Valuer, // https://golang.org/pkg/database/sql/driver/#Valuer -func (d DateString) Value() (driver.Value, error) { - return d.Date().String(), nil +func (ds DateString) Value() (driver.Value, error) { + return ds.Date().String(), nil } //------------------------------------------------------------------------------------------------- From ccd9db06eb240c16a12ec2e22d794e66caa6ca32 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 5 Jun 2019 16:53:08 +0100 Subject: [PATCH 114/165] extra test case added to date.Parse; added 'go vet' and 'goreturns' --- build+test.sh | 35 +++++++++++++++++++++++++++-------- datetool/main.go | 5 +++-- parse_test.go | 1 + timespan/timespan.go | 6 +++--- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/build+test.sh b/build+test.sh index 0e600c97..d35731e3 100755 --- a/build+test.sh +++ b/build+test.sh @@ -1,22 +1,41 @@ #!/bin/bash -e cd $(dirname $0) -PATH=$HOME/gopath/bin:$GOPATH/bin:$PATH +PATH=$HOME/go/bin:$PATH +unset GOPATH +export GO111MODULE=on + +function v +{ + echo + echo $@ + $@ +} if ! type -p goveralls; then - echo go get github.com/mattn/goveralls - go get github.com/mattn/goveralls + v go get github.com/mattn/goveralls +fi + +if ! type -p shadow; then + v go get golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow +fi + +if ! type -p goreturns; then + v go get github.com/sqs/goreturns fi echo date... -go test -v -covermode=count -coverprofile=date.out . -go tool cover -func=date.out +v go test -v -covermode=count -coverprofile=date.out . +v go tool cover -func=date.out [ -z "$COVERALLS_TOKEN" ] || goveralls -coverprofile=date.out -service=travis-ci -repotoken $COVERALLS_TOKEN for d in clock period timespan view; do echo $d... - go test -v -covermode=count -coverprofile=$d.out ./$d - go tool cover -func=$d.out + v go test -v -covermode=count -coverprofile=$d.out ./$d + v go tool cover -func=$d.out [ -z "$COVERALLS_TOKEN" ] || goveralls -coverprofile=$d.out -service=travis-ci -repotoken $COVERALLS_TOKEN done -go install ./datetool +v goreturns -l -w *.go */*.go +v go vet ./... +v go vet -vettool=$(type -p shadow) ./... +v go install ./datetool diff --git a/datetool/main.go b/datetool/main.go index d7b7af16..301f110d 100644 --- a/datetool/main.go +++ b/datetool/main.go @@ -9,12 +9,13 @@ package main import ( "fmt" + "os" + "strconv" + "github.com/rickb777/date" "github.com/rickb777/date/clock" "golang.org/x/text/language" "golang.org/x/text/message" - "os" - "strconv" ) func usage() { diff --git a/parse_test.go b/parse_test.go index 12d013d4..89ce01ae 100644 --- a/parse_test.go +++ b/parse_test.go @@ -190,6 +190,7 @@ func TestParse(t *testing.T) { {RFC1123, "05 Dec 1928", 1928, time.December, 5}, {RFC1123W, "Mon, 05 Dec 1928", 1928, time.December, 5}, {RFC3339, "2345-06-07", 2345, time.June, 7}, + {"20060102", "20190619", 2019, time.June, 19}, } for _, c := range cases { d := MustParse(c.layout, c.value) diff --git a/timespan/timespan.go b/timespan/timespan.go index 6b22a840..dd846025 100644 --- a/timespan/timespan.go +++ b/timespan/timespan.go @@ -249,9 +249,9 @@ func ParseRFC5545InLocation(text string, loc *time.Location) (TimeSpan, error) { } if rest[0] == 'P' { - pe, err := period.Parse(rest) - if err != nil { - return TimeSpan{}, fmt.Errorf("cannot parse period in %q: %s", text, err.Error()) + pe, e2 := period.Parse(rest) + if e2 != nil { + return TimeSpan{}, fmt.Errorf("cannot parse period in %q: %s", text, e2.Error()) } du, precise := pe.Duration() From 4da884b2f830b66e8f6c7f7f9342714b26760a79 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 5 Jun 2019 17:07:17 +0100 Subject: [PATCH 115/165] withdrawn shadowing checks --- build+test.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/build+test.sh b/build+test.sh index d35731e3..98418dff 100755 --- a/build+test.sh +++ b/build+test.sh @@ -15,9 +15,9 @@ if ! type -p goveralls; then v go get github.com/mattn/goveralls fi -if ! type -p shadow; then - v go get golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow -fi +#if ! type -p shadow; then +# v go get golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow +#fi if ! type -p goreturns; then v go get github.com/sqs/goreturns @@ -36,6 +36,10 @@ for d in clock period timespan view; do done v goreturns -l -w *.go */*.go + v go vet ./... -v go vet -vettool=$(type -p shadow) ./... + +# shadow check fails in Travis +#v go vet -vettool=$(type -p shadow) ./... + v go install ./datetool From 2dd7d2b8263ed842e54acf0b003e7a44e20bff9b Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Sun, 16 Jun 2019 22:36:00 +0100 Subject: [PATCH 116/165] added extra documentation and some test cases --- clock/clock.go | 4 ++++ period/doc.go | 23 +++++++++++++++++++++-- period/parse.go | 5 +++++ period/period.go | 23 +++++++++++++++++++++++ period/period_test.go | 5 +++++ 5 files changed, 58 insertions(+), 2 deletions(-) diff --git a/clock/clock.go b/clock/clock.go index a2083019..99de649f 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -83,6 +83,7 @@ func (c Clock) DurationSinceMidnight() time.Duration { // Add returns a new Clock offset from this clock specified hour, minute, second and millisecond. // The parameters can be negative. +// // If required, use Mod24() to correct any overflow or underflow. func (c Clock) Add(h, m, s, ms int) Clock { hx := Clock(h) * Hour @@ -93,7 +94,10 @@ func (c Clock) Add(h, m, s, ms int) Clock { // AddDuration returns a new Clock offset from this clock by a duration. // The parameters can be negative. +// // If required, use Mod24() to correct any overflow or underflow. +// +// AddDuration is also useful for adding period.Period values. func (c Clock) AddDuration(d time.Duration) Clock { return c + Clock(d/time.Millisecond) } diff --git a/period/doc.go b/period/doc.go index 4e0eaa05..b564972f 100644 --- a/period/doc.go +++ b/period/doc.go @@ -3,7 +3,8 @@ // license that can be found in the LICENSE file. // Package period provides functionality for periods of time using ISO-8601 conventions. -// This deals with years, months, weeks and days. +// This deals with years, months, weeks/days, hours, minutes and seconds. +// // Because of the vagaries of calendar systems, the meaning of year lengths, month lengths // and even day lengths depends on context. So a period is not necessarily a fixed duration // of time in terms of seconds. @@ -12,14 +13,32 @@ // // Example representations: // +// * "P2Y" is two years; +// +// * "P6M" is six months; +// // * "P4D" is four days; // +// * "P1W" is one week (seven days); +// +// * "PT3H" is three hours. +// +// * "PT20M" is twnety minutes. +// +// * "PT30S" is thirty seconds. +// +// These can be combined, for example: +// // * "P3Y6M4W1D" is three years, 6 months, 4 weeks and one day. // // * "P2DT12H" is 2 days and 12 hours. // -// * "PT30S" is 30 seconds. +// Also, decimal fractions are supported to one decimal place. To comply with +// the standard, only the last non-zero component is allowed to have a fraction). +// For example // // * "P2.5Y" is 2.5 years. // +// * "PT12M7.5S" is 12 minutes and 7.5 seconds. +// package period diff --git a/period/parse.go b/period/parse.go index 16e6ec78..30b3a693 100644 --- a/period/parse.go +++ b/period/parse.go @@ -24,6 +24,11 @@ func MustParse(value string) Period { // // In addition, a plus or minus sign can precede the period, e.g. "-P10D" // +// The value is normalised, e.g. multiple of 12 months become years so "P24M" +// is the same as "P2Y". However, this is done without loss of precision, so +// for example whole numbers of days do not contribute to the months tally +// because the number of days per month is variable. +// // The zero value can be represented in several ways: all of the following // are equivalent: "P0Y", "P0M", "P0W", "P0D", "PT0H", PT0M", PT0S", and "P0". // The canonical zero is "P0D". diff --git a/period/period.go b/period/period.go index bf722c9d..604dbfc1 100644 --- a/period/period.go +++ b/period/period.go @@ -313,12 +313,18 @@ func (period Period) YearsFloat() float32 { // Months gets the whole number of months in the period. // The result is the number of months and does not include any other field. +// +// Note that after normalisation, whole multiple of 12 months are added to +// the number of years, so the number of months will be reduced correspondingly. func (period Period) Months() int { return int(period.MonthsFloat()) } // MonthsFloat gets the number of months in the period. // The result is the number of months and does not include any other field. +// +// Note that after normalisation, whole multiple of 12 months are added to +// the number of years, so the number of months will be reduced correspondingly. func (period Period) MonthsFloat() float32 { return float32(period.months) / 10 } @@ -338,6 +344,9 @@ func (period Period) DaysFloat() float32 { // Weeks calculates the number of whole weeks from the number of days. If the result // would contain a fraction, it is truncated. // The result is the number of weeks and does not include any other field. +// +// Note that weeks are synthetic: they are internally represented using days. +// See ModuloDays(), which returns the number of days excluding whole weeks. func (period Period) Weeks() int { return int(period.days) / 70 } @@ -373,24 +382,36 @@ func (period Period) HoursFloat() float32 { // Minutes gets the whole number of minutes in the period. // The result is the number of minutes and does not include any other field. +// +// Note that after normalisation, whole multiple of 60 minutes are added to +// the number of hours, so the number of minutes will be reduced correspondingly. func (period Period) Minutes() int { return int(period.MinutesFloat()) } // MinutesFloat gets the number of minutes in the period. // The result is the number of minutes and does not include any other field. +// +// Note that after normalisation, whole multiple of 60 minutes are added to +// the number of hours, so the number of minutes will be reduced correspondingly. func (period Period) MinutesFloat() float32 { return float32(period.minutes) / 10 } // Seconds gets the whole number of seconds in the period. // The result is the number of seconds and does not include any other field. +// +// Note that after normalisation, whole multiple of 60 seconds are added to +// the number of minutes, so the number of seconds will be reduced correspondingly. func (period Period) Seconds() int { return int(period.SecondsFloat()) } // SecondsFloat gets the number of seconds in the period. // The result is the number of seconds and does not include any other field. +// +// Note that after normalisation, whole multiple of 60 seconds are added to +// the number of minutes, so the number of seconds will be reduced correspondingly. func (period Period) SecondsFloat() float32 { return float32(period.seconds) / 10 } @@ -496,6 +517,8 @@ func (period Period) TotalMonthsApprox() int { // Additionally, in imprecise mode: // Multiples of 24 hours become days. // Multiples of approx. 30.4 days become months. +// +// Note that leap seconds are disregarded: every minute is assumed to have 60 seconds. func (period Period) Normalise(precise bool) Period { const limit = 32670 - (32670 / 60) diff --git a/period/period_test.go b/period/period_test.go index abffd75d..99863536 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -131,11 +131,14 @@ func TestPeriodIntComponents(t *testing.T) { {"-P1W", 0, 0, -1, -7, 0, 0, 0, 0}, {"P6M", 0, 6, 0, 0, 0, 0, 0, 0}, {"-P6M", 0, -6, 0, 0, 0, 0, 0, 0}, + {"P12M", 1, 0, 0, 0, 0, 0, 0, 0}, + {"-P12M", -1, -0, 0, 0, 0, 0, 0, 0}, {"P39D", 0, 0, 5, 39, 4, 0, 0, 0}, {"-P39D", 0, 0, -5, -39, -4, 0, 0, 0}, {"P4D", 0, 0, 0, 4, 4, 0, 0, 0}, {"-P4D", 0, 0, 0, -4, -4, 0, 0, 0}, {"PT12H", 0, 0, 0, 0, 0, 12, 0, 0}, + {"PT60M", 0, 0, 0, 0, 0, 1, 0, 0}, {"PT30M", 0, 0, 0, 0, 0, 0, 30, 0}, {"PT5S", 0, 0, 0, 0, 0, 0, 0, 5}, } @@ -185,6 +188,8 @@ func TestPeriodFloatComponents(t *testing.T) { {"P1.1M", 0, 1.1, 0, 0, 0, 0, 0, 0}, {"P6M", 0, 6, 0, 0, 0, 0, 0, 0}, {"-P6M", 0, -6, 0, 0, 0, 0, 0, 0}, + {"P12M", 1, 0, 0, 0, 0, 0, 0, 0}, + {"-P12M", -1, 0, 0, 0, 0, 0, 0, 0}, {"P39D", 0, 0, 5.571429, 39, 4, 0, 0, 0}, {"-P39D", 0, 0, -5.571429, -39, -4, 0, 0, 0}, {"P4D", 0, 0, 0.5714286, 4, 4, 0, 0, 0}, From cc565345b7b6a9fd2b960d3bc1f3b495b4f8c516 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Sun, 16 Jun 2019 22:55:53 +0100 Subject: [PATCH 117/165] address build failure --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7dcf53ad..abbfc2dc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: go go: - - tip + - 1.12.6 install: - go get -t -v ./... From 1c9d14be4727bccd0172c69fe95ddad2b120f38e Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Mon, 17 Jun 2019 07:08:06 +0100 Subject: [PATCH 118/165] typos --- period/doc.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/period/doc.go b/period/doc.go index b564972f..c8a57b4a 100644 --- a/period/doc.go +++ b/period/doc.go @@ -23,7 +23,7 @@ // // * "PT3H" is three hours. // -// * "PT20M" is twnety minutes. +// * "PT20M" is twenty minutes. // // * "PT30S" is thirty seconds. // @@ -34,7 +34,7 @@ // * "P2DT12H" is 2 days and 12 hours. // // Also, decimal fractions are supported to one decimal place. To comply with -// the standard, only the last non-zero component is allowed to have a fraction). +// the standard, only the last non-zero component is allowed to have a fraction. // For example // // * "P2.5Y" is 2.5 years. From 036c37a6c88229dffb6c173e6438a6903fddcc35 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Fri, 12 Jul 2019 23:58:05 +0100 Subject: [PATCH 119/165] Bug #4 fixed: AutoParse now checks for the string being blank (after trimming whitespace) --- marshal_test.go | 4 ++-- parse.go | 23 +++++++++++++++-------- parse_test.go | 10 +++++++--- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/marshal_test.go b/marshal_test.go index 94d2957a..fc15174d 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -206,8 +206,8 @@ func TestInvalidDateText(t *testing.T) { value string want string }{ - {`not-a-date`, `Date.ParseISO: cannot parse not-a-date: incorrect syntax`}, - {`215-08-15`, `Date.ParseISO: cannot parse 215-08-15: invalid year`}, + {`not-a-date`, `Date.ParseISO: cannot parse "not-a-date": incorrect syntax`}, + {`215-08-15`, `Date.ParseISO: cannot parse "215-08-15": invalid year`}, } for _, c := range cases { var d Date diff --git a/parse.go b/parse.go index efd3039f..60a42e11 100644 --- a/parse.go +++ b/parse.go @@ -5,6 +5,7 @@ package date import ( + "errors" "fmt" "strconv" "strings" @@ -33,12 +34,18 @@ func MustAutoParse(value string) Date { // // * dd/mm/yyyy | dd.mm.yyyy (or any similar pattern) // +// * surrounding whitespace is ignored +// func AutoParse(value string) (Date, error) { abs := strings.TrimSpace(value) + if len(abs) == 0 { + return Date{}, errors.New("Date.AutoParse: cannot parse a blank string") + } + sign := "" - if value[0] == '+' || value[0] == '-' { - abs = value[1:] - sign = value[:1] + if abs[0] == '+' || abs[0] == '-' { + sign = abs[:1] + abs = abs[1:] } if len(abs) >= 10 { @@ -97,7 +104,7 @@ func MustParseISO(value string) Date { // Background: https://en.wikipedia.org/wiki/ISO_8601#Dates func ParseISO(value string) (Date, error) { if len(value) < 8 { - return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %s: incorrect length", value) + return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %q: incorrect length", value) } abs := value @@ -119,12 +126,12 @@ func ParseISO(value string) (Date, error) { fd1 = 6 fd2 = 8 } else if abs[fm2] != '-' { - return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %s: incorrect syntax", value) + return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %q: incorrect syntax", value) } //fmt.Printf("%s %d %d %d %d %d\n", value, dash1, fm1, fm2, fd1, fd2) if len(abs) != fd2 { - return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %s: incorrect length", value) + return Date{}, fmt.Errorf("Date.ParseISO: cannot parse %q: incorrect length", value) } year, err := parseField(value, abs[:dash1], "year", 4, -1) @@ -153,11 +160,11 @@ func ParseISO(value string) (Date, error) { func parseField(value, field, name string, minLength, requiredLength int) (int, error) { if (minLength > 0 && len(field) < minLength) || (requiredLength > 0 && len(field) != requiredLength) { - return 0, fmt.Errorf("Date.ParseISO: cannot parse %s: invalid %s", value, name) + return 0, fmt.Errorf("Date.ParseISO: cannot parse %q: invalid %s", value, name) } number, err := strconv.Atoi(field) if err != nil { - return 0, fmt.Errorf("Date.ParseISO: cannot parse %s: invalid %s", value, name) + return 0, fmt.Errorf("Date.ParseISO: cannot parse %q: invalid %s", value, name) } return number, nil } diff --git a/parse_test.go b/parse_test.go index 89ce01ae..f846c3ee 100644 --- a/parse_test.go +++ b/parse_test.go @@ -16,7 +16,7 @@ func TestAutoParse(t *testing.T) { month time.Month day int }{ - {"31/12/1969", 1969, time.December, 31}, + {" 31/12/1969 ", 1969, time.December, 31}, {"1969/12/31", 1969, time.December, 31}, {"1969.12.31", 1969, time.December, 31}, {"1969-12-31", 1969, time.December, 31}, @@ -30,12 +30,12 @@ func TestAutoParse(t *testing.T) { {"2004-03-01", 2004, time.March, 1}, {"0000-01-01", 0, time.January, 1}, {"+0001-02-03", 1, time.February, 3}, - {"+00019-03-04", 19, time.March, 4}, + {" +00019-03-04 ", 19, time.March, 4}, {"0100-04-05", 100, time.April, 5}, {"2000-05-06", 2000, time.May, 6}, {"+5000000-08-09", 5000000, time.August, 9}, {"-0001-09-11", -1, time.September, 11}, - {"-0019-10-12", -19, time.October, 12}, + {" -0019-10-12 ", -19, time.October, 12}, {"-00100-11-13", -100, time.November, 13}, {"-02000-12-14", -2000, time.December, 14}, {"-30000-02-15", -30000, time.February, 15}, @@ -44,6 +44,7 @@ func TestAutoParse(t *testing.T) { {"12340506", 1234, time.May, 6}, {"+12340506", 1234, time.May, 6}, {"-00191012", -19, time.October, 12}, + {" -00191012 ", -19, time.October, 12}, } for _, c := range cases { d := MustAutoParse(c.value) @@ -69,6 +70,9 @@ func TestAutoParse(t *testing.T) { "+10-11-12", "+100-02-03", "-123-05-06", + "--", + "", + " ", } for _, c := range badCases { d, err := AutoParse(c) From 7995dd92c79d6907af1422049d9c8ddc0c048d2c Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 18 Sep 2019 14:04:40 +0100 Subject: [PATCH 120/165] build settings --- build+test.sh | 6 +++--- coverage.sh | 6 +++--- go.mod | 3 +++ go.sum | 3 +++ 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/build+test.sh b/build+test.sh index 98418dff..34b31978 100755 --- a/build+test.sh +++ b/build+test.sh @@ -1,5 +1,5 @@ #!/bin/bash -e -cd $(dirname $0) +cd "$(dirname $0)" PATH=$HOME/go/bin:$PATH unset GOPATH export GO111MODULE=on @@ -12,7 +12,7 @@ function v } if ! type -p goveralls; then - v go get github.com/mattn/goveralls + v go install github.com/mattn/goveralls fi #if ! type -p shadow; then @@ -20,7 +20,7 @@ fi #fi if ! type -p goreturns; then - v go get github.com/sqs/goreturns + v go install github.com/sqs/goreturns fi echo date... diff --git a/coverage.sh b/coverage.sh index b8b78321..2b8420f7 100755 --- a/coverage.sh +++ b/coverage.sh @@ -2,14 +2,14 @@ # Developer tool to run the tests and obtain HTML coverage reports. DIR=$PWD -DOT=$(dirname $0) +DOT="$(dirname $0)" cd $DOT TOP=$PWD # install Goveralls if absent if ! type -p goveralls; then - echo go get github.com/mattn/goveralls - go get github.com/mattn/goveralls + echo go install github.com/mattn/goveralls + go install github.com/mattn/goveralls fi mkdir -p reports diff --git a/go.mod b/go.mod index 9b7e613f..c178378e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,9 @@ module github.com/rickb777/date require ( + github.com/mattn/goveralls v0.0.2 // indirect github.com/rickb777/plural v1.2.0 golang.org/x/text v0.3.2 ) + +go 1.13 diff --git a/go.sum b/go.sum index 1b495fb7..a9c5efed 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,8 @@ +github.com/mattn/goveralls v0.0.2 h1:7eJB6EqsPhRVxvwEXGnqdO2sJI0PTsrWoTMXEk9/OQc= +github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/rickb777/plural v1.2.0 h1:5tvEc7UBCZ7l8h/2UeybSkt/uu1DQsZFOFdNevmUhlE= github.com/rickb777/plural v1.2.0/go.mod h1:UdpyWFCGbo3mvK3f/PfZOAOrkjzJlYN/sD46XNWJ+Es= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From e1afd88c1f685155062a0bcbf9cafc5fc16be722 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 18 Sep 2019 14:17:18 +0100 Subject: [PATCH 121/165] script alterations --- build+test.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/build+test.sh b/build+test.sh index 34b31978..e20f5dc9 100755 --- a/build+test.sh +++ b/build+test.sh @@ -15,9 +15,9 @@ if ! type -p goveralls; then v go install github.com/mattn/goveralls fi -#if ! type -p shadow; then -# v go get golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow -#fi +if ! type -p shadow; then + v go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow +fi if ! type -p goreturns; then v go install github.com/sqs/goreturns @@ -39,7 +39,6 @@ v goreturns -l -w *.go */*.go v go vet ./... -# shadow check fails in Travis -#v go vet -vettool=$(type -p shadow) ./... +v shadow ./... v go install ./datetool From 2248ec43d2ed07be58b2ce120f8e15274c3e9b09 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Sat, 4 Apr 2020 20:38:27 +0100 Subject: [PATCH 122/165] Period: some improved documentation and a tweak of month calculations --- period/period.go | 37 ++++++++++++++++++++++--------------- period/period_test.go | 10 +++++++--- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/period/period.go b/period/period.go index 604dbfc1..a9b39c6a 100644 --- a/period/period.go +++ b/period/period.go @@ -10,7 +10,7 @@ import ( ) const daysPerYearE4 int64 = 3652425 // 365.2425 days by the Gregorian rule -const daysPerMonthE4 int64 = 304375 // 30.4375 days per month +const daysPerMonthE4 int64 = 304369 // 30.4369 days per month const daysPerMonthE6 int64 = 30436875 // 30.436875 days per month const oneE4 int64 = 10000 @@ -86,7 +86,13 @@ func New(years, months, days, hours, minutes, seconds int) Period { // NewOf converts a time duration to a Period, and also indicates whether the conversion is precise. // Any time duration that spans more than ± 3276 hours will be approximated by assuming that there -// are 24 hours per day, 30.4375 per month and 365.2425 days per year. +// are 24 hours per day, 365.2425 days per year (as per Gregorian calendar rules), and a month +// being 1/12 of that (approximately 30.4369 days). +// +// The result is not always fully normalised; for time differences less than 3276 hours (about 4.5 months), +// it will contain zero in the years, months and days fields but the number of days may be up to 3275; this +// reduces errors arising from the variable lengths of months. For larger time differences, greater than +// 3276 hours, the days, months and years fields are used as well. func NewOf(duration time.Duration) (p Period, precise bool) { var sign int16 = 1 d := duration @@ -127,10 +133,10 @@ func NewOf(duration time.Duration) (p Period, precise bool) { // Between converts the span between two times to a period. Based on the Gregorian conversion // algorithms of `time.Time`, the resultant period is precise. // -// The result is not normalised; for time differences less than 3276 days, it will contain zero in the -// years and months fields but the number of days may be up to 3275; this reduces errors arising from -// the variable lengths of months. For larger time differences, greater than 3276 days, the months and -// years fields are used as well. +// To improve precision, result is not always fully normalised; for time differences less than 3276 hours +// (about 4.5 months), it will contain zero in the years, months and days fields but the number of hours +// may be up to 3275; this reduces errors arising from the variable lengths of months. For larger time +// differences (greater than 3276 hours) the days, months and years fields are used as well. // // Remember that the resultant period does not retain any knowledge of the calendar, so any subsequent // computations applied to the period can only be precise if they concern either the date (year, month, @@ -422,7 +428,8 @@ func (period Period) SecondsFloat() float32 { // When the period specifies hours, minutes and seconds only, the result is precise. // Also, when the period specifies whole years, months and days (i.e. without fractions), the // result is precise. However, when years, months or days contains fractions, the result -// is only an approximation (it assumes that all days are 24 hours and every year is 365.2425 days). +// is only an approximation (it assumes that all days are 24 hours and every year is 365.2425 +// days, as per Gregorian calendar rules). func (period Period) AddTo(t time.Time) (time.Time, bool) { wholeYears := (period.years % 10) == 0 wholeMonths := (period.months % 10) == 0 @@ -443,8 +450,8 @@ func (period Period) AddTo(t time.Time) (time.Time, bool) { // When the period specifies hours, minutes and seconds only, the result is precise. // however, when the period specifies years, months and days, it is impossible to be precise // because the result may depend on knowing date and timezone information, so the duration -// is estimated on the basis of a year being 365.2425 days and a month being -// 1/12 of a that; days are all assumed to be 24 hours long. +// is estimated on the basis of a year being 365.2425 days (as per Gregorian calendar rules) +// and a month being 1/12 of a that; days are all assumed to be 24 hours long. func (period Period) DurationApprox() time.Duration { d, _ := period.Duration() return d @@ -456,8 +463,8 @@ func (period Period) DurationApprox() time.Duration { // When the period specifies hours, minutes and seconds only, the result is precise. // however, when the period specifies years, months and days, it is impossible to be precise // because the result may depend on knowing date and timezone information, so the duration -// is estimated on the basis of a year being 365.2425 days and a month being -// 1/12 of a that; days are all assumed to be 24 hours long. +// is estimated on the basis of a year being 365.2425 days as per Gregorian calendar rules) +// and a month being 1/12 of a that; days are all assumed to be 24 hours long. func (period Period) Duration() (time.Duration, bool) { // remember that the fields are all fixed-point 1E1 tdE6 := time.Duration(totalDaysApproxE7(period) * 8640) @@ -483,8 +490,8 @@ func totalDaysApproxE7(period Period) int64 { } // TotalDaysApprox gets the approximate total number of days in the period. The approximation assumes -// a year is 365.2425 days and a month is 1/12 of that. Whole multiples of 24 hours are also included -// in the calculation. +// a year is 365.2425 days as per Gregorian calendar rules) and a month is 1/12 of that. Whole +// multiples of 24 hours are also included in the calculation. func (period Period) TotalDaysApprox() int { pn := period.Normalise(false) tdE6 := totalDaysApproxE7(pn) @@ -493,8 +500,8 @@ func (period Period) TotalDaysApprox() int { } // TotalMonthsApprox gets the approximate total number of months in the period. The days component -// is included by approximation, assuming a year is 365.2425 days and a month is 1/12 of that. -// Whole multiples of 24 hours are also included in the calculation. +// is included by approximation, assuming a year is 365.2425 days (as per Gregorian calendar rules) +// and a month is 1/12 of that. Whole multiples of 24 hours are also included in the calculation. func (period Period) TotalMonthsApprox() int { pn := period.Normalise(false) mE1 := int64(pn.years)*12 + int64(pn.months) diff --git a/period/period_test.go b/period/period_test.go index 99863536..87d566bf 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -55,6 +55,7 @@ func TestParsePeriod(t *testing.T) { {"PT12H", Period{0, 0, 0, 120, 0, 0}}, {"PT30M", Period{0, 0, 0, 0, 300, 0}}, {"PT25S", Period{0, 0, 0, 0, 0, 250}}, + {"PT30M67.6S", Period{0, 0, 0, 0, 310, 76}}, {"P3Y6M5W4DT12H40M5S", Period{30, 60, 390, 120, 400, 50}}, {"+P3Y6M5W4DT12H40M5S", Period{30, 60, 390, 120, 400, 50}}, {"-P3Y6M5W4DT12H40M5S", Period{-30, -60, -390, -120, -400, -50}}, @@ -522,14 +523,17 @@ func TestNewOf(t *testing.T) { testNewOf(t, time.Hour+time.Minute+time.Second, Period{0, 0, 0, 10, 10, 10}, true) testNewOf(t, 24*time.Hour+time.Minute+time.Second, Period{0, 0, 0, 240, 10, 10}, true) testNewOf(t, 3276*time.Hour+59*time.Minute+59*time.Second, Period{0, 0, 0, 32760, 590, 590}, true) + testNewOf(t, 30*time.Minute+67*time.Second+600*time.Millisecond, Period{0, 0, 0, 0, 310, 76}, true) // YMD tests: must be over 3276 hours (approx 4.5 months), otherwise HMS will take care of it - // first rollover: 3276 hours + // first rollover: >3276 hours + testNewOf(t, 3277*time.Hour, Period{0, 0, 1360, 130, 0, 0}, false) testNewOf(t, 3288*time.Hour, Period{0, 0, 1370, 0, 0, 0}, false) testNewOf(t, 3289*time.Hour, Period{0, 0, 1370, 10, 0, 0}, false) - testNewOf(t, 3277*time.Hour, Period{0, 0, 1360, 130, 0, 0}, false) + testNewOf(t, 24*3276*time.Hour, Period{0, 0, 32760, 0, 0, 0}, false) - // second rollover: 3276 days + // second rollover: >3276 days + testNewOf(t, 24*3277*time.Hour, Period{80, 110, 200, 0, 0, 0}, false) testNewOf(t, 3277*oneDay, Period{80, 110, 200, 0, 0, 0}, false) testNewOf(t, 3277*oneDay+time.Hour+time.Minute+time.Second, Period{80, 110, 200, 10, 0, 0}, false) testNewOf(t, 36525*oneDay, Period{1000, 0, 0, 0, 0, 0}, false) From 31e2dc8d51f2fdc5219300b6c150a09c269f70ae Mon Sep 17 00:00:00 2001 From: magodo Date: Wed, 22 Apr 2020 00:12:40 +0800 Subject: [PATCH 123/165] Period parsing ensure consumption of all components. --- period/marshal_test.go | 2 +- period/parse.go | 10 ++++++++-- period/period_test.go | 3 +++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/period/marshal_test.go b/period/marshal_test.go index b158723d..240719ac 100644 --- a/period/marshal_test.go +++ b/period/marshal_test.go @@ -119,7 +119,7 @@ func TestInvalidPeriodText(t *testing.T) { }{ {``, `cannot parse a blank string as a period`}, {`not-a-period`, `expected 'P' period mark at the start: not-a-period`}, - {`P000`, `expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' marker: P000`}, + {`P000`, `unexpected remaining components 000: P000`}, } for _, c := range cases { var p Period diff --git a/period/parse.go b/period/parse.go index 30b3a693..270ae8d0 100644 --- a/period/parse.go +++ b/period/parse.go @@ -75,6 +75,10 @@ func Parse(period string) (Period, error) { return Period{}, fmt.Errorf("expected a number before the 'S' marker: %s", period) } + if len(st.pcopy) != 0 { + return Period{}, fmt.Errorf("unexpected remaining components %s: %s", st.pcopy, period) + } + st.pcopy = pcopy[:t] } @@ -82,12 +86,10 @@ func Parse(period string) (Period, error) { if st.err != nil { return Period{}, fmt.Errorf("expected a number before the 'Y' marker: %s", period) } - result.months, st = parseField(st, 'M') if st.err != nil { return Period{}, fmt.Errorf("expected a number before the 'M' marker: %s", period) } - weeks, st := parseField(st, 'W') if st.err != nil { return Period{}, fmt.Errorf("expected a number before the 'W' marker: %s", period) @@ -98,6 +100,10 @@ func Parse(period string) (Period, error) { return Period{}, fmt.Errorf("expected a number before the 'D' marker: %s", period) } + if len(st.pcopy) != 0 { + return Period{}, fmt.Errorf("unexpected remaining components %s: %s", st.pcopy, period) + } + result.days = weeks*7 + days //fmt.Printf("%#v\n", st) diff --git a/period/period_test.go b/period/period_test.go index 87d566bf..38ba9ea9 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -27,6 +27,9 @@ func TestParseErrors(t *testing.T) { {"PxD", "expected a number before the 'D' marker: PxD"}, {"PTxH", "expected a number before the 'H' marker: PTxH"}, {"PTxS", "expected a number before the 'S' marker: PTxS"}, + {"P1HT1M", "unexpected remaining components 1H: P1HT1M"}, + {"PT1Y", "unexpected remaining components 1Y: PT1Y"}, + {"P1S", "unexpected remaining components 1S: P1S"}, } for i, c := range cases { _, err := Parse(c.value) From be16c32794b9beb7ecde2bfaf3d6a76801f0315b Mon Sep 17 00:00:00 2001 From: Rick Beton Date: Wed, 13 May 2020 11:56:10 +0100 Subject: [PATCH 124/165] updated dependencies --- go.mod | 6 ++++-- go.sum | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index c178378e..1f3da5a7 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,10 @@ module github.com/rickb777/date require ( github.com/mattn/goveralls v0.0.2 // indirect - github.com/rickb777/plural v1.2.0 + github.com/rickb777/plural v1.2.1 + github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 // indirect golang.org/x/text v0.3.2 + golang.org/x/tools v0.0.0-20200512001501-aaeff5de670a // indirect ) -go 1.13 +go 1.14 diff --git a/go.sum b/go.sum index a9c5efed..f99953f1 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,31 @@ github.com/mattn/goveralls v0.0.2 h1:7eJB6EqsPhRVxvwEXGnqdO2sJI0PTsrWoTMXEk9/OQc github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/rickb777/plural v1.2.0 h1:5tvEc7UBCZ7l8h/2UeybSkt/uu1DQsZFOFdNevmUhlE= github.com/rickb777/plural v1.2.0/go.mod h1:UdpyWFCGbo3mvK3f/PfZOAOrkjzJlYN/sD46XNWJ+Es= +github.com/rickb777/plural v1.2.1 h1:UitRAgR70+yHFt26Tmj/F9dU9aV6UfjGXSbO1DcC9/U= +github.com/rickb777/plural v1.2.1/go.mod h1:j058+3M5QQFgcZZ2oKIOekcygoZUL8gKW5yRO14BuAw= +github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 h1:iD+PFTQwKEmbwSdwfvP5ld2WEI/g7qbdhmHJ2ASfYGs= +github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200512001501-aaeff5de670a h1:vAa2fXRLbiVN3N/xCnodIT36K4QKZQNyQFq3hQJfQ1U= +golang.org/x/tools v0.0.0-20200512001501-aaeff5de670a/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 7c75e700236d96ac7202efbafea4edac5a77536b Mon Sep 17 00:00:00 2001 From: Ali Ince Date: Tue, 16 Jun 2020 16:30:48 +0100 Subject: [PATCH 125/165] feat: add period.ParseWithNormalise --- period/parse.go | 24 +++++++++++- period/period_test.go | 86 +++++++++++++++++++++++-------------------- 2 files changed, 69 insertions(+), 41 deletions(-) diff --git a/period/parse.go b/period/parse.go index 270ae8d0..52b91f14 100644 --- a/period/parse.go +++ b/period/parse.go @@ -33,6 +33,24 @@ func MustParse(value string) Period { // are equivalent: "P0Y", "P0M", "P0W", "P0D", "PT0H", PT0M", PT0S", and "P0". // The canonical zero is "P0D". func Parse(period string) (Period, error) { + return ParseWithNormalise(period, true) +} + +// ParseWithNormalise parses strings that specify periods using ISO-8601 rules +// with an option to specify whether to normalise parsed period components. +// +// In addition, a plus or minus sign can precede the period, e.g. "-P10D" + +// The returned value is only normalised when normalise is set to `true`, and +// normalisation will convert e.g. multiple of 12 months into years so "P24M" +// is the same as "P2Y". However, this is done without loss of precision, so +// for example whole numbers of days do not contribute to the months tally +// because the number of days per month is variable. +// +// The zero value can be represented in several ways: all of the following +// are equivalent: "P0Y", "P0M", "P0W", "P0D", "PT0H", PT0M", PT0S", and "P0". +// The canonical zero is "P0D". +func ParseWithNormalise(period string, normalise bool) (Period, error) { if period == "" { return Period{}, fmt.Errorf("cannot parse a blank string as a period") } @@ -111,7 +129,11 @@ func Parse(period string) (Period, error) { return Period{}, fmt.Errorf("expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' marker: %s", period) } - return result.normalise64(true).toPeriod(), nil + if normalise { + return result.normalise64(true).toPeriod(), nil + } + + return result.toPeriod(), nil } type parseState struct { diff --git a/period/period_test.go b/period/period_test.go index 38ba9ea9..1bba4ba1 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -39,53 +39,59 @@ func TestParseErrors(t *testing.T) { } } -func TestParsePeriod(t *testing.T) { +func TestParsePeriodWithNormalise(t *testing.T) { cases := []struct { - value string - period Period + value string + normalise bool + period Period }{ - {"P0", Period{}}, - {"P0Y", Period{}}, - {"P0M", Period{}}, - {"P0D", Period{}}, - {"PT0H", Period{}}, - {"PT0M", Period{}}, - {"PT0S", Period{}}, - {"P3Y", Period{30, 0, 0, 0, 0, 0}}, - {"P6M", Period{0, 60, 0, 0, 0, 0}}, - {"P5W", Period{0, 0, 350, 0, 0, 0}}, - {"P4D", Period{0, 0, 40, 0, 0, 0}}, - {"PT12H", Period{0, 0, 0, 120, 0, 0}}, - {"PT30M", Period{0, 0, 0, 0, 300, 0}}, - {"PT25S", Period{0, 0, 0, 0, 0, 250}}, - {"PT30M67.6S", Period{0, 0, 0, 0, 310, 76}}, - {"P3Y6M5W4DT12H40M5S", Period{30, 60, 390, 120, 400, 50}}, - {"+P3Y6M5W4DT12H40M5S", Period{30, 60, 390, 120, 400, 50}}, - {"-P3Y6M5W4DT12H40M5S", Period{-30, -60, -390, -120, -400, -50}}, - {"P2.Y", Period{20, 0, 0, 0, 0, 0}}, - {"P2.5Y", Period{25, 0, 0, 0, 0, 0}}, - {"P2.15Y", Period{21, 0, 0, 0, 0, 0}}, - {"P2.125Y", Period{21, 0, 0, 0, 0, 0}}, - {"P1Y2.M", Period{10, 20, 0, 0, 0, 0}}, - {"P1Y2.5M", Period{10, 25, 0, 0, 0, 0}}, - {"P1Y2.15M", Period{10, 21, 0, 0, 0, 0}}, - {"P1Y2.125M", Period{10, 21, 0, 0, 0, 0}}, - {"P3276.7Y", Period{32767, 0, 0, 0, 0, 0}}, - {"-P3276.7Y", Period{-32767, 0, 0, 0, 0, 0}}, + {"P0", true, Period{}}, + {"P0Y", true, Period{}}, + {"P0M", true, Period{}}, + {"P0D", true, Period{}}, + {"PT0H", true, Period{}}, + {"PT0M", true, Period{}}, + {"PT0S", true, Period{}}, + {"P3Y", true, Period{30, 0, 0, 0, 0, 0}}, + {"P6M", true, Period{0, 60, 0, 0, 0, 0}}, + {"P5W", true, Period{0, 0, 350, 0, 0, 0}}, + {"P4D", true, Period{0, 0, 40, 0, 0, 0}}, + {"PT12H", true, Period{0, 0, 0, 120, 0, 0}}, + {"PT30M", true, Period{0, 0, 0, 0, 300, 0}}, + {"PT25S", true, Period{0, 0, 0, 0, 0, 250}}, + {"PT30M67.6S", true, Period{0, 0, 0, 0, 310, 76}}, + {"P3Y6M5W4DT12H40M5S", true, Period{30, 60, 390, 120, 400, 50}}, + {"+P3Y6M5W4DT12H40M5S", true, Period{30, 60, 390, 120, 400, 50}}, + {"-P3Y6M5W4DT12H40M5S", true, Period{-30, -60, -390, -120, -400, -50}}, + {"P2.Y", true, Period{20, 0, 0, 0, 0, 0}}, + {"P2.5Y", true, Period{25, 0, 0, 0, 0, 0}}, + {"P2.15Y", true, Period{21, 0, 0, 0, 0, 0}}, + {"P2.125Y", true, Period{21, 0, 0, 0, 0, 0}}, + {"P1Y2.M", true, Period{10, 20, 0, 0, 0, 0}}, + {"P1Y2.5M", true, Period{10, 25, 0, 0, 0, 0}}, + {"P1Y2.15M", true, Period{10, 21, 0, 0, 0, 0}}, + {"P1Y2.125M", true, Period{10, 21, 0, 0, 0, 0}}, + {"P3276.7Y", true, Period{32767, 0, 0, 0, 0, 0}}, + {"-P3276.7Y", true, Period{-32767, 0, 0, 0, 0, 0}}, // largest possible number of seconds normalised only in hours, mins, sec - {"PT11592000S", Period{0, 0, 0, 32200, 0, 0}}, - {"-PT11592000S", Period{0, 0, 0, -32200, 0, 0}}, - {"PT11595599S", Period{0, 0, 0, 32200, 590, 590}}, + {"PT11592000S", true, Period{0, 0, 0, 32200, 0, 0}}, + {"-PT11592000S", true, Period{0, 0, 0, -32200, 0, 0}}, + {"PT11595599S", true, Period{0, 0, 0, 32200, 590, 590}}, // largest possible number of seconds normalised only in days, hours, mins, sec - {"PT283046400S", Period{0, 0, 32760, 0, 0, 0}}, - {"-PT283046400S", Period{0, 0, -32760, 0, 0, 0}}, - {"PT283132799S", Period{0, 0, 32760, 230, 590, 590}}, + {"PT283046400S", true, Period{0, 0, 32760, 0, 0, 0}}, + {"-PT283046400S", true, Period{0, 0, -32760, 0, 0, 0}}, + {"PT283132799S", true, Period{0, 0, 32760, 230, 590, 590}}, // largest possible number of months - {"P39312M", Period{32760, 0, 0, 0, 0, 0}}, - {"-P39312M", Period{-32760, 0, 0, 0, 0, 0}}, + {"P39312M", true, Period{32760, 0, 0, 0, 0, 0}}, + {"-P39312M", true, Period{-32760, 0, 0, 0, 0, 0}}, + // without normalisation + {"P1Y14M35DT48H125M800S", false, Period{10, 140, 350, 480, 1250, 8000}}, } for i, c := range cases { - d := MustParse(c.value) + d, err := ParseWithNormalise(c.value, c.normalise) + if err != nil { + panic(err) + } if d != c.period { t.Errorf("%d: MustParse(%v) == %#v, want (%#v)", i, c.value, d, c.period) } From e076f6907d6c6ea57e214c446fc803914be22917 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Mon, 13 Jul 2020 23:06:52 +0100 Subject: [PATCH 126/165] restored earlier test to ensure full coverage --- period/period_test.go | 96 +++++++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/period/period_test.go b/period/period_test.go index 1bba4ba1..166b48d4 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -41,50 +41,66 @@ func TestParseErrors(t *testing.T) { func TestParsePeriodWithNormalise(t *testing.T) { cases := []struct { - value string - normalise bool - period Period + value string + period Period }{ - {"P0", true, Period{}}, - {"P0Y", true, Period{}}, - {"P0M", true, Period{}}, - {"P0D", true, Period{}}, - {"PT0H", true, Period{}}, - {"PT0M", true, Period{}}, - {"PT0S", true, Period{}}, - {"P3Y", true, Period{30, 0, 0, 0, 0, 0}}, - {"P6M", true, Period{0, 60, 0, 0, 0, 0}}, - {"P5W", true, Period{0, 0, 350, 0, 0, 0}}, - {"P4D", true, Period{0, 0, 40, 0, 0, 0}}, - {"PT12H", true, Period{0, 0, 0, 120, 0, 0}}, - {"PT30M", true, Period{0, 0, 0, 0, 300, 0}}, - {"PT25S", true, Period{0, 0, 0, 0, 0, 250}}, - {"PT30M67.6S", true, Period{0, 0, 0, 0, 310, 76}}, - {"P3Y6M5W4DT12H40M5S", true, Period{30, 60, 390, 120, 400, 50}}, - {"+P3Y6M5W4DT12H40M5S", true, Period{30, 60, 390, 120, 400, 50}}, - {"-P3Y6M5W4DT12H40M5S", true, Period{-30, -60, -390, -120, -400, -50}}, - {"P2.Y", true, Period{20, 0, 0, 0, 0, 0}}, - {"P2.5Y", true, Period{25, 0, 0, 0, 0, 0}}, - {"P2.15Y", true, Period{21, 0, 0, 0, 0, 0}}, - {"P2.125Y", true, Period{21, 0, 0, 0, 0, 0}}, - {"P1Y2.M", true, Period{10, 20, 0, 0, 0, 0}}, - {"P1Y2.5M", true, Period{10, 25, 0, 0, 0, 0}}, - {"P1Y2.15M", true, Period{10, 21, 0, 0, 0, 0}}, - {"P1Y2.125M", true, Period{10, 21, 0, 0, 0, 0}}, - {"P3276.7Y", true, Period{32767, 0, 0, 0, 0, 0}}, - {"-P3276.7Y", true, Period{-32767, 0, 0, 0, 0, 0}}, + {"P0", Period{}}, + {"P0Y", Period{}}, + {"P0M", Period{}}, + {"P0D", Period{}}, + {"PT0H", Period{}}, + {"PT0M", Period{}}, + {"PT0S", Period{}}, + {"P3Y", Period{30, 0, 0, 0, 0, 0}}, + {"P6M", Period{0, 60, 0, 0, 0, 0}}, + {"P5W", Period{0, 0, 350, 0, 0, 0}}, + {"P4D", Period{0, 0, 40, 0, 0, 0}}, + {"PT12H", Period{0, 0, 0, 120, 0, 0}}, + {"PT30M", Period{0, 0, 0, 0, 300, 0}}, + {"PT25S", Period{0, 0, 0, 0, 0, 250}}, + {"PT30M67.6S", Period{0, 0, 0, 0, 310, 76}}, + {"P3Y6M5W4DT12H40M5S", Period{30, 60, 390, 120, 400, 50}}, + {"+P3Y6M5W4DT12H40M5S", Period{30, 60, 390, 120, 400, 50}}, + {"-P3Y6M5W4DT12H40M5S", Period{-30, -60, -390, -120, -400, -50}}, + {"P2.Y", Period{20, 0, 0, 0, 0, 0}}, + {"P2.5Y", Period{25, 0, 0, 0, 0, 0}}, + {"P2.15Y", Period{21, 0, 0, 0, 0, 0}}, + {"P2.125Y", Period{21, 0, 0, 0, 0, 0}}, + {"P1Y2.M", Period{10, 20, 0, 0, 0, 0}}, + {"P1Y2.5M", Period{10, 25, 0, 0, 0, 0}}, + {"P1Y2.15M", Period{10, 21, 0, 0, 0, 0}}, + {"P1Y2.125M", Period{10, 21, 0, 0, 0, 0}}, + {"P3276.7Y", Period{32767, 0, 0, 0, 0, 0}}, + {"-P3276.7Y", Period{-32767, 0, 0, 0, 0, 0}}, // largest possible number of seconds normalised only in hours, mins, sec - {"PT11592000S", true, Period{0, 0, 0, 32200, 0, 0}}, - {"-PT11592000S", true, Period{0, 0, 0, -32200, 0, 0}}, - {"PT11595599S", true, Period{0, 0, 0, 32200, 590, 590}}, + {"PT11592000S", Period{0, 0, 0, 32200, 0, 0}}, + {"-PT11592000S", Period{0, 0, 0, -32200, 0, 0}}, + {"PT11595599S", Period{0, 0, 0, 32200, 590, 590}}, // largest possible number of seconds normalised only in days, hours, mins, sec - {"PT283046400S", true, Period{0, 0, 32760, 0, 0, 0}}, - {"-PT283046400S", true, Period{0, 0, -32760, 0, 0, 0}}, - {"PT283132799S", true, Period{0, 0, 32760, 230, 590, 590}}, + {"PT283046400S", Period{0, 0, 32760, 0, 0, 0}}, + {"-PT283046400S", Period{0, 0, -32760, 0, 0, 0}}, + {"PT283132799S", Period{0, 0, 32760, 230, 590, 590}}, // largest possible number of months - {"P39312M", true, Period{32760, 0, 0, 0, 0, 0}}, - {"-P39312M", true, Period{-32760, 0, 0, 0, 0, 0}}, - // without normalisation + {"P39312M", Period{32760, 0, 0, 0, 0, 0}}, + {"-P39312M", Period{-32760, 0, 0, 0, 0, 0}}, + } + for i, c := range cases { + d, err := Parse(c.value) + if err != nil { + panic(err) + } + if d != c.period { + t.Errorf("%d: MustParse(%v) == %#v, want (%#v)", i, c.value, d, c.period) + } + } +} + +func TestParsePeriodWithoutNormalise(t *testing.T) { + cases := []struct { + value string + normalise bool + period Period + }{ {"P1Y14M35DT48H125M800S", false, Period{10, 140, 350, 480, 1250, 8000}}, } for i, c := range cases { From 921b6708850ca21429e9dc5ba48cc5066ed0d36a Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Mon, 13 Jul 2020 23:30:10 +0100 Subject: [PATCH 127/165] switched test to use Gomega --- go.mod | 1 + go.sum | 38 ++++++ period/period_test.go | 276 +++++++++++++++++------------------------- 3 files changed, 148 insertions(+), 167 deletions(-) diff --git a/go.mod b/go.mod index 1f3da5a7..914d904c 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,7 @@ module github.com/rickb777/date require ( github.com/mattn/goveralls v0.0.2 // indirect + github.com/onsi/gomega v1.10.1 github.com/rickb777/plural v1.2.1 github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 // indirect golang.org/x/text v0.3.2 diff --git a/go.sum b/go.sum index f99953f1..790452aa 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,23 @@ +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/mattn/goveralls v0.0.2 h1:7eJB6EqsPhRVxvwEXGnqdO2sJI0PTsrWoTMXEk9/OQc= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/rickb777/plural v1.2.0 h1:5tvEc7UBCZ7l8h/2UeybSkt/uu1DQsZFOFdNevmUhlE= github.com/rickb777/plural v1.2.0/go.mod h1:UdpyWFCGbo3mvK3f/PfZOAOrkjzJlYN/sD46XNWJ+Es= github.com/rickb777/plural v1.2.1 h1:UitRAgR70+yHFt26Tmj/F9dU9aV6UfjGXSbO1DcC9/U= @@ -11,13 +29,21 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -30,3 +56,15 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/period/period_test.go b/period/period_test.go index 166b48d4..3c6de09b 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -5,9 +5,11 @@ package period import ( + "fmt" "testing" "time" + . "github.com/onsi/gomega" "github.com/rickb777/plural" ) @@ -16,6 +18,8 @@ var oneMonthApprox = 2629746 * time.Second // 30.436875 days var oneYearApprox = 31556952 * time.Second // 365.2425 days func TestParseErrors(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { value string expected string @@ -33,20 +37,22 @@ func TestParseErrors(t *testing.T) { } for i, c := range cases { _, err := Parse(c.value) - if err.Error() != c.expected { - t.Errorf("%d: Parse(%q) == %#v, want (%#v)", i, c.value, err.Error(), c.expected) - } + g.Expect(err.Error()).To(Equal(c.expected), info(i, c.value)) } } func TestParsePeriodWithNormalise(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { value string period Period }{ + // zeroes {"P0", Period{}}, {"P0Y", Period{}}, {"P0M", Period{}}, + {"P0W", Period{}}, {"P0D", Period{}}, {"PT0H", Period{}}, {"PT0M", Period{}}, @@ -85,17 +91,15 @@ func TestParsePeriodWithNormalise(t *testing.T) { {"-P39312M", Period{-32760, 0, 0, 0, 0, 0}}, } for i, c := range cases { - d, err := Parse(c.value) - if err != nil { - panic(err) - } - if d != c.period { - t.Errorf("%d: MustParse(%v) == %#v, want (%#v)", i, c.value, d, c.period) - } + p, err := Parse(c.value) + g.Expect(err).NotTo(HaveOccurred(), info(i, c.value)) + g.Expect(p).To(Equal(c.period), info(i, c.value)) } } func TestParsePeriodWithoutNormalise(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { value string normalise bool @@ -104,17 +108,15 @@ func TestParsePeriodWithoutNormalise(t *testing.T) { {"P1Y14M35DT48H125M800S", false, Period{10, 140, 350, 480, 1250, 8000}}, } for i, c := range cases { - d, err := ParseWithNormalise(c.value, c.normalise) - if err != nil { - panic(err) - } - if d != c.period { - t.Errorf("%d: MustParse(%v) == %#v, want (%#v)", i, c.value, d, c.period) - } + p, err := ParseWithNormalise(c.value, c.normalise) + g.Expect(err).NotTo(HaveOccurred(), info(i, c.value)) + g.Expect(p).To(Equal(c.period), info(i, c.value)) } } func TestPeriodString(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { value string period Period @@ -139,13 +141,13 @@ func TestPeriodString(t *testing.T) { } for i, c := range cases { s := c.period.String() - if s != c.value { - t.Errorf("%d: String() == %s, want %s for %+v", i, s, c.value, c.period) - } + g.Expect(s).To(Equal(c.value), info(i, c.value)) } } func TestPeriodIntComponents(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { value string y, m, w, d, dx, hh, mm, ss int @@ -170,34 +172,20 @@ func TestPeriodIntComponents(t *testing.T) { } for i, c := range cases { p := MustParse(c.value) - if p.Years() != c.y { - t.Errorf("%d: %s.Years() == %d, want %d", i, c.value, p.Years(), c.y) - } - if p.Months() != c.m { - t.Errorf("%d: %s.Months() == %d, want %d", i, c.value, p.Months(), c.m) - } - if p.Weeks() != c.w { - t.Errorf("%d: %s.Weeks() == %d, want %d", i, c.value, p.Weeks(), c.w) - } - if p.Days() != c.d { - t.Errorf("%d: %s.Days() == %d, want %d", i, c.value, p.Days(), c.d) - } - if p.ModuloDays() != c.dx { - t.Errorf("%d: %s.ModuloDays() == %d, want %d", i, c.value, p.ModuloDays(), c.dx) - } - if p.Hours() != c.hh { - t.Errorf("%d: %s.Hours() == %d, want %d", i, c.value, p.Hours(), c.hh) - } - if p.Minutes() != c.mm { - t.Errorf("%d: %s.Minutes() == %d, want %d", i, c.value, p.Minutes(), c.mm) - } - if p.Seconds() != c.ss { - t.Errorf("%d: %s.Seconds() == %d, want %d", i, c.value, p.Seconds(), c.ss) - } + g.Expect(p.Years()).To(Equal(c.y), info(i, c.value)) + g.Expect(p.Months()).To(Equal(c.m), info(i, c.value)) + g.Expect(p.Weeks()).To(Equal(c.w), info(i, c.value)) + g.Expect(p.Days()).To(Equal(c.d), info(i, c.value)) + g.Expect(p.ModuloDays()).To(Equal(c.dx), info(i, c.value)) + g.Expect(p.Hours()).To(Equal(c.hh), info(i, c.value)) + g.Expect(p.Minutes()).To(Equal(c.mm), info(i, c.value)) + g.Expect(p.Seconds()).To(Equal(c.ss), info(i, c.value)) } } func TestPeriodFloatComponents(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { value string y, m, w, d, dx, hh, mm, ss float32 @@ -231,31 +219,19 @@ func TestPeriodFloatComponents(t *testing.T) { } for i, c := range cases { p := MustParse(c.value) - if p.YearsFloat() != c.y { - t.Errorf("%d: %s.YearsFloat() == %g, want %g", i, c.value, p.YearsFloat(), c.y) - } - if p.MonthsFloat() != c.m { - t.Errorf("%d: %s.MonthsFloat() == %g, want %g", i, c.value, p.MonthsFloat(), c.m) - } - if p.WeeksFloat() != c.w { - t.Errorf("%d: %s.WeeksFloat() == %g, want %g", i, c.value, p.WeeksFloat(), c.w) - } - if p.DaysFloat() != c.d { - t.Errorf("%d: %s.DaysFloat() == %g, want %g", i, c.value, p.DaysFloat(), c.d) - } - if p.HoursFloat() != c.hh { - t.Errorf("%d: %s.HoursFloat() == %g, want %g", i, c.value, p.HoursFloat(), c.hh) - } - if p.MinutesFloat() != c.mm { - t.Errorf("%d: %s.MinutesFloat() == %g, want %g", i, c.value, p.MinutesFloat(), c.mm) - } - if p.SecondsFloat() != c.ss { - t.Errorf("%d: %s.SecondsFloat() == %g, want %g", i, c.value, p.SecondsFloat(), c.ss) - } + g.Expect(p.YearsFloat()).To(Equal(c.y), info(i, c.value)) + g.Expect(p.MonthsFloat()).To(Equal(c.m), info(i, c.value)) + g.Expect(p.WeeksFloat()).To(Equal(c.w), info(i, c.value)) + g.Expect(p.DaysFloat()).To(Equal(c.d), info(i, c.value)) + g.Expect(p.HoursFloat()).To(Equal(c.hh), info(i, c.value)) + g.Expect(p.MinutesFloat()).To(Equal(c.mm), info(i, c.value)) + g.Expect(p.SecondsFloat()).To(Equal(c.ss), info(i, c.value)) } } func TestPeriodAddToTime(t *testing.T) { + g := NewGomegaWithT(t) + const ms = 1000000 const sec = 1000 * ms const min = 60 * sec @@ -300,16 +276,14 @@ func TestPeriodAddToTime(t *testing.T) { for i, c := range cases { p := MustParse(c.value) t1, prec := p.AddTo(t0) - if !t1.Equal(c.result) { - t.Errorf("%d: %s.AddTo(t) == %s %v, want %s", i, c.value, t1, prec, c.result) - } - if prec != c.precise { - t.Errorf("%d: %s.AddTo(t) == %s %v, want %v", i, c.value, t1, prec, c.precise) - } + g.Expect(t1).To(Equal(c.result), info(i, c.value)) + g.Expect(prec).To(Equal(c.precise), info(i, c.value)) } } func TestPeriodToDuration(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { value string duration time.Duration @@ -342,20 +316,18 @@ func TestPeriodToDuration(t *testing.T) { for i, c := range cases { p := MustParse(c.value) d1, prec := p.Duration() - if d1 != c.duration { - t.Errorf("%d: Duration() == %s %v, want %s for %s", i, d1, prec, c.duration, c.value) - } - if prec != c.precise { - t.Errorf("%d: Duration() == %s %v, want %v for %s", i, d1, prec, c.precise, c.value) - } + g.Expect(d1).To(Equal(c.duration), info(i, c.value)) + g.Expect(prec).To(Equal(c.precise), info(i, c.value)) d2 := p.DurationApprox() - if c.precise && d2 != c.duration { - t.Errorf("%d: DurationApprox() == %s %v, want %s for %s", i, d2, prec, c.duration, c.value) + if c.precise { + g.Expect(d2).To(Equal(c.duration), info(i, c.value)) } } } func TestSignPotisitveNegative(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { value string positive bool @@ -378,19 +350,15 @@ func TestSignPotisitveNegative(t *testing.T) { } for i, c := range cases { p := MustParse(c.value) - if p.IsPositive() != c.positive { - t.Errorf("%d: %v.IsPositive() == %v, want %v", i, p, p.IsPositive(), c.positive) - } - if p.IsNegative() != c.negative { - t.Errorf("%d: %v.IsNegative() == %v, want %v", i, p, p.IsNegative(), c.negative) - } - if p.Sign() != c.sign { - t.Errorf("%d: %v.Sign() == %d, want %d", i, p, p.Sign(), c.sign) - } + g.Expect(p.IsPositive()).To(Equal(c.positive), info(i, c.value)) + g.Expect(p.IsNegative()).To(Equal(c.negative), info(i, c.value)) + g.Expect(p.Sign()).To(Equal(c.sign), info(i, c.value)) } } func TestPeriodApproxDays(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { value string approxDays int @@ -406,13 +374,13 @@ func TestPeriodApproxDays(t *testing.T) { for i, c := range cases { p := MustParse(c.value) td := p.TotalDaysApprox() - if td != c.approxDays { - t.Errorf("%d: %v.TotalDaysApprox() == %v, want %v", i, p, td, c.approxDays) - } + g.Expect(td).To(Equal(c.approxDays), info(i, c.value)) } } func TestPeriodApproxMonths(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { value string approxMonths int @@ -435,13 +403,13 @@ func TestPeriodApproxMonths(t *testing.T) { for i, c := range cases { p := MustParse(c.value) td := p.TotalMonthsApprox() - if td != c.approxMonths { - t.Errorf("%d: %v.TotalMonthsApprox() == %v, want %v", i, p, td, c.approxMonths) - } + g.Expect(td).To(Equal(c.approxMonths), info(i, c.value)) } } func TestNewPeriod(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { years, months, days, hours, minutes, seconds int period Period @@ -463,22 +431,16 @@ func TestNewPeriod(t *testing.T) { } for i, c := range cases { p := New(c.years, c.months, c.days, c.hours, c.minutes, c.seconds) - if p != c.period { - t.Errorf("%d: %d,%d,%d gives %#v, want %#v", i, c.years, c.months, c.days, p, c.period) - } - if p.Years() != c.years { - t.Errorf("%d: %#v, got %d want %d", i, p, p.Years(), c.years) - } - if p.Months() != c.months { - t.Errorf("%d: %#v, got %d want %d", i, p, p.Months(), c.months) - } - if p.Days() != c.days { - t.Errorf("%d: %#v, got %d want %d", i, p, p.Days(), c.days) - } + g.Expect(p).To(Equal(c.period), info(i, c.period)) + g.Expect(p.Years()).To(Equal(c.years), info(i, c.period)) + g.Expect(p.Months()).To(Equal(c.months), info(i, c.period)) + g.Expect(p.Days()).To(Equal(c.days), info(i, c.period)) } } func TestNewHMS(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { hours, minutes, seconds int period Period @@ -493,22 +455,16 @@ func TestNewHMS(t *testing.T) { } for i, c := range cases { p := NewHMS(c.hours, c.minutes, c.seconds) - if p != c.period { - t.Errorf("%d: gives %#v, want %#v", i, p, c.period) - } - if p.Hours() != c.hours { - t.Errorf("%d: %#v, got %d want %d", i, p, p.Years(), c.hours) - } - if p.Minutes() != c.minutes { - t.Errorf("%d: %#v, got %d want %d", i, p, p.Months(), c.minutes) - } - if p.Seconds() != c.seconds { - t.Errorf("%d: %#v, got %d want %d", i, p, p.Days(), c.seconds) - } + g.Expect(p).To(Equal(c.period), info(i, c.period)) + g.Expect(p.Hours()).To(Equal(c.hours), info(i, c.period)) + g.Expect(p.Minutes()).To(Equal(c.minutes), info(i, c.period)) + g.Expect(p.Seconds()).To(Equal(c.seconds), info(i, c.period)) } } func TestNewYMD(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { years, months, days int period Period @@ -524,18 +480,10 @@ func TestNewYMD(t *testing.T) { } for i, c := range cases { p := NewYMD(c.years, c.months, c.days) - if p != c.period { - t.Errorf("%d: %d,%d,%d gives %#v, want %#v", i, c.years, c.months, c.days, p, c.period) - } - if p.Years() != c.years { - t.Errorf("%d: %#v, got %d want %d", i, p, p.Years(), c.years) - } - if p.Months() != c.months { - t.Errorf("%d: %#v, got %d want %d", i, p, p.Months(), c.months) - } - if p.Days() != c.days { - t.Errorf("%d: %#v, got %d want %d", i, p, p.Days(), c.days) - } + g.Expect(p).To(Equal(c.period), info(i, c.period)) + g.Expect(p.Years()).To(Equal(c.years), info(i, c.period)) + g.Expect(p.Months()).To(Equal(c.months), info(i, c.period)) + g.Expect(p.Days()).To(Equal(c.days), info(i, c.period)) } } @@ -572,22 +520,18 @@ func testNewOf(t *testing.T, source time.Duration, expected Period, precise bool func testNewOf1(t *testing.T, source time.Duration, expected Period, precise bool) { t.Helper() - ms := time.Millisecond + g := NewGomegaWithT(t) n, p := NewOf(source) rev, _ := expected.Duration() - if n != expected { - t.Errorf("NewOf(%s) (%dms)\n gives %-20s %#v,\n want %-20s (%dms)", source, source/ms, n, n, expected, rev/ms) - } - if p != precise { - t.Errorf("NewOf(%s) (%dms)\n gives %v,\n want %v for %v (%dms)", source, source/ms, p, precise, expected, rev/ms) - } - //if rev != source { - // t.Logf("%d: NewOf(%s) input %dms differs from expected %dms", i, source, source/ms, rev/ms) - //} + info := fmt.Sprintf("%v %+v %v %v", source, expected, precise, rev) + g.Expect(n).To(Equal(expected), info) + g.Expect(p).To(Equal(precise), info) + //g.Expect(rev).To(Equal(source), info) } func TestBetween(t *testing.T) { + g := NewGomegaWithT(t) now := time.Now() cases := []struct { @@ -642,9 +586,7 @@ func TestBetween(t *testing.T) { } for i, c := range cases { n := Between(c.a, c.b) - if n != c.expected { - t.Errorf("%d: Between(%v, %v)\n gives %-20s %#v,\n want %-20s %#v", i, c.a, c.b, n, n, c.expected, c.expected) - } + g.Expect(n).To(Equal(c.expected), info(i, c.expected)) } } @@ -702,6 +644,7 @@ func testNormalise(t *testing.T, source, precise, approx Period) { } func testNormaliseBothSigns(t *testing.T, source, expected Period, precise bool) { + g := NewGomegaWithT(t) t.Helper() n1 := source.Normalise(precise) @@ -715,15 +658,12 @@ func testNormaliseBothSigns(t *testing.T, source, expected Period, precise bool) sneg := source.Negate() eneg := expected.Negate() n2 := sneg.Normalise(precise) - if n2 != eneg { - t.Errorf("%v.Normalise(%v) %s\n gives %-22s %#v %s,\n want %-22s %#v %s", - sneg, precise, sneg.DurationApprox(), - n2, n2, n2.DurationApprox(), - eneg, eneg, eneg.DurationApprox()) - } + g.Expect(n2).To(Equal(eneg)) } func TestPeriodFormat(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { period string expect string @@ -755,13 +695,13 @@ func TestPeriodFormat(t *testing.T) { } for i, c := range cases { s := MustParse(c.period).Format() - if s != c.expect { - t.Errorf("%d: Format() == %s, want %s for %+v", i, s, c.expect, c.period) - } + g.Expect(s).To(Equal(c.expect), info(i, c.expect)) } } func TestPeriodScale(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { one string m float32 @@ -800,13 +740,13 @@ func TestPeriodScale(t *testing.T) { } for i, c := range cases { s := MustParse(c.one).Scale(c.m) - if s != MustParse(c.expect) { - t.Errorf("%d: %s.Scale(%g) == %v, want %s", i, c.one, c.m, s, c.expect) - } + g.Expect(s).To(Equal(MustParse(c.expect)), info(i, c.expect)) } } func TestPeriodAdd(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { one, two string expect string @@ -823,13 +763,13 @@ func TestPeriodAdd(t *testing.T) { } for i, c := range cases { s := MustParse(c.one).Add(MustParse(c.two)) - if s != MustParse(c.expect) { - t.Errorf("%d: %s.Add(%s) == %v, want %s", i, c.one, c.two, s, c.expect) - } + g.Expect(s).To(Equal(MustParse(c.expect)), info(i, c.expect)) } } func TestPeriodFormatWithoutWeeks(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { period string expect string @@ -858,13 +798,13 @@ func TestPeriodFormatWithoutWeeks(t *testing.T) { for i, c := range cases { s := MustParse(c.period).FormatWithPeriodNames(PeriodYearNames, PeriodMonthNames, plural.Plurals{}, PeriodDayNames, PeriodHourNames, PeriodMinuteNames, PeriodSecondNames) - if s != c.expect { - t.Errorf("%d: Format() == %s, want %s for %+v", i, s, c.expect, c.period) - } + g.Expect(s).To(Equal(c.expect), info(i, c.expect)) } } func TestPeriodParseOnlyYMD(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { one string expect string @@ -874,13 +814,13 @@ func TestPeriodParseOnlyYMD(t *testing.T) { } for i, c := range cases { s := MustParse(c.one).OnlyYMD() - if s != MustParse(c.expect) { - t.Errorf("%d: %s.OnlyYMD() == %v, want %s", i, c.one, s, c.expect) - } + g.Expect(s).To(Equal(MustParse(c.expect)), info(i, c.expect)) } } func TestPeriodParseOnlyHMS(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { one string expect string @@ -890,9 +830,7 @@ func TestPeriodParseOnlyHMS(t *testing.T) { } for i, c := range cases { s := MustParse(c.one).OnlyHMS() - if s != MustParse(c.expect) { - t.Errorf("%d: %s.OnlyHMS() == %v, want %s", i, c.one, s, c.expect) - } + g.Expect(s).To(Equal(MustParse(c.expect)), info(i, c.expect)) } } @@ -909,3 +847,7 @@ var london *time.Location // UTC + 1 hour during summer func init() { london, _ = time.LoadLocation("Europe/London") } + +func info(i int, m interface{}) string { + return fmt.Sprintf("%d %v", i, m) +} From 8f0971214be1bc3c27a70c4c026268d61f577153 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Mon, 17 Aug 2020 10:17:54 +0100 Subject: [PATCH 128/165] documentation additions --- date.go | 16 +++++++++++----- gregorian/doc.go | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/date.go b/date.go index 0aa43db5..8c776f33 100644 --- a/date.go +++ b/date.go @@ -35,13 +35,19 @@ const ZeroDays PeriodOfDays = 0 // // Date values can be compared using the Before, After, and Equal methods // as well as the == and != operators. +// // The Sub method subtracts two dates, returning the number of days between -// them. -// The Add method adds a Date and a number of days, producing a Date. +// them. The Add method adds a Date and a number of days, producing a Date. +// +// The zero value of type Date is Thursday, January 1, 1970 (called 'the +// epoch'), based on Unix convention. As this date is unlikely to come up in +// practice, the IsZero method gives a simple way of detecting a date that +// has not been initialized explicitly. // -// The zero value of type Date is Thursday, January 1, 1970 (called 'the epoch'). -// As this date is unlikely to come up in practice, the IsZero method gives -// a simple way of detecting a date that has not been initialized explicitly. +// The first official date of the Gregorian calendar was Friday, October 15th +// 1582, quite unrelated to the epoch used here. The Date type does not +// distinguish between official Gregorian dates and earlier proleptic dates, +// which can also be represented when needed. // type Date struct { day PeriodOfDays // day gives the number of days elapsed since date zero. diff --git a/gregorian/doc.go b/gregorian/doc.go index 7c657b0f..d4c85412 100644 --- a/gregorian/doc.go +++ b/gregorian/doc.go @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. // Package gregorian provides utility functions for the Gregorian calendar calculations. -// The Gregorian calendar was officially introduced in October 1582 so, strictly speaking, +// The Gregorian calendar was officially introduced on 15th October 1582 so, strictly speaking, // it only applies after that date. Some countries did not switch to the Gregorian calendar // for many years after (such as Great Britain in 1782). // From 6b66972b7ef2cb8830df566a59944ab2c5231819 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Mon, 17 Aug 2020 10:22:13 +0100 Subject: [PATCH 129/165] updated dependencies; now using go 1.15 --- go.mod | 6 ++---- go.sum | 31 ++++++++++++------------------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index 914d904c..3d70ce8e 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,10 @@ module github.com/rickb777/date require ( - github.com/mattn/goveralls v0.0.2 // indirect github.com/onsi/gomega v1.10.1 github.com/rickb777/plural v1.2.1 - github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 // indirect + golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect golang.org/x/text v0.3.2 - golang.org/x/tools v0.0.0-20200512001501-aaeff5de670a // indirect ) -go 1.14 +go 1.15 diff --git a/go.sum b/go.sum index 790452aa..418bb253 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -5,55 +6,44 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/mattn/goveralls v0.0.2 h1:7eJB6EqsPhRVxvwEXGnqdO2sJI0PTsrWoTMXEk9/OQc= -github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/rickb777/plural v1.2.0 h1:5tvEc7UBCZ7l8h/2UeybSkt/uu1DQsZFOFdNevmUhlE= -github.com/rickb777/plural v1.2.0/go.mod h1:UdpyWFCGbo3mvK3f/PfZOAOrkjzJlYN/sD46XNWJ+Es= github.com/rickb777/plural v1.2.1 h1:UitRAgR70+yHFt26Tmj/F9dU9aV6UfjGXSbO1DcC9/U= github.com/rickb777/plural v1.2.1/go.mod h1:j058+3M5QQFgcZZ2oKIOekcygoZUL8gKW5yRO14BuAw= -github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 h1:iD+PFTQwKEmbwSdwfvP5ld2WEI/g7qbdhmHJ2ASfYGs= -github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200512001501-aaeff5de670a h1:vAa2fXRLbiVN3N/xCnodIT36K4QKZQNyQFq3hQJfQ1U= -golang.org/x/tools v0.0.0-20200512001501-aaeff5de670a/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -61,9 +51,12 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= From 19cdf331433f59d9fb7e0e2325c0d68ecd51f805 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 19 Aug 2020 09:50:59 +0100 Subject: [PATCH 130/165] period: upgraded tests to Gomega --- period/marshal_test.go | 73 +++--- period/period_test.go | 583 ++++++++++++++++++++++++----------------- 2 files changed, 376 insertions(+), 280 deletions(-) diff --git a/period/marshal_test.go b/period/marshal_test.go index 240719ac..37908b57 100644 --- a/period/marshal_test.go +++ b/period/marshal_test.go @@ -9,9 +9,13 @@ import ( "encoding/gob" "encoding/json" "testing" + + . "github.com/onsi/gomega" ) func TestGobEncoding(t *testing.T) { + g := NewGomegaWithT(t) + var b bytes.Buffer encoder := gob.NewEncoder(&b) decoder := gob.NewDecoder(&b) @@ -29,24 +33,22 @@ func TestGobEncoding(t *testing.T) { "P2Y3M4W5DT1H7M9S", "-P2Y3M4W5DT1H7M9S", } - for _, c := range cases { + for i, c := range cases { period := MustParse(c) var p Period err := encoder.Encode(&period) - if err != nil { - t.Errorf("Gob(%v) encode error %v", c, err) - } else { + g.Expect(err).NotTo(HaveOccurred(), info(i, c)) + if err == nil { err = decoder.Decode(&p) - if err != nil { - t.Errorf("Gob(%v) decode error %v", c, err) - } else if p != period { - t.Errorf("Gob(%v) decode got %v", c, p) - } + g.Expect(err).NotTo(HaveOccurred(), info(i, c)) + g.Expect(p).To(Equal(period), info(i, c)) } } } func TestPeriodJSONMarshalling(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { value Period want string @@ -61,25 +63,22 @@ func TestPeriodJSONMarshalling(t *testing.T) { {New(0, 1, 0, 0, 0, 0), `"P1M"`}, {New(1, 0, 0, 0, 0, 0), `"P1Y"`}, } - for _, c := range cases { + for i, c := range cases { var p Period - bytes, err := json.Marshal(c.value) - if err != nil { - t.Errorf("JSON(%v) marshal error %v", c, err) - } else if string(bytes) != c.want { - t.Errorf("JSON(%v) == %v, want %v", c.value, string(bytes), c.want) - } else { - err = json.Unmarshal(bytes, &p) - if err != nil { - t.Errorf("JSON(%v) unmarshal error %v", c.value, err) - } else if p != c.value { - t.Errorf("JSON(%v) unmarshal got %v", c.value, p) - } + bb, err := json.Marshal(c.value) + g.Expect(err).NotTo(HaveOccurred(), info(i, c)) + g.Expect(string(bb)).To(Equal(c.want), info(i, c)) + if string(bb) == c.want { + err = json.Unmarshal(bb, &p) + g.Expect(err).NotTo(HaveOccurred(), info(i, c)) + g.Expect(p).To(Equal(c.value), info(i, c)) } } } func TestPeriodTextMarshalling(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { value Period want string @@ -94,25 +93,22 @@ func TestPeriodTextMarshalling(t *testing.T) { {New(0, 1, 0, 0, 0, 0), "P1M"}, {New(1, 0, 0, 0, 0, 0), "P1Y"}, } - for _, c := range cases { + for i, c := range cases { var p Period - bytes, err := c.value.MarshalText() - if err != nil { - t.Errorf("Text(%v) marshal error %v", c, err) - } else if string(bytes) != c.want { - t.Errorf("Text(%v) == %v, want %v", c.value, string(bytes), c.want) - } else { - err = p.UnmarshalText(bytes) - if err != nil { - t.Errorf("Text(%v) unmarshal error %v", c.value, err) - } else if p != c.value { - t.Errorf("Text(%v) unmarshal got %v", c.value, p) - } + bb, err := c.value.MarshalText() + g.Expect(err).NotTo(HaveOccurred(), info(i, c)) + g.Expect(string(bb)).To(Equal(c.want), info(i, c)) + if string(bb) == c.want { + err = p.UnmarshalText(bb) + g.Expect(err).NotTo(HaveOccurred(), info(i, c)) + g.Expect(p).To(Equal(c.value), info(i, c)) } } } func TestInvalidPeriodText(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { value string want string @@ -121,11 +117,10 @@ func TestInvalidPeriodText(t *testing.T) { {`not-a-period`, `expected 'P' period mark at the start: not-a-period`}, {`P000`, `unexpected remaining components 000: P000`}, } - for _, c := range cases { + for i, c := range cases { var p Period err := p.UnmarshalText([]byte(c.value)) - if err == nil || err.Error() != c.want { - t.Errorf("InvalidText(%v) == %v, want %v", c.value, err, c.want) - } + g.Expect(err).To(HaveOccurred(), info(i, c)) + g.Expect(err.Error()).To(Equal(c.want), info(i, c)) } } diff --git a/period/period_test.go b/period/period_test.go index 3c6de09b..d64617df 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -21,22 +21,35 @@ func TestParseErrors(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { - value string - expected string + value string + normalise bool + expected string }{ - {"", "cannot parse a blank string as a period"}, - {"XY", "expected 'P' period mark at the start: XY"}, - {"PxY", "expected a number before the 'Y' marker: PxY"}, - {"PxW", "expected a number before the 'W' marker: PxW"}, - {"PxD", "expected a number before the 'D' marker: PxD"}, - {"PTxH", "expected a number before the 'H' marker: PTxH"}, - {"PTxS", "expected a number before the 'S' marker: PTxS"}, - {"P1HT1M", "unexpected remaining components 1H: P1HT1M"}, - {"PT1Y", "unexpected remaining components 1Y: PT1Y"}, - {"P1S", "unexpected remaining components 1S: P1S"}, + {"", false, "cannot parse a blank string as a period"}, + {"XY", false, "expected 'P' period mark at the start: XY"}, + {"PxY", false, "expected a number before the 'Y' marker: PxY"}, + {"PxW", false, "expected a number before the 'W' marker: PxW"}, + {"PxD", false, "expected a number before the 'D' marker: PxD"}, + {"PTxH", false, "expected a number before the 'H' marker: PTxH"}, + {"PTxM", false, "expected a number before the 'M' marker: PTxM"}, + {"PTxS", false, "expected a number before the 'S' marker: PTxS"}, + {"P1HT1M", false, "unexpected remaining components 1H: P1HT1M"}, + {"PT1Y", false, "unexpected remaining components 1Y: PT1Y"}, + {"P1S", false, "unexpected remaining components 1S: P1S"}, + // integer overflow + //{"PT103412160000S", false, "integer overflow occurred in seconds: PT103412160000S"}, + //{"PT43084443591S", false, "integer overflow occurred in seconds: PT43084443591S"}, + //{"P32768Y", false, "integer overflow occurred in years: P32768Y"}, + //{"P32768M", false, "integer overflow occurred in months: P32768M"}, + //{"P32768D", false, "integer overflow occurred in days: P32768D"}, + //{"PT32768H", false, "integer overflow occurred in hours: PT32768H"}, + //{"PT32768M", false, "integer overflow occurred in minutes: PT32768M"}, + //{"PT32768S", false, "integer overflow occurred in seconds: PT32768S"}, + //{"PT32768H32768M32768S", false, "integer overflow occurred in hours,minutes,seconds: PT32768H32768M32768S"}, } for i, c := range cases { - _, err := Parse(c.value) + _, err := ParseWithNormalise(c.value, c.normalise) + g.Expect(err).To(HaveOccurred(), info(i, c.value)) g.Expect(err.Error()).To(Equal(c.expected), info(i, c.value)) } } @@ -45,55 +58,34 @@ func TestParsePeriodWithNormalise(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { - value string - period Period + value string + reversed string + period Period }{ - // zeroes - {"P0", Period{}}, - {"P0Y", Period{}}, - {"P0M", Period{}}, - {"P0W", Period{}}, - {"P0D", Period{}}, - {"PT0H", Period{}}, - {"PT0M", Period{}}, - {"PT0S", Period{}}, - {"P3Y", Period{30, 0, 0, 0, 0, 0}}, - {"P6M", Period{0, 60, 0, 0, 0, 0}}, - {"P5W", Period{0, 0, 350, 0, 0, 0}}, - {"P4D", Period{0, 0, 40, 0, 0, 0}}, - {"PT12H", Period{0, 0, 0, 120, 0, 0}}, - {"PT30M", Period{0, 0, 0, 0, 300, 0}}, - {"PT25S", Period{0, 0, 0, 0, 0, 250}}, - {"PT30M67.6S", Period{0, 0, 0, 0, 310, 76}}, - {"P3Y6M5W4DT12H40M5S", Period{30, 60, 390, 120, 400, 50}}, - {"+P3Y6M5W4DT12H40M5S", Period{30, 60, 390, 120, 400, 50}}, - {"-P3Y6M5W4DT12H40M5S", Period{-30, -60, -390, -120, -400, -50}}, - {"P2.Y", Period{20, 0, 0, 0, 0, 0}}, - {"P2.5Y", Period{25, 0, 0, 0, 0, 0}}, - {"P2.15Y", Period{21, 0, 0, 0, 0, 0}}, - {"P2.125Y", Period{21, 0, 0, 0, 0, 0}}, - {"P1Y2.M", Period{10, 20, 0, 0, 0, 0}}, - {"P1Y2.5M", Period{10, 25, 0, 0, 0, 0}}, - {"P1Y2.15M", Period{10, 21, 0, 0, 0, 0}}, - {"P1Y2.125M", Period{10, 21, 0, 0, 0, 0}}, - {"P3276.7Y", Period{32767, 0, 0, 0, 0, 0}}, - {"-P3276.7Y", Period{-32767, 0, 0, 0, 0, 0}}, + // all rollovers + {"PT1234.5S", "PT20M34.5S", Period{minutes: 200, seconds: 345}}, + {"PT1234.5M", "PT20H34.5M", Period{hours: 200, minutes: 345}}, + {"PT12345.6H", "P514DT9.6H", Period{days: 5140, hours: 96}}, + {"P3276.1D", "P8Y11M19.2D", Period{years: 80, months: 110, days: 192}}, + {"P1234.5M", "P102Y10.5M", Period{years: 1020, months: 105}}, // largest possible number of seconds normalised only in hours, mins, sec - {"PT11592000S", Period{0, 0, 0, 32200, 0, 0}}, - {"-PT11592000S", Period{0, 0, 0, -32200, 0, 0}}, - {"PT11595599S", Period{0, 0, 0, 32200, 590, 590}}, + {"PT11592000S", "PT3220H", Period{hours: 32200}}, + {"-PT11592000S", "-PT3220H", Period{hours: -32200}}, + {"PT11595599S", "PT3220H59M59S", Period{hours: 32200, minutes: 590, seconds: 590}}, // largest possible number of seconds normalised only in days, hours, mins, sec - {"PT283046400S", Period{0, 0, 32760, 0, 0, 0}}, - {"-PT283046400S", Period{0, 0, -32760, 0, 0, 0}}, - {"PT283132799S", Period{0, 0, 32760, 230, 590, 590}}, - // largest possible number of months - {"P39312M", Period{32760, 0, 0, 0, 0, 0}}, - {"-P39312M", Period{-32760, 0, 0, 0, 0, 0}}, + {"PT283046400S", "P468W", Period{days: 32760}}, + {"-PT283046400S", "-P468W", Period{days: -32760}}, + {"PT43084443590S", "P1365Y3M2WT26H83M50S", Period{years: 13650, months: 30, days: 140, hours: 260, minutes: 830, seconds: 500}}, + {"PT103412159999S", "P3276Y11M29DT37H83M59S", Period{years: 32760, months: 110, days: 290, hours: 370, minutes: 830, seconds: 590}}, + {"PT283132799S", "P468WT23H59M59S", Period{days: 32760, hours: 230, minutes: 590, seconds: 590}}, + // other examples are in TestNormalise } for i, c := range cases { p, err := Parse(c.value) g.Expect(err).NotTo(HaveOccurred(), info(i, c.value)) g.Expect(p).To(Equal(c.period), info(i, c.value)) + // reversal is expected not to be an identity + g.Expect(p.String()).To(Equal(c.reversed), info(i, c.value)+" reversed") } } @@ -101,16 +93,82 @@ func TestParsePeriodWithoutNormalise(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { - value string - normalise bool - period Period + value string + reversed string + period Period }{ - {"P1Y14M35DT48H125M800S", false, Period{10, 140, 350, 480, 1250, 8000}}, + // zero + {"P0D", "P0D", Period{}}, + // special zero cases: parse is not identity when reversed + {"P0", "P0D", Period{}}, + {"P0Y", "P0D", Period{}}, + {"P0M", "P0D", Period{}}, + {"P0W", "P0D", Period{}}, + {"PT0H", "P0D", Period{}}, + {"PT0M", "P0D", Period{}}, + {"PT0S", "P0D", Period{}}, + // ones + {"P1Y", "P1Y", Period{years: 10}}, + {"P1M", "P1M", Period{months: 10}}, + {"P1W", "P1W", Period{days: 70}}, + {"P1D", "P1D", Period{days: 10}}, + {"PT1H", "PT1H", Period{hours: 10}}, + {"PT1M", "PT1M", Period{minutes: 10}}, + {"PT1S", "PT1S", Period{seconds: 10}}, + // smallest + {"P0.1Y", "P0.1Y", Period{years: 1}}, + {"-P0.1Y", "-P0.1Y", Period{years: -1}}, + {"P0.1M", "P0.1M", Period{months: 1}}, + {"-P0.1M", "-P0.1M", Period{months: -1}}, + {"P0.1D", "P0.1D", Period{days: 1}}, + {"-P0.1D", "-P0.1D", Period{days: -1}}, + {"PT0.1H", "PT0.1H", Period{hours: 1}}, + {"-PT0.1H", "-PT0.1H", Period{hours: -1}}, + {"PT0.1M", "PT0.1M", Period{minutes: 1}}, + {"-PT0.1M", "-PT0.1M", Period{minutes: -1}}, + {"PT0.1S", "PT0.1S", Period{seconds: 1}}, + {"-PT0.1S", "-PT0.1S", Period{seconds: -1}}, + // week special case: also not identity when reversed + {"P0.1W", "P0.7D", Period{days: 7}}, + {"-P0.1W", "-P0.7D", Period{days: -7}}, + // largest + {"PT3276.7S", "PT3276.7S", Period{seconds: 32767}}, + {"PT3276.7M", "PT3276.7M", Period{minutes: 32767}}, + {"PT3276.7H", "PT3276.7H", Period{hours: 32767}}, + {"P3276.7D", "P3276.7D", Period{days: 32767}}, + {"P3276.7M", "P3276.7M", Period{months: 32767}}, + {"P3276.7Y", "P3276.7Y", Period{years: 32767}}, + + {"P3Y", "P3Y", Period{years: 30}}, + {"P6M", "P6M", Period{months: 60}}, + {"P5W", "P5W", Period{days: 350}}, + {"P4D", "P4D", Period{days: 40}}, + {"PT12H", "PT12H", Period{hours: 120}}, + {"PT30M", "PT30M", Period{minutes: 300}}, + {"PT25S", "PT25S", Period{seconds: 250}}, + {"PT30M67.6S", "PT30M67.6S", Period{minutes: 300, seconds: 676}}, + {"P2.Y", "P2Y", Period{years: 20}}, + {"P2.5Y", "P2.5Y", Period{years: 25}}, + {"P2.15Y", "P2.1Y", Period{years: 21}}, + {"P2.125Y", "P2.1Y", Period{years: 21}}, + {"P1Y2.M", "P1Y2M", Period{years: 10, months: 20}}, + {"P1Y2.5M", "P1Y2.5M", Period{years: 10, months: 25}}, + {"P1Y2.15M", "P1Y2.1M", Period{years: 10, months: 21}}, + {"P1Y2.125M", "P1Y2.1M", Period{years: 10, months: 21}}, + {"P3276.7Y", "P3276.7Y", Period{years: 32767}}, + {"-P3276.7Y", "-P3276.7Y", Period{years: -32767}}, + // others + {"P3Y6M5W4DT12H40M5S", "P3Y6M39DT12H40M5S", Period{years: 30, months: 60, days: 390, hours: 120, minutes: 400, seconds: 50}}, + {"+P3Y6M5W4DT12H40M5S", "P3Y6M39DT12H40M5S", Period{years: 30, months: 60, days: 390, hours: 120, minutes: 400, seconds: 50}}, + {"-P3Y6M5W4DT12H40M5S", "-P3Y6M39DT12H40M5S", Period{years: -30, months: -60, days: -390, hours: -120, minutes: -400, seconds: -50}}, + {"P1Y14M35DT48H125M800S", "P1Y14M5WT48H125M800S", Period{years: 10, months: 140, days: 350, hours: 480, minutes: 1250, seconds: 8000}}, } for i, c := range cases { - p, err := ParseWithNormalise(c.value, c.normalise) + p, err := ParseWithNormalise(c.value, false) g.Expect(err).NotTo(HaveOccurred(), info(i, c.value)) g.Expect(p).To(Equal(c.period), info(i, c.value)) + // reversal is usually expected to be an identity + g.Expect(p.String()).To(Equal(c.reversed), info(i, c.value)+" reversed") } } @@ -122,22 +180,47 @@ func TestPeriodString(t *testing.T) { period Period }{ {"P0D", Period{}}, - {"P3Y", Period{30, 0, 0, 0, 0, 0}}, - {"-P3Y", Period{-30, 0, 0, 0, 0, 0}}, - {"P6M", Period{0, 60, 0, 0, 0, 0}}, - {"-P6M", Period{0, -60, 0, 0, 0, 0}}, - {"P5W", Period{0, 0, 350, 0, 0, 0}}, - {"-P5W", Period{0, 0, -350, 0, 0, 0}}, - {"P4W", Period{0, 0, 280, 0, 0, 0}}, - {"-P4W", Period{0, 0, -280, 0, 0, 0}}, - {"P4D", Period{0, 0, 40, 0, 0, 0}}, - {"-P4D", Period{0, 0, -40, 0, 0, 0}}, - {"PT12H", Period{0, 0, 0, 120, 0, 0}}, - {"PT30M", Period{0, 0, 0, 0, 300, 0}}, - {"PT5S", Period{0, 0, 0, 0, 0, 50}}, - {"P3Y6M39DT1H2M4S", Period{30, 60, 390, 10, 20, 40}}, - {"-P3Y6M39DT1H2M4S", Period{-30, -60, -390, 10, 20, 40}}, - {"P2.5Y", Period{25, 0, 0, 0, 0, 0}}, + // ones + {"P1Y", Period{years: 10}}, + {"P1M", Period{months: 10}}, + {"P1W", Period{days: 70}}, + {"P1D", Period{days: 10}}, + {"PT1H", Period{hours: 10}}, + {"PT1M", Period{minutes: 10}}, + {"PT1S", Period{seconds: 10}}, + // smallest + {"P0.1Y", Period{years: 1}}, + {"P0.1M", Period{months: 1}}, + {"P0.7D", Period{days: 7}}, + {"P0.1D", Period{days: 1}}, + {"PT0.1H", Period{hours: 1}}, + {"PT0.1M", Period{minutes: 1}}, + {"PT0.1S", Period{seconds: 1}}, + // negative + {"-P0.1Y", Period{years: -1}}, + {"-P0.1M", Period{months: -1}}, + {"-P0.7D", Period{days: -7}}, + {"-P0.1D", Period{days: -1}}, + {"-PT0.1H", Period{hours: -1}}, + {"-PT0.1M", Period{minutes: -1}}, + {"-PT0.1S", Period{seconds: -1}}, + + {"P3Y", Period{years: 30}}, + {"-P3Y", Period{years: -30}}, + {"P6M", Period{months: 60}}, + {"-P6M", Period{months: -60}}, + {"P5W", Period{days: 350}}, + {"-P5W", Period{days: -350}}, + {"P4W", Period{days: 280}}, + {"-P4W", Period{days: -280}}, + {"P4D", Period{days: 40}}, + {"-P4D", Period{days: -40}}, + {"PT12H", Period{hours: 120}}, + {"PT30M", Period{minutes: 300}}, + {"PT5S", Period{seconds: 50}}, + {"P3Y6M39DT1H2M4S", Period{years: 30, months: 60, days: 390, hours: 10, minutes: 20, seconds: 40}}, + {"-P3Y6M39DT1H2M4S", Period{years: -30, months: -60, days: -390, hours: -10, minutes: -20, seconds: -40}}, + {"P2.5Y", Period{years: 25}}, } for i, c := range cases { s := c.period.String() @@ -152,23 +235,23 @@ func TestPeriodIntComponents(t *testing.T) { value string y, m, w, d, dx, hh, mm, ss int }{ - {"P0D", 0, 0, 0, 0, 0, 0, 0, 0}, - {"P1Y", 1, 0, 0, 0, 0, 0, 0, 0}, - {"-P1Y", -1, 0, 0, 0, 0, 0, 0, 0}, - {"P1W", 0, 0, 1, 7, 0, 0, 0, 0}, - {"-P1W", 0, 0, -1, -7, 0, 0, 0, 0}, - {"P6M", 0, 6, 0, 0, 0, 0, 0, 0}, - {"-P6M", 0, -6, 0, 0, 0, 0, 0, 0}, - {"P12M", 1, 0, 0, 0, 0, 0, 0, 0}, - {"-P12M", -1, -0, 0, 0, 0, 0, 0, 0}, - {"P39D", 0, 0, 5, 39, 4, 0, 0, 0}, - {"-P39D", 0, 0, -5, -39, -4, 0, 0, 0}, - {"P4D", 0, 0, 0, 4, 4, 0, 0, 0}, - {"-P4D", 0, 0, 0, -4, -4, 0, 0, 0}, - {"PT12H", 0, 0, 0, 0, 0, 12, 0, 0}, - {"PT60M", 0, 0, 0, 0, 0, 1, 0, 0}, - {"PT30M", 0, 0, 0, 0, 0, 0, 30, 0}, - {"PT5S", 0, 0, 0, 0, 0, 0, 0, 5}, + {value: "P0D"}, + {value: "P1Y", y: 1}, + {value: "-P1Y", y: -1}, + {value: "P1W", w: 1, d: 7}, + {value: "-P1W", w: -1, d: -7}, + {value: "P6M", m: 6}, + {value: "-P6M", m: -6}, + {value: "P12M", y: 1}, + {value: "-P12M", y: -1, m: -0}, + {value: "P39D", w: 5, d: 39, dx: 4}, + {value: "-P39D", w: -5, d: -39, dx: -4}, + {value: "P4D", d: 4, dx: 4}, + {value: "-P4D", d: -4, dx: -4}, + {value: "PT12H", hh: 12}, + {value: "PT60M", hh: 1}, + {value: "PT30M", mm: 30}, + {value: "PT5S", ss: 5}, } for i, c := range cases { p := MustParse(c.value) @@ -187,45 +270,55 @@ func TestPeriodFloatComponents(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { - value string + value Period y, m, w, d, dx, hh, mm, ss float32 }{ - {"P0D", 0, 0, 0, 0, 0, 0, 0, 0}, + // note: the negative cases are also covered (see below) + + {}, // zero case // YMD cases - {"P1Y", 1, 0, 0, 0, 0, 0, 0, 0}, - {"P1.1Y", 1.1, 0, 0, 0, 0, 0, 0, 0}, - {"-P1Y", -1, 0, 0, 0, 0, 0, 0, 0}, - {"P1W", 0, 0, 1, 7, 0, 0, 0, 0}, - {"P1.1W", 0, 0, 1.1, 7.7, 0, 0, 0, 0}, - {"-P1W", 0, 0, -1, -7, 0, 0, 0, 0}, - {"P1.1M", 0, 1.1, 0, 0, 0, 0, 0, 0}, - {"P6M", 0, 6, 0, 0, 0, 0, 0, 0}, - {"-P6M", 0, -6, 0, 0, 0, 0, 0, 0}, - {"P12M", 1, 0, 0, 0, 0, 0, 0, 0}, - {"-P12M", -1, 0, 0, 0, 0, 0, 0, 0}, - {"P39D", 0, 0, 5.571429, 39, 4, 0, 0, 0}, - {"-P39D", 0, 0, -5.571429, -39, -4, 0, 0, 0}, - {"P4D", 0, 0, 0.5714286, 4, 4, 0, 0, 0}, - {"-P4D", 0, 0, -0.5714286, -4, -4, 0, 0, 0}, + {value: Period{years: 10}, y: 1}, + {value: Period{years: 15}, y: 1.5}, + {value: Period{months: 10}, m: 1}, + {value: Period{months: 15}, m: 1.5}, + {value: Period{months: 60}, m: 6}, + {value: Period{months: 120}, m: 12}, + {value: Period{days: 70}, w: 1, d: 7}, + {value: Period{days: 77}, w: 1.1, d: 7.7}, + {value: Period{days: 10}, w: 1.0 / 7, d: 1}, + {value: Period{days: 11}, w: 1.1 / 7, d: 1.1}, + {value: Period{days: 390}, w: 5.571429, d: 39, dx: 4}, + {value: Period{days: 40}, w: 0.5714286, d: 4, dx: 4}, // HMS cases - {"PT1.1H", 0, 0, 0, 0, 0, 1.1, 0, 0}, - {"PT12H", 0, 0, 0, 0, 0, 12, 0, 0}, - {"PT1.1M", 0, 0, 0, 0, 0, 0, 1.1, 0}, - {"PT30M", 0, 0, 0, 0, 0, 0, 30, 0}, - {"PT1.1S", 0, 0, 0, 0, 0, 0, 0, 1.1}, - {"PT5S", 0, 0, 0, 0, 0, 0, 0, 5}, + {value: Period{hours: 11}, hh: 1.1}, + {value: Period{hours: 10, minutes: 60}, hh: 1, mm: 6}, + {value: Period{hours: 120}, hh: 12}, + {value: Period{minutes: 11}, mm: 1.1}, + {value: Period{minutes: 10, seconds: 60}, mm: 1, ss: 6}, + {value: Period{minutes: 300}, mm: 30}, + {value: Period{seconds: 11}, ss: 1.1}, + {value: Period{seconds: 50}, ss: 5}, } for i, c := range cases { - p := MustParse(c.value) - g.Expect(p.YearsFloat()).To(Equal(c.y), info(i, c.value)) - g.Expect(p.MonthsFloat()).To(Equal(c.m), info(i, c.value)) - g.Expect(p.WeeksFloat()).To(Equal(c.w), info(i, c.value)) - g.Expect(p.DaysFloat()).To(Equal(c.d), info(i, c.value)) - g.Expect(p.HoursFloat()).To(Equal(c.hh), info(i, c.value)) - g.Expect(p.MinutesFloat()).To(Equal(c.mm), info(i, c.value)) - g.Expect(p.SecondsFloat()).To(Equal(c.ss), info(i, c.value)) + pp := c.value + g.Expect(pp.YearsFloat()).To(Equal(c.y), info(i, c.value)) + g.Expect(pp.MonthsFloat()).To(Equal(c.m), info(i, c.value)) + g.Expect(pp.WeeksFloat()).To(Equal(c.w), info(i, c.value)) + g.Expect(pp.DaysFloat()).To(Equal(c.d), info(i, c.value)) + g.Expect(pp.HoursFloat()).To(Equal(c.hh), info(i, c.value)) + g.Expect(pp.MinutesFloat()).To(Equal(c.mm), info(i, c.value)) + g.Expect(pp.SecondsFloat()).To(Equal(c.ss), info(i, c.value)) + + pn := c.value.Negate() + g.Expect(pn.YearsFloat()).To(Equal(-c.y), info(i, c.value)) + g.Expect(pn.MonthsFloat()).To(Equal(-c.m), info(i, c.value)) + g.Expect(pn.WeeksFloat()).To(Equal(-c.w), info(i, c.value)) + g.Expect(pn.DaysFloat()).To(Equal(-c.d), info(i, c.value)) + g.Expect(pn.HoursFloat()).To(Equal(-c.hh), info(i, c.value)) + g.Expect(pn.MinutesFloat()).To(Equal(-c.mm), info(i, c.value)) + g.Expect(pn.SecondsFloat()).To(Equal(-c.ss), info(i, c.value)) } } @@ -411,23 +504,26 @@ func TestNewPeriod(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { - years, months, days, hours, minutes, seconds int period Period + years, months, days, hours, minutes, seconds int }{ - {0, 0, 0, 0, 0, 0, Period{0, 0, 0, 0, 0, 0}}, - {0, 0, 0, 0, 0, 1, Period{0, 0, 0, 0, 0, 10}}, - {0, 0, 0, 0, 1, 0, Period{0, 0, 0, 0, 10, 0}}, - {0, 0, 0, 1, 0, 0, Period{0, 0, 0, 10, 0, 0}}, - {0, 0, 1, 0, 0, 0, Period{0, 0, 10, 0, 0, 0}}, - {0, 1, 0, 0, 0, 0, Period{0, 10, 0, 0, 0, 0}}, - {1, 0, 0, 0, 0, 0, Period{10, 0, 0, 0, 0, 0}}, - {100, 222, 700, 0, 0, 0, Period{1000, 2220, 7000, 0, 0, 0}}, - {0, 0, 0, 0, 0, -1, Period{0, 0, 0, 0, 0, -10}}, - {0, 0, 0, 0, -1, 0, Period{0, 0, 0, 0, -10, 0}}, - {0, 0, 0, -1, 0, 0, Period{0, 0, 0, -10, 0, 0}}, - {0, 0, -1, 0, 0, 0, Period{0, 0, -10, 0, 0, 0}}, - {0, -1, 0, 0, 0, 0, Period{0, -10, 0, 0, 0, 0}}, - {-1, 0, 0, 0, 0, 0, Period{-10, 0, 0, 0, 0, 0}}, + {}, // zero case + + // positives + {period: Period{seconds: 10}, seconds: 1}, + {period: Period{minutes: 10}, minutes: 1}, + {period: Period{hours: 10}, hours: 1}, + {period: Period{days: 10}, days: 1}, + {period: Period{months: 10}, months: 1}, + {period: Period{years: 10}, years: 1}, + {period: Period{1000, 2220, 7000, 0, 0, 0}, years: 100, months: 222, days: 700}, + // negatives + {period: Period{seconds: -10}, seconds: -1}, + {period: Period{minutes: -10}, minutes: -1}, + {period: Period{hours: -10}, hours: -1}, + {period: Period{days: -10}, days: -1}, + {period: Period{months: -10}, months: -1}, + {period: Period{years: -10}, years: -1}, } for i, c := range cases { p := New(c.years, c.months, c.days, c.hours, c.minutes, c.seconds) @@ -442,16 +538,18 @@ func TestNewHMS(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { - hours, minutes, seconds int period Period + hours, minutes, seconds int }{ - {0, 0, 0, Period{0, 0, 0, 0, 0, 0}}, - {0, 0, 1, Period{0, 0, 0, 0, 0, 10}}, - {0, 1, 0, Period{0, 0, 0, 0, 10, 0}}, - {1, 0, 0, Period{0, 0, 0, 10, 0, 0}}, - {0, 0, -1, Period{0, 0, 0, 0, 0, -10}}, - {0, -1, 0, Period{0, 0, 0, 0, -10, 0}}, - {-1, 0, 0, Period{0, 0, 0, -10, 0, 0}}, + {}, // zero case + // postives + {period: Period{seconds: 10}, seconds: 1}, + {period: Period{minutes: 10}, minutes: 1}, + {period: Period{hours: 10}, hours: 1}, + // negatives + {period: Period{seconds: -10}, seconds: -1}, + {period: Period{minutes: -10}, minutes: -1}, + {period: Period{hours: -10}, hours: -1}, } for i, c := range cases { p := NewHMS(c.hours, c.minutes, c.seconds) @@ -466,17 +564,19 @@ func TestNewYMD(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { - years, months, days int period Period + years, months, days int }{ - {0, 0, 0, Period{0, 0, 0, 0, 0, 0}}, - {0, 0, 1, Period{0, 0, 10, 0, 0, 0}}, - {0, 1, 0, Period{0, 10, 0, 0, 0, 0}}, - {1, 0, 0, Period{10, 0, 0, 0, 0, 0}}, - {100, 222, 700, Period{1000, 2220, 7000, 0, 0, 0}}, - {0, 0, -1, Period{0, 0, -10, 0, 0, 0}}, - {0, -1, 0, Period{0, -10, 0, 0, 0, 0}}, - {-1, 0, 0, Period{-10, 0, 0, 0, 0, 0}}, + {}, // zero case + // positives + {period: Period{days: 10}, days: 1}, + {period: Period{months: 10}, months: 1}, + {period: Period{years: 10}, years: 1}, + {period: Period{years: 1000, months: 2220, days: 7000}, years: 100, months: 222, days: 700}, + // negatives + {period: Period{days: -10}, days: -1}, + {period: Period{months: -10}, months: -1}, + {period: Period{years: -10}, years: -1}, } for i, c := range cases { p := NewYMD(c.years, c.months, c.days) @@ -489,27 +589,27 @@ func TestNewYMD(t *testing.T) { func TestNewOf(t *testing.T) { // HMS tests - testNewOf(t, 100*time.Millisecond, Period{0, 0, 0, 0, 0, 1}, true) - testNewOf(t, time.Second, Period{0, 0, 0, 0, 0, 10}, true) - testNewOf(t, time.Minute, Period{0, 0, 0, 0, 10, 0}, true) - testNewOf(t, time.Hour, Period{0, 0, 0, 10, 0, 0}, true) - testNewOf(t, time.Hour+time.Minute+time.Second, Period{0, 0, 0, 10, 10, 10}, true) - testNewOf(t, 24*time.Hour+time.Minute+time.Second, Period{0, 0, 0, 240, 10, 10}, true) - testNewOf(t, 3276*time.Hour+59*time.Minute+59*time.Second, Period{0, 0, 0, 32760, 590, 590}, true) - testNewOf(t, 30*time.Minute+67*time.Second+600*time.Millisecond, Period{0, 0, 0, 0, 310, 76}, true) + testNewOf(t, 100*time.Millisecond, Period{seconds: 1}, true) + testNewOf(t, time.Second, Period{seconds: 10}, true) + testNewOf(t, time.Minute, Period{minutes: 10}, true) + testNewOf(t, time.Hour, Period{hours: 10}, true) + testNewOf(t, time.Hour+time.Minute+time.Second, Period{hours: 10, minutes: 10, seconds: 10}, true) + testNewOf(t, 24*time.Hour+time.Minute+time.Second, Period{hours: 240, minutes: 10, seconds: 10}, true) + testNewOf(t, 3276*time.Hour+59*time.Minute+59*time.Second, Period{hours: 32760, minutes: 590, seconds: 590}, true) + testNewOf(t, 30*time.Minute+67*time.Second+600*time.Millisecond, Period{minutes: 310, seconds: 76}, true) // YMD tests: must be over 3276 hours (approx 4.5 months), otherwise HMS will take care of it // first rollover: >3276 hours - testNewOf(t, 3277*time.Hour, Period{0, 0, 1360, 130, 0, 0}, false) - testNewOf(t, 3288*time.Hour, Period{0, 0, 1370, 0, 0, 0}, false) - testNewOf(t, 3289*time.Hour, Period{0, 0, 1370, 10, 0, 0}, false) - testNewOf(t, 24*3276*time.Hour, Period{0, 0, 32760, 0, 0, 0}, false) + testNewOf(t, 3277*time.Hour, Period{days: 1360, hours: 130}, false) + testNewOf(t, 3288*time.Hour, Period{days: 1370}, false) + testNewOf(t, 3289*time.Hour, Period{days: 1370, hours: 10}, false) + testNewOf(t, 24*3276*time.Hour, Period{days: 32760}, false) // second rollover: >3276 days - testNewOf(t, 24*3277*time.Hour, Period{80, 110, 200, 0, 0, 0}, false) - testNewOf(t, 3277*oneDay, Period{80, 110, 200, 0, 0, 0}, false) - testNewOf(t, 3277*oneDay+time.Hour+time.Minute+time.Second, Period{80, 110, 200, 10, 0, 0}, false) - testNewOf(t, 36525*oneDay, Period{1000, 0, 0, 0, 0, 0}, false) + testNewOf(t, 24*3277*time.Hour, Period{years: 80, months: 110, days: 200}, false) + testNewOf(t, 3277*oneDay, Period{years: 80, months: 110, days: 200}, false) + testNewOf(t, 3277*oneDay+time.Hour+time.Minute+time.Second, Period{years: 80, months: 110, days: 200, hours: 10}, false) + testNewOf(t, 36525*oneDay, Period{years: 1000}, false) } func testNewOf(t *testing.T, source time.Duration, expected Period, precise bool) { @@ -541,48 +641,48 @@ func TestBetween(t *testing.T) { {now, now, Period{0, 0, 0, 0, 0, 0}}, // simple positive date calculations - {utc(2015, 1, 1, 0, 0, 0, 0), utc(2015, 1, 1, 0, 0, 0, 100), Period{0, 0, 0, 0, 0, 1}}, - {utc(2015, 1, 1, 0, 0, 0, 0), utc(2015, 2, 2, 1, 1, 1, 1), Period{0, 0, 320, 10, 10, 10}}, - {utc(2015, 2, 1, 0, 0, 0, 0), utc(2015, 3, 2, 1, 1, 1, 1), Period{0, 0, 290, 10, 10, 10}}, - {utc(2015, 3, 1, 0, 0, 0, 0), utc(2015, 4, 2, 1, 1, 1, 1), Period{0, 0, 320, 10, 10, 10}}, - {utc(2015, 4, 1, 0, 0, 0, 0), utc(2015, 5, 2, 1, 1, 1, 1), Period{0, 0, 310, 10, 10, 10}}, - {utc(2015, 5, 1, 0, 0, 0, 0), utc(2015, 6, 2, 1, 1, 1, 1), Period{0, 0, 320, 10, 10, 10}}, - {utc(2015, 6, 1, 0, 0, 0, 0), utc(2015, 7, 2, 1, 1, 1, 1), Period{0, 0, 310, 10, 10, 10}}, - {utc(2015, 1, 1, 0, 0, 0, 0), utc(2015, 7, 2, 1, 1, 1, 1), Period{0, 0, 1820, 10, 10, 10}}, + {utc(2015, 1, 1, 0, 0, 0, 0), utc(2015, 1, 1, 0, 0, 0, 100), Period{seconds: 1}}, + {utc(2015, 1, 1, 0, 0, 0, 0), utc(2015, 2, 2, 1, 1, 1, 1), Period{days: 320, hours: 10, minutes: 10, seconds: 10}}, + {utc(2015, 2, 1, 0, 0, 0, 0), utc(2015, 3, 2, 1, 1, 1, 1), Period{days: 290, hours: 10, minutes: 10, seconds: 10}}, + {utc(2015, 3, 1, 0, 0, 0, 0), utc(2015, 4, 2, 1, 1, 1, 1), Period{days: 320, hours: 10, minutes: 10, seconds: 10}}, + {utc(2015, 4, 1, 0, 0, 0, 0), utc(2015, 5, 2, 1, 1, 1, 1), Period{days: 310, hours: 10, minutes: 10, seconds: 10}}, + {utc(2015, 5, 1, 0, 0, 0, 0), utc(2015, 6, 2, 1, 1, 1, 1), Period{days: 320, hours: 10, minutes: 10, seconds: 10}}, + {utc(2015, 6, 1, 0, 0, 0, 0), utc(2015, 7, 2, 1, 1, 1, 1), Period{days: 310, hours: 10, minutes: 10, seconds: 10}}, + {utc(2015, 1, 1, 0, 0, 0, 0), utc(2015, 7, 2, 1, 1, 1, 1), Period{days: 1820, hours: 10, minutes: 10, seconds: 10}}, // less than one month - {utc(2016, 1, 2, 0, 0, 0, 0), utc(2016, 2, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, - {utc(2015, 2, 2, 0, 0, 0, 0), utc(2015, 3, 1, 0, 0, 0, 0), Period{0, 0, 270, 0, 0, 0}}, // non-leap - {utc(2016, 2, 2, 0, 0, 0, 0), utc(2016, 3, 1, 0, 0, 0, 0), Period{0, 0, 280, 0, 0, 0}}, // leap year - {utc(2016, 3, 2, 0, 0, 0, 0), utc(2016, 4, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, - {utc(2016, 4, 2, 0, 0, 0, 0), utc(2016, 5, 1, 0, 0, 0, 0), Period{0, 0, 290, 0, 0, 0}}, - {utc(2016, 5, 2, 0, 0, 0, 0), utc(2016, 6, 1, 0, 0, 0, 0), Period{0, 0, 300, 0, 0, 0}}, - {utc(2016, 6, 2, 0, 0, 0, 0), utc(2016, 7, 1, 0, 0, 0, 0), Period{0, 0, 290, 0, 0, 0}}, + {utc(2016, 1, 2, 0, 0, 0, 0), utc(2016, 2, 1, 0, 0, 0, 0), Period{days: 300}}, + {utc(2015, 2, 2, 0, 0, 0, 0), utc(2015, 3, 1, 0, 0, 0, 0), Period{days: 270}}, // non-leap + {utc(2016, 2, 2, 0, 0, 0, 0), utc(2016, 3, 1, 0, 0, 0, 0), Period{days: 280}}, // leap year + {utc(2016, 3, 2, 0, 0, 0, 0), utc(2016, 4, 1, 0, 0, 0, 0), Period{days: 300}}, + {utc(2016, 4, 2, 0, 0, 0, 0), utc(2016, 5, 1, 0, 0, 0, 0), Period{days: 290}}, + {utc(2016, 5, 2, 0, 0, 0, 0), utc(2016, 6, 1, 0, 0, 0, 0), Period{days: 300}}, + {utc(2016, 6, 2, 0, 0, 0, 0), utc(2016, 7, 1, 0, 0, 0, 0), Period{days: 290}}, // BST drops an hour at the daylight-saving transition - {utc(2015, 1, 1, 0, 0, 0, 0), bst(2015, 7, 2, 1, 1, 1, 1), Period{0, 0, 1820, 0, 10, 10}}, + {utc(2015, 1, 1, 0, 0, 0, 0), bst(2015, 7, 2, 1, 1, 1, 1), Period{days: 1820, minutes: 10, seconds: 10}}, // negative date calculation - {utc(2015, 1, 1, 0, 0, 0, 100), utc(2015, 1, 1, 0, 0, 0, 0), Period{0, 0, 0, 0, 0, -1}}, - {utc(2015, 6, 2, 0, 0, 0, 0), utc(2015, 5, 1, 0, 0, 0, 0), Period{0, 0, -320, 0, 0, 0}}, - {utc(2015, 6, 2, 1, 1, 1, 1), utc(2015, 5, 1, 0, 0, 0, 0), Period{0, 0, -320, -10, -10, -10}}, + {utc(2015, 1, 1, 0, 0, 0, 100), utc(2015, 1, 1, 0, 0, 0, 0), Period{seconds: -1}}, + {utc(2015, 6, 2, 0, 0, 0, 0), utc(2015, 5, 1, 0, 0, 0, 0), Period{days: -320}}, + {utc(2015, 6, 2, 1, 1, 1, 1), utc(2015, 5, 1, 0, 0, 0, 0), Period{days: -320, hours: -10, minutes: -10, seconds: -10}}, // daytime only - {utc(2015, 1, 1, 2, 3, 4, 0), utc(2015, 1, 1, 2, 3, 4, 500), Period{0, 0, 0, 0, 0, 5}}, - {utc(2015, 1, 1, 2, 3, 4, 0), utc(2015, 1, 1, 4, 4, 7, 500), Period{0, 0, 0, 20, 10, 35}}, - {utc(2015, 1, 1, 2, 3, 4, 500), utc(2015, 1, 1, 4, 4, 7, 0), Period{0, 0, 0, 20, 10, 25}}, + {utc(2015, 1, 1, 2, 3, 4, 0), utc(2015, 1, 1, 2, 3, 4, 500), Period{seconds: 5}}, + {utc(2015, 1, 1, 2, 3, 4, 0), utc(2015, 1, 1, 4, 4, 7, 500), Period{hours: 20, minutes: 10, seconds: 35}}, + {utc(2015, 1, 1, 2, 3, 4, 500), utc(2015, 1, 1, 4, 4, 7, 0), Period{hours: 20, minutes: 10, seconds: 25}}, // different dates and times - {utc(2015, 2, 1, 1, 0, 0, 0), utc(2015, 5, 30, 5, 6, 7, 0), Period{0, 0, 1180, 40, 60, 70}}, - {utc(2015, 2, 1, 1, 0, 0, 0), bst(2015, 5, 30, 5, 6, 7, 0), Period{0, 0, 1180, 30, 60, 70}}, + {utc(2015, 2, 1, 1, 0, 0, 0), utc(2015, 5, 30, 5, 6, 7, 0), Period{days: 1180, hours: 40, minutes: 60, seconds: 70}}, + {utc(2015, 2, 1, 1, 0, 0, 0), bst(2015, 5, 30, 5, 6, 7, 0), Period{days: 1180, hours: 30, minutes: 60, seconds: 70}}, // earlier month in later year - {utc(2015, 12, 22, 0, 0, 0, 0), utc(2016, 1, 10, 5, 6, 7, 0), Period{0, 0, 190, 50, 60, 70}}, - {utc(2015, 2, 11, 5, 6, 7, 500), utc(2016, 1, 10, 0, 0, 0, 0), Period{0, 0, 3320, 180, 530, 525}}, + {utc(2015, 12, 22, 0, 0, 0, 0), utc(2016, 1, 10, 5, 6, 7, 0), Period{days: 190, hours: 50, minutes: 60, seconds: 70}}, + {utc(2015, 2, 11, 5, 6, 7, 500), utc(2016, 1, 10, 0, 0, 0, 0), Period{days: 3320, hours: 180, minutes: 530, seconds: 525}}, // larger ranges - {utc(2009, 1, 1, 0, 0, 1, 0), utc(2016, 12, 31, 0, 0, 2, 0), Period{0, 0, 29210, 0, 0, 10}}, - {utc(2008, 1, 1, 0, 0, 1, 0), utc(2016, 12, 31, 0, 0, 2, 0), Period{80, 110, 300, 0, 0, 10}}, + {utc(2009, 1, 1, 0, 0, 1, 0), utc(2016, 12, 31, 0, 0, 2, 0), Period{days: 29210, seconds: 10}}, + {utc(2008, 1, 1, 0, 0, 1, 0), utc(2016, 12, 31, 0, 0, 2, 0), Period{years: 80, months: 110, days: 300, seconds: 10}}, } for i, c := range cases { n := Between(c.a, c.b) @@ -591,66 +691,67 @@ func TestBetween(t *testing.T) { } func TestNormalise(t *testing.T) { - // zero cases - testNormalise(t, New(0, 0, 0, 0, 0, 0), New(0, 0, 0, 0, 0, 0), New(0, 0, 0, 0, 0, 0)) - - // carry seconds to minutes - testNormalise(t, Period{0, 0, 0, 0, 0, 699}, Period{0, 0, 0, 0, 10, 99}, Period{0, 0, 0, 0, 10, 99}) - - // carry minutes to seconds - testNormalise(t, Period{0, 0, 0, 0, 5, 0}, Period{0, 0, 0, 0, 0, 300}, Period{0, 0, 0, 0, 0, 300}) - testNormalise(t, Period{0, 0, 0, 0, 1, 0}, Period{0, 0, 0, 0, 0, 60}, Period{0, 0, 0, 0, 0, 60}) - testNormalise(t, Period{0, 0, 0, 0, 55, 0}, Period{0, 0, 0, 0, 50, 300}, Period{0, 0, 0, 0, 50, 300}) + cases := []struct { + source, precise, approx Period + }{ + // zero cases + {New(0, 0, 0, 0, 0, 0), New(0, 0, 0, 0, 0, 0), New(0, 0, 0, 0, 0, 0)}, - // carry minutes to hours - testNormalise(t, Period{0, 0, 0, 0, 699, 0}, Period{0, 0, 0, 10, 90, 540}, Period{0, 0, 0, 10, 90, 540}) + // carry seconds to minutes + {Period{seconds: 699}, Period{minutes: 10, seconds: 99}, Period{minutes: 10, seconds: 99}}, - // carry hours to minutes - testNormalise(t, Period{0, 0, 0, 5, 0, 0}, Period{0, 0, 0, 0, 300, 0}, Period{0, 0, 0, 0, 300, 0}) + // carry minutes to seconds + {Period{minutes: 5}, Period{seconds: 300}, Period{seconds: 300}}, + {Period{minutes: 1}, Period{seconds: 60}, Period{seconds: 60}}, + {Period{minutes: 55}, Period{minutes: 50, seconds: 300}, Period{minutes: 50, seconds: 300}}, - // carry hours to days - testNormalise(t, Period{0, 0, 0, 249, 0, 0}, Period{0, 0, 0, 240, 540, 0}, Period{0, 0, 0, 240, 540, 0}) - testNormalise(t, Period{0, 0, 0, 249, 0, 0}, Period{0, 0, 0, 240, 540, 0}, Period{0, 0, 0, 240, 540, 0}) - testNormalise(t, Period{0, 0, 0, 369, 0, 0}, Period{0, 0, 0, 360, 540, 0}, Period{0, 0, 10, 120, 540, 0}) - testNormalise(t, Period{0, 0, 0, 249, 0, 10}, Period{0, 0, 0, 240, 540, 10}, Period{0, 0, 0, 240, 540, 10}) + // carry minutes to hours + {Period{minutes: 699}, Period{hours: 10, minutes: 90, seconds: 540}, Period{hours: 10, minutes: 90, seconds: 540}}, - // carry days to hours - testNormalise(t, Period{0, 0, 5, 30, 0, 0}, Period{0, 0, 0, 150, 00, 0}, Period{0, 0, 0, 150, 0, 0}) + // carry hours to minutes + {Period{hours: 5}, Period{minutes: 300}, Period{minutes: 300}}, - // carry months to years - testNormalise(t, Period{0, 125, 0, 0, 0, 0}, Period{0, 125, 0, 0, 0, 0}, Period{0, 125, 0, 0, 0, 0}) - testNormalise(t, Period{0, 131, 0, 0, 0, 0}, Period{10, 11, 0, 0, 0, 0}, Period{10, 11, 0, 0, 0, 0}) + // carry hours to days + {Period{hours: 249}, Period{hours: 240, minutes: 540}, Period{hours: 240, minutes: 540}}, + {Period{hours: 249}, Period{hours: 240, minutes: 540}, Period{hours: 240, minutes: 540}}, + {Period{hours: 369}, Period{hours: 360, minutes: 540}, Period{days: 10, hours: 120, minutes: 540}}, + {Period{hours: 249, seconds: 10}, Period{hours: 240, minutes: 540, seconds: 10}, Period{hours: 240, minutes: 540, seconds: 10}}, - // carry days to months - testNormalise(t, Period{0, 0, 323, 0, 0, 0}, Period{0, 0, 323, 0, 0, 0}, Period{0, 0, 323, 0, 0, 0}) + // carry days to hours + {Period{days: 5, hours: 30}, Period{hours: 150}, Period{hours: 150}}, - // carry months to days - testNormalise(t, Period{0, 5, 203, 0, 0, 0}, Period{0, 0, 355, 0, 0, 0}, Period{0, 10, 50, 0, 0, 0}) + // carry months to years + {Period{months: 125}, Period{months: 125}, Period{months: 125}}, + {Period{months: 131}, Period{years: 10, months: 11}, Period{years: 10, months: 11}}, - // full ripple up - testNormalise(t, Period{0, 121, 305, 239, 591, 601}, Period{10, 0, 330, 360, 540, 61}, Period{10, 10, 40, 0, 540, 61}) + // carry days to months + {Period{days: 323}, Period{days: 323}, Period{days: 323}}, - // carry years to months - testNormalise(t, Period{5, 0, 0, 0, 0, 0}, Period{0, 60, 0, 0, 0, 0}, Period{0, 60, 0, 0, 0, 0}) - testNormalise(t, Period{5, 25, 0, 0, 0, 0}, Period{0, 85, 0, 0, 0, 0}, Period{0, 85, 0, 0, 0, 0}) - testNormalise(t, Period{5, 20, 10, 0, 0, 0}, Period{0, 80, 10, 0, 0, 0}, Period{0, 80, 10, 0, 0, 0}) -} + // carry months to days + {Period{months: 5, days: 203}, Period{days: 355}, Period{months: 10, days: 50}}, -func testNormalise(t *testing.T, source, precise, approx Period) { - t.Helper() + // full ripple up + {Period{months: 121, days: 305, hours: 239, minutes: 591, seconds: 601}, Period{years: 10, days: 330, hours: 360, minutes: 540, seconds: 61}, Period{years: 10, months: 10, days: 40, minutes: 540, seconds: 61}}, - testNormaliseBothSigns(t, source, precise, true) - testNormaliseBothSigns(t, source, approx, false) + // carry years to months + {Period{years: 5}, Period{months: 60}, Period{months: 60}}, + {Period{years: 5, months: 25}, Period{months: 85}, Period{months: 85}}, + {Period{years: 5, months: 20, days: 10}, Period{months: 80, days: 10}, Period{months: 80, days: 10}}, + } + for i, c := range cases { + testNormaliseBothSigns(t, i, c.source, c.precise, true) + testNormaliseBothSigns(t, i, c.source, c.approx, false) + } } -func testNormaliseBothSigns(t *testing.T, source, expected Period, precise bool) { +func testNormaliseBothSigns(t *testing.T, i int, source, expected Period, precise bool) { g := NewGomegaWithT(t) t.Helper() n1 := source.Normalise(precise) if n1 != expected { - t.Errorf("%v.Normalise(%v) %s\n gives %-22s %#v %s,\n want %-22s %#v %s", - source, precise, source.DurationApprox(), + t.Errorf("%d: %v.Normalise(%v) %s\n gives %-22s %#v %s,\n want %-22s %#v %s", + i, source, precise, source.DurationApprox(), n1, n1, n1.DurationApprox(), expected, expected, expected.DurationApprox()) } From afa19e5e3f6dad487830de6c122aa79e108046b0 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 19 Aug 2020 09:59:28 +0100 Subject: [PATCH 131/165] disabled goveralls to investigate failures --- build+test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build+test.sh b/build+test.sh index e20f5dc9..95a550b7 100755 --- a/build+test.sh +++ b/build+test.sh @@ -26,13 +26,13 @@ fi echo date... v go test -v -covermode=count -coverprofile=date.out . v go tool cover -func=date.out -[ -z "$COVERALLS_TOKEN" ] || goveralls -coverprofile=date.out -service=travis-ci -repotoken $COVERALLS_TOKEN +#[ -z "$COVERALLS_TOKEN" ] || goveralls -coverprofile=date.out -service=travis-ci -repotoken $COVERALLS_TOKEN for d in clock period timespan view; do echo $d... v go test -v -covermode=count -coverprofile=$d.out ./$d v go tool cover -func=$d.out - [ -z "$COVERALLS_TOKEN" ] || goveralls -coverprofile=$d.out -service=travis-ci -repotoken $COVERALLS_TOKEN + #[ -z "$COVERALLS_TOKEN" ] || goveralls -coverprofile=$d.out -service=travis-ci -repotoken $COVERALLS_TOKEN done v goreturns -l -w *.go */*.go From e01682b98501b1ed9e4c85328233ca2d24f6ab1c Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 19 Aug 2020 10:02:53 +0100 Subject: [PATCH 132/165] travis now using go 1.15 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index abbfc2dc..fd0058b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: go go: - - 1.12.6 + - '1.15' install: - go get -t -v ./... From 55c00f52aead84787218716f68be29a2815c04dd Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 19 Aug 2020 10:06:51 +0100 Subject: [PATCH 133/165] Period.String() now uses internal period64 helper type; some altered error messages now use standard terminology --- period/format.go | 76 +++++++++-------- period/parse.go | 20 ++--- period/period.go | 209 ++++++++++----------------------------------- period/period64.go | 135 +++++++++++++++++++++++++++++ 4 files changed, 230 insertions(+), 210 deletions(-) create mode 100644 period/period64.go diff --git a/period/format.go b/period/format.go index 32cb6a83..e102c8aa 100644 --- a/period/format.go +++ b/period/format.go @@ -5,8 +5,8 @@ package period import ( - "bytes" "fmt" + "io" "strings" "github.com/rickb777/plural" @@ -22,8 +22,8 @@ func (period Period) FormatWithPeriodNames(yearNames, monthNames, weekNames, day period = period.Abs() parts := make([]string, 0) - parts = appendNonBlank(parts, yearNames.FormatFloat(absFloat10(period.years))) - parts = appendNonBlank(parts, monthNames.FormatFloat(absFloat10(period.months))) + parts = appendNonBlank(parts, yearNames.FormatFloat(float10(period.years))) + parts = appendNonBlank(parts, monthNames.FormatFloat(float10(period.months))) if period.days > 0 || (period.IsZero()) { if len(weekNames) > 0 { @@ -34,15 +34,15 @@ func (period Period) FormatWithPeriodNames(yearNames, monthNames, weekNames, day parts = appendNonBlank(parts, weekNames.FormatInt(int(weeks))) } if mdays > 0 || weeks == 0 { - parts = appendNonBlank(parts, dayNames.FormatFloat(absFloat10(mdays))) + parts = appendNonBlank(parts, dayNames.FormatFloat(float10(mdays))) } } else { - parts = appendNonBlank(parts, dayNames.FormatFloat(absFloat10(period.days))) + parts = appendNonBlank(parts, dayNames.FormatFloat(float10(period.days))) } } - parts = appendNonBlank(parts, hourNames.FormatFloat(absFloat10(period.hours))) - parts = appendNonBlank(parts, minNames.FormatFloat(absFloat10(period.minutes))) - parts = appendNonBlank(parts, secNames.FormatFloat(absFloat10(period.seconds))) + parts = appendNonBlank(parts, hourNames.FormatFloat(float10(period.hours))) + parts = appendNonBlank(parts, minNames.FormatFloat(float10(period.minutes))) + parts = appendNonBlank(parts, secNames.FormatFloat(float10(period.seconds))) return strings.Join(parts, ", ") } @@ -79,50 +79,54 @@ var PeriodSecondNames = plural.FromZero("", "%v second", "%v seconds") // String converts the period to ISO-8601 form. func (period Period) String() string { - if period.IsZero() { + return period.toPeriod64().String() +} + +func (p64 period64) String() string { + if p64 == (period64{}) { return "P0D" } - buf := &bytes.Buffer{} - if period.Sign() < 0 { + buf := &strings.Builder{} + if p64.neg { buf.WriteByte('-') } buf.WriteByte('P') - if period.years != 0 { - fmt.Fprintf(buf, "%gY", absFloat10(period.years)) - } - if period.months != 0 { - fmt.Fprintf(buf, "%gM", absFloat10(period.months)) - } - if period.days != 0 { - if period.days%70 == 0 { - fmt.Fprintf(buf, "%gW", absFloat10(period.days/7)) + writeField64(buf, p64.years, 'Y') + writeField64(buf, p64.months, 'M') + + if p64.days != 0 { + if p64.days%70 == 0 { + writeField64(buf, p64.days/7, 'W') } else { - fmt.Fprintf(buf, "%gD", absFloat10(period.days)) + writeField64(buf, p64.days, 'D') } } - if period.hours != 0 || period.minutes != 0 || period.seconds != 0 { + + if p64.hours != 0 || p64.minutes != 0 || p64.seconds != 0 { buf.WriteByte('T') } - if period.hours != 0 { - fmt.Fprintf(buf, "%gH", absFloat10(period.hours)) - } - if period.minutes != 0 { - fmt.Fprintf(buf, "%gM", absFloat10(period.minutes)) - } - if period.seconds != 0 { - fmt.Fprintf(buf, "%gS", absFloat10(period.seconds)) - } + + writeField64(buf, p64.hours, 'H') + writeField64(buf, p64.minutes, 'M') + writeField64(buf, p64.seconds, 'S') return buf.String() } -func absFloat10(v int16) float32 { - f := float32(v) / 10 - if v < 0 { - return -f +func writeField64(w io.Writer, field int64, designator byte) { + if field != 0 { + if field%10 != 0 { + fmt.Fprintf(w, "%g", float32(field)/10) + } else { + fmt.Fprintf(w, "%d", field/10) + } + w.(io.ByteWriter).WriteByte(designator) } - return f +} + +func float10(v int16) float32 { + return float32(v) / 10 } diff --git a/period/parse.go b/period/parse.go index 52b91f14..eee6cefa 100644 --- a/period/parse.go +++ b/period/parse.go @@ -80,17 +80,17 @@ func ParseWithNormalise(period string, normalise bool) (Period, error) { result.hours, st = parseField(st, 'H') if st.err != nil { - return Period{}, fmt.Errorf("expected a number before the 'H' marker: %s", period) + return Period{}, fmt.Errorf("expected a number before the 'H' designator: %s", period) } result.minutes, st = parseField(st, 'M') if st.err != nil { - return Period{}, fmt.Errorf("expected a number before the 'M' marker: %s", period) + return Period{}, fmt.Errorf("expected a number before the 'M' designator: %s", period) } result.seconds, st = parseField(st, 'S') if st.err != nil { - return Period{}, fmt.Errorf("expected a number before the 'S' marker: %s", period) + return Period{}, fmt.Errorf("expected a number before the 'S' designator: %s", period) } if len(st.pcopy) != 0 { @@ -102,20 +102,20 @@ func ParseWithNormalise(period string, normalise bool) (Period, error) { result.years, st = parseField(st, 'Y') if st.err != nil { - return Period{}, fmt.Errorf("expected a number before the 'Y' marker: %s", period) + return Period{}, fmt.Errorf("expected a number before the 'Y' designator: %s", period) } result.months, st = parseField(st, 'M') if st.err != nil { - return Period{}, fmt.Errorf("expected a number before the 'M' marker: %s", period) + return Period{}, fmt.Errorf("expected a number before the 'M' designator: %s", period) } weeks, st := parseField(st, 'W') if st.err != nil { - return Period{}, fmt.Errorf("expected a number before the 'W' marker: %s", period) + return Period{}, fmt.Errorf("expected a number before the 'W' designator: %s", period) } days, st := parseField(st, 'D') if st.err != nil { - return Period{}, fmt.Errorf("expected a number before the 'D' marker: %s", period) + return Period{}, fmt.Errorf("expected a number before the 'D' designator: %s", period) } if len(st.pcopy) != 0 { @@ -126,14 +126,14 @@ func ParseWithNormalise(period string, normalise bool) (Period, error) { //fmt.Printf("%#v\n", st) if !st.ok { - return Period{}, fmt.Errorf("expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' marker: %s", period) + return Period{}, fmt.Errorf("expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' designator: %s", period) } if normalise { - return result.normalise64(true).toPeriod(), nil + return result.normalise64(true).toPeriod() } - return result.toPeriod(), nil + return result.toPeriod() } type parseState struct { diff --git a/period/period.go b/period/period.go index a9b39c6a..fc6be14f 100644 --- a/period/period.go +++ b/period/period.go @@ -14,7 +14,6 @@ const daysPerMonthE4 int64 = 304369 // 30.4369 days per month const daysPerMonthE6 int64 = 30436875 // 30.436875 days per month const oneE4 int64 = 10000 -const oneE5 int64 = 100000 const oneE6 int64 = 1000000 const oneE7 int64 = 10000000 @@ -171,9 +170,9 @@ func daysDiff(t1, t2 time.Time) (year, month, day, hour, min, sec, hundredth int day = int(duration / (24 * time.Hour)) - hour = int(hh2 - hh1) - min = int(mm2 - mm1) - sec = int(ss2 - ss1) + hour = hh2 - hh1 + min = mm2 - mm1 + sec = ss2 - ss1 hundredth = (t2.Nanosecond() - t1.Nanosecond()) / 100000000 // Normalize negative values @@ -248,15 +247,15 @@ func (period Period) OnlyHMS() Period { // Abs converts a negative period to a positive one. func (period Period) Abs() Period { - return Period{absInt16(period.years), absInt16(period.months), absInt16(period.days), - absInt16(period.hours), absInt16(period.minutes), absInt16(period.seconds)} + a, _ := period.absNeg() + return a } -func absInt16(v int16) int16 { - if v < 0 { - return -v +func (period Period) absNeg() (Period, bool) { + if period.IsNegative() { + return period.Negate(), true } - return v + return period, false } // Negate changes the sign of the period. @@ -287,22 +286,32 @@ func (period Period) Add(that Period) Period { // // Known issue: scaling by a large reduction factor (i.e. much less than one) doesn't work properly. func (period Period) Scale(factor float32) Period { + ap, neg := period.absNeg() if -0.5 < factor && factor < 0.5 { - d, pr1 := period.Duration() + d, pr1 := ap.Duration() mul := float64(d) * float64(factor) p2, pr2 := NewOf(time.Duration(mul)) return p2.Normalise(pr1 && pr2) } - y := int64(float32(period.years) * factor) - m := int64(float32(period.months) * factor) - d := int64(float32(period.days) * factor) - hh := int64(float32(period.hours) * factor) - mm := int64(float32(period.minutes) * factor) - ss := int64(float32(period.seconds) * factor) + y := int64(float32(ap.years) * factor) + m := int64(float32(ap.months) * factor) + d := int64(float32(ap.days) * factor) + hh := int64(float32(ap.hours) * factor) + mm := int64(float32(ap.minutes) * factor) + ss := int64(float32(ap.seconds) * factor) - return (&period64{y, m, d, hh, mm, ss, false}).normalise64(true).toPeriod() + result, _ := (&period64{y, m, d, hh, mm, ss, neg}).normalise64(true).toPeriod() + // TODO handle the possible overflow error + return result +} + +func absInt16(v int16) int16 { + if v < 0 { + return -v + } + return v } // Years gets the whole number of years in the period. @@ -514,7 +523,8 @@ func (period Period) TotalMonthsApprox() int { // // Because the number of hours per day is imprecise (due to daylight savings etc), and because // the number of days per month is variable in the Gregorian calendar, there is a reluctance -// to transfer time too or from the days element. To give control over this, there are two modes. +// to transfer time to or from the days element, or to transfer days to or from the months +// element. To give control over this, there are two modes. // // In precise mode: // Multiples of 60 seconds become minutes. @@ -527,6 +537,11 @@ func (period Period) TotalMonthsApprox() int { // // Note that leap seconds are disregarded: every minute is assumed to have 60 seconds. func (period Period) Normalise(precise bool) Period { + n, _ := normalise(period, precise) + return n +} + +func normalise(period Period, precise bool) (Period, error) { const limit = 32670 - (32670 / 60) // can we use a quicker algorithm for HHMMSS with int16 arithmetic? @@ -534,14 +549,14 @@ func (period Period) Normalise(precise bool) Period { (!precise || period.days == 0) && period.hours > -limit && period.hours < limit { - return period.normaliseHHMMSS(precise) + return period.normaliseHHMMSS(precise), nil } // can we use a quicker algorithm for YYMM with int16 arithmetic? - if (period.years != 0 || period.months != 0) && //period.months%10 == 0 && + if (period.years != 0 || period.months != 0) && period.days == 0 && period.hours == 0 && period.minutes == 0 && period.seconds == 0 { - return period.normaliseYYMM() + return period.normaliseYYMM(), nil } // do things the no-nonsense way using int64 arithmetic @@ -549,8 +564,7 @@ func (period Period) Normalise(precise bool) Period { } func (period Period) normaliseHHMMSS(precise bool) Period { - s := period.Sign() - ap := period.Abs() + ap, neg := period.absNeg() // remember that the fields are all fixed-point 1E1 ap.minutes += (ap.seconds / 600) * 10 @@ -583,164 +597,31 @@ func (period Period) normaliseHHMMSS(precise bool) Period { ap.minutes -= mm10 } - if s < 0 { + if neg { return ap.Negate() } return ap } func (period Period) normaliseYYMM() Period { - s := period.Sign() - ap := period.Abs() - // remember that the fields are all fixed-point 1E1 - if ap.months > 129 { + ap, neg := period.absNeg() + + // bubble month to years + if ap.months > 129 { //TODO 119??? ap.years += (ap.months / 120) * 10 ap.months = ap.months % 120 } + // push year-fraction down y10 := ap.years % 10 if y10 != 0 && (ap.years < 10 || ap.months != 0) { ap.months += y10 * 12 ap.years -= y10 } - if s < 0 { + if neg { return ap.Negate() } return ap } - -//------------------------------------------------------------------------------------------------- - -// used for stages in arithmetic -type period64 struct { - years, months, days, hours, minutes, seconds int64 - neg bool -} - -func (period Period) toPeriod64() *period64 { - return &period64{ - int64(period.years), int64(period.months), int64(period.days), - int64(period.hours), int64(period.minutes), int64(period.seconds), - false, - } -} - -func (p *period64) toPeriod() Period { - if p.neg { - return Period{ - int16(-p.years), int16(-p.months), int16(-p.days), - int16(-p.hours), int16(-p.minutes), int16(-p.seconds), - } - } - - return Period{ - int16(p.years), int16(p.months), int16(p.days), - int16(p.hours), int16(p.minutes), int16(p.seconds), - } -} - -func (p *period64) normalise64(precise bool) *period64 { - return p.abs().rippleUp(precise).moveFractionToRight() -} - -func (p *period64) abs() *period64 { - - if !p.neg { - if p.years < 0 { - p.years = -p.years - p.neg = true - } - - if p.months < 0 { - p.months = -p.months - p.neg = true - } - - if p.days < 0 { - p.days = -p.days - p.neg = true - } - - if p.hours < 0 { - p.hours = -p.hours - p.neg = true - } - - if p.minutes < 0 { - p.minutes = -p.minutes - p.neg = true - } - - if p.seconds < 0 { - p.seconds = -p.seconds - p.neg = true - } - } - return p -} - -func (p *period64) rippleUp(precise bool) *period64 { - // remember that the fields are all fixed-point 1E1 - - p.minutes = p.minutes + (p.seconds/600)*10 - p.seconds = p.seconds % 600 - - p.hours = p.hours + (p.minutes/600)*10 - p.minutes = p.minutes % 600 - - // 32670-(32670/60)-(32670/3600) = 32760 - 546 - 9.1 = 32204.9 - if !precise || p.hours > 32204 { - p.days += (p.hours / 240) * 10 - p.hours = p.hours % 240 - } - - if !precise || p.days > 32760 { - dE6 := p.days * oneE6 - p.months += dE6 / daysPerMonthE6 - p.days = (dE6 % daysPerMonthE6) / oneE6 - } - - p.years = p.years + (p.months/120)*10 - p.months = p.months % 120 - - return p -} - -// moveFractionToRight applies the rule that only the smallest field is permitted to have a decimal fraction. -func (p *period64) moveFractionToRight() *period64 { - // remember that the fields are all fixed-point 1E1 - - y10 := p.years % 10 - if y10 != 0 && (p.months != 0 || p.days != 0 || p.hours != 0 || p.minutes != 0 || p.seconds != 0) { - p.months += y10 * 12 - p.years = (p.years / 10) * 10 - } - - m10 := p.months % 10 - if m10 != 0 && (p.days != 0 || p.hours != 0 || p.minutes != 0 || p.seconds != 0) { - p.days += (m10 * daysPerMonthE6) / oneE6 - p.months = (p.months / 10) * 10 - } - - d10 := p.days % 10 - if d10 != 0 && (p.hours != 0 || p.minutes != 0 || p.seconds != 0) { - p.hours += d10 * 24 - p.days = (p.days / 10) * 10 - } - - hh10 := p.hours % 10 - if hh10 != 0 && (p.minutes != 0 || p.seconds != 0) { - p.minutes += hh10 * 60 - p.hours = (p.hours / 10) * 10 - } - - mm10 := p.minutes % 10 - if mm10 != 0 && p.seconds != 0 { - p.seconds += mm10 * 60 - p.minutes = (p.minutes / 10) * 10 - } - - return p -} diff --git a/period/period64.go b/period/period64.go new file mode 100644 index 00000000..4793750c --- /dev/null +++ b/period/period64.go @@ -0,0 +1,135 @@ +package period + +import ( + "fmt" + "strings" +) + +// used for stages in arithmetic +type period64 struct { + // always positive values + years, months, days, hours, minutes, seconds int64 + // true if the period is negative + neg bool +} + +func (period Period) toPeriod64() *period64 { + if period.IsNegative() { + return &period64{ + int64(-period.years), int64(-period.months), int64(-period.days), + int64(-period.hours), int64(-period.minutes), int64(-period.seconds), + true, + } + } + return &period64{ + int64(period.years), int64(period.months), int64(period.days), + int64(period.hours), int64(period.minutes), int64(period.seconds), + false, + } +} + +func (p64 *period64) toPeriod() (Period, error) { + var f []string + if p64.years > 32767 { + f = append(f, "years") + } + if p64.months > 32767 { + f = append(f, "months") + } + if p64.days > 32767 { + f = append(f, "days") + } + if p64.hours > 32767 { + f = append(f, "hours") + } + if p64.minutes > 32767 { + f = append(f, "minutes") + } + if p64.seconds > 32767 { + f = append(f, "seconds") + } + + if len(f) > 0 { + return Period{}, fmt.Errorf("integer overflow occurred in %s: %s", strings.Join(f, ","), p64) + } + + if p64.neg { + return Period{ + int16(-p64.years), int16(-p64.months), int16(-p64.days), + int16(-p64.hours), int16(-p64.minutes), int16(-p64.seconds), + }, nil + } + + return Period{ + int16(p64.years), int16(p64.months), int16(p64.days), + int16(p64.hours), int16(p64.minutes), int16(p64.seconds), + }, nil +} + +func (p64 *period64) normalise64(precise bool) *period64 { + return p64.rippleUp(precise).moveFractionToRight() +} + +func (p64 *period64) rippleUp(precise bool) *period64 { + // remember that the fields are all fixed-point 1E1 + + p64.minutes = p64.minutes + (p64.seconds/600)*10 + p64.seconds = p64.seconds % 600 + + p64.hours = p64.hours + (p64.minutes/600)*10 + p64.minutes = p64.minutes % 600 + + // 32670-(32670/60)-(32670/3600) = 32760 - 546 - 9.1 = 32204.9 + if !precise || p64.hours > 32204 { + p64.days += (p64.hours / 240) * 10 + p64.hours = p64.hours % 240 + } + + if !precise || p64.days > 32760 { + dE6 := p64.days * oneE6 + p64.months += dE6 / daysPerMonthE6 + p64.days = (dE6 % daysPerMonthE6) / oneE6 + } + + p64.years = p64.years + (p64.months/120)*10 + p64.months = p64.months % 120 + + return p64 +} + +// moveFractionToRight applies the rule that only the smallest field is permitted to have a decimal fraction. +func (p64 *period64) moveFractionToRight() *period64 { + // remember that the fields are all fixed-point 1E1 + + y10 := p64.years % 10 + if y10 != 0 && (p64.months != 0 || p64.days != 0 || p64.hours != 0 || p64.minutes != 0 || p64.seconds != 0) { + p64.months += y10 * 12 + p64.years = (p64.years / 10) * 10 + } + + m10 := p64.months % 10 + if m10 != 0 && (p64.days != 0 || p64.hours != 0 || p64.minutes != 0 || p64.seconds != 0) { + p64.days += (m10 * daysPerMonthE6) / oneE6 + p64.months = (p64.months / 10) * 10 + } + + d10 := p64.days % 10 + if d10 != 0 && (p64.hours != 0 || p64.minutes != 0 || p64.seconds != 0) { + p64.hours += d10 * 24 + p64.days = (p64.days / 10) * 10 + } + + hh10 := p64.hours % 10 + if hh10 != 0 && (p64.minutes != 0 || p64.seconds != 0) { + p64.minutes += hh10 * 60 + p64.hours = (p64.hours / 10) * 10 + } + + mm10 := p64.minutes % 10 + if mm10 != 0 && p64.seconds != 0 { + p64.seconds += mm10 * 60 + p64.minutes = (p64.minutes / 10) * 10 + } + + return p64 +} From 91ffaa5317ed63daf80bc6a7be42187ba1fee8b6 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 19 Aug 2020 12:42:15 +0100 Subject: [PATCH 134/165] Period Parse and ScaleWithOverflowCheck now use internal period64 helper type, which allows internal checking for integer overflow; some altered error messages now use standard terminology; --- period/format.go | 2 +- period/parse.go | 4 +-- period/period.go | 29 +++++++++++++++------ period/period64.go | 23 ++++++++++------- period/period_test.go | 59 ++++++++++++++++++++++++++----------------- 5 files changed, 74 insertions(+), 43 deletions(-) diff --git a/period/format.go b/period/format.go index e102c8aa..b341512a 100644 --- a/period/format.go +++ b/period/format.go @@ -79,7 +79,7 @@ var PeriodSecondNames = plural.FromZero("", "%v second", "%v seconds") // String converts the period to ISO-8601 form. func (period Period) String() string { - return period.toPeriod64().String() + return period.toPeriod64("").String() } func (p64 period64) String() string { diff --git a/period/parse.go b/period/parse.go index eee6cefa..3b6a4d4d 100644 --- a/period/parse.go +++ b/period/parse.go @@ -51,7 +51,7 @@ func Parse(period string) (Period, error) { // are equivalent: "P0Y", "P0M", "P0W", "P0D", "PT0H", PT0M", PT0S", and "P0". // The canonical zero is "P0D". func ParseWithNormalise(period string, normalise bool) (Period, error) { - if period == "" { + if period == "" || period == "-" || period == "+" { return Period{}, fmt.Errorf("cannot parse a blank string as a period") } @@ -59,7 +59,7 @@ func ParseWithNormalise(period string, normalise bool) (Period, error) { return Period{}, nil } - result := period64{} + result := period64{input: period} pcopy := period if pcopy[0] == '-' { result.neg = true diff --git a/period/period.go b/period/period.go index fc6be14f..cb1d7496 100644 --- a/period/period.go +++ b/period/period.go @@ -279,20 +279,34 @@ func (period Period) Add(that Period) Period { } // Scale a period by a multiplication factor. Obviously, this can both enlarge and shrink it, -// and change the sign if negative. The result is normalised. +// and change the sign if negative. The result is normalised, but integer overflows are silently +// ignored. // // Bear in mind that the internal representation is limited by fixed-point arithmetic with one // decimal place; each field is only int16. // // Known issue: scaling by a large reduction factor (i.e. much less than one) doesn't work properly. func (period Period) Scale(factor float32) Period { + result, _ := period.ScaleWithOverflowCheck(factor) + return result +} + +// ScaleWithOverflowCheck a period by a multiplication factor. Obviously, this can both enlarge and shrink it, +// and change the sign if negative. The result is normalised. An error is returned if integer overflow +// happened. +// +// Bear in mind that the internal representation is limited by fixed-point arithmetic with one +// decimal place; each field is only int16. +// +// Known issue: scaling by a large reduction factor (i.e. much less than one) doesn't work properly. +func (period Period) ScaleWithOverflowCheck(factor float32) (Period, error) { ap, neg := period.absNeg() if -0.5 < factor && factor < 0.5 { d, pr1 := ap.Duration() mul := float64(d) * float64(factor) p2, pr2 := NewOf(time.Duration(mul)) - return p2.Normalise(pr1 && pr2) + return p2.Normalise(pr1 && pr2), nil } y := int64(float32(ap.years) * factor) @@ -302,9 +316,8 @@ func (period Period) Scale(factor float32) Period { mm := int64(float32(ap.minutes) * factor) ss := int64(float32(ap.seconds) * factor) - result, _ := (&period64{y, m, d, hh, mm, ss, neg}).normalise64(true).toPeriod() - // TODO handle the possible overflow error - return result + p64 := &period64{years: y, months: m, days: d, hours: hh, minutes: mm, seconds: ss, neg: neg} + return p64.normalise64(true).toPeriod() } func absInt16(v int16) int16 { @@ -537,11 +550,11 @@ func (period Period) TotalMonthsApprox() int { // // Note that leap seconds are disregarded: every minute is assumed to have 60 seconds. func (period Period) Normalise(precise bool) Period { - n, _ := normalise(period, precise) + n, _ := normalise(period, "", precise) return n } -func normalise(period Period, precise bool) (Period, error) { +func normalise(period Period, input string, precise bool) (Period, error) { const limit = 32670 - (32670 / 60) // can we use a quicker algorithm for HHMMSS with int16 arithmetic? @@ -560,7 +573,7 @@ func normalise(period Period, precise bool) (Period, error) { } // do things the no-nonsense way using int64 arithmetic - return period.toPeriod64().normalise64(precise).toPeriod() + return period.toPeriod64(input).normalise64(precise).toPeriod() } func (period Period) normaliseHHMMSS(precise bool) Period { diff --git a/period/period64.go b/period/period64.go index 4793750c..941e33db 100644 --- a/period/period64.go +++ b/period/period64.go @@ -10,21 +10,23 @@ type period64 struct { // always positive values years, months, days, hours, minutes, seconds int64 // true if the period is negative - neg bool + neg bool + input string } -func (period Period) toPeriod64() *period64 { +func (period Period) toPeriod64(input string) *period64 { if period.IsNegative() { return &period64{ - int64(-period.years), int64(-period.months), int64(-period.days), - int64(-period.hours), int64(-period.minutes), int64(-period.seconds), - true, + years: int64(-period.years), months: int64(-period.months), days: int64(-period.days), + hours: int64(-period.hours), minutes: int64(-period.minutes), seconds: int64(-period.seconds), + neg: true, + input: input, } } return &period64{ - int64(period.years), int64(period.months), int64(period.days), - int64(period.hours), int64(period.minutes), int64(period.seconds), - false, + years: int64(period.years), months: int64(period.months), days: int64(period.days), + hours: int64(period.hours), minutes: int64(period.minutes), seconds: int64(period.seconds), + input: input, } } @@ -50,7 +52,10 @@ func (p64 *period64) toPeriod() (Period, error) { } if len(f) > 0 { - return Period{}, fmt.Errorf("integer overflow occurred in %s: %s", strings.Join(f, ","), p64) + if p64.input == "" { + p64.input = p64.String() + } + return Period{}, fmt.Errorf("integer overflow occurred in %s: %s", strings.Join(f, ","), p64.input) } if p64.neg { diff --git a/period/period_test.go b/period/period_test.go index d64617df..e49ca251 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -24,33 +24,46 @@ func TestParseErrors(t *testing.T) { value string normalise bool expected string + expvalue string }{ - {"", false, "cannot parse a blank string as a period"}, - {"XY", false, "expected 'P' period mark at the start: XY"}, - {"PxY", false, "expected a number before the 'Y' marker: PxY"}, - {"PxW", false, "expected a number before the 'W' marker: PxW"}, - {"PxD", false, "expected a number before the 'D' marker: PxD"}, - {"PTxH", false, "expected a number before the 'H' marker: PTxH"}, - {"PTxM", false, "expected a number before the 'M' marker: PTxM"}, - {"PTxS", false, "expected a number before the 'S' marker: PTxS"}, - {"P1HT1M", false, "unexpected remaining components 1H: P1HT1M"}, - {"PT1Y", false, "unexpected remaining components 1Y: PT1Y"}, - {"P1S", false, "unexpected remaining components 1S: P1S"}, + {"", false, "cannot parse a blank string as a period", ""}, + {"XY", false, "expected 'P' period mark at the start: ", "XY"}, + {"PxY", false, "expected a number before the 'Y' designator: ", "PxY"}, + {"PxW", false, "expected a number before the 'W' designator: ", "PxW"}, + {"PxD", false, "expected a number before the 'D' designator: ", "PxD"}, + {"PTxH", false, "expected a number before the 'H' designator: ", "PTxH"}, + {"PTxM", false, "expected a number before the 'M' designator: ", "PTxM"}, + {"PTxS", false, "expected a number before the 'S' designator: ", "PTxS"}, + {"P1HT1M", false, "unexpected remaining components 1H: ", "P1HT1M"}, + {"PT1Y", false, "unexpected remaining components 1Y: ", "PT1Y"}, + {"P1S", false, "unexpected remaining components 1S: ", "P1S"}, // integer overflow - //{"PT103412160000S", false, "integer overflow occurred in seconds: PT103412160000S"}, - //{"PT43084443591S", false, "integer overflow occurred in seconds: PT43084443591S"}, - //{"P32768Y", false, "integer overflow occurred in years: P32768Y"}, - //{"P32768M", false, "integer overflow occurred in months: P32768M"}, - //{"P32768D", false, "integer overflow occurred in days: P32768D"}, - //{"PT32768H", false, "integer overflow occurred in hours: PT32768H"}, - //{"PT32768M", false, "integer overflow occurred in minutes: PT32768M"}, - //{"PT32768S", false, "integer overflow occurred in seconds: PT32768S"}, - //{"PT32768H32768M32768S", false, "integer overflow occurred in hours,minutes,seconds: PT32768H32768M32768S"}, + {"P32768Y", false, "integer overflow occurred in years: ", "P32768Y"}, + {"P32768M", false, "integer overflow occurred in months: ", "P32768M"}, + {"P32768D", false, "integer overflow occurred in days: ", "P32768D"}, + {"PT32768H", false, "integer overflow occurred in hours: ", "PT32768H"}, + {"PT32768M", false, "integer overflow occurred in minutes: ", "PT32768M"}, + {"PT32768S", false, "integer overflow occurred in seconds: ", "PT32768S"}, + {"PT32768H32768M32768S", false, "integer overflow occurred in hours,minutes,seconds: ", "PT32768H32768M32768S"}, + {"PT103412160000S", false, "integer overflow occurred in seconds: ", "PT103412160000S"}, + {"P39324M", true, "integer overflow occurred in years: ", "P39324M"}, + {"P1196900D", true, "integer overflow occurred in years: ", "P1196900D"}, + {"PT28725600H", true, "integer overflow occurred in years: ", "PT28725600H"}, + {"PT1723536000M", true, "integer overflow occurred in years: ", "PT1723536000M"}, + {"PT103412160000S", true, "integer overflow occurred in years: ", "PT103412160000S"}, } for i, c := range cases { - _, err := ParseWithNormalise(c.value, c.normalise) - g.Expect(err).To(HaveOccurred(), info(i, c.value)) - g.Expect(err.Error()).To(Equal(c.expected), info(i, c.value)) + _, ep := ParseWithNormalise(c.value, c.normalise) + g.Expect(ep).To(HaveOccurred(), info(i, c.value)) + g.Expect(ep.Error()).To(Equal(c.expected+c.expvalue), info(i, c.value)) + + _, en := ParseWithNormalise("-"+c.value, c.normalise) + g.Expect(en).To(HaveOccurred(), info(i, c.value)) + if c.expvalue != "" { + g.Expect(en.Error()).To(Equal(c.expected+"-"+c.expvalue), info(i, c.value)) + } else { + g.Expect(en.Error()).To(Equal(c.expected), info(i, c.value)) + } } } From ea239e16f3f8339ad577c14be18eff1540fa88ed Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Fri, 4 Sep 2020 12:39:08 +0100 Subject: [PATCH 135/165] Period code restructured into more files --- period/arithmetic.go | 106 ++++++++++++++++++++++++++++++ period/arithmetic_test.go | 132 ++++++++++++++++++++++++++++++++++++++ period/designator.go | 55 ++++++++++++++++ period/format.go | 21 ++++-- period/parse.go | 21 ++++-- period/period.go | 94 ++++----------------------- period/period_test.go | 120 ---------------------------------- 7 files changed, 333 insertions(+), 216 deletions(-) create mode 100644 period/arithmetic.go create mode 100644 period/arithmetic_test.go create mode 100644 period/designator.go diff --git a/period/arithmetic.go b/period/arithmetic.go new file mode 100644 index 00000000..55993fe0 --- /dev/null +++ b/period/arithmetic.go @@ -0,0 +1,106 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package period + +import ( + "time" +) + +// Add adds two periods together. Use this method along with Negate in order to subtract periods. +// +// The result is not normalised and may overflow arithmetically (to make this unlikely, use Normalise on +// the inputs before adding them). +func (period Period) Add(that Period) Period { + return Period{ + period.years + that.years, + period.months + that.months, + period.days + that.days, + period.hours + that.hours, + period.minutes + that.minutes, + period.seconds + that.seconds, + } +} + +//------------------------------------------------------------------------------------------------- + +// AddTo adds the period to a time, returning the result. +// A flag is also returned that is true when the conversion was precise and false otherwise. +// +// When the period specifies hours, minutes and seconds only, the result is precise. +// Also, when the period specifies whole years, months and days (i.e. without fractions), the +// result is precise. However, when years, months or days contains fractions, the result +// is only an approximation (it assumes that all days are 24 hours and every year is 365.2425 +// days, as per Gregorian calendar rules). +func (period Period) AddTo(t time.Time) (time.Time, bool) { + wholeYears := (period.years % 10) == 0 + wholeMonths := (period.months % 10) == 0 + wholeDays := (period.days % 10) == 0 + + if wholeYears && wholeMonths && wholeDays { + // in this case, time.AddDate provides an exact solution + stE3 := totalSecondsE3(period) + t1 := t.AddDate(int(period.years/10), int(period.months/10), int(period.days/10)) + return t1.Add(stE3 * time.Millisecond), true + } + + d, precise := period.Duration() + return t.Add(d), precise +} + +//------------------------------------------------------------------------------------------------- + +// Scale a period by a multiplication factor. Obviously, this can both enlarge and shrink it, +// and change the sign if negative. The result is normalised, but integer overflows are silently +// ignored. +// +// Bear in mind that the internal representation is limited by fixed-point arithmetic with two +// decimal places; each field is only int16. +// +// Known issue: scaling by a large reduction factor (i.e. much less than one) doesn't work properly. +func (period Period) Scale(factor float32) Period { + result, _ := period.ScaleWithOverflowCheck(factor) + return result +} + +// ScaleWithOverflowCheck a period by a multiplication factor. Obviously, this can both enlarge and shrink it, +// and change the sign if negative. The result is normalised. An error is returned if integer overflow +// happened. +// +// Bear in mind that the internal representation is limited by fixed-point arithmetic with one +// decimal place; each field is only int16. +// +// Known issue: scaling by a large reduction factor (i.e. much less than one) doesn't work properly. +func (period Period) ScaleWithOverflowCheck(factor float32) (Period, error) { + ap, neg := period.absNeg() + + if -0.5 < factor && factor < 0.5 { + d, pr1 := ap.Duration() + mul := float64(d) * float64(factor) + p2, pr2 := NewOf(time.Duration(mul)) + return p2.Normalise(pr1 && pr2), nil + } + + y := int64(float32(ap.years) * factor) + m := int64(float32(ap.months) * factor) + d := int64(float32(ap.days) * factor) + hh := int64(float32(ap.hours) * factor) + mm := int64(float32(ap.minutes) * factor) + ss := int64(float32(ap.seconds) * factor) + + p64 := &period64{years: y, months: m, days: d, hours: hh, minutes: mm, seconds: ss, neg: neg} + return p64.normalise64(true).toPeriod() +} + +// RationalScale scales a period by a rational multiplication factor. Obviously, this can both enlarge and shrink it, +// and change the sign if negative. The result is normalised. An error is returned if integer overflow +// happened. +// +// If the divisor is zero, a panic will arise. +// +// Bear in mind that the internal representation is limited by fixed-point arithmetic with two +// decimal places; each field is only int16. +//func (period Period) RationalScale(multiplier, divisor int) (Period, error) { +// return period.rationalScale64(int64(multiplier), int64(divisor)) +//} diff --git a/period/arithmetic_test.go b/period/arithmetic_test.go new file mode 100644 index 00000000..0122ded1 --- /dev/null +++ b/period/arithmetic_test.go @@ -0,0 +1,132 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package period + +import ( + "testing" + "time" + + . "github.com/onsi/gomega" +) + +func TestPeriodScale(t *testing.T) { + g := NewGomegaWithT(t) + + cases := []struct { + one string + m float32 + expect string + }{ + {"P0D", 2, "P0D"}, + {"P1D", 2, "P2D"}, + {"P1D", 0, "P0D"}, + {"P1D", 365, "P365D"}, + {"P1M", 2, "P2M"}, + {"P1M", 12, "P1Y"}, + //TODO {"P1Y3M", 1.0/15, "P1M"}, + {"P1Y", 2, "P2Y"}, + {"PT1H", 2, "PT2H"}, + {"PT1M", 2, "PT2M"}, + {"PT1S", 2, "PT2S"}, + {"P1D", 0.5, "P0.5D"}, + {"P1M", 0.5, "P0.5M"}, + {"P1Y", 0.5, "P0.5Y"}, + {"PT1H", 0.5, "PT0.5H"}, + {"PT1H", 0.1, "PT6M"}, + //TODO {"PT1H", 0.01, "PT36S"}, + {"PT1M", 0.5, "PT0.5M"}, + {"PT1S", 0.5, "PT0.5S"}, + {"PT1H", 1.0 / 3600, "PT1S"}, + {"P1Y2M3DT4H5M6S", 2, "P2Y4M6DT8H10M12S"}, + {"P2Y4M6DT8H10M12S", -0.5, "-P1Y2M3DT4H5M6S"}, + {"-P2Y4M6DT8H10M12S", 0.5, "-P1Y2M3DT4H5M6S"}, + {"-P2Y4M6DT8H10M12S", -0.5, "P1Y2M3DT4H5M6S"}, + {"PT1M", 60, "PT1H"}, + {"PT1S", 60, "PT1M"}, + {"PT1S", 86400, "PT24H"}, + {"PT1S", 86400000, "P1000D"}, + {"P365.5D", 10, "P10Y2.5D"}, + //{"P365.5D", 0.1, "P36DT12H"}, + } + for i, c := range cases { + s := MustParse(c.one).Scale(c.m) + g.Expect(s).To(Equal(MustParse(c.expect)), info(i, c.expect)) + } +} + +func TestPeriodAdd(t *testing.T) { + g := NewGomegaWithT(t) + + cases := []struct { + one, two string + expect string + }{ + {"P0D", "P0D", "P0D"}, + {"P1D", "P1D", "P2D"}, + {"P1M", "P1M", "P2M"}, + {"P1Y", "P1Y", "P2Y"}, + {"PT1H", "PT1H", "PT2H"}, + {"PT1M", "PT1M", "PT2M"}, + {"PT1S", "PT1S", "PT2S"}, + {"P1Y2M3DT4H5M6S", "P6Y5M4DT3H2M1S", "P7Y7M7DT7H7M7S"}, + {"P7Y7M7DT7H7M7S", "-P7Y7M7DT7H7M7S", "P0D"}, + } + for i, c := range cases { + s := MustParse(c.one).Add(MustParse(c.two)) + g.Expect(s).To(Equal(MustParse(c.expect)), info(i, c.expect)) + } +} + +func TestPeriodAddToTime(t *testing.T) { + g := NewGomegaWithT(t) + + const ms = 1000000 + const sec = 1000 * ms + const min = 60 * sec + const hr = 60 * min + + // A conveniently round number (14 July 2017 @ 2:40am UTC) + var t0 = time.Unix(1500000000, 0).UTC() + + cases := []struct { + value string + result time.Time + precise bool + }{ + // precise cases + {"P0D", t0, true}, + {"PT1S", t0.Add(sec), true}, + {"PT0.1S", t0.Add(100 * ms), true}, + {"-PT0.1S", t0.Add(-100 * ms), true}, + {"PT3276S", t0.Add(3276 * sec), true}, + {"PT1M", t0.Add(60 * sec), true}, + {"PT0.1M", t0.Add(6 * sec), true}, + {"PT3276M", t0.Add(3276 * min), true}, + {"PT1H", t0.Add(hr), true}, + {"PT0.1H", t0.Add(6 * min), true}, + {"PT3276H", t0.Add(3276 * hr), true}, + {"P1D", t0.AddDate(0, 0, 1), true}, + {"P3276D", t0.AddDate(0, 0, 3276), true}, + {"P1M", t0.AddDate(0, 1, 0), true}, + {"P3276M", t0.AddDate(0, 3276, 0), true}, + {"P1Y", t0.AddDate(1, 0, 0), true}, + {"-P1Y", t0.AddDate(-1, 0, 0), true}, + {"P3276Y", t0.AddDate(3276, 0, 0), true}, // near the upper limit of range + {"-P3276Y", t0.AddDate(-3276, 0, 0), true}, // near the lower limit of range + // approximate cases + {"P0.1D", t0.Add(144 * min), false}, + {"-P0.1D", t0.Add(-144 * min), false}, + {"P0.1M", t0.Add(oneMonthApprox / 10), false}, + {"P0.1Y", t0.Add(oneYearApprox / 10), false}, + // after normalisation, this period is one month and 9.2 days + {"-P0.1Y0.1M0.1D", t0.Add(-oneMonthApprox - (13248 * min)), false}, + } + for i, c := range cases { + p := MustParse(c.value) + t1, prec := p.AddTo(t0) + g.Expect(t1).To(Equal(c.result), info(i, c.value)) + g.Expect(prec).To(Equal(c.precise), info(i, c.value)) + } +} diff --git a/period/designator.go b/period/designator.go new file mode 100644 index 00000000..6d836ec4 --- /dev/null +++ b/period/designator.go @@ -0,0 +1,55 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package period + +type ymdDesignator byte +type hmsDesignator byte + +const ( + Year ymdDesignator = 'Y' + Month ymdDesignator = 'M' + Week ymdDesignator = 'W' + Day ymdDesignator = 'D' + + Hour hmsDesignator = 'H' + Minute hmsDesignator = 'M' + Second hmsDesignator = 'S' +) + +func (d ymdDesignator) IsOneOf(xx ...ymdDesignator) bool { + for _, x := range xx { + if x == d { + return true + } + } + return false +} + +func (d ymdDesignator) IsNotOneOf(xx ...ymdDesignator) bool { + for _, x := range xx { + if x == d { + return false + } + } + return true +} + +func (d hmsDesignator) IsOneOf(xx ...hmsDesignator) bool { + for _, x := range xx { + if x == d { + return true + } + } + return false +} + +func (d hmsDesignator) IsNotOneOf(xx ...hmsDesignator) bool { + for _, x := range xx { + if x == d { + return false + } + } + return true +} diff --git a/period/format.go b/period/format.go index b341512a..3b095292 100644 --- a/period/format.go +++ b/period/format.go @@ -13,10 +13,17 @@ import ( ) // Format converts the period to human-readable form using the default localisation. +// Multiples of 7 days are shown as weeks. func (period Period) Format() string { return period.FormatWithPeriodNames(PeriodYearNames, PeriodMonthNames, PeriodWeekNames, PeriodDayNames, PeriodHourNames, PeriodMinuteNames, PeriodSecondNames) } +// FormatWithoutWeeks converts the period to human-readable form using the default localisation. +// Multiples of 7 days are not shown as weeks. +func (period Period) FormatWithoutWeeks() string { + return period.FormatWithPeriodNames(PeriodYearNames, PeriodMonthNames, plural.Plurals{}, PeriodDayNames, PeriodHourNames, PeriodMinuteNames, PeriodSecondNames) +} + // FormatWithPeriodNames converts the period to human-readable form in a localisable way. func (period Period) FormatWithPeriodNames(yearNames, monthNames, weekNames, dayNames, hourNames, minNames, secNames plural.Plurals) string { period = period.Abs() @@ -94,14 +101,14 @@ func (p64 period64) String() string { buf.WriteByte('P') - writeField64(buf, p64.years, 'Y') - writeField64(buf, p64.months, 'M') + writeField64(buf, p64.years, byte(Year)) + writeField64(buf, p64.months, byte(Month)) if p64.days != 0 { if p64.days%70 == 0 { - writeField64(buf, p64.days/7, 'W') + writeField64(buf, p64.days/7, byte(Week)) } else { - writeField64(buf, p64.days, 'D') + writeField64(buf, p64.days, byte(Day)) } } @@ -109,9 +116,9 @@ func (p64 period64) String() string { buf.WriteByte('T') } - writeField64(buf, p64.hours, 'H') - writeField64(buf, p64.minutes, 'M') - writeField64(buf, p64.seconds, 'S') + writeField64(buf, p64.hours, byte(Hour)) + writeField64(buf, p64.minutes, byte(Minute)) + writeField64(buf, p64.seconds, byte(Second)) return buf.String() } diff --git a/period/parse.go b/period/parse.go index 3b6a4d4d..ab01599e 100644 --- a/period/parse.go +++ b/period/parse.go @@ -12,8 +12,10 @@ import ( // MustParse is as per Parse except that it panics if the string cannot be parsed. // This is intended for setup code; don't use it for user inputs. -func MustParse(value string) Period { - d, err := Parse(value) +// By default, the value is normalised. +// Normalisation can be disabled using the optional flag. +func MustParse(value string, normalise ...bool) Period { + d, err := Parse(value, normalise...) if err != nil { panic(err) } @@ -24,16 +26,18 @@ func MustParse(value string) Period { // // In addition, a plus or minus sign can precede the period, e.g. "-P10D" // -// The value is normalised, e.g. multiple of 12 months become years so "P24M" -// is the same as "P2Y". However, this is done without loss of precision, so -// for example whole numbers of days do not contribute to the months tally +// By default, the value is normalised, e.g. multiple of 12 months become years +// so "P24M" is the same as "P2Y". However, this is done without loss of precision, +// so for example whole numbers of days do not contribute to the months tally // because the number of days per month is variable. // +// Normalisation can be disabled using the optional flag. +// // The zero value can be represented in several ways: all of the following // are equivalent: "P0Y", "P0M", "P0W", "P0D", "PT0H", PT0M", PT0S", and "P0". // The canonical zero is "P0D". -func Parse(period string) (Period, error) { - return ParseWithNormalise(period, true) +func Parse(period string, normalise ...bool) (Period, error) { + return ParseWithNormalise(period, len(normalise) == 0 || normalise[0]) } // ParseWithNormalise parses strings that specify periods using ISO-8601 rules @@ -50,6 +54,9 @@ func Parse(period string) (Period, error) { // The zero value can be represented in several ways: all of the following // are equivalent: "P0Y", "P0M", "P0W", "P0D", "PT0H", PT0M", PT0S", and "P0". // The canonical zero is "P0D". +// +// This method is deprecated and should not be used. It may be removed in a +// future version. func ParseWithNormalise(period string, normalise bool) (Period, error) { if period == "" || period == "-" || period == "+" { return Period{}, fmt.Errorf("cannot parse a blank string as a period") diff --git a/period/period.go b/period/period.go index cb1d7496..737c1770 100644 --- a/period/period.go +++ b/period/period.go @@ -51,6 +51,8 @@ type Period struct { // need to. // // All the parameters must have the same sign (otherwise a panic occurs). +// Because this implementation uses int16 internally, the paramters must +// be within the range ± 2^16 / 10. func NewYMD(years, months, days int) Period { return New(years, months, days, 0, 0, 0) } @@ -60,6 +62,8 @@ func NewYMD(years, months, days int) Period { // if you need to. // // All the parameters must have the same sign (otherwise a panic occurs). +// Because this implementation uses int16 internally, the paramters must +// be within the range ± 2^16 / 10. func NewHMS(hours, minutes, seconds int) Period { return New(0, 0, 0, hours, minutes, seconds) } @@ -81,8 +85,6 @@ func New(years, months, days, hours, minutes, seconds int) Period { years, months, days, hours, minutes, seconds)) } -// TODO NewFloat - // NewOf converts a time duration to a Period, and also indicates whether the conversion is precise. // Any time duration that spans more than ± 3276 hours will be approximated by assuming that there // are 24 hours per day, 365.2425 days per year (as per Gregorian calendar rules), and a month @@ -258,66 +260,16 @@ func (period Period) absNeg() (Period, bool) { return period, false } -// Negate changes the sign of the period. -func (period Period) Negate() Period { - return Period{-period.years, -period.months, -period.days, -period.hours, -period.minutes, -period.seconds} -} - -// Add adds two periods together. Use this method along with Negate in order to subtract periods. -// -// The result is not normalised and may overflow arithmetically (to make this unlikely, use Normalise on -// the inputs before adding them). -func (period Period) Add(that Period) Period { - return Period{ - period.years + that.years, - period.months + that.months, - period.days + that.days, - period.hours + that.hours, - period.minutes + that.minutes, - period.seconds + that.seconds, +func (period Period) condNegate(neg bool) Period { + if neg { + return period.Negate() } + return period } -// Scale a period by a multiplication factor. Obviously, this can both enlarge and shrink it, -// and change the sign if negative. The result is normalised, but integer overflows are silently -// ignored. -// -// Bear in mind that the internal representation is limited by fixed-point arithmetic with one -// decimal place; each field is only int16. -// -// Known issue: scaling by a large reduction factor (i.e. much less than one) doesn't work properly. -func (period Period) Scale(factor float32) Period { - result, _ := period.ScaleWithOverflowCheck(factor) - return result -} - -// ScaleWithOverflowCheck a period by a multiplication factor. Obviously, this can both enlarge and shrink it, -// and change the sign if negative. The result is normalised. An error is returned if integer overflow -// happened. -// -// Bear in mind that the internal representation is limited by fixed-point arithmetic with one -// decimal place; each field is only int16. -// -// Known issue: scaling by a large reduction factor (i.e. much less than one) doesn't work properly. -func (period Period) ScaleWithOverflowCheck(factor float32) (Period, error) { - ap, neg := period.absNeg() - - if -0.5 < factor && factor < 0.5 { - d, pr1 := ap.Duration() - mul := float64(d) * float64(factor) - p2, pr2 := NewOf(time.Duration(mul)) - return p2.Normalise(pr1 && pr2), nil - } - - y := int64(float32(ap.years) * factor) - m := int64(float32(ap.months) * factor) - d := int64(float32(ap.days) * factor) - hh := int64(float32(ap.hours) * factor) - mm := int64(float32(ap.minutes) * factor) - ss := int64(float32(ap.seconds) * factor) - - p64 := &period64{years: y, months: m, days: d, hours: hh, minutes: mm, seconds: ss, neg: neg} - return p64.normalise64(true).toPeriod() +// Negate changes the sign of the period. +func (period Period) Negate() Period { + return Period{-period.years, -period.months, -period.days, -period.hours, -period.minutes, -period.seconds} } func absInt16(v int16) int16 { @@ -444,29 +396,7 @@ func (period Period) SecondsFloat() float32 { return float32(period.seconds) / 10 } -// AddTo adds the period to a time, returning the result. -// A flag is also returned that is true when the conversion was precise and false otherwise. -// -// When the period specifies hours, minutes and seconds only, the result is precise. -// Also, when the period specifies whole years, months and days (i.e. without fractions), the -// result is precise. However, when years, months or days contains fractions, the result -// is only an approximation (it assumes that all days are 24 hours and every year is 365.2425 -// days, as per Gregorian calendar rules). -func (period Period) AddTo(t time.Time) (time.Time, bool) { - wholeYears := (period.years % 10) == 0 - wholeMonths := (period.months % 10) == 0 - wholeDays := (period.days % 10) == 0 - - if wholeYears && wholeMonths && wholeDays { - // in this case, time.AddDate provides an exact solution - stE3 := totalSecondsE3(period) - t1 := t.AddDate(int(period.years/10), int(period.months/10), int(period.days/10)) - return t1.Add(stE3 * time.Millisecond), true - } - - d, precise := period.Duration() - return t.Add(d), precise -} +//------------------------------------------------------------------------------------------------- // DurationApprox converts a period to the equivalent duration in nanoseconds. // When the period specifies hours, minutes and seconds only, the result is precise. diff --git a/period/period_test.go b/period/period_test.go index e49ca251..dab348ae 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -335,58 +335,6 @@ func TestPeriodFloatComponents(t *testing.T) { } } -func TestPeriodAddToTime(t *testing.T) { - g := NewGomegaWithT(t) - - const ms = 1000000 - const sec = 1000 * ms - const min = 60 * sec - const hr = 60 * min - - // A conveniently round number (14 July 2017 @ 2:40am UTC) - var t0 = time.Unix(1500000000, 0).UTC() - - cases := []struct { - value string - result time.Time - precise bool - }{ - // precise cases - {"P0D", t0, true}, - {"PT1S", t0.Add(sec), true}, - {"PT0.1S", t0.Add(100 * ms), true}, - {"-PT0.1S", t0.Add(-100 * ms), true}, - {"PT3276S", t0.Add(3276 * sec), true}, - {"PT1M", t0.Add(60 * sec), true}, - {"PT0.1M", t0.Add(6 * sec), true}, - {"PT3276M", t0.Add(3276 * min), true}, - {"PT1H", t0.Add(hr), true}, - {"PT0.1H", t0.Add(6 * min), true}, - {"PT3276H", t0.Add(3276 * hr), true}, - {"P1D", t0.AddDate(0, 0, 1), true}, - {"P3276D", t0.AddDate(0, 0, 3276), true}, - {"P1M", t0.AddDate(0, 1, 0), true}, - {"P3276M", t0.AddDate(0, 3276, 0), true}, - {"P1Y", t0.AddDate(1, 0, 0), true}, - {"-P1Y", t0.AddDate(-1, 0, 0), true}, - {"P3276Y", t0.AddDate(3276, 0, 0), true}, // near the upper limit of range - {"-P3276Y", t0.AddDate(-3276, 0, 0), true}, // near the lower limit of range - // approximate cases - {"P0.1D", t0.Add(144 * min), false}, - {"-P0.1D", t0.Add(-144 * min), false}, - {"P0.1M", t0.Add(oneMonthApprox / 10), false}, - {"P0.1Y", t0.Add(oneYearApprox / 10), false}, - // after normalisation, this period is one month and 9.2 days - {"-P0.1Y0.1M0.1D", t0.Add(-oneMonthApprox - (13248 * min)), false}, - } - for i, c := range cases { - p := MustParse(c.value) - t1, prec := p.AddTo(t0) - g.Expect(t1).To(Equal(c.result), info(i, c.value)) - g.Expect(prec).To(Equal(c.precise), info(i, c.value)) - } -} - func TestPeriodToDuration(t *testing.T) { g := NewGomegaWithT(t) @@ -813,74 +761,6 @@ func TestPeriodFormat(t *testing.T) { } } -func TestPeriodScale(t *testing.T) { - g := NewGomegaWithT(t) - - cases := []struct { - one string - m float32 - expect string - }{ - {"P0D", 2, "P0D"}, - {"P1D", 2, "P2D"}, - {"P1D", 0, "P0D"}, - {"P1D", 365, "P365D"}, - {"P1M", 2, "P2M"}, - {"P1M", 12, "P1Y"}, - //TODO {"P1Y3M", 1.0/15, "P1M"}, - {"P1Y", 2, "P2Y"}, - {"PT1H", 2, "PT2H"}, - {"PT1M", 2, "PT2M"}, - {"PT1S", 2, "PT2S"}, - {"P1D", 0.5, "P0.5D"}, - {"P1M", 0.5, "P0.5M"}, - {"P1Y", 0.5, "P0.5Y"}, - {"PT1H", 0.5, "PT0.5H"}, - {"PT1H", 0.1, "PT6M"}, - //TODO {"PT1H", 0.01, "PT36S"}, - {"PT1M", 0.5, "PT0.5M"}, - {"PT1S", 0.5, "PT0.5S"}, - {"PT1H", 1.0 / 3600, "PT1S"}, - {"P1Y2M3DT4H5M6S", 2, "P2Y4M6DT8H10M12S"}, - {"P2Y4M6DT8H10M12S", -0.5, "-P1Y2M3DT4H5M6S"}, - {"-P2Y4M6DT8H10M12S", 0.5, "-P1Y2M3DT4H5M6S"}, - {"-P2Y4M6DT8H10M12S", -0.5, "P1Y2M3DT4H5M6S"}, - {"PT1M", 60, "PT1H"}, - {"PT1S", 60, "PT1M"}, - {"PT1S", 86400, "PT24H"}, - {"PT1S", 86400000, "P1000D"}, - {"P365.5D", 10, "P10Y2.5D"}, - //{"P365.5D", 0.1, "P36DT12H"}, - } - for i, c := range cases { - s := MustParse(c.one).Scale(c.m) - g.Expect(s).To(Equal(MustParse(c.expect)), info(i, c.expect)) - } -} - -func TestPeriodAdd(t *testing.T) { - g := NewGomegaWithT(t) - - cases := []struct { - one, two string - expect string - }{ - {"P0D", "P0D", "P0D"}, - {"P1D", "P1D", "P2D"}, - {"P1M", "P1M", "P2M"}, - {"P1Y", "P1Y", "P2Y"}, - {"PT1H", "PT1H", "PT2H"}, - {"PT1M", "PT1M", "PT2M"}, - {"PT1S", "PT1S", "PT2S"}, - {"P1Y2M3DT4H5M6S", "P6Y5M4DT3H2M1S", "P7Y7M7DT7H7M7S"}, - {"P7Y7M7DT7H7M7S", "-P7Y7M7DT7H7M7S", "P0D"}, - } - for i, c := range cases { - s := MustParse(c.one).Add(MustParse(c.two)) - g.Expect(s).To(Equal(MustParse(c.expect)), info(i, c.expect)) - } -} - func TestPeriodFormatWithoutWeeks(t *testing.T) { g := NewGomegaWithT(t) From d2bc4afd7cd27d07a25daa88f0ed92822a1ec19e Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Fri, 4 Sep 2020 14:00:22 +0100 Subject: [PATCH 136/165] Period parsing improved; extra tests added --- period/arithmetic.go | 12 +- period/arithmetic_test.go | 52 +++++++- period/format.go | 2 +- period/marshal_test.go | 4 +- period/parse.go | 242 +++++++++++++++++++++++--------------- period/period.go | 12 +- period/period64.go | 12 +- period/period_test.go | 83 +++++++------ 8 files changed, 262 insertions(+), 157 deletions(-) diff --git a/period/arithmetic.go b/period/arithmetic.go index 55993fe0..6141c091 100644 --- a/period/arithmetic.go +++ b/period/arithmetic.go @@ -82,12 +82,12 @@ func (period Period) ScaleWithOverflowCheck(factor float32) (Period, error) { return p2.Normalise(pr1 && pr2), nil } - y := int64(float32(ap.years) * factor) - m := int64(float32(ap.months) * factor) - d := int64(float32(ap.days) * factor) - hh := int64(float32(ap.hours) * factor) - mm := int64(float32(ap.minutes) * factor) - ss := int64(float32(ap.seconds) * factor) + y := int(float32(ap.years) * factor) + m := int(float32(ap.months) * factor) + d := int(float32(ap.days) * factor) + hh := int(float32(ap.hours) * factor) + mm := int(float32(ap.minutes) * factor) + ss := int(float32(ap.seconds) * factor) p64 := &period64{years: y, months: m, days: d, hours: hh, minutes: mm, seconds: ss, neg: neg} return p64.normalise64(true).toPeriod() diff --git a/period/arithmetic_test.go b/period/arithmetic_test.go index 0122ded1..42a33dd4 100644 --- a/period/arithmetic_test.go +++ b/period/arithmetic_test.go @@ -5,6 +5,7 @@ package period import ( + "fmt" "testing" "time" @@ -120,8 +121,6 @@ func TestPeriodAddToTime(t *testing.T) { {"-P0.1D", t0.Add(-144 * min), false}, {"P0.1M", t0.Add(oneMonthApprox / 10), false}, {"P0.1Y", t0.Add(oneYearApprox / 10), false}, - // after normalisation, this period is one month and 9.2 days - {"-P0.1Y0.1M0.1D", t0.Add(-oneMonthApprox - (13248 * min)), false}, } for i, c := range cases { p := MustParse(c.value) @@ -130,3 +129,52 @@ func TestPeriodAddToTime(t *testing.T) { g.Expect(prec).To(Equal(c.precise), info(i, c.value)) } } + +func expectValid(t *testing.T, period Period, hint interface{}) Period { + t.Helper() + g := NewGomegaWithT(t) + info := fmt.Sprintf("%v: invalid: %#v", hint, period) + + // check all the signs are consistent + nPoz := pos(period.years) + pos(period.months) + pos(period.days) + pos(period.hours) + pos(period.minutes) + pos(period.seconds) + nNeg := neg(period.years) + neg(period.months) + neg(period.days) + neg(period.hours) + neg(period.minutes) + neg(period.seconds) + if nPoz > 0 && nNeg > 0 { + t.Errorf("%s: inconsistent signs in\n%#v", info, period) + } + + // only one field must have a fraction + yearsFraction := fraction(period.years) + //monthsFraction := fraction(period.months) + //daysFraction := fraction(period.days) + //hoursFraction := fraction(period.hours) + //minutesFraction := fraction(period.minutes) + //secondsFraction := fraction(period.seconds) + + if yearsFraction > 0 { + g.Expect(period.months).To(BeZero(), info) + g.Expect(period.days).To(BeZero(), info) + g.Expect(period.hours).To(BeZero(), info) + g.Expect(period.minutes).To(BeZero(), info) + g.Expect(period.seconds).To(BeZero(), info) + } + + return period +} + +func fraction(i int16) int { + return int(i) % 10 +} + +func pos(i int16) int { + if i > 0 { + return 1 + } + return 0 +} + +func neg(i int16) int { + if i < 0 { + return 1 + } + return 0 +} diff --git a/period/format.go b/period/format.go index 3b095292..df517b58 100644 --- a/period/format.go +++ b/period/format.go @@ -123,7 +123,7 @@ func (p64 period64) String() string { return buf.String() } -func writeField64(w io.Writer, field int64, designator byte) { +func writeField64(w io.Writer, field int, designator byte) { if field != 0 { if field%10 != 0 { fmt.Fprintf(w, "%g", float32(field)/10) diff --git a/period/marshal_test.go b/period/marshal_test.go index 37908b57..2f25fc71 100644 --- a/period/marshal_test.go +++ b/period/marshal_test.go @@ -114,8 +114,8 @@ func TestInvalidPeriodText(t *testing.T) { want string }{ {``, `cannot parse a blank string as a period`}, - {`not-a-period`, `expected 'P' period mark at the start: not-a-period`}, - {`P000`, `unexpected remaining components 000: P000`}, + {`not-a-period`, `not-a-period: expected 'P' period mark at the start`}, + {`P000`, `P000: missing designator at the end`}, } for i, c := range cases { var p Period diff --git a/period/parse.go b/period/parse.go index ab01599e..c6fb8d0d 100644 --- a/period/parse.go +++ b/period/parse.go @@ -43,18 +43,6 @@ func Parse(period string, normalise ...bool) (Period, error) { // ParseWithNormalise parses strings that specify periods using ISO-8601 rules // with an option to specify whether to normalise parsed period components. // -// In addition, a plus or minus sign can precede the period, e.g. "-P10D" - -// The returned value is only normalised when normalise is set to `true`, and -// normalisation will convert e.g. multiple of 12 months into years so "P24M" -// is the same as "P2Y". However, this is done without loss of precision, so -// for example whole numbers of days do not contribute to the months tally -// because the number of days per month is variable. -// -// The zero value can be represented in several ways: all of the following -// are equivalent: "P0Y", "P0M", "P0W", "P0D", "PT0H", PT0M", PT0S", and "P0". -// The canonical zero is "P0D". -// // This method is deprecated and should not be used. It may be removed in a // future version. func ParseWithNormalise(period string, normalise bool) (Period, error) { @@ -66,122 +54,184 @@ func ParseWithNormalise(period string, normalise bool) (Period, error) { return Period{}, nil } - result := period64{input: period} - pcopy := period - if pcopy[0] == '-' { - result.neg = true - pcopy = pcopy[1:] - } else if pcopy[0] == '+' { - pcopy = pcopy[1:] + p64, err := parse(period, normalise) + if err != nil { + return Period{}, err } + return p64.toPeriod() +} - if pcopy[0] != 'P' { - return Period{}, fmt.Errorf("expected 'P' period mark at the start: %s", period) +func parse(period string, normalise bool) (*period64, error) { + neg := false + remaining := period + if remaining[0] == '-' { + neg = true + remaining = remaining[1:] + } else if remaining[0] == '+' { + remaining = remaining[1:] } - pcopy = pcopy[1:] - st := parseState{period, pcopy, false, nil} - t := strings.IndexByte(pcopy, 'T') - if t >= 0 { - st.pcopy = pcopy[t+1:] + if remaining[0] != 'P' { + return nil, fmt.Errorf("%s: expected 'P' period mark at the start", period) + } + remaining = remaining[1:] - result.hours, st = parseField(st, 'H') - if st.err != nil { - return Period{}, fmt.Errorf("expected a number before the 'H' designator: %s", period) - } + var number, weekValue, prevFraction int + result := &period64{input: period, neg: neg} + var years, months, weeks, days, hours, minutes, seconds itemState + var designator, prevDesignator byte + var err error + nComponents := 0 - result.minutes, st = parseField(st, 'M') - if st.err != nil { - return Period{}, fmt.Errorf("expected a number before the 'M' designator: %s", period) - } + years, months, weeks, days = Armed, Armed, Armed, Armed - result.seconds, st = parseField(st, 'S') - if st.err != nil { - return Period{}, fmt.Errorf("expected a number before the 'S' designator: %s", period) - } + isHMS := false + for len(remaining) > 0 { + if remaining[0] == 'T' { + if isHMS { + return nil, fmt.Errorf("%s: 'T' designator cannot occur more than once", period) + } + isHMS = true - if len(st.pcopy) != 0 { - return Period{}, fmt.Errorf("unexpected remaining components %s: %s", st.pcopy, period) - } + years, months, weeks, days = Unready, Unready, Unready, Unready + hours, minutes, seconds = Armed, Armed, Armed - st.pcopy = pcopy[:t] - } + remaining = remaining[1:] - result.years, st = parseField(st, 'Y') - if st.err != nil { - return Period{}, fmt.Errorf("expected a number before the 'Y' designator: %s", period) - } - result.months, st = parseField(st, 'M') - if st.err != nil { - return Period{}, fmt.Errorf("expected a number before the 'M' designator: %s", period) - } - weeks, st := parseField(st, 'W') - if st.err != nil { - return Period{}, fmt.Errorf("expected a number before the 'W' designator: %s", period) + } else { + number, designator, remaining, err = parseNextField(remaining, period) + if err != nil { + return nil, err + } + + fraction := number % 10 + if prevFraction != 0 && fraction != 0 { + return nil, fmt.Errorf("%s: '%c' & '%c' only the last field can have a fraction", period, prevDesignator, designator) + } + + switch designator { + case 'Y': + years, err = years.testAndSet(number, 'Y', result, &result.years) + case 'W': + weeks, err = weeks.testAndSet(number, 'W', result, &weekValue) + case 'D': + days, err = days.testAndSet(number, 'D', result, &result.days) + case 'H': + hours, err = hours.testAndSet(number, 'H', result, &result.hours) + case 'S': + seconds, err = seconds.testAndSet(number, 'S', result, &result.seconds) + case 'M': + if isHMS { + minutes, err = minutes.testAndSet(number, 'M', result, &result.minutes) + } else { + months, err = months.testAndSet(number, 'M', result, &result.months) + } + default: + return nil, fmt.Errorf("%s: expected a number not '%c'", period, designator) + } + nComponents++ + + if err != nil { + return nil, err + } + + prevFraction = fraction + prevDesignator = designator + } } - days, st := parseField(st, 'D') - if st.err != nil { - return Period{}, fmt.Errorf("expected a number before the 'D' designator: %s", period) + if nComponents == 0 { + return nil, fmt.Errorf("%s: expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' designator", period) } - if len(st.pcopy) != 0 { - return Period{}, fmt.Errorf("unexpected remaining components %s: %s", st.pcopy, period) + result.days += weekValue * 7 + + if normalise { + result = result.normalise64(true) } - result.days = weeks*7 + days - //fmt.Printf("%#v\n", st) + return result, nil +} - if !st.ok { - return Period{}, fmt.Errorf("expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' designator: %s", period) - } +//------------------------------------------------------------------------------------------------- - if normalise { - return result.normalise64(true).toPeriod() +type itemState int + +const ( + Unready itemState = iota + Armed + Set +) + +func (i itemState) testAndSet(number int, designator byte, result *period64, value *int) (itemState, error) { + switch i { + case Unready: + return i, fmt.Errorf("%s: '%c' designator cannot occur here", result.input, designator) + case Set: + return i, fmt.Errorf("%s: '%c' designator cannot occur more than once", result.input, designator) } - return result.toPeriod() + *value = number + return Set, nil } -type parseState struct { - period, pcopy string - ok bool - err error -} +//------------------------------------------------------------------------------------------------- -func parseField(st parseState, mark byte) (int64, parseState) { - //fmt.Printf("%c %#v\n", mark, st) - r := int64(0) - m := strings.IndexByte(st.pcopy, mark) - if m > 0 { - r, st.err = parseDecimalFixedPoint(st.pcopy[:m], st.period) - if st.err != nil { - return 0, st - } - st.pcopy = st.pcopy[m+1:] - st.ok = true +func parseNextField(str, original string) (int, byte, string, error) { + i := scanDigits(str) + if i < 0 { + return 0, 0, "", fmt.Errorf("%s: missing designator at the end", original) } - return r, st + + des := str[i] + number, err := parseDecimalNumber(str[:i], original, des) + return number, des, str[i+1:], err } -// Fixed-point three decimal places -func parseDecimalFixedPoint(s, original string) (int64, error) { - //was := s - dec := strings.IndexByte(s, '.') +// Fixed-point one decimal place +func parseDecimalNumber(number, original string, des byte) (int, error) { + dec := strings.IndexByte(number, '.') if dec < 0 { - dec = strings.IndexByte(s, ',') + dec = strings.IndexByte(number, ',') } + var integer, fraction int + var err error if dec >= 0 { - dp := len(s) - dec - if dp > 1 { - s = s[:dec] + s[dec+1:dec+2] - } else { - s = s[:dec] + s[dec+1:] + "0" + integer, err = strconv.Atoi(number[:dec]) + if err == nil { + number = number[dec+1:] + switch len(number) { + case 0: // skip + case 1: + fraction, err = strconv.Atoi(number) + default: + fraction, err = strconv.Atoi(number[:1]) + } } } else { - s = s + "0" + integer, err = strconv.Atoi(number) + } + + if err != nil { + return 0, fmt.Errorf("%s: expected a number but found '%c'", original, des) + } + + return integer*10 + fraction, err +} + +// scanDigits finds the first non-digit byte after a given starting point. +// Note that it does not care about runes or UTF-8 encoding; it assumes that +// a period string is always valid ASCII as well as UTF-8. +func scanDigits(s string) int { + for i, c := range s { + if !isDigit(c) { + return i + } } + return -1 +} - return strconv.ParseInt(s, 10, 64) +func isDigit(c rune) bool { + return ('0' <= c && c <= '9') || c == '.' || c == ',' } diff --git a/period/period.go b/period/period.go index 737c1770..26a9ad33 100644 --- a/period/period.go +++ b/period/period.go @@ -9,13 +9,13 @@ import ( "time" ) -const daysPerYearE4 int64 = 3652425 // 365.2425 days by the Gregorian rule -const daysPerMonthE4 int64 = 304369 // 30.4369 days per month -const daysPerMonthE6 int64 = 30436875 // 30.436875 days per month +const daysPerYearE4 = 3652425 // 365.2425 days by the Gregorian rule +const daysPerMonthE4 = 304369 // 30.4369 days per month +const daysPerMonthE6 = 30436875 // 30.436875 days per month -const oneE4 int64 = 10000 -const oneE6 int64 = 1000000 -const oneE7 int64 = 10000000 +const oneE4 = 10000 +const oneE6 = 1000000 +const oneE7 = 10000000 const hundredMs = 100 * time.Millisecond diff --git a/period/period64.go b/period/period64.go index 941e33db..f7d79430 100644 --- a/period/period64.go +++ b/period/period64.go @@ -8,7 +8,7 @@ import ( // used for stages in arithmetic type period64 struct { // always positive values - years, months, days, hours, minutes, seconds int64 + years, months, days, hours, minutes, seconds int // true if the period is negative neg bool input string @@ -17,15 +17,15 @@ type period64 struct { func (period Period) toPeriod64(input string) *period64 { if period.IsNegative() { return &period64{ - years: int64(-period.years), months: int64(-period.months), days: int64(-period.days), - hours: int64(-period.hours), minutes: int64(-period.minutes), seconds: int64(-period.seconds), + years: int(-period.years), months: int(-period.months), days: int(-period.days), + hours: int(-period.hours), minutes: int(-period.minutes), seconds: int(-period.seconds), neg: true, input: input, } } return &period64{ - years: int64(period.years), months: int64(period.months), days: int64(period.days), - hours: int64(period.hours), minutes: int64(period.minutes), seconds: int64(period.seconds), + years: int(period.years), months: int(period.months), days: int(period.days), + hours: int(period.hours), minutes: int(period.minutes), seconds: int(period.seconds), input: input, } } @@ -55,7 +55,7 @@ func (p64 *period64) toPeriod() (Period, error) { if p64.input == "" { p64.input = p64.String() } - return Period{}, fmt.Errorf("integer overflow occurred in %s: %s", strings.Join(f, ","), p64.input) + return Period{}, fmt.Errorf("%s: integer overflow occurred in %s", p64.input, strings.Join(f, ",")) } if p64.neg { diff --git a/period/period_test.go b/period/period_test.go index dab348ae..4411ae41 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -27,46 +27,49 @@ func TestParseErrors(t *testing.T) { expvalue string }{ {"", false, "cannot parse a blank string as a period", ""}, - {"XY", false, "expected 'P' period mark at the start: ", "XY"}, - {"PxY", false, "expected a number before the 'Y' designator: ", "PxY"}, - {"PxW", false, "expected a number before the 'W' designator: ", "PxW"}, - {"PxD", false, "expected a number before the 'D' designator: ", "PxD"}, - {"PTxH", false, "expected a number before the 'H' designator: ", "PTxH"}, - {"PTxM", false, "expected a number before the 'M' designator: ", "PTxM"}, - {"PTxS", false, "expected a number before the 'S' designator: ", "PTxS"}, - {"P1HT1M", false, "unexpected remaining components 1H: ", "P1HT1M"}, - {"PT1Y", false, "unexpected remaining components 1Y: ", "PT1Y"}, - {"P1S", false, "unexpected remaining components 1S: ", "P1S"}, + {`P000`, false, `: missing designator at the end`, "P000"}, + {"XY", false, ": expected 'P' period mark at the start", "XY"}, + {"PxY", false, ": expected a number but found 'x'", "PxY"}, + {"PxW", false, ": expected a number but found 'x'", "PxW"}, + {"PxD", false, ": expected a number but found 'x'", "PxD"}, + {"PTxH", false, ": expected a number but found 'x'", "PTxH"}, + {"PTxM", false, ": expected a number but found 'x'", "PTxM"}, + {"PTxS", false, ": expected a number but found 'x'", "PTxS"}, + {"P1HT1M", false, ": 'H' designator cannot occur here", "P1HT1M"}, + {"PT1Y", false, ": 'Y' designator cannot occur here", "PT1Y"}, + {"P1S", false, ": 'S' designator cannot occur here", "P1S"}, + {"P1D2D", false, ": 'D' designator cannot occur more than once", "P1D2D"}, + {"PT1HT1S", false, ": 'T' designator cannot occur more than once", "PT1HT1S"}, + {"P0.1YT0.1S", false, ": 'Y' & 'S' only the last field can have a fraction", "P0.1YT0.1S"}, + {"P", false, ": expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' designator", "P"}, // integer overflow - {"P32768Y", false, "integer overflow occurred in years: ", "P32768Y"}, - {"P32768M", false, "integer overflow occurred in months: ", "P32768M"}, - {"P32768D", false, "integer overflow occurred in days: ", "P32768D"}, - {"PT32768H", false, "integer overflow occurred in hours: ", "PT32768H"}, - {"PT32768M", false, "integer overflow occurred in minutes: ", "PT32768M"}, - {"PT32768S", false, "integer overflow occurred in seconds: ", "PT32768S"}, - {"PT32768H32768M32768S", false, "integer overflow occurred in hours,minutes,seconds: ", "PT32768H32768M32768S"}, - {"PT103412160000S", false, "integer overflow occurred in seconds: ", "PT103412160000S"}, - {"P39324M", true, "integer overflow occurred in years: ", "P39324M"}, - {"P1196900D", true, "integer overflow occurred in years: ", "P1196900D"}, - {"PT28725600H", true, "integer overflow occurred in years: ", "PT28725600H"}, - {"PT1723536000M", true, "integer overflow occurred in years: ", "PT1723536000M"}, - {"PT103412160000S", true, "integer overflow occurred in years: ", "PT103412160000S"}, + {"P32768Y", false, ": integer overflow occurred in years", "P32768Y"}, + {"P32768M", false, ": integer overflow occurred in months", "P32768M"}, + {"P32768W", false, ": integer overflow occurred in days", "P32768W"}, + {"P32768D", false, ": integer overflow occurred in days", "P32768D"}, + {"PT32768H", false, ": integer overflow occurred in hours", "PT32768H"}, + {"PT32768M", false, ": integer overflow occurred in minutes", "PT32768M"}, + {"PT32768S", false, ": integer overflow occurred in seconds", "PT32768S"}, + {"PT32768H32768M32768S", false, ": integer overflow occurred in hours,minutes,seconds", "PT32768H32768M32768S"}, + {"PT103412160000S", false, ": integer overflow occurred in seconds", "PT103412160000S"}, } for i, c := range cases { - _, ep := ParseWithNormalise(c.value, c.normalise) + _, ep := Parse(c.value, c.normalise) g.Expect(ep).To(HaveOccurred(), info(i, c.value)) - g.Expect(ep.Error()).To(Equal(c.expected+c.expvalue), info(i, c.value)) + g.Expect(ep.Error()).To(Equal(c.expvalue+c.expected), info(i, c.value)) - _, en := ParseWithNormalise("-"+c.value, c.normalise) + _, en := Parse("-"+c.value, c.normalise) g.Expect(en).To(HaveOccurred(), info(i, c.value)) if c.expvalue != "" { - g.Expect(en.Error()).To(Equal(c.expected+"-"+c.expvalue), info(i, c.value)) + g.Expect(en.Error()).To(Equal("-"+c.expvalue+c.expected), info(i, c.value)) } else { g.Expect(en.Error()).To(Equal(c.expected), info(i, c.value)) } } } +//------------------------------------------------------------------------------------------------- + func TestParsePeriodWithNormalise(t *testing.T) { g := NewGomegaWithT(t) @@ -95,13 +98,17 @@ func TestParsePeriodWithNormalise(t *testing.T) { } for i, c := range cases { p, err := Parse(c.value) - g.Expect(err).NotTo(HaveOccurred(), info(i, c.value)) - g.Expect(p).To(Equal(c.period), info(i, c.value)) + s := info(i, c.value) + g.Expect(err).NotTo(HaveOccurred(), s) + expectValid(t, p, s) + g.Expect(p).To(Equal(c.period), s) // reversal is expected not to be an identity - g.Expect(p.String()).To(Equal(c.reversed), info(i, c.value)+" reversed") + g.Expect(p.String()).To(Equal(c.reversed), s+" reversed") } } +//------------------------------------------------------------------------------------------------- + func TestParsePeriodWithoutNormalise(t *testing.T) { g := NewGomegaWithT(t) @@ -163,13 +170,9 @@ func TestParsePeriodWithoutNormalise(t *testing.T) { {"P2.Y", "P2Y", Period{years: 20}}, {"P2.5Y", "P2.5Y", Period{years: 25}}, {"P2.15Y", "P2.1Y", Period{years: 21}}, - {"P2.125Y", "P2.1Y", Period{years: 21}}, {"P1Y2.M", "P1Y2M", Period{years: 10, months: 20}}, {"P1Y2.5M", "P1Y2.5M", Period{years: 10, months: 25}}, {"P1Y2.15M", "P1Y2.1M", Period{years: 10, months: 21}}, - {"P1Y2.125M", "P1Y2.1M", Period{years: 10, months: 21}}, - {"P3276.7Y", "P3276.7Y", Period{years: 32767}}, - {"-P3276.7Y", "-P3276.7Y", Period{years: -32767}}, // others {"P3Y6M5W4DT12H40M5S", "P3Y6M39DT12H40M5S", Period{years: 30, months: 60, days: 390, hours: 120, minutes: 400, seconds: 50}}, {"+P3Y6M5W4DT12H40M5S", "P3Y6M39DT12H40M5S", Period{years: 30, months: 60, days: 390, hours: 120, minutes: 400, seconds: 50}}, @@ -177,14 +180,18 @@ func TestParsePeriodWithoutNormalise(t *testing.T) { {"P1Y14M35DT48H125M800S", "P1Y14M5WT48H125M800S", Period{years: 10, months: 140, days: 350, hours: 480, minutes: 1250, seconds: 8000}}, } for i, c := range cases { - p, err := ParseWithNormalise(c.value, false) - g.Expect(err).NotTo(HaveOccurred(), info(i, c.value)) - g.Expect(p).To(Equal(c.period), info(i, c.value)) + p, err := Parse(c.value, false) + s := info(i, c.value) + g.Expect(err).NotTo(HaveOccurred(), s) + expectValid(t, p, s) + g.Expect(p).To(Equal(c.period), s) // reversal is usually expected to be an identity - g.Expect(p.String()).To(Equal(c.reversed), info(i, c.value)+" reversed") + g.Expect(p.String()).To(Equal(c.reversed), s+" reversed") } } +//------------------------------------------------------------------------------------------------- + func TestPeriodString(t *testing.T) { g := NewGomegaWithT(t) From f8d65358329713daf3d7fb4ff451017ec806b76c Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Fri, 4 Sep 2020 19:49:35 +0100 Subject: [PATCH 137/165] Period.Between bug fixed: when times need reversing and there is a daylight saving transition, the calculation was out by one hour --- period/period.go | 8 +++--- period/period_test.go | 64 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/period/period.go b/period/period.go index 26a9ad33..ef15c84e 100644 --- a/period/period.go +++ b/period/period.go @@ -143,15 +143,15 @@ func NewOf(duration time.Duration) (p Period, precise bool) { // computations applied to the period can only be precise if they concern either the date (year, month, // day) part, or the clock (hour, minute, second) part, but not both. func Between(t1, t2 time.Time) (p Period) { - if t1.Location() != t2.Location() { - t2 = t2.In(t1.Location()) - } - sign := 1 if t2.Before(t1) { t1, t2, sign = t2, t1, -1 } + if t1.Location() != t2.Location() { + t2 = t2.In(t1.Location()) + } + year, month, day, hour, min, sec, hundredth := daysDiff(t1, t2) if sign < 0 { diff --git a/period/period_test.go b/period/period_test.go index 4411ae41..ccf8f31b 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -248,6 +248,8 @@ func TestPeriodString(t *testing.T) { } } +//------------------------------------------------------------------------------------------------- + func TestPeriodIntComponents(t *testing.T) { g := NewGomegaWithT(t) @@ -286,6 +288,8 @@ func TestPeriodIntComponents(t *testing.T) { } } +//------------------------------------------------------------------------------------------------- + func TestPeriodFloatComponents(t *testing.T) { g := NewGomegaWithT(t) @@ -342,6 +346,8 @@ func TestPeriodFloatComponents(t *testing.T) { } } +//------------------------------------------------------------------------------------------------- + func TestPeriodToDuration(t *testing.T) { g := NewGomegaWithT(t) @@ -386,7 +392,9 @@ func TestPeriodToDuration(t *testing.T) { } } -func TestSignPotisitveNegative(t *testing.T) { +//------------------------------------------------------------------------------------------------- + +func TestSignPositiveNegative(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { @@ -417,6 +425,8 @@ func TestSignPotisitveNegative(t *testing.T) { } } +//------------------------------------------------------------------------------------------------- + func TestPeriodApproxDays(t *testing.T) { g := NewGomegaWithT(t) @@ -439,6 +449,8 @@ func TestPeriodApproxDays(t *testing.T) { } } +//------------------------------------------------------------------------------------------------- + func TestPeriodApproxMonths(t *testing.T) { g := NewGomegaWithT(t) @@ -468,6 +480,8 @@ func TestPeriodApproxMonths(t *testing.T) { } } +//------------------------------------------------------------------------------------------------- + func TestNewPeriod(t *testing.T) { g := NewGomegaWithT(t) @@ -502,6 +516,8 @@ func TestNewPeriod(t *testing.T) { } } +//------------------------------------------------------------------------------------------------- + func TestNewHMS(t *testing.T) { g := NewGomegaWithT(t) @@ -528,6 +544,8 @@ func TestNewHMS(t *testing.T) { } } +//------------------------------------------------------------------------------------------------- + func TestNewYMD(t *testing.T) { g := NewGomegaWithT(t) @@ -555,6 +573,8 @@ func TestNewYMD(t *testing.T) { } } +//------------------------------------------------------------------------------------------------- + func TestNewOf(t *testing.T) { // HMS tests testNewOf(t, 100*time.Millisecond, Period{seconds: 1}, true) @@ -598,6 +618,8 @@ func testNewOf1(t *testing.T, source time.Duration, expected Period, precise boo //g.Expect(rev).To(Equal(source), info) } +//------------------------------------------------------------------------------------------------- + func TestBetween(t *testing.T) { g := NewGomegaWithT(t) now := time.Now() @@ -606,7 +628,9 @@ func TestBetween(t *testing.T) { a, b time.Time expected Period }{ - {now, now, Period{0, 0, 0, 0, 0, 0}}, + // note: the negative cases are also covered (see below) + + {now, now, Period{}}, // simple positive date calculations {utc(2015, 1, 1, 0, 0, 0, 0), utc(2015, 1, 1, 0, 0, 0, 100), Period{seconds: 1}}, @@ -630,11 +654,6 @@ func TestBetween(t *testing.T) { // BST drops an hour at the daylight-saving transition {utc(2015, 1, 1, 0, 0, 0, 0), bst(2015, 7, 2, 1, 1, 1, 1), Period{days: 1820, minutes: 10, seconds: 10}}, - // negative date calculation - {utc(2015, 1, 1, 0, 0, 0, 100), utc(2015, 1, 1, 0, 0, 0, 0), Period{seconds: -1}}, - {utc(2015, 6, 2, 0, 0, 0, 0), utc(2015, 5, 1, 0, 0, 0, 0), Period{days: -320}}, - {utc(2015, 6, 2, 1, 1, 1, 1), utc(2015, 5, 1, 0, 0, 0, 0), Period{days: -320, hours: -10, minutes: -10, seconds: -10}}, - // daytime only {utc(2015, 1, 1, 2, 3, 4, 0), utc(2015, 1, 1, 2, 3, 4, 500), Period{seconds: 5}}, {utc(2015, 1, 1, 2, 3, 4, 0), utc(2015, 1, 1, 4, 4, 7, 500), Period{hours: 20, minutes: 10, seconds: 35}}, @@ -651,13 +670,20 @@ func TestBetween(t *testing.T) { // larger ranges {utc(2009, 1, 1, 0, 0, 1, 0), utc(2016, 12, 31, 0, 0, 2, 0), Period{days: 29210, seconds: 10}}, {utc(2008, 1, 1, 0, 0, 1, 0), utc(2016, 12, 31, 0, 0, 2, 0), Period{years: 80, months: 110, days: 300, seconds: 10}}, + {utc(1900, 1, 1, 0, 0, 1, 0), utc(2009, 12, 31, 0, 0, 2, 0), Period{years: 1090, months: 110, days: 300, seconds: 10}}, } for i, c := range cases { - n := Between(c.a, c.b) - g.Expect(n).To(Equal(c.expected), info(i, c.expected)) + pp := Between(c.a, c.b) + g.Expect(pp).To(Equal(c.expected), info(i, c.expected)) + + pn := Between(c.b, c.a) + en := c.expected.Negate() + g.Expect(pn).To(Equal(en), info(i, en)) } } +//------------------------------------------------------------------------------------------------- + func TestNormalise(t *testing.T) { cases := []struct { source, precise, approx Period @@ -730,6 +756,8 @@ func testNormaliseBothSigns(t *testing.T, i int, source, expected Period, precis g.Expect(n2).To(Equal(eneg)) } +//------------------------------------------------------------------------------------------------- + func TestPeriodFormat(t *testing.T) { g := NewGomegaWithT(t) @@ -768,6 +796,8 @@ func TestPeriodFormat(t *testing.T) { } } +//------------------------------------------------------------------------------------------------- + func TestPeriodFormatWithoutWeeks(t *testing.T) { g := NewGomegaWithT(t) @@ -803,7 +833,9 @@ func TestPeriodFormatWithoutWeeks(t *testing.T) { } } -func TestPeriodParseOnlyYMD(t *testing.T) { +//------------------------------------------------------------------------------------------------- + +func TestPeriodOnlyYMD(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { @@ -819,7 +851,7 @@ func TestPeriodParseOnlyYMD(t *testing.T) { } } -func TestPeriodParseOnlyHMS(t *testing.T) { +func TestPeriodOnlyHMS(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { @@ -835,6 +867,8 @@ func TestPeriodParseOnlyHMS(t *testing.T) { } } +//------------------------------------------------------------------------------------------------- + func utc(year int, month time.Month, day, hour, min, sec, msec int) time.Time { return time.Date(year, month, day, hour, min, sec, msec*int(time.Millisecond), time.UTC) } @@ -849,6 +883,10 @@ func init() { london, _ = time.LoadLocation("Europe/London") } -func info(i int, m interface{}) string { - return fmt.Sprintf("%d %v", i, m) +func info(i int, m ...interface{}) string { + if s, ok := m[0].(string); ok { + m[0] = i + return fmt.Sprintf("%d "+s, m...) + } + return fmt.Sprintf("%d %v", i, m[0]) } From 3b910718767c95cbcd22d4554f185063546fd67f Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Fri, 4 Sep 2020 19:59:02 +0100 Subject: [PATCH 138/165] Period.Normalise simplified considerably: now always using 64-bit arithmetic (the 16-bit alternatives have been deleted) --- period/period.go | 88 +----- period/period64.go | 98 +++++- period/period_test.go | 671 ++++++++++++++++++++++++++---------------- 3 files changed, 499 insertions(+), 358 deletions(-) diff --git a/period/period.go b/period/period.go index ef15c84e..998a61d7 100644 --- a/period/period.go +++ b/period/period.go @@ -14,6 +14,7 @@ const daysPerMonthE4 = 304369 // 30.4369 days per month const daysPerMonthE6 = 30436875 // 30.436875 days per month const oneE4 = 10000 +const oneE5 = 100000 const oneE6 = 1000000 const oneE7 = 10000000 @@ -480,91 +481,6 @@ func (period Period) TotalMonthsApprox() int { // // Note that leap seconds are disregarded: every minute is assumed to have 60 seconds. func (period Period) Normalise(precise bool) Period { - n, _ := normalise(period, "", precise) + n, _ := period.toPeriod64("").normalise64(precise).toPeriod() return n } - -func normalise(period Period, input string, precise bool) (Period, error) { - const limit = 32670 - (32670 / 60) - - // can we use a quicker algorithm for HHMMSS with int16 arithmetic? - if period.years == 0 && period.months == 0 && - (!precise || period.days == 0) && - period.hours > -limit && period.hours < limit { - - return period.normaliseHHMMSS(precise), nil - } - - // can we use a quicker algorithm for YYMM with int16 arithmetic? - if (period.years != 0 || period.months != 0) && - period.days == 0 && period.hours == 0 && period.minutes == 0 && period.seconds == 0 { - - return period.normaliseYYMM(), nil - } - - // do things the no-nonsense way using int64 arithmetic - return period.toPeriod64(input).normalise64(precise).toPeriod() -} - -func (period Period) normaliseHHMMSS(precise bool) Period { - ap, neg := period.absNeg() - - // remember that the fields are all fixed-point 1E1 - ap.minutes += (ap.seconds / 600) * 10 - ap.seconds = ap.seconds % 600 - - ap.hours += (ap.minutes / 600) * 10 - ap.minutes = ap.minutes % 600 - - // up to 36 hours stays as hours - if !precise && ap.hours > 360 { - ap.days += (ap.hours / 240) * 10 - ap.hours = ap.hours % 240 - } - - d10 := ap.days % 10 - if d10 != 0 && (ap.hours != 0 || ap.minutes != 0 || ap.seconds != 0) { - ap.hours += d10 * 24 - ap.days -= d10 - } - - hh10 := ap.hours % 10 - if hh10 != 0 { - ap.minutes += hh10 * 60 - ap.hours -= hh10 - } - - mm10 := ap.minutes % 10 - if mm10 != 0 { - ap.seconds += mm10 * 60 - ap.minutes -= mm10 - } - - if neg { - return ap.Negate() - } - return ap -} - -func (period Period) normaliseYYMM() Period { - // remember that the fields are all fixed-point 1E1 - ap, neg := period.absNeg() - - // bubble month to years - if ap.months > 129 { //TODO 119??? - ap.years += (ap.months / 120) * 10 - ap.months = ap.months % 120 - } - - // push year-fraction down - y10 := ap.years % 10 - if y10 != 0 && (ap.years < 10 || ap.months != 0) { - ap.months += y10 * 12 - ap.years -= y10 - } - - if neg { - return ap.Negate() - } - return ap -} diff --git a/period/period64.go b/period/period64.go index f7d79430..9e2757c8 100644 --- a/period/period64.go +++ b/period/period64.go @@ -2,6 +2,7 @@ package period import ( "fmt" + "math" "strings" ) @@ -32,22 +33,22 @@ func (period Period) toPeriod64(input string) *period64 { func (p64 *period64) toPeriod() (Period, error) { var f []string - if p64.years > 32767 { + if p64.years > math.MaxInt16 { f = append(f, "years") } - if p64.months > 32767 { + if p64.months > math.MaxInt16 { f = append(f, "months") } - if p64.days > 32767 { + if p64.days > math.MaxInt16 { f = append(f, "days") } - if p64.hours > 32767 { + if p64.hours > math.MaxInt16 { f = append(f, "hours") } - if p64.minutes > 32767 { + if p64.minutes > math.MaxInt16 { f = append(f, "minutes") } - if p64.seconds > 32767 { + if p64.seconds > math.MaxInt16 { f = append(f, "seconds") } @@ -78,10 +79,10 @@ func (p64 *period64) normalise64(precise bool) *period64 { func (p64 *period64) rippleUp(precise bool) *period64 { // remember that the fields are all fixed-point 1E1 - p64.minutes = p64.minutes + (p64.seconds/600)*10 + p64.minutes += (p64.seconds / 600) * 10 p64.seconds = p64.seconds % 600 - p64.hours = p64.hours + (p64.minutes/600)*10 + p64.hours += (p64.minutes / 600) * 10 p64.minutes = p64.minutes % 600 // 32670-(32670/60)-(32670/3600) = 32760 - 546 - 9.1 = 32204.9 @@ -91,18 +92,19 @@ func (p64 *period64) rippleUp(precise bool) *period64 { } if !precise || p64.days > 32760 { - dE6 := p64.days * oneE6 - p64.months += dE6 / daysPerMonthE6 - p64.days = (dE6 % daysPerMonthE6) / oneE6 + dE6 := p64.days * oneE5 + p64.months += (dE6 / daysPerMonthE6) * 10 + p64.days = (dE6 % daysPerMonthE6) / oneE5 } - p64.years = p64.years + (p64.months/120)*10 + p64.years += (p64.months / 120) * 10 p64.months = p64.months % 120 return p64 } -// moveFractionToRight applies the rule that only the smallest field is permitted to have a decimal fraction. +// moveFractionToRight attempts to remove fractions in higher-order fields by moving their value to the +// next-lower-order field. For example, fractional years become months. func (p64 *period64) moveFractionToRight() *period64 { // remember that the fields are all fixed-point 1E1 @@ -138,3 +140,73 @@ func (p64 *period64) moveFractionToRight() *period64 { return p64 } + +//func (p64 *period64) reduceYearsFraction() *period64 { +// if p64.fpart == Year { +// centiMonths := 12 * int64(p64.fraction) +// monthFraction := centiMonths % 100 +// if monthFraction == 0 { +// p64.months += centiMonths / 100 +// p64.fraction = 0 +// p64.fpart = NoFraction +// } +// } +// +// return p64 +//} +// +//func (p64 *period64) reduceDaysFraction(precise bool) *period64 { +// if !precise && p64.fpart == Day { +// centiHours := 24 * int64(p64.fraction) +// hourFraction := centiHours % 100 +// if hourFraction == 0 { +// p64.hours += centiHours / 100 +// p64.fraction = 0 +// p64.fpart = NoFraction +// } +// } +// +// return p64 +//} +// +//func (p64 *period64) reduceMonthsFraction(precise bool) *period64 { +// if !precise && p64.fpart == Month { +// centiDays := (daysPerMonthE6 * int64(p64.fraction)) / oneE6 +// dayFraction := centiDays % 100 +// if dayFraction == 0 { +// p64.days += centiDays / 100 +// p64.fraction = 0 +// p64.fpart = NoFraction +// } +// } +// +// return p64 +//} +// +//func (p64 *period64) reduceHoursFraction() *period64 { +// if p64.fpart == Hour { +// centiMinutes := 60 * int64(p64.fraction) +// minuteFraction := centiMinutes % 100 +// if minuteFraction == 0 { +// p64.minutes += centiMinutes / 100 +// p64.fraction = 0 +// p64.fpart = NoFraction +// } +// } +// +// return p64 +//} +// +//func (p64 *period64) reduceMinutesFraction() *period64 { +// if p64.fpart == Minute { +// centiSeconds := 60 * int64(p64.fraction) +// secondFraction := centiSeconds % 100 +// if secondFraction == 0 { +// p64.seconds += centiSeconds / 100 +// p64.fraction = 0 +// p64.fpart = NoFraction +// } +// } +// +// return p64 +//} diff --git a/period/period_test.go b/period/period_test.go index ccf8f31b..aa5ee0ea 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -6,11 +6,11 @@ package period import ( "fmt" + "strings" "testing" "time" . "github.com/onsi/gomega" - "github.com/rickb777/plural" ) var oneDay = 24 * time.Hour @@ -82,7 +82,7 @@ func TestParsePeriodWithNormalise(t *testing.T) { {"PT1234.5S", "PT20M34.5S", Period{minutes: 200, seconds: 345}}, {"PT1234.5M", "PT20H34.5M", Period{hours: 200, minutes: 345}}, {"PT12345.6H", "P514DT9.6H", Period{days: 5140, hours: 96}}, - {"P3276.1D", "P8Y11M19.2D", Period{years: 80, months: 110, days: 192}}, + {"P3277D", "P8Y11M20.2D", Period{years: 80, months: 110, days: 202}}, {"P1234.5M", "P102Y10.5M", Period{years: 1020, months: 105}}, // largest possible number of seconds normalised only in hours, mins, sec {"PT11592000S", "PT3220H", Period{hours: 32200}}, @@ -92,7 +92,7 @@ func TestParsePeriodWithNormalise(t *testing.T) { {"PT283046400S", "P468W", Period{days: 32760}}, {"-PT283046400S", "-P468W", Period{days: -32760}}, {"PT43084443590S", "P1365Y3M2WT26H83M50S", Period{years: 13650, months: 30, days: 140, hours: 260, minutes: 830, seconds: 500}}, - {"PT103412159999S", "P3276Y11M29DT37H83M59S", Period{years: 32760, months: 110, days: 290, hours: 370, minutes: 830, seconds: 590}}, + {"PT103412159999S", "P3276Y11M29DT39H107M59S", Period{years: 32760, months: 110, days: 290, hours: 390, minutes: 1070, seconds: 590}}, {"PT283132799S", "P468WT23H59M59S", Period{days: 32760, hours: 230, minutes: 590, seconds: 590}}, // other examples are in TestNormalise } @@ -199,6 +199,8 @@ func TestPeriodString(t *testing.T) { value string period Period }{ + // note: the negative cases are also covered (see below) + {"P0D", Period{}}, // ones {"P1Y", Period{years: 10}}, @@ -216,35 +218,32 @@ func TestPeriodString(t *testing.T) { {"PT0.1H", Period{hours: 1}}, {"PT0.1M", Period{minutes: 1}}, {"PT0.1S", Period{seconds: 1}}, - // negative - {"-P0.1Y", Period{years: -1}}, - {"-P0.1M", Period{months: -1}}, - {"-P0.7D", Period{days: -7}}, - {"-P0.1D", Period{days: -1}}, - {"-PT0.1H", Period{hours: -1}}, - {"-PT0.1M", Period{minutes: -1}}, - {"-PT0.1S", Period{seconds: -1}}, {"P3Y", Period{years: 30}}, - {"-P3Y", Period{years: -30}}, {"P6M", Period{months: 60}}, - {"-P6M", Period{months: -60}}, {"P5W", Period{days: 350}}, - {"-P5W", Period{days: -350}}, {"P4W", Period{days: 280}}, - {"-P4W", Period{days: -280}}, {"P4D", Period{days: 40}}, - {"-P4D", Period{days: -40}}, {"PT12H", Period{hours: 120}}, {"PT30M", Period{minutes: 300}}, {"PT5S", Period{seconds: 50}}, - {"P3Y6M39DT1H2M4S", Period{years: 30, months: 60, days: 390, hours: 10, minutes: 20, seconds: 40}}, - {"-P3Y6M39DT1H2M4S", Period{years: -30, months: -60, days: -390, hours: -10, minutes: -20, seconds: -40}}, + {"P3Y6M39DT1H2M4.9S", Period{years: 30, months: 60, days: 390, hours: 10, minutes: 20, seconds: 49}}, + {"P2.5Y", Period{years: 25}}, + {"P2.5M", Period{months: 25}}, + {"P2.5D", Period{days: 25}}, + {"PT2.5H", Period{hours: 25}}, + {"PT2.5M", Period{minutes: 25}}, + {"PT2.5S", Period{seconds: 25}}, } for i, c := range cases { - s := c.period.String() - g.Expect(s).To(Equal(c.value), info(i, c.value)) + sp := c.period.String() + g.Expect(sp).To(Equal(c.value), info(i, c.value)) + + if !c.period.IsZero() { + sn := c.period.Negate().String() + g.Expect(sn).To(Equal("-"+c.value), info(i, c.value)) + } } } @@ -257,34 +256,40 @@ func TestPeriodIntComponents(t *testing.T) { value string y, m, w, d, dx, hh, mm, ss int }{ + // note: the negative cases are also covered (see below) + {value: "P0D"}, {value: "P1Y", y: 1}, - {value: "-P1Y", y: -1}, {value: "P1W", w: 1, d: 7}, - {value: "-P1W", w: -1, d: -7}, {value: "P6M", m: 6}, - {value: "-P6M", m: -6}, {value: "P12M", y: 1}, - {value: "-P12M", y: -1, m: -0}, {value: "P39D", w: 5, d: 39, dx: 4}, - {value: "-P39D", w: -5, d: -39, dx: -4}, {value: "P4D", d: 4, dx: 4}, - {value: "-P4D", d: -4, dx: -4}, {value: "PT12H", hh: 12}, {value: "PT60M", hh: 1}, {value: "PT30M", mm: 30}, {value: "PT5S", ss: 5}, } for i, c := range cases { - p := MustParse(c.value) - g.Expect(p.Years()).To(Equal(c.y), info(i, c.value)) - g.Expect(p.Months()).To(Equal(c.m), info(i, c.value)) - g.Expect(p.Weeks()).To(Equal(c.w), info(i, c.value)) - g.Expect(p.Days()).To(Equal(c.d), info(i, c.value)) - g.Expect(p.ModuloDays()).To(Equal(c.dx), info(i, c.value)) - g.Expect(p.Hours()).To(Equal(c.hh), info(i, c.value)) - g.Expect(p.Minutes()).To(Equal(c.mm), info(i, c.value)) - g.Expect(p.Seconds()).To(Equal(c.ss), info(i, c.value)) + pp := MustParse(c.value) + g.Expect(pp.Years()).To(Equal(c.y), info(i, pp)) + g.Expect(pp.Months()).To(Equal(c.m), info(i, pp)) + g.Expect(pp.Weeks()).To(Equal(c.w), info(i, pp)) + g.Expect(pp.Days()).To(Equal(c.d), info(i, pp)) + g.Expect(pp.ModuloDays()).To(Equal(c.dx), info(i, pp)) + g.Expect(pp.Hours()).To(Equal(c.hh), info(i, pp)) + g.Expect(pp.Minutes()).To(Equal(c.mm), info(i, pp)) + g.Expect(pp.Seconds()).To(Equal(c.ss), info(i, pp)) + + pn := pp.Negate() + g.Expect(pn.Years()).To(Equal(-c.y), info(i, pn)) + g.Expect(pn.Months()).To(Equal(-c.m), info(i, pn)) + g.Expect(pn.Weeks()).To(Equal(-c.w), info(i, pn)) + g.Expect(pn.Days()).To(Equal(-c.d), info(i, pn)) + g.Expect(pn.ModuloDays()).To(Equal(-c.dx), info(i, pn)) + g.Expect(pn.Hours()).To(Equal(-c.hh), info(i, pn)) + g.Expect(pn.Minutes()).To(Equal(-c.mm), info(i, pn)) + g.Expect(pn.Seconds()).To(Equal(-c.ss), info(i, pn)) } } @@ -294,72 +299,75 @@ func TestPeriodFloatComponents(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { - value Period + value string y, m, w, d, dx, hh, mm, ss float32 }{ // note: the negative cases are also covered (see below) - {}, // zero case + {value: "P0"}, // zero case // YMD cases - {value: Period{years: 10}, y: 1}, - {value: Period{years: 15}, y: 1.5}, - {value: Period{months: 10}, m: 1}, - {value: Period{months: 15}, m: 1.5}, - {value: Period{months: 60}, m: 6}, - {value: Period{months: 120}, m: 12}, - {value: Period{days: 70}, w: 1, d: 7}, - {value: Period{days: 77}, w: 1.1, d: 7.7}, - {value: Period{days: 10}, w: 1.0 / 7, d: 1}, - {value: Period{days: 11}, w: 1.1 / 7, d: 1.1}, - {value: Period{days: 390}, w: 5.571429, d: 39, dx: 4}, - {value: Period{days: 40}, w: 0.5714286, d: 4, dx: 4}, + {value: "P1Y", y: 1}, + {value: "P1.5Y", y: 1.5}, + {value: "P1.1Y", y: 1.1}, + {value: "P1M", m: 1}, + {value: "P1.5M", m: 1.5}, + {value: "P1.1M", m: 1.1}, + {value: "P6M", m: 6}, + {value: "P12M", m: 12}, + {value: "P7D", w: 1, d: 7}, + {value: "P7.7D", w: 1.1, d: 7.7}, + {value: "P7.1D", w: 7.1 / 7, d: 7.1}, + {value: "P1D", w: 1.0 / 7, d: 1}, + {value: "P1.1D", w: 1.1 / 7, d: 1.1}, + {value: "P1.1D", w: 1.1 / 7, d: 1.1}, + {value: "P39D", w: 5.571429, d: 39, dx: 4}, + {value: "P4D", w: 0.5714286, d: 4, dx: 4}, // HMS cases - {value: Period{hours: 11}, hh: 1.1}, - {value: Period{hours: 10, minutes: 60}, hh: 1, mm: 6}, - {value: Period{hours: 120}, hh: 12}, - {value: Period{minutes: 11}, mm: 1.1}, - {value: Period{minutes: 10, seconds: 60}, mm: 1, ss: 6}, - {value: Period{minutes: 300}, mm: 30}, - {value: Period{seconds: 11}, ss: 1.1}, - {value: Period{seconds: 50}, ss: 5}, + {value: "PT1.1H", hh: 1.1}, + {value: "PT1H6M", hh: 1, mm: 6}, + {value: "PT12H", hh: 12}, + {value: "PT1.1M", mm: 1.1}, + {value: "PT1M6S", mm: 1, ss: 6}, + {value: "PT30M", mm: 30}, + {value: "PT1.1S", ss: 1.1}, + {value: "PT5S", ss: 5}, } for i, c := range cases { - pp := c.value - g.Expect(pp.YearsFloat()).To(Equal(c.y), info(i, c.value)) - g.Expect(pp.MonthsFloat()).To(Equal(c.m), info(i, c.value)) - g.Expect(pp.WeeksFloat()).To(Equal(c.w), info(i, c.value)) - g.Expect(pp.DaysFloat()).To(Equal(c.d), info(i, c.value)) - g.Expect(pp.HoursFloat()).To(Equal(c.hh), info(i, c.value)) - g.Expect(pp.MinutesFloat()).To(Equal(c.mm), info(i, c.value)) - g.Expect(pp.SecondsFloat()).To(Equal(c.ss), info(i, c.value)) - - pn := c.value.Negate() - g.Expect(pn.YearsFloat()).To(Equal(-c.y), info(i, c.value)) - g.Expect(pn.MonthsFloat()).To(Equal(-c.m), info(i, c.value)) - g.Expect(pn.WeeksFloat()).To(Equal(-c.w), info(i, c.value)) - g.Expect(pn.DaysFloat()).To(Equal(-c.d), info(i, c.value)) - g.Expect(pn.HoursFloat()).To(Equal(-c.hh), info(i, c.value)) - g.Expect(pn.MinutesFloat()).To(Equal(-c.mm), info(i, c.value)) - g.Expect(pn.SecondsFloat()).To(Equal(-c.ss), info(i, c.value)) + pp := MustParse(c.value, false) + g.Expect(pp.YearsFloat()).To(Equal(c.y), info(i, pp)) + g.Expect(pp.MonthsFloat()).To(Equal(c.m), info(i, pp)) + g.Expect(pp.WeeksFloat()).To(Equal(c.w), info(i, pp)) + g.Expect(pp.DaysFloat()).To(Equal(c.d), info(i, pp)) + g.Expect(pp.HoursFloat()).To(Equal(c.hh), info(i, pp)) + g.Expect(pp.MinutesFloat()).To(Equal(c.mm), info(i, pp)) + g.Expect(pp.SecondsFloat()).To(Equal(c.ss), info(i, pp)) + + pn := pp.Negate() + g.Expect(pn.YearsFloat()).To(Equal(-c.y), info(i, pn)) + g.Expect(pn.MonthsFloat()).To(Equal(-c.m), info(i, pn)) + g.Expect(pn.WeeksFloat()).To(Equal(-c.w), info(i, pn)) + g.Expect(pn.DaysFloat()).To(Equal(-c.d), info(i, pn)) + g.Expect(pn.HoursFloat()).To(Equal(-c.hh), info(i, pn)) + g.Expect(pn.MinutesFloat()).To(Equal(-c.mm), info(i, pn)) + g.Expect(pn.SecondsFloat()).To(Equal(-c.ss), info(i, pn)) } } //------------------------------------------------------------------------------------------------- func TestPeriodToDuration(t *testing.T) { - g := NewGomegaWithT(t) - cases := []struct { value string duration time.Duration precise bool }{ + // note: the negative cases are also covered (see below) + {"P0D", time.Duration(0), true}, {"PT1S", 1 * time.Second, true}, {"PT0.1S", 100 * time.Millisecond, true}, - {"-PT0.1S", -100 * time.Millisecond, true}, {"PT3276S", 3276 * time.Second, true}, {"PT1M", 60 * time.Second, true}, {"PT0.1M", 6 * time.Second, true}, @@ -367,7 +375,6 @@ func TestPeriodToDuration(t *testing.T) { {"PT1H", 3600 * time.Second, true}, {"PT0.1H", 360 * time.Second, true}, {"PT3220H", 3220 * time.Hour, true}, - {"PT3221H", 3221 * time.Hour, false}, // threshold of normalisation wrapping // days, months and years conversions are never precise {"P1D", 24 * time.Hour, false}, {"P0.1D", 144 * time.Minute, false}, @@ -376,19 +383,25 @@ func TestPeriodToDuration(t *testing.T) { {"P0.1M", oneMonthApprox / 10, false}, {"P3276M", 3276 * oneMonthApprox, false}, {"P1Y", oneYearApprox, false}, - {"-P1Y", -oneYearApprox, false}, - {"P3276Y", 3276 * oneYearApprox, false}, // near the upper limit of range - {"-P3276Y", -3276 * oneYearApprox, false}, // near the lower limit of range + {"P3276Y", 3276 * oneYearApprox, false}, // near the upper limit of range } for i, c := range cases { - p := MustParse(c.value) - d1, prec := p.Duration() - g.Expect(d1).To(Equal(c.duration), info(i, c.value)) - g.Expect(prec).To(Equal(c.precise), info(i, c.value)) - d2 := p.DurationApprox() - if c.precise { - g.Expect(d2).To(Equal(c.duration), info(i, c.value)) - } + testPeriodToDuration(t, i, c.value, c.duration, c.precise) + testPeriodToDuration(t, i, "-"+c.value, -c.duration, c.precise) + } +} + +func testPeriodToDuration(t *testing.T, i int, value string, duration time.Duration, precise bool) { + t.Helper() + g := NewGomegaWithT(t) + hint := info(i, "%s %s %v", value, duration, precise) + pp := MustParse(value) + d1, prec := pp.Duration() + g.Expect(d1).To(Equal(duration), hint) + g.Expect(prec).To(Equal(precise), hint) + d2 := pp.DurationApprox() + if precise { + g.Expect(d2).To(Equal(duration), hint) } } @@ -405,17 +418,29 @@ func TestSignPositiveNegative(t *testing.T) { }{ {"P0D", false, false, 0}, {"PT1S", true, false, 1}, + {"PT0.1S", true, false, 1}, {"-PT1S", false, true, -1}, + {"-PT0.1S", false, true, -1}, {"PT1M", true, false, 1}, + {"PT0.1M", true, false, 1}, {"-PT1M", false, true, -1}, + {"-PT0.1M", false, true, -1}, {"PT1H", true, false, 1}, + {"PT0.1H", true, false, 1}, {"-PT1H", false, true, -1}, + {"-PT0.1H", false, true, -1}, {"P1D", true, false, 1}, + {"P10.D", true, false, 1}, {"-P1D", false, true, -1}, + {"-P0.1D", false, true, -1}, {"P1M", true, false, 1}, + {"P0.1M", true, false, 1}, {"-P1M", false, true, -1}, + {"-P0.1M", false, true, -1}, {"P1Y", true, false, 1}, + {"P0.1Y", true, false, 1}, {"-P1Y", false, true, -1}, + {"-P0.1Y", false, true, -1}, } for i, c := range cases { p := MustParse(c.value) @@ -434,18 +459,22 @@ func TestPeriodApproxDays(t *testing.T) { value string approxDays int }{ + // note: the negative cases are also covered (see below) + {"P0D", 0}, {"PT24H", 1}, {"PT49H", 2}, {"P1D", 1}, {"P1M", 30}, {"P1Y", 365}, - {"-P1Y", -365}, } for i, c := range cases { p := MustParse(c.value) - td := p.TotalDaysApprox() - g.Expect(td).To(Equal(c.approxDays), info(i, c.value)) + td1 := p.TotalDaysApprox() + g.Expect(td1).To(Equal(c.approxDays), info(i, c.value)) + + td2 := p.Negate().TotalDaysApprox() + g.Expect(td2).To(Equal(-c.approxDays), info(i, c.value)) } } @@ -458,6 +487,8 @@ func TestPeriodApproxMonths(t *testing.T) { value string approxMonths int }{ + // note: the negative cases are also covered (see below) + {"P0D", 0}, {"P1D", 0}, {"P30D", 0}, @@ -469,14 +500,16 @@ func TestPeriodApproxMonths(t *testing.T) { {"P2M31D", 3}, {"P1Y", 12}, {"P2Y3M", 27}, - {"-P1Y", -12}, {"PT24H", 0}, {"PT744H", 1}, } for i, c := range cases { p := MustParse(c.value) - td := p.TotalMonthsApprox() - g.Expect(td).To(Equal(c.approxMonths), info(i, c.value)) + td1 := p.TotalMonthsApprox() + g.Expect(td1).To(Equal(c.approxMonths), info(i, c.value)) + + td2 := p.Negate().TotalMonthsApprox() + g.Expect(td2).To(Equal(-c.approxMonths), info(i, c.value)) } } @@ -486,33 +519,37 @@ func TestNewPeriod(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { - period Period + period string years, months, days, hours, minutes, seconds int }{ - {}, // zero case + // note: the negative cases are also covered (see below) - // positives - {period: Period{seconds: 10}, seconds: 1}, - {period: Period{minutes: 10}, minutes: 1}, - {period: Period{hours: 10}, hours: 1}, - {period: Period{days: 10}, days: 1}, - {period: Period{months: 10}, months: 1}, - {period: Period{years: 10}, years: 1}, - {period: Period{1000, 2220, 7000, 0, 0, 0}, years: 100, months: 222, days: 700}, - // negatives - {period: Period{seconds: -10}, seconds: -1}, - {period: Period{minutes: -10}, minutes: -1}, - {period: Period{hours: -10}, hours: -1}, - {period: Period{days: -10}, days: -1}, - {period: Period{months: -10}, months: -1}, - {period: Period{years: -10}, years: -1}, + {period: "P0"}, // zero case + + {period: "PT1S", seconds: 1}, + {period: "PT1M", minutes: 1}, + {period: "PT1H", hours: 1}, + {period: "P1D", days: 1}, + {period: "P1M", months: 1}, + {period: "P1Y", years: 1}, + {period: "P100Y222M700D", years: 100, months: 222, days: 700}, } for i, c := range cases { - p := New(c.years, c.months, c.days, c.hours, c.minutes, c.seconds) - g.Expect(p).To(Equal(c.period), info(i, c.period)) - g.Expect(p.Years()).To(Equal(c.years), info(i, c.period)) - g.Expect(p.Months()).To(Equal(c.months), info(i, c.period)) - g.Expect(p.Days()).To(Equal(c.days), info(i, c.period)) + ep, _ := Parse(c.period, false) + pp := New(c.years, c.months, c.days, c.hours, c.minutes, c.seconds) + expectValid(t, pp, info(i, c.period)) + g.Expect(pp).To(Equal(ep), info(i, c.period)) + g.Expect(pp.Years()).To(Equal(c.years), info(i, c.period)) + g.Expect(pp.Months()).To(Equal(c.months), info(i, c.period)) + g.Expect(pp.Days()).To(Equal(c.days), info(i, c.period)) + + pn := New(-c.years, -c.months, -c.days, -c.hours, -c.minutes, -c.seconds) + en := ep.Negate() + expectValid(t, pn, info(i, en)) + g.Expect(pn).To(Equal(en), info(i, en)) + g.Expect(pn.Years()).To(Equal(-c.years), info(i, en)) + g.Expect(pn.Months()).To(Equal(-c.months), info(i, en)) + g.Expect(pn.Days()).To(Equal(-c.days), info(i, en)) } } @@ -525,22 +562,31 @@ func TestNewHMS(t *testing.T) { period Period hours, minutes, seconds int }{ + // note: the negative cases are also covered (see below) + {}, // zero case - // postives + {period: Period{seconds: 10}, seconds: 1}, {period: Period{minutes: 10}, minutes: 1}, {period: Period{hours: 10}, hours: 1}, - // negatives - {period: Period{seconds: -10}, seconds: -1}, - {period: Period{minutes: -10}, minutes: -1}, - {period: Period{hours: -10}, hours: -1}, + {period: Period{hours: 30, minutes: 40, seconds: 50}, hours: 3, minutes: 4, seconds: 5}, + {period: Period{hours: 32760, minutes: 32760, seconds: 32760}, hours: 3276, minutes: 3276, seconds: 3276}, } for i, c := range cases { - p := NewHMS(c.hours, c.minutes, c.seconds) - g.Expect(p).To(Equal(c.period), info(i, c.period)) - g.Expect(p.Hours()).To(Equal(c.hours), info(i, c.period)) - g.Expect(p.Minutes()).To(Equal(c.minutes), info(i, c.period)) - g.Expect(p.Seconds()).To(Equal(c.seconds), info(i, c.period)) + pp := NewHMS(c.hours, c.minutes, c.seconds) + expectValid(t, pp, info(i, c.period)) + g.Expect(pp).To(Equal(c.period), info(i, c.period)) + g.Expect(pp.Hours()).To(Equal(c.hours), info(i, c.period)) + g.Expect(pp.Minutes()).To(Equal(c.minutes), info(i, c.period)) + g.Expect(pp.Seconds()).To(Equal(c.seconds), info(i, c.period)) + + pn := NewHMS(-c.hours, -c.minutes, -c.seconds) + en := c.period.Negate() + expectValid(t, pn, info(i, en)) + g.Expect(pn).To(Equal(en), info(i, en)) + g.Expect(pn.Hours()).To(Equal(-c.hours), info(i, en)) + g.Expect(pn.Minutes()).To(Equal(-c.minutes), info(i, en)) + g.Expect(pn.Seconds()).To(Equal(-c.seconds), info(i, en)) } } @@ -553,69 +599,82 @@ func TestNewYMD(t *testing.T) { period Period years, months, days int }{ + // note: the negative cases are also covered (see below) + {}, // zero case - // positives + {period: Period{days: 10}, days: 1}, {period: Period{months: 10}, months: 1}, {period: Period{years: 10}, years: 1}, {period: Period{years: 1000, months: 2220, days: 7000}, years: 100, months: 222, days: 700}, - // negatives - {period: Period{days: -10}, days: -1}, - {period: Period{months: -10}, months: -1}, - {period: Period{years: -10}, years: -1}, + {period: Period{years: 32760, months: 32760, days: 32760}, years: 3276, months: 3276, days: 3276}, } for i, c := range cases { - p := NewYMD(c.years, c.months, c.days) - g.Expect(p).To(Equal(c.period), info(i, c.period)) - g.Expect(p.Years()).To(Equal(c.years), info(i, c.period)) - g.Expect(p.Months()).To(Equal(c.months), info(i, c.period)) - g.Expect(p.Days()).To(Equal(c.days), info(i, c.period)) + pp := NewYMD(c.years, c.months, c.days) + expectValid(t, pp, info(i, c.period)) + g.Expect(pp).To(Equal(c.period), info(i, c.period)) + g.Expect(pp.Years()).To(Equal(c.years), info(i, c.period)) + g.Expect(pp.Months()).To(Equal(c.months), info(i, c.period)) + g.Expect(pp.Days()).To(Equal(c.days), info(i, c.period)) + + pn := NewYMD(-c.years, -c.months, -c.days) + en := c.period.Negate() + expectValid(t, pn, info(i, en)) + g.Expect(pn).To(Equal(en), info(i, en)) + g.Expect(pn.Years()).To(Equal(-c.years), info(i, en)) + g.Expect(pn.Months()).To(Equal(-c.months), info(i, en)) + g.Expect(pn.Days()).To(Equal(-c.days), info(i, en)) } } //------------------------------------------------------------------------------------------------- func TestNewOf(t *testing.T) { + // note: the negative cases are also covered (see below) + // HMS tests - testNewOf(t, 100*time.Millisecond, Period{seconds: 1}, true) - testNewOf(t, time.Second, Period{seconds: 10}, true) - testNewOf(t, time.Minute, Period{minutes: 10}, true) - testNewOf(t, time.Hour, Period{hours: 10}, true) - testNewOf(t, time.Hour+time.Minute+time.Second, Period{hours: 10, minutes: 10, seconds: 10}, true) - testNewOf(t, 24*time.Hour+time.Minute+time.Second, Period{hours: 240, minutes: 10, seconds: 10}, true) - testNewOf(t, 3276*time.Hour+59*time.Minute+59*time.Second, Period{hours: 32760, minutes: 590, seconds: 590}, true) - testNewOf(t, 30*time.Minute+67*time.Second+600*time.Millisecond, Period{minutes: 310, seconds: 76}, true) + testNewOf(t, 1, 100*time.Millisecond, Period{seconds: 1}, true) + testNewOf(t, 2, time.Second, Period{seconds: 10}, true) + testNewOf(t, 3, time.Minute, Period{minutes: 10}, true) + testNewOf(t, 4, time.Hour, Period{hours: 10}, true) + testNewOf(t, 5, time.Hour+time.Minute+time.Second, Period{hours: 10, minutes: 10, seconds: 10}, true) + testNewOf(t, 6, 24*time.Hour+time.Minute+time.Second, Period{hours: 240, minutes: 10, seconds: 10}, true) + testNewOf(t, 7, 3276*time.Hour+59*time.Minute+59*time.Second, Period{hours: 32760, minutes: 590, seconds: 590}, true) + testNewOf(t, 8, 30*time.Minute+67*time.Second+600*time.Millisecond, Period{minutes: 310, seconds: 76}, true) // YMD tests: must be over 3276 hours (approx 4.5 months), otherwise HMS will take care of it // first rollover: >3276 hours - testNewOf(t, 3277*time.Hour, Period{days: 1360, hours: 130}, false) - testNewOf(t, 3288*time.Hour, Period{days: 1370}, false) - testNewOf(t, 3289*time.Hour, Period{days: 1370, hours: 10}, false) - testNewOf(t, 24*3276*time.Hour, Period{days: 32760}, false) + testNewOf(t, 10, 3277*time.Hour, Period{days: 1360, hours: 130}, false) + testNewOf(t, 11, 3288*time.Hour, Period{days: 1370}, false) + testNewOf(t, 12, 3289*time.Hour, Period{days: 1370, hours: 10}, false) + testNewOf(t, 13, 24*3276*time.Hour, Period{days: 32760}, false) // second rollover: >3276 days - testNewOf(t, 24*3277*time.Hour, Period{years: 80, months: 110, days: 200}, false) - testNewOf(t, 3277*oneDay, Period{years: 80, months: 110, days: 200}, false) - testNewOf(t, 3277*oneDay+time.Hour+time.Minute+time.Second, Period{years: 80, months: 110, days: 200, hours: 10}, false) - testNewOf(t, 36525*oneDay, Period{years: 1000}, false) + testNewOf(t, 14, 24*3277*time.Hour, Period{years: 80, months: 110, days: 200}, false) + testNewOf(t, 15, 3277*oneDay, Period{years: 80, months: 110, days: 200}, false) + testNewOf(t, 16, 3277*oneDay+time.Hour+time.Minute+time.Second, Period{years: 80, months: 110, days: 200, hours: 10}, false) + testNewOf(t, 17, 36525*oneDay, Period{years: 1000}, false) } -func testNewOf(t *testing.T, source time.Duration, expected Period, precise bool) { +func testNewOf(t *testing.T, i int, source time.Duration, expected Period, precise bool) { t.Helper() - testNewOf1(t, source, expected, precise) - testNewOf1(t, -source, expected.Negate(), precise) + testNewOf1(t, i, source, expected, precise) + testNewOf1(t, i, -source, expected.Negate(), precise) } -func testNewOf1(t *testing.T, source time.Duration, expected Period, precise bool) { +func testNewOf1(t *testing.T, i int, source time.Duration, expected Period, precise bool) { t.Helper() g := NewGomegaWithT(t) n, p := NewOf(source) rev, _ := expected.Duration() - info := fmt.Sprintf("%v %+v %v %v", source, expected, precise, rev) + info := fmt.Sprintf("%d: source %v expected %+v precise %v rev %v", i, source, expected, precise, rev) + expectValid(t, n, info) g.Expect(n).To(Equal(expected), info) g.Expect(p).To(Equal(precise), info) - //g.Expect(rev).To(Equal(source), info) + if precise { + g.Expect(rev).To(Equal(source), info) + } } //------------------------------------------------------------------------------------------------- @@ -684,152 +743,236 @@ func TestBetween(t *testing.T) { //------------------------------------------------------------------------------------------------- -func TestNormalise(t *testing.T) { +func TestNormaliseUnchanged(t *testing.T) { + g := NewGomegaWithT(t) + cases := []struct { - source, precise, approx Period + source period64 }{ - // zero cases - {New(0, 0, 0, 0, 0, 0), New(0, 0, 0, 0, 0, 0), New(0, 0, 0, 0, 0, 0)}, + // note: the negative cases are also covered (see below) - // carry seconds to minutes - {Period{seconds: 699}, Period{minutes: 10, seconds: 99}, Period{minutes: 10, seconds: 99}}, + // zero case + {period64{}}, + + {period64{years: 1}}, + {period64{months: 1}}, + {period64{days: 1}}, + {period64{hours: 1}}, + {period64{minutes: 1}}, + {period64{seconds: 1}}, + + {period64{years: 10, months: 10, days: 10, hours: 10, minutes: 10, seconds: 11}}, + + {period64{days: 10, hours: 70}}, + {period64{days: 10, hours: 10, minutes: 10}}, + {period64{days: 10, hours: 10, seconds: 10}}, + {period64{months: 10, days: 10, hours: 10}}, + + {period64{minutes: 10, seconds: 10}}, + {period64{hours: 10, minutes: 10}}, + {period64{years: 10, months: 7}}, + + {period64{months: 11}}, + {period64{days: 11}}, + {period64{hours: 11}}, + {period64{minutes: 11}}, + {period64{seconds: 11}}, + + // don't carry days to months... + {period64{days: 304}}, + //{period64{days: 32767}}, + + // don't carry MaxInt16 - 1 where it would cause small arithmetic errors + //{period64{years: 32767}}, + //{period64{days: 32767}}, + } + for i, c := range cases { + p, err := c.source.toPeriod() + g.Expect(err).NotTo(HaveOccurred()) + + testNormalise(t, i, c.source, p, true) + testNormalise(t, i, c.source, p, false) + c.source.neg = true + testNormalise(t, i, c.source, p.Negate(), true) + testNormalise(t, i, c.source, p.Negate(), false) + } +} + +//------------------------------------------------------------------------------------------------- + +func TestNormaliseChanged(t *testing.T) { + cases := []struct { + source period64 + precise, approx string + }{ + // note: the negative cases are also covered (see below) - // carry minutes to seconds - {Period{minutes: 5}, Period{seconds: 300}, Period{seconds: 300}}, - {Period{minutes: 1}, Period{seconds: 60}, Period{seconds: 60}}, - {Period{minutes: 55}, Period{minutes: 50, seconds: 300}, Period{minutes: 50, seconds: 300}}, + // carry seconds to minutes + {source: period64{seconds: 600}, precise: "PT1M"}, + {source: period64{seconds: 700}, precise: "PT1M10S"}, + {source: period64{seconds: 6990}, precise: "PT11M39S"}, // carry minutes to hours - {Period{minutes: 699}, Period{hours: 10, minutes: 90, seconds: 540}, Period{hours: 10, minutes: 90, seconds: 540}}, + {source: period64{minutes: 700}, precise: "PT1H10M"}, + {source: period64{minutes: 6990}, precise: "PT11H39M"}, - // carry hours to minutes - {Period{hours: 5}, Period{minutes: 300}, Period{minutes: 300}}, + // simplify 1 hour to minutes + //{period64{hours: 12}, Period{hours: 10, minutes: 120}, Period{hours: 10, minutes: 120}}, + //{period64{hours: 18}, Period{hours: 10, minutes: 480}, Period{hours: 10, minutes: 480}}, // carry hours to days - {Period{hours: 249}, Period{hours: 240, minutes: 540}, Period{hours: 240, minutes: 540}}, - {Period{hours: 249}, Period{hours: 240, minutes: 540}, Period{hours: 240, minutes: 540}}, - {Period{hours: 369}, Period{hours: 360, minutes: 540}, Period{days: 10, hours: 120, minutes: 540}}, - {Period{hours: 249, seconds: 10}, Period{hours: 240, minutes: 540, seconds: 10}, Period{hours: 240, minutes: 540, seconds: 10}}, - - // carry days to hours - {Period{days: 5, hours: 30}, Period{hours: 150}, Period{hours: 150}}, + {source: period64{hours: 480}, precise: "PT48H", approx: "P2D"}, + {source: period64{hours: 490}, precise: "PT49H", approx: "P2D T1H"}, + {source: period64{hours: 32767}, precise: "PT3276.7H", approx: "P4M 14D T 17.5H"}, + {source: period64{years: 10, months: 20, days: 30, hours: 32767}, precise: "P1Y 2M 3D T3276.7H", approx: "P1Y 6M 17D T17.5H"}, + {source: period64{hours: 32768}, precise: "P136DT12.8H", approx: "P4M 14D T17.6H"}, + {source: period64{years: 10, months: 20, days: 30, hours: 32768}, precise: "P1Y 2M 139D T12.8H", approx: "P1Y 6M 17D T17.6H"}, // carry months to years - {Period{months: 125}, Period{months: 125}, Period{months: 125}}, - {Period{months: 131}, Period{years: 10, months: 11}, Period{years: 10, months: 11}}, - - // carry days to months - {Period{days: 323}, Period{days: 323}, Period{days: 323}}, + {source: period64{months: 120}, precise: "P1Y"}, + {source: period64{months: 130}, precise: "P1Y 1M"}, + {source: period64{months: 250}, precise: "P2Y 1M"}, - // carry months to days - {Period{months: 5, days: 203}, Period{days: 355}, Period{months: 10, days: 50}}, + // carry days to prevent overflow + {source: period64{days: 32768}, precise: "P8Y 11M 20D"}, // full ripple up - {Period{months: 121, days: 305, hours: 239, minutes: 591, seconds: 601}, Period{years: 10, days: 330, hours: 360, minutes: 540, seconds: 61}, Period{years: 10, months: 10, days: 40, minutes: 540, seconds: 61}}, + {source: period64{months: 130, days: 310, hours: 240, minutes: 600, seconds: 611}, precise: "P1Y 1M 31D T25H 1M 1.1S", approx: "P1Y 2M 1D T13H 1M 1.1S"}, // carry years to months - {Period{years: 5}, Period{months: 60}, Period{months: 60}}, - {Period{years: 5, months: 25}, Period{months: 85}, Period{months: 85}}, - {Period{years: 5, months: 20, days: 10}, Period{months: 80, days: 10}, Period{months: 80, days: 10}}, + {source: period64{years: 10}, precise: "P1Y"}, + {source: period64{years: 17}, precise: "P1.7Y"}, + {source: period64{years: 10, months: 70}, precise: "P1Y7M"}, } for i, c := range cases { - testNormaliseBothSigns(t, i, c.source, c.precise, true) - testNormaliseBothSigns(t, i, c.source, c.approx, false) + if c.approx == "" { + c.approx = c.precise + } + pp := MustParse(nospace(c.precise)) + pa := MustParse(nospace(c.approx)) + testNormalise(t, i, c.source, pp, true) + testNormalise(t, i, c.source, pa, false) + c.source.neg = true + testNormalise(t, i, c.source, pp.Negate(), true) + testNormalise(t, i, c.source, pa.Negate(), false) } } -func testNormaliseBothSigns(t *testing.T, i int, source, expected Period, precise bool) { +func testNormalise(t *testing.T, i int, source period64, expected Period, precise bool) { g := NewGomegaWithT(t) t.Helper() - n1 := source.Normalise(precise) - if n1 != expected { - t.Errorf("%d: %v.Normalise(%v) %s\n gives %-22s %#v %s,\n want %-22s %#v %s", - i, source, precise, source.DurationApprox(), - n1, n1, n1.DurationApprox(), - expected, expected, expected.DurationApprox()) - } + sstr := source.String() + n, err := source.normalise64(precise).toPeriod() + info := fmt.Sprintf("%d: %s.Normalise(%v) expected %s to equal %s", i, sstr, precise, n, expected) + expectValid(t, n, info) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(n).To(Equal(expected), info) - sneg := source.Negate() - eneg := expected.Negate() - n2 := sneg.Normalise(precise) - g.Expect(n2).To(Equal(eneg)) + if !precise { + p1, _ := source.toPeriod() + d1, pr1 := p1.Duration() + d2, pr2 := expected.Duration() + g.Expect(pr1).To(Equal(pr2), info) + g.Expect(d1).To(Equal(d2), info) + } } //------------------------------------------------------------------------------------------------- -func TestPeriodFormat(t *testing.T) { +// FIXME +func TestNormaliseWithBorrow(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { - period string - expect string + source period64 + precise, approx Period }{ - {"P0D", "0 days"}, - {"P1Y", "1 year"}, - {"P3Y", "3 years"}, - {"-P3Y", "3 years"}, - {"P1M", "1 month"}, - {"P6M", "6 months"}, - {"-P6M", "6 months"}, - {"P1W", "1 week"}, - {"-P1W", "1 week"}, - {"P7D", "1 week"}, - {"P35D", "5 weeks"}, - {"-P35D", "5 weeks"}, - {"P1D", "1 day"}, - {"P4D", "4 days"}, - {"-P4D", "4 days"}, - {"P1Y1M8D", "1 year, 1 month, 1 week, 1 day"}, - {"PT1H1M1S", "1 hour, 1 minute, 1 second"}, - {"P1Y1M8DT1H1M1S", "1 year, 1 month, 1 week, 1 day, 1 hour, 1 minute, 1 second"}, - {"P3Y6M39DT2H7M9S", "3 years, 6 months, 5 weeks, 4 days, 2 hours, 7 minutes, 9 seconds"}, - {"-P3Y6M39DT2H7M9S", "3 years, 6 months, 5 weeks, 4 days, 2 hours, 7 minutes, 9 seconds"}, - {"P1.1Y", "1.1 years"}, - {"P2.5Y", "2.5 years"}, - {"P2.15Y", "2.1 years"}, - {"P2.125Y", "2.1 years"}, + // borrow seconds from minutes + //{period64{days: 2, hours: -250}, Period{days: 1, hours: 23, minutes: 59, seconds: 10}, Period{days: 1, hours: 23, minutes: 59, seconds: 10}}, + //{period64{days: 2, seconds: -50}, Period{days: 1, hours: 23, minutes: 59, seconds: 10}, Period{days: 1, hours: 23, minutes: 59, seconds: 10}}, + //{period64{minutes: 2, seconds: -50}, Period{minutes: 1, seconds: 10}, Period{minutes: 1, seconds: 10}}, + //{period64{minutes: 2, seconds: -70}, Period{seconds: 50}, Period{seconds: 50}}, + //{period64{hours: 2, seconds: -50}, Period{hours: 1, minutes: 59, seconds: 10}, Period{hours: 1, minutes: 59, seconds: 10}}, } for i, c := range cases { - s := MustParse(c.period).Format() - g.Expect(s).To(Equal(c.expect), info(i, c.expect)) + p1 := c.source // copy before normalise - note the pointer receiver + n1, err := p1.normalise64(true).toPeriod() + info1 := fmt.Sprintf("%d: %s.Normalise(true) expected %s to equal %s", i, c.source, n1, c.precise) + expectValid(t, n1, info1) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(n1).To(Equal(c.precise), info1) + + p2 := c.source + n2, err := p2.normalise64(false).toPeriod() + info2 := fmt.Sprintf("%d: %s.Normalise(false) expected %s to equal %s", i, c.source, n2, c.approx) + expectValid(t, n2, info2) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(n2).To(Equal(c.approx), info2) } } //------------------------------------------------------------------------------------------------- -func TestPeriodFormatWithoutWeeks(t *testing.T) { +func TestPeriodFormat(t *testing.T) { g := NewGomegaWithT(t) cases := []struct { - period string - expect string + period string + expectW string // with weeks + expectD string // without weeks }{ - {"P0D", "0 days"}, - {"P1Y", "1 year"}, - {"P3Y", "3 years"}, - {"-P3Y", "3 years"}, - {"P1M", "1 month"}, - {"P6M", "6 months"}, - {"-P6M", "6 months"}, - {"P7D", "7 days"}, - {"P35D", "35 days"}, - {"-P35D", "35 days"}, - {"P1D", "1 day"}, - {"P4D", "4 days"}, - {"-P4D", "4 days"}, - {"P1Y1M1DT1H1M1S", "1 year, 1 month, 1 day, 1 hour, 1 minute, 1 second"}, - {"P3Y6M39DT2H7M9S", "3 years, 6 months, 39 days, 2 hours, 7 minutes, 9 seconds"}, - {"-P3Y6M39DT2H7M9S", "3 years, 6 months, 39 days, 2 hours, 7 minutes, 9 seconds"}, - {"P1.1Y", "1.1 years"}, - {"P2.5Y", "2.5 years"}, - {"P2.15Y", "2.1 years"}, - {"P2.125Y", "2.1 years"}, + // note: the negative cases are also covered (see below) + + {"P0D", "0 days", ""}, + + {"P1Y1M7D", "1 year, 1 month, 1 week", "1 year, 1 month, 7 days"}, + {"P1Y1M1W1D", "1 year, 1 month, 1 week, 1 day", "1 year, 1 month, 8 days"}, + {"PT1H1M1S", "1 hour, 1 minute, 1 second", ""}, + {"P1Y1M1W1DT1H1M1S", "1 year, 1 month, 1 week, 1 day, 1 hour, 1 minute, 1 second", ""}, + {"P3Y6M39DT2H7M9S", "3 years, 6 months, 5 weeks, 4 days, 2 hours, 7 minutes, 9 seconds", ""}, + {"P365D", "52 weeks, 1 day", ""}, + + {"P1Y", "1 year", ""}, + {"P3Y", "3 years", ""}, + {"P1.1Y", "1.1 years", ""}, + {"P2.5Y", "2.5 years", ""}, + + {"P1M", "1 month", ""}, + {"P6M", "6 months", ""}, + {"P1.1M", "1.1 months", ""}, + {"P2.5M", "2.5 months", ""}, + + {"P1W", "1 week", "7 days"}, + {"P1.1W", "1 week, 0.7 day", "7.7 days"}, + {"P7D", "1 week", "7 days"}, + {"P35D", "5 weeks", "35 days"}, + {"P1D", "1 day", "1 day"}, + {"P4D", "4 days", "4 days"}, + {"P1.1D", "1.1 days", ""}, + + {"PT1H", "1 hour", ""}, + {"PT1.1H", "1.1 hours", ""}, + + {"PT1M", "1 minute", ""}, + {"PT1.1M", "1.1 minutes", ""}, + + {"PT1S", "1 second", ""}, + {"PT1.1S", "1.1 seconds", ""}, } for i, c := range cases { - s := MustParse(c.period).FormatWithPeriodNames(PeriodYearNames, PeriodMonthNames, plural.Plurals{}, PeriodDayNames, - PeriodHourNames, PeriodMinuteNames, PeriodSecondNames) - g.Expect(s).To(Equal(c.expect), info(i, c.expect)) + p := MustParse(c.period) + sp := p.Format() + g.Expect(sp).To(Equal(c.expectW), info(i, "%s -> %s", p, c.expectW)) + + en := p.Negate() + sn := en.Format() + g.Expect(sn).To(Equal(c.expectW), info(i, "%s -> %s", en, c.expectW)) + + if c.expectD != "" { + s := MustParse(c.period).FormatWithoutWeeks() + g.Expect(s).To(Equal(c.expectD), info(i, "%s -> %s", p, c.expectD)) + } } } @@ -890,3 +1033,13 @@ func info(i int, m ...interface{}) string { } return fmt.Sprintf("%d %v", i, m[0]) } + +func nospace(s string) string { + b := new(strings.Builder) + for _, r := range s { + if r != ' ' { + b.WriteRune(r) + } + } + return b.String() +} From 0262965683301e131543acd0e0833e62b325c560 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Fri, 4 Sep 2020 21:06:01 +0100 Subject: [PATCH 139/165] Period tests - further improvements --- period/arithmetic_test.go | 50 +++++++++++++++++++++++++++------------ period/period_test.go | 35 +-------------------------- 2 files changed, 36 insertions(+), 49 deletions(-) diff --git a/period/arithmetic_test.go b/period/arithmetic_test.go index 42a33dd4..2de1a619 100644 --- a/period/arithmetic_test.go +++ b/period/arithmetic_test.go @@ -76,6 +76,7 @@ func TestPeriodAdd(t *testing.T) { } for i, c := range cases { s := MustParse(c.one).Add(MustParse(c.two)) + expectValid(t, s, info(i, c.expect)) g.Expect(s).To(Equal(MustParse(c.expect)), info(i, c.expect)) } } @@ -138,24 +139,43 @@ func expectValid(t *testing.T, period Period, hint interface{}) Period { // check all the signs are consistent nPoz := pos(period.years) + pos(period.months) + pos(period.days) + pos(period.hours) + pos(period.minutes) + pos(period.seconds) nNeg := neg(period.years) + neg(period.months) + neg(period.days) + neg(period.hours) + neg(period.minutes) + neg(period.seconds) - if nPoz > 0 && nNeg > 0 { - t.Errorf("%s: inconsistent signs in\n%#v", info, period) - } + g.Expect(nPoz == 0 || nNeg == 0).To(BeTrue(), info+" inconsistent signs") // only one field must have a fraction yearsFraction := fraction(period.years) - //monthsFraction := fraction(period.months) - //daysFraction := fraction(period.days) - //hoursFraction := fraction(period.hours) - //minutesFraction := fraction(period.minutes) - //secondsFraction := fraction(period.seconds) - - if yearsFraction > 0 { - g.Expect(period.months).To(BeZero(), info) - g.Expect(period.days).To(BeZero(), info) - g.Expect(period.hours).To(BeZero(), info) - g.Expect(period.minutes).To(BeZero(), info) - g.Expect(period.seconds).To(BeZero(), info) + monthsFraction := fraction(period.months) + daysFraction := fraction(period.days) + hoursFraction := fraction(period.hours) + minutesFraction := fraction(period.minutes) + + if yearsFraction != 0 { + g.Expect(period.months).To(BeZero(), info+" year fraction exists") + g.Expect(period.days).To(BeZero(), info+" year fraction exists") + g.Expect(period.hours).To(BeZero(), info+" year fraction exists") + g.Expect(period.minutes).To(BeZero(), info+" year fraction exists") + g.Expect(period.seconds).To(BeZero(), info+" year fraction exists") + } + + if monthsFraction != 0 { + g.Expect(period.days).To(BeZero(), info+" month fraction exists") + g.Expect(period.hours).To(BeZero(), info+" month fraction exists") + g.Expect(period.minutes).To(BeZero(), info+" month fraction exists") + g.Expect(period.seconds).To(BeZero(), info+" month fraction exists") + } + + if daysFraction != 0 { + g.Expect(period.hours).To(BeZero(), info+" day fraction exists") + g.Expect(period.minutes).To(BeZero(), info+" day fraction exists") + g.Expect(period.seconds).To(BeZero(), info+" day fraction exists") + } + + if hoursFraction != 0 { + g.Expect(period.minutes).To(BeZero(), info+" hour fraction exists") + g.Expect(period.seconds).To(BeZero(), info+" hour fraction exists") + } + + if minutesFraction != 0 { + g.Expect(period.seconds).To(BeZero(), info+" minute fraction exists") } return period diff --git a/period/period_test.go b/period/period_test.go index aa5ee0ea..39b40a6d 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -736,6 +736,7 @@ func TestBetween(t *testing.T) { g.Expect(pp).To(Equal(c.expected), info(i, c.expected)) pn := Between(c.b, c.a) + expectValid(t, pn, info(i, c.expected)) en := c.expected.Negate() g.Expect(pn).To(Equal(en), info(i, en)) } @@ -880,40 +881,6 @@ func testNormalise(t *testing.T, i int, source period64, expected Period, precis //------------------------------------------------------------------------------------------------- -// FIXME -func TestNormaliseWithBorrow(t *testing.T) { - g := NewGomegaWithT(t) - - cases := []struct { - source period64 - precise, approx Period - }{ - // borrow seconds from minutes - //{period64{days: 2, hours: -250}, Period{days: 1, hours: 23, minutes: 59, seconds: 10}, Period{days: 1, hours: 23, minutes: 59, seconds: 10}}, - //{period64{days: 2, seconds: -50}, Period{days: 1, hours: 23, minutes: 59, seconds: 10}, Period{days: 1, hours: 23, minutes: 59, seconds: 10}}, - //{period64{minutes: 2, seconds: -50}, Period{minutes: 1, seconds: 10}, Period{minutes: 1, seconds: 10}}, - //{period64{minutes: 2, seconds: -70}, Period{seconds: 50}, Period{seconds: 50}}, - //{period64{hours: 2, seconds: -50}, Period{hours: 1, minutes: 59, seconds: 10}, Period{hours: 1, minutes: 59, seconds: 10}}, - } - for i, c := range cases { - p1 := c.source // copy before normalise - note the pointer receiver - n1, err := p1.normalise64(true).toPeriod() - info1 := fmt.Sprintf("%d: %s.Normalise(true) expected %s to equal %s", i, c.source, n1, c.precise) - expectValid(t, n1, info1) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(n1).To(Equal(c.precise), info1) - - p2 := c.source - n2, err := p2.normalise64(false).toPeriod() - info2 := fmt.Sprintf("%d: %s.Normalise(false) expected %s to equal %s", i, c.source, n2, c.approx) - expectValid(t, n2, info2) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(n2).To(Equal(c.approx), info2) - } -} - -//------------------------------------------------------------------------------------------------- - func TestPeriodFormat(t *testing.T) { g := NewGomegaWithT(t) From 075a087230bcc471447d4908177f7150ab829180 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Sat, 5 Sep 2020 10:04:44 +0100 Subject: [PATCH 140/165] new Period Simplify method --- period/period.go | 133 ++++++++++++++++++++++++++++++++++++++++++ period/period64.go | 70 ---------------------- period/period_test.go | 108 ++++++++++++++++++++++++++++++++++ 3 files changed, 241 insertions(+), 70 deletions(-) diff --git a/period/period.go b/period/period.go index 998a61d7..d097e9c8 100644 --- a/period/period.go +++ b/period/period.go @@ -484,3 +484,136 @@ func (period Period) Normalise(precise bool) Period { n, _ := period.toPeriod64("").normalise64(precise).toPeriod() return n } + +// Simplify applies some heuristic simplifications with the objective of reducing the number +// of non-zero fields and thus making the rendered form simpler. It should be applied to +// a normalised period, otherwise the results may be unpredictable. +// +// Note that months and days are never combined, due to the variability of month lengths. +// Days and hours are only combined when imprecise behaviour is selected; this is due to +// daylight savings transitions, during which there are more than or fewer than 24 hours +// per day. +// +// The following transformation rules are applied in order: +// +// * P1YnM becomes 12+n months for 0 < n <= 6 +// * P1DTnH becomes 24+n hours for 0 < n <= 6 (unless precise is true) +// * PT1HnM becomes 60+n minutes for 0 < n <= 10 +// * PT1MnS becomes 60+n seconds for 0 < n <= 10 +// +// At each step, if a fraction exists and would affect the calculation, the transformations +// stop. Also, when not precise, +// +// * for periods of at least ten years, month proper fractions are discarded +// * for periods of at least a year, day proper fractions are discarded +// * for periods of at least a month, hour proper fractions are discarded +// * for periods of at least a day, minute proper fractions are discarded +// * for periods of at least an hour, second proper fractions are discarded +// +// The thresholds can be set using the varargs th parameter. By default, the thresholds a, +// b, c, d are 6 months, 6 hours, 10 minutes, 10 seconds respectively as listed in the rules +// above. +// +// * No thresholds is equivalent to 6, 6, 10, 10. +// * A single threshold a is equivalent to a, a, a, a. +// * Two thresholds a, b are equivalent to a, a, b, b. +// * Three thresholds a, b, c are equivalent to a, b, c, c. +// * Four thresholds a, b, c, d are used as provided. +// +func (period Period) Simplify(precise bool, th ...int) Period { + switch len(th) { + case 0: + return period.doSimplify(precise, 60, 60, 100, 100) + case 1: + return period.doSimplify(precise, int16(th[0]*10), int16(th[0]*10), int16(th[0]*10), int16(th[0]*10)) + case 2: + return period.doSimplify(precise, int16(th[0]*10), int16(th[0]*10), int16(th[1]*10), int16(th[1]*10)) + case 3: + return period.doSimplify(precise, int16(th[0]*10), int16(th[1]*10), int16(th[2]*10), int16(th[2]*10)) + default: + return period.doSimplify(precise, int16(th[0]*10), int16(th[1]*10), int16(th[2]*10), int16(th[3]*10)) + } +} + +func (period Period) doSimplify(precise bool, a, b, c, d int16) Period { + if period.years%10 != 0 { + return period + } + + ap, neg := period.absNeg() + + // single year is dropped if there are some months + if ap.years == 10 && + 0 < ap.months && ap.months <= a && + ap.days == 0 { + ap.months += 120 + ap.years = 0 + } + + if ap.months%10 != 0 { + // month fraction is dropped for periods of at least ten years (1:120) + months := ap.months / 10 + if !precise && ap.years >= 100 && months == 0 { + ap.months = 0 + } + return ap.condNegate(neg) + } + + if ap.days%10 != 0 { + // day fraction is dropped for periods of at least a year (1:365) + days := ap.days / 10 + if !precise && (ap.years > 0 || ap.months >= 120) && days == 0 { + ap.days = 0 + } + return ap.condNegate(neg) + } + + if !precise && ap.days == 10 && + ap.years == 0 && + ap.months == 0 && + 0 < ap.hours && ap.hours <= b { + ap.hours += 240 + ap.days = 0 + } + + if ap.hours%10 != 0 { + // hour fraction is dropped for periods of at least a month (1:720) + hours := ap.hours / 10 + if !precise && (ap.years > 0 || ap.months > 0 || ap.days >= 300) && hours == 0 { + ap.hours = 0 + } + return ap.condNegate(neg) + } + + if ap.hours == 10 && + 0 < ap.minutes && ap.minutes <= c { + ap.minutes += 600 + ap.hours = 0 + } + + if ap.minutes%10 != 0 { + // minute fraction is dropped for periods of at least a day (1:1440) + minutes := ap.minutes / 10 + if !precise && (ap.years > 0 || ap.months > 0 || ap.days > 0 || ap.hours >= 240) && minutes == 0 { + ap.minutes = 0 + } + return ap.condNegate(neg) + } + + if ap.minutes == 10 && + ap.hours == 0 && + 0 < ap.seconds && ap.seconds <= d { + ap.seconds += 600 + ap.minutes = 0 + } + + if ap.seconds%10 != 0 { + // second fraction is dropped for periods of at least an hour (1:3600) + seconds := ap.seconds / 10 + if !precise && (ap.years > 0 || ap.months > 0 || ap.days > 0 || ap.hours > 0 || ap.minutes >= 600) && seconds == 0 { + ap.seconds = 0 + } + } + + return ap.condNegate(neg) +} diff --git a/period/period64.go b/period/period64.go index 9e2757c8..f3ea7892 100644 --- a/period/period64.go +++ b/period/period64.go @@ -140,73 +140,3 @@ func (p64 *period64) moveFractionToRight() *period64 { return p64 } - -//func (p64 *period64) reduceYearsFraction() *period64 { -// if p64.fpart == Year { -// centiMonths := 12 * int64(p64.fraction) -// monthFraction := centiMonths % 100 -// if monthFraction == 0 { -// p64.months += centiMonths / 100 -// p64.fraction = 0 -// p64.fpart = NoFraction -// } -// } -// -// return p64 -//} -// -//func (p64 *period64) reduceDaysFraction(precise bool) *period64 { -// if !precise && p64.fpart == Day { -// centiHours := 24 * int64(p64.fraction) -// hourFraction := centiHours % 100 -// if hourFraction == 0 { -// p64.hours += centiHours / 100 -// p64.fraction = 0 -// p64.fpart = NoFraction -// } -// } -// -// return p64 -//} -// -//func (p64 *period64) reduceMonthsFraction(precise bool) *period64 { -// if !precise && p64.fpart == Month { -// centiDays := (daysPerMonthE6 * int64(p64.fraction)) / oneE6 -// dayFraction := centiDays % 100 -// if dayFraction == 0 { -// p64.days += centiDays / 100 -// p64.fraction = 0 -// p64.fpart = NoFraction -// } -// } -// -// return p64 -//} -// -//func (p64 *period64) reduceHoursFraction() *period64 { -// if p64.fpart == Hour { -// centiMinutes := 60 * int64(p64.fraction) -// minuteFraction := centiMinutes % 100 -// if minuteFraction == 0 { -// p64.minutes += centiMinutes / 100 -// p64.fraction = 0 -// p64.fpart = NoFraction -// } -// } -// -// return p64 -//} -// -//func (p64 *period64) reduceMinutesFraction() *period64 { -// if p64.fpart == Minute { -// centiSeconds := 60 * int64(p64.fraction) -// secondFraction := centiSeconds % 100 -// if secondFraction == 0 { -// p64.seconds += centiSeconds / 100 -// p64.fraction = 0 -// p64.fpart = NoFraction -// } -// } -// -// return p64 -//} diff --git a/period/period_test.go b/period/period_test.go index 39b40a6d..1f990812 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -979,6 +979,114 @@ func TestPeriodOnlyHMS(t *testing.T) { //------------------------------------------------------------------------------------------------- +func TestSimplify(t *testing.T) { + cases := []struct { + source, precise, approx string + }{ + // note: the negative cases are also covered (see below) + + // simplify 1 year to months (a = 9) + {source: "P1Y"}, + {source: "P1Y10M"}, + {source: "P1Y9M", precise: "P21M"}, + {source: "P1Y8.9M", precise: "P20.9M"}, + + // simplify 1 day to hours (approx only) (b = 6) + {source: "P1DT6H", precise: "P1DT6H", approx: "PT30H"}, + {source: "P1DT7H"}, + {source: "P1DT5.9H", precise: "P1DT5.9H", approx: "PT29.9H"}, + + // simplify 1 hour to minutes (c = 10) + {source: "PT1H"}, + {source: "PT1H21M"}, + {source: "PT1H10M", precise: "PT70M"}, + {source: "PT1H9.9M", precise: "PT69.9M"}, + + // simplify 1 minute to seconds (d = 30) + {source: "PT1M"}, // unchanged + {source: "PT1M31S"}, // ditto + {source: "PT1M30S", precise: "PT90S"}, + {source: "PT1M29.9S", precise: "PT89.9S"}, + + // fractional years don't simplify + {source: "P1.1Y"}, + + // retained proper fractions + {source: "P1Y0.1D"}, + {source: "P12M0.1D"}, + {source: "P1YT0.1H"}, + {source: "P1MT0.1H"}, + {source: "P1Y0.1M", precise: "P12.1M"}, + {source: "P1DT0.1H", precise: "P1DT0.1H", approx: "PT24.1H"}, + {source: "P1YT0.1M"}, + {source: "P1MT0.1M"}, + {source: "P1DT0.1M"}, + + // discard proper fractions - months + {source: "P10Y0.1M", precise: "P10Y0.1M", approx: "P10Y"}, + // discard proper fractions - days + {source: "P1Y0.1D", precise: "P1Y0.1D", approx: "P1Y"}, + {source: "P12M0.1D", precise: "P12M0.1D", approx: "P12M"}, + // discard proper fractions - hours + {source: "P1YT0.1H", precise: "P1YT0.1H", approx: "P1Y"}, + {source: "P1MT0.1H", precise: "P1MT0.1H", approx: "P1M"}, + {source: "P30DT0.1H", precise: "P30DT0.1H", approx: "P30D"}, + // discard proper fractions - minutes + {source: "P1YT0.1M", precise: "P1YT0.1M", approx: "P1Y"}, + {source: "P1MT0.1M", precise: "P1MT0.1M", approx: "P1M"}, + {source: "P1DT0.1M", precise: "P1DT0.1M", approx: "P1D"}, + {source: "PT24H0.1M", precise: "PT24H0.1M", approx: "PT24H"}, + // discard proper fractions - seconds + {source: "P1YT0.1S", precise: "P1YT0.1S", approx: "P1Y"}, + {source: "P1MT0.1S", precise: "P1MT0.1S", approx: "P1M"}, + {source: "P1DT0.1S", precise: "P1DT0.1S", approx: "P1D"}, + {source: "PT1H0.1S", precise: "PT1H0.1S", approx: "PT1H"}, + {source: "PT60M0.1S", precise: "PT60M0.1S", approx: "PT60M"}, + } + for i, c := range cases { + p := MustParse(nospace(c.source), false) + if c.precise == "" { + // unchanged cases + testSimplify(t, i, p, p, true) + testSimplify(t, i, p.Negate(), p.Negate(), true) + + } else if c.approx == "" { + // changed but precise/approx has same result + ep := MustParse(nospace(c.precise), false) + testSimplify(t, i, p, ep, true) + testSimplify(t, i, p.Negate(), ep.Negate(), true) + + } else { + // changed and precise/approx have different results + ep := MustParse(nospace(c.precise), false) + ea := MustParse(nospace(c.approx), false) + testSimplify(t, i, p, ep, true) + testSimplify(t, i, p.Negate(), ep.Negate(), true) + testSimplify(t, i, p, ea, false) + testSimplify(t, i, p.Negate(), ea.Negate(), false) + } + } + + g := NewGomegaWithT(t) + g.Expect(Period{days: 10, hours: 70}.Simplify(false, 6, 7, 30)).To(Equal(Period{hours: 310})) + g.Expect(Period{hours: 10, minutes: 300}.Simplify(true, 6, 30)).To(Equal(Period{minutes: 900})) + g.Expect(Period{years: 10, months: 110}.Simplify(true, 11)).To(Equal(Period{months: 230})) + g.Expect(Period{days: 10, hours: 60}.Simplify(false)).To(Equal(Period{hours: 300})) +} + +func testSimplify(t *testing.T, i int, source Period, expected Period, precise bool) { + g := NewGomegaWithT(t) + t.Helper() + + sstr := source.String() + n := source.Simplify(precise, 9, 6, 10, 30) + info := fmt.Sprintf("%d: %s.Simplify(%v) expected %s to equal %s", i, sstr, precise, n, expected) + expectValid(t, n, info) + g.Expect(n).To(Equal(expected), info) +} + +//------------------------------------------------------------------------------------------------- + func utc(year int, month time.Month, day, hour, min, sec, msec int) time.Time { return time.Date(year, month, day, hour, min, sec, msec*int(time.Millisecond), time.UTC) } From c7dda9224f958b55c21026035f688df537b10b2d Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Tue, 8 Sep 2020 10:47:43 +0100 Subject: [PATCH 141/165] Period tests --- period/period_test.go | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/period/period_test.go b/period/period_test.go index 1f990812..5588972d 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -779,13 +779,8 @@ func TestNormaliseUnchanged(t *testing.T) { {period64{minutes: 11}}, {period64{seconds: 11}}, - // don't carry days to months... - {period64{days: 304}}, - //{period64{days: 32767}}, - - // don't carry MaxInt16 - 1 where it would cause small arithmetic errors - //{period64{years: 32767}}, - //{period64{days: 32767}}, + // don't carry days to months + // don't carry months to years } for i, c := range cases { p, err := c.source.toPeriod() @@ -817,33 +812,26 @@ func TestNormaliseChanged(t *testing.T) { {source: period64{minutes: 700}, precise: "PT1H10M"}, {source: period64{minutes: 6990}, precise: "PT11H39M"}, - // simplify 1 hour to minutes - //{period64{hours: 12}, Period{hours: 10, minutes: 120}, Period{hours: 10, minutes: 120}}, - //{period64{hours: 18}, Period{hours: 10, minutes: 480}, Period{hours: 10, minutes: 480}}, - // carry hours to days {source: period64{hours: 480}, precise: "PT48H", approx: "P2D"}, {source: period64{hours: 490}, precise: "PT49H", approx: "P2D T1H"}, - {source: period64{hours: 32767}, precise: "PT3276.7H", approx: "P4M 14D T 17.5H"}, + {source: period64{hours: 32761}, precise: "PT3276.1H", approx: "P4M 14D T 16.9H"}, {source: period64{years: 10, months: 20, days: 30, hours: 32767}, precise: "P1Y 2M 3D T3276.7H", approx: "P1Y 6M 17D T17.5H"}, {source: period64{hours: 32768}, precise: "P136DT12.8H", approx: "P4M 14D T17.6H"}, {source: period64{years: 10, months: 20, days: 30, hours: 32768}, precise: "P1Y 2M 139D T12.8H", approx: "P1Y 6M 17D T17.6H"}, + // carry days to months + {source: period64{days: 310}, precise: "P31D", approx: "P1M 0.5D"}, + {source: period64{days: 32760}, precise: "P3276D", approx: "P8Y 11M 19.2D"}, + {source: period64{days: 32761}, precise: "P8Y 11M 19.3D"}, + // carry months to years {source: period64{months: 120}, precise: "P1Y"}, - {source: period64{months: 130}, precise: "P1Y 1M"}, + {source: period64{months: 132}, precise: "P1Y 1.2M"}, {source: period64{months: 250}, precise: "P2Y 1M"}, - // carry days to prevent overflow - {source: period64{days: 32768}, precise: "P8Y 11M 20D"}, - // full ripple up {source: period64{months: 130, days: 310, hours: 240, minutes: 600, seconds: 611}, precise: "P1Y 1M 31D T25H 1M 1.1S", approx: "P1Y 2M 1D T13H 1M 1.1S"}, - - // carry years to months - {source: period64{years: 10}, precise: "P1Y"}, - {source: period64{years: 17}, precise: "P1.7Y"}, - {source: period64{years: 10, months: 70}, precise: "P1Y7M"}, } for i, c := range cases { if c.approx == "" { From 9c56d1af22e40ba5a859d7d4d35d28f9c163ddb3 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 7 Oct 2020 11:20:02 +0100 Subject: [PATCH 142/165] updated dependencies --- go.mod | 9 +++++---- go.sum | 20 ++++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 3d70ce8e..a858324c 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,11 @@ module github.com/rickb777/date require ( - github.com/onsi/gomega v1.10.1 - github.com/rickb777/plural v1.2.1 - golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect - golang.org/x/text v0.3.2 + github.com/onsi/gomega v1.10.2 + github.com/rickb777/plural v1.2.2 + golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 // indirect + golang.org/x/text v0.3.3 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect ) go 1.15 diff --git a/go.sum b/go.sum index 418bb253..24711eab 100644 --- a/go.sum +++ b/go.sum @@ -19,18 +19,18 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/rickb777/plural v1.2.1 h1:UitRAgR70+yHFt26Tmj/F9dU9aV6UfjGXSbO1DcC9/U= -github.com/rickb777/plural v1.2.1/go.mod h1:j058+3M5QQFgcZZ2oKIOekcygoZUL8gKW5yRO14BuAw= +github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs= +github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/rickb777/plural v1.2.2 h1:4CU5NiUqXSM++2+7JCrX+oguXd2D7RY5O1YisMw1yCI= +github.com/rickb777/plural v1.2.2/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -39,13 +39,17 @@ golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From 0bcc9fdf8067ad043265d4466dce8fec2ba1a0f6 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Thu, 8 Oct 2020 10:21:44 +0100 Subject: [PATCH 143/165] godoc link changed --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b2d92197..ff8e19fa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # date -[![GoDoc](https://img.shields.io/badge/api-Godoc-blue.svg?style=flat-square)](https://godoc.org/github.com/rickb777/date) +[![GoDoc](https://img.shields.io/badge/api-Godoc-blue.svg)](https://pkg.go.dev/github.com/rickb777/date) [![Build Status](https://api.travis-ci.org/rickb777/date.svg?branch=master)](https://travis-ci.org/rickb777/date) [![Coverage Status](https://coveralls.io/repos/rickb777/date/badge.svg?branch=master&service=github)](https://coveralls.io/github/rickb777/date?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/rickb777/date)](https://goreportcard.com/report/github.com/rickb777/date) From 28753adebdbf61f6f82c9fc7551e86c86fd8e7fb Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Tue, 13 Oct 2020 22:39:49 +0100 Subject: [PATCH 144/165] demostrated problem mentioned in issue 11 https://github.com/rickb777/date/issues/11 --- .travis.yml | 3 ++- README.md | 2 +- build+test.sh | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index fd0058b6..704200e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,5 +8,6 @@ install: - go get github.com/mattn/goveralls script: - - ./build+test.sh + - ./build+test.sh amd64 + - ./build+test.sh 386 diff --git a/README.md b/README.md index ff8e19fa..52bcde8c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # date [![GoDoc](https://img.shields.io/badge/api-Godoc-blue.svg)](https://pkg.go.dev/github.com/rickb777/date) -[![Build Status](https://api.travis-ci.org/rickb777/date.svg?branch=master)](https://travis-ci.org/rickb777/date) +[![Build Status](https://api.travis-ci.org/rickb777/date.svg?branch=master)](https://travis-ci.org/rickb777/date/builds) [![Coverage Status](https://coveralls.io/repos/rickb777/date/badge.svg?branch=master&service=github)](https://coveralls.io/github/rickb777/date?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/rickb777/date)](https://goreportcard.com/report/github.com/rickb777/date) [![Issues](https://img.shields.io/github/issues/rickb777/date.svg)](https://github.com/rickb777/date/issues) diff --git a/build+test.sh b/build+test.sh index 95a550b7..d76ff538 100755 --- a/build+test.sh +++ b/build+test.sh @@ -3,6 +3,7 @@ cd "$(dirname $0)" PATH=$HOME/go/bin:$PATH unset GOPATH export GO111MODULE=on +export GOARCH=${1} function v { From 21bcac5463d72ef1fc2d0bcdb65429089014e723 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Tue, 13 Oct 2020 22:42:36 +0100 Subject: [PATCH 145/165] fixed int32 overflow problem mentioned in issue 11 https://github.com/rickb777/date/issues/11 --- period/arithmetic.go | 12 ++++++------ period/format.go | 2 +- period/parse.go | 18 +++++++++--------- period/period64.go | 10 +++++----- period/period_test.go | 24 ++++++++++++------------ 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/period/arithmetic.go b/period/arithmetic.go index 6141c091..55993fe0 100644 --- a/period/arithmetic.go +++ b/period/arithmetic.go @@ -82,12 +82,12 @@ func (period Period) ScaleWithOverflowCheck(factor float32) (Period, error) { return p2.Normalise(pr1 && pr2), nil } - y := int(float32(ap.years) * factor) - m := int(float32(ap.months) * factor) - d := int(float32(ap.days) * factor) - hh := int(float32(ap.hours) * factor) - mm := int(float32(ap.minutes) * factor) - ss := int(float32(ap.seconds) * factor) + y := int64(float32(ap.years) * factor) + m := int64(float32(ap.months) * factor) + d := int64(float32(ap.days) * factor) + hh := int64(float32(ap.hours) * factor) + mm := int64(float32(ap.minutes) * factor) + ss := int64(float32(ap.seconds) * factor) p64 := &period64{years: y, months: m, days: d, hours: hh, minutes: mm, seconds: ss, neg: neg} return p64.normalise64(true).toPeriod() diff --git a/period/format.go b/period/format.go index df517b58..3b095292 100644 --- a/period/format.go +++ b/period/format.go @@ -123,7 +123,7 @@ func (p64 period64) String() string { return buf.String() } -func writeField64(w io.Writer, field int, designator byte) { +func writeField64(w io.Writer, field int64, designator byte) { if field != 0 { if field%10 != 0 { fmt.Fprintf(w, "%g", float32(field)/10) diff --git a/period/parse.go b/period/parse.go index c6fb8d0d..304bc501 100644 --- a/period/parse.go +++ b/period/parse.go @@ -76,7 +76,7 @@ func parse(period string, normalise bool) (*period64, error) { } remaining = remaining[1:] - var number, weekValue, prevFraction int + var number, weekValue, prevFraction int64 result := &period64{input: period, neg: neg} var years, months, weeks, days, hours, minutes, seconds itemState var designator, prevDesignator byte @@ -163,7 +163,7 @@ const ( Set ) -func (i itemState) testAndSet(number int, designator byte, result *period64, value *int) (itemState, error) { +func (i itemState) testAndSet(number int64, designator byte, result *period64, value *int64) (itemState, error) { switch i { case Unready: return i, fmt.Errorf("%s: '%c' designator cannot occur here", result.input, designator) @@ -177,7 +177,7 @@ func (i itemState) testAndSet(number int, designator byte, result *period64, val //------------------------------------------------------------------------------------------------- -func parseNextField(str, original string) (int, byte, string, error) { +func parseNextField(str, original string) (int64, byte, string, error) { i := scanDigits(str) if i < 0 { return 0, 0, "", fmt.Errorf("%s: missing designator at the end", original) @@ -189,28 +189,28 @@ func parseNextField(str, original string) (int, byte, string, error) { } // Fixed-point one decimal place -func parseDecimalNumber(number, original string, des byte) (int, error) { +func parseDecimalNumber(number, original string, des byte) (int64, error) { dec := strings.IndexByte(number, '.') if dec < 0 { dec = strings.IndexByte(number, ',') } - var integer, fraction int + var integer, fraction int64 var err error if dec >= 0 { - integer, err = strconv.Atoi(number[:dec]) + integer, err = strconv.ParseInt(number[:dec], 10, 64) if err == nil { number = number[dec+1:] switch len(number) { case 0: // skip case 1: - fraction, err = strconv.Atoi(number) + fraction, err = strconv.ParseInt(number, 10, 64) default: - fraction, err = strconv.Atoi(number[:1]) + fraction, err = strconv.ParseInt(number[:1], 10, 64) } } } else { - integer, err = strconv.Atoi(number) + integer, err = strconv.ParseInt(number, 10, 64) } if err != nil { diff --git a/period/period64.go b/period/period64.go index f3ea7892..f3e2f4b0 100644 --- a/period/period64.go +++ b/period/period64.go @@ -9,7 +9,7 @@ import ( // used for stages in arithmetic type period64 struct { // always positive values - years, months, days, hours, minutes, seconds int + years, months, days, hours, minutes, seconds int64 // true if the period is negative neg bool input string @@ -18,15 +18,15 @@ type period64 struct { func (period Period) toPeriod64(input string) *period64 { if period.IsNegative() { return &period64{ - years: int(-period.years), months: int(-period.months), days: int(-period.days), - hours: int(-period.hours), minutes: int(-period.minutes), seconds: int(-period.seconds), + years: int64(-period.years), months: int64(-period.months), days: int64(-period.days), + hours: int64(-period.hours), minutes: int64(-period.minutes), seconds: int64(-period.seconds), neg: true, input: input, } } return &period64{ - years: int(period.years), months: int(period.months), days: int(period.days), - hours: int(period.hours), minutes: int(period.minutes), seconds: int(period.seconds), + years: int64(period.years), months: int64(period.months), days: int64(period.days), + hours: int64(period.hours), minutes: int64(period.minutes), seconds: int64(period.seconds), input: input, } } diff --git a/period/period_test.go b/period/period_test.go index 5588972d..31ecc932 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -804,24 +804,24 @@ func TestNormaliseChanged(t *testing.T) { // note: the negative cases are also covered (see below) // carry seconds to minutes - {source: period64{seconds: 600}, precise: "PT1M"}, - {source: period64{seconds: 700}, precise: "PT1M10S"}, - {source: period64{seconds: 6990}, precise: "PT11M39S"}, + //{source: period64{seconds: 600}, precise: "PT1M"}, + //{source: period64{seconds: 700}, precise: "PT1M10S"}, + //{source: period64{seconds: 6990}, precise: "PT11M39S"}, // carry minutes to hours - {source: period64{minutes: 700}, precise: "PT1H10M"}, - {source: period64{minutes: 6990}, precise: "PT11H39M"}, + //{source: period64{minutes: 700}, precise: "PT1H10M"}, + //{source: period64{minutes: 6990}, precise: "PT11H39M"}, // carry hours to days - {source: period64{hours: 480}, precise: "PT48H", approx: "P2D"}, - {source: period64{hours: 490}, precise: "PT49H", approx: "P2D T1H"}, - {source: period64{hours: 32761}, precise: "PT3276.1H", approx: "P4M 14D T 16.9H"}, - {source: period64{years: 10, months: 20, days: 30, hours: 32767}, precise: "P1Y 2M 3D T3276.7H", approx: "P1Y 6M 17D T17.5H"}, - {source: period64{hours: 32768}, precise: "P136DT12.8H", approx: "P4M 14D T17.6H"}, - {source: period64{years: 10, months: 20, days: 30, hours: 32768}, precise: "P1Y 2M 139D T12.8H", approx: "P1Y 6M 17D T17.6H"}, + //{source: period64{hours: 480}, precise: "PT48H", approx: "P2D"}, + //{source: period64{hours: 490}, precise: "PT49H", approx: "P2D T1H"}, + //{source: period64{hours: 32761}, precise: "PT3276.1H", approx: "P4M 14D T 16.9H"}, + //{source: period64{years: 10, months: 20, days: 30, hours: 32767}, precise: "P1Y 2M 3D T3276.7H", approx: "P1Y 6M 17D T17.5H"}, + //{source: period64{hours: 32768}, precise: "P136DT12.8H", approx: "P4M 14D T17.6H"}, + //{source: period64{years: 10, months: 20, days: 30, hours: 32768}, precise: "P1Y 2M 139D T12.8H", approx: "P1Y 6M 17D T17.6H"}, // carry days to months - {source: period64{days: 310}, precise: "P31D", approx: "P1M 0.5D"}, + //{source: period64{days: 310}, precise: "P31D", approx: "P1M 0.5D"}, {source: period64{days: 32760}, precise: "P3276D", approx: "P8Y 11M 19.2D"}, {source: period64{days: 32761}, precise: "P8Y 11M 19.3D"}, From df0520392e01276498e40e23b04a1abb624ffe34 Mon Sep 17 00:00:00 2001 From: Tamal Saha Date: Wed, 21 Oct 2020 01:42:39 -0700 Subject: [PATCH 146/165] Add flag methods to Period Signed-off-by: Tamal Saha --- period/flag.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 period/flag.go diff --git a/period/flag.go b/period/flag.go new file mode 100644 index 00000000..9488efa7 --- /dev/null +++ b/period/flag.go @@ -0,0 +1,16 @@ +package period + +// Set enables use of Period by the flag API. +func (period *Period) Set(p string) error { + p2, err := Parse(p) + if err != nil { + return err + } + *period = p2 + return nil +} + +// Type is for compatibility with the spf13/pflag library. +func (period Period) Type() string { + return "period" +} From 0bd1a9f267eea5c2612f220800d67f243417ba3d Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Mon, 2 Nov 2020 13:12:09 +0000 Subject: [PATCH 147/165] updated dependencies --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index a858324c..3f31c549 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/rickb777/date require ( - github.com/onsi/gomega v1.10.2 + github.com/onsi/gomega v1.10.3 github.com/rickb777/plural v1.2.2 - golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 // indirect - golang.org/x/text v0.3.3 + golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 // indirect + golang.org/x/text v0.3.4 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect ) diff --git a/go.sum b/go.sum index 24711eab..c4d0e084 100644 --- a/go.sum +++ b/go.sum @@ -19,31 +19,31 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs= -github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/rickb777/plural v1.2.2 h1:4CU5NiUqXSM++2+7JCrX+oguXd2D7RY5O1YisMw1yCI= github.com/rickb777/plural v1.2.2/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M= golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= From 15a0c2a4044e50285a49af4a67d053dd1b412cd6 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Fri, 11 Dec 2020 17:38:26 +0000 Subject: [PATCH 148/165] added SQL support for Period type, i.e. Scanner and Valuer --- go.mod | 5 +++-- go.sum | 17 +++++++++------ period/marshal.go | 4 +++- period/sql.go | 36 ++++++++++++++++++++++++++++++++ period/sql_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 period/sql.go create mode 100644 period/sql_test.go diff --git a/go.mod b/go.mod index 3f31c549..59c87d35 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ module github.com/rickb777/date require ( - github.com/onsi/gomega v1.10.3 + github.com/onsi/gomega v1.10.4 github.com/rickb777/plural v1.2.2 - golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 // indirect + golang.org/x/net v0.0.0-20201209123823-ac852fbbde11 // indirect golang.org/x/text v0.3.4 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) go 1.15 diff --git a/go.sum b/go.sum index c4d0e084..435cf6ff 100644 --- a/go.sum +++ b/go.sum @@ -19,18 +19,18 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA= -github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U= +github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ= github.com/rickb777/plural v1.2.2 h1:4CU5NiUqXSM++2+7JCrX+oguXd2D7RY5O1YisMw1yCI= github.com/rickb777/plural v1.2.2/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M= -golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11 h1:lwlPPsmjDKK0J6eG6xDWd5XPehI0R024zxjDnw3esPA= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -39,6 +39,9 @@ golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -65,3 +68,5 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/period/marshal.go b/period/marshal.go index c87ad5f6..b1eeb3e8 100644 --- a/period/marshal.go +++ b/period/marshal.go @@ -12,17 +12,19 @@ func (period Period) MarshalBinary() ([]byte, error) { } // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. -// This also provides support for gob encoding. +// This also provides support for gob decoding. func (period *Period) UnmarshalBinary(data []byte) error { return period.UnmarshalText(data) } // MarshalText implements the encoding.TextMarshaler interface for Periods. +// This also provides support for JSON encoding. func (period Period) MarshalText() ([]byte, error) { return []byte(period.String()), nil } // UnmarshalText implements the encoding.TextUnmarshaler interface for Periods. +// This also provides support for JSON decoding. func (period *Period) UnmarshalText(data []byte) (err error) { u, err := Parse(string(data)) if err == nil { diff --git a/period/sql.go b/period/sql.go new file mode 100644 index 00000000..660b512c --- /dev/null +++ b/period/sql.go @@ -0,0 +1,36 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package period + +import ( + "database/sql/driver" + "fmt" +) + +// Scan parses some value, which can be either string ot []byte. +// It implements sql.Scanner, https://golang.org/pkg/database/sql/#Scanner +func (p *Period) Scan(value interface{}) (err error) { + if value == nil { + return nil + } + + err = nil + switch v := value.(type) { + case []byte: + *p, err = Parse(string(v)) + case string: + *p, err = Parse(v) + default: + err = fmt.Errorf("%T %+v is not a meaningful period", value, value) + } + + return err +} + +// Value converts the value to a string. It implements driver.Valuer, +// https://golang.org/pkg/database/sql/driver/#Valuer +func (p Period) Value() (driver.Value, error) { + return p.String(), nil +} diff --git a/period/sql_test.go b/period/sql_test.go new file mode 100644 index 00000000..80823f35 --- /dev/null +++ b/period/sql_test.go @@ -0,0 +1,52 @@ +// Copyright 2015 Rick Beton. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package period + +import ( + "database/sql/driver" + "testing" + + . "github.com/onsi/gomega" +) + +func TestPeriodScan(t *testing.T) { + g := NewGomegaWithT(t) + + cases := []struct { + v interface{} + expected Period + }{ + {[]byte("P1Y3M"), MustParse("P1Y3M")}, + {"P1Y3M", MustParse("P1Y3M")}, + } + + for _, c := range cases { + r := new(Period) + e := r.Scan(c.v) + g.Expect(e).NotTo(HaveOccurred()) + g.Expect(*r).To(Equal(c.expected)) + + var d driver.Valuer = *r + + q, e := d.Value() + g.Expect(e).NotTo(HaveOccurred()) + g.Expect(q.(string)).To(Equal(c.expected.String())) + } +} + +func TestPeriodScan_nil_value(t *testing.T) { + g := NewGomegaWithT(t) + r := new(Period) + e := r.Scan(nil) + g.Expect(e).NotTo(HaveOccurred()) +} + +func TestPeriodScan_problem_type(t *testing.T) { + g := NewGomegaWithT(t) + r := new(Period) + e := r.Scan(1) + g.Expect(e).To(HaveOccurred()) + g.Expect(e.Error()).To(ContainSubstring("not a meaningful period")) +} From 80b1a0385969ebe3c755c0ac477b71b2f584b59f Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Fri, 11 Dec 2020 18:10:09 +0000 Subject: [PATCH 149/165] comments etc --- period/sql.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/period/sql.go b/period/sql.go index 660b512c..31bf248a 100644 --- a/period/sql.go +++ b/period/sql.go @@ -9,9 +9,9 @@ import ( "fmt" ) -// Scan parses some value, which can be either string ot []byte. +// Scan parses some value, which can be either string or []byte. // It implements sql.Scanner, https://golang.org/pkg/database/sql/#Scanner -func (p *Period) Scan(value interface{}) (err error) { +func (period *Period) Scan(value interface{}) (err error) { if value == nil { return nil } @@ -19,9 +19,9 @@ func (p *Period) Scan(value interface{}) (err error) { err = nil switch v := value.(type) { case []byte: - *p, err = Parse(string(v)) + *period, err = Parse(string(v)) case string: - *p, err = Parse(v) + *period, err = Parse(v) default: err = fmt.Errorf("%T %+v is not a meaningful period", value, value) } @@ -29,8 +29,8 @@ func (p *Period) Scan(value interface{}) (err error) { return err } -// Value converts the value to a string. It implements driver.Valuer, +// Value converts the period to a string. It implements driver.Valuer, // https://golang.org/pkg/database/sql/driver/#Valuer -func (p Period) Value() (driver.Value, error) { - return p.String(), nil +func (period Period) Value() (driver.Value, error) { + return period.String(), nil } From e9e3f8d4f001367644f7e3ea02d6995d336e5eca Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 16 Dec 2020 08:13:33 +0000 Subject: [PATCH 150/165] sql Scan now disables normalisation, assuming that the retrieved value exactly matches that expected --- period/sql.go | 4 ++-- period/sql_test.go | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/period/sql.go b/period/sql.go index 31bf248a..31b54642 100644 --- a/period/sql.go +++ b/period/sql.go @@ -19,9 +19,9 @@ func (period *Period) Scan(value interface{}) (err error) { err = nil switch v := value.(type) { case []byte: - *period, err = Parse(string(v)) + *period, err = Parse(string(v), false) case string: - *period, err = Parse(v) + *period, err = Parse(v, false) default: err = fmt.Errorf("%T %+v is not a meaningful period", value, value) } diff --git a/period/sql_test.go b/period/sql_test.go index 80823f35..b3462250 100644 --- a/period/sql_test.go +++ b/period/sql_test.go @@ -18,8 +18,13 @@ func TestPeriodScan(t *testing.T) { v interface{} expected Period }{ - {[]byte("P1Y3M"), MustParse("P1Y3M")}, - {"P1Y3M", MustParse("P1Y3M")}, + {[]byte("P1Y3M"), MustParse("P1Y3M", false)}, + {"P1Y3M", MustParse("P1Y3M", false)}, + + // normalise should be disabled so that the retrieved value exactly + // matches the stored value + {[]byte("P48M"), MustParse("P48M", false)}, + {"P48M", MustParse("P48M", false)}, } for _, c := range cases { From 5225eda7026b43a6a866ab91694085df11f9e085 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 16 Dec 2020 08:43:10 +0000 Subject: [PATCH 151/165] marshaling also disables normalisation, assuming that the retrieved value exactly matches that expected --- period/arithmetic_test.go | 8 ++++---- period/marshal.go | 2 +- period/marshal_test.go | 3 ++- period/period_test.go | 30 +++++++++++++++--------------- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/period/arithmetic_test.go b/period/arithmetic_test.go index 2de1a619..fde1d27f 100644 --- a/period/arithmetic_test.go +++ b/period/arithmetic_test.go @@ -52,8 +52,8 @@ func TestPeriodScale(t *testing.T) { //{"P365.5D", 0.1, "P36DT12H"}, } for i, c := range cases { - s := MustParse(c.one).Scale(c.m) - g.Expect(s).To(Equal(MustParse(c.expect)), info(i, c.expect)) + s := MustParse(c.one, false).Scale(c.m) + g.Expect(s).To(Equal(MustParse(c.expect, false)), info(i, c.expect)) } } @@ -75,9 +75,9 @@ func TestPeriodAdd(t *testing.T) { {"P7Y7M7DT7H7M7S", "-P7Y7M7DT7H7M7S", "P0D"}, } for i, c := range cases { - s := MustParse(c.one).Add(MustParse(c.two)) + s := MustParse(c.one, false).Add(MustParse(c.two, false)) expectValid(t, s, info(i, c.expect)) - g.Expect(s).To(Equal(MustParse(c.expect)), info(i, c.expect)) + g.Expect(s).To(Equal(MustParse(c.expect, false)), info(i, c.expect)) } } diff --git a/period/marshal.go b/period/marshal.go index b1eeb3e8..6e1f2f1c 100644 --- a/period/marshal.go +++ b/period/marshal.go @@ -26,7 +26,7 @@ func (period Period) MarshalText() ([]byte, error) { // UnmarshalText implements the encoding.TextUnmarshaler interface for Periods. // This also provides support for JSON decoding. func (period *Period) UnmarshalText(data []byte) (err error) { - u, err := Parse(string(data)) + u, err := Parse(string(data), false) if err == nil { *period = u } diff --git a/period/marshal_test.go b/period/marshal_test.go index 2f25fc71..191539a3 100644 --- a/period/marshal_test.go +++ b/period/marshal_test.go @@ -32,9 +32,10 @@ func TestGobEncoding(t *testing.T) { "-P2Y3M4W5D", "P2Y3M4W5DT1H7M9S", "-P2Y3M4W5DT1H7M9S", + "P48M", } for i, c := range cases { - period := MustParse(c) + period := MustParse(c, false) var p Period err := encoder.Encode(&period) g.Expect(err).NotTo(HaveOccurred(), info(i, c)) diff --git a/period/period_test.go b/period/period_test.go index 31ecc932..043fe782 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -262,16 +262,16 @@ func TestPeriodIntComponents(t *testing.T) { {value: "P1Y", y: 1}, {value: "P1W", w: 1, d: 7}, {value: "P6M", m: 6}, - {value: "P12M", y: 1}, + {value: "P12M", m: 12}, {value: "P39D", w: 5, d: 39, dx: 4}, {value: "P4D", d: 4, dx: 4}, {value: "PT12H", hh: 12}, - {value: "PT60M", hh: 1}, + {value: "PT60M", mm: 60}, {value: "PT30M", mm: 30}, {value: "PT5S", ss: 5}, } for i, c := range cases { - pp := MustParse(c.value) + pp := MustParse(c.value, false) g.Expect(pp.Years()).To(Equal(c.y), info(i, pp)) g.Expect(pp.Months()).To(Equal(c.m), info(i, pp)) g.Expect(pp.Weeks()).To(Equal(c.w), info(i, pp)) @@ -395,7 +395,7 @@ func testPeriodToDuration(t *testing.T, i int, value string, duration time.Durat t.Helper() g := NewGomegaWithT(t) hint := info(i, "%s %s %v", value, duration, precise) - pp := MustParse(value) + pp := MustParse(value, false) d1, prec := pp.Duration() g.Expect(d1).To(Equal(duration), hint) g.Expect(prec).To(Equal(precise), hint) @@ -443,7 +443,7 @@ func TestSignPositiveNegative(t *testing.T) { {"-P0.1Y", false, true, -1}, } for i, c := range cases { - p := MustParse(c.value) + p := MustParse(c.value, false) g.Expect(p.IsPositive()).To(Equal(c.positive), info(i, c.value)) g.Expect(p.IsNegative()).To(Equal(c.negative), info(i, c.value)) g.Expect(p.Sign()).To(Equal(c.sign), info(i, c.value)) @@ -469,7 +469,7 @@ func TestPeriodApproxDays(t *testing.T) { {"P1Y", 365}, } for i, c := range cases { - p := MustParse(c.value) + p := MustParse(c.value, false) td1 := p.TotalDaysApprox() g.Expect(td1).To(Equal(c.approxDays), info(i, c.value)) @@ -504,7 +504,7 @@ func TestPeriodApproxMonths(t *testing.T) { {"PT744H", 1}, } for i, c := range cases { - p := MustParse(c.value) + p := MustParse(c.value, false) td1 := p.TotalMonthsApprox() g.Expect(td1).To(Equal(c.approxMonths), info(i, c.value)) @@ -837,8 +837,8 @@ func TestNormaliseChanged(t *testing.T) { if c.approx == "" { c.approx = c.precise } - pp := MustParse(nospace(c.precise)) - pa := MustParse(nospace(c.approx)) + pp := MustParse(nospace(c.precise), false) + pa := MustParse(nospace(c.approx), false) testNormalise(t, i, c.source, pp, true) testNormalise(t, i, c.source, pa, false) c.source.neg = true @@ -916,7 +916,7 @@ func TestPeriodFormat(t *testing.T) { {"PT1.1S", "1.1 seconds", ""}, } for i, c := range cases { - p := MustParse(c.period) + p := MustParse(c.period, false) sp := p.Format() g.Expect(sp).To(Equal(c.expectW), info(i, "%s -> %s", p, c.expectW)) @@ -925,7 +925,7 @@ func TestPeriodFormat(t *testing.T) { g.Expect(sn).To(Equal(c.expectW), info(i, "%s -> %s", en, c.expectW)) if c.expectD != "" { - s := MustParse(c.period).FormatWithoutWeeks() + s := MustParse(c.period, false).FormatWithoutWeeks() g.Expect(s).To(Equal(c.expectD), info(i, "%s -> %s", p, c.expectD)) } } @@ -944,8 +944,8 @@ func TestPeriodOnlyYMD(t *testing.T) { {"-P6Y5M4DT3H2M1S", "-P6Y5M4D"}, } for i, c := range cases { - s := MustParse(c.one).OnlyYMD() - g.Expect(s).To(Equal(MustParse(c.expect)), info(i, c.expect)) + s := MustParse(c.one, false).OnlyYMD() + g.Expect(s).To(Equal(MustParse(c.expect, false)), info(i, c.expect)) } } @@ -960,8 +960,8 @@ func TestPeriodOnlyHMS(t *testing.T) { {"-P6Y5M4DT3H2M1S", "-PT3H2M1S"}, } for i, c := range cases { - s := MustParse(c.one).OnlyHMS() - g.Expect(s).To(Equal(MustParse(c.expect)), info(i, c.expect)) + s := MustParse(c.one, false).OnlyHMS() + g.Expect(s).To(Equal(MustParse(c.expect, false)), info(i, c.expect)) } } From ae06fa11053285643332dd1b2f7bc33d7c51a92e Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Thu, 17 Dec 2020 14:45:50 +0000 Subject: [PATCH 152/165] updated dependencies --- go.mod | 4 ++-- go.sum | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 59c87d35..13c83f53 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,8 @@ module github.com/rickb777/date require ( github.com/onsi/gomega v1.10.4 - github.com/rickb777/plural v1.2.2 - golang.org/x/net v0.0.0-20201209123823-ac852fbbde11 // indirect + github.com/rickb777/plural v1.3.0 + golang.org/x/net v0.0.0-20201216054612-986b41b23924 // indirect golang.org/x/text v0.3.4 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 435cf6ff..6a1fa0d0 100644 --- a/go.sum +++ b/go.sum @@ -23,14 +23,16 @@ github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U= github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ= github.com/rickb777/plural v1.2.2 h1:4CU5NiUqXSM++2+7JCrX+oguXd2D7RY5O1YisMw1yCI= github.com/rickb777/plural v1.2.2/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA= +github.com/rickb777/plural v1.3.0 h1:cN3M4IcJCGiGpa92S3xJgiBQfqGDFj7J8JyObugVwAU= +github.com/rickb777/plural v1.3.0/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11 h1:lwlPPsmjDKK0J6eG6xDWd5XPehI0R024zxjDnw3esPA= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY= +golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= From 5df9e8c6809053cc3540a3c6845f6df7f41fc42a Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Mon, 15 Feb 2021 22:06:55 +0000 Subject: [PATCH 153/165] updated dependencies --- go.mod | 6 +++--- go.sum | 14 ++++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 13c83f53..282b1a2c 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/rickb777/date require ( - github.com/onsi/gomega v1.10.4 + github.com/onsi/gomega v1.10.5 github.com/rickb777/plural v1.3.0 - golang.org/x/net v0.0.0-20201216054612-986b41b23924 // indirect - golang.org/x/text v0.3.4 + golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect + golang.org/x/text v0.3.5 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 6a1fa0d0..6ea427e9 100644 --- a/go.sum +++ b/go.sum @@ -19,10 +19,8 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U= -github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ= -github.com/rickb777/plural v1.2.2 h1:4CU5NiUqXSM++2+7JCrX+oguXd2D7RY5O1YisMw1yCI= -github.com/rickb777/plural v1.2.2/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA= +github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ= +github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= github.com/rickb777/plural v1.3.0 h1:cN3M4IcJCGiGpa92S3xJgiBQfqGDFj7J8JyObugVwAU= github.com/rickb777/plural v1.3.0/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -31,8 +29,8 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY= -golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -47,8 +45,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= From 2e89fc3894a76d04fdf60d78a6141015a083f6f0 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Sat, 11 Sep 2021 09:34:06 +0100 Subject: [PATCH 154/165] upgrade to Go 1.17 --- go.mod | 3 +-- go.sum | 11 ----------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 282b1a2c..8aad16ad 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,7 @@ require ( github.com/rickb777/plural v1.3.0 golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect golang.org/x/text v0.3.5 - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) -go 1.15 +go 1.17 diff --git a/go.sum b/go.sum index 6ea427e9..b58c9912 100644 --- a/go.sum +++ b/go.sum @@ -6,11 +6,9 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= @@ -27,7 +25,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -37,28 +34,21 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -66,7 +56,6 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= From 59eb296c841374e8a09a7651482f938e7f30661e Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Fri, 17 Dec 2021 09:42:09 +0000 Subject: [PATCH 155/165] Date JSON marshalling/unmarshalling tweaked so that the zero value can be represented as a blank string and the tag 'omitempty' can now work. --- go.mod | 12 ++++++---- go.sum | 61 ++++++++++++++++++++++++++++++++++++++----------- marshal.go | 32 ++++++++++++++++++++++++++ marshal_test.go | 6 ++--- 4 files changed, 91 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 8aad16ad..5df922f1 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,14 @@ module github.com/rickb777/date require ( - github.com/onsi/gomega v1.10.5 - github.com/rickb777/plural v1.3.0 - golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect - golang.org/x/text v0.3.5 + github.com/onsi/gomega v1.17.0 + github.com/rickb777/plural v1.4.1 + golang.org/x/text v0.3.7 +) + +require ( + golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect + golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index b58c9912..d645b677 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -7,54 +11,85 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ= -github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= -github.com/rickb777/plural v1.3.0 h1:cN3M4IcJCGiGpa92S3xJgiBQfqGDFj7J8JyObugVwAU= -github.com/rickb777/plural v1.3.0/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rickb777/plural v1.4.1 h1:5MMLcbIaapLFmvDGRT5iPk8877hpTPt8Y9cdSKRw9sU= +github.com/rickb777/plural v1.4.1/go.mod h1:kdmXUpmKBJTS0FtG/TFumd//VBWsNTD7zOw7x4umxNw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= +golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/marshal.go b/marshal.go index 377a64ff..2880268d 100644 --- a/marshal.go +++ b/marshal.go @@ -43,6 +43,22 @@ func (ds *DateString) UnmarshalBinary(data []byte) error { return (*Date)(ds).UnmarshalBinary(data) } +// MarshalJSON implements the json.Marshaler interface. +// The date is given in ISO 8601 extended format (e.g. "2006-01-02"). +// If the year of the date falls outside the [0,9999] range, this format +// produces an expanded year representation with possibly extra year digits +// beyond the prescribed four-digit minimum and with a + or - sign prefix +// (e.g. , "+12345-06-07", "-0987-06-05"). +// Note that the zero value is marshalled as a blank string, which allows +// "omitempty" to work. +func (d Date) MarshalJSON() ([]byte, error) { + if d.IsZero() { + return []byte{'"', '"'}, nil + } + s := "\"" + d.String() + "\"" + return []byte(s), nil +} + // MarshalText implements the encoding.TextMarshaler interface. // The date is given in ISO 8601 extended format (e.g. "2006-01-02"). // If the year of the date falls outside the [0,9999] range, this format @@ -58,7 +74,11 @@ func (d Date) MarshalText() ([]byte, error) { // (e.g. "2006-01-02", "+12345-06-07", "-0987-06-05"); // the year must use at least 4 digits and if outside the [0,9999] range // must be prefixed with a + or - sign. +// Note that the a blank string is unmarshalled as the zero value. func (d *Date) UnmarshalText(data []byte) (err error) { + if len(data) == 0 { + return nil + } u, err := ParseISO(string(data)) if err == nil { d.day = u.day @@ -66,6 +86,18 @@ func (d *Date) UnmarshalText(data []byte) (err error) { return err } +// MarshalJSON implements the json.Marshaler interface. +// The date is given in ISO 8601 extended format (e.g. "2006-01-02"). +// If the year of the date falls outside the [0,9999] range, this format +// produces an expanded year representation with possibly extra year digits +// beyond the prescribed four-digit minimum and with a + or - sign prefix +// (e.g. , "+12345-06-07", "-0987-06-05"). +// Note that the zero value is marshalled as a blank string, which allows +// "omitempty" to work. +func (ds DateString) MarshalJSON() ([]byte, error) { + return Date(ds).MarshalJSON() +} + // MarshalText implements the encoding.TextMarshaler interface. // The date is given in ISO 8601 extended format (e.g. "2006-01-02"). // If the year of the date falls outside the [0,9999] range, this format diff --git a/marshal_test.go b/marshal_test.go index fc15174d..fd183831 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -64,7 +64,7 @@ func TestDateJSONMarshalling(t *testing.T) { {New(-1, time.December, 31), `"-0001-12-31"`}, {New(0, time.January, 1), `"0000-01-01"`}, {New(1, time.January, 1), `"0001-01-01"`}, - {New(1970, time.January, 1), `"1970-01-01"`}, + {New(1970, time.January, 1), `""`}, {New(2012, time.June, 25), `"2012-06-25"`}, {New(12345, time.June, 7), `"+12345-06-07"`}, } @@ -120,7 +120,7 @@ func TestDateTextMarshalling(t *testing.T) { if err != nil { t.Errorf("Text(%v) marshal error %v", c, err) } else if string(bb1) != c.want { - t.Errorf("Text(%v) == %v, want %v", c.value, string(bb1), c.want) + t.Errorf("Text(%v) == %q, want %q", c.value, string(bb1), c.want) } else { err = d.UnmarshalText(bb1) if err != nil { @@ -135,7 +135,7 @@ func TestDateTextMarshalling(t *testing.T) { if err != nil { t.Errorf("Text(%v) marshal error %v", c, err) } else if string(bb2) != c.want { - t.Errorf("Text(%v) == %v, want %v", c.value, string(bb2), c.want) + t.Errorf("Text(%v) == %v, want %q", c.value, string(bb2), c.want) } else { err = ds.UnmarshalText(bb2) if err != nil { From 126ffe0dbb4d6ed08d6ec26a7d4e02c4d3663d7b Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Wed, 2 Feb 2022 14:27:27 +0000 Subject: [PATCH 156/165] Updated dependencies. Began work on forked repo for `period`. --- .travis.yml | 2 +- build+test.sh | 1 - go.mod | 6 +++--- go.sum | 23 ++++++++++++++++------- period/doc.go | 3 +++ 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 704200e4..e77cc767 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: go go: - - '1.15' + - '1.17' install: - go get -t -v ./... diff --git a/build+test.sh b/build+test.sh index d76ff538..443456e8 100755 --- a/build+test.sh +++ b/build+test.sh @@ -2,7 +2,6 @@ cd "$(dirname $0)" PATH=$HOME/go/bin:$PATH unset GOPATH -export GO111MODULE=on export GOARCH=${1} function v diff --git a/go.mod b/go.mod index 5df922f1..0e09baa0 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,14 @@ module github.com/rickb777/date require ( - github.com/onsi/gomega v1.17.0 + github.com/onsi/gomega v1.18.1 github.com/rickb777/plural v1.4.1 golang.org/x/text v0.3.7 ) require ( - golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect - golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect + golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect + golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index d645b677..be2cda00 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,9 @@ +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -17,18 +19,22 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ= +github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rickb777/plural v1.4.1 h1:5MMLcbIaapLFmvDGRT5iPk8877hpTPt8Y9cdSKRw9sU= github.com/rickb777/plural v1.4.1/go.mod h1:kdmXUpmKBJTS0FtG/TFumd//VBWsNTD7zOw7x4umxNw= @@ -45,8 +51,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -56,14 +62,18 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 h1:XDXtA5hveEEV8JB2l7nhMTp3t3cHp9ZpwcdjqyEWLlo= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -87,7 +97,6 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/period/doc.go b/period/doc.go index c8a57b4a..e328b0ba 100644 --- a/period/doc.go +++ b/period/doc.go @@ -5,6 +5,9 @@ // Package period provides functionality for periods of time using ISO-8601 conventions. // This deals with years, months, weeks/days, hours, minutes and seconds. // +// *** Warning: this package is the subject of many issues, so a replacement is under +// development. Please see https://github.com/rickb777/period. +// // Because of the vagaries of calendar systems, the meaning of year lengths, month lengths // and even day lengths depends on context. So a period is not necessarily a fixed duration // of time in terms of seconds. From c32516f0b1524949a30cd442a76d637569c3865b Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Thu, 12 May 2022 16:41:06 +0100 Subject: [PATCH 157/165] Bugfix: Date.MarshalJSON incorrectly wrote the zero value as a blank string, which might raise difficulties are the receiver. Code that relied on this incorrect behaviour might see this as a breaking change. --- build+test.sh | 2 ++ date_test.go | 12 ++++++------ format.go | 16 ++++++++++++++-- format_test.go | 6 +++--- go.mod | 8 +++++--- go.sum | 20 ++++++++++++-------- marshal.go | 12 +++++++----- marshal_test.go | 17 ++++++++++------- sql.go | 26 ++++++++++++++++++-------- sql_test.go | 18 +++++------------- 10 files changed, 82 insertions(+), 55 deletions(-) diff --git a/build+test.sh b/build+test.sh index 443456e8..b07e45b3 100755 --- a/build+test.sh +++ b/build+test.sh @@ -16,10 +16,12 @@ if ! type -p goveralls; then fi if ! type -p shadow; then + v go get golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow v go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow fi if ! type -p goreturns; then + v go get github.com/sqs/goreturns v go install github.com/sqs/goreturns fi diff --git a/date_test.go b/date_test.go index b75e8575..39bf6e64 100644 --- a/date_test.go +++ b/date_test.go @@ -23,7 +23,7 @@ func same(d Date, t time.Time) bool { yd == yt && wd == wt } -func TestNew(t *testing.T) { +func TestDate_New(t *testing.T) { cases := []string{ "0000-01-01T00:00:00+00:00", "0001-01-01T00:00:00+00:00", @@ -52,7 +52,7 @@ func TestNew(t *testing.T) { } } -func TestDaysSinceEpoch(t *testing.T) { +func TestDate_DaysSinceEpoch(t *testing.T) { zero := Date{}.DaysSinceEpoch() if zero != 0 { t.Errorf("Non zero %v", zero) @@ -69,7 +69,7 @@ func TestDaysSinceEpoch(t *testing.T) { } } -func TestToday(t *testing.T) { +func TestDate_Today(t *testing.T) { today := Today() now := time.Now() if !same(today, now) { @@ -91,7 +91,7 @@ func TestToday(t *testing.T) { } } -func TestTime(t *testing.T) { +func TestDate_Time(t *testing.T) { cases := []struct { d Date }{ @@ -217,7 +217,7 @@ func TestArithmetic(t *testing.T) { } } -func TestAddDate(t *testing.T) { +func TestDate_AddDate(t *testing.T) { cases := []struct { d Date years, months, days int @@ -241,7 +241,7 @@ func TestAddDate(t *testing.T) { } } -func TestAddPeriod(t *testing.T) { +func TestDate_AddPeriod(t *testing.T) { cases := []struct { in Date delta period.Period diff --git a/format.go b/format.go index c1e59b74..b330a046 100644 --- a/format.go +++ b/format.go @@ -6,6 +6,7 @@ package date import ( "fmt" + "io" "strings" ) @@ -35,11 +36,22 @@ const ( // with possibly extra year digits beyond the prescribed four-digit minimum // and with a + or - sign prefix (e.g. , "+12345-06-07", "-0987-06-05"). func (d Date) String() string { + buf := &strings.Builder{} + buf.Grow(12) + d.WriteTo(buf) + return buf.String() +} + +// WriteTo is as per String, albeit writing to an io.Writer. +func (d Date) WriteTo(w io.Writer) (n64 int64, err error) { + var n int year, month, day := d.Date() if 0 <= year && year < 10000 { - return fmt.Sprintf("%04d-%02d-%02d", year, month, day) + n, err = fmt.Fprintf(w, "%04d-%02d-%02d", year, month, day) + } else { + n, err = fmt.Fprintf(w, "%+05d-%02d-%02d", year, month, day) } - return fmt.Sprintf("%+05d-%02d-%02d", year, month, day) + return int64(n), err } // FormatISO returns a textual representation of the date value formatted diff --git a/format_test.go b/format_test.go index cf596aa3..93568970 100644 --- a/format_test.go +++ b/format_test.go @@ -8,7 +8,7 @@ import ( "testing" ) -func TestString(t *testing.T) { +func TestDate_String(t *testing.T) { cases := []struct { value string }{ @@ -28,7 +28,7 @@ func TestString(t *testing.T) { } } -func TestFormatISO(t *testing.T) { +func TestDate_FormatISO(t *testing.T) { cases := []struct { value string n int @@ -53,7 +53,7 @@ func TestFormatISO(t *testing.T) { } } -func TestFormat(t *testing.T) { +func TestDate_Format(t *testing.T) { cases := []struct { value string format string diff --git a/go.mod b/go.mod index 0e09baa0..3dbf4110 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,16 @@ module github.com/rickb777/date require ( - github.com/onsi/gomega v1.18.1 + github.com/onsi/gomega v1.19.0 github.com/rickb777/plural v1.4.1 golang.org/x/text v0.3.7 ) require ( - golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect - golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 // indirect + github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 // indirect + golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect + golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect + golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index be2cda00..94e90494 100644 --- a/go.sum +++ b/go.sum @@ -28,16 +28,18 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ= -github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.3 h1:e/3Cwtogj0HA+25nMP1jCMDIf8RtRYbGwGGuBIFztkc= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= -github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rickb777/plural v1.4.1 h1:5MMLcbIaapLFmvDGRT5iPk8877hpTPt8Y9cdSKRw9sU= github.com/rickb777/plural v1.4.1/go.mod h1:kdmXUpmKBJTS0FtG/TFumd//VBWsNTD7zOw7x4umxNw= +github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 h1:iD+PFTQwKEmbwSdwfvP5ld2WEI/g7qbdhmHJ2ASfYGs= +github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -51,8 +53,9 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -70,8 +73,8 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 h1:XDXtA5hveEEV8JB2l7nhMTp3t3cHp9ZpwcdjqyEWLlo= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -79,6 +82,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= diff --git a/marshal.go b/marshal.go index 2880268d..15106937 100644 --- a/marshal.go +++ b/marshal.go @@ -5,6 +5,7 @@ package date import ( + "bytes" "errors" ) @@ -52,11 +53,12 @@ func (ds *DateString) UnmarshalBinary(data []byte) error { // Note that the zero value is marshalled as a blank string, which allows // "omitempty" to work. func (d Date) MarshalJSON() ([]byte, error) { - if d.IsZero() { - return []byte{'"', '"'}, nil - } - s := "\"" + d.String() + "\"" - return []byte(s), nil + buf := &bytes.Buffer{} + buf.Grow(14) + buf.WriteByte('"') + d.WriteTo(buf) + buf.WriteByte('"') + return buf.Bytes(), nil } // MarshalText implements the encoding.TextMarshaler interface. diff --git a/marshal_test.go b/marshal_test.go index fd183831..1a02d94c 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -12,7 +12,7 @@ import ( "time" ) -func TestGobEncoding(t *testing.T) { +func TestDate_gob_Encode_round_tripe(t *testing.T) { cases := []Date{ New(-11111, time.February, 3), New(-1, time.December, 31), @@ -55,7 +55,7 @@ func TestGobEncoding(t *testing.T) { } } -func TestDateJSONMarshalling(t *testing.T) { +func TestDate_MarshalJSON_round_trip(t *testing.T) { cases := []struct { value Date want string @@ -64,7 +64,7 @@ func TestDateJSONMarshalling(t *testing.T) { {New(-1, time.December, 31), `"-0001-12-31"`}, {New(0, time.January, 1), `"0000-01-01"`}, {New(1, time.January, 1), `"0001-01-01"`}, - {New(1970, time.January, 1), `""`}, + {New(1970, time.January, 1), `"1970-01-01"`}, {New(2012, time.June, 25), `"2012-06-25"`}, {New(12345, time.June, 7), `"+12345-06-07"`}, } @@ -84,6 +84,7 @@ func TestDateJSONMarshalling(t *testing.T) { } } + // consistency var ds DateString bb2, err := json.Marshal(c.value.DateString()) if err != nil { @@ -101,7 +102,7 @@ func TestDateJSONMarshalling(t *testing.T) { } } -func TestDateTextMarshalling(t *testing.T) { +func TestDate_MarshalText_round_trip(t *testing.T) { cases := []struct { value Date want string @@ -130,6 +131,7 @@ func TestDateTextMarshalling(t *testing.T) { } } + // consistency var ds DateString bb2, err := c.value.DateString().MarshalText() if err != nil { @@ -147,7 +149,7 @@ func TestDateTextMarshalling(t *testing.T) { } } -func TestDateBinaryMarshalling(t *testing.T) { +func TestDate_MarshalBinary_round_trip(t *testing.T) { cases := []struct { value Date }{ @@ -173,6 +175,7 @@ func TestDateBinaryMarshalling(t *testing.T) { } } + // consistency check bb2, err := c.value.MarshalBinary() if err != nil { t.Errorf("Binary(%v) marshal error %v", c, err) @@ -188,7 +191,7 @@ func TestDateBinaryMarshalling(t *testing.T) { } } -func TestDateBinaryUnmarshallingErrors(t *testing.T) { +func TestDate_UnmarshalBinary_errors(t *testing.T) { var d Date err1 := d.UnmarshalBinary([]byte{}) if err1 == nil { @@ -201,7 +204,7 @@ func TestDateBinaryUnmarshallingErrors(t *testing.T) { } } -func TestInvalidDateText(t *testing.T) { +func TestDate_UnmarshalText_invalid_date_text(t *testing.T) { cases := []struct { value string want string diff --git a/sql.go b/sql.go index 54642fc8..426cc457 100644 --- a/sql.go +++ b/sql.go @@ -16,8 +16,11 @@ import ( // The underlying column type can be an integer (period of days since the epoch), // a string, or a DATE. -// Scan parses some value. It implements sql.Scanner, -// https://golang.org/pkg/database/sql/#Scanner +// Scan parses some value. If the value holds an integer, it is treated as the +// period-of-days value that represents a Date. Otherwise, if it holds a string, +// the AutoParse function is used. +// +// This implements sql.Scanner https://golang.org/pkg/database/sql/#Scanner func (d *Date) Scan(value interface{}) (err error) { if value == nil { return nil @@ -54,8 +57,10 @@ func (d *Date) scanString(value string) (err error) { return err } -// Value converts the value to an int64. It implements driver.Valuer, -// https://golang.org/pkg/database/sql/driver/#Valuer +// Value converts the value to an int64. Note that if you need to store as a string, +// convert the Date to a DateString. +// +// This implements driver.Valuer https://golang.org/pkg/database/sql/driver/#Valuer func (d Date) Value() (driver.Value, error) { return int64(d.day), nil } @@ -77,8 +82,11 @@ func (d Date) DateString() DateString { return DateString(d) } -// Scan parses some value. It implements sql.Scanner, -// https://golang.org/pkg/database/sql/#Scanner +// Scan parses some value. If the value holds an integer, it is treated as the +// period-of-days value that represents a Date. Otherwise, if it holds a string, +// the AutoParse function is used. +// +// This implements sql.Scanner https://golang.org/pkg/database/sql/#Scanner func (ds *DateString) Scan(value interface{}) (err error) { if value == nil { return nil @@ -86,8 +94,10 @@ func (ds *DateString) Scan(value interface{}) (err error) { return (*Date)(ds).Scan(value) } -// Value converts the value to an int64. It implements driver.Valuer, -// https://golang.org/pkg/database/sql/driver/#Valuer +// Value converts the value to a string. Note that if you only need to store as an int64, +// convert the DateString to a Date. +// +// This implements driver.Valuer https://golang.org/pkg/database/sql/driver/#Valuer func (ds DateString) Value() (driver.Value, error) { return ds.Date().String(), nil } diff --git a/sql_test.go b/sql_test.go index d07f8de5..44dc5f5e 100644 --- a/sql_test.go +++ b/sql_test.go @@ -9,7 +9,7 @@ import ( "testing" ) -func TestDateScan(t *testing.T) { +func TestDate_Scan(t *testing.T) { cases := []struct { v interface{} expected PeriodOfDays @@ -51,7 +51,7 @@ func TestDateScan(t *testing.T) { } } -func TestDateStringScan(t *testing.T) { +func TestDateString_Scan(t *testing.T) { cases := []struct { v interface{} expected string @@ -88,7 +88,7 @@ func TestDateStringScan(t *testing.T) { } } -func TestDateScanWithJunk(t *testing.T) { +func TestDate_Scan_with_junk(t *testing.T) { cases := []struct { v interface{} expected string @@ -106,7 +106,7 @@ func TestDateScanWithJunk(t *testing.T) { } } -func TestDateStringScanWithJunk(t *testing.T) { +func TestDateString_Scan_with_junk(t *testing.T) { cases := []struct { v interface{} expected string @@ -124,15 +124,7 @@ func TestDateStringScanWithJunk(t *testing.T) { } } -func TestDateScanWithNil(t *testing.T) { - var r *Date - e := r.Scan(nil) - if e != nil { - t.Errorf("Got %v", e) - } -} - -func TestDateAsStringScanWithNil(t *testing.T) { +func TestDate_Scan_with_nil(t *testing.T) { var r *Date e := r.Scan(nil) if e != nil { From 754e6905926bb48ac6ae65cbc4186310136cca1f Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Tue, 2 Aug 2022 21:17:06 +0100 Subject: [PATCH 158/165] Documentation improvements & tests tweaked --- date.go | 20 +++++++++++--------- date_test.go | 52 ++++++++++++++++++++++++++++++++++++++------------- go.mod | 2 -- go.sum | 3 --- parse_test.go | 5 +++-- sql.go | 7 +------ 6 files changed, 54 insertions(+), 35 deletions(-) diff --git a/date.go b/date.go index 8c776f33..da813f70 100644 --- a/date.go +++ b/date.go @@ -19,7 +19,7 @@ type PeriodOfDays int32 // ZeroDays is the named zero value for PeriodOfDays. const ZeroDays PeriodOfDays = 0 -// A Date represents a date under the (proleptic) Gregorian calendar as +// A Date represents a date under the proleptic Gregorian calendar as // used by ISO 8601. This calendar uses astronomical year numbering, // so it includes a year 0 and represents earlier years as negative numbers // (i.e. year 0 is 1 BC; year -1 is 2 BC, and so on). @@ -30,8 +30,8 @@ const ZeroDays PeriodOfDays = 0 // // Programs using dates should typically store and pass them as values, // not pointers. That is, date variables and struct fields should be of -// type date.Date, not *date.Date. A Date value can be used by -// multiple goroutines simultaneously. +// type date.Date, not *date.Date unless the pointer indicates an optional +// value. A Date value can be used by multiple goroutines simultaneously. // // Date values can be compared using the Before, After, and Equal methods // as well as the == and != operators. @@ -40,9 +40,9 @@ const ZeroDays PeriodOfDays = 0 // them. The Add method adds a Date and a number of days, producing a Date. // // The zero value of type Date is Thursday, January 1, 1970 (called 'the -// epoch'), based on Unix convention. As this date is unlikely to come up in -// practice, the IsZero method gives a simple way of detecting a date that -// has not been initialized explicitly. +// epoch'), based on Unix convention. The IsZero method gives a simple way +// of detecting a date that has not been initialized explicitly, with the +// caveat that this is also a 'normal' date. // // The first official date of the Gregorian calendar was Friday, October 15th // 1582, quite unrelated to the epoch used here. The Date type does not @@ -188,7 +188,9 @@ func (d Date) ISOWeek() (year, week int) { return t.ISOWeek() } -// IsZero reports whether t represents the zero date. +// IsZero reports whether d represents the zero (i.e. uninitialised) date. +// Because Date follows Unix conventions, it is based on 1970-01-01. So be +// careful with this: the corresponding 1970-01-01 date is not itself a 'zero'. func (d Date) IsZero() bool { return d.day == 0 } @@ -272,12 +274,12 @@ func (d Date) DaysSinceEpoch() (days PeriodOfDays) { return d.day } -// IsLeap simply tests whether a given year is a leap year, using the Gregorian calendar algorithm. +// IsLeap simply tests whether a given year is a leap year, using the proleptic Gregorian calendar algorithm. func IsLeap(year int) bool { return gregorian.IsLeap(year) } -// DaysIn gives the number of days in a given month, according to the Gregorian calendar. +// DaysIn gives the number of days in a given month, according to the proleptic Gregorian calendar. func DaysIn(year int, month time.Month) int { return gregorian.DaysIn(year, month) } diff --git a/date_test.go b/date_test.go index 39bf6e64..085e3b22 100644 --- a/date_test.go +++ b/date_test.go @@ -52,6 +52,32 @@ func TestDate_New(t *testing.T) { } } +func Test_New_and_Add(t *testing.T) { + cases := []struct { + offset PeriodOfDays + expected Date + }{ + // For year Y, Julian date offset is + // D = [Y/100] - [Y/400] - 2 + {offset: -135140, expected: New(1600, time.January, 1)}, // 10 days Julian offset + {offset: -98615, expected: New(1700, time.January, 1)}, // 10 days Julian offset + {offset: -62091, expected: New(1800, time.January, 1)}, + {offset: -365, expected: New(1969, time.January, 1)}, + {offset: 0, expected: New(1970, time.January, 1)}, + {offset: 365, expected: New(1971, time.January, 1)}, + {offset: 36525, expected: New(2070, time.January, 1)}, + } + + zero := Date{} + + for i, c := range cases { + d2 := zero.Add(c.offset) + if !d2.Equal(c.expected) { + t.Errorf("%d: %d gives %s, wanted %s", i, c.offset, d2, c.expected) + } + } +} + func TestDate_DaysSinceEpoch(t *testing.T) { zero := Date{}.DaysSinceEpoch() if zero != 0 { @@ -178,21 +204,21 @@ func testPredicate(t *testing.T, di, dj Date, p, q bool, m string) { } func TestArithmetic(t *testing.T) { - cases := []struct { - d Date - }{ - {New(-1234, time.February, 5)}, - {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)}, + dates := []Date{ + New(-1234, time.February, 5), + 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), } + offsets := []PeriodOfDays{-1000000, -9999, -555, -99, -22, -1, 0, 1, 22, 99, 555, 9999, 1000000} - for _, c := range cases { - di := c.d + + for _, d := range dates { + di := d for _, days := range offsets { dj := di.Add(days) days2 := dj.Sub(di) diff --git a/go.mod b/go.mod index 3dbf4110..fab31730 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,8 @@ require ( ) require ( - github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 // indirect golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect - golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 94e90494..d40a23ed 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,6 @@ github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rickb777/plural v1.4.1 h1:5MMLcbIaapLFmvDGRT5iPk8877hpTPt8Y9cdSKRw9sU= github.com/rickb777/plural v1.4.1/go.mod h1:kdmXUpmKBJTS0FtG/TFumd//VBWsNTD7zOw7x4umxNw= -github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 h1:iD+PFTQwKEmbwSdwfvP5ld2WEI/g7qbdhmHJ2ASfYGs= -github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -82,7 +80,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= diff --git a/parse_test.go b/parse_test.go index f846c3ee..cb22d146 100644 --- a/parse_test.go +++ b/parse_test.go @@ -16,12 +16,13 @@ func TestAutoParse(t *testing.T) { month time.Month day int }{ + {"01-01-1970", 1970, time.January, 1}, + {"+1970-01-01", 1970, time.January, 1}, + {"+01970-01-02", 1970, time.January, 2}, {" 31/12/1969 ", 1969, time.December, 31}, {"1969/12/31", 1969, time.December, 31}, {"1969.12.31", 1969, time.December, 31}, {"1969-12-31", 1969, time.December, 31}, - {"+1970-01-01", 1970, time.January, 1}, - {"+01970-01-02", 1970, time.January, 2}, {"2000-02-28", 2000, time.February, 28}, {"+2000-02-29", 2000, time.February, 29}, {"+02000-03-01", 2000, time.March, 1}, diff --git a/sql.go b/sql.go index 426cc457..ec10c105 100644 --- a/sql.go +++ b/sql.go @@ -104,10 +104,5 @@ func (ds DateString) Value() (driver.Value, error) { //------------------------------------------------------------------------------------------------- -// DisableTextStorage reduces the Scan method so that only integers are handled. -// Normally, database types int64, []byte, string and time.Time are supported. -// When set true, only int64 is supported; this mode allows optimisation of SQL -// result processing and would only be used during development. -// -// Deprecated: this is no longer used. +// Deprecated: DisableTextStorage is no longer used. var DisableTextStorage = false From 168141bda41b1a29df266ba1b45cdbbe587c30a9 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Fri, 4 Nov 2022 11:45:29 +0000 Subject: [PATCH 159/165] Updated dependencies & re-fmt --- clock/clock.go | 1 - date.go | 1 - datetool/main.go | 1 - doc.go | 5 ++-- format.go | 12 ++++++--- go.mod | 15 ++++++++---- go.sum | 58 ++++++++++++++++++++++++++++++++++++++------ parse.go | 5 ++-- period/doc.go | 1 - period/period.go | 2 -- timespan/doc.go | 1 - timespan/timespan.go | 4 +-- 12 files changed, 77 insertions(+), 29 deletions(-) diff --git a/clock/clock.go b/clock/clock.go index 99de649f..81d018c7 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -3,7 +3,6 @@ // license that can be found in the LICENSE file. // Package clock specifies a time of day with resolution to the nearest millisecond. -// package clock import ( diff --git a/date.go b/date.go index da813f70..76946cb9 100644 --- a/date.go +++ b/date.go @@ -48,7 +48,6 @@ const ZeroDays PeriodOfDays = 0 // 1582, quite unrelated to the epoch used here. The Date type does not // distinguish between official Gregorian dates and earlier proleptic dates, // which can also be represented when needed. -// type Date struct { day PeriodOfDays // day gives the number of days elapsed since date zero. } diff --git a/datetool/main.go b/datetool/main.go index 301f110d..19061441 100644 --- a/datetool/main.go +++ b/datetool/main.go @@ -4,7 +4,6 @@ // This tool prints equivalences between the string representation and the internal numerical // representation for dates and clocks. -// package main import ( diff --git a/doc.go b/doc.go index 9cf72db0..a5ba05bd 100644 --- a/doc.go +++ b/doc.go @@ -19,14 +19,14 @@ // // * `view.VDate` which wraps `Date` for use in templates etc. // -// Credits +// # Credits // // This package follows very closely the design of package time // (http://golang.org/pkg/time/) in the standard library, many of the Date // methods are implemented using the corresponding methods of the time.Time // type, and much of the documentation is copied directly from that package. // -// References +// # References // // https://golang.org/src/time/time.go // @@ -45,5 +45,4 @@ // https://tools.ietf.org/html/rfc1123 // // https://tools.ietf.org/html/rfc3339 -// package date diff --git a/format.go b/format.go index b330a046..ab611352 100644 --- a/format.go +++ b/format.go @@ -13,7 +13,9 @@ import ( // These are predefined layouts for use in Date.Format and Date.Parse. // The reference date used in the layouts is the same date used by the // time package in the standard library: -// Monday, Jan 2, 2006 +// +// Monday, Jan 2, 2006 +// // To define your own format, write down what the reference date would look // like formatted your way; see the values of the predefined layouts for // examples. The model is to demonstrate what the reference date looks like @@ -76,7 +78,9 @@ func (d Date) FormatISO(yearDigits int) string { // Format returns a textual representation of the date value formatted according // to layout, which defines the format by showing how the reference date, // defined to be -// Mon, Jan 2, 2006 +// +// Mon, Jan 2, 2006 +// // would be displayed if it were the value; it serves as an example of the // desired output. // @@ -86,7 +90,9 @@ func (d Date) FormatISO(yearDigits int) string { // // Additionally, it is able to insert the day-number suffix into the output string. // This is done by including "nd" in the format string, which will become -// Mon, Jan 2nd, 2006 +// +// Mon, Jan 2nd, 2006 +// // For example, New Year's Day might be rendered as "Fri, Jan 1st, 2016". To alter // the suffix strings for a different locale, change DaySuffixes or use FormatWithSuffixes // instead. diff --git a/go.mod b/go.mod index fab31730..54607ce6 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,20 @@ module github.com/rickb777/date require ( - github.com/onsi/gomega v1.19.0 + github.com/onsi/gomega v1.24.0 github.com/rickb777/plural v1.4.1 - golang.org/x/text v0.3.7 + golang.org/x/text v0.4.0 ) require ( - golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect - golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/mattn/goveralls v0.0.11 // indirect + github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 // indirect + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect + golang.org/x/net v0.1.0 // indirect + golang.org/x/sys v0.1.0 // indirect + golang.org/x/tools v0.1.12 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) go 1.17 diff --git a/go.sum b/go.sum index d40a23ed..22a16339 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -19,44 +21,73 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/mattn/goveralls v0.0.11 h1:eJXea6R6IFlL1QMKNMzDvvHv/hwGrnvyig4N+0+XiMM= +github.com/mattn/goveralls v0.0.11/go.mod h1:gU8SyhNswsJKchEV93xRQxX6X3Ei4PJdQk/6ZHvrvRk= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.1.3 h1:e/3Cwtogj0HA+25nMP1jCMDIf8RtRYbGwGGuBIFztkc= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= +github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= +github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs= +github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= +github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= +github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= +github.com/onsi/gomega v1.24.0 h1:+0glovB9Jd6z3VR+ScSwQqXVTIfJcGA9UBM8yzQxhqg= +github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rickb777/plural v1.4.1 h1:5MMLcbIaapLFmvDGRT5iPk8877hpTPt8Y9cdSKRw9sU= github.com/rickb777/plural v1.4.1/go.mod h1:kdmXUpmKBJTS0FtG/TFumd//VBWsNTD7zOw7x4umxNw= +github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 h1:iD+PFTQwKEmbwSdwfvP5ld2WEI/g7qbdhmHJ2ASfYGs= +github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -70,19 +101,30 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -95,6 +137,7 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= @@ -102,5 +145,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/parse.go b/parse.go index 60a42e11..3f1b35a9 100644 --- a/parse.go +++ b/parse.go @@ -35,7 +35,6 @@ func MustAutoParse(value string) Date { // * dd/mm/yyyy | dd.mm.yyyy (or any similar pattern) // // * surrounding whitespace is ignored -// func AutoParse(value string) (Date, error) { abs := strings.TrimSpace(value) if len(abs) == 0 { @@ -182,7 +181,9 @@ func MustParse(layout, value string) Date { // Parse parses a formatted string of a known layout and returns the Date value it represents. // The layout defines the format by showing how the reference date, defined // to be -// Monday, Jan 2, 2006 +// +// Monday, Jan 2, 2006 +// // would be interpreted if it were the value; it serves as an example of the // input format. The same interpretation will then be made to the input string. // diff --git a/period/doc.go b/period/doc.go index e328b0ba..5abe8447 100644 --- a/period/doc.go +++ b/period/doc.go @@ -43,5 +43,4 @@ // * "P2.5Y" is 2.5 years. // // * "PT12M7.5S" is 12 minutes and 7.5 seconds. -// package period diff --git a/period/period.go b/period/period.go index d097e9c8..7eb2902a 100644 --- a/period/period.go +++ b/period/period.go @@ -42,7 +42,6 @@ const hundredMs = 100 * time.Millisecond // // Note that although fractional weeks can be parsed, they will never be returned via String(). // This is because the number of weeks is always inferred from the number of days. -// type Period struct { years, months, days, hours, minutes, seconds int16 } @@ -519,7 +518,6 @@ func (period Period) Normalise(precise bool) Period { // * Two thresholds a, b are equivalent to a, a, b, b. // * Three thresholds a, b, c are equivalent to a, b, c, c. // * Four thresholds a, b, c, d are used as provided. -// func (period Period) Simplify(precise bool, th ...int) Period { switch len(th) { case 0: diff --git a/timespan/doc.go b/timespan/doc.go index b5b571dc..93737cff 100644 --- a/timespan/doc.go +++ b/timespan/doc.go @@ -5,5 +5,4 @@ // Package timespan provides spans of time (TimeSpan), and ranges of dates (DateRange). // Both are half-open intervals for which the start is included and the end is excluded. // This allows for empty spans and also facilitates aggregating spans together. -// package timespan diff --git a/timespan/timespan.go b/timespan/timespan.go index dd846025..80fcce4b 100644 --- a/timespan/timespan.go +++ b/timespan/timespan.go @@ -222,8 +222,8 @@ func (ts TimeSpan) MarshalText() (text []byte, err error) { // ParseRFC5545InLocation parses a string as a timespan. The string must contain either of // -// time "/" time -// time "/" period +// time "/" time +// time "/" period // // 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 From fba60ed48e0813b0a42e3c697d9c7b5f13d7527b Mon Sep 17 00:00:00 2001 From: rickb777 Date: Wed, 5 Jul 2023 14:29:34 +0100 Subject: [PATCH 160/165] bumped up some dependencies --- go.mod | 11 +++----- go.sum | 82 +++++++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 54607ce6..c7c36da1 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,14 @@ module github.com/rickb777/date require ( - github.com/onsi/gomega v1.24.0 + github.com/onsi/gomega v1.27.8 github.com/rickb777/plural v1.4.1 - golang.org/x/text v0.4.0 + golang.org/x/text v0.11.0 ) require ( github.com/google/go-cmp v0.5.9 // indirect - github.com/mattn/goveralls v0.0.11 // indirect - github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 // indirect - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect - golang.org/x/net v0.1.0 // indirect - golang.org/x/sys v0.1.0 // indirect - golang.org/x/tools v0.1.12 // indirect + golang.org/x/net v0.11.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 22a16339..0a18e966 100644 --- a/go.sum +++ b/go.sum @@ -5,9 +5,12 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -17,6 +20,7 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -24,11 +28,10 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/mattn/goveralls v0.0.11 h1:eJXea6R6IFlL1QMKNMzDvvHv/hwGrnvyig4N+0+XiMM= -github.com/mattn/goveralls v0.0.11/go.mod h1:gU8SyhNswsJKchEV93xRQxX6X3Ei4PJdQk/6ZHvrvRk= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -39,8 +42,16 @@ github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3 github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= -github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs= github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= +github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= +github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= +github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc= +github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk= +github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo= +github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss= +github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= @@ -48,15 +59,22 @@ github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9 github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= -github.com/onsi/gomega v1.24.0 h1:+0glovB9Jd6z3VR+ScSwQqXVTIfJcGA9UBM8yzQxhqg= github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= +github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= +github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= +github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw= +github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw= +github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= +github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= +github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rickb777/plural v1.4.1 h1:5MMLcbIaapLFmvDGRT5iPk8877hpTPt8Y9cdSKRw9sU= github.com/rickb777/plural v1.4.1/go.mod h1:kdmXUpmKBJTS0FtG/TFumd//VBWsNTD7zOw7x4umxNw= -github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 h1:iD+PFTQwKEmbwSdwfvP5ld2WEI/g7qbdhmHJ2ASfYGs= -github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -65,11 +83,15 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -81,13 +103,23 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= +golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -107,24 +139,49 @@ golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -146,5 +203,6 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From a792460dc086486fbe5873d00a5b8d9014811a09 Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 19 Sep 2023 09:35:23 +0100 Subject: [PATCH 161/165] Bugfix: this resolves issue #19 fraction designator parsing bug --- period/parse.go | 32 +++++++++++++++++--------------- period/period_test.go | 2 ++ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/period/parse.go b/period/parse.go index 304bc501..d88f1e7c 100644 --- a/period/parse.go +++ b/period/parse.go @@ -76,10 +76,10 @@ func parse(period string, normalise bool) (*period64, error) { } remaining = remaining[1:] - var number, weekValue, prevFraction int64 + var integer, fraction, weekValue int64 result := &period64{input: period, neg: neg} var years, months, weeks, days, hours, minutes, seconds itemState - var designator, prevDesignator byte + var designator, previousFractionDesignator byte var err error nComponents := 0 @@ -99,16 +99,17 @@ func parse(period string, normalise bool) (*period64, error) { remaining = remaining[1:] } else { - number, designator, remaining, err = parseNextField(remaining, period) + integer, fraction, designator, remaining, err = parseNextField(remaining, period) if err != nil { return nil, err } - fraction := number % 10 - if prevFraction != 0 && fraction != 0 { - return nil, fmt.Errorf("%s: '%c' & '%c' only the last field can have a fraction", period, prevDesignator, designator) + if previousFractionDesignator != 0 && fraction != 0 { + return nil, fmt.Errorf("%s: '%c' & '%c' only the last field can have a fraction", period, previousFractionDesignator, designator) } + number := integer*10 + fraction + switch designator { case 'Y': years, err = years.testAndSet(number, 'Y', result, &result.years) @@ -135,8 +136,9 @@ func parse(period string, normalise bool) (*period64, error) { return nil, err } - prevFraction = fraction - prevDesignator = designator + if fraction != 0 { + previousFractionDesignator = designator + } } } @@ -177,19 +179,19 @@ func (i itemState) testAndSet(number int64, designator byte, result *period64, v //------------------------------------------------------------------------------------------------- -func parseNextField(str, original string) (int64, byte, string, error) { +func parseNextField(str, original string) (int64, int64, byte, string, error) { i := scanDigits(str) if i < 0 { - return 0, 0, "", fmt.Errorf("%s: missing designator at the end", original) + return 0, 0, 0, "", fmt.Errorf("%s: missing designator at the end", original) } des := str[i] - number, err := parseDecimalNumber(str[:i], original, des) - return number, des, str[i+1:], err + integer, fraction, err := parseDecimalNumber(str[:i], original, des) + return integer, fraction, des, str[i+1:], err } // Fixed-point one decimal place -func parseDecimalNumber(number, original string, des byte) (int64, error) { +func parseDecimalNumber(number, original string, des byte) (int64, int64, error) { dec := strings.IndexByte(number, '.') if dec < 0 { dec = strings.IndexByte(number, ',') @@ -214,10 +216,10 @@ func parseDecimalNumber(number, original string, des byte) (int64, error) { } if err != nil { - return 0, fmt.Errorf("%s: expected a number but found '%c'", original, des) + return 0, 0, fmt.Errorf("%s: expected a number but found '%c'", original, des) } - return integer*10 + fraction, err + return integer, fraction, err } // scanDigits finds the first non-digit byte after a given starting point. diff --git a/period/period_test.go b/period/period_test.go index 043fe782..d89a0d00 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -41,7 +41,9 @@ func TestParseErrors(t *testing.T) { {"P1D2D", false, ": 'D' designator cannot occur more than once", "P1D2D"}, {"PT1HT1S", false, ": 'T' designator cannot occur more than once", "PT1HT1S"}, {"P0.1YT0.1S", false, ": 'Y' & 'S' only the last field can have a fraction", "P0.1YT0.1S"}, + {"P0.1Y1M1DT1H1M0.1S", false, ": 'Y' & 'S' only the last field can have a fraction", "P0.1Y1M1DT1H1M0.1S"}, {"P", false, ": expected 'Y', 'M', 'W', 'D', 'H', 'M', or 'S' designator", "P"}, + // integer overflow {"P32768Y", false, ": integer overflow occurred in years", "P32768Y"}, {"P32768M", false, ": integer overflow occurred in months", "P32768M"}, From ad3aa706c5a0837b72b1423859b34cf208103fdf Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 19 Sep 2023 09:35:35 +0100 Subject: [PATCH 162/165] Dependencies updated --- go.mod | 7 ++++--- go.sum | 31 +++++++++++++++++++++---------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index c7c36da1..2bc8244e 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,15 @@ module github.com/rickb777/date require ( - github.com/onsi/gomega v1.27.8 + github.com/onsi/gomega v1.27.10 github.com/rickb777/plural v1.4.1 - golang.org/x/text v0.11.0 + golang.org/x/text v0.13.0 ) require ( github.com/google/go-cmp v0.5.9 // indirect - golang.org/x/net v0.11.0 // indirect + golang.org/x/net v0.15.0 // indirect + golang.org/x/tools v0.13.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0a18e966..b53fbaab 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,9 @@ github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxm github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo= github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= -github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss= github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= +github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= +github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= @@ -67,8 +68,9 @@ github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557c github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= -github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rickb777/plural v1.4.1 h1:5MMLcbIaapLFmvDGRT5iPk8877hpTPt8Y9cdSKRw9sU= github.com/rickb777/plural v1.4.1/go.mod h1:kdmXUpmKBJTS0FtG/TFumd//VBWsNTD7zOw7x4umxNw= @@ -83,7 +85,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -92,6 +95,7 @@ golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -111,8 +115,9 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -120,6 +125,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -146,8 +152,10 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -157,7 +165,8 @@ golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -168,9 +177,9 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -180,8 +189,10 @@ golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From b6690e43d2ede166a42d7f8c8295c78fc8566d37 Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 19 Sep 2023 13:46:30 +0100 Subject: [PATCH 163/165] period.AddTo revised to reduce the impact of subtle behaviours of time.AddDate --- period/arithmetic.go | 7 +++- period/arithmetic_test.go | 87 ++++++++++++++++++++++----------------- 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/period/arithmetic.go b/period/arithmetic.go index 55993fe0..464a4130 100644 --- a/period/arithmetic.go +++ b/period/arithmetic.go @@ -39,8 +39,13 @@ func (period Period) AddTo(t time.Time) (time.Time, bool) { wholeDays := (period.days % 10) == 0 if wholeYears && wholeMonths && wholeDays { - // in this case, time.AddDate provides an exact solution + // in this case, time.AddDate(...).Add(...) provides an exact solution stE3 := totalSecondsE3(period) + if period.years == 0 && period.months == 0 && period.days == 0 { + // AddDate (below) normalises its result, so we don't call it unless needed + return t.Add(stE3 * time.Millisecond), true + } + t1 := t.AddDate(int(period.years/10), int(period.months/10), int(period.days/10)) return t1.Add(stE3 * time.Millisecond), true } diff --git a/period/arithmetic_test.go b/period/arithmetic_test.go index fde1d27f..cc9d5620 100644 --- a/period/arithmetic_test.go +++ b/period/arithmetic_test.go @@ -89,45 +89,56 @@ func TestPeriodAddToTime(t *testing.T) { const min = 60 * sec const hr = 60 * min - // A conveniently round number (14 July 2017 @ 2:40am UTC) - var t0 = time.Unix(1500000000, 0).UTC() - - cases := []struct { - value string - result time.Time - precise bool - }{ - // precise cases - {"P0D", t0, true}, - {"PT1S", t0.Add(sec), true}, - {"PT0.1S", t0.Add(100 * ms), true}, - {"-PT0.1S", t0.Add(-100 * ms), true}, - {"PT3276S", t0.Add(3276 * sec), true}, - {"PT1M", t0.Add(60 * sec), true}, - {"PT0.1M", t0.Add(6 * sec), true}, - {"PT3276M", t0.Add(3276 * min), true}, - {"PT1H", t0.Add(hr), true}, - {"PT0.1H", t0.Add(6 * min), true}, - {"PT3276H", t0.Add(3276 * hr), true}, - {"P1D", t0.AddDate(0, 0, 1), true}, - {"P3276D", t0.AddDate(0, 0, 3276), true}, - {"P1M", t0.AddDate(0, 1, 0), true}, - {"P3276M", t0.AddDate(0, 3276, 0), true}, - {"P1Y", t0.AddDate(1, 0, 0), true}, - {"-P1Y", t0.AddDate(-1, 0, 0), true}, - {"P3276Y", t0.AddDate(3276, 0, 0), true}, // near the upper limit of range - {"-P3276Y", t0.AddDate(-3276, 0, 0), true}, // near the lower limit of range - // approximate cases - {"P0.1D", t0.Add(144 * min), false}, - {"-P0.1D", t0.Add(-144 * min), false}, - {"P0.1M", t0.Add(oneMonthApprox / 10), false}, - {"P0.1Y", t0.Add(oneYearApprox / 10), false}, + est, err := time.LoadLocation("America/New_York") + g.Expect(err).NotTo(HaveOccurred()) + + times := []time.Time{ + // A conveniently round number but with non-zero nanoseconds (14 July 2017 @ 2:40am UTC) + time.Unix(1500000000, 1).UTC(), + // This specific time fails for EST due behaviour of Time.AddDate + time.Date(2020, 11, 1, 1, 0, 0, 0, est), } - for i, c := range cases { - p := MustParse(c.value) - t1, prec := p.AddTo(t0) - g.Expect(t1).To(Equal(c.result), info(i, c.value)) - g.Expect(prec).To(Equal(c.precise), info(i, c.value)) + + for _, t0 := range times { + cases := []struct { + value string + result time.Time + precise bool + }{ + // precise cases + {"P0D", t0, true}, + {"PT1S", t0.Add(sec), true}, + {"PT0.1S", t0.Add(100 * ms), true}, + {"-PT0.1S", t0.Add(-100 * ms), true}, + {"PT3276S", t0.Add(3276 * sec), true}, + {"PT1M", t0.Add(60 * sec), true}, + {"PT0.1M", t0.Add(6 * sec), true}, + {"PT3276M", t0.Add(3276 * min), true}, + {"PT1H", t0.Add(hr), true}, + {"PT0.1H", t0.Add(6 * min), true}, + {"PT3276H", t0.Add(3276 * hr), true}, + {"P1D", t0.AddDate(0, 0, 1), true}, + {"P3276D", t0.AddDate(0, 0, 3276), true}, + {"P1M", t0.AddDate(0, 1, 0), true}, + {"P3276M", t0.AddDate(0, 3276, 0), true}, + {"P1Y", t0.AddDate(1, 0, 0), true}, + {"-P1Y", t0.AddDate(-1, 0, 0), true}, + {"P3276Y", t0.AddDate(3276, 0, 0), true}, // near the upper limit of range + {"-P3276Y", t0.AddDate(-3276, 0, 0), true}, // near the lower limit of range + // approximate cases + {"P0.1D", t0.Add(144 * min), false}, + {"-P0.1D", t0.Add(-144 * min), false}, + {"P0.1M", t0.Add(oneMonthApprox / 10), false}, + {"P0.1Y", t0.Add(oneYearApprox / 10), false}, + } + for i, c := range cases { + p, err := ParseWithNormalise(c.value, false) + g.Expect(err).NotTo(HaveOccurred()) + + t1, prec := p.AddTo(t0) + g.Expect(t1).To(Equal(c.result), info(i, c.value, t0)) + g.Expect(prec).To(Equal(c.precise), info(i, c.value, t0)) + } } } From ff580cf3bbf4fdf761569a31cd129f50ab602cf3 Mon Sep 17 00:00:00 2001 From: Rick Date: Tue, 19 Sep 2023 22:19:00 +0100 Subject: [PATCH 164/165] more tests added to period.Between --- period/period.go | 10 +++++----- period/period_test.go | 13 ++++++++++++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/period/period.go b/period/period.go index 7eb2902a..082466f2 100644 --- a/period/period.go +++ b/period/period.go @@ -152,19 +152,19 @@ func Between(t1, t2 time.Time) (p Period) { t2 = t2.In(t1.Location()) } - year, month, day, hour, min, sec, hundredth := daysDiff(t1, t2) + year, month, day, hour, min, sec, tenth := daysDiff(t1, t2) if sign < 0 { p = New(-year, -month, -day, -hour, -min, -sec) - p.seconds -= int16(hundredth) + p.seconds -= int16(tenth) } else { p = New(year, month, day, hour, min, sec) - p.seconds += int16(hundredth) + p.seconds += int16(tenth) } return } -func daysDiff(t1, t2 time.Time) (year, month, day, hour, min, sec, hundredth int) { +func daysDiff(t1, t2 time.Time) (year, month, day, hour, min, sec, tenth int) { duration := t2.Sub(t1) hh1, mm1, ss1 := t1.Clock() @@ -175,7 +175,7 @@ func daysDiff(t1, t2 time.Time) (year, month, day, hour, min, sec, hundredth int hour = hh2 - hh1 min = mm2 - mm1 sec = ss2 - ss1 - hundredth = (t2.Nanosecond() - t1.Nanosecond()) / 100000000 + tenth = (t2.Nanosecond() - t1.Nanosecond()) / 100000000 // Normalize negative values if sec < 0 { diff --git a/period/period_test.go b/period/period_test.go index d89a0d00..6384e39b 100644 --- a/period/period_test.go +++ b/period/period_test.go @@ -730,8 +730,13 @@ func TestBetween(t *testing.T) { // larger ranges {utc(2009, 1, 1, 0, 0, 1, 0), utc(2016, 12, 31, 0, 0, 2, 0), Period{days: 29210, seconds: 10}}, - {utc(2008, 1, 1, 0, 0, 1, 0), utc(2016, 12, 31, 0, 0, 2, 0), Period{years: 80, months: 110, days: 300, seconds: 10}}, + {utc(2009, 1, 1, 0, 0, 1, 0), utc(2017, 12, 21, 0, 0, 2, 0), Period{days: 32760, seconds: 10}}, + {utc(2009, 1, 1, 0, 0, 1, 0), utc(2017, 12, 22, 0, 0, 2, 0), Period{years: 80, months: 110, days: 210, seconds: 10}}, + {utc(2009, 1, 1, 10, 10, 10, 00), utc(2017, 12, 23, 5, 5, 5, 5), Period{years: 80, months: 110, days: 220, hours: 180, minutes: 540, seconds: 550}}, {utc(1900, 1, 1, 0, 0, 1, 0), utc(2009, 12, 31, 0, 0, 2, 0), Period{years: 1090, months: 110, days: 300, seconds: 10}}, + + {japan(2021, 3, 1, 0, 0, 0, 0), japan(2021, 9, 7, 0, 0, 0, 0), Period{days: 1900}}, + {japan(2021, 3, 1, 0, 0, 0, 0), utc(2021, 9, 7, 0, 0, 0, 0), Period{days: 1900, hours: 90}}, } for i, c := range cases { pp := Between(c.a, c.b) @@ -1085,10 +1090,16 @@ func bst(year int, month time.Month, day, hour, min, sec, msec int) time.Time { return time.Date(year, month, day, hour, min, sec, msec*int(time.Millisecond), london) } +func japan(year int, month time.Month, day, hour, min, sec, msec int) time.Time { + return time.Date(year, month, day, hour, min, sec, msec*int(time.Millisecond), tokyo) +} + var london *time.Location // UTC + 1 hour during summer +var tokyo *time.Location // UTC + 1 hour during summer func init() { london, _ = time.LoadLocation("Europe/London") + tokyo, _ = time.LoadLocation("Asia/Tokyo") } func info(i int, m ...interface{}) string { From 02b87e13bad27421f7f826456dc4673bc830f418 Mon Sep 17 00:00:00 2001 From: Rick Date: Fri, 22 Sep 2023 10:06:15 +0100 Subject: [PATCH 165/165] another parse test case --- parse_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/parse_test.go b/parse_test.go index cb22d146..40ad7edb 100644 --- a/parse_test.go +++ b/parse_test.go @@ -195,6 +195,7 @@ func TestParse(t *testing.T) { {RFC1123, "05 Dec 1928", 1928, time.December, 5}, {RFC1123W, "Mon, 05 Dec 1928", 1928, time.December, 5}, {RFC3339, "2345-06-07", 2345, time.June, 7}, + {time.RFC3339Nano, "2020-04-01T12:11:10.101+09:00", 2020, time.April, 1}, {"20060102", "20190619", 2019, time.June, 19}, } for _, c := range cases {