shithub: hugo

Download patch

ref: 26eeb2914720929d2d778f14d6a4bf737014e9e3
parent: b886fa46bb92916152476cfac45c7a5ee5e5820a
author: Mark Johnson <739719+virgofx@users.noreply.github.com>
date: Mon Oct 19 11:58:05 EDT 2020

tpl: Update Hugo time to support optional [LOCATION] parameter

--- a/docs/content/en/functions/time.md
+++ b/docs/content/en/functions/time.md
@@ -10,8 +10,8 @@
 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 @@
 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"
@@ -26,6 +26,18 @@
 {{ (time "2016-05-28").YearDay }} → 149
 {{ 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
 
--- a/tpl/time/init.go
+++ b/tpl/time/init.go
@@ -34,15 +34,26 @@
 				//
 				// 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
 			},
 		}
 
--- a/tpl/time/time.go
+++ b/tpl/time/time.go
@@ -31,13 +31,73 @@
 
 // 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
--- a/tpl/time/time_test.go
+++ b/tpl/time/time_test.go
@@ -18,6 +18,44 @@
 	"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()