From ee102a3528797051def014df9a7ead9dd1fd5cc9 Mon Sep 17 00:00:00 2001 From: Thomas Pelletier Date: Tue, 20 Apr 2021 23:16:08 -0400 Subject: [PATCH] decoder: fix time fractional parsing --- decode.go | 39 +- .../imported_tests/unmarshal_imported_test.go | 409 +++++++++--------- unmarshaler.go | 6 +- unmarshaler_test.go | 67 +++ 4 files changed, 307 insertions(+), 214 deletions(-) diff --git a/decode.go b/decode.go index 938587b..f4d2783 100644 --- a/decode.go +++ b/decode.go @@ -136,7 +136,7 @@ func parseDateTime(b []byte) (time.Time, error) { var ( errParseLocalDateTimeWrongLength = errors.New( - "local datetimes are expected to have the format YYYY-MM-DDTHH:MM:SS[.NNNNNN]", + "local datetimes are expected to have the format YYYY-MM-DDTHH:MM:SS[.NNNNNNNNN]", ) errParseLocalDateTimeWrongSeparator = errors.New("datetime separator is expected to be T or a space") ) @@ -144,8 +144,8 @@ var ( func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) { var dt LocalDateTime - const localDateTimeByteLen = 11 - if len(b) < localDateTimeByteLen { + const localDateTimeByteMinLen = 11 + if len(b) < localDateTimeByteMinLen { return dt, nil, errParseLocalDateTimeWrongLength } @@ -207,18 +207,41 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) { return t, nil, err } - if len(b) >= 15 && b[8] == '.' { - t.Nanosecond, err = parseDecimalDigits(b[9:15]) - if err != nil { - return t, nil, err + if len(b) >= 9 && b[8] == '.' { + frac := 0 + digits := 0 + + for i, c := range b[9:] { + if !isDigit(c) { + if i == 0 { + return t, nil, newDecodeError(b[i:i+1], "need at least one digit after fraction point") + } + + break + } + if i >= 9 { + return t, nil, newDecodeError(b[i:i+1], "maximum precision for date time is nanosecond") + } + + frac *= 10 + frac += int(c - '0') + digits++ } - return t, b[15:], nil + t.Nanosecond = frac * nanosecPower(digits) + + return t, b[9+digits:], nil } return t, b[8:], nil } +var nspow = []int{0, 1e8, 1e7, 1e6, 1e5, 1e4, 1e3, 1e2, 1e1, 1e0} + +func nanosecPower(n int) int { + return nspow[n] +} + var ( errParseFloatStartDot = errors.New("float cannot start with a dot") errParseFloatEndDot = errors.New("float cannot end with a dot") diff --git a/internal/imported_tests/unmarshal_imported_test.go b/internal/imported_tests/unmarshal_imported_test.go index c641a2a..8dbfec4 100644 --- a/internal/imported_tests/unmarshal_imported_test.go +++ b/internal/imported_tests/unmarshal_imported_test.go @@ -1430,211 +1430,210 @@ func TestUnmarshalPreservesUnexportedFields(t *testing.T) { }) } -// -//func TestUnmarshalLocalDate(t *testing.T) { -// t.Run("ToLocalDate", func(t *testing.T) { -// type dateStruct struct { -// Date toml.LocalDate -// } -// -// doc := `date = 1979-05-27` -// -// var obj dateStruct -// -// err := toml.Unmarshal([]byte(doc), &obj) -// -// if err != nil { -// t.Fatal(err) -// } -// -// if obj.Date.Year != 1979 { -// t.Errorf("expected year 1979, got %d", obj.Date.Year) -// } -// if obj.Date.Month != 5 { -// t.Errorf("expected month 5, got %d", obj.Date.Month) -// } -// if obj.Date.Day != 27 { -// t.Errorf("expected day 27, got %d", obj.Date.Day) -// } -// }) -// -// t.Run("ToLocalDate", func(t *testing.T) { -// type dateStruct struct { -// Date time.Time -// } -// -// doc := `date = 1979-05-27` -// -// var obj dateStruct -// -// err := toml.Unmarshal([]byte(doc), &obj) -// -// if err != nil { -// t.Fatal(err) -// } -// -// if obj.Date.Year() != 1979 { -// t.Errorf("expected year 1979, got %d", obj.Date.Year()) -// } -// if obj.Date.Month() != 5 { -// t.Errorf("expected month 5, got %d", obj.Date.Month()) -// } -// if obj.Date.Day() != 27 { -// t.Errorf("expected day 27, got %d", obj.Date.Day()) -// } -// }) -//} -// -//func TestUnmarshalLocalDateTime(t *testing.T) { -// examples := []struct { -// name string -// in string -// out toml.LocalDateTime -// }{ -// { -// name: "normal", -// in: "1979-05-27T07:32:00", -// out: toml.LocalDateTime{ -// Date: toml.LocalDate{ -// Year: 1979, -// Month: 5, -// Day: 27, -// }, -// Time: toml.LocalTime{ -// Hour: 7, -// Minute: 32, -// Second: 0, -// Nanosecond: 0, -// }, -// }}, -// { -// name: "with nanoseconds", -// in: "1979-05-27T00:32:00.999999", -// out: toml.LocalDateTime{ -// Date: toml.LocalDate{ -// Year: 1979, -// Month: 5, -// Day: 27, -// }, -// Time: toml.LocalTime{ -// Hour: 0, -// Minute: 32, -// Second: 0, -// Nanosecond: 999999000, -// }, -// }, -// }, -// } -// -// for i, example := range examples { -// doc := fmt.Sprintf(`date = %s`, example.in) -// -// t.Run(fmt.Sprintf("ToLocalDateTime_%d_%s", i, example.name), func(t *testing.T) { -// type dateStruct struct { -// Date toml.LocalDateTime -// } -// -// var obj dateStruct -// -// err := toml.Unmarshal([]byte(doc), &obj) -// -// if err != nil { -// t.Fatal(err) -// } -// -// if obj.Date != example.out { -// t.Errorf("expected '%s', got '%s'", example.out, obj.Date) -// } -// }) -// -// t.Run(fmt.Sprintf("ToTime_%d_%s", i, example.name), func(t *testing.T) { -// type dateStruct struct { -// Date time.Time -// } -// -// var obj dateStruct -// -// err := toml.Unmarshal([]byte(doc), &obj) -// -// if err != nil { -// 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.Month() != example.out.Date.Month { -// t.Errorf("expected month %d, got %d", example.out.Date.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.Hour() != example.out.Time.Hour { -// t.Errorf("expected hour %d, got %d", example.out.Time.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.Second() != example.out.Time.Second { -// t.Errorf("expected second %d, got %d", example.out.Time.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()) -// } -// }) -// } -//} -// -//func TestUnmarshalLocalTime(t *testing.T) { -// examples := []struct { -// name string -// in string -// out toml.LocalTime -// }{ -// { -// name: "normal", -// in: "07:32:00", -// out: toml.LocalTime{ -// Hour: 7, -// Minute: 32, -// Second: 0, -// Nanosecond: 0, -// }, -// }, -// { -// name: "with nanoseconds", -// in: "00:32:00.999999", -// out: toml.LocalTime{ -// Hour: 0, -// Minute: 32, -// Second: 0, -// Nanosecond: 999999000, -// }, -// }, -// } -// -// for i, example := range examples { -// doc := fmt.Sprintf(`Time = %s`, example.in) -// -// t.Run(fmt.Sprintf("ToLocalTime_%d_%s", i, example.name), func(t *testing.T) { -// type dateStruct struct { -// Time toml.LocalTime -// } -// -// var obj dateStruct -// -// err := toml.Unmarshal([]byte(doc), &obj) -// -// if err != nil { -// t.Fatal(err) -// } -// -// if obj.Time != example.out { -// t.Errorf("expected '%s', got '%s'", example.out, obj.Time) -// } -// }) -// } -//} +func TestUnmarshalLocalDate(t *testing.T) { + t.Run("ToLocalDate", func(t *testing.T) { + type dateStruct struct { + Date toml.LocalDate + } + + doc := `date = 1979-05-27` + + var obj dateStruct + + err := toml.Unmarshal([]byte(doc), &obj) + + if err != nil { + t.Fatal(err) + } + + if obj.Date.Year != 1979 { + t.Errorf("expected year 1979, got %d", obj.Date.Year) + } + if obj.Date.Month != 5 { + t.Errorf("expected month 5, got %d", obj.Date.Month) + } + if obj.Date.Day != 27 { + t.Errorf("expected day 27, got %d", obj.Date.Day) + } + }) + + t.Run("ToLocalDate", func(t *testing.T) { + type dateStruct struct { + Date time.Time + } + + doc := `date = 1979-05-27` + + var obj dateStruct + + err := toml.Unmarshal([]byte(doc), &obj) + + if err != nil { + t.Fatal(err) + } + + if obj.Date.Year() != 1979 { + t.Errorf("expected year 1979, got %d", obj.Date.Year()) + } + if obj.Date.Month() != 5 { + t.Errorf("expected month 5, got %d", obj.Date.Month()) + } + if obj.Date.Day() != 27 { + t.Errorf("expected day 27, got %d", obj.Date.Day()) + } + }) +} + +func TestUnmarshalLocalDateTime(t *testing.T) { + examples := []struct { + name string + in string + out toml.LocalDateTime + }{ + { + name: "normal", + in: "1979-05-27T07:32:00", + out: toml.LocalDateTime{ + Date: toml.LocalDate{ + Year: 1979, + Month: 5, + Day: 27, + }, + Time: toml.LocalTime{ + Hour: 7, + Minute: 32, + Second: 0, + Nanosecond: 0, + }, + }}, + { + name: "with nanoseconds", + in: "1979-05-27T00:32:00.999999", + out: toml.LocalDateTime{ + Date: toml.LocalDate{ + Year: 1979, + Month: 5, + Day: 27, + }, + Time: toml.LocalTime{ + Hour: 0, + Minute: 32, + Second: 0, + Nanosecond: 999999000, + }, + }, + }, + } + + for i, example := range examples { + doc := fmt.Sprintf(`date = %s`, example.in) + + t.Run(fmt.Sprintf("ToLocalDateTime_%d_%s", i, example.name), func(t *testing.T) { + type dateStruct struct { + Date toml.LocalDateTime + } + + var obj dateStruct + + err := toml.Unmarshal([]byte(doc), &obj) + + if err != nil { + t.Fatal(err) + } + + if obj.Date != example.out { + t.Errorf("expected '%s', got '%s'", example.out, obj.Date) + } + }) + + t.Run(fmt.Sprintf("ToTime_%d_%s", i, example.name), func(t *testing.T) { + type dateStruct struct { + Date time.Time + } + + var obj dateStruct + + err := toml.Unmarshal([]byte(doc), &obj) + + if err != nil { + 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.Month() != example.out.Date.Month { + t.Errorf("expected month %d, got %d", example.out.Date.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.Hour() != example.out.Time.Hour { + t.Errorf("expected hour %d, got %d", example.out.Time.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.Second() != example.out.Time.Second { + t.Errorf("expected second %d, got %d", example.out.Time.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()) + } + }) + } +} + +func TestUnmarshalLocalTime(t *testing.T) { + examples := []struct { + name string + in string + out toml.LocalTime + }{ + { + name: "normal", + in: "07:32:00", + out: toml.LocalTime{ + Hour: 7, + Minute: 32, + Second: 0, + Nanosecond: 0, + }, + }, + { + name: "with nanoseconds", + in: "00:32:00.999999", + out: toml.LocalTime{ + Hour: 0, + Minute: 32, + Second: 0, + Nanosecond: 999999000, + }, + }, + } + + for i, example := range examples { + doc := fmt.Sprintf(`Time = %s`, example.in) + + t.Run(fmt.Sprintf("ToLocalTime_%d_%s", i, example.name), func(t *testing.T) { + type dateStruct struct { + Time toml.LocalTime + } + + var obj dateStruct + + err := toml.Unmarshal([]byte(doc), &obj) + + if err != nil { + t.Fatal(err) + } + + if obj.Time != example.out { + t.Errorf("expected '%s', got '%s'", example.out, obj.Time) + } + }) + } +} // test case for issue #339 func TestUnmarshalSameInnerField(t *testing.T) { diff --git a/unmarshaler.go b/unmarshaler.go index f2956dd..b4d5954 100644 --- a/unmarshaler.go +++ b/unmarshaler.go @@ -286,7 +286,7 @@ func tryTextUnmarshaler(x target, node ast.Node) (bool, error) { return false, nil } - // Special case for time, becase we allow to unmarshal to it from + // Special case for time, because we allow to unmarshal to it from // different kind of AST nodes. if v.Type() == timeType { return false, nil @@ -374,6 +374,10 @@ func unmarshalDateTime(x target, node ast.Node) error { } func setLocalDateTime(x target, v LocalDateTime) error { + if x.get().Type() == timeType { + cast := v.In(time.Local) + return setDateTime(x, cast) + } return x.set(reflect.ValueOf(v)) } diff --git a/unmarshaler_test.go b/unmarshaler_test.go index dde7559..7d1bd07 100644 --- a/unmarshaler_test.go +++ b/unmarshaler_test.go @@ -960,6 +960,73 @@ world'`, } } +func TestLocalDateTime(t *testing.T) { + t.Parallel() + + examples := []struct { + desc string + input string + }{ + { + desc: "9 digits", + input: "2006-01-02T15:04:05.123456789", + }, + { + desc: "8 digits", + input: "2006-01-02T15:04:05.12345678", + }, + { + desc: "7 digits", + input: "2006-01-02T15:04:05.1234567", + }, + { + desc: "6 digits", + input: "2006-01-02T15:04:05.123456", + }, + { + desc: "5 digits", + input: "2006-01-02T15:04:05.12345", + }, + { + desc: "4 digits", + input: "2006-01-02T15:04:05.1234", + }, + { + desc: "3 digits", + input: "2006-01-02T15:04:05.123", + }, + { + desc: "2 digits", + input: "2006-01-02T15:04:05.12", + }, + { + desc: "1 digit", + input: "2006-01-02T15:04:05.1", + }, + { + desc: "0 digit", + input: "2006-01-02T15:04:05", + }, + } + + for _, e := range examples { + e := e + t.Run(e.desc, func(t *testing.T) { + t.Parallel() + t.Log("input:", e.input) + doc := `a = ` + e.input + m := map[string]toml.LocalDateTime{} + err := toml.Unmarshal([]byte(doc), &m) + require.NoError(t, err) + actual := m["a"] + golang, err := time.Parse("2006-01-02T15:04:05.999999999", e.input) + require.NoError(t, err) + expected := toml.LocalDateTimeOf(golang) + require.Equal(t, expected, actual) + }) + } +} + func TestIssue287(t *testing.T) { b := `y=[[{}]]` v := map[string]interface{}{}