Repository: go-sprout/sprout (go)

Issue:
bug: Tests fail locally if local timezone != UTC
### Describe the bug

running the tests locally it makes it fail when the timezone is not set to UTC. 
```
    --- FAIL: TestDateModify/AddTenMinutes (0.00s)
        functions_test.go:100: 
                Error Trace:    /sprout/pesticide/template_test_helpers.go:45
                Error:          Not equal: 
                                expected: "07 May 24 15:14 +0000"
                                actual  : "07 May 24 17:14 +0200"
                            
                                Diff:
                                --- Expected
                                +++ Actual
                                @@ -1 +1 @@
                                -07 May 24 15:14 +0000
                                +07 May 24 17:14 +0200
                Test:           TestDateModify/AddTenMinutes
```

### To Reproduce

1. Set your timezone to a different one than UTC
2. Execute tests

### Expected behavior

_No response_

### Code Snippet

```Shell

```

### Version of software

latest main checkout

### Additional context

_No response_

### Code of Conduct

- [x] I agree to follow this project's Code of Conduct

Pull request description:
fix(date): use available timezone if any
## 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

Patch:
diff --git a/.golangci.yaml b/.golangci.yaml
index 8d6b696..7064a27 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -1,5 +1,5 @@
 ---
-# Thanks to @ccoVeille for the configuration template from 
+# Thanks to @ccoVeille for the configuration template from
 # https://github.com/ccoVeille/golangci-lint-config-examples
 linters:
   enable:
@@ -9,6 +9,7 @@ linters:
     - gofumpt
     - gosimple
     - govet
+    - importas
     - ineffassign
     - staticcheck
     - misspell
@@ -19,6 +20,12 @@ linters:
     - usestdlibvars
 
 linters-settings:
+  importas:
+    alias:
+      # prevent conflicts with first level std packages
+      - pkg:  "[a-z][0-9a-z]+"
+        alias: ""
+
   gofumpt:
     module-path: github.com/go-sprout/sprout
   misspell:
diff --git a/docs/registries/conversion.md b/docs/registries/conversion.md
index 5846fa6..1cb0bf7 100644
--- a/docs/registries/conversion.md
+++ b/docs/registries/conversion.md
@@ -192,7 +192,7 @@ See more about Golang Layout on the [official documentation](https://go.dev/src/
 
 toLocalDate converts a string to a time.Time object based on a format specification and the local timezone.
 
-<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)
+<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)
 </code></pre></td></tr></tbody></table>
 
 {% tabs %}
diff --git a/docs/registries/time.md b/docs/registries/time.md
index 92a6194..1b703d4 100644
--- a/docs/registries/time.md
+++ b/docs/registries/time.md
@@ -19,7 +19,7 @@ import "github.com/go-sprout/sprout/registry/time"
 
 The function formats a given date or the current time into a specified format string.
 
-<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)
+<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)
 </code></pre></td></tr></tbody></table>
 
 {% tabs %}
@@ -34,7 +34,7 @@ The function formats a given date or the current time into a specified format st
 
 The function formats a given date or the current time into a specified format string for a specified timezone.
 
-<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)
+<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)
 </code></pre></td></tr></tbody></table>
 
 {% tabs %}
@@ -121,7 +121,7 @@ The function returns the Unix epoch timestamp for a given date.
 
 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.
 
-<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)
+<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)
 </code></pre></td></tr></tbody></table>
 
 {% tabs %}
