tpl: Update Hugo time to support optional [LOCATION] parameter
authorMark Johnson <739719+virgofx@users.noreply.github.com>
Mon, 19 Oct 2020 22:58:05 +0000 (15:58 -0700)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 21 Oct 2020 07:49:25 +0000 (09:49 +0200)
docs/content/en/functions/time.md
tpl/time/init.go
tpl/time/time.go
tpl/time/time_test.go

index 3be2d4368148ab977b51c1c5dcf8021b8e0d9fa6..57d5f65f8810ab593127d79ba5bef0652220703c 100644 (file)
@@ -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.
index 3112999e444bae10004fb3b640191d0d22ca9f0a..7abb36637c644d2f35898be33251055899634d2f 100644 (file)
@@ -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
                        },
                }
 
index 598124648f64307e5173566037617216770d13c4..c3a01003a07760159e997c234ae69e8e005fdeb6 100644 (file)
@@ -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
index 01cf4e03bd2ed53028101b25313e3b06c5b3b1ad..d9e11287816d101254ac073c1b3469bdaafe8688 100644 (file)
@@ -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()