Skip to content

Commit 5f8850e

Browse files
ccoVeille42atomys
andauthored
fix(date): use available timezone if any (#94)
## Description It's important to use the date timezone if it has any. ## Changes Do not switch back to Local timezone if not needed ## Fixes #81 ## Checklist - [X] I have read the **CONTRIBUTING.md** document. - [X] My code follows the code style of this project. - [X] I have added tests to cover my changes. - [X] All new and existing tests passed. - [ ] I have updated the documentation accordingly. - [ ] This change requires a change to the documentation on the website. ## Additional Information <!-- Any additional information regarding this pull request. --> Timezones are always fun --------- Signed-off-by: ccoVeille <[email protected]> Co-authored-by: Atomys <[email protected]>
1 parent 1fb3882 commit 5f8850e

13 files changed

+402
-112
lines changed

.golangci.yaml

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
# Thanks to @ccoVeille for the configuration template from
2+
# Thanks to @ccoVeille for the configuration template from
33
# https://github.com/ccoVeille/golangci-lint-config-examples
44
linters:
55
enable:
@@ -9,6 +9,7 @@ linters:
99
- gofumpt
1010
- gosimple
1111
- govet
12+
- importas
1213
- ineffassign
1314
- staticcheck
1415
- misspell
@@ -19,6 +20,12 @@ linters:
1920
- usestdlibvars
2021

2122
linters-settings:
23+
importas:
24+
alias:
25+
# prevent conflicts with first level std packages
26+
- pkg: "[a-z][0-9a-z]+"
27+
alias: ""
28+
2229
gofumpt:
2330
module-path: github.com/go-sprout/sprout
2431
misspell:

benchmarks/comparison_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"sync"
1010
"testing"
1111
"text/template"
12-
gotime "time"
12+
"time"
1313

1414
"github.com/Masterminds/sprig/v3"
1515
"github.com/go-sprout/sprout"
@@ -29,7 +29,7 @@ import (
2929
"github.com/go-sprout/sprout/registry/slices"
3030
"github.com/go-sprout/sprout/registry/std"
3131
"github.com/go-sprout/sprout/registry/strings"
32-
"github.com/go-sprout/sprout/registry/time"
32+
rtime "github.com/go-sprout/sprout/registry/time"
3333
"github.com/go-sprout/sprout/registry/uniqueid"
3434
"github.com/go-sprout/sprout/sprigin"
3535
"github.com/stretchr/testify/assert"
@@ -48,8 +48,8 @@ var data = map[string]any{
4848
"object": struct{ Name string }{"example object"},
4949
"func": func() string { return "example function" },
5050
"error": fmt.Errorf("example error"),
51-
"time": gotime.Now(),
52-
"duration": 5 * gotime.Second,
51+
"time": time.Now(),
52+
"duration": 5 * time.Second,
5353
"channel": make(chan any),
5454
"json": `{"foo": "bar"}`,
5555
"yaml": "foo: bar",
@@ -136,7 +136,7 @@ func sproutBench(templatePath string) {
136136
semver.NewRegistry(),
137137
backward.NewRegistry(),
138138
reflect.NewRegistry(),
139-
time.NewRegistry(),
139+
rtime.NewRegistry(),
140140
strings.NewRegistry(),
141141
random.NewRegistry(),
142142
checksum.NewRegistry(),

docs/registries/conversion.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ See more about Golang Layout on the [official documentation](https://go.dev/src/
192192

193193
toLocalDate converts a string to a time.Time object based on a format specification and the local timezone.
194194

195-
<table data-header-hidden><thead><tr><th width="162">Name</th><th>Value</th></tr></thead><tbody><tr><td>Signature</td><td><pre class="language-go"><code class="lang-go">ToLocalDate(fmt, timezone, str string) (time.Time, error)
195+
<table data-header-hidden><thead><tr><th width="162">Name</th><th>Value</th></tr></thead><tbody><tr><td>Signature</td><td><pre class="language-go"><code class="lang-go">ToLocalDate(layout, timezone, value string) (time.Time, error)
196196
</code></pre></td></tr></tbody></table>
197197

198198
{% tabs %}

docs/registries/time.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import "github.com/go-sprout/sprout/registry/time"
1919

2020
The function formats a given date or the current time into a specified format string.
2121

22-
<table data-header-hidden><thead><tr><th width="174">Name</th><th>Value</th></tr></thead><tbody><tr><td>Signature</td><td><pre class="language-go"><code class="lang-go"> Date(fmt string, date any) (string, error)
22+
<table data-header-hidden><thead><tr><th width="174">Name</th><th>Value</th></tr></thead><tbody><tr><td>Signature</td><td><pre class="language-go"><code class="lang-go"> Date(layout string, date any) (string, error)
2323
</code></pre></td></tr></tbody></table>
2424

2525
{% tabs %}
@@ -34,7 +34,7 @@ The function formats a given date or the current time into a specified format st
3434

3535
The function formats a given date or the current time into a specified format string for a specified timezone.
3636

37-
<table data-header-hidden><thead><tr><th width="124">Name</th><th>Value</th></tr></thead><tbody><tr><td>Signature</td><td><pre class="language-go"><code class="lang-go">DateInZone(fmt string, date any, zone string) (string, error)
37+
<table data-header-hidden><thead><tr><th width="124">Name</th><th>Value</th></tr></thead><tbody><tr><td>Signature</td><td><pre class="language-go"><code class="lang-go">DateInZone(layout string, date any, zone string) (string, error)
3838
</code></pre></td></tr></tbody></table>
3939

4040
{% tabs %}
@@ -121,7 +121,7 @@ The function returns the Unix epoch timestamp for a given date.
121121

122122
The function adjusts a given date by a specified duration, returning the modified date. If the duration format is incorrect, it returns the original date without any changes, in case of must version, an error is returned.
123123

124-
<table data-header-hidden><thead><tr><th width="164">Name</th><th>Value</th></tr></thead><tbody><tr><td>Signature</td><td><pre class="language-go"><code class="lang-go">DateModify(fmt string, date time.Time) (time.Time, error)
124+
<table data-header-hidden><thead><tr><th width="164">Name</th><th>Value</th></tr></thead><tbody><tr><td>Signature</td><td><pre class="language-go"><code class="lang-go">DateModify(layout string, date time.Time) (time.Time, error)
125125
</code></pre></td></tr></tbody></table>
126126

127127
{% tabs %}

handler.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package sprout
33
import (
44
"log/slog"
55
"slices"
6-
gostrings "strings"
6+
"strings"
77

88
"golang.org/x/text/cases"
99
"golang.org/x/text/language"
@@ -259,7 +259,7 @@ func safeFuncName(name string) string {
259259
return ""
260260
}
261261

262-
var b gostrings.Builder
262+
var b strings.Builder
263263
b.Grow(len(name) + 4)
264264

265265
b.WriteString("safe")

pesticide/time_test_helpers.go

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package pesticide
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
// ForceTimeLocal temporarily sets [time.Local] for test purpose.
9+
func ForceTimeLocal(t *testing.T, local *time.Location) {
10+
t.Helper()
11+
12+
originalLocal := time.Local
13+
time.Local = local
14+
t.Cleanup(func() { time.Local = originalLocal })
15+
}

registry/conversion/functions.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ func (cr *ConversionRegistry) ToString(value any) string {
170170
//
171171
// Parameters:
172172
//
173-
// fmt string - the date format string.
173+
// layout string - the date format string.
174174
// value string - the date string to parse.
175175
//
176176
// Returns:
@@ -181,16 +181,16 @@ func (cr *ConversionRegistry) ToString(value any) string {
181181
// For an example of this function in a Go template, refer to [Sprout Documentation: toDate].
182182
//
183183
// [Sprout Documentation: toDate]: https://docs.atom.codes/sprout/registries/conversion#todate
184-
func (cr *ConversionRegistry) ToDate(fmt, value string) (time.Time, error) {
185-
return time.ParseInLocation(fmt, value, time.Local)
184+
func (cr *ConversionRegistry) ToDate(layout, value string) (time.Time, error) {
185+
return time.ParseInLocation(layout, value, time.Local)
186186
}
187187

188188
// ToLocalDate converts a string to a time.Time object based on a format specification
189189
// and the local timezone.
190190
//
191191
// Parameters:
192192
//
193-
// fmt string - the date format string.
193+
// layout string - the date format string.
194194
// value string - the date string to parse.
195195
//
196196
// Returns:
@@ -201,13 +201,13 @@ func (cr *ConversionRegistry) ToDate(fmt, value string) (time.Time, error) {
201201
// For an example of this function in a Go template, refer to [Sprout Documentation: toLocalDate].
202202
//
203203
// [Sprout Documentation: toLocalDate]: https://docs.atom.codes/sprout/registries/conversion#tolocaldate
204-
func (cr *ConversionRegistry) ToLocalDate(fmt, timezone, value string) (time.Time, error) {
204+
func (cr *ConversionRegistry) ToLocalDate(layout, timezone, value string) (time.Time, error) {
205205
location, err := time.LoadLocation(timezone)
206206
if err != nil {
207207
return time.Time{}, err
208208
}
209209

210-
return time.ParseInLocation(fmt, value, location)
210+
return time.ParseInLocation(layout, value, location)
211211
}
212212

213213
// ToDuration converts a value to a time.Duration.

registry/conversion/functions_test.go

+148-27
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package conversion_test
33
import (
44
"fmt"
55
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/require"
69

710
"github.com/go-sprout/sprout/pesticide"
811
"github.com/go-sprout/sprout/registry/conversion"
@@ -120,34 +123,152 @@ func TestToString(t *testing.T) {
120123
}
121124

122125
func TestToDate(t *testing.T) {
123-
tc := []pesticide.TestCase{
124-
{
125-
Name: "TestDate",
126-
Input: `{{$v := toDate "2006-01-02" .V }}{{typeOf $v}}-{{$v}}`,
127-
Data: map[string]any{"V": "2024-05-09"},
128-
ExpectedOutput: "time.Time-2024-05-09 00:00:00 +0000 UTC",
129-
},
130-
{
131-
Name: "TestDate",
132-
Input: `{{$v := toDate "2006-01-02 15:04:05 MST" .V }}{{typeOf $v}}-{{$v}}`,
133-
Data: map[string]any{"V": "2024-05-09 00:00:00 UTC"},
134-
ExpectedOutput: "time.Time-2024-05-09 00:00:00 +0000 UTC",
135-
},
136-
{
137-
Name: "TestInvalidValue",
138-
Input: `{{$v := toDate "2006-01-02" .V }}{{typeOf $v}}-{{$v}}`,
139-
Data: map[string]any{"V": ""},
140-
ExpectedErr: "cannot parse \"\" as \"2006\"",
141-
},
142-
{
143-
Name: "TestInvalidLayout",
144-
Input: `{{$v := toDate "invalid" .V }}{{typeOf $v}}-{{$v}}`,
145-
Data: map[string]any{"V": "2024-05-09"},
146-
ExpectedErr: "cannot parse \"2024-05-09\" as \"invalid\"",
147-
},
148-
}
126+
t.Run("dates with numeric timezone offset", func(t *testing.T) {
127+
// Please refer to https://pkg.go.dev/time#Parse
128+
// When parsing a time with a zone offset like -0700,
129+
// if the offset corresponds to a time zone used by the current location (Local),
130+
// then Parse uses that location and zone in the returned time.
131+
// Otherwise it records the time as being in a fabricated location with time fixed at the given zone offset.
149132

150-
pesticide.RunTestCases(t, conversion.NewRegistry(), tc)
133+
// So we have to temporarily force time.Local a known timezone
134+
// to validate the behavior of toDate function
135+
local, err := time.LoadLocation("America/New_York")
136+
require.NoError(t, err)
137+
138+
// temporarily force time.Local to New York
139+
pesticide.ForceTimeLocal(t, local)
140+
141+
tc := []pesticide.TestCase{
142+
{
143+
Name: "date with UTC timezone",
144+
Input: `{{$v := toDate "2006-01-02 15:04:05 -0700" .V }}{{typeOf $v}}-{{$v}}`,
145+
Data: map[string]any{"V": "2024-05-09 00:00:00 +0000"},
146+
ExpectedOutput: "time.Time-2024-05-09 00:00:00 +0000 +0000",
147+
},
148+
{
149+
Name: "date with non-UTC timezone equal to local timezone",
150+
Input: `{{$v := toDate "2006-01-02 15:04:05 -0700" .V }}{{typeOf $v}}-{{$v}}`,
151+
Data: map[string]any{"V": "2024-05-09 00:00:00 -0400"},
152+
ExpectedOutput: "time.Time-2024-05-09 00:00:00 -0400 EDT",
153+
},
154+
{
155+
Name: "date with non-UTC timezone different than local",
156+
Input: `{{$v := toDate "2006-01-02 15:04:05 -0700" .V }}{{typeOf $v}}-{{$v}}`,
157+
Data: map[string]any{"V": "2024-05-09 00:00:00 -0700"},
158+
ExpectedOutput: "time.Time-2024-05-09 00:00:00 -0700 -0700",
159+
},
160+
}
161+
162+
pesticide.RunTestCases(t, conversion.NewRegistry(), tc)
163+
})
164+
165+
t.Run("dates with abbreviated timezone", func(t *testing.T) {
166+
// Please refer to https://pkg.go.dev/time#Parse
167+
// When parsing a time with a zone abbreviation like MST,
168+
// if the zone abbreviation has a defined offset in the current location,
169+
// then that offset is used.
170+
// The zone abbreviation "UTC" is recognized as UTC regardless of location.
171+
// To avoid such problems, prefer time layouts that use a numeric zone offset, or use ParseInLocation.
172+
173+
// So we have to temporarily force time.Local a known timezone
174+
// to validate the behavior of toDate function
175+
local, err := time.LoadLocation("America/New_York")
176+
require.NoError(t, err)
177+
178+
// temporarily force time.Local to New York
179+
pesticide.ForceTimeLocal(t, local)
180+
181+
tc := []pesticide.TestCase{
182+
{
183+
Name: "date with UTC timezone",
184+
Input: `{{$v := toDate "2006-01-02 15:04:05 MST" .V }}{{typeOf $v}}-{{$v}}`,
185+
Data: map[string]any{"V": "2024-05-09 00:00:00 UTC"},
186+
ExpectedOutput: "time.Time-2024-05-09 00:00:00 +0000 UTC",
187+
},
188+
{
189+
Name: "date with non-UTC timezone equal to local timezone",
190+
Input: `{{$v := toDate "2006-01-02 15:04:05 MST" .V }}{{typeOf $v}}-{{$v}}`,
191+
Data: map[string]any{"V": "2024-05-09 00:00:00 EDT"},
192+
ExpectedOutput: "time.Time-2024-05-09 00:00:00 -0400 EDT",
193+
},
194+
{
195+
Name: "date with non-UTC timezone different than local",
196+
Input: `{{$v := toDate "2006-01-02 15:04:05 MST" .V }}{{typeOf $v}}-{{$v}}`,
197+
Data: map[string]any{"V": "2024-05-09 00:00:00 MST"},
198+
ExpectedOutput: "time.Time-2024-05-09 00:00:00 +0000 MST",
199+
},
200+
}
201+
202+
pesticide.RunTestCases(t, conversion.NewRegistry(), tc)
203+
})
204+
205+
t.Run("dates without timezone (local time should be assumed)", func(t *testing.T) {
206+
t.Run("UTC", func(t *testing.T) {
207+
// temporarily force time.Local to UTC
208+
pesticide.ForceTimeLocal(t, time.UTC)
209+
210+
tc := []pesticide.TestCase{
211+
{
212+
Name: "short date",
213+
Input: `{{$v := toDate "2006-01-02" .V }}{{typeOf $v}}-{{$v}}`,
214+
Data: map[string]any{"V": "2024-05-09"},
215+
ExpectedOutput: "time.Time-2024-05-09 00:00:00 +0000 UTC",
216+
},
217+
{
218+
Name: "datetime ",
219+
Input: `{{$v := toDate "2006-01-02 15:04:05" .V }}{{typeOf $v}}-{{$v}}`,
220+
Data: map[string]any{"V": "2024-05-09 01:02:03"},
221+
ExpectedOutput: "time.Time-2024-05-09 01:02:03 +0000 UTC",
222+
},
223+
}
224+
225+
pesticide.RunTestCases(t, conversion.NewRegistry(), tc)
226+
})
227+
228+
t.Run("New York timezone", func(t *testing.T) {
229+
local, err := time.LoadLocation("America/New_York")
230+
require.NoError(t, err)
231+
232+
// temporarily force time.Local to New York
233+
pesticide.ForceTimeLocal(t, local)
234+
235+
tc := []pesticide.TestCase{
236+
{
237+
Name: "short date",
238+
Input: `{{$v := toDate "2006-01-02" .V }}{{typeOf $v}}-{{$v}}`,
239+
Data: map[string]any{"V": "2024-05-09"},
240+
ExpectedOutput: "time.Time-2024-05-09 00:00:00 -0400 EDT",
241+
},
242+
{
243+
Name: "datetime ",
244+
Input: `{{$v := toDate "2006-01-02 15:04:05" .V }}{{typeOf $v}}-{{$v}}`,
245+
Data: map[string]any{"V": "2024-05-09 01:02:03"},
246+
ExpectedOutput: "time.Time-2024-05-09 01:02:03 -0400 EDT",
247+
},
248+
}
249+
250+
pesticide.RunTestCases(t, conversion.NewRegistry(), tc)
251+
})
252+
})
253+
254+
t.Run("invalid layout", func(t *testing.T) {
255+
tc := []pesticide.TestCase{
256+
{
257+
Name: "TestInvalidValue",
258+
Input: `{{$v := toDate "2006-01-02" .V }}{{typeOf $v}}-{{$v}}`,
259+
Data: map[string]any{"V": ""},
260+
ExpectedErr: `cannot parse "" as "2006"`,
261+
},
262+
{
263+
Name: "TestInvalidLayout",
264+
Input: `{{$v := toDate "invalid" .V }}{{typeOf $v}}-{{$v}}`,
265+
Data: map[string]any{"V": "2024-05-09"},
266+
ExpectedErr: `cannot parse "2024-05-09" as "invalid"`,
267+
},
268+
}
269+
270+
pesticide.RunTestCases(t, conversion.NewRegistry(), tc)
271+
})
151272
}
152273

153274
func TestToLocalDate(t *testing.T) {

0 commit comments

Comments
 (0)