-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathtparse.go
172 lines (163 loc) · 4.92 KB
/
tparse.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package tparse
import (
"bytes"
"fmt"
"math"
"strconv"
"strings"
"time"
"unicode"
)
// Parse will return the time value corresponding to the specified layout and value. It also parses
// floating point and integer epoch values.
func Parse(layout, value string) (time.Time, error) {
return ParseWithMap(layout, value, make(map[string]time.Time))
}
// ParseNow will return the time value corresponding to the specified layout and value. It also
// parses floating point and integer epoch values. It recognizes the special string `now` and
// replaces that with the time ParseNow is called. This allows a suffix adding or subtracting
// various values from the base time. For instance, ParseNow(time.ANSIC, "now+1d") will return a
// time corresponding to 24 hours from the moment the function is invoked.
//
// In addition to the duration abbreviations recognized by time.ParseDuration, it recognizes various
// tokens for days, weeks, months, and years.
//
// package main
//
// import (
// "fmt"
// "os"
// "time"
//
// "github.com/karrick/tparse"
// )
//
// func main() {
// actual, err := tparse.ParseNow(time.RFC3339, "now+1d3w4mo7y6h4m")
// if err != nil {
// fmt.Fprintf(os.Stderr, "error: %s\n", err)
// os.Exit(1)
// }
//
// fmt.Printf("time is: %s\n", actual)
// }
func ParseNow(layout, value string) (time.Time, error) {
m := map[string]time.Time{"now": time.Now()}
return ParseWithMap(layout, value, m)
}
// ParseWithMap will return the time value corresponding to the specified layout and value. It also
// parses floating point and integer epoch values. It accepts a map of strings to time.Time values,
// and if the value string starts with one of the keys in the map, it replaces the string with the
// corresponding time.Time value.
//
// package main
//
// import (
// "fmt"
// "os"
// "time"
//
// "github.com/karrick/tparse"
// )
//
// func main() {
// m := make(map[string]time.Time)
// m["start"] = start
//
// end, err := tparse.ParseWithMap(time.RFC3339, "start+8h", m)
// if err != nil {
// fmt.Fprintf(os.Stderr, "error: %s\n", err)
// os.Exit(1)
// }
//
// fmt.Printf("start: %s; end: %s\n", start, end)
// }
func ParseWithMap(layout, value string, dict map[string]time.Time) (time.Time, error) {
if epoch, err := strconv.ParseFloat(value, 64); err == nil && epoch >= 0 {
trunc := math.Trunc(epoch)
nanos := fractionToNanos(epoch - trunc)
return time.Unix(int64(trunc), int64(nanos)), nil
}
var base time.Time
var y, m, d int
var duration time.Duration
var direction = 1
var err error
for k, v := range dict {
if strings.HasPrefix(value, k) {
base = v
if len(value) > len(k) {
// maybe has +, -
switch dir := value[len(k)]; dir {
case '+':
// no-op
case '-':
direction = -1
default:
return base, fmt.Errorf("expected '+' or '-': %q", dir)
}
var nv string
y, m, d, nv = ymd(value[len(k)+1:])
if len(nv) > 0 {
duration, err = time.ParseDuration(nv)
if err != nil {
return base, err
}
}
}
if direction < 0 {
y = -y
m = -m
d = -d
}
return base.Add(time.Duration(int(duration)*direction)).AddDate(y, m, d), nil
}
}
return time.Parse(layout, value)
}
func fractionToNanos(fraction float64) int64 {
return int64(fraction * float64(time.Second/time.Nanosecond))
}
func ymd(value string) (int, int, int, string) {
// alternating numbers and strings
var y, m, d int
var accum int // accumulates digits
var unit []byte // accumulates units
var unproc []byte // accumulate unprocessed durations to return
unitComplete := func() {
// NOTE: compare byte slices because some units, i.e. ms, are multi-rune
if bytes.Equal(unit, []byte{'d'}) || bytes.Equal(unit, []byte{'d', 'a', 'y'}) || bytes.Equal(unit, []byte{'d', 'a', 'y', 's'}) {
d += accum
} else if bytes.Equal(unit, []byte{'w'}) || bytes.Equal(unit, []byte{'w', 'e', 'e', 'k'}) || bytes.Equal(unit, []byte{'w', 'e', 'e', 'k', 's'}) {
d += 7 * accum
} 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'}) {
m += accum
} else if bytes.Equal(unit, []byte{'y'}) || bytes.Equal(unit, []byte{'y', 'e', 'a', 'r'}) || bytes.Equal(unit, []byte{'y', 'e', 'a', 'r', 's'}) {
y += accum
} else {
unproc = append(append(unproc, strconv.Itoa(accum)...), unit...)
}
}
expectDigit := true
for _, rune := range value {
if unicode.IsDigit(rune) {
if expectDigit {
accum = accum*10 + int(rune-'0')
} else {
unitComplete()
unit = unit[:0]
accum = int(rune - '0')
}
continue
}
unit = append(unit, string(rune)...)
expectDigit = false
}
if len(unit) > 0 {
unitComplete()
accum = 0
unit = unit[:0]
}
// log.Printf("y: %d; m: %d; d: %d; nv: %q", y, m, d, unproc)
return y, m, d, string(unproc)
}