diff --git a/handler.go b/handler.go
index f873b58..5757e7f 100644
--- a/handler.go
+++ b/handler.go
@@ -3,7 +3,7 @@ package sprout
 import (
 	"log/slog"
 	"slices"
-	gostrings "strings"
+	"strings"
 
 	"golang.org/x/text/cases"
 	"golang.org/x/text/language"
@@ -259,7 +259,7 @@ func safeFuncName(name string) string {
 		return ""
 	}
 
-	var b gostrings.Builder
+	var b strings.Builder
 	b.Grow(len(name) + 4)
 
 	b.WriteString("safe")
diff --git a/registry/conversion/functions.go b/registry/conversion/functions.go
index ffac425..c488813 100644
--- a/registry/conversion/functions.go
+++ b/registry/conversion/functions.go
@@ -170,7 +170,7 @@ func (cr *ConversionRegistry) ToString(value any) string {
 //
 // Parameters:
 //
-//	fmt string - the date format string.
+//	layout string - the date format string.
 //	value string - the date string to parse.
 //
 // Returns:
@@ -181,8 +181,8 @@ func (cr *ConversionRegistry) ToString(value any) string {
 // For an example of this function in a Go template, refer to [Sprout Documentation: toDate].
 //
 // [Sprout Documentation: toDate]: https://docs.atom.codes/sprout/registries/conversion#todate
-func (cr *ConversionRegistry) ToDate(fmt, value string) (time.Time, error) {
-	return time.ParseInLocation(fmt, value, time.Local)
+func (cr *ConversionRegistry) ToDate(layout, value string) (time.Time, error) {
+	return time.ParseInLocation(layout, value, time.Local)
 }
 
 // ToLocalDate converts a string to a time.Time object based on a format specification
@@ -190,7 +190,7 @@ func (cr *ConversionRegistry) ToDate(fmt, value string) (time.Time, error) {
 //
 // Parameters:
 //
-//	fmt string - the date format string.
+//	layout string - the date format string.
 //	value string - the date string to parse.
 //
 // Returns:
@@ -201,13 +201,13 @@ func (cr *ConversionRegistry) ToDate(fmt, value string) (time.Time, error) {
 // For an example of this function in a Go template, refer to [Sprout Documentation: toLocalDate].
 //
 // [Sprout Documentation: toLocalDate]: https://docs.atom.codes/sprout/registries/conversion#tolocaldate
-func (cr *ConversionRegistry) ToLocalDate(fmt, timezone, value string) (time.Time, error) {
+func (cr *ConversionRegistry) ToLocalDate(layout, timezone, value string) (time.Time, error) {
 	location, err := time.LoadLocation(timezone)
 	if err != nil {
 		return time.Time{}, err
 	}
 
-	return time.ParseInLocation(fmt, value, location)
+	return time.ParseInLocation(layout, value, location)
 }
 
 // ToDuration converts a value to a time.Duration.
diff --git a/registry/time/functions.go b/registry/time/functions.go
index 7e0ad2f..6b846e0 100644
--- a/registry/time/functions.go
+++ b/registry/time/functions.go
@@ -10,8 +10,8 @@ import (
 //
 // Parameters:
 //
-//	fmt string - the format string.
-//	date any - the date to format or the current time if not a date type.
+//	layout string - the format string.
+//	date   any    - the date to format or the current time if not a date type.
 //
 // Returns:
 //
@@ -21,16 +21,21 @@ import (
 // For an example of this function in a Go template, refer to [Sprout Documentation: date].
 //
 // [Sprout Documentation: date]: https://docs.atom.codes/sprout/registries/time#date
-func (tr *TimeRegistry) Date(fmt string, date any) (string, error) {
-	return tr.DateInZone(fmt, date, "Local")
+func (tr *TimeRegistry) Date(layout string, date any) (string, error) {
+	t := computeTimeFromFormat(date)
+
+	// compute the timezone from the date if it has one
+	loc := time.FixedZone(t.Zone())
+
+	return t.In(loc).Format(layout), nil
 }
 
 // DateInZone formats a given date or current time into a specified format string in a specified timezone.
 //
 // Parameters:
 //
-//	fmt string - the format string.
-//	date any - the date to format, in various acceptable formats.
+//	layout string - the format string.
+//	date any    - the date to format, in various acceptable formats.
 //	zone string - the timezone name.
 //
 // Returns:
@@ -41,30 +46,14 @@ func (tr *TimeRegistry) Date(fmt string, date any) (string, error) {
 // For an example of this function in a Go template, refer to [Sprout Documentation: dateInZone].
 //
 // [Sprout Documentation: dateInZone]: https://docs.atom.codes/sprout/registries/time#dateinzone
-func (tr *TimeRegistry) DateInZone(fmt string, date any, zone string) (string, error) {
-	// TODO: Change signature
-	var t time.Time
-	switch date := date.(type) {
-	default:
-		t = time.Now()
-	case time.Time:
-		t = date
-	case *time.Time:
-		t = *date
-	case int64:
-		t = time.Unix(date, 0)
-	case int:
-		t = time.Unix(int64(date), 0)
-	case int32:
-		t = time.Unix(int64(date), 0)
-	}
-
+func (tr *TimeRegistry) DateInZone(layout string, date any, zone string) (string, error) {
+	t := computeTimeFromFormat(date)
 	loc, err := time.LoadLocation(zone)
 	if err != nil {
-		return t.In(time.UTC).Format(fmt), err
+		return t.In(time.UTC).Format(layout), err
 	}
 
-	return t.In(loc).Format(fmt), nil
+	return t.In(loc).Format(layout), nil
 }
 
 // Duration converts seconds into a human-readable duration string.
@@ -169,7 +158,7 @@ func (tr *TimeRegistry) UnixEpoch(date time.Time) string {
 //
 // Parameters:
 //
-//	fmt string - the duration string to add to the date, such as "2h" for two hours.
+//	layout string - the duration string to add to the date, such as "2h" for two hours.
 //	date time.Time - the date to modify.
 //
 // Returns:
@@ -180,8 +169,8 @@ func (tr *TimeRegistry) UnixEpoch(date time.Time) string {
 // For an example of this function in a Go template, refer to [Sprout Documentation: dateModify].
 //
 // [Sprout Documentation: dateModify]: https://docs.atom.codes/sprout/registries/time#datemodify
-func (tr *TimeRegistry) DateModify(fmt string, date time.Time) (time.Time, error) {
-	d, err := time.ParseDuration(fmt)
+func (tr *TimeRegistry) DateModify(layout string, date time.Time) (time.Time, error) {
+	d, err := time.ParseDuration(layout)
 	if err != nil {
 		return time.Time{}, err
 	}
diff --git a/registry/time/helpers.go b/registry/time/helpers.go
new file mode 100644
index 0000000..36317c9
--- /dev/null
+++ b/registry/time/helpers.go
@@ -0,0 +1,24 @@
+package time
+
+import (
+	"time"
+)
+
+// computeTimeFromFormat returns a time.Time object from the given date.
+func computeTimeFromFormat(date any) time.Time {
+	switch date := date.(type) {
+	case time.Time:
+		return date
+	case *time.Time:
+		return *date
+	case int64:
+		return time.Unix(date, 0)
+	case int:
+		return time.Unix(int64(date), 0)
+	case int32:
+		return time.Unix(int64(date), 0)
+	}
+
+	// otherwise, fallback to the current time
+	return time.Now().Local()
+}
diff --git a/sprigin/sprig_backward_compatibility.go b/sprigin/sprig_backward_compatibility.go
index d01ec9c..511ce7e 100644
--- a/sprigin/sprig_backward_compatibility.go
+++ b/sprigin/sprig_backward_compatibility.go
@@ -3,7 +3,7 @@ package sprigin
 import (
 	htemplate "html/template"
 	"log/slog"
-	gostrings "strings"
+	"strings"
 	ttemplate "text/template"
 
 	"github.com/go-sprout/sprout"
@@ -23,7 +23,7 @@ import (
 	"github.com/go-sprout/sprout/registry/semver"
 	"github.com/go-sprout/sprout/registry/slices"
 	"github.com/go-sprout/sprout/registry/std"
-	"github.com/go-sprout/sprout/registry/strings"
+	rstrings "github.com/go-sprout/sprout/registry/strings"
 	"github.com/go-sprout/sprout/registry/time"
 	"github.com/go-sprout/sprout/registry/uniqueid"
 )
@@ -164,7 +164,7 @@ func (sh *SprigHandler) Build() sprout.FunctionMap {
 		backward.NewRegistry(),
 		reflect.NewRegistry(),
 		time.NewRegistry(),
-		strings.NewRegistry(),
+		rstrings.NewRegistry(),
 		random.NewRegistry(),
 		checksum.NewRegistry(),
 		conversion.NewRegistry(),
@@ -197,7 +197,7 @@ func (sh *SprigHandler) Build() sprout.FunctionMap {
 	// BACKWARDS COMPATIBILITY
 	// Ensure error handling is consistent with sprig functions
 	for funcName, fn := range sh.funcsMap {
-		if !gostrings.HasPrefix(funcName, "must") {
+		if !strings.HasPrefix(funcName, "must") {
 			sh.funcsMap[funcName] = func(args ...any) (any, error) {
 				out, _ := runtime.SafeCall(fn, args...)
 				return out, nil


Test patch:
diff --git a/benchmarks/comparison_test.go b/benchmarks/comparison_test.go
index c9de91f..7013fff 100644
--- a/benchmarks/comparison_test.go
+++ b/benchmarks/comparison_test.go
@@ -9,7 +9,7 @@ import (
 	"sync"
 	"testing"
 	"text/template"
-	gotime "time"
+	"time"
 
 	"github.com/Masterminds/sprig/v3"
 	"github.com/go-sprout/sprout"
@@ -29,7 +29,7 @@ import (
 	"github.com/go-sprout/sprout/registry/slices"
 	"github.com/go-sprout/sprout/registry/std"
 	"github.com/go-sprout/sprout/registry/strings"
-	"github.com/go-sprout/sprout/registry/time"
+	rtime "github.com/go-sprout/sprout/registry/time"
 	"github.com/go-sprout/sprout/registry/uniqueid"
 	"github.com/go-sprout/sprout/sprigin"
 	"github.com/stretchr/testify/assert"
@@ -48,8 +48,8 @@ var data = map[string]any{
 	"object":      struct{ Name string }{"example object"},
 	"func":        func() string { return "example function" },
 	"error":       fmt.Errorf("example error"),
-	"time":        gotime.Now(),
-	"duration":    5 * gotime.Second,
+	"time":        time.Now(),
+	"duration":    5 * time.Second,
 	"channel":     make(chan any),
 	"json":        `{"foo": "bar"}`,
 	"yaml":        "foo: bar",
@@ -136,7 +136,7 @@ func sproutBench(templatePath string) {
 		semver.NewRegistry(),
 		backward.NewRegistry(),
 		reflect.NewRegistry(),
-		time.NewRegistry(),
+		rtime.NewRegistry(),
 		strings.NewRegistry(),
 		random.NewRegistry(),
 		checksum.NewRegistry(),
diff --git a/pesticide/time_test_helpers.go b/pesticide/time_test_helpers.go
new file mode 100644
index 0000000..df38288
--- /dev/null
+++ b/pesticide/time_test_helpers.go
@@ -0,0 +1,15 @@
+package pesticide
+
+import (
+	"testing"
+	"time"
+)
+
+// ForceTimeLocal temporarily sets [time.Local] for test purpose.
+func ForceTimeLocal(t *testing.T, local *time.Location) {
+	t.Helper()
+
+	originalLocal := time.Local
+	time.Local = local
+	t.Cleanup(func() { time.Local = originalLocal })
+}
diff --git a/registry/conversion/functions_test.go b/registry/conversion/functions_test.go
index fb040b0..b62e83e 100644
--- a/registry/conversion/functions_test.go
+++ b/registry/conversion/functions_test.go
@@ -3,6 +3,9 @@ package conversion_test
 import (
 	"fmt"
 	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
 
 	"github.com/go-sprout/sprout/pesticide"
 	"github.com/go-sprout/sprout/registry/conversion"
@@ -120,34 +123,152 @@ func TestToString(t *testing.T) {
 }
 
 func TestToDate(t *testing.T) {
-	tc := []pesticide.TestCase{
-		{
-			Name:           "TestDate",
-			Input:          `{{$v := toDate "2006-01-02" .V }}{{typeOf $v}}-{{$v}}`,
-			Data:           map[string]any{"V": "2024-05-09"},
-			ExpectedOutput: "time.Time-2024-05-09 00:00:00 +0000 UTC",
-		},
-		{
-			Name:           "TestDate",
-			Input:          `{{$v := toDate "2006-01-02 15:04:05 MST" .V }}{{typeOf $v}}-{{$v}}`,
-			Data:           map[string]any{"V": "2024-05-09 00:00:00 UTC"},
-			ExpectedOutput: "time.Time-2024-05-09 00:00:00 +0000 UTC",
-		},
-		{
-			Name:        "TestInvalidValue",
-			Input:       `{{$v := toDate "2006-01-02" .V }}{{typeOf $v}}-{{$v}}`,
-			Data:        map[string]any{"V": ""},
-			ExpectedErr: "cannot parse \"\" as \"2006\"",
-		},
-		{
-			Name:        "TestInvalidLayout",
-			Input:       `{{$v := toDate "invalid" .V }}{{typeOf $v}}-{{$v}}`,
-			Data:        map[string]any{"V": "2024-05-09"},
-			ExpectedErr: "cannot parse \"2024-05-09\" as \"invalid\"",
-		},
-	}
+	t.Run("dates with numeric timezone offset", func(t *testing.T) {
+		// Please refer to https://pkg.go.dev/time#Parse
+		// When parsing a time with a zone offset like -0700,
+		// if the offset corresponds to a time zone used by the current location (Local),
+		// then Parse uses that location and zone in the returned time.
+		// Otherwise it records the time as being in a fabricated location with time fixed at the given zone offset.
 
-	pesticide.RunTestCases(t, conversion.NewRegistry(), tc)
+		// So we have to temporarily force time.Local a known timezone
+		// to validate the behavior of toDate function
+		local, err := time.LoadLocation("America/New_York")
+		require.NoError(t, err)
+
+		// temporarily force time.Local to New York
+		pesticide.ForceTimeLocal(t, local)
+
+		tc := []pesticide.TestCase{
+			{
+				Name:           "date with UTC timezone",
+				Input:          `{{$v := toDate "2006-01-02 15:04:05 -0700" .V }}{{typeOf $v}}-{{$v}}`,
+				Data:           map[string]any{"V": "2024-05-09 00:00:00 +0000"},
+				ExpectedOutput: "time.Time-2024-05-09 00:00:00 +0000 +0000",
+			},
+			{
+				Name:           "date with non-UTC timezone equal to local timezone",
+				Input:          `{{$v := toDate "2006-01-02 15:04:05 -0700" .V }}{{typeOf $v}}-{{$v}}`,
+				Data:           map[string]any{"V": "2024-05-09 00:00:00 -0400"},
+				ExpectedOutput: "time.Time-2024-05-09 00:00:00 -0400 EDT",
+			},
+			{
+				Name:           "date with non-UTC timezone different than local",
+				Input:          `{{$v := toDate "2006-01-02 15:04:05 -0700" .V }}{{typeOf $v}}-{{$v}}`,
+				Data:           map[string]any{"V": "2024-05-09 00:00:00 -0700"},
+				ExpectedOutput: "time.Time-2024-05-09 00:00:00 -0700 -0700",
+			},
+		}
+
+		pesticide.RunTestCases(t, conversion.NewRegistry(), tc)
+	})
+
+	t.Run("dates with abbreviated timezone", func(t *testing.T) {
+		// Please refer to https://pkg.go.dev/time#Parse
+		// When parsing a time with a zone abbreviation like MST,
+		// if the zone abbreviation has a defined offset in the current location,
+		// then that offset is used.
+		// The zone abbreviation "UTC" is recognized as UTC regardless of location.
+		// To avoid such problems, prefer time layouts that use a numeric zone offset, or use ParseInLocation.
+
+		// So we have to temporarily force time.Local a known timezone
+		// to validate the behavior of toDate function
+		local, err := time.LoadLocation("America/New_York")
+		require.NoError(t, err)
+
+		// temporarily force time.Local to New York
+		pesticide.ForceTimeLocal(t, local)
+
+		tc := []pesticide.TestCase{
+			{
+				Name:           "date with UTC timezone",
+				Input:          `{{$v := toDate "2006-01-02 15:04:05 MST" .V }}{{typeOf $v}}-{{$v}}`,
+				Data:           map[string]any{"V": "2024-05-09 00:00:00 UTC"},
+				ExpectedOutput: "time.Time-2024-05-09 00:00:00 +0000 UTC",
+			},
+			{
+				Name:           "date with non-UTC timezone equal to local timezone",
+				Input:          `{{$v := toDate "2006-01-02 15:04:05 MST" .V }}{{typeOf $v}}-{{$v}}`,
+				Data:           map[string]any{"V": "2024-05-09 00:00:00 EDT"},
+				ExpectedOutput: "time.Time-2024-05-09 00:00:00 -0400 EDT",
+			},
+			{
+				Name:           "date with non-UTC timezone different than local",
+				Input:          `{{$v := toDate "2006-01-02 15:04:05 MST" .V }}{{typeOf $v}}-{{$v}}`,
+				Data:           map[string]any{"V": "2024-05-09 00:00:00 MST"},
+				ExpectedOutput: "time.Time-2024-05-09 00:00:00 +0000 MST",
+			},
+		}
+
+		pesticide.RunTestCases(t, conversion.NewRegistry(), tc)
+	})
+
+	t.Run("dates without timezone (local time should be assumed)", func(t *testing.T) {
+		t.Run("UTC", func(t *testing.T) {
+			// temporarily force time.Local to UTC
+			pesticide.ForceTimeLocal(t, time.UTC)
+
+			tc := []pesticide.TestCase{
+				{
+					Name:           "short date",
+					Input:          `{{$v := toDate "2006-01-02" .V }}{{typeOf $v}}-{{$v}}`,
+					Data:           map[string]any{"V": "2024-05-09"},
+					ExpectedOutput: "time.Time-2024-05-09 00:00:00 +0000 UTC",
+				},
+				{
+					Name:           "datetime ",
+					Input:          `{{$v := toDate "2006-01-02 15:04:05" .V }}{{typeOf $v}}-{{$v}}`,
+					Data:           map[string]any{"V": "2024-05-09 01:02:03"},
+					ExpectedOutput: "time.Time-2024-05-09 01:02:03 +0000 UTC",
+				},
+			}
+
+			pesticide.RunTestCases(t, conversion.NewRegistry(), tc)
+		})
+
+		t.Run("New York timezone", func(t *testing.T) {
+			local, err := time.LoadLocation("America/New_York")
+			require.NoError(t, err)
+
+			// temporarily force time.Local to New York
+			pesticide.ForceTimeLocal(t, local)
+
+			tc := []pesticide.TestCase{
+				{
+					Name:           "short date",
+					Input:          `{{$v := toDate "2006-01-02" .V }}{{typeOf $v}}-{{$v}}`,
+					Data:           map[string]any{"V": "2024-05-09"},
+					ExpectedOutput: "time.Time-2024-05-09 00:00:00 -0400 EDT",
+				},
+				{
+					Name:           "datetime ",
+					Input:          `{{$v := toDate "2006-01-02 15:04:05" .V }}{{typeOf $v}}-{{$v}}`,
+					Data:           map[string]any{"V": "2024-05-09 01:02:03"},
+					ExpectedOutput: "time.Time-2024-05-09 01:02:03 -0400 EDT",
+				},
+			}
+
+			pesticide.RunTestCases(t, conversion.NewRegistry(), tc)
+		})
+	})
+
+	t.Run("invalid layout", func(t *testing.T) {
+		tc := []pesticide.TestCase{
+			{
+				Name:        "TestInvalidValue",
+				Input:       `{{$v := toDate "2006-01-02" .V }}{{typeOf $v}}-{{$v}}`,
+				Data:        map[string]any{"V": ""},
+				ExpectedErr: `cannot parse "" as "2006"`,
+			},
+			{
+				Name:        "TestInvalidLayout",
+				Input:       `{{$v := toDate "invalid" .V }}{{typeOf $v}}-{{$v}}`,
+				Data:        map[string]any{"V": "2024-05-09"},
+				ExpectedErr: `cannot parse "2024-05-09" as "invalid"`,
+			},
+		}
+
+		pesticide.RunTestCases(t, conversion.NewRegistry(), tc)
+	})
 }
 
 func TestToLocalDate(t *testing.T) {
diff --git a/registry/time/functions_test.go b/registry/time/functions_test.go
index 70430a3..76faef7 100644
--- a/registry/time/functions_test.go
+++ b/registry/time/functions_test.go
@@ -2,27 +2,103 @@ package time_test
 
 import (
 	"testing"
-	goTime "time"
+	"time"
+
+	"github.com/stretchr/testify/require"
 
 	"github.com/go-sprout/sprout/pesticide"
-	"github.com/go-sprout/sprout/registry/time"
+	rtime "github.com/go-sprout/sprout/registry/time"
 )
 
 func TestDate(t *testing.T) {
-	timeTest := goTime.Date(2024, 5, 7, 15, 4, 5, 0, goTime.UTC)
+	t.Run("UTC", func(t *testing.T) {
+		timeTest := time.Date(2024, 5, 7, 15, 4, 5, 0, time.UTC)
+		tc := []pesticide.TestCase{
+			{Name: "TestTimeObject", Input: `{{ .V | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: "07 May 24 15:04 +0000", Data: map[string]any{"V": timeTest}},
+			{Name: "TestTimeObjectPointer", Input: `{{ .V | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: "07 May 24 15:04 +0000", Data: map[string]any{"V": &timeTest}},
+		}
 
-	tc := []pesticide.TestCase{
-		{Name: "TestTimeObject", Input: `{{ .V | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: "07 May 24 15:04 +0000", Data: map[string]any{"V": timeTest}},
-		{Name: "TestTimeObjectPointer", Input: `{{ .V | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: "07 May 24 15:04 +0000", Data: map[string]any{"V": &timeTest}},
-		{Name: "TestTimeObjectUnix", Input: `{{ .V | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: "07 May 24 15:04 +0000", Data: map[string]any{"V": timeTest.Unix()}},
-		{Name: "TestTimeObjectUnixInt", Input: `{{ .V | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: "07 May 24 15:04 +0000", Data: map[string]any{"V": int(timeTest.Unix())}},
-	}
+		pesticide.RunTestCases(t, rtime.NewRegistry(), tc)
+	})
+
+	t.Run("New York timezone", func(t *testing.T) {
+		local, err := time.LoadLocation("America/New_York")
+		require.NoError(t, err)
+
+		timeTest := time.Date(2024, 5, 7, 15, 4, 5, 0, local)
+
+		tc := []pesticide.TestCase{
+			{Name: "TestTimeObject", Input: `{{ .V | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: "07 May 24 15:04 -0400", Data: map[string]any{"V": timeTest}},
+			{Name: "TestTimeObjectPointer", Input: `{{ .V | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: "07 May 24 15:04 -0400", Data: map[string]any{"V": &timeTest}},
+		}
+
+		pesticide.RunTestCases(t, rtime.NewRegistry(), tc)
+	})
+
+	t.Run("New York offset", func(t *testing.T) {
+		timeTest, err := time.Parse("02 Jan 06 15:04 -0700", "07 May 24 15:04 -0400")
+		require.NoError(t, err)
+
+		tc := []pesticide.TestCase{
+			{Name: "TestTimeObject", Input: `{{ .V | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: "07 May 24 15:04 -0400", Data: map[string]any{"V": timeTest}},
+			{Name: "TestTimeObjectPointer", Input: `{{ .V | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: "07 May 24 15:04 -0400", Data: map[string]any{"V": &timeTest}},
+		}
+
+		pesticide.RunTestCases(t, rtime.NewRegistry(), tc)
+	})
+
+	t.Run("New York timezone", func(t *testing.T) {
+		local, err := time.LoadLocation("America/New_York")
+		require.NoError(t, err)
+
+		timeTest := time.Date(2024, 5, 7, 15, 4, 5, 0, local)
+
+		tc := []pesticide.TestCase{
+			{Name: "TestTimeObject", Input: `{{ .V | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: "07 May 24 15:04 -0400", Data: map[string]any{"V": timeTest}},
+			{Name: "TestTimeObjectPointer", Input: `{{ .V | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: "07 May 24 15:04 -0400", Data: map[string]any{"V": &timeTest}},
+		}
+
+		pesticide.RunTestCases(t, rtime.NewRegistry(), tc)
+	})
+
+	t.Run("unixtime", func(t *testing.T) {
+		t.Run("UTC", func(t *testing.T) {
+			// temporarily force time.Local to UTC
+			pesticide.ForceTimeLocal(t, time.UTC)
+
+			// here we are simulating a [gotime.Now]
+			timeTest := time.Date(2024, 5, 7, 15, 4, 5, 0, time.UTC)
+
+			tc := []pesticide.TestCase{
+				{Name: "TestTimeObjectUnix", Input: `{{ .V | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: "07 May 24 15:04 +0000", Data: map[string]any{"V": timeTest.Unix()}},
+				{Name: "TestTimeObjectUnixInt", Input: `{{ .V | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: "07 May 24 15:04 +0000", Data: map[string]any{"V": int(timeTest.Unix())}},
+			}
+
+			pesticide.RunTestCases(t, rtime.NewRegistry(), tc)
+		})
+
+		t.Run("New York timezone", func(t *testing.T) {
+			local, err := time.LoadLocation("America/New_York")
+			require.NoError(t, err)
+
+			// temporarily force time.Local to New York
+			pesticide.ForceTimeLocal(t, local)
+
+			// here we are simulating a [gotime.Now] call
+			timeTest := time.Date(2024, 5, 7, 15, 4, 5, 0, local)
+
+			tc := []pesticide.TestCase{
+				{Name: "TestTimeObject", Input: `{{ .V | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: "07 May 24 15:04 -0400", Data: map[string]any{"V": timeTest}},
+				{Name: "TestTimeObjectPointer", Input: `{{ .V | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: "07 May 24 15:04 -0400", Data: map[string]any{"V": &timeTest}},
+			}
 
-	pesticide.RunTestCases(t, time.NewRegistry(), tc)
+			pesticide.RunTestCases(t, rtime.NewRegistry(), tc)
+		})
+	})
 }
 
 func TestDateInZone(t *testing.T) {
-	timeTest := goTime.Date(2024, 5, 7, 15, 4, 5, 0, goTime.UTC)
+	timeTest := time.Date(2024, 5, 7, 15, 4, 5, 0, time.UTC)
 
 	tc := []pesticide.TestCase{
 		{Name: "TestTimeObject", Input: `{{ dateInZone "02 Jan 06 15:04 -0700" .V "UTC" }}`, ExpectedOutput: "07 May 24 15:04 +0000", Data: map[string]any{"V": timeTest}},
@@ -30,11 +106,11 @@ func TestDateInZone(t *testing.T) {
 		{Name: "TestTimeObjectUnix", Input: `{{ dateInZone "02 Jan 06 15:04 -0700" .V "UTC" }}`, ExpectedOutput: "07 May 24 15:04 +0000", Data: map[string]any{"V": timeTest.Unix()}},
 		{Name: "TestTimeObjectUnixInt", Input: `{{ dateInZone "02 Jan 06 15:04 -0700" .V "UTC" }}`, ExpectedOutput: "07 May 24 15:04 +0000", Data: map[string]any{"V": int(timeTest.Unix())}},
 		{Name: "TestTimeObjectUnixInt", Input: `{{ dateInZone "02 Jan 06 15:04 -0700" .V "UTC" }}`, ExpectedOutput: "07 May 24 15:04 +0000", Data: map[string]any{"V": int32(timeTest.Unix())}},
-		{Name: "TestWithInvalidInput", Input: `{{ dateInZone "02 Jan 06 15:04 -0700" .V "UTC" }}`, ExpectedOutput: goTime.Now().Format("02 Jan 06 15:04 -0700"), Data: map[string]any{"V": "invalid"}},
+		{Name: "TestWithInvalidInput", Input: `{{ dateInZone "02 Jan 06 15:04 -0700" .V "UTC" }}`, ExpectedOutput: time.Now().UTC().Format("02 Jan 06 15:04 -0700"), Data: map[string]any{"V": "invalid"}},
 		{Name: "TestWithInvalidZone", Input: `{{ dateInZone "02 Jan 06 15:04 -0700" .V "invalid" }}`, ExpectedErr: "unknown time zone invalid", Data: map[string]any{"V": timeTest}},
 	}
 
-	pesticide.RunTestCases(t, time.NewRegistry(), tc)
+	pesticide.RunTestCases(t, rtime.NewRegistry(), tc)
 }
 
 func TestDuration(t *testing.T) {
@@ -48,11 +124,11 @@ func TestDuration(t *testing.T) {
 		{Name: "TestDurationWithInvalidType", Input: `{{ .V | duration }}`, ExpectedOutput: "0s", Data: map[string]any{"V": make(chan int)}},
 	}
 
-	pesticide.RunTestCases(t, time.NewRegistry(), tc)
+	pesticide.RunTestCases(t, rtime.NewRegistry(), tc)
 }
 
 func TestDateAgo(t *testing.T) {
-	timeTest := goTime.Now().Add(-goTime.Hour * 24)
+	timeTest := time.Now().Add(-time.Hour * 24)
 
 	tc := []pesticide.TestCase{
 		{Name: "TestTimeObject", Input: `{{ .V | dateAgo | substr 0 5 }}`, ExpectedOutput: "24h0m", Data: map[string]any{"V": timeTest}},
@@ -63,29 +139,29 @@ func TestDateAgo(t *testing.T) {
 		{Name: "TestWithInvalidInput", Input: `{{ .V | dateAgo }}`, ExpectedOutput: "0s", Data: map[string]any{"V": "invalid"}},
 	}
 
-	pesticide.RunTestCases(t, time.NewRegistry(), tc)
+	pesticide.RunTestCases(t, rtime.NewRegistry(), tc)
 }
 
 func TestNow(t *testing.T) {
 	tc := []pesticide.TestCase{
-		{Name: "TestNow", Input: `{{ now | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: goTime.Now().Format("02 Jan 06 15:04 -0700")},
+		{Name: "TestNow", Input: `{{ now | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: time.Now().Format("02 Jan 06 15:04 -0700")},
 	}
 
-	pesticide.RunTestCases(t, time.NewRegistry(), tc)
+	pesticide.RunTestCases(t, rtime.NewRegistry(), tc)
 }
 
 func TestUnixEpoch(t *testing.T) {
-	timeTest := goTime.Date(2024, 5, 7, 15, 4, 5, 0, goTime.UTC)
+	timeTest := time.Date(2024, 5, 7, 15, 4, 5, 0, time.UTC)
 
 	tc := []pesticide.TestCase{
 		{Name: "TestUnixEpoch", Input: `{{ .V | unixEpoch }}`, ExpectedOutput: "1715094245", Data: map[string]any{"V": timeTest}},
 	}
 
-	pesticide.RunTestCases(t, time.NewRegistry(), tc)
+	pesticide.RunTestCases(t, rtime.NewRegistry(), tc)
 }
 
 func TestDateModify(t *testing.T) {
-	timeTest := goTime.Date(2024, 5, 7, 15, 4, 5, 0, goTime.UTC)
+	timeTest := time.Date(2024, 5, 7, 15, 4, 5, 0, time.UTC)
 
 	tc := []pesticide.TestCase{
 		{Name: "AddOneHour", Input: `{{ .V | mustDateModify "1h" | date "02 Jan 06 15:04 -0700" }}`, ExpectedOutput: "07 May 24 16:04 +0000", Data: map[string]any{"V": timeTest}},
@@ -97,7 +173,7 @@ func TestDateModify(t *testing.T) {
 		{Name: "WithInvalidInput", Input: `{{ .V | mustDateModify "zz" }}`, Data: map[string]any{"V": timeTest}, ExpectedErr: "invalid duration"},
 	}
 
-	pesticide.RunTestCases(t, time.NewRegistry(), tc)
+	pesticide.RunTestCases(t, rtime.NewRegistry(), tc)
 }
 
 func TestDurationRound(t *testing.T) {
@@ -106,20 +182,20 @@ func TestDurationRound(t *testing.T) {
 		{Name: "RoundToHour", Input: `{{ .V | durationRound }}`, ExpectedOutput: "2h", Data: map[string]any{"V": "2h5s"}},
 		{Name: "RoundToDay", Input: `{{ .V | durationRound }}`, ExpectedOutput: "1d", Data: map[string]any{"V": "24h5s"}},
 		{Name: "RoundToMonth", Input: `{{ .V | durationRound }}`, ExpectedOutput: "3mo", Data: map[string]any{"V": "2400h5s"}},
-		{Name: "RoundToMinute", Input: `{{ .V | durationRound }}`, ExpectedOutput: "45m", Data: map[string]any{"V": int64(45*goTime.Minute + 30*goTime.Second)}},
-		{Name: "RoundToSecond", Input: `{{ .V | durationRound }}`, ExpectedOutput: "1s", Data: map[string]any{"V": int64(1*goTime.Second + 500*goTime.Millisecond)}},
-		{Name: "RoundaDuration", Input: `{{ .V | durationRound }}`, ExpectedOutput: "2s", Data: map[string]any{"V": 2 * goTime.Second}},
-		{Name: "RoundToYear", Input: `{{ .V | durationRound }}`, ExpectedOutput: "1y", Data: map[string]any{"V": int64(365*24*goTime.Hour + 12*goTime.Hour)}},
-		{Name: "RoundToYearNegative", Input: `{{ .V | durationRound }}`, ExpectedOutput: "1y", Data: map[string]any{"V": goTime.Now().Add(-365*24*goTime.Hour - 72*goTime.Hour)}},
+		{Name: "RoundToMinute", Input: `{{ .V | durationRound }}`, ExpectedOutput: "45m", Data: map[string]any{"V": int64(45*time.Minute + 30*time.Second)}},
+		{Name: "RoundToSecond", Input: `{{ .V | durationRound }}`, ExpectedOutput: "1s", Data: map[string]any{"V": int64(1*time.Second + 500*time.Millisecond)}},
+		{Name: "RoundaDuration", Input: `{{ .V | durationRound }}`, ExpectedOutput: "2s", Data: map[string]any{"V": 2 * time.Second}},
+		{Name: "RoundToYear", Input: `{{ .V | durationRound }}`, ExpectedOutput: "1y", Data: map[string]any{"V": int64(365*24*time.Hour + 12*time.Hour)}},
+		{Name: "RoundToYearNegative", Input: `{{ .V | durationRound }}`, ExpectedOutput: "1y", Data: map[string]any{"V": time.Now().Add(-365*24*time.Hour - 72*time.Hour)}},
 		{Name: "InvalidInput", Input: `{{ .V | durationRound }}`, ExpectedOutput: "0s", Data: map[string]any{"V": make(chan int)}},
 		{Name: "RoundToHourNegative", Input: `{{ .V | durationRound }}`, ExpectedOutput: "-1h", Data: map[string]any{"V": "-1h01s"}},
 	}
 
-	pesticide.RunTestCases(t, time.NewRegistry(), tc)
+	pesticide.RunTestCases(t, rtime.NewRegistry(), tc)
 }
 
 func TestHtmlDate(t *testing.T) {
-	timeTest := goTime.Date(2024, 5, 7, 15, 4, 5, 0, goTime.UTC)
+	timeTest := time.Date(2024, 5, 7, 15, 4, 5, 0, time.UTC)
 
 	tc := []pesticide.TestCase{
 		{Name: "TestTimeObject", Input: `{{ .V | htmlDate }}`, ExpectedOutput: "2024-05-07", Data: map[string]any{"V": timeTest}},
@@ -128,14 +204,14 @@ func TestHtmlDate(t *testing.T) {
 		{Name: "TestTimeObjectUnixInt", Input: `{{ .V | htmlDate }}`, ExpectedOutput: "2024-05-07", Data: map[string]any{"V": int(timeTest.Unix())}},
 		{Name: "TestTimeObjectUnixInt32", Input: `{{ .V | htmlDate }}`, ExpectedOutput: "2024-05-07", Data: map[string]any{"V": int32(timeTest.Unix())}},
 		{Name: "TestZeroValue", Input: `{{ .V | htmlDate }}`, ExpectedOutput: "1970-01-01", Data: map[string]any{"V": 0}},
-		{Name: "TestWithInvalidInput", Input: `{{ .V | htmlDate }}`, ExpectedOutput: goTime.Now().Format("2006-01-02"), Data: map[string]any{"V": make(chan int)}},
+		{Name: "TestWithInvalidInput", Input: `{{ .V | htmlDate }}`, ExpectedOutput: time.Now().Format("2006-01-02"), Data: map[string]any{"V": make(chan int)}},
 	}
 
-	pesticide.RunTestCases(t, time.NewRegistry(), tc)
+	pesticide.RunTestCases(t, rtime.NewRegistry(), tc)
 }
 
 func TestHtmlDateInZone(t *testing.T) {
-	timeTest := goTime.Date(2024, 5, 7, 15, 4, 5, 0, goTime.UTC)
+	timeTest := time.Date(2024, 5, 7, 15, 4, 5, 0, time.UTC)
 
 	tc := []pesticide.TestCase{
 		{Name: "TestTimeObject", Input: `{{ htmlDateInZone .V "UTC" }}`, ExpectedOutput: "2024-05-07", Data: map[string]any{"V": timeTest}},
@@ -143,8 +219,8 @@ func TestHtmlDateInZone(t *testing.T) {
 		{Name: "TestTimeObjectUnix", Input: `{{ htmlDateInZone .V "UTC" }}`, ExpectedOutput: "2024-05-07", Data: map[string]any{"V": timeTest.Unix()}},
 		{Name: "TestTimeObjectUnixInt", Input: `{{ htmlDateInZone .V "UTC" }}`, ExpectedOutput: "2024-05-07", Data: map[string]any{"V": int(timeTest.Unix())}},
 		{Name: "TestTimeObjectUnixInt32", Input: `{{ htmlDateInZone .V "UTC" }}`, ExpectedOutput: "2024-05-07", Data: map[string]any{"V": int32(timeTest.Unix())}},
-		{Name: "TestWithInvalidInput", Input: `{{ htmlDateInZone .V "UTC" }}`, ExpectedOutput: goTime.Now().Format("2006-01-02"), Data: map[string]any{"V": make(chan int)}},
+		{Name: "TestWithInvalidInput", Input: `{{ htmlDateInZone .V "UTC" }}`, ExpectedOutput: time.Now().Format("2006-01-02"), Data: map[string]any{"V": make(chan int)}},
 	}
 
-	pesticide.RunTestCases(t, time.NewRegistry(), tc)
+	pesticide.RunTestCases(t, rtime.NewRegistry(), tc)
 }
diff --git a/registry/time/helpers_test.go b/registry/time/helpers_test.go
new file mode 100644
index 0000000..c5ec12f
--- /dev/null
+++ b/registry/time/helpers_test.go
@@ -0,0 +1,58 @@
+package time
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestComputeTimeFromFormat(t *testing.T) {
+	now := time.Now()
+
+	tests := []struct {
+		name string
+		date any
+		want time.Time
+	}{
+		{
+			name: "time.Time",
+			date: now,
+			want: now,
+		},
+		{
+			name: "*time.Time",
+			date: &now,
+			want: now,
+		},
+		{
+			name: "int64",
+			date: int64(1643723900),
+			want: time.Unix(1643723900, 0),
+		},
+		{
+			name: "int",
+			date: 1643723900,
+			want: time.Unix(int64(1643723900), 0),
+		},
+		{
+			name: "int32",
+			date: int32(1643723900),
+			want: time.Unix(int64(1643723900), 0),
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := computeTimeFromFormat(tt.date)
+			assert.Equal(t, tt.want, got)
+		})
+	}
+
+	t.Run("invalid format", func(t *testing.T) {
+		// computeTimeFromFormat generates the current time if the format is invalid
+		got := computeTimeFromFormat("invalid date")
+
+		// so we can only guess the date is close to the current time
+		assert.Less(t, time.Since(got), 10*time.Millisecond)
+	})
+}