From 26eeb2914720929d2d778f14d6a4bf737014e9e3 Mon Sep 17 00:00:00 2001 From: Mark Johnson <739719+virgofx@users.noreply.github.com> Date: Mon, 19 Oct 2020 15:58:05 -0700 Subject: [PATCH] tpl: Update Hugo time to support optional [LOCATION] parameter --- docs/content/en/functions/time.md | 18 +++++++-- tpl/time/init.go | 23 ++++++++--- tpl/time/time.go | 64 ++++++++++++++++++++++++++++++- tpl/time/time_test.go | 38 ++++++++++++++++++ 4 files changed, 132 insertions(+), 11 deletions(-) diff --git a/docs/content/en/functions/time.md b/docs/content/en/functions/time.md index 3be2d4368..57d5f65f8 100644 --- a/docs/content/en/functions/time.md +++ b/docs/content/en/functions/time.md @@ -10,8 +10,8 @@ categories: [functions] menu: docs: parent: "functions" -keywords: [dates,time] -signature: ["time INPUT"] +keywords: [dates,time,location] +signature: ["time INPUT [LOCATION]"] workson: [] hugoversion: relatedfuncs: [] @@ -19,7 +19,7 @@ deprecated: false aliases: [] --- -`time` converts a timestamp string into a [`time.Time`](https://godoc.org/time#Time) structure so you can access its fields: +`time` converts a timestamp string with an optional timezone into a [`time.Time`](https://godoc.org/time#Time) structure so you can access its fields: ``` {{ time "2016-05-28" }} → "2016-05-28T00:00:00Z" @@ -27,6 +27,18 @@ aliases: [] {{ mul 1000 (time "2016-05-28T10:30:00.00+10:00").Unix }} → 1464395400000, or Unix time in milliseconds ``` +## Using Timezone + +The optional 2nd parameter [LOCATION] argument is a string that references a timezone that is associated with the specified time value. If the time value has an explicit timezone or offset specified, it will take precedence over an explicit [LOCATION]. + +``` +{{ time "2020-10-20" }} → 2020-10-20 00:00:00 +0000 UTC +{{ time "2020-10-20" "America/Los_Angeles" }} → 2020-10-20 00:00:00 -0700 PDT +{{ time "2020-01-20" "America/Los_Angeles" }} → 2020-01-20 00:00:00 -0800 PST +``` + +> **Note**: Timezone support via the [LOCATION] parameter is included with Hugo `0.77`. + ## Example: Using `time` to get Month Index The following example takes a UNIX timestamp---set as `utimestamp: "1489276800"` in a content's front matter---converts the timestamp (string) to an integer using the [`int` function][int], and then uses [`printf`][] to convert the `Month` property of `time` into an index. diff --git a/tpl/time/init.go b/tpl/time/init.go index 3112999e4..7abb36637 100644 --- a/tpl/time/init.go +++ b/tpl/time/init.go @@ -34,15 +34,26 @@ func init() { // // If args are passed, call AsTime(). - if len(args) == 0 { + switch len(args) { + case 0: return ctx - } + case 1: + t, err := ctx.AsTime(args[0]) + if err != nil { + return err + } + return t + case 2: + t, err := ctx.AsTime(args[0], args[1]) + if err != nil { + return err + } + return t - t, err := ctx.AsTime(args[0]) - if err != nil { - return err + // 3 or more arguments. Currently not supported. + default: + return "Invalid arguments supplied to `time`. Refer to time documentation: https://gohugo.io/functions/time/" } - return t }, } diff --git a/tpl/time/time.go b/tpl/time/time.go index 598124648..c3a01003a 100644 --- a/tpl/time/time.go +++ b/tpl/time/time.go @@ -31,13 +31,73 @@ type Namespace struct{} // AsTime converts the textual representation of the datetime string into // a time.Time interface. -func (ns *Namespace) AsTime(v interface{}) (interface{}, error) { +func (ns *Namespace) AsTime(v interface{}, args ...interface{}) (interface{}, error) { t, err := cast.ToTimeE(v) if err != nil { return nil, err } - return t, nil + if len(args) == 0 { + return t, nil + } + + // Otherwise, if a location is specified, attempt to parse the time using the location specified. + // Note: In this case, we require the input variable to be a string for proper parsing. + // Note: We can't convert an existing parsed time by using the `Time.In()` as this CONVERTS/MODIFIES + // the resulting time. + + switch givenType := v.(type) { + case string: + // Good, we only support strings + break + + default: + return nil, fmt.Errorf("Creating a time instance with location requires a value of type String. Given type: %s", givenType) + } + + location, err := _time.LoadLocation(args[0].(string)) + if err != nil { + return nil, err + } + + // Note: Cast currently doesn't support time with non-default locations. For now, just inlining this. + // Reference: https://github.com/spf13/cast/pull/80 + + fmts := []string{ + _time.RFC3339, + "2006-01-02T15:04:05", // iso8601 without timezone + _time.RFC1123Z, + _time.RFC1123, + _time.RFC822Z, + _time.RFC822, + _time.RFC850, + _time.ANSIC, + _time.UnixDate, + _time.RubyDate, + "2006-01-02 15:04:05.999999999 -0700 MST", // Time.String() + "2006-01-02", + "02 Jan 2006", + "2006-01-02T15:04:05-0700", // RFC3339 without timezone hh:mm colon + "2006-01-02 15:04:05 -07:00", + "2006-01-02 15:04:05 -0700", + "2006-01-02 15:04:05Z07:00", // RFC3339 without T + "2006-01-02 15:04:05Z0700", // RFC3339 without T or timezone hh:mm colon + "2006-01-02 15:04:05", + _time.Kitchen, + _time.Stamp, + _time.StampMilli, + _time.StampMicro, + _time.StampNano, + } + + for _, dateType := range fmts { + t, err := _time.ParseInLocation(dateType, v.(string), location) + if err == nil { + return t, nil + } + } + + return nil, fmt.Errorf("Unable to ParseInLocation using date \"%s\" with timezone \"%s\"", v, location) } // Format converts the textual representation of the datetime string into diff --git a/tpl/time/time_test.go b/tpl/time/time_test.go index 01cf4e03b..d9e112878 100644 --- a/tpl/time/time_test.go +++ b/tpl/time/time_test.go @@ -18,6 +18,44 @@ import ( "time" ) +func TestTimeLocation(t *testing.T) { + t.Parallel() + + ns := New() + + for i, test := range []struct { + value string + location string + expect interface{} + }{ + {"2020-10-20", "", "2020-10-20 00:00:00 +0000 UTC"}, + {"2020-10-20", "America/New_York", "2020-10-20 00:00:00 -0400 EDT"}, + {"2020-01-20", "America/New_York", "2020-01-20 00:00:00 -0500 EST"}, + {"2020-10-20 20:33:59", "", "2020-10-20 20:33:59 +0000 UTC"}, + {"2020-10-20 20:33:59", "America/New_York", "2020-10-20 20:33:59 -0400 EDT"}, + // The following have an explicit offset specified. In this case, it overrides timezone + {"2020-09-23T20:33:44-0700", "", "2020-09-23 20:33:44 -0700 -0700"}, + {"2020-09-23T20:33:44-0700", "America/New_York", "2020-09-23 20:33:44 -0700 -0700"}, + {"2020-01-20", "invalid-timezone", false}, // unknown time zone invalid-timezone + {"invalid-value", "", false}, + } { + result, err := ns.AsTime(test.value, test.location) + if b, ok := test.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] AsTime didn't return an expected error, got %v", i, result) + } + } else { + if err != nil { + t.Errorf("[%d] AsTime failed: %s", i, err) + continue + } + if result.(time.Time).String() != test.expect { + t.Errorf("[%d] AsTime got %v but expected %v", i, result, test.expect) + } + } + } +} + func TestFormat(t *testing.T) { t.Parallel()