From efc48942a52a2b42abd9e4c26a8279567eecd6ef Mon Sep 17 00:00:00 2001
From: Carrie Edwards <edwrdscarrie@gmail.com>
Date: Wed, 5 Oct 2022 09:58:33 -0700
Subject: [PATCH 1/5] Add time parsing that supports absolute and relative time

---
 pkg/parser/time_parser.go | 241 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 241 insertions(+)
 create mode 100644 pkg/parser/time_parser.go

diff --git a/pkg/parser/time_parser.go b/pkg/parser/time_parser.go
new file mode 100644
index 000000000..13db18300
--- /dev/null
+++ b/pkg/parser/time_parser.go
@@ -0,0 +1,241 @@
+package parser
+
+import (
+	"fmt"
+	"github.com/leekchan/timeutil"
+	"strconv"
+	"strings"
+	"time"
+)
+
+var months = []string{"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"}
+var weekdays = []string{"sun", "mon", "tue", "wed", "thu", "fri", "sat"}
+
+func ParseDateTime(dateTime string, defaultSign int) (int64, error) {
+	var parsed []string
+	var offset string
+	var ref string
+	r := strings.NewReplacer(
+		" ", "",
+		",", "",
+		"_", "",
+	)
+	parsedTime := strings.TrimSpace(dateTime)
+	parsedTime = r.Replace(parsedTime)
+
+	val, err := strconv.Atoi(parsedTime)
+	if err == nil {
+		year, _ := strconv.Atoi(parsedTime[:4])
+		month, _ := strconv.Atoi(parsedTime[4:6])
+		day, _ := strconv.Atoi(parsedTime[6:])
+		if len(parsedTime) != 8 || year < 1900 || month > 13 || day > 32 {
+			return int64(val), nil
+		}
+	}
+	if strings.Contains(parsedTime, "-") {
+		parsed = strings.SplitN(parsedTime, "-", 2)
+		offset = "-" + parsed[1]
+		ref = parsed[0]
+	} else if strings.Contains(parsedTime, "+") {
+		parsed = strings.SplitN(parsedTime, "+", 2)
+		offset = "+" + parsed[1]
+		ref = parsed[0]
+	} else {
+		offset = ""
+		ref = parsedTime
+	}
+
+	refTime, _ := parseTimeReference(ref)
+	interval, _ := parseInterval(offset, defaultSign)
+
+	total := refTime + interval
+	return total, nil
+
+}
+
+func parseTimeReference(ref string) (int64, error) {
+	if ref == "" {
+		return 0, nil
+	}
+	var rawRef = ref
+	var err error
+	var refDate time.Time
+
+	refDate, _ = getReferenceDate(ref)
+
+	// Day reference
+	if strings.Contains(ref, "today") || strings.Contains(ref, "yesterday") || strings.Contains(ref, "tomorrow") {
+		if strings.Contains(ref, "yesterday") {
+			refDate = refDate.AddDate(0, 0, -1)
+		} else if strings.Contains(ref, "tomorrow") {
+			refDate = time.Now().AddDate(0, 0, 1)
+		}
+	} else if strings.Count(ref, "/") == 2 { // MM/DD/YY format
+		refDate, err = time.Parse("2006/01/02", ref)
+		if err != nil {
+			return 0, err
+		}
+	} else if _, err := strconv.Atoi(ref); err == nil && len(ref) == 8 { // YYYYMMDD format
+		refDate, err = time.Parse("20060102", ref)
+		if err != nil {
+			return 0, err
+		}
+	} else if len(ref) >= 3 && stringMatchesList(ref[:3], months) { // MonthName DayOfMonth
+		var day int
+		if val, err := strconv.Atoi(ref[(len(ref) - 2):]); err == nil {
+			day = val
+		} else if val, err := strconv.Atoi(ref[(len(ref) - 1):]); err == nil {
+			day = val
+		} else {
+			return 0, fmt.Errorf("Day of month required after month name: %s", rawRef)
+		}
+		refDate = refDate.AddDate(0, 0, day)
+	} else if len(ref) >= 3 && stringMatchesList(ref[:3], weekdays) { // DayOfWeek (Monday, etc)
+		dayName := timeutil.Strftime(&refDate, "%a")
+		dayName = strings.ToLower(dayName)
+		today := stringMatchesListIndex(dayName, weekdays)
+		twoWeeks := append(weekdays, weekdays...)
+		dayOffset := today - stringMatchesListIndex(ref[:3], twoWeeks)
+		if dayOffset < 0 {
+			dayOffset += 7
+		}
+		refDate = refDate.AddDate(0, 0, -(dayOffset))
+	} else if ref == "" {
+		return 0, fmt.Errorf("Unknown day reference: %s", rawRef)
+	}
+
+	return refDate.Unix(), nil
+}
+
+// IntervalString converts a sign and string into a number of seconds
+func parseInterval(s string, defaultSign int) (int64, error) {
+	if len(s) == 0 {
+		return 0, nil
+	}
+	sign := defaultSign
+
+	switch s[0] {
+	case '-':
+		sign = -1
+		s = s[1:]
+	case '+':
+		sign = 1
+		s = s[1:]
+	}
+
+	var totalInterval int64
+	for len(s) > 0 {
+		var j int
+		for j < len(s) && '0' <= s[j] && s[j] <= '9' {
+			j++
+		}
+		var offsetStr string
+		offsetStr, s = s[:j], s[j:]
+
+		j = 0
+		for j < len(s) && (s[j] < '0' || '9' < s[j]) {
+			j++
+		}
+		var unitStr string
+		unitStr, s = s[:j], s[j:]
+
+		var units int
+		switch unitStr {
+		case "s", "sec", "secs", "second", "seconds":
+			units = 1
+		case "m", "min", "mins", "minute", "minutes":
+			units = 60
+		case "h", "hour", "hours":
+			units = 60 * 60
+		case "d", "day", "days":
+			units = 24 * 60 * 60
+		case "w", "week", "weeks":
+			units = 7 * 24 * 60 * 60
+		case "mon", "month", "months":
+			units = 30 * 24 * 60 * 60
+		case "y", "year", "years":
+			units = 365 * 24 * 60 * 60
+		default:
+			return 0, ErrUnknownTimeUnits
+		}
+
+		offset, err := strconv.Atoi(offsetStr)
+		if err != nil {
+			return 0, err
+		}
+		totalInterval += int64(sign * offset * units)
+	}
+
+	return totalInterval, nil
+}
+
+func stringMatchesList(a string, list []string) bool {
+	for _, b := range list {
+		if b == a {
+			return true
+		}
+	}
+	return false
+}
+
+func getReferenceDate(ref string) (time.Time, string) {
+	// Time-of-day reference
+	var hour = 0
+	var minute = 0
+	i := strings.Index(ref, ":")
+	if i > 0 && i < 3 {
+		hour, _ = strconv.Atoi(ref[:i])
+		minute, _ = strconv.Atoi(ref[i+1 : i+3])
+		ref = ref[i+3:]
+		if ref[:2] == "am" {
+			ref = ref[2:]
+		} else if ref[:2] == "pm" {
+			hour = (hour + 12) % 24
+			ref = ref[2:]
+		}
+	}
+
+	// X am or XXam
+	i = strings.Index(ref, "am")
+	if i > 0 && i < 3 {
+		hour, _ = strconv.Atoi(ref[:i])
+		ref = ref[i+2:]
+	}
+
+	// X pm or XX pm
+	i = strings.Index(ref, "pm")
+	if i > 0 && i < 3 {
+		hr, _ := strconv.Atoi(ref[:i])
+		hour = (hr + 12) % 24
+		ref = ref[i+2:]
+	}
+
+	if strings.HasPrefix(ref, "noon") {
+		hour = 12
+		minute = 0
+		ref = ref[4:]
+	} else if strings.HasPrefix(ref, "midnight") {
+		hour = 0
+		minute = 0
+		ref = ref[8:]
+	} else if strings.HasPrefix(ref, "teatime") {
+		hour = 16
+		minute = 16
+		ref = ref[7:]
+	}
+
+	now := time.Now().UTC()
+	timeZone := time.UTC
+	refDate := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, timeZone)
+
+	return refDate, ref
+}
+
+func stringMatchesListIndex(a string, list []string) int {
+	for i, b := range list {
+		if b == a {
+			return i
+		}
+	}
+	return -1
+}

