From f6b38c33b7fe522e9c5539f66bd8966ce0006ac6 Mon Sep 17 00:00:00 2001 From: Thomas Pelletier Date: Tue, 8 Jun 2021 20:27:05 -0400 Subject: [PATCH] Provide own implementation of Local* (#558) * Reduces the public API. * Reuses optimized parsing functions. * Removes reliance on Google code under Apache license. --- decode.go | 20 +- .../imported_tests/unmarshal_imported_test.go | 36 +- localtime.go | 339 +++-------- localtime_test.go | 528 +++--------------- unmarshaler.go | 5 +- unmarshaler_test.go | 12 +- 6 files changed, 186 insertions(+), 754 deletions(-) diff --git a/decode.go b/decode.go index 88e6a42..3f44d16 100644 --- a/decode.go +++ b/decode.go @@ -39,7 +39,7 @@ func parseLocalDate(b []byte) (LocalDate, error) { v := parseDecimalDigits(b[5:7]) - date.Month = time.Month(v) + date.Month = v date.Day = parseDecimalDigits(b[8:10]) @@ -100,13 +100,13 @@ func parseDateTime(b []byte) (time.Time, error) { } t := time.Date( - dt.Date.Year, - dt.Date.Month, - dt.Date.Day, - dt.Time.Hour, - dt.Time.Minute, - dt.Time.Second, - dt.Time.Nanosecond, + dt.Year, + time.Month(dt.Month), + dt.Day, + dt.Hour, + dt.Minute, + dt.Second, + dt.Nanosecond, zone) return t, nil @@ -124,7 +124,7 @@ func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) { if err != nil { return dt, nil, err } - dt.Date = date + dt.LocalDate = date sep := b[10] if sep != 'T' && sep != ' ' { @@ -135,7 +135,7 @@ func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) { if err != nil { return dt, nil, err } - dt.Time = t + dt.LocalTime = t return dt, rest, nil } diff --git a/internal/imported_tests/unmarshal_imported_test.go b/internal/imported_tests/unmarshal_imported_test.go index 2345445..bac07ac 100644 --- a/internal/imported_tests/unmarshal_imported_test.go +++ b/internal/imported_tests/unmarshal_imported_test.go @@ -1487,12 +1487,12 @@ func TestUnmarshalLocalDateTime(t *testing.T) { name: "normal", in: "1979-05-27T07:32:00", out: toml.LocalDateTime{ - Date: toml.LocalDate{ + LocalDate: toml.LocalDate{ Year: 1979, Month: 5, Day: 27, }, - Time: toml.LocalTime{ + LocalTime: toml.LocalTime{ Hour: 7, Minute: 32, Second: 0, @@ -1504,12 +1504,12 @@ func TestUnmarshalLocalDateTime(t *testing.T) { name: "with nanoseconds", in: "1979-05-27T00:32:00.999999", out: toml.LocalDateTime{ - Date: toml.LocalDate{ + LocalDate: toml.LocalDate{ Year: 1979, Month: 5, Day: 27, }, - Time: toml.LocalTime{ + LocalTime: toml.LocalTime{ Hour: 0, Minute: 32, Second: 0, @@ -1551,26 +1551,26 @@ func TestUnmarshalLocalDateTime(t *testing.T) { t.Fatal(err) } - if obj.Date.Year() != example.out.Date.Year { - t.Errorf("expected year %d, got %d", example.out.Date.Year, obj.Date.Year()) + if obj.Date.Year() != example.out.Year { + t.Errorf("expected year %d, got %d", example.out.Year, obj.Date.Year()) } - if obj.Date.Month() != example.out.Date.Month { - t.Errorf("expected month %d, got %d", example.out.Date.Month, obj.Date.Month()) + if obj.Date.Month() != time.Month(example.out.Month) { + t.Errorf("expected month %d, got %d", example.out.Month, obj.Date.Month()) } - if obj.Date.Day() != example.out.Date.Day { - t.Errorf("expected day %d, got %d", example.out.Date.Day, obj.Date.Day()) + if obj.Date.Day() != example.out.Day { + t.Errorf("expected day %d, got %d", example.out.Day, obj.Date.Day()) } - if obj.Date.Hour() != example.out.Time.Hour { - t.Errorf("expected hour %d, got %d", example.out.Time.Hour, obj.Date.Hour()) + if obj.Date.Hour() != example.out.Hour { + t.Errorf("expected hour %d, got %d", example.out.Hour, obj.Date.Hour()) } - if obj.Date.Minute() != example.out.Time.Minute { - t.Errorf("expected minute %d, got %d", example.out.Time.Minute, obj.Date.Minute()) + if obj.Date.Minute() != example.out.Minute { + t.Errorf("expected minute %d, got %d", example.out.Minute, obj.Date.Minute()) } - if obj.Date.Second() != example.out.Time.Second { - t.Errorf("expected second %d, got %d", example.out.Time.Second, obj.Date.Second()) + if obj.Date.Second() != example.out.Second { + t.Errorf("expected second %d, got %d", example.out.Second, obj.Date.Second()) } - if obj.Date.Nanosecond() != example.out.Time.Nanosecond { - t.Errorf("expected nanoseconds %d, got %d", example.out.Time.Nanosecond, obj.Date.Nanosecond()) + if obj.Date.Nanosecond() != example.out.Nanosecond { + t.Errorf("expected nanoseconds %d, got %d", example.out.Nanosecond, obj.Date.Nanosecond()) } }) } diff --git a/localtime.go b/localtime.go index a947044..4e32ecc 100644 --- a/localtime.go +++ b/localtime.go @@ -1,29 +1,3 @@ -// Implementation of TOML's local date/time. -// Copied over from https://github.com/googleapis/google-cloud-go/blob/master/civil/civil.go -// to avoid pulling all the Google dependencies. -// -// Copyright 2016 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package civil implements types for civil time, a time-zone-independent -// representation of time that follows the rules of the proleptic -// Gregorian calendar with exactly 24-hour days, 60-minute hours, and 60-second -// minutes. -// -// Because they lack location information, these types do not represent unique -// moments or intervals of time. Use time.Time for that purpose. - package toml import ( @@ -31,270 +5,105 @@ import ( "time" ) -// A LocalDate represents a date (year, month, day). -// -// This type does not include location information, and therefore does not -// describe a unique 24-hour timespan. +// LocalDate represents a calendar day in no specific timezone. type LocalDate struct { - Year int // Year (e.g., 2014). - Month time.Month // Month of the year (January = 1, ...). - Day int // Day of the month, starting at 1. + Year int + Month int + Day int } -// LocalDateOf returns the LocalDate in which a time occurs in that time's location. -func LocalDateOf(t time.Time) LocalDate { - var d LocalDate - d.Year, d.Month, d.Day = t.Date() - - return d +// AsTime converts d into a specific time instance at midnight in zone. +func (d LocalDate) AsTime(zone *time.Location) time.Time { + return time.Date(d.Year, time.Month(d.Month), d.Day, 0, 0, 0, 0, zone) } -// ParseLocalDate parses a string in RFC3339 full-date format and returns the date value it represents. -func ParseLocalDate(s string) (LocalDate, error) { - t, err := time.Parse("2006-01-02", s) - if err != nil { - return LocalDate{}, err - } - - return LocalDateOf(t), nil -} - -// String returns the date in RFC3339 full-date format. +// String returns RFC 3339 representation of d. func (d LocalDate) String() string { return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day) } -// IsValid reports whether the date is valid. -func (d LocalDate) IsValid() bool { - return LocalDateOf(d.In(time.UTC)) == d -} - -// In returns the time corresponding to time 00:00:00 of the date in the location. -// -// In is always consistent with time.LocalDate, even when time.LocalDate returns a time -// on a different day. For example, if loc is America/Indiana/Vincennes, then both -// time.LocalDate(1955, time.May, 1, 0, 0, 0, 0, loc) -// and -// civil.LocalDate{Year: 1955, Month: time.May, Day: 1}.In(loc) -// return 23:00:00 on April 30, 1955. -// -// In panics if loc is nil. -func (d LocalDate) In(loc *time.Location) time.Time { - return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, loc) -} - -// AddDays returns the date that is n days in the future. -// n can also be negative to go into the past. -func (d LocalDate) AddDays(n int) LocalDate { - return LocalDateOf(d.In(time.UTC).AddDate(0, 0, n)) -} - -// DaysSince returns the signed number of days between the date and s, not including the end day. -// This is the inverse operation to AddDays. -func (d LocalDate) DaysSince(s LocalDate) (days int) { - // We convert to Unix time so we do not have to worry about leap seconds: - // Unix time increases by exactly 86400 seconds per day. - deltaUnix := d.In(time.UTC).Unix() - s.In(time.UTC).Unix() - - const secondsInADay = 86400 - - return int(deltaUnix / secondsInADay) -} - -// Before reports whether d1 occurs before future date. -func (d LocalDate) Before(future LocalDate) bool { - if d.Year != future.Year { - return d.Year < future.Year - } - - if d.Month != future.Month { - return d.Month < future.Month - } - - return d.Day < future.Day -} - -// After reports whether d1 occurs after past date. -func (d LocalDate) After(past LocalDate) bool { - return past.Before(d) -} - -// MarshalText implements the encoding.TextMarshaler interface. -// The output is the result of d.String(). +// MarshalText returns RFC 3339 representation of d. func (d LocalDate) MarshalText() ([]byte, error) { return []byte(d.String()), nil } -// UnmarshalText implements the encoding.TextUnmarshaler interface. -// The date is expected to be a string in a format accepted by ParseLocalDate. -func (d *LocalDate) UnmarshalText(data []byte) error { - var err error - *d, err = ParseLocalDate(string(data)) - - return err -} - -// A LocalTime represents a time with nanosecond precision. -// -// This type does not include location information, and therefore does not -// describe a unique moment in time. -// -// This type exists to represent the TIME type in storage-based APIs like BigQuery. -// Most operations on Times are unlikely to be meaningful. Prefer the LocalDateTime type. -type LocalTime struct { - Hour int // The hour of the day in 24-hour format; range [0-23] - Minute int // The minute of the hour; range [0-59] - Second int // The second of the minute; range [0-59] - Nanosecond int // The nanosecond of the second; range [0-999999999] -} - -// LocalTimeOf returns the LocalTime representing the time of day in which a time occurs -// in that time's location. It ignores the date. -func LocalTimeOf(t time.Time) LocalTime { - var tm LocalTime - tm.Hour, tm.Minute, tm.Second = t.Clock() - tm.Nanosecond = t.Nanosecond() - - return tm -} - -// ParseLocalTime parses a string and returns the time value it represents. -// ParseLocalTime accepts an extended form of the RFC3339 partial-time format. After -// the HH:MM:SS part of the string, an optional fractional part may appear, -// consisting of a decimal point followed by one to nine decimal digits. -// (RFC3339 admits only one digit after the decimal point). -func ParseLocalTime(s string) (LocalTime, error) { - t, err := time.Parse("15:04:05.999999999", s) +// UnmarshalText parses b using RFC 3339 to fill d. +func (d *LocalDate) UnmarshalText(b []byte) error { + res, err := parseLocalDate(b) if err != nil { - return LocalTime{}, err + return err } - - return LocalTimeOf(t), nil + *d = res + return nil } -// String returns the date in the format described in ParseLocalTime. If Nanoseconds -// is zero, no fractional part will be generated. Otherwise, the result will -// end with a fractional part consisting of a decimal point and nine digits. -func (t LocalTime) String() string { - s := fmt.Sprintf("%02d:%02d:%02d", t.Hour, t.Minute, t.Second) - if t.Nanosecond == 0 { +// LocalTime represents a time of day of no specific day in no specific +// timezone. +type LocalTime struct { + Hour int + Minute int + Second int + Nanosecond int +} + +// String returns RFC 3339 representation of d. +func (d LocalTime) String() string { + s := fmt.Sprintf("%02d:%02d:%02d", d.Hour, d.Minute, d.Second) + if d.Nanosecond == 0 { return s } - - return s + fmt.Sprintf(".%09d", t.Nanosecond) + return s + fmt.Sprintf(".%09d", d.Nanosecond) } -// IsValid reports whether the time is valid. -func (t LocalTime) IsValid() bool { - // Construct a non-zero time. - tm := time.Date(2, 2, 2, t.Hour, t.Minute, t.Second, t.Nanosecond, time.UTC) - - return LocalTimeOf(tm) == t +// MarshalText returns RFC 3339 representation of d. +func (d LocalTime) MarshalText() ([]byte, error) { + return []byte(d.String()), nil } -// MarshalText implements the encoding.TextMarshaler interface. -// The output is the result of t.String(). -func (t LocalTime) MarshalText() ([]byte, error) { - return []byte(t.String()), nil -} - -// UnmarshalText implements the encoding.TextUnmarshaler interface. -// The time is expected to be a string in a format accepted by ParseLocalTime. -func (t *LocalTime) UnmarshalText(data []byte) error { - var err error - *t, err = ParseLocalTime(string(data)) - - return err -} - -// A LocalDateTime represents a date and time. -// -// This type does not include location information, and therefore does not -// describe a unique moment in time. -type LocalDateTime struct { - Date LocalDate - Time LocalTime -} - -// Note: We deliberately do not embed LocalDate into LocalDateTime, to avoid promoting AddDays and Sub. - -// LocalDateTimeOf returns the LocalDateTime in which a time occurs in that time's location. -func LocalDateTimeOf(t time.Time) LocalDateTime { - return LocalDateTime{ - Date: LocalDateOf(t), - Time: LocalTimeOf(t), +// UnmarshalText parses b using RFC 3339 to fill d. +func (d *LocalTime) UnmarshalText(b []byte) error { + res, left, err := parseLocalTime(b) + if err == nil && len(left) != 0 { + err = newDecodeError(left, "extra characters") } -} - -// ParseLocalDateTime parses a string and returns the LocalDateTime it represents. -// ParseLocalDateTime accepts a variant of the RFC3339 date-time format that omits -// the time offset but includes an optional fractional time, as described in -// ParseLocalTime. Informally, the accepted format is -// YYYY-MM-DDTHH:MM:SS[.FFFFFFFFF] -// where the 'T' may be a lower-case 't'. -func ParseLocalDateTime(s string) (LocalDateTime, error) { - t, err := time.Parse("2006-01-02T15:04:05.999999999", s) if err != nil { - t, err = time.Parse("2006-01-02t15:04:05.999999999", s) - if err != nil { - return LocalDateTime{}, err - } + return err + } + *d = res + return nil +} + +// LocalDateTime represents a time of a specific day in no specific timezone. +type LocalDateTime struct { + LocalDate + LocalTime +} + +// AsTime converts d into a specific time instance in zone. +func (d LocalDateTime) AsTime(zone *time.Location) time.Time { + return time.Date(d.Year, time.Month(d.Month), d.Day, d.Hour, d.Minute, d.Second, d.Nanosecond, zone) +} + +// String returns RFC 3339 representation of d. +func (d LocalDateTime) String() string { + return d.LocalDate.String() + " " + d.LocalTime.String() +} + +// MarshalText returns RFC 3339 representation of d. +func (d LocalDateTime) MarshalText() ([]byte, error) { + return []byte(d.String()), nil +} + +// UnmarshalText parses b using RFC 3339 to fill d. +func (d *LocalDateTime) UnmarshalText(data []byte) error { + res, left, err := parseLocalDateTime(data) + if err == nil && len(left) != 0 { + err = newDecodeError(left, "extra characters") + } + if err != nil { + return err } - return LocalDateTimeOf(t), nil -} - -// String returns the date in the format described in ParseLocalDate. -func (dt LocalDateTime) String() string { - return dt.Date.String() + "T" + dt.Time.String() -} - -// IsValid reports whether the datetime is valid. -func (dt LocalDateTime) IsValid() bool { - return dt.Date.IsValid() && dt.Time.IsValid() -} - -// In returns the time corresponding to the LocalDateTime in the given location. -// -// If the time is missing or ambigous at the location, In returns the same -// result as time.LocalDate. For example, if loc is America/Indiana/Vincennes, then -// both -// time.LocalDate(1955, time.May, 1, 0, 30, 0, 0, loc) -// and -// civil.LocalDateTime{ -// civil.LocalDate{Year: 1955, Month: time.May, Day: 1}}, -// civil.LocalTime{Minute: 30}}.In(loc) -// return 23:30:00 on April 30, 1955. -// -// In panics if loc is nil. -func (dt LocalDateTime) In(loc *time.Location) time.Time { - return time.Date( - dt.Date.Year, dt.Date.Month, dt.Date.Day, - dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc, - ) -} - -// Before reports whether dt occurs before future. -func (dt LocalDateTime) Before(future LocalDateTime) bool { - return dt.In(time.UTC).Before(future.In(time.UTC)) -} - -// After reports whether dt occurs after past. -func (dt LocalDateTime) After(past LocalDateTime) bool { - return past.Before(dt) -} - -// MarshalText implements the encoding.TextMarshaler interface. -// The output is the result of dt.String(). -func (dt LocalDateTime) MarshalText() ([]byte, error) { - return []byte(dt.String()), nil -} - -// UnmarshalText implements the encoding.TextUnmarshaler interface. -// The datetime is expected to be a string in a format accepted by ParseLocalDateTime. -func (dt *LocalDateTime) UnmarshalText(data []byte) error { - var err error - *dt, err = ParseLocalDateTime(string(data)) - - return err + *d = res + return nil } diff --git a/localtime_test.go b/localtime_test.go index 646a3db..6ad9f0a 100644 --- a/localtime_test.go +++ b/localtime_test.go @@ -1,489 +1,107 @@ -// Copyright 2016 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package toml +package toml_test import ( - "encoding/json" - "reflect" "testing" "time" + + "github.com/pelletier/go-toml/v2" + "github.com/stretchr/testify/require" ) -func cmpEqual(x, y interface{}) bool { - return reflect.DeepEqual(x, y) +func TestLocalDate_AsTime(t *testing.T) { + d := toml.LocalDate{2021, 6, 8} + cast := d.AsTime(time.UTC) + require.Equal(t, time.Date(2021, time.June, 8, 0, 0, 0, 0, time.UTC), cast) } -func TestDates(t *testing.T) { - - for _, test := range []struct { - date LocalDate - loc *time.Location - wantStr string - wantTime time.Time - }{ - { - date: LocalDate{2014, 7, 29}, - loc: time.Local, - wantStr: "2014-07-29", - wantTime: time.Date(2014, time.July, 29, 0, 0, 0, 0, time.Local), - }, - { - date: LocalDateOf(time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local)), - loc: time.UTC, - wantStr: "2014-08-20", - wantTime: time.Date(2014, 8, 20, 0, 0, 0, 0, time.UTC), - }, - { - date: LocalDateOf(time.Date(999, time.January, 26, 0, 0, 0, 0, time.Local)), - loc: time.UTC, - wantStr: "0999-01-26", - wantTime: time.Date(999, 1, 26, 0, 0, 0, 0, time.UTC), - }, - } { - if got := test.date.String(); got != test.wantStr { - t.Errorf("%#v.String() = %q, want %q", test.date, got, test.wantStr) - } - - if got := test.date.In(test.loc); !got.Equal(test.wantTime) { - t.Errorf("%#v.In(%v) = %v, want %v", test.date, test.loc, got, test.wantTime) - } - } +func TestLocalDate_String(t *testing.T) { + d := toml.LocalDate{2021, 6, 8} + require.Equal(t, "2021-06-08", d.String()) } -func TestDateIsValid(t *testing.T) { - - for _, test := range []struct { - date LocalDate - want bool - }{ - {LocalDate{2014, 7, 29}, true}, - {LocalDate{2000, 2, 29}, true}, - {LocalDate{10000, 12, 31}, true}, - {LocalDate{1, 1, 1}, true}, - {LocalDate{0, 1, 1}, true}, // year zero is OK - {LocalDate{-1, 1, 1}, true}, // negative year is OK - {LocalDate{1, 0, 1}, false}, - {LocalDate{1, 1, 0}, false}, - {LocalDate{2016, 1, 32}, false}, - {LocalDate{2016, 13, 1}, false}, - {LocalDate{1, -1, 1}, false}, - {LocalDate{1, 1, -1}, false}, - } { - got := test.date.IsValid() - if got != test.want { - t.Errorf("%#v: got %t, want %t", test.date, got, test.want) - } - } +func TestLocalDate_MarshalText(t *testing.T) { + d := toml.LocalDate{2021, 6, 8} + b, err := d.MarshalText() + require.NoError(t, err) + require.Equal(t, []byte("2021-06-08"), b) } -func TestParseDate(t *testing.T) { +func TestLocalDate_UnmarshalMarshalText(t *testing.T) { + d := toml.LocalDate{} + err := d.UnmarshalText([]byte("2021-06-08")) + require.NoError(t, err) + require.Equal(t, toml.LocalDate{2021, 6, 8}, d) - var emptyDate LocalDate - - for _, test := range []struct { - str string - want LocalDate // if empty, expect an error - }{ - {"2016-01-02", LocalDate{2016, 1, 2}}, - {"2016-12-31", LocalDate{2016, 12, 31}}, - {"0003-02-04", LocalDate{3, 2, 4}}, - {"999-01-26", emptyDate}, - {"", emptyDate}, - {"2016-01-02x", emptyDate}, - } { - got, err := ParseLocalDate(test.str) - if got != test.want { - t.Errorf("ParseLocalDate(%q) = %+v, want %+v", test.str, got, test.want) - } - - if err != nil && test.want != (emptyDate) { - t.Errorf("Unexpected error %v from ParseLocalDate(%q)", err, test.str) - } - } + err = d.UnmarshalText([]byte("what")) + require.Error(t, err) } -func TestDateArithmetic(t *testing.T) { - - for _, test := range []struct { - desc string - start LocalDate - end LocalDate - days int - }{ - { - desc: "zero days noop", - start: LocalDate{2014, 5, 9}, - end: LocalDate{2014, 5, 9}, - days: 0, - }, - { - desc: "crossing a year boundary", - start: LocalDate{2014, 12, 31}, - end: LocalDate{2015, 1, 1}, - days: 1, - }, - { - desc: "negative number of days", - start: LocalDate{2015, 1, 1}, - end: LocalDate{2014, 12, 31}, - days: -1, - }, - { - desc: "full leap year", - start: LocalDate{2004, 1, 1}, - end: LocalDate{2005, 1, 1}, - days: 366, - }, - { - desc: "full non-leap year", - start: LocalDate{2001, 1, 1}, - end: LocalDate{2002, 1, 1}, - days: 365, - }, - { - desc: "crossing a leap second", - start: LocalDate{1972, 6, 30}, - end: LocalDate{1972, 7, 1}, - days: 1, - }, - { - desc: "dates before the unix epoch", - start: LocalDate{101, 1, 1}, - end: LocalDate{102, 1, 1}, - days: 365, - }, - } { - if got := test.start.AddDays(test.days); got != test.end { - t.Errorf("[%s] %#v.AddDays(%v) = %#v, want %#v", test.desc, test.start, test.days, got, test.end) - } - - if got := test.end.DaysSince(test.start); got != test.days { - t.Errorf("[%s] %#v.Sub(%#v) = %v, want %v", test.desc, test.end, test.start, got, test.days) - } - } +func TestLocalTime_String(t *testing.T) { + d := toml.LocalTime{20, 12, 1, 2} + require.Equal(t, "20:12:01.000000002", d.String()) + d = toml.LocalTime{20, 12, 1, 0} + require.Equal(t, "20:12:01", d.String()) } -func TestDateBefore(t *testing.T) { - - for _, test := range []struct { - d1, d2 LocalDate - want bool - }{ - {LocalDate{2016, 12, 31}, LocalDate{2017, 1, 1}, true}, - {LocalDate{2016, 1, 1}, LocalDate{2016, 1, 1}, false}, - {LocalDate{2016, 12, 30}, LocalDate{2016, 12, 31}, true}, - {LocalDate{2016, 1, 30}, LocalDate{2016, 12, 31}, true}, - } { - if got := test.d1.Before(test.d2); got != test.want { - t.Errorf("%v.Before(%v): got %t, want %t", test.d1, test.d2, got, test.want) - } - } +func TestLocalTime_MarshalText(t *testing.T) { + d := toml.LocalTime{20, 12, 1, 2} + b, err := d.MarshalText() + require.NoError(t, err) + require.Equal(t, []byte("20:12:01.000000002"), b) } -func TestDateAfter(t *testing.T) { +func TestLocalTime_UnmarshalMarshalText(t *testing.T) { + d := toml.LocalTime{} + err := d.UnmarshalText([]byte("20:12:01.000000002")) + require.NoError(t, err) + require.Equal(t, toml.LocalTime{20, 12, 1, 2}, d) - for _, test := range []struct { - d1, d2 LocalDate - want bool - }{ - {LocalDate{2016, 12, 31}, LocalDate{2017, 1, 1}, false}, - {LocalDate{2016, 1, 1}, LocalDate{2016, 1, 1}, false}, - {LocalDate{2016, 12, 30}, LocalDate{2016, 12, 31}, false}, - } { - if got := test.d1.After(test.d2); got != test.want { - t.Errorf("%v.After(%v): got %t, want %t", test.d1, test.d2, got, test.want) - } - } + err = d.UnmarshalText([]byte("what")) + require.Error(t, err) + + err = d.UnmarshalText([]byte("20:12:01.000000002 bad")) + require.Error(t, err) } -func TestTimeToString(t *testing.T) { - - for _, test := range []struct { - str string - time LocalTime - roundTrip bool // ParseLocalTime(str).String() == str? - }{ - {"13:26:33", LocalTime{13, 26, 33, 0}, true}, - {"01:02:03.000023456", LocalTime{1, 2, 3, 23456}, true}, - {"00:00:00.000000001", LocalTime{0, 0, 0, 1}, true}, - {"13:26:03.1", LocalTime{13, 26, 3, 100000000}, false}, - {"13:26:33.0000003", LocalTime{13, 26, 33, 300}, false}, - } { - gotTime, err := ParseLocalTime(test.str) - if err != nil { - t.Errorf("ParseLocalTime(%q): got error: %v", test.str, err) - - continue - } - - if gotTime != test.time { - t.Errorf("ParseLocalTime(%q) = %+v, want %+v", test.str, gotTime, test.time) - } - - if test.roundTrip { - gotStr := test.time.String() - if gotStr != test.str { - t.Errorf("%#v.String() = %q, want %q", test.time, gotStr, test.str) - } - } +func TestLocalDateTime_AsTime(t *testing.T) { + d := toml.LocalDateTime{ + toml.LocalDate{2021, 6, 8}, + toml.LocalTime{20, 12, 1, 2}, } + cast := d.AsTime(time.UTC) + require.Equal(t, time.Date(2021, time.June, 8, 20, 12, 1, 2, time.UTC), cast) } -func TestTimeOf(t *testing.T) { - - for _, test := range []struct { - time time.Time - want LocalTime - }{ - {time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local), LocalTime{15, 8, 43, 1}}, - {time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC), LocalTime{0, 0, 0, 0}}, - } { - if got := LocalTimeOf(test.time); got != test.want { - t.Errorf("LocalTimeOf(%v) = %+v, want %+v", test.time, got, test.want) - } +func TestLocalDateTime_String(t *testing.T) { + d := toml.LocalDateTime{ + toml.LocalDate{2021, 6, 8}, + toml.LocalTime{20, 12, 1, 2}, } + require.Equal(t, "2021-06-08 20:12:01.000000002", d.String()) } -func TestTimeIsValid(t *testing.T) { - - for _, test := range []struct { - time LocalTime - want bool - }{ - {LocalTime{0, 0, 0, 0}, true}, - {LocalTime{23, 0, 0, 0}, true}, - {LocalTime{23, 59, 59, 999999999}, true}, - {LocalTime{24, 59, 59, 999999999}, false}, - {LocalTime{23, 60, 59, 999999999}, false}, - {LocalTime{23, 59, 60, 999999999}, false}, - {LocalTime{23, 59, 59, 1000000000}, false}, - {LocalTime{-1, 0, 0, 0}, false}, - {LocalTime{0, -1, 0, 0}, false}, - {LocalTime{0, 0, -1, 0}, false}, - {LocalTime{0, 0, 0, -1}, false}, - } { - got := test.time.IsValid() - if got != test.want { - t.Errorf("%#v: got %t, want %t", test.time, got, test.want) - } +func TestLocalDateTime_MarshalText(t *testing.T) { + d := toml.LocalDateTime{ + toml.LocalDate{2021, 6, 8}, + toml.LocalTime{20, 12, 1, 2}, } + b, err := d.MarshalText() + require.NoError(t, err) + require.Equal(t, []byte("2021-06-08 20:12:01.000000002"), b) } -func TestDateTimeToString(t *testing.T) { +func TestLocalDateTime_UnmarshalMarshalText(t *testing.T) { + d := toml.LocalDateTime{} + err := d.UnmarshalText([]byte("2021-06-08 20:12:01.000000002")) + require.NoError(t, err) + require.Equal(t, toml.LocalDateTime{ + toml.LocalDate{2021, 6, 8}, + toml.LocalTime{20, 12, 1, 2}, + }, d) - for _, test := range []struct { - str string - dateTime LocalDateTime - roundTrip bool // ParseLocalDateTime(str).String() == str? - }{ - {"2016-03-22T13:26:33", LocalDateTime{LocalDate{2016, 3, 22}, LocalTime{13, 26, 33, 0}}, true}, - {"2016-03-22T13:26:33.000000600", LocalDateTime{LocalDate{2016, 3, 22}, LocalTime{13, 26, 33, 600}}, true}, - {"2016-03-22t13:26:33", LocalDateTime{LocalDate{2016, 3, 22}, LocalTime{13, 26, 33, 0}}, false}, - } { - gotDateTime, err := ParseLocalDateTime(test.str) - if err != nil { - t.Errorf("ParseLocalDateTime(%q): got error: %v", test.str, err) + err = d.UnmarshalText([]byte("what")) + require.Error(t, err) - continue - } - - if gotDateTime != test.dateTime { - t.Errorf("ParseLocalDateTime(%q) = %+v, want %+v", test.str, gotDateTime, test.dateTime) - } - - if test.roundTrip { - gotStr := test.dateTime.String() - if gotStr != test.str { - t.Errorf("%#v.String() = %q, want %q", test.dateTime, gotStr, test.str) - } - } - } -} - -func TestParseDateTimeErrors(t *testing.T) { - - for _, str := range []string{ - "", - "2016-03-22", // just a date - "13:26:33", // just a time - "2016-03-22 13:26:33", // wrong separating character - "2016-03-22T13:26:33x", // extra at end - } { - if _, err := ParseLocalDateTime(str); err == nil { - t.Errorf("ParseLocalDateTime(%q) succeeded, want error", str) - } - } -} - -func TestDateTimeOf(t *testing.T) { - - for _, test := range []struct { - time time.Time - want LocalDateTime - }{ - { - time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local), - LocalDateTime{LocalDate{2014, 8, 20}, LocalTime{15, 8, 43, 1}}, - }, - { - time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC), - LocalDateTime{LocalDate{1, 1, 1}, LocalTime{0, 0, 0, 0}}, - }, - } { - if got := LocalDateTimeOf(test.time); got != test.want { - t.Errorf("LocalDateTimeOf(%v) = %+v, want %+v", test.time, got, test.want) - } - } -} - -func TestDateTimeIsValid(t *testing.T) { - - // No need to be exhaustive here; it's just LocalDate.IsValid && LocalTime.IsValid. - for _, test := range []struct { - dt LocalDateTime - want bool - }{ - {LocalDateTime{LocalDate{2016, 3, 20}, LocalTime{0, 0, 0, 0}}, true}, - {LocalDateTime{LocalDate{2016, -3, 20}, LocalTime{0, 0, 0, 0}}, false}, - {LocalDateTime{LocalDate{2016, 3, 20}, LocalTime{24, 0, 0, 0}}, false}, - } { - got := test.dt.IsValid() - if got != test.want { - t.Errorf("%#v: got %t, want %t", test.dt, got, test.want) - } - } -} - -func TestDateTimeIn(t *testing.T) { - - dt := LocalDateTime{LocalDate{2016, 1, 2}, LocalTime{3, 4, 5, 6}} - - want := time.Date(2016, 1, 2, 3, 4, 5, 6, time.UTC) - if got := dt.In(time.UTC); !got.Equal(want) { - t.Errorf("got %v, want %v", got, want) - } -} - -func TestDateTimeBefore(t *testing.T) { - - d1 := LocalDate{2016, 12, 31} - d2 := LocalDate{2017, 1, 1} - t1 := LocalTime{5, 6, 7, 8} - t2 := LocalTime{5, 6, 7, 9} - - for _, test := range []struct { - dt1, dt2 LocalDateTime - want bool - }{ - {LocalDateTime{d1, t1}, LocalDateTime{d2, t1}, true}, - {LocalDateTime{d1, t1}, LocalDateTime{d1, t2}, true}, - {LocalDateTime{d2, t1}, LocalDateTime{d1, t1}, false}, - {LocalDateTime{d2, t1}, LocalDateTime{d2, t1}, false}, - } { - if got := test.dt1.Before(test.dt2); got != test.want { - t.Errorf("%v.Before(%v): got %t, want %t", test.dt1, test.dt2, got, test.want) - } - } -} - -func TestDateTimeAfter(t *testing.T) { - - d1 := LocalDate{2016, 12, 31} - d2 := LocalDate{2017, 1, 1} - t1 := LocalTime{5, 6, 7, 8} - t2 := LocalTime{5, 6, 7, 9} - - for _, test := range []struct { - dt1, dt2 LocalDateTime - want bool - }{ - {LocalDateTime{d1, t1}, LocalDateTime{d2, t1}, false}, - {LocalDateTime{d1, t1}, LocalDateTime{d1, t2}, false}, - {LocalDateTime{d2, t1}, LocalDateTime{d1, t1}, true}, - {LocalDateTime{d2, t1}, LocalDateTime{d2, t1}, false}, - } { - if got := test.dt1.After(test.dt2); got != test.want { - t.Errorf("%v.After(%v): got %t, want %t", test.dt1, test.dt2, got, test.want) - } - } -} - -func TestMarshalJSON(t *testing.T) { - - for _, test := range []struct { - value interface{} - want string - }{ - {LocalDate{1987, 4, 15}, `"1987-04-15"`}, - {LocalTime{18, 54, 2, 0}, `"18:54:02"`}, - {LocalDateTime{LocalDate{1987, 4, 15}, LocalTime{18, 54, 2, 0}}, `"1987-04-15T18:54:02"`}, - } { - bgot, err := json.Marshal(test.value) - if err != nil { - t.Fatal(err) - } - - if got := string(bgot); got != test.want { - t.Errorf("%#v: got %s, want %s", test.value, got, test.want) - } - } -} - -func TestUnmarshalJSON(t *testing.T) { - - var ( - d LocalDate - tm LocalTime - dt LocalDateTime - ) - - for _, test := range []struct { - data string - ptr interface{} - want interface{} - }{ - {`"1987-04-15"`, &d, &LocalDate{1987, 4, 15}}, - {`"1987-04-\u0031\u0035"`, &d, &LocalDate{1987, 4, 15}}, - {`"18:54:02"`, &tm, &LocalTime{18, 54, 2, 0}}, - {`"1987-04-15T18:54:02"`, &dt, &LocalDateTime{LocalDate{1987, 4, 15}, LocalTime{18, 54, 2, 0}}}, - } { - if err := json.Unmarshal([]byte(test.data), test.ptr); err != nil { - t.Fatalf("%s: %v", test.data, err) - } - - if !cmpEqual(test.ptr, test.want) { - t.Errorf("%s: got %#v, want %#v", test.data, test.ptr, test.want) - } - } - - for _, bad := range []string{ - "", `""`, `"bad"`, `"1987-04-15x"`, - `19870415`, // a JSON number - `11987-04-15x`, // not a JSON string - - } { - if json.Unmarshal([]byte(bad), &d) == nil { - t.Errorf("%q, LocalDate: got nil, want error", bad) - } - - if json.Unmarshal([]byte(bad), &tm) == nil { - t.Errorf("%q, LocalTime: got nil, want error", bad) - } - - if json.Unmarshal([]byte(bad), &dt) == nil { - t.Errorf("%q, LocalDateTime: got nil, want error", bad) - } - } + err = d.UnmarshalText([]byte("2021-06-08 20:12:01.000000002 bad")) + require.Error(t, err) } diff --git a/unmarshaler.go b/unmarshaler.go index fd2e42f..6346985 100644 --- a/unmarshaler.go +++ b/unmarshaler.go @@ -720,8 +720,7 @@ func (d *decoder) unmarshalLocalDate(value *ast.Node, v reflect.Value) error { } if v.Type() == timeType { - cast := ld.In(time.Local) - + cast := ld.AsTime(time.Local) v.Set(reflect.ValueOf(cast)) return nil } @@ -742,7 +741,7 @@ func (d *decoder) unmarshalLocalDateTime(value *ast.Node, v reflect.Value) error } if v.Type() == timeType { - cast := ldt.In(time.Local) + cast := ldt.AsTime(time.Local) v.Set(reflect.ValueOf(cast)) return nil diff --git a/unmarshaler_test.go b/unmarshaler_test.go index 1deb890..6b13460 100644 --- a/unmarshaler_test.go +++ b/unmarshaler_test.go @@ -315,7 +315,10 @@ func TestUnmarshal(t *testing.T) { return test{ target: &doc{}, expected: &doc{ - A: toml.LocalDateTimeOf(time.Date(1979, 5, 27, 0, 32, 0, 0, time.Local)), + A: toml.LocalDateTime{ + toml.LocalDate{1979, 5, 27}, + toml.LocalTime{0, 32, 0, 0}, + }, }, } }, @@ -331,7 +334,7 @@ func TestUnmarshal(t *testing.T) { return test{ target: &doc{}, expected: &doc{ - A: toml.LocalDateOf(time.Date(1979, 5, 27, 0, 32, 0, 0, time.Local)), + A: toml.LocalDate{1979, 5, 27}, }, } }, @@ -1977,7 +1980,10 @@ func TestLocalDateTime(t *testing.T) { actual := m["a"] golang, err := time.Parse("2006-01-02T15:04:05.999999999", e.input) require.NoError(t, err) - expected := toml.LocalDateTimeOf(golang) + expected := toml.LocalDateTime{ + toml.LocalDate{golang.Year(), int(golang.Month()), golang.Day()}, + toml.LocalTime{golang.Hour(), golang.Minute(), golang.Second(), golang.Nanosecond()}, + } require.Equal(t, expected, actual) }) }