Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
25 changes: 24 additions & 1 deletion time_zone.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ import (
// See: https://www.iana.org/time-zones.
// The zero value represents the UTC time zone.
// Loading "UTC" or an empty string always results in the zero value.
//
// To compare two TimeZone values, use the [TimeZone.Equal] method rather than
// the == operator. A TimeZone holds a *time.Location, and time.LoadLocation
// returns a distinct pointer on each call, so two values loaded from the same
// name are not guaranteed to be ==. (Comparing against the zero value with
// == is safe, since the zero value holds a nil pointer.)
type TimeZone struct {
loc *time.Location
}
Expand All @@ -28,6 +34,7 @@ var (
_ fmt.Stringer = TimeZone{}
_ sql.Scanner = &TimeZone{}
_ driver.Valuer = TimeZone{}
_ encoding.TextAppender = TimeZone{}
_ encoding.TextMarshaler = TimeZone{}
_ encoding.TextUnmarshaler = &TimeZone{}
_ json.Marshaler = TimeZone{}
Expand Down Expand Up @@ -68,6 +75,15 @@ func (z TimeZone) Location() *time.Location {
return time.UTC
}

// Equal reports whether z and other represent the same IANA time zone.
//
// Use Equal instead of the == operator: a TimeZone holds a *time.Location,
// and time.LoadLocation returns a distinct pointer on each call, so two
// TimeZone values loaded from the same name are not guaranteed to be ==.
func (z TimeZone) Equal(other TimeZone) bool {
return z.String() == other.String()
}

// LoadString loads a TimeZone by IANA time zone name.
// An empty string and "UTC" both set z to the zero value.
// Returns an error if the name is invalid or equals "Local".
Expand Down Expand Up @@ -119,10 +135,17 @@ func (z TimeZone) Value() (driver.Value, error) {
return z.String(), nil
}

// AppendText implements the encoding.TextAppender interface.
// It appends the IANA time zone name (e.g. "America/New_York" or "UTC") to b
// and returns the extended buffer.
func (z TimeZone) AppendText(b []byte) ([]byte, error) {
return append(b, z.String()...), nil
}

// MarshalText implements the encoding.TextMarshaler interface.
// It encodes the time zone as its IANA name (e.g. "America/New_York" or "UTC").
func (z TimeZone) MarshalText() (text []byte, err error) {
return []byte(z.String()), nil
return z.AppendText(nil)
}

// UnmarshalText implements the encoding.TextUnmarshaler interface.
Expand Down
29 changes: 29 additions & 0 deletions time_zone_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ func ExampleTimeZone_zeroValue() {
// true
}

func ExampleTimeZone_Equal() {
a := tz.MustLoadTimeZone("America/New_York")
b := tz.MustLoadTimeZone("America/New_York")

// Each load returns a distinct *time.Location, so == is unreliable,
// but Equal compares by IANA name.
fmt.Println(a == b)
fmt.Println(a.Equal(b))

// The zero value equals "UTC".
fmt.Println(tz.TimeZone{}.Equal(tz.MustLoadTimeZone("UTC")))

// Output:
// false
// true
// true
}

func ExampleMustLoadTimeZone() {
z := tz.MustLoadTimeZone("America/New_York")
fmt.Println(z.String())
Expand Down Expand Up @@ -91,6 +109,17 @@ func ExampleUTCTimeZone() {
// true
}

func ExampleTimeZone_AppendText() {
z := tz.MustLoadTimeZone("Asia/Tokyo")

b := []byte("zone=")
b, _ = z.AppendText(b)
fmt.Println(string(b))

// Output:
// zone=Asia/Tokyo
}

func ExampleTimeZone_MarshalText() {
z := tz.MustLoadTimeZone("Asia/Tokyo")
text, _ := z.MarshalText()
Expand Down
96 changes: 96 additions & 0 deletions time_zone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,56 @@ func TestTimeZone_Location_NonUTC(t *testing.T) {
assert.NotSame(t, loc, time.UTC)
}

func TestTimeZone_Equal(t *testing.T) {
t.Parallel()

tests := []struct {
name string
z tz.TimeZone
o tz.TimeZone
want bool
}{
{
name: "BothZeroValue",
z: tz.TimeZone{},
o: tz.TimeZone{},
want: true,
},
{
name: "ZeroValueAndUTC",
z: tz.TimeZone{},
o: mustLoadTimeZone(t, "UTC"),
want: true,
},
{
name: "SameNameDistinctPointers",
z: mustLoadTimeZone(t, "America/New_York"),
o: mustLoadTimeZone(t, "America/New_York"),
want: true,
},
{
name: "Different",
z: mustLoadTimeZone(t, "America/New_York"),
o: mustLoadTimeZone(t, "Asia/Tokyo"),
want: false,
},
{
name: "NonUTCAndZeroValue",
z: mustLoadTimeZone(t, "Asia/Tokyo"),
o: tz.TimeZone{},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

assert.Equal(t, tt.want, tt.z.Equal(tt.o))
assert.Equal(t, tt.want, tt.o.Equal(tt.z))
})
}
}

func TestTimeZone_LoadString(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -369,6 +419,52 @@ func TestTimeZone_Value(t *testing.T) {
}
}

func TestTimeZone_AppendText(t *testing.T) {
t.Parallel()

tests := []struct {
name string
z tz.TimeZone
prefix []byte
want []byte
wantErr bool
}{
{
name: "UTC",
z: mustLoadTimeZone(t, "UTC"),
want: []byte("UTC"),
wantErr: false,
},
{
name: "Asia/Tokyo",
z: mustLoadTimeZone(t, "Asia/Tokyo"),
want: []byte("Asia/Tokyo"),
wantErr: false,
},
{
name: "AppendsToExistingBuffer",
z: mustLoadTimeZone(t, "Asia/Tokyo"),
prefix: []byte("zone="),
want: []byte("zone=Asia/Tokyo"),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

got, err := tt.z.AppendText(tt.prefix)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}

assert.Equal(t, tt.want, got)
})
}
}

func TestTimeZone_MarshalText(t *testing.T) {
t.Parallel()

Expand Down
Loading