From f399c0d79a14112b16bee2a29c70ae408b1a1d8a Mon Sep 17 00:00:00 2001
From: Carrie Edwards <edwrdscarrie@gmail.com>
Date: Wed, 5 Oct 2022 10:25:27 -0700
Subject: [PATCH 2/5] Update dependencies

---
 go.mod             | 1 +
 go.sum             | 2 ++
 vendor/modules.txt | 3 +++
 3 files changed, 6 insertions(+)

diff --git a/go.mod b/go.mod
index 14ea36417..298291c20 100644
--- a/go.mod
+++ b/go.mod
@@ -22,6 +22,7 @@ require (
 	github.com/google/flatbuffers v2.0.6+incompatible // indirect
 	github.com/google/uuid v1.3.0 // indirect
 	github.com/gorilla/handlers v1.5.1
+	github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d
 	github.com/lib/pq v1.10.6
 	github.com/lomik/og-rek v0.0.0-20170411191824-628eefeb8d80
 	github.com/lomik/zapwriter v0.0.0-20210624082824-c1161d1eb463
diff --git a/go.sum b/go.sum
index 556211458..6dd260335 100644
--- a/go.sum
+++ b/go.sum
@@ -361,6 +361,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d h1:2puqoOQwi3Ai1oznMOsFIbifm6kIfJaLLyYzWD4IzTs=
+github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d/go.mod h1:hO90vCP2x3exaSH58BIAowSKvV+0OsY21TtzuFGHON4=
 github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
 github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lomik/og-rek v0.0.0-20170411191824-628eefeb8d80 h1:KVyDGUXjVOdHQt24wIgY4ZdGFXHtQHLWw0L/MAK3Kb0=
diff --git a/vendor/modules.txt b/vendor/modules.txt
index eb29b2d38..cfe19e8e0 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -94,6 +94,9 @@ github.com/hashicorp/hcl/hcl/token
 github.com/hashicorp/hcl/json/parser
 github.com/hashicorp/hcl/json/scanner
 github.com/hashicorp/hcl/json/token
+# github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d
+## explicit
+github.com/leekchan/timeutil
 # github.com/lib/pq v1.10.6
 ## explicit
 github.com/lib/pq

From d4446a16c5b4d99ecaa7b1073841bdddb9a0e1e5 Mon Sep 17 00:00:00 2001
From: Carrie Edwards <edwrdscarrie@gmail.com>
Date: Wed, 5 Oct 2022 14:47:58 -0700
Subject: [PATCH 3/5] Fix bugs in parsing dates/times

---
 pkg/parser/time_parser.go | 79 +++++++++++++++++++++++++++------------
 1 file changed, 55 insertions(+), 24 deletions(-)

diff --git a/pkg/parser/time_parser.go b/pkg/parser/time_parser.go
index 13db18300..681693ab5 100644
--- a/pkg/parser/time_parser.go
+++ b/pkg/parser/time_parser.go
@@ -11,7 +11,16 @@ import (
 var months = []string{"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"}
 var weekdays = []string{"sun", "mon", "tue", "wed", "thu", "fri", "sat"}
 
-func ParseDateTime(dateTime string, defaultSign int) (int64, error) {
+type dateTime struct {
+	year        int
+	month       int
+	day         int
+	seconds     int
+	nanoseconds int
+	location    time.Location
+}
+
+func ParseDateTime(dateTime string, defaultSign int, now time.Time) (int64, error) {
 	var parsed []string
 	var offset string
 	var ref string
@@ -45,7 +54,7 @@ func ParseDateTime(dateTime string, defaultSign int) (int64, error) {
 		ref = parsedTime
 	}
 
-	refTime, _ := parseTimeReference(ref)
+	refTime, _ := parseTimeReference(ref, now)
 	interval, _ := parseInterval(offset, defaultSign)
 
 	total := refTime + interval
@@ -53,33 +62,47 @@ func ParseDateTime(dateTime string, defaultSign int) (int64, error) {
 
 }
 
-func parseTimeReference(ref string) (int64, error) {
-	if ref == "" {
-		return 0, nil
+func parseTimeReference(ref string, now time.Time) (int64, error) {
+	if ref == "" || ref == "now" {
+		return now.Unix(), nil
 	}
+
+	var hour int
+	var minute int
 	var rawRef = ref
-	var err error
-	var refDate time.Time
+	//var err error
+	var refDate = now
 
-	refDate, _ = getReferenceDate(ref)
+	hour, minute, ref = getReferenceDate(ref)
+	if ref == "" {
+		refDate = time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, time.UTC)
+		return refDate.Unix(), nil
+	}
 
 	// Day reference
 	if strings.Contains(ref, "today") || strings.Contains(ref, "yesterday") || strings.Contains(ref, "tomorrow") {
 		if strings.Contains(ref, "yesterday") {
-			refDate = refDate.AddDate(0, 0, -1)
+			refDate = time.Date(now.Year(), now.Month(), -1, hour, minute, 0, 0, time.UTC)
 		} else if strings.Contains(ref, "tomorrow") {
-			refDate = time.Now().AddDate(0, 0, 1)
+			refDate = time.Date(now.Year(), now.Month(), 1, hour, minute, 0, 0, time.UTC)
 		}
-	} else if strings.Count(ref, "/") == 2 { // MM/DD/YY format
-		refDate, err = time.Parse("2006/01/02", ref)
-		if err != nil {
-			return 0, err
+	} else if strings.Count(ref, "/") == 2 { // MM/DD/YY[YY] format
+		parsed := strings.SplitN(ref, "/", 3)
+		year, _ := strconv.Atoi(parsed[2])
+		month, _ := strconv.Atoi(parsed[0])
+		day, _ := strconv.Atoi(parsed[1])
+		if year < 1900 {
+			year += 1900
 		}
-	} else if _, err := strconv.Atoi(ref); err == nil && len(ref) == 8 { // YYYYMMDD format
-		refDate, err = time.Parse("20060102", ref)
-		if err != nil {
-			return 0, err
+		if year < 1970 {
+			year += 100
 		}
+		refDate = time.Date(year, time.Month(month), day, hour, minute, 0, 0, time.UTC)
+	} else if _, err := strconv.Atoi(ref); err == nil && len(ref) == 8 { // YYYYMMDD format
+		year, _ := strconv.Atoi(ref[:4])
+		month, _ := strconv.Atoi(ref[4:6])
+		day, _ := strconv.Atoi(ref[6:])
+		refDate = time.Date(year, time.Month(month), day, hour, minute, 0, 0, time.UTC)
 	} else if len(ref) >= 3 && stringMatchesList(ref[:3], months) { // MonthName DayOfMonth
 		var day int
 		if val, err := strconv.Atoi(ref[(len(ref) - 2):]); err == nil {
@@ -100,7 +123,7 @@ func parseTimeReference(ref string) (int64, error) {
 			dayOffset += 7
 		}
 		refDate = refDate.AddDate(0, 0, -(dayOffset))
-	} else if ref == "" {
+	} else {
 		return 0, fmt.Errorf("Unknown day reference: %s", rawRef)
 	}
 
@@ -178,7 +201,7 @@ func stringMatchesList(a string, list []string) bool {
 	return false
 }
 
-func getReferenceDate(ref string) (time.Time, string) {
+func getReferenceDate(ref string) (hr int, min int, remaining string) {
 	// Time-of-day reference
 	var hour = 0
 	var minute = 0
@@ -187,7 +210,9 @@ func getReferenceDate(ref string) (time.Time, string) {
 		hour, _ = strconv.Atoi(ref[:i])
 		minute, _ = strconv.Atoi(ref[i+1 : i+3])
 		ref = ref[i+3:]
-		if ref[:2] == "am" {
+		if ref == "" {
+			return hour, minute, ref
+		} else if ref[:2] == "am" {
 			ref = ref[2:]
 		} else if ref[:2] == "pm" {
 			hour = (hour + 12) % 24
@@ -224,11 +249,17 @@ func getReferenceDate(ref string) (time.Time, string) {
 		ref = ref[7:]
 	}
 
+	//now := time.Now().UTC()
+	//timeZone := time.UTC
+	//refDate := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, timeZone)
+
+	return hour, minute, ref
+}
+
+func setHourMinute(hour, minute int) time.Time {
 	now := time.Now().UTC()
 	timeZone := time.UTC
-	refDate := time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, timeZone)
-
-	return refDate, ref
+	return time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, timeZone)
 }
 
 func stringMatchesListIndex(a string, list []string) int {

From 1a651fa5b4072333582b98c493e5ee6334227d1c Mon Sep 17 00:00:00 2001
From: Carrie Edwards <edwrdscarrie@gmail.com>
Date: Wed, 5 Oct 2022 14:48:10 -0700
Subject: [PATCH 4/5] Add test

---
 pkg/parser/time_parser_test.go | 150 +++++++++++++++++++++++++++++++++
 1 file changed, 150 insertions(+)
 create mode 100644 pkg/parser/time_parser_test.go

diff --git a/pkg/parser/time_parser_test.go b/pkg/parser/time_parser_test.go
new file mode 100644
index 000000000..b67293ecf
--- /dev/null
+++ b/pkg/parser/time_parser_test.go
@@ -0,0 +1,150 @@
+package parser
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestParseTime(t *testing.T) {
+	now := time.Date(2015, time.Month(3), 8, 12, 0, 0, 0, time.UTC)
+	tests := []struct {
+		s              string
+		expectedResult int64
+	}{
+		{
+			s:              "12:0020150308",
+			expectedResult: 1425816000,
+		},
+		{
+			s:              "9:0020150308",
+			expectedResult: 1425805200,
+		},
+		{
+			s:              "20150110",
+			expectedResult: 1420848000,
+		},
+		{
+			s:              "midnight",
+			expectedResult: 1425772800,
+		},
+		{
+			s:              "midnight+1h",
+			expectedResult: 1425776400,
+		},
+		{
+			s:              "midnight_tomorrow",
+			expectedResult: 1425168000,
+		},
+		{
+			s:              "midnight_tomorrow+3h",
+			expectedResult: 1425178800,
+		},
+		{
+			s:              "now",
+			expectedResult: 1425816000,
+		},
+		{
+			s:              "-1h",
+			expectedResult: 1425812400,
+		},
+		{
+			s:              "8:50",
+			expectedResult: 1425804600,
+		},
+		{
+			s:              "8:50am",
+			expectedResult: 1425804600,
+		},
+		{
+			s:              "8:50pm",
+			expectedResult: 1425847800,
+		},
+		{
+			s:              "8am",
+			expectedResult: 1425801600,
+		},
+		{
+			s:              "10pm",
+			expectedResult: 1425852000,
+		},
+		{
+			s:              "noon",
+			expectedResult: 1425816000,
+		},
+		{
+			s:              "midnight",
+			expectedResult: 1425772800,
+		},
+		{
+			s:              "teatime",
+			expectedResult: 1425831360,
+		},
+		{
+			s:              "yesterday",
+			expectedResult: 1424995200,
+		},
+		{
+			s:              "today",
+			expectedResult: 1425816000,
+		},
+		{
+			s:              "tomorrow",
+			expectedResult: 1425168000,
+		},
+		{
+			s:              "02/25/15",
+			expectedResult: 1424822400,
+		},
+		{
+			s:              "20140606",
+			expectedResult: 1402012800,
+		},
+		{
+			s:              "january8",
+			expectedResult: 1426507200,
+		},
+		{
+			s:              "january10",
+			expectedResult: 1426680000,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.s, func(t *testing.T) {
+			assert := assert.New(t)
+
+			time, err := ParseDateTime(tt.s, -1, now)
+			assert.NoError(err)
+			fmt.Println("time: ", time)
+			assert.Equal(tt.expectedResult, time, tt.s)
+		})
+	}
+}
+
+func testInvalidTimes(t *testing.T) {
+	now := time.Date(2015, time.Month(3), 8, 12, 0, 0, 0, time.UTC)
+
+	tests := []struct {
+		s              string
+		expectedResult int64
+	}{
+		{
+			s:              "12:0020150308",
+			expectedResult: 1664985600,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.s, func(t *testing.T) {
+			assert := assert.New(t)
+
+			time, err := ParseDateTime(tt.s, -1, now)
+			assert.NoError(err)
+			fmt.Println("time: ", time)
+			assert.Equal(tt.expectedResult, time, tt.s)
+		})
+	}
+}

From d4d7335c847190192406330714db64b9538bbb1a Mon Sep 17 00:00:00 2001
From: Carrie Edwards <edwrdscarrie@gmail.com>
Date: Wed, 5 Oct 2022 15:04:20 -0700
Subject: [PATCH 5/5] Add dependency

---
 .../github.com/leekchan/timeutil/.gitignore   |  24 ++
 .../github.com/leekchan/timeutil/.travis.yml  |   7 +
 vendor/github.com/leekchan/timeutil/LICENSE   |  22 ++
 vendor/github.com/leekchan/timeutil/README.md | 225 ++++++++++++++++++
 .../github.com/leekchan/timeutil/strftime.go  | 157 ++++++++++++
 .../github.com/leekchan/timeutil/timedelta.go |  73 ++++++
 6 files changed, 508 insertions(+)
 create mode 100644 vendor/github.com/leekchan/timeutil/.gitignore
 create mode 100644 vendor/github.com/leekchan/timeutil/.travis.yml
 create mode 100644 vendor/github.com/leekchan/timeutil/LICENSE
 create mode 100644 vendor/github.com/leekchan/timeutil/README.md
 create mode 100644 vendor/github.com/leekchan/timeutil/strftime.go
 create mode 100644 vendor/github.com/leekchan/timeutil/timedelta.go

diff --git a/vendor/github.com/leekchan/timeutil/.gitignore b/vendor/github.com/leekchan/timeutil/.gitignore
new file mode 100644
index 000000000..daf913b1b
--- /dev/null
+++ b/vendor/github.com/leekchan/timeutil/.gitignore
@@ -0,0 +1,24 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+*.test
+*.prof
diff --git a/vendor/github.com/leekchan/timeutil/.travis.yml b/vendor/github.com/leekchan/timeutil/.travis.yml
new file mode 100644
index 000000000..b50016540
--- /dev/null
+++ b/vendor/github.com/leekchan/timeutil/.travis.yml
@@ -0,0 +1,7 @@
+language: go
+go:
+  - tip
+before_install:
+  - go get github.com/axw/gocov/gocov
+  - go get github.com/mattn/goveralls
+  - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi
\ No newline at end of file
diff --git a/vendor/github.com/leekchan/timeutil/LICENSE b/vendor/github.com/leekchan/timeutil/LICENSE
new file mode 100644
index 000000000..244f4d8ed
--- /dev/null
+++ b/vendor/github.com/leekchan/timeutil/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Kyoung-chan Lee (leekchan@gmail.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/vendor/github.com/leekchan/timeutil/README.md b/vendor/github.com/leekchan/timeutil/README.md
new file mode 100644
index 000000000..48900257c
--- /dev/null
+++ b/vendor/github.com/leekchan/timeutil/README.md
@@ -0,0 +1,225 @@
+# timeutil - useful extensions to the golang's time package
+[![Build Status](https://travis-ci.org/leekchan/timeutil.svg?branch=master)](https://travis-ci.org/leekchan/timeutil)
+[![Coverage Status](https://coveralls.io/repos/leekchan/timeutil/badge.svg?branch=master&service=github)](https://coveralls.io/github/leekchan/timeutil?branch=master)
+[![GoDoc](https://godoc.org/github.com/leekchan/timeutil?status.svg)](https://godoc.org/github.com/leekchan/timeutil)
+
+timeutil provides useful extensions (Timedelta, Strftime, ...) to the golang's time package.
+
+
+## Quick Start
+
+```
+go get github.com/leekchan/timeutil
+```
+
+example.go
+
+```Go
+package main
+
+import (
+    "fmt"
+    "time"
+
+    "github.com/leekchan/timeutil"
+)
+
+func main() {
+    // Timedelta
+    // A basic usage.
+    base := time.Date(2015, 2, 3, 0, 0, 0, 0, time.UTC)
+    td := timeutil.Timedelta{Days: 10, Minutes: 17, Seconds: 56}
+
+    result := base.Add(td.Duration())
+    fmt.Println(result) // "2015-02-13 00:17:56 +0000 UTC"
+
+    // Operation : Add
+    base = time.Date(2015, 2, 3, 0, 0, 0, 0, time.UTC)
+
+    td = timeutil.Timedelta{Days: 1, Minutes: 1, Seconds: 1}
+    td2 := timeutil.Timedelta{Days: 2, Minutes: 2, Seconds: 2}
+    td = td.Add(&td2) // td = td + td2
+
+    result = base.Add(td.Duration())
+    fmt.Println(result) // "2015-02-06 00:03:03 +0000 UTC"
+
+    // Operation : Subtract
+    base = time.Date(2015, 2, 3, 0, 0, 0, 0, time.UTC)
+
+    td = timeutil.Timedelta{Days: 2, Minutes: 2, Seconds: 2}
+    td2 = timeutil.Timedelta{Days: 1, Minutes: 1, Seconds: 1}
+    td = td.Subtract(&td2) // td = td - td2
+
+    result = base.Add(td.Duration())
+    fmt.Println(result) // "2015-02-04 00:01:01 +0000 UTC"
+
+    // Operation : Abs
+    base = time.Date(2015, 2, 3, 0, 0, 0, 0, time.UTC)
+
+    td = timeutil.Timedelta{Days: 1, Minutes: 1, Seconds: 1}
+    td2 = timeutil.Timedelta{Days: 2, Minutes: 2, Seconds: 2}
+    td = td.Subtract(&td2) // td = td - td2
+    td = td.Abs()          // td = |td|
+
+    result = base.Add(td.Duration())
+    fmt.Println(result) // "2015-02-04 00:01:01 +0000 UTC"
+    
+    
+    // Strftime
+    date := time.Date(2015, 7, 2, 15, 24, 30, 35, time.UTC)
+    str := timeutil.Strftime(&date, "%a %b %d %I:%M:%S %p %Y")
+    fmt.Println(str) // "Thu Jul 02 03:24:30 PM 2015"
+    
+    // Unicode support
+    str = timeutil.Strftime(&date, "작성일 : %a %b %d %I:%M:%S %p %Y")
+    fmt.Println(str) // "작성일 : Thu Jul 02 03:24:30 PM 2015"
+}
+```
+
+
+## Timedelta
+
+Timedelta represents a duration between two dates. (inspired by python's timedelta)
+
+### Timedelta struct
+
+```Go
+type Timedelta struct {
+    Days, Seconds, Microseconds, Milliseconds, Minutes, Hours, Weeks time.Duration
+}
+```
+
+### Initialization
+
+All fields are optional and default to 0. You can initialize any type of timedelta by specifying field values which you want to use.
+
+**Examples:**
+
+```Go
+td := timeutil.Timedelta{Days: 10}
+td = timeutil.Timedelta{Minutes: 17}
+td = timeutil.Timedelta{Seconds: 56}
+td = timeutil.Timedelta{Days: 10, Minutes: 17, Seconds: 56}
+td = timeutil.Timedelta{Days: 1, Seconds: 1, Microseconds: 1, Milliseconds: 1, Minutes: 1, Hours: 1, Weeks: 1}
+```
+
+### func (t *Timedelta) Duration() time.Duration
+
+Duration() returns time.Duration. time.Duration can be added to time.Date.
+
+**Examples:**
+
+```Go
+base := time.Date(2015, 2, 3, 0, 0, 0, 0, time.UTC)
+td := timeutil.Timedelta{Days: 10, Minutes: 17, Seconds: 56}
+
+result := base.Add(td.Duration())
+fmt.Println(result) // "2015-02-13 00:17:56 +0000 UTC"
+```
+
+### Operations
+
+#### func (t *Timedelta) Add(t2 *Timedelta)
+
+Add returns the Timedelta t+t2.
+
+**Examples:**
+
+```Go
+base := time.Date(2015, 2, 3, 0, 0, 0, 0, time.UTC)
+td := timeutil.Timedelta{Days: 1, Minutes: 1, Seconds: 1}
+td2 := timeutil.Timedelta{Days: 2, Minutes: 2, Seconds: 2}
+td = td.Add(&td2) // td = td + td2
+
+result = base.Add(td.Duration())
+fmt.Println(result) // "2015-02-06 00:03:03 +0000 UTC"
+```
+
+#### func (t *Timedelta) Subtract(t2 *Timedelta) Timedelta
+
+Subtract returns the Timedelta t-t2.
+
+**Examples:**
+
+```Go
+base := time.Date(2015, 2, 3, 0, 0, 0, 0, time.UTC)
+
+td := timeutil.Timedelta{Days: 2, Minutes: 2, Seconds: 2}
+td2 := timeutil.Timedelta{Days: 1, Minutes: 1, Seconds: 1}
+td = td.Subtract(&td2) // td = td - td2
+
+result = base.Add(td.Duration())
+fmt.Println(result) // "2015-02-04 00:01:01 +0000 UTC"
+```
+
+#### func (t *Timedelta) Abs() Timedelta
+
+Abs returns the absolute value of t
+
+**Examples:**
+
+```Go
+base := time.Date(2015, 2, 3, 0, 0, 0, 0, time.UTC)
+
+td := timeutil.Timedelta{Days: 1, Minutes: 1, Seconds: 1}
+td2 := timeutil.Timedelta{Days: 2, Minutes: 2, Seconds: 2}
+td = td.Subtract(&td2) // td = td - td2
+td = td.Abs() // td = |td|
+
+result = base.Add(td.Duration())
+fmt.Println(result) // "2015-02-04 00:01:01 +0000 UTC"
+```
+
+
+## Strftime
+
+Strftime formats time.Date according to the directives in the given format string. The directives begins with a percent (%) character.
+
+(Strftime supports unicode format string.)
+
+
+Directive | Meaning | Example
+-------------| ------------- | -------------
+%a | Weekday as locale’s abbreviated name. | Sun, Mon, ..., Sat
+%A | Weekday as locale’s full name.     | Sunday, Monday, ..., Saturday 
+%w | Weekday as a decimal number, where 0 is Sunday and 6 is Saturday | 0, 1, ..., 6     
+%d | Day of the month as a zero-padded decimal number. | 01, 02, ..., 31 
+%b | Month as locale’s abbreviated name. | Jan, Feb, ..., Dec
+%B | Month as locale’s full name. | January, February, ..., December
+%m | Month as a zero-padded decimal number. | 01, 02, ..., 12
+%y | Year without century as a zero-padded decimal number. | 00, 01, ..., 99
+%Y | Year with century as a decimal number. |   1970, 1988, 2001, 2013
+%H | Hour (24-hour clock) as a zero-padded decimal number. | 00, 01, ..., 23
+%I | Hour (12-hour clock) as a zero-padded decimal number. | 01, 02, ..., 12 
+%p | Meridian indicator. (AM or PM.) | AM, PM
+%M | Minute as a zero-padded decimal number. | 00, 01, ..., 59
+%S | Second as a zero-padded decimal number. | 00, 01, ..., 59
+%f | Microsecond as a decimal number, zero-padded on the left. | 000000, 000001, ..., 999999
+%z | UTC offset in the form +HHMM or -HHMM | +0000
+%Z | Time zone name | UTC
+%j | Day of the year as a zero-padded decimal number | 001, 002, ..., 366
+%U | Week number of the year (Sunday as the first day of the week) as a zero padded decimal number. All days in a new year preceding the first Sunday are considered to be in week 0. | 00, 01, ..., 53 
+%W | Week number of the year (Monday as the first day of the week) as a decimal number. All days in a new year preceding the first Monday are considered to be in week 0.   | 00, 01, ..., 53
+%c | Date and time representation. | Tue Aug 16 21:30:00 1988
+%x | Date representation. | 08/16/88
+%X | Time representation. | 21:30:00
+%% | A literal '%' character. | %
+
+**Examples:**
+
+```Go
+date := time.Date(2015, 7, 2, 15, 24, 30, 35, time.UTC)
+str := timeutil.Strftime(&date, "%a %b %d %I:%M:%S %p %Y")
+fmt.Println(str) // "Thu Jul 02 03:24:30 PM 2015"
+
+// Unicode support
+str = timeutil.Strftime(&date, "작성일 : %a %b %d %I:%M:%S %p %Y")
+fmt.Println(str) // "작성일 : Thu Jul 02 03:24:30 PM 2015"
+```
+
+## TODO
+
+* Locale support
+* Strptime - a function which returns a time.Date parsed according to a format string
+* Auto date parser - a generic string parser which is able to parse most known formats to represent a date
+* And other useful features...
\ No newline at end of file
diff --git a/vendor/github.com/leekchan/timeutil/strftime.go b/vendor/github.com/leekchan/timeutil/strftime.go
new file mode 100644
index 000000000..643b79472
--- /dev/null
+++ b/vendor/github.com/leekchan/timeutil/strftime.go
@@ -0,0 +1,157 @@
+package timeutil
+
+import (
+	"fmt"
+	"time"
+	"strings"
+)
+
+var longDayNames = []string{
+	"Sunday",
+	"Monday",
+	"Tuesday",
+	"Wednesday",
+	"Thursday",
+	"Friday",
+	"Saturday",
+}
+
+var shortDayNames = []string{
+	"Sun",
+	"Mon",
+	"Tue",
+	"Wed",
+	"Thu",
+	"Fri",
+	"Sat",
+}
+
+var shortMonthNames = []string{
+	"---",
+	"Jan",
+	"Feb",
+	"Mar",
+	"Apr",
+	"May",
+	"Jun",
+	"Jul",
+	"Aug",
+	"Sep",
+	"Oct",
+	"Nov",
+	"Dec",
+}
+
+var longMonthNames = []string{
+	"---",
+	"January",
+	"February",
+	"March",
+	"April",
+	"May",
+	"June",
+	"July",
+	"August",
+	"September",
+	"October",
+	"November",
+	"December",
+}
+
+func weekNumber(t *time.Time, char int) int {
+	weekday := int(t.Weekday())
+
+	if char == 'W' {
+		// Monday as the first day of the week
+		if weekday == 0 {
+			weekday = 6
+		} else {
+			weekday -= 1
+		}
+	}
+
+	return (t.YearDay() + 6 - weekday) / 7
+}
+
+// Strftime formats time.Date according to the directives in the given format string. The directives begins with a percent (%) character.
+func Strftime(t *time.Time, f string) string {
+	var result []string
+	format := []rune(f)
+
+	add := func(str string) {
+		result = append(result, str)
+	}
+
+	for i := 0; i < len(format); i++ {
+		switch format[i] {
+		case '%':
+			if i < len(format)-1 {
+				switch format[i+1] {
+				case 'a':
+					add(shortDayNames[t.Weekday()])
+				case 'A':
+					add(longDayNames[t.Weekday()])
+				case 'w':
+					add(fmt.Sprintf("%d", t.Weekday()))
+				case 'd':
+					add(fmt.Sprintf("%02d", t.Day()))
+				case 'b':
+					add(shortMonthNames[t.Month()])
+				case 'B':
+					add(longMonthNames[t.Month()])
+				case 'm':
+					add(fmt.Sprintf("%02d", t.Month()))
+				case 'y':
+					add(fmt.Sprintf("%02d", t.Year()%100))
+				case 'Y':
+					add(fmt.Sprintf("%02d", t.Year()))
+				case 'H':
+					add(fmt.Sprintf("%02d", t.Hour()))
+				case 'I':
+					if t.Hour() == 0 {
+						add(fmt.Sprintf("%02d", 12))
+					} else if t.Hour() > 12 {
+						add(fmt.Sprintf("%02d", t.Hour()-12))
+					} else {
+						add(fmt.Sprintf("%02d", t.Hour()))
+					}
+				case 'p':
+					if t.Hour() < 12 {
+						add("AM")
+					} else {
+						add("PM")
+					}
+				case 'M':
+					add(fmt.Sprintf("%02d", t.Minute()))
+				case 'S':
+					add(fmt.Sprintf("%02d", t.Second()))
+				case 'f':
+					add(fmt.Sprintf("%06d", t.Nanosecond()/1000))
+				case 'z':
+					add(t.Format("-0700"))
+				case 'Z':
+					add(t.Format("MST"))
+				case 'j':
+					add(fmt.Sprintf("%03d", t.YearDay()))
+				case 'U':
+					add(fmt.Sprintf("%02d", weekNumber(t, 'U')))
+				case 'W':
+					add(fmt.Sprintf("%02d", weekNumber(t, 'W')))
+				case 'c':
+					add(t.Format("Mon Jan 2 15:04:05 2006"))
+				case 'x':
+					add(fmt.Sprintf("%02d/%02d/%02d", t.Month(), t.Day(), t.Year()%100))
+				case 'X':
+					add(fmt.Sprintf("%02d:%02d:%02d", t.Hour(), t.Minute(), t.Second()))
+				case '%':
+					add("%")
+				}
+				i += 1
+			}
+		default:
+			add(string(format[i]))
+		}
+	}
+
+	return strings.Join(result, "")
+}
diff --git a/vendor/github.com/leekchan/timeutil/timedelta.go b/vendor/github.com/leekchan/timeutil/timedelta.go
new file mode 100644
index 000000000..8d2dd11b1
--- /dev/null
+++ b/vendor/github.com/leekchan/timeutil/timedelta.go
@@ -0,0 +1,73 @@
+package timeutil
+
+import (
+	"time"
+)
+
+func abs(v time.Duration) time.Duration {
+	if v < 0 {
+		v *= -1
+	}
+	return v
+}
+
+// Timedelta represents a duration between two dates.
+// All fields are optional and default to 0. You can initialize any type of timedelta by specifying field values which you want to use.
+type Timedelta struct {
+	Days, Seconds, Microseconds, Milliseconds, Minutes, Hours, Weeks time.Duration
+}
+
+// Add returns the Timedelta t+t2.
+func (t *Timedelta) Add(t2 *Timedelta) Timedelta {
+	return Timedelta{
+		Days:         t.Days + t2.Days,
+		Seconds:      t.Seconds + t2.Seconds,
+		Microseconds: t.Microseconds + t2.Microseconds,
+		Milliseconds: t.Milliseconds + t2.Milliseconds,
+		Minutes:      t.Minutes + t2.Minutes,
+		Hours:        t.Hours + t2.Hours,
+		Weeks:        t.Weeks + t2.Weeks,
+	}
+}
+
+// Subtract returns the Timedelta t-t2.
+func (t *Timedelta) Subtract(t2 *Timedelta) Timedelta {
+	return Timedelta{
+		Days:         t.Days - t2.Days,
+		Seconds:      t.Seconds - t2.Seconds,
+		Microseconds: t.Microseconds - t2.Microseconds,
+		Milliseconds: t.Milliseconds - t2.Milliseconds,
+		Minutes:      t.Minutes - t2.Minutes,
+		Hours:        t.Hours - t2.Hours,
+		Weeks:        t.Weeks - t2.Weeks,
+	}
+}
+
+// Abs returns the absolute value of t
+func (t *Timedelta) Abs() Timedelta {
+	return Timedelta{
+		Days:         abs(t.Days),
+		Seconds:      abs(t.Seconds),
+		Microseconds: abs(t.Microseconds),
+		Milliseconds: abs(t.Milliseconds),
+		Minutes:      abs(t.Minutes),
+		Hours:        abs(t.Hours),
+		Weeks:        abs(t.Weeks),
+	}
+}
+
+// Duration() returns time.Duration. time.Duration can be added to time.Date.
+func (t *Timedelta) Duration() time.Duration {
+	return t.Days*24*time.Hour +
+		t.Seconds*time.Second +
+		t.Microseconds*time.Microsecond +
+		t.Milliseconds*time.Millisecond +
+		t.Minutes*time.Minute +
+		t.Hours*time.Hour +
+		t.Weeks*7*24*time.Hour
+}
+
+// String returns a string representing the Timedelta's duration in the form "72h3m0.5s".
+func (t *Timedelta) String() string {
+	return t.Duration().String()
+}