Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decode: preserve nanosecond precision when decoding time #626

Merged
merged 3 commits into from
Oct 18, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ func parseLocalTime(b []byte) (LocalTime, []byte, error) {
}

t.Nanosecond = frac * nspow[digits]
t.Precision = digits

return t, b[9+digits:], nil
}
Expand Down
2 changes: 2 additions & 0 deletions internal/imported_tests/unmarshal_imported_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1514,6 +1514,7 @@ func TestUnmarshalLocalDateTime(t *testing.T) {
Minute: 32,
Second: 0,
Nanosecond: 999999000,
Precision: 6,
},
},
},
Expand Down Expand Up @@ -1600,6 +1601,7 @@ func TestUnmarshalLocalTime(t *testing.T) {
Minute: 32,
Second: 0,
Nanosecond: 999999000,
Precision: 6,
},
},
}
Expand Down
5 changes: 3 additions & 2 deletions localtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,16 @@ type LocalTime struct {
Minute int
Second int
Nanosecond int
Precision 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 {
if d.Nanosecond == 0 && d.Precision == 0 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for later: I'd expect this to be a || instead of a &&, so that when Precision is 0 the nanoseconds are not displayed. However this does create an issue where the zero-value of LocalTime is has the surprising behavior:

t := LocalTime{Nanosecond: 42}
t.String() => "00:00:00"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My primary use case was decoding. I was trying to allow for this scenario (which I failed to cover in this PR):

diff --git a/localtime_test.go b/localtime_test.go
index 089a8ba..9c9b681 100644
--- a/localtime_test.go
+++ b/localtime_test.go
@@ -41,6 +41,8 @@ func TestLocalTime_String(t *testing.T) {
        require.Equal(t, "20:12:01.000000002", d.String())
        d = toml.LocalTime{20, 12, 1, 0, 0}
        require.Equal(t, "20:12:01", d.String())
+       d = toml.LocalTime{20, 12, 1, 0, 9}
+       require.Equal(t, "20:12:01.000000000", d.String())
 }

 func TestLocalTime_MarshalText(t *testing.T) {
diff --git a/unmarshaler_test.go b/unmarshaler_test.go
index e7e44f7..0fddbbc 100644
--- a/unmarshaler_test.go
+++ b/unmarshaler_test.go
@@ -2038,6 +2038,11 @@ func TestLocalDateTime(t *testing.T) {
                input string
                prec  int
        }{
+               {
+                       desc:  "9 digits zero nanoseconds",
+                       input: "2006-01-02T15:04:05.000000000",
+                       prec:  9,
+               },
                {
                        desc:  "9 digits",
                        input: "2006-01-02T15:04:05.123456789",

I can push these test additions up if you want me to.

I hadn't completely thought through the case you offered. If the user plans to have custom LocalTime values, they will need to also set Precision in order for the String() output to be what's expected. Is this a use case we need to be concerned about? I assumed that most people will be using time.Time values in their app instead of toml.LocalTime.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead and pushed up these additional test cases.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ended up going with a mix of both ideas by providing the minimum number of digits as precision when it is not specified d58efa3.

return s
}
return s + fmt.Sprintf(".%09d", d.Nanosecond)
return s + fmt.Sprintf(".%09d", d.Nanosecond)[:d.Precision+1]
}

// MarshalText returns RFC 3339 representation of d.
Expand Down
23 changes: 15 additions & 8 deletions localtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ func TestLocalDate_UnmarshalMarshalText(t *testing.T) {
}

func TestLocalTime_String(t *testing.T) {
d := toml.LocalTime{20, 12, 1, 2}
d := toml.LocalTime{20, 12, 1, 2, 9}
require.Equal(t, "20:12:01.000000002", d.String())
d = toml.LocalTime{20, 12, 1, 0}
d = toml.LocalTime{20, 12, 1, 0, 0}
require.Equal(t, "20:12:01", d.String())
}

func TestLocalTime_MarshalText(t *testing.T) {
d := toml.LocalTime{20, 12, 1, 2}
d := toml.LocalTime{20, 12, 1, 2, 9}
b, err := d.MarshalText()
require.NoError(t, err)
require.Equal(t, []byte("20:12:01.000000002"), b)
Expand All @@ -54,7 +54,7 @@ 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)
require.Equal(t, toml.LocalTime{20, 12, 1, 2, 9}, d)

err = d.UnmarshalText([]byte("what"))
require.Error(t, err)
Expand All @@ -63,10 +63,17 @@ func TestLocalTime_UnmarshalMarshalText(t *testing.T) {
require.Error(t, err)
}

func TestLocalTime_RoundTrip(t *testing.T) {
var d struct{ A toml.LocalTime }
err := toml.Unmarshal([]byte("a=20:12:01.500"), &d)
require.NoError(t, err)
require.Equal(t, "20:12:01.500", d.A.String())
}

func TestLocalDateTime_AsTime(t *testing.T) {
d := toml.LocalDateTime{
toml.LocalDate{2021, 6, 8},
toml.LocalTime{20, 12, 1, 2},
toml.LocalTime{20, 12, 1, 2, 9},
}
cast := d.AsTime(time.UTC)
require.Equal(t, time.Date(2021, time.June, 8, 20, 12, 1, 2, time.UTC), cast)
Expand All @@ -75,15 +82,15 @@ func TestLocalDateTime_AsTime(t *testing.T) {
func TestLocalDateTime_String(t *testing.T) {
d := toml.LocalDateTime{
toml.LocalDate{2021, 6, 8},
toml.LocalTime{20, 12, 1, 2},
toml.LocalTime{20, 12, 1, 2, 9},
}
require.Equal(t, "2021-06-08 20:12:01.000000002", d.String())
}

func TestLocalDateTime_MarshalText(t *testing.T) {
d := toml.LocalDateTime{
toml.LocalDate{2021, 6, 8},
toml.LocalTime{20, 12, 1, 2},
toml.LocalTime{20, 12, 1, 2, 9},
}
b, err := d.MarshalText()
require.NoError(t, err)
Expand All @@ -96,7 +103,7 @@ func TestLocalDateTime_UnmarshalMarshalText(t *testing.T) {
require.NoError(t, err)
require.Equal(t, toml.LocalDateTime{
toml.LocalDate{2021, 6, 8},
toml.LocalTime{20, 12, 1, 2},
toml.LocalTime{20, 12, 1, 2, 9},
}, d)

err = d.UnmarshalText([]byte("what"))
Expand Down
16 changes: 13 additions & 3 deletions unmarshaler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ func TestUnmarshal(t *testing.T) {
expected: &doc{
A: toml.LocalDateTime{
toml.LocalDate{1979, 5, 27},
toml.LocalTime{0, 32, 0, 0},
toml.LocalTime{0, 32, 0, 0, 0},
},
},
}
Expand Down Expand Up @@ -348,7 +348,7 @@ func TestUnmarshal(t *testing.T) {
return test{
target: &v,
expected: &map[string]interface{}{
"a": toml.LocalTime{Hour: 12, Minute: 8, Second: 5, Nanosecond: 666666666},
"a": toml.LocalTime{Hour: 12, Minute: 8, Second: 5, Nanosecond: 666666666, Precision: 9},
},
}
},
Expand Down Expand Up @@ -2036,42 +2036,52 @@ func TestLocalDateTime(t *testing.T) {
examples := []struct {
desc string
input string
prec int
}{
{
desc: "9 digits",
input: "2006-01-02T15:04:05.123456789",
prec: 9,
},
{
desc: "8 digits",
input: "2006-01-02T15:04:05.12345678",
prec: 8,
},
{
desc: "7 digits",
input: "2006-01-02T15:04:05.1234567",
prec: 7,
},
{
desc: "6 digits",
input: "2006-01-02T15:04:05.123456",
prec: 6,
},
{
desc: "5 digits",
input: "2006-01-02T15:04:05.12345",
prec: 5,
},
{
desc: "4 digits",
input: "2006-01-02T15:04:05.1234",
prec: 4,
},
{
desc: "3 digits",
input: "2006-01-02T15:04:05.123",
prec: 3,
},
{
desc: "2 digits",
input: "2006-01-02T15:04:05.12",
prec: 2,
},
{
desc: "1 digit",
input: "2006-01-02T15:04:05.1",
prec: 1,
},
{
desc: "0 digit",
Expand All @@ -2092,7 +2102,7 @@ func TestLocalDateTime(t *testing.T) {
require.NoError(t, err)
expected := toml.LocalDateTime{
toml.LocalDate{golang.Year(), int(golang.Month()), golang.Day()},
toml.LocalTime{golang.Hour(), golang.Minute(), golang.Second(), golang.Nanosecond()},
toml.LocalTime{golang.Hour(), golang.Minute(), golang.Second(), golang.Nanosecond(), e.prec},
}
require.Equal(t, expected, actual)
})
Expand Down