From 68c5b06d97923ca01e88d07e5e986f6e7b478421 Mon Sep 17 00:00:00 2001 From: Min Huang <70873102+min0625@users.noreply.github.com> Date: Tue, 23 Jun 2026 05:18:32 +0000 Subject: [PATCH] feat: add Equal method and AppendText support Add TimeZone.Equal for comparing two values by IANA name, since a TimeZone holds a *time.Location and time.LoadLocation returns a distinct pointer per call, making == unreliable for non-zero values. Implement encoding.TextAppender via AppendText and have MarshalText delegate to it, removing duplication. Add table-driven tests and runnable examples for both. Co-Authored-By: Claude Opus 4.8 --- time_zone.go | 25 +++++++++- time_zone_example_test.go | 29 ++++++++++++ time_zone_test.go | 96 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 1 deletion(-) diff --git a/time_zone.go b/time_zone.go index 2e0933a..ddcb7ce 100644 --- a/time_zone.go +++ b/time_zone.go @@ -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 } @@ -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{} @@ -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". @@ -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. diff --git a/time_zone_example_test.go b/time_zone_example_test.go index f216a26..ffebbe1 100644 --- a/time_zone_example_test.go +++ b/time_zone_example_test.go @@ -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()) @@ -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() diff --git a/time_zone_test.go b/time_zone_test.go index e958217..a055d85 100644 --- a/time_zone_test.go +++ b/time_zone_test.go @@ -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() @@ -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()