From cbd225bc835ad0523012406893a0cd3843956191 Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Sat, 2 Sep 2017 09:39:16 -0400 Subject: Adding new time funcs Signed-off-by: Dave Henderson --- docs/content/functions/time.md | 164 ++++++++++++++++++++++++++++++++++++++++ funcs.go | 1 + funcs/time.go | 167 +++++++++++++++++++++++++++++++++++++++++ funcs/time_test.go | 60 +++++++++++++++ test/integration/time.bats | 34 +++++++++ time/time.go | 11 +++ 6 files changed, 437 insertions(+) create mode 100644 docs/content/functions/time.md create mode 100644 funcs/time.go create mode 100644 funcs/time_test.go create mode 100644 test/integration/time.bats create mode 100644 time/time.go diff --git a/docs/content/functions/time.md b/docs/content/functions/time.md new file mode 100644 index 00000000..fc6e0bb0 --- /dev/null +++ b/docs/content/functions/time.md @@ -0,0 +1,164 @@ +--- +title: time functions +menu: + main: + parent: functions +--- + +This namespace wraps Go's [`time` package](https://golang.org/pkg/time/), and a +few of the functions return a `time.Time` value. All of the +[`time.Time` functions](https://golang.org/pkg/time/#Time) can then be used to +convert, adjust, or format the time in your template. + +An important difference between this and many other time/date utilities is how +parsing and formatting is accomplished. Instead of relying solely on pre-defined +formats, or having a complex system of variables, formatting is accomplished by +declaring an example of the layout you wish to display. + +This uses a _reference time_, which is: + +``` +Mon Jan 2 15:04:05 -0700 MST 2006 +``` + +### Constants + +#### format layouts + +Some pre-defined layouts have been provided for convenience: + +| layout name | value | +|-------------|-------| +| `time.ANSIC` | `"Mon Jan _2 15:04:05 2006"` | +| `time.UnixDate` | `"Mon Jan _2 15:04:05 MST 2006"` | +| `time.RubyDate` | `"Mon Jan 02 15:04:05 -0700 2006"` | +| `time.RFC822` | `"02 Jan 06 15:04 MST"` | +| `time.RFC822Z` | `"02 Jan 06 15:04 -0700"` // RFC822 with numeric zone | +| `time.RFC850` | `"Monday, 02-Jan-06 15:04:05 MST"` | +| `time.RFC1123` | `"Mon, 02 Jan 2006 15:04:05 MST"` | +| `time.RFC1123Z` | `"Mon, 02 Jan 2006 15:04:05 -0700"` // RFC1123 with numeric zone | +| `time.RFC3339` | `"2006-01-02T15:04:05Z07:00"` | +| `time.RFC3339Nano` | `"2006-01-02T15:04:05.999999999Z07:00"` | +| `time.Kitchen` | `"3:04PM"` | +| `time.Stamp` | `"Jan _2 15:04:05"` | +| `time.StampMilli` | `"Jan _2 15:04:05.000" `| +| `time.StampMicro` | `"Jan _2 15:04:05.000000"` | +| `time.StampNano` | `"Jan _2 15:04:05.000000000"` | + +See below for examples of how these layouts can be used. + +#### durations + +Some operations (such as [`Time.Add`](https://golang.org/pkg/time/#Time.Add) and +[`Time.Round`](https://golang.org/pkg/time/#Time.Round)) require a +[`Duration`](https://golang.org/pkg/time/#Duration) value. These can be created +conveniently with the following functions: + +- `time.Nanosecond` +- `time.Microsecond` +- `time.Millisecond` +- `time.Second` +- `time.Minute` +- `time.Hour` + +For example: + +```console +$ gomplate -i '{{ (time.Now).Format time.Kitchen }} +{{ ((time.Now).Add (time.Hour 2)).Format time.Kitchen }}' +9:05AM +11:05AM +``` + +## `time.Now` + +Returns the current local time, as a `time.Time`. This wraps [`time.Now`](https://golang.org/pkg/time/#Now). + +Usually, further functions are called using the value returned by `Now`. + +### Usage +```go +time.Now +``` + +### Examples + +Usage with [`UTC`](https://golang.org/pkg/time/#Time.UTC) and [`Format`](https://golang.org/pkg/time/#Time.Format): +```console +$ gomplate -i '{{ (time.Now).UTC.Format "Day 2 of month 1 in year 2006 (timezone MST)" }}' +Day 14 of month 10 in year 2017 (timezone UTC) +``` + +Usage with [`AddDate`](https://golang.org/pkg/time/#Time.AddDate): +```console +$ date +Sat Oct 14 09:57:02 EDT 2017 +$ gomplate -i '{{ ((time.Now).AddDate 0 1 0).Format "Mon Jan 2 15:04:05 MST 2006" }}' +Tue Nov 14 09:57:02 EST 2017 +``` + +_(notice how the TZ adjusted for daylight savings!)_ + +## `time.Parse` + +Parses a timestamp defined by the given layout. This wraps [`time.Parse`](https://golang.org/pkg/time/#Parse). + +A number of pre-defined layouts are provided as constants, defined +[here](https://golang.org/pkg/time/#pkg-constants). + +Just like [`time.Now`](#time-now), this is usually used in conjunction with +other functions. + +### Usage +```go +time.Parse layout timestamp +``` + +### Examples + +Usage with [`Format`](https://golang.org/pkg/time/#Time.Format): +```console +$ gomplate -i '{{ (time.Parse "2006-01-02" "1993-10-23").Format "Monday January 2, 2006" }}' +Saturday October 23, 1993 +``` + +## `time.Unix` + +Returns the local `Time` corresponding to the given Unix time, in seconds since +January 1, 1970 UTC. Note that fractional seconds can be used to denote +milliseconds, but must be specified as a string, not a floating point number. + +### Usage +```go +time.Unix time +``` + +### Example + +_with whole seconds:_ +```console +$ gomplate -i '{{ (time.Unix 42).UTC.Format time.Stamp}}' +Jan 1, 00:00:42 +``` + +_with fractional seconds:_ +```console +$ gomplate -i '{{ (time.Unix "123456.789").UTC.Format time.StampMilli}}' +Jan 2 10:17:36.789 +``` + +## `time.ZoneName` + +Return the local system's time zone's name. + +### Usage +```go +time.ZoneName +``` + +### Example + +```console +$ gomplate -i '{{time.ZoneName}}' +EDT +``` diff --git a/funcs.go b/funcs.go index f4a5d6fc..28bfc907 100644 --- a/funcs.go +++ b/funcs.go @@ -18,5 +18,6 @@ func initFuncs(d *data.Data) template.FuncMap { funcs.AddStringFuncs(f) funcs.AddEnvFuncs(f) funcs.AddConvFuncs(f) + funcs.AddTimeFuncs(f) return f } diff --git a/funcs/time.go b/funcs/time.go new file mode 100644 index 00000000..4515f4e8 --- /dev/null +++ b/funcs/time.go @@ -0,0 +1,167 @@ +package funcs + +import ( + "fmt" + "strconv" + "strings" + "sync" + gotime "time" + + "github.com/hairyhenderson/gomplate/time" +) + +var ( + timeNS *TimeFuncs + timeNSInit sync.Once +) + +// TimeNS - +func TimeNS() *TimeFuncs { + timeNSInit.Do(func() { + timeNS = &TimeFuncs{ + ANSIC: gotime.ANSIC, + UnixDate: gotime.UnixDate, + RubyDate: gotime.RubyDate, + RFC822: gotime.RFC822, + RFC822Z: gotime.RFC822Z, + RFC850: gotime.RFC850, + RFC1123: gotime.RFC1123, + RFC1123Z: gotime.RFC1123Z, + RFC3339: gotime.RFC3339, + RFC3339Nano: gotime.RFC3339Nano, + Kitchen: gotime.Kitchen, + Stamp: gotime.Stamp, + StampMilli: gotime.StampMilli, + StampMicro: gotime.StampMicro, + StampNano: gotime.StampNano, + } + }) + return timeNS +} + +// AddTimeFuncs - +func AddTimeFuncs(f map[string]interface{}) { + f["time"] = TimeNS +} + +// TimeFuncs - +type TimeFuncs struct { + ANSIC string + UnixDate string + RubyDate string + RFC822 string + RFC822Z string + RFC850 string + RFC1123 string + RFC1123Z string + RFC3339 string + RFC3339Nano string + Kitchen string + Stamp string + StampMilli string + StampMicro string + StampNano string +} + +// ZoneName - return the local system's time zone's name +func (f *TimeFuncs) ZoneName() string { + return time.ZoneName() +} + +// Parse - +func (f *TimeFuncs) Parse(layout, value string) (gotime.Time, error) { + return gotime.Parse(layout, value) +} + +// Now - +func (f *TimeFuncs) Now() gotime.Time { + return gotime.Now() +} + +// Unix - convert UNIX time (in seconds since the UNIX epoch) into a time.Time for further processing +// Takes a string or number (int or float) +func (f *TimeFuncs) Unix(in interface{}) (gotime.Time, error) { + sec, nsec, err := parseNum(in) + if err != nil { + return gotime.Time{}, err + } + return gotime.Unix(sec, nsec), nil +} + +// Nanosecond - +func (f *TimeFuncs) Nanosecond(n int64) gotime.Duration { + return gotime.Nanosecond * gotime.Duration(n) +} + +// Microsecond - +func (f *TimeFuncs) Microsecond(n int64) gotime.Duration { + return gotime.Microsecond * gotime.Duration(n) +} + +// Millisecond - +func (f *TimeFuncs) Millisecond(n int64) gotime.Duration { + return gotime.Millisecond * gotime.Duration(n) +} + +// Second - +func (f *TimeFuncs) Second(n int64) gotime.Duration { + return gotime.Second * gotime.Duration(n) +} + +// Minute - +func (f *TimeFuncs) Minute(n int64) gotime.Duration { + return gotime.Minute * gotime.Duration(n) +} + +// Hour - +func (f *TimeFuncs) Hour(n int64) gotime.Duration { + return gotime.Hour * gotime.Duration(n) +} + +// convert a number input to a pair of int64s, representing the integer portion and the decimal remainder +// this can handle a string as well as any integer or float type +// precision is at the "nano" level (i.e. 1e+9) +func parseNum(in interface{}) (integral int64, fractional int64, err error) { + if s, ok := in.(string); ok { + ss := strings.Split(s, ".") + if len(ss) > 2 { + return 0, 0, fmt.Errorf("can not parse '%s' as a number - too many decimal points", s) + } + if len(ss) == 1 { + integral, err := strconv.ParseInt(s, 0, 64) + return integral, 0, err + } + integral, err := strconv.ParseInt(ss[0], 0, 64) + if err != nil { + return integral, 0, err + } + fractional, err = strconv.ParseInt(padRight(ss[1], "0", 9), 0, 64) + return integral, fractional, err + } + if s, ok := in.(fmt.Stringer); ok { + return parseNum(s.String()) + } + if i, ok := in.(int); ok { + return int64(i), 0, nil + } + if u, ok := in.(uint64); ok { + return int64(u), 0, nil + } + if f, ok := in.(float64); ok { + return 0, 0, fmt.Errorf("can not parse floating point number (%f) - use a string instead", f) + } + if in == nil { + return 0, 0, nil + } + return 0, 0, nil +} + +// pads a number with zeroes +func padRight(in, pad string, length int) string { + for { + in += pad + if len(in) > length { + return in[0:length] + } + } +} diff --git a/funcs/time_test.go b/funcs/time_test.go new file mode 100644 index 00000000..99ab1e34 --- /dev/null +++ b/funcs/time_test.go @@ -0,0 +1,60 @@ +package funcs + +import ( + "math" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseNum(t *testing.T) { + i, f, _ := parseNum("42") + assert.Equal(t, int64(42), i) + assert.Equal(t, int64(0), f) + + i, f, _ = parseNum(42) + assert.Equal(t, int64(42), i) + assert.Equal(t, int64(0), f) + + i, f, _ = parseNum(big.NewInt(42)) + assert.Equal(t, int64(42), i) + assert.Equal(t, int64(0), f) + + i, f, _ = parseNum(big.NewFloat(42.0)) + assert.Equal(t, int64(42), i) + assert.Equal(t, int64(0), f) + + i, f, _ = parseNum(uint64(math.MaxInt64)) + assert.Equal(t, int64(uint64(math.MaxInt64)), i) + assert.Equal(t, int64(0), f) + + i, f, _ = parseNum("9223372036854775807.999999999") + assert.Equal(t, int64(9223372036854775807), i) + assert.Equal(t, int64(999999999), f) + + i, f, _ = parseNum("999999999999999.123456789123") + assert.Equal(t, int64(999999999999999), i) + assert.Equal(t, int64(123456789), f) + + i, f, _ = parseNum("123456.789") + assert.Equal(t, int64(123456), i) + assert.Equal(t, int64(789000000), f) + + _, _, err := parseNum("bogus.9223372036854775807") + assert.Error(t, err) + + _, _, err = parseNum("bogus") + assert.Error(t, err) + + _, _, err = parseNum("1.2.3") + assert.Error(t, err) + + _, _, err = parseNum(1.1) + assert.Error(t, err) + + i, f, err = parseNum(nil) + assert.Zero(t, i) + assert.Zero(t, f) + assert.NoError(t, err) +} diff --git a/test/integration/time.bats b/test/integration/time.bats new file mode 100644 index 00000000..17fe40da --- /dev/null +++ b/test/integration/time.bats @@ -0,0 +1,34 @@ +#!/usr/bin/env bats + +load helper + +@test "'time.ZoneName'" { + gomplate -i '{{ time.ZoneName }}' + [ "$status" -eq 0 ] + [[ "${output}" == `date +"%Z"` ]] +} + +@test "'(time.Now).Format'" { + gomplate -i '{{ (time.Now).Format "2006-01-02 15 -0700" }}' + [ "$status" -eq 0 ] + [[ "${output}" == `date +"%Y-%m-%d %H %z"` ]] +} + +@test "'(time.Parse).Format'" { + in=`date -u --date='@1234567890'` + gomplate -i "{{ (time.Parse \"Mon Jan 02 15:04:05 MST 2006\" \"${in}\").Format \"2006-01-02 15 -0700\" }}" + [ "$status" -eq 0 ] + [[ "${output}" == "2009-02-13 23 +0000" ]] +} + +@test "'(time.Unix).UTC.Format' int" { + gomplate -i '{{ (time.Unix 1234567890).UTC.Format "2006-01-02 15 -0700" }}' + [ "$status" -eq 0 ] + [[ "${output}" == "2009-02-13 23 +0000" ]] +} + +@test "'(time.Unix).UTC.Format' string" { + gomplate -i '{{ (time.Unix "1234567890").UTC.Format "2006-01-02 15 -0700" }}' + [ "$status" -eq 0 ] + [[ "${output}" == "2009-02-13 23 +0000" ]] +} diff --git a/time/time.go b/time/time.go new file mode 100644 index 00000000..e1241d3f --- /dev/null +++ b/time/time.go @@ -0,0 +1,11 @@ +package time + +import ( + "time" +) + +// ZoneName - a convenience function for determining the current timezone's name +func ZoneName() string { + n, _ := time.Now().Zone() + return n +} -- cgit v1.2.3