├── go.sum ├── v2 ├── go.sum ├── go.mod ├── ensure_test.go ├── tparse.go └── tparse_test.go ├── go.mod ├── benchmarks ├── benchmarks_test.go ├── go.sum ├── go.mod ├── goparsetime_test.go └── tparse_test.go ├── .gitignore ├── LICENSE ├── tparse_test.go ├── README.md └── tparse.go /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/karrick/tparse 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /v2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/karrick/tparse/v2 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /benchmarks/benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package benchmarks 2 | 3 | const benchmarkDuration = "15h45m38s" 4 | const benchmarkNowMinusDuration = "now-21second" 5 | const rfc3339 = "2006-01-02T15:04:05Z" 6 | -------------------------------------------------------------------------------- /benchmarks/go.sum: -------------------------------------------------------------------------------- 1 | github.com/etdub/goparsetime v0.0.0-20160315173935-ea17b0ac3318 h1:iguwbR+9xsizl84VMHU47I4OOWYSex1HZRotEoqziWQ= 2 | github.com/etdub/goparsetime v0.0.0-20160315173935-ea17b0ac3318/go.mod h1:O/QFFckzvu1KpS1AOuQGgi6ErznEF8nZZVNDDMXlDP4= 3 | -------------------------------------------------------------------------------- /benchmarks/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/karrick/tparse/benchmarks 2 | 3 | go 1.12 4 | 5 | replace github.com/karrick/tparse => ../v2/ 6 | 7 | require ( 8 | github.com/etdub/goparsetime v0.0.0-20160315173935-ea17b0ac3318 9 | github.com/karrick/tparse v0.0.0 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /benchmarks/goparsetime_test.go: -------------------------------------------------------------------------------- 1 | package benchmarks 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/etdub/goparsetime" 8 | ) 9 | 10 | func BenchmarkParseNowMinusDurationGoParseTime(b *testing.B) { 11 | var t time.Time 12 | var err error 13 | value := "now-5s" 14 | 15 | for i := 0; i < b.N; i++ { 16 | t, err = goparsetime.Parsetime(value) 17 | if err != nil { 18 | b.Fatal(err) 19 | } 20 | } 21 | _ = t 22 | } 23 | -------------------------------------------------------------------------------- /v2/ensure_test.go: -------------------------------------------------------------------------------- 1 | package tparse 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func ensureError(tb testing.TB, err error, contains ...string) { 9 | tb.Helper() 10 | if len(contains) == 0 || (len(contains) == 1 && contains[0] == "") { 11 | if err != nil { 12 | tb.Fatalf("GOT: %v; WANT: %v", err, contains) 13 | } 14 | } else if err == nil { 15 | tb.Errorf("GOT: %v; WANT: %v", err, contains) 16 | } else { 17 | for _, stub := range contains { 18 | if stub != "" && !strings.Contains(err.Error(), stub) { 19 | tb.Errorf("GOT: %v; WANT: %q", err, stub) 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Karrick McDermott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /tparse_test.go: -------------------------------------------------------------------------------- 1 | package tparse 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestParseFloatingEpoch(t *testing.T) { 9 | actual, err := Parse("", "1445535988.5") 10 | if err != nil { 11 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 12 | } 13 | expected := time.Unix(1445535988, fractionToNanos(0.5)) 14 | if actual != expected { 15 | t.Errorf("Actual: %s; Expected: %s", actual, expected) 16 | } 17 | } 18 | 19 | func TestParseFloatingNegativeEpoch(t *testing.T) { 20 | _, err := Parse("", "-1445535988.5") 21 | if _, ok := err.(*time.ParseError); err == nil || !ok { 22 | t.Errorf("Actual: %#v; Expected: %s", err, "fixme") 23 | } 24 | } 25 | 26 | func TestParseNow(t *testing.T) { 27 | before := time.Now() 28 | actual, err := ParseNow("", "now") 29 | if err != nil { 30 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 31 | } 32 | after := time.Now() 33 | if before.After(actual) || actual.After(after) { 34 | t.Errorf("Actual: %s; Expected between: %s and %s", actual, before, after) 35 | } 36 | } 37 | 38 | func TestParseNowMinusMilliisecond(t *testing.T) { 39 | before := time.Now() 40 | time.Sleep(10 * time.Millisecond) 41 | actual, err := ParseNow("", "now-10ms") 42 | if err != nil { 43 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 44 | } 45 | after := time.Now() 46 | if before.After(actual) || actual.After(after) { 47 | t.Errorf("Actual: %s; Expected between: %s and %s", actual, before, after) 48 | } 49 | } 50 | 51 | func TestParseNowPlusMilliisecond(t *testing.T) { 52 | before := time.Now() 53 | actual, err := ParseNow("", "now+10ms") 54 | if err != nil { 55 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 56 | } 57 | time.Sleep(10 * time.Millisecond) 58 | after := time.Now() 59 | if before.After(actual) || actual.After(after) { 60 | t.Errorf("Actual: %s; Expected between: %s and %s", actual, before, after) 61 | } 62 | } 63 | 64 | func TestParseLayout(t *testing.T) { 65 | actual, err := Parse(time.RFC3339, "2006-01-02T15:04:05Z") 66 | if err != nil { 67 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 68 | } 69 | expected := time.Unix(1136214245, 0) 70 | if !actual.Equal(expected) { 71 | t.Errorf("Actual: %d; Expected: %d", actual.Unix(), expected.Unix()) 72 | } 73 | } 74 | 75 | func TestParseNowPlusDay(t *testing.T) { 76 | before := time.Now().UTC().AddDate(0, 0, 1).Add(time.Hour).Add(time.Minute) 77 | actual, err := ParseNow("", "now+1h1d1m") 78 | if err != nil { 79 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 80 | } 81 | after := time.Now().UTC().AddDate(0, 0, 1).Add(time.Hour).Add(time.Minute) 82 | actual = actual.UTC() 83 | if before.After(actual) || actual.After(after) { 84 | t.Errorf("Actual: %s; Expected between: %s and %s", actual, before, after) 85 | } 86 | } 87 | 88 | func TestParseUsingMap(t *testing.T) { 89 | before := time.Now().UTC() 90 | dict := map[string]time.Time{ 91 | "start": time.Now().UTC().AddDate(0, 0, -7), 92 | } 93 | after := time.Now().UTC() 94 | 95 | actual, err := ParseWithMap(time.ANSIC, "start+1week", dict) 96 | if err != nil { 97 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 98 | } 99 | 100 | actual = actual.UTC() 101 | if before.After(actual) || actual.After(after) { 102 | t.Errorf("Actual: %s; Expected between: %s and %s", actual, before, after) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /benchmarks/tparse_test.go: -------------------------------------------------------------------------------- 1 | package benchmarks 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/karrick/tparse/v2" 8 | ) 9 | 10 | func BenchmarkAddDuration(b *testing.B) { 11 | var err error 12 | var t time.Time 13 | epoch := time.Now().UTC() 14 | 15 | for i := 0; i < b.N; i++ { 16 | t, err = tparse.AddDuration(epoch, benchmarkDuration) 17 | if err != nil { 18 | b.Fatal(err) 19 | } 20 | } 21 | _ = t 22 | } 23 | 24 | func BenchmarkParseDurationPseudoStandardLibrary(b *testing.B) { 25 | var d time.Duration 26 | var err error 27 | 28 | for i := 0; i < b.N; i++ { 29 | d, err = time.ParseDuration(benchmarkDuration) 30 | if err != nil { 31 | b.Fatal(err) 32 | } 33 | } 34 | _ = d 35 | } 36 | 37 | func BenchmarkAddDurationStandardLibrary(b *testing.B) { 38 | var d time.Duration 39 | var err error 40 | var t time.Time 41 | epoch := time.Now().UTC() 42 | 43 | for i := 0; i < b.N; i++ { 44 | d, err = time.ParseDuration(benchmarkDuration) 45 | if err != nil { 46 | b.Fatal(err) 47 | } 48 | t = epoch.Add(d) 49 | } 50 | _ = t 51 | } 52 | 53 | // 54 | 55 | func BenchmarkParseNowMinusDuration(b *testing.B) { 56 | var t time.Time 57 | var err error 58 | 59 | for i := 0; i < b.N; i++ { 60 | t, err = tparse.ParseNow("", benchmarkNowMinusDuration) 61 | if err != nil { 62 | b.Fatal(err) 63 | } 64 | } 65 | _ = t 66 | } 67 | 68 | func BenchmarkParseWithMapEpoch(b *testing.B) { 69 | var t time.Time 70 | var err error 71 | value := "1458179403.12345" 72 | 73 | for i := 0; i < b.N; i++ { 74 | t, err = tparse.ParseWithMap(time.ANSIC, value, nil) 75 | if err != nil { 76 | b.Fatal(err) 77 | } 78 | } 79 | _ = t 80 | } 81 | 82 | func BenchmarkParseWithMapKeyedValue(b *testing.B) { 83 | var t time.Time 84 | var err error 85 | value := "end" 86 | 87 | m := make(map[string]time.Time) 88 | m["end"] = time.Now() 89 | 90 | for i := 0; i < b.N; i++ { 91 | t, err = tparse.ParseWithMap(time.ANSIC, value, m) 92 | if err != nil { 93 | b.Fatal(err) 94 | } 95 | } 96 | _ = t 97 | } 98 | 99 | func BenchmarkParseWithMapKeyedValueAndDuration(b *testing.B) { 100 | var t time.Time 101 | var err error 102 | value := "end+1hr" 103 | 104 | m := make(map[string]time.Time) 105 | m["end"] = time.Now() 106 | 107 | for i := 0; i < b.N; i++ { 108 | t, err = tparse.ParseWithMap(time.ANSIC, value, m) 109 | if err != nil { 110 | b.Fatal(err) 111 | } 112 | } 113 | _ = t 114 | } 115 | 116 | // 117 | 118 | func BenchmarkParseRFC3339(b *testing.B) { 119 | var t time.Time 120 | var err error 121 | 122 | for i := 0; i < b.N; i++ { 123 | t, err = tparse.Parse(time.RFC3339, rfc3339) 124 | if err != nil { 125 | b.Fatal(err) 126 | } 127 | } 128 | _ = t 129 | } 130 | 131 | func BenchmarkParseRFC3339StandardLibrary(b *testing.B) { 132 | var t time.Time 133 | var err error 134 | 135 | for i := 0; i < b.N; i++ { 136 | t, err = time.Parse(time.RFC3339, rfc3339) 137 | if err != nil { 138 | b.Fatal(err) 139 | } 140 | } 141 | _ = t 142 | } 143 | 144 | func BenchmarkParseNow(b *testing.B) { 145 | var t time.Time 146 | var err error 147 | value := "now-5s" 148 | 149 | for i := 0; i < b.N; i++ { 150 | t, err = tparse.ParseNow(time.ANSIC, value) 151 | if err != nil { 152 | b.Fatal(err) 153 | } 154 | } 155 | _ = t 156 | } 157 | 158 | func BenchmarkParseUsingMap(b *testing.B) { 159 | var t time.Time 160 | var err error 161 | value := "end-1mo" 162 | 163 | m := make(map[string]time.Time) 164 | m["end"] = time.Now() 165 | 166 | for i := 0; i < b.N; i++ { 167 | t, err = tparse.ParseWithMap(time.ANSIC, value, m) 168 | if err != nil { 169 | b.Fatal(err) 170 | } 171 | } 172 | _ = t 173 | } 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tparse 2 | 3 | `Parse` will return the time corresponding to the layout and value. 4 | It also parses floating point epoch values, and values of "now", 5 | "now+DURATION", and "now-DURATION". 6 | 7 | In addition to the duration abbreviations recognized by 8 | `time.ParseDuration`, tparse recognizes various tokens for days, 9 | weeks, months, and years, as well as tokens for seconds, minutes, and 10 | hours. 11 | 12 | Like `time.ParseDuration`, it accepts multiple fractional scalars, so 13 | "now+1.5days-3.21hours" is evaluated properly. 14 | 15 | ## Documentation 16 | 17 | In addition to this handy README.md file, documentation is available 18 | in godoc format at 19 | [![GoDoc](https://godoc.org/github.com/karrick/tparse?status.svg)](https://godoc.org/github.com/karrick/tparse). 20 | 21 | ## Examples 22 | 23 | ### ParseNow 24 | 25 | `ParseNow` can parse time values that are relative to the current 26 | time, by specifying a string starting with "now", a '+' or '-' byte, 27 | followed by a time duration. 28 | 29 | ```Go 30 | package main 31 | 32 | import ( 33 | "fmt" 34 | "os" 35 | "time" 36 | "github.com/karrick/tparse" 37 | ) 38 | 39 | func main() { 40 | actual, err := tparse.ParseNow(time.RFC3339, "now+1d-3w4mo+7y6h4m") 41 | if err != nil { 42 | fmt.Fprintf(os.Stderr, "error: %s\n", err) 43 | os.Exit(1) 44 | } 45 | fmt.Printf("time is: %s\n", actual) 46 | } 47 | ``` 48 | 49 | ### ParseWithMap 50 | 51 | `ParseWithMap` can parse time values that use a base time other than "now". 52 | 53 | ```Go 54 | package main 55 | 56 | import ( 57 | "fmt" 58 | "os" 59 | "time" 60 | "github.com/karrick/tparse" 61 | ) 62 | 63 | func main() { 64 | m := make(map[string]time.Time) 65 | m["end"] = time.Now() 66 | 67 | start, err := tparse.ParseWithMap(time.RFC3339, "end-12h", m) 68 | if err != nil { 69 | fmt.Fprintf(os.Stderr, "error: %s\n", err) 70 | os.Exit(1) 71 | } 72 | 73 | fmt.Printf("start: %s; end: %s\n", start, end) 74 | } 75 | ``` 76 | 77 | ### AddDuration 78 | 79 | `AddDuration` is used to compute the value of a duration string and 80 | add it to a known time. This function is used by the other library 81 | functions to parse all duration strings. 82 | 83 | The following tokens may be used to specify the respective unit of 84 | time: 85 | 86 | * Nanosecond: ns 87 | * Microsecond: us, µs (U+00B5 = micro symbol), μs (U+03BC = Greek letter mu) 88 | * Millisecond: ms 89 | * Second: s, sec, second, seconds 90 | * Minute: m, min, minute, minutes 91 | * Hour: h, hr, hour, hours 92 | * Day: d, day, days 93 | * Week: w, wk, week, weeks 94 | * Month: mo, mon, month, months 95 | * Year: y, yr, year, years 96 | 97 | ```Go 98 | package main 99 | 100 | import ( 101 | "fmt" 102 | "os" 103 | "time" 104 | 105 | "github.com/karrick/tparse" 106 | ) 107 | 108 | func main() { 109 | now := time.Now() 110 | another, err := tparse.AddDuration(now, "+1d3w4mo-7y6h4m") 111 | if err != nil { 112 | fmt.Fprintf(os.Stderr, "error: %s\n", err) 113 | os.Exit(1) 114 | } 115 | 116 | fmt.Printf("time is: %s\n", another) 117 | } 118 | ``` 119 | 120 | ### AbsoluteDuration 121 | 122 | When you would rather have the `time.Duration` representation of a 123 | duration string, there is a function for that, but with a 124 | caveat. 125 | 126 | First, not every month has 30 days, and therefore Go does not have a 127 | `time.Duration` type constant to represent one month. 128 | 129 | When I add one month to February 3, do I get March 3 or March 4? 130 | Depends on what the year is and whether or not that year is a leap 131 | year. 132 | 133 | Is one month always 30 days? Is one month 31 days, or 28, or 29? I did 134 | not want to have to answer this question, so I defaulted to saying the 135 | length of one month depends on which month and year, and I allowed the 136 | Go standard library to add duration concretely to a given moment in 137 | time. 138 | 139 | Consider the below two examples of calling `AbsoluteDuration` with the 140 | same duration string, but different base times. 141 | 142 | ```Go 143 | func ExampleAbsoluteDuration() { 144 | t1 := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) 145 | 146 | d1, err := AbsoluteDuration(t1, "1.5month") 147 | if err != nil { 148 | fmt.Println(err) 149 | return 150 | } 151 | 152 | fmt.Println(d1) 153 | 154 | t2 := time.Date(2020, time.February, 10, 23, 0, 0, 0, time.UTC) 155 | 156 | d2, err := AbsoluteDuration(t2, "1.5month") 157 | if err != nil { 158 | fmt.Println(err) 159 | return 160 | } 161 | 162 | fmt.Println(d2) 163 | // Output: 164 | // 1080h0m0s 165 | // 1056h0m0s 166 | } 167 | ``` 168 | 169 | ## Benchmark against goparsetime 170 | 171 | ```Bash 172 | GO111MODULE=on go test -bench=. -tags goparsetime 173 | ``` 174 | -------------------------------------------------------------------------------- /tparse.go: -------------------------------------------------------------------------------- 1 | package tparse 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "strconv" 8 | "strings" 9 | "time" 10 | "unicode" 11 | ) 12 | 13 | // Parse will return the time value corresponding to the specified layout and value. It also parses 14 | // floating point and integer epoch values. 15 | func Parse(layout, value string) (time.Time, error) { 16 | return ParseWithMap(layout, value, make(map[string]time.Time)) 17 | } 18 | 19 | // ParseNow will return the time value corresponding to the specified layout and value. It also 20 | // parses floating point and integer epoch values. It recognizes the special string `now` and 21 | // replaces that with the time ParseNow is called. This allows a suffix adding or subtracting 22 | // various values from the base time. For instance, ParseNow(time.ANSIC, "now+1d") will return a 23 | // time corresponding to 24 hours from the moment the function is invoked. 24 | // 25 | // In addition to the duration abbreviations recognized by time.ParseDuration, it recognizes various 26 | // tokens for days, weeks, months, and years. 27 | // 28 | // package main 29 | // 30 | // import ( 31 | // "fmt" 32 | // "os" 33 | // "time" 34 | // 35 | // "github.com/karrick/tparse" 36 | // ) 37 | // 38 | // func main() { 39 | // actual, err := tparse.ParseNow(time.RFC3339, "now+1d3w4mo7y6h4m") 40 | // if err != nil { 41 | // fmt.Fprintf(os.Stderr, "error: %s\n", err) 42 | // os.Exit(1) 43 | // } 44 | // 45 | // fmt.Printf("time is: %s\n", actual) 46 | // } 47 | func ParseNow(layout, value string) (time.Time, error) { 48 | m := map[string]time.Time{"now": time.Now()} 49 | return ParseWithMap(layout, value, m) 50 | } 51 | 52 | // ParseWithMap will return the time value corresponding to the specified layout and value. It also 53 | // parses floating point and integer epoch values. It accepts a map of strings to time.Time values, 54 | // and if the value string starts with one of the keys in the map, it replaces the string with the 55 | // corresponding time.Time value. 56 | // 57 | // package main 58 | // 59 | // import ( 60 | // "fmt" 61 | // "os" 62 | // "time" 63 | // 64 | // "github.com/karrick/tparse" 65 | // ) 66 | // 67 | // func main() { 68 | // m := make(map[string]time.Time) 69 | // m["start"] = start 70 | // 71 | // end, err := tparse.ParseWithMap(time.RFC3339, "start+8h", m) 72 | // if err != nil { 73 | // fmt.Fprintf(os.Stderr, "error: %s\n", err) 74 | // os.Exit(1) 75 | // } 76 | // 77 | // fmt.Printf("start: %s; end: %s\n", start, end) 78 | // } 79 | func ParseWithMap(layout, value string, dict map[string]time.Time) (time.Time, error) { 80 | if epoch, err := strconv.ParseFloat(value, 64); err == nil && epoch >= 0 { 81 | trunc := math.Trunc(epoch) 82 | nanos := fractionToNanos(epoch - trunc) 83 | return time.Unix(int64(trunc), int64(nanos)), nil 84 | } 85 | var base time.Time 86 | var y, m, d int 87 | var duration time.Duration 88 | var direction = 1 89 | var err error 90 | 91 | for k, v := range dict { 92 | if strings.HasPrefix(value, k) { 93 | base = v 94 | if len(value) > len(k) { 95 | // maybe has +, - 96 | switch dir := value[len(k)]; dir { 97 | case '+': 98 | // no-op 99 | case '-': 100 | direction = -1 101 | default: 102 | return base, fmt.Errorf("expected '+' or '-': %q", dir) 103 | } 104 | var nv string 105 | y, m, d, nv = ymd(value[len(k)+1:]) 106 | if len(nv) > 0 { 107 | duration, err = time.ParseDuration(nv) 108 | if err != nil { 109 | return base, err 110 | } 111 | } 112 | } 113 | if direction < 0 { 114 | y = -y 115 | m = -m 116 | d = -d 117 | } 118 | return base.Add(time.Duration(int(duration)*direction)).AddDate(y, m, d), nil 119 | } 120 | } 121 | return time.Parse(layout, value) 122 | } 123 | 124 | func fractionToNanos(fraction float64) int64 { 125 | return int64(fraction * float64(time.Second/time.Nanosecond)) 126 | } 127 | 128 | func ymd(value string) (int, int, int, string) { 129 | // alternating numbers and strings 130 | var y, m, d int 131 | var accum int // accumulates digits 132 | var unit []byte // accumulates units 133 | var unproc []byte // accumulate unprocessed durations to return 134 | 135 | unitComplete := func() { 136 | // NOTE: compare byte slices because some units, i.e. ms, are multi-rune 137 | if bytes.Equal(unit, []byte{'d'}) || bytes.Equal(unit, []byte{'d', 'a', 'y'}) || bytes.Equal(unit, []byte{'d', 'a', 'y', 's'}) { 138 | d += accum 139 | } else if bytes.Equal(unit, []byte{'w'}) || bytes.Equal(unit, []byte{'w', 'e', 'e', 'k'}) || bytes.Equal(unit, []byte{'w', 'e', 'e', 'k', 's'}) { 140 | d += 7 * accum 141 | } else if bytes.Equal(unit, []byte{'m', 'o'}) || bytes.Equal(unit, []byte{'m', 'o', 'n'}) || bytes.Equal(unit, []byte{'m', 'o', 'n', 't', 'h'}) || bytes.Equal(unit, []byte{'m', 'o', 'n', 't', 'h', 's'}) || bytes.Equal(unit, []byte{'m', 't', 'h'}) || bytes.Equal(unit, []byte{'m', 'n'}) { 142 | m += accum 143 | } else if bytes.Equal(unit, []byte{'y'}) || bytes.Equal(unit, []byte{'y', 'e', 'a', 'r'}) || bytes.Equal(unit, []byte{'y', 'e', 'a', 'r', 's'}) { 144 | y += accum 145 | } else { 146 | unproc = append(append(unproc, strconv.Itoa(accum)...), unit...) 147 | } 148 | } 149 | 150 | expectDigit := true 151 | for _, rune := range value { 152 | if unicode.IsDigit(rune) { 153 | if expectDigit { 154 | accum = accum*10 + int(rune-'0') 155 | } else { 156 | unitComplete() 157 | unit = unit[:0] 158 | accum = int(rune - '0') 159 | } 160 | continue 161 | } 162 | unit = append(unit, string(rune)...) 163 | expectDigit = false 164 | } 165 | if len(unit) > 0 { 166 | unitComplete() 167 | accum = 0 168 | unit = unit[:0] 169 | } 170 | // log.Printf("y: %d; m: %d; d: %d; nv: %q", y, m, d, unproc) 171 | return y, m, d, string(unproc) 172 | } 173 | -------------------------------------------------------------------------------- /v2/tparse.go: -------------------------------------------------------------------------------- 1 | package tparse 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | func fractionToNanos(fraction float64) int64 { 13 | return int64(fraction * float64(time.Second/time.Nanosecond)) 14 | } 15 | 16 | var unitMap = map[string]float64{ 17 | "ns": float64(time.Nanosecond), 18 | "us": float64(time.Microsecond), 19 | "µs": float64(time.Microsecond), // U+00B5 = micro symbol 20 | "μs": float64(time.Microsecond), // U+03BC = Greek letter mu 21 | "ms": float64(time.Millisecond), 22 | "s": float64(time.Second), 23 | "sec": float64(time.Second), 24 | "second": float64(time.Second), 25 | "seconds": float64(time.Second), 26 | "m": float64(time.Minute), 27 | "min": float64(time.Minute), 28 | "minute": float64(time.Minute), 29 | "minutes": float64(time.Minute), 30 | "h": float64(time.Hour), 31 | "hr": float64(time.Hour), 32 | "hour": float64(time.Hour), 33 | "hours": float64(time.Hour), 34 | "d": float64(time.Hour * 24), 35 | "day": float64(time.Hour * 24), 36 | "days": float64(time.Hour * 24), 37 | "w": float64(time.Hour * 24 * 7), 38 | "week": float64(time.Hour * 24 * 7), 39 | "weeks": float64(time.Hour * 24 * 7), 40 | "wk": float64(time.Hour * 24 * 7), 41 | } 42 | 43 | // AbsoluteDuration returns the time.Duration between the base time and the 44 | // result of adding the duration string. This takes into account the number of 45 | // days in the intervening months and years. 46 | func AbsoluteDuration(base time.Time, duration string) (time.Duration, error) { 47 | var d time.Duration 48 | 49 | t2, err := AddDuration(base, duration) 50 | if err != nil { 51 | return d, err 52 | } 53 | 54 | return t2.Sub(base), nil 55 | } 56 | 57 | // AddDuration parses the duration string, and adds the calculated duration value to the provided 58 | // base time. On error, it returns the base time and the error. 59 | // 60 | // Like `time.ParseDuration`, this accepts multiple fractional scalars, so "now+1.5days-3.21hours" 61 | // is evaluated properly. 62 | // 63 | // The following tokens may be used to specify the respective unit of time: 64 | // 65 | // * Nanosecond: ns 66 | // * Microsecond: us, µs (U+00B5 = micro symbol), μs (U+03BC = Greek letter mu) 67 | // * Millisecond: ms 68 | // * Second: s, sec, second, seconds 69 | // * Minute: m, min, minute, minutes 70 | // * Hour: h, hr, hour, hours 71 | // * Day: d, day, days 72 | // * Week: w, wk, week, weeks 73 | // * Month: mo, mon, month, months 74 | // * Year: y, yr, year, years 75 | // 76 | // package main 77 | // 78 | // import ( 79 | // "fmt" 80 | // "os" 81 | // "time" 82 | // 83 | // "github.com/karrick/tparse" 84 | // ) 85 | // 86 | // func main() { 87 | // now := time.Now() 88 | // another, err := tparse.AddDuration(now, "now+1d3w4mo-7y6h4m") 89 | // if err != nil { 90 | // fmt.Fprintf(os.Stderr, "error: %s\n", err) 91 | // os.Exit(1) 92 | // } 93 | // 94 | // fmt.Printf("time is: %s\n", another) 95 | // } 96 | func AddDuration(base time.Time, s string) (time.Time, error) { 97 | if len(s) == 0 { 98 | return base, nil 99 | } 100 | var isNegative bool 101 | var exp, whole, fraction int64 102 | var number, totalYears, totalMonths, totalDays, totalDuration float64 103 | 104 | for s != "" { 105 | // consume possible sign 106 | if s[0] == '+' { 107 | if len(s) == 1 { 108 | return base, fmt.Errorf("cannot parse sign without digits: '+'") 109 | } 110 | isNegative = false 111 | s = s[1:] 112 | } else if s[0] == '-' { 113 | if len(s) == 1 { 114 | return base, fmt.Errorf("cannot parse sign without digits: '-'") 115 | } 116 | isNegative = true 117 | s = s[1:] 118 | } 119 | // consume digits 120 | var done bool 121 | for !done && len(s) > 0 { 122 | c := s[0] 123 | switch { 124 | case c >= '0' && c <= '9': 125 | d := int64(c - '0') 126 | if exp > 0 { 127 | exp++ 128 | fraction = 10*fraction + d 129 | } else { 130 | whole = 10*whole + d 131 | } 132 | s = s[1:] 133 | case c == '.': 134 | if exp > 0 { 135 | return base, fmt.Errorf("invalid floating point number format: two decimal points found") 136 | } 137 | exp = 1 138 | fraction = 0 139 | s = s[1:] 140 | default: 141 | done = true 142 | } 143 | } 144 | // adjust number 145 | number = float64(whole) 146 | if exp > 0 { 147 | number += float64(fraction) * math.Pow(10, float64(1-exp)) 148 | } 149 | if isNegative { 150 | number *= -1 151 | } 152 | // find end of unit 153 | var i int 154 | for ; i < len(s) && s[i] != '+' && s[i] != '-' && (s[i] < '0' || s[i] > '9'); i++ { 155 | // identifier bytes: no-op 156 | } 157 | unit := s[:i] 158 | // fmt.Printf("number: %f; unit: %q\n", number, unit) 159 | if duration, ok := unitMap[unit]; ok { 160 | totalDuration += number * duration 161 | } else { 162 | switch unit { 163 | case "mo", "mon", "month", "months": 164 | totalMonths += number 165 | case "y", "yr", "year", "years": 166 | totalYears += number 167 | default: 168 | if unit == "" { 169 | return base, errors.New("duration missing units") 170 | } 171 | return base, fmt.Errorf("unknown unit in duration: %q", unit) 172 | } 173 | } 174 | 175 | s = s[i:] 176 | whole = 0 177 | } 178 | if totalYears != 0 { 179 | whole := math.Trunc(totalYears) 180 | fraction := totalYears - whole 181 | totalYears = whole 182 | totalMonths += 12 * fraction 183 | } 184 | if totalMonths != 0 { 185 | whole := math.Trunc(totalMonths) 186 | fraction := totalMonths - whole 187 | totalMonths = whole 188 | totalDays += 30 * fraction 189 | } 190 | if totalDays != 0 { 191 | whole := math.Trunc(totalDays) 192 | fraction := totalDays - whole 193 | totalDays = whole 194 | totalDuration += (fraction * 24.0 * float64(time.Hour)) 195 | } 196 | if totalYears != 0 || totalMonths != 0 || totalDays != 0 { 197 | base = base.AddDate(int(totalYears), int(totalMonths), int(totalDays)) 198 | } 199 | if totalDuration != 0 { 200 | base = base.Add(time.Duration(totalDuration)) 201 | } 202 | return base, nil 203 | } 204 | 205 | // Parse will return the time value corresponding to the specified layout and value. It also parses 206 | // floating point and integer epoch values. 207 | func Parse(layout, value string) (time.Time, error) { 208 | return ParseWithMap(layout, value, nil) 209 | } 210 | 211 | // ParseNow will return the time value corresponding to the specified layout and value. It also 212 | // parses floating point and integer epoch values. It recognizes the special string `now` and 213 | // replaces that with the time ParseNow is called. This allows a suffix adding or subtracting 214 | // various values from the base time. For instance, ParseNow(time.ANSIC, "now+1d") will return a 215 | // time corresponding to 24 hours from the moment the function is invoked. 216 | // 217 | // In addition to the duration abbreviations recognized by time.ParseDuration, it recognizes various 218 | // tokens for days, weeks, months, and years. 219 | // 220 | // package main 221 | // 222 | // import ( 223 | // "fmt" 224 | // "os" 225 | // "time" 226 | // 227 | // "github.com/karrick/tparse" 228 | // ) 229 | // 230 | // func main() { 231 | // actual, err := tparse.ParseNow(time.RFC3339, "now+1d3w4mo7y6h4m") 232 | // if err != nil { 233 | // fmt.Fprintf(os.Stderr, "error: %s\n", err) 234 | // os.Exit(1) 235 | // } 236 | // 237 | // fmt.Printf("time is: %s\n", actual) 238 | // } 239 | func ParseNow(layout, value string) (time.Time, error) { 240 | if strings.HasPrefix(value, "now") { 241 | return AddDuration(time.Now(), value[3:]) 242 | } 243 | return ParseWithMap(layout, value, nil) 244 | } 245 | 246 | // ParseWithMap will return the time value corresponding to the specified layout and value. It also 247 | // parses floating point and integer epoch values. It accepts a map of strings to time.Time values, 248 | // and if the value string starts with one of the keys in the map, it replaces the string with the 249 | // corresponding time.Time value. 250 | // 251 | // package main 252 | // 253 | // import ( 254 | // "fmt" 255 | // "os" 256 | // "time" 257 | // "github.com/karrick/tparse" 258 | // ) 259 | // 260 | // func main() { 261 | // m := make(map[string]time.Time) 262 | // m["end"] = time.Now() 263 | // 264 | // start, err := tparse.ParseWithMap(time.RFC3339, "end-12h", m) 265 | // if err != nil { 266 | // fmt.Fprintf(os.Stderr, "error: %s\n", err) 267 | // os.Exit(1) 268 | // } 269 | // 270 | // fmt.Printf("start: %s; end: %s\n", start, end) 271 | // } 272 | func ParseWithMap(layout, value string, dict map[string]time.Time) (time.Time, error) { 273 | return ParseWithMapInLocation(layout, value, dict, nil) 274 | } 275 | 276 | func ParseWithMapInLocation(layout, value string, dict map[string]time.Time, loc *time.Location) (time.Time, error) { 277 | // find longest matching key in dict 278 | var matchKey string 279 | for k := range dict { 280 | if strings.HasPrefix(value, k) && len(k) > len(matchKey) { 281 | matchKey = k 282 | } 283 | } 284 | if len(matchKey) > 0 { 285 | return AddDuration(dict[matchKey], value[len(matchKey):]) 286 | } 287 | 288 | if loc != nil { 289 | return time.ParseInLocation(layout, value, loc) 290 | } 291 | 292 | // takes about 90ns even if fails 293 | if epoch, err := strconv.ParseFloat(value, 64); err == nil && epoch >= 0 { 294 | trunc := math.Trunc(epoch) 295 | nanos := fractionToNanos(epoch - trunc) 296 | return time.Unix(int64(trunc), int64(nanos)), nil 297 | } 298 | 299 | return time.Parse(layout, value) 300 | } 301 | -------------------------------------------------------------------------------- /v2/tparse_test.go: -------------------------------------------------------------------------------- 1 | package tparse 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | const rfc3339 = "2006-01-02T15:04:05Z" 10 | 11 | // AddDuration 12 | 13 | func TestAddDurationRejectsSignWithoutDigits(t *testing.T) { 14 | t.Run("negative", func(t *testing.T) { 15 | _, err := AddDuration(time.Now(), "-") 16 | if err == nil { 17 | t.Errorf("(GOT): %v; (WNT): %v", err, "cannot parse sign without digits") 18 | } 19 | }) 20 | t.Run("positive", func(t *testing.T) { 21 | _, err := AddDuration(time.Now(), "+") 22 | if err == nil { 23 | t.Errorf("(GOT): %v; (WNT): %v", err, "cannot parse sign without digits") 24 | } 25 | }) 26 | } 27 | 28 | func TestAddDurationPositiveFractionalYear(t *testing.T) { 29 | start, err := Parse(time.RFC3339, "2003-07-02T15:04:05Z") 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | expected, err := Parse(time.RFC3339, "2006-01-02T15:04:05Z") 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | actual, err := AddDuration(start, "+2.5years") 40 | if err != nil { 41 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 42 | } 43 | 44 | if actual != expected { 45 | t.Errorf("Actual: %s; Expected: %s", actual, expected) 46 | } 47 | } 48 | 49 | func TestAddDurationNegativeFractionalYear(t *testing.T) { 50 | start, err := Parse(time.RFC3339, "2006-01-02T15:04:05Z") 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | expected, err := Parse(time.RFC3339, "2003-07-02T15:04:05Z") 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | actual, err := AddDuration(start, "-2.5years") 61 | if err != nil { 62 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 63 | } 64 | 65 | if actual != expected { 66 | t.Errorf("Actual: %s; Expected: %s", actual, expected) 67 | } 68 | } 69 | 70 | func TestAddDurationPositiveFractionalMonth(t *testing.T) { 71 | start, err := Parse(time.RFC3339, "2003-06-01T15:04:05Z") 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | 76 | expected, err := Parse(time.RFC3339, "2003-08-16T15:04:05Z") 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | 81 | actual, err := AddDuration(start, "+2.5months") 82 | if err != nil { 83 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 84 | } 85 | 86 | if actual != expected { 87 | t.Errorf("Actual: %s; Expected: %s", actual, expected) 88 | } 89 | } 90 | 91 | func TestAddDurationNegativeFractionalMonth(t *testing.T) { 92 | start, err := Parse(time.RFC3339, "2003-08-16T15:04:05Z") 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | 97 | expected, err := Parse(time.RFC3339, "2003-06-01T15:04:05Z") 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | 102 | actual, err := AddDuration(start, "-2.5months") 103 | if err != nil { 104 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 105 | } 106 | 107 | if actual != expected { 108 | t.Errorf("Actual: %s; Expected: %s", actual, expected) 109 | } 110 | } 111 | 112 | func TestAddDurationPositiveFractionalDay(t *testing.T) { 113 | start, err := Parse(time.RFC3339, "2003-06-01T15:04:05Z") 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | 118 | expected, err := Parse(time.RFC3339, "2003-06-04T03:04:05Z") 119 | if err != nil { 120 | t.Fatal(err) 121 | } 122 | 123 | actual, err := AddDuration(start, "+2.5days") 124 | if err != nil { 125 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 126 | } 127 | 128 | if actual != expected { 129 | t.Errorf("Actual: %s; Expected: %s", actual, expected) 130 | } 131 | } 132 | 133 | func TestAddDurationNegativeFractionalDay(t *testing.T) { 134 | start, err := Parse(time.RFC3339, "2003-06-04T03:04:05Z") 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | 139 | expected, err := Parse(time.RFC3339, "2003-06-01T15:04:05Z") 140 | if err != nil { 141 | t.Fatal(err) 142 | } 143 | 144 | actual, err := AddDuration(start, "-2.5days") 145 | if err != nil { 146 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 147 | } 148 | 149 | if actual != expected { 150 | t.Errorf("Actual: %s; Expected: %s", actual, expected) 151 | } 152 | } 153 | 154 | func TestAddDurationMissignUnits(t *testing.T) { 155 | t.Run("zero", func(t *testing.T) { 156 | _, err := AddDuration(time.Now(), "0") 157 | ensureError(t, err, "duration missing units") 158 | }) 159 | 160 | t.Run("plus zero", func(t *testing.T) { 161 | _, err := AddDuration(time.Now(), "+0") 162 | ensureError(t, err, "duration missing units") 163 | }) 164 | 165 | t.Run("minus zero", func(t *testing.T) { 166 | _, err := AddDuration(time.Now(), "-0") 167 | ensureError(t, err, "duration missing units") 168 | }) 169 | 170 | t.Run("one", func(t *testing.T) { 171 | _, err := AddDuration(time.Now(), "1") 172 | ensureError(t, err, "duration missing units") 173 | }) 174 | 175 | t.Run("float", func(t *testing.T) { 176 | _, err := AddDuration(time.Now(), "12.3") 177 | ensureError(t, err, "duration missing units") 178 | }) 179 | } 180 | 181 | // ParseWithMap 182 | 183 | func TestParseWithMapFloatingEpochPositive(t *testing.T) { 184 | actual, err := ParseWithMap("", "1445535988.5", nil) 185 | if err != nil { 186 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 187 | } 188 | 189 | nanos := int64(0.5 * float64(time.Second/time.Nanosecond)) 190 | 191 | expected := time.Unix(1445535988, nanos) 192 | if actual != expected { 193 | t.Errorf("Actual: %s; Expected: %s", actual, expected) 194 | } 195 | } 196 | 197 | func TestParseWithMapFloatingEpochNegative(t *testing.T) { 198 | _, err := ParseWithMap("", "-1445535988.5", nil) 199 | if _, ok := err.(*time.ParseError); err == nil || !ok { 200 | t.Errorf("Actual: %#v; Expected: %s", err, "negative floating point not allowed") 201 | } 202 | } 203 | 204 | func TestParseWithMap(t *testing.T) { 205 | before := time.Now().UTC() 206 | dict := map[string]time.Time{ 207 | "start": time.Now().UTC().AddDate(0, 0, -7), 208 | } 209 | after := time.Now().UTC() 210 | 211 | actual, err := ParseWithMap(time.ANSIC, "start+1week", dict) 212 | if err != nil { 213 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 214 | } 215 | 216 | actual = actual.UTC() 217 | if before.After(actual) || actual.After(after) { 218 | t.Errorf("Actual: %s; Expected between: %s and %s", actual, before, after) 219 | } 220 | } 221 | 222 | // ParseNow 223 | 224 | func TestParseNow(t *testing.T) { 225 | before := time.Now() 226 | actual, err := ParseNow("", "now") 227 | if err != nil { 228 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 229 | } 230 | after := time.Now() 231 | if before.After(actual) || actual.After(after) { 232 | t.Errorf("Actual: %s; Expected between: %s and %s", actual, before, after) 233 | } 234 | } 235 | 236 | func TestParseNowMinusSecond(t *testing.T) { 237 | before := time.Now().UTC().Add(-2 * time.Second) 238 | actual, err := ParseNow("", "now-2second") 239 | if err != nil { 240 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 241 | } 242 | after := time.Now().UTC().Add(-2 * time.Second) 243 | actual = actual.UTC() 244 | if before.After(actual) || actual.After(after) { 245 | t.Errorf("Actual: %s; Expected between: %s and %s", actual, before, after) 246 | } 247 | } 248 | 249 | func TestParseNowMinusMillisecond(t *testing.T) { 250 | before := time.Now() 251 | time.Sleep(10 * time.Millisecond) 252 | actual, err := ParseNow("", "now-10ms") 253 | if err != nil { 254 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 255 | } 256 | after := time.Now() 257 | if before.After(actual) || actual.After(after) { 258 | t.Errorf("Actual: %s; Expected between: %s and %s", actual, before, after) 259 | } 260 | } 261 | 262 | func TestParseNowPlusMillisecond(t *testing.T) { 263 | before := time.Now() 264 | actual, err := ParseNow("", "now+10ms") 265 | if err != nil { 266 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 267 | } 268 | time.Sleep(10 * time.Millisecond) 269 | after := time.Now() 270 | if before.After(actual) || actual.After(after) { 271 | t.Errorf("Actual: %s; Expected between: %s and %s", actual, before, after) 272 | } 273 | } 274 | 275 | func TestParseNowPlusQuarterDay(t *testing.T) { 276 | before := time.Now().UTC().Add(6 * time.Hour) 277 | actual, err := ParseNow("", "now+0.25day") 278 | if err != nil { 279 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 280 | } 281 | after := time.Now().UTC().Add(6 * time.Hour) 282 | actual = actual.UTC() 283 | if before.After(actual) || actual.After(after) { 284 | t.Errorf("Actual: %s; Expected between: %s and %s", actual, before, after) 285 | } 286 | } 287 | 288 | func TestParseNowPlusDay(t *testing.T) { 289 | before := time.Now().UTC().AddDate(0, 0, 1).Add(time.Hour).Add(time.Minute) 290 | actual, err := ParseNow("", "now+1h1d1m") 291 | if err != nil { 292 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 293 | } 294 | after := time.Now().UTC().AddDate(0, 0, 1).Add(time.Hour).Add(time.Minute) 295 | actual = actual.UTC() 296 | if before.After(actual) || actual.After(after) { 297 | t.Errorf("Actual: %s; Expected between: %s and %s", actual, before, after) 298 | } 299 | } 300 | 301 | func TestParseNowPlusAndMinus(t *testing.T) { 302 | before := time.Now().UTC().Add(time.Hour).AddDate(0, 0, -1).Add(time.Minute) 303 | actual, err := ParseNow("", "now+1h-1d+1m") 304 | if err != nil { 305 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 306 | } 307 | after := time.Now().UTC().Add(time.Hour).AddDate(0, 0, -1).Add(time.Minute) 308 | actual = actual.UTC() 309 | if before.After(actual) || actual.After(after) { 310 | t.Errorf("Actual: %s; Expected between: %s and %s", actual, before, after) 311 | } 312 | } 313 | 314 | func TestParseNowMinusAndPlus(t *testing.T) { 315 | before := time.Now().UTC().Add(-time.Hour*12).AddDate(0, 0, 34).Add(-time.Minute * 56) 316 | actual, err := ParseNow("", "now-12hour+34day-56min") 317 | if err != nil { 318 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 319 | } 320 | after := time.Now().UTC().Add(-time.Hour*12).AddDate(0, 0, 34).Add(-time.Minute * 56) 321 | actual = actual.UTC() 322 | if before.After(actual) || actual.After(after) { 323 | t.Errorf("Actual: %s; Expected between: %s and %s", actual, before, after) 324 | } 325 | } 326 | 327 | // Parse 328 | 329 | func TestParseLayout(t *testing.T) { 330 | actual, err := Parse(time.RFC3339, rfc3339) 331 | if err != nil { 332 | t.Errorf("Actual: %#v; Expected: %#v", err, nil) 333 | } 334 | expected := time.Unix(1136214245, 0) 335 | if !actual.Equal(expected) { 336 | t.Errorf("Actual: %d; Expected: %d", actual.Unix(), expected.Unix()) 337 | } 338 | } 339 | 340 | func ExampleAbsoluteDuration() { 341 | t1 := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) 342 | 343 | d1, err := AbsoluteDuration(t1, "1.5month") 344 | if err != nil { 345 | fmt.Println(err) 346 | return 347 | } 348 | 349 | fmt.Println(d1) 350 | 351 | t2 := time.Date(2020, time.February, 10, 23, 0, 0, 0, time.UTC) 352 | 353 | d2, err := AbsoluteDuration(t2, "1.5month") 354 | if err != nil { 355 | fmt.Println(err) 356 | return 357 | } 358 | 359 | fmt.Println(d2) 360 | // Output: 361 | // 1080h0m0s 362 | // 1056h0m0s 363 | } 364 | --------------------------------------------------------------------------------