forked from kataras/iris
-
Notifications
You must be signed in to change notification settings - Fork 0
/
iso8601.go
226 lines (192 loc) · 5.76 KB
/
iso8601.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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
package jsonx
import (
"database/sql/driver"
"errors"
"fmt"
"strconv"
"strings"
"time"
// To load all system and embedded locations by name:
// _ "time/tzdata" // OR build with: -tags timetzdata
)
var fixedEastUTCLocations = make(map[int]*time.Location)
// RegisterFixedLocation should be called on initialization of the program.
// It registers a fixed location to the time parser.
//
// E.g. for input of 2023-02-04T09:48:14+03:00 to result a time string of 2023-02-04 09:48:14 +0300 EEST
// you have to RegisterFixedLocation("EEST", 10800) otherwise it will result to: 2023-02-04 09:48:14 +0300 +0300.
func RegisterFixedLocation(name string, secondsFromUTC int) {
loc := time.FixedZone(name, secondsFromUTC)
fixedEastUTCLocations[secondsFromUTC] = loc
}
func init() {
RegisterFixedLocation("EEST", 3*60*60) // + 3 hours.
RegisterFixedLocation("UTC", 0)
}
const (
// ISO8601Layout holds the time layout for the the javascript iso time.
// Read more at: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString.
ISO8601Layout = "2006-01-02T15:04:05"
// ISO8601ZLayout same as ISO8601Layout but with the timezone suffix.
ISO8601ZLayout = "2006-01-02T15:04:05Z"
// ISO8601ZUTCOffsetLayout ISO 8601 format, with full time and zone with UTC offset.
// Example: 2022-08-10T03:21:00.000000+03:00, 2023-02-04T09:48:14+00:00, 2022-08-09T00:00:00.000000.
ISO8601ZUTCOffsetLayout = "2006-01-02T15:04:05.999999Z07:00" // -07:00
)
// ISO8601 describes a time compatible with javascript time format.
type ISO8601 time.Time
// ParseISO8601 reads from "s" and returns the ISO8601 time.
func ParseISO8601(s string) (ISO8601, error) {
if s == "" || s == "null" {
return ISO8601{}, nil
}
var (
tt time.Time
err error
)
if idx := strings.LastIndexFunc(s, startUTCOffsetIndexFunc); idx > 18 /* should have some distance, with and without milliseconds */ {
length := parseSignedOffset(s[idx:])
if idx+1 > idx+length || len(s) <= idx+length+1 {
return ISO8601{}, fmt.Errorf("ISO8601: invalid timezone format: %s", s[idx:])
}
offsetText := s[idx+1 : idx+length]
offset, parseErr := strconv.Atoi(offsetText)
if parseErr != nil {
return ISO8601{}, fmt.Errorf("ISO8601: %w", parseErr)
}
// E.g. offset of +0300 is returned as 10800 which is - (3 * 60 * 60).
secondsEastUTC := offset * 60 * 60
if loc, ok := fixedEastUTCLocations[secondsEastUTC]; ok { // Specific (fixed) zone.
tt, err = time.ParseInLocation(ISO8601ZUTCOffsetLayout, s, loc)
} else { // Local or UTC.
tt, err = time.Parse(ISO8601ZUTCOffsetLayout, s)
}
} else if s[len(s)-1] == 'Z' {
tt, err = time.Parse(ISO8601ZLayout, s)
} else {
tt, err = time.Parse(ISO8601Layout, s)
}
if err != nil {
return ISO8601{}, fmt.Errorf("ISO8601: %w", err)
}
return ISO8601(tt), nil
}
// UnmarshalJSON parses the "b" into ISO8601 time.
func (t *ISO8601) UnmarshalJSON(b []byte) error {
if len(b) == 0 {
return nil
}
s := strings.Trim(string(b), `"`)
tt, err := ParseISO8601(s)
if err != nil {
return err
}
*t = tt
return nil
}
// MarshalJSON writes a quoted string in the ISO8601 time format.
func (t ISO8601) MarshalJSON() ([]byte, error) {
if s := t.String(); s != "" {
s = strconv.Quote(s)
return []byte(s), nil
}
return nullLiteral, nil // Note: if the front-end wants an empty string instead I must change that.
}
// ToTime returns the unwrapped *t to time.Time.
func (t *ISO8601) ToTime() time.Time {
tt := time.Time(*t)
return tt
}
// IsZero reports whether "t" is zero time.
// It completes the pg.Zeroer interface.
func (t ISO8601) IsZero() bool {
return time.Time(t).IsZero()
}
// String returns the text representation of the "t" using the ISO8601 time layout.
func (t ISO8601) String() string {
tt := t.ToTime()
if tt.IsZero() {
return ""
}
return tt.Format(ISO8601Layout)
}
// ToSimpleDate converts the current ISO8601 "t" to SimpleDate in specific location.
func (t ISO8601) ToSimpleDate(in *time.Location) SimpleDate {
if in == nil {
in = time.UTC
}
return SimpleDateFromTime(t.ToTime().In(in))
}
// Value returns the database value of time.Time.
func (t ISO8601) Value() (driver.Value, error) {
return time.Time(t), nil
}
// Scan completes the sql driver.Scanner interface.
func (t *ISO8601) Scan(src interface{}) error {
switch v := src.(type) {
case time.Time: // type was set to timestamp
if v.IsZero() {
return nil // don't set zero, ignore it.
}
*t = ISO8601(v)
case string:
tt, err := ParseISO8601(v)
if err != nil {
return err
}
*t = tt
case []byte:
return t.Scan(string(v))
case nil:
*t = ISO8601(time.Time{})
default:
return fmt.Errorf("ISO8601: unknown type of: %T", v)
}
return nil
}
// parseSignedOffset parses a signed timezone offset (e.g. "+03" or "-04").
// The function checks for a signed number in the range -23 through +23 excluding zero.
// Returns length of the found offset string or 0 otherwise.
//
// Language internal function.
func parseSignedOffset(value string) int {
sign := value[0]
if sign != '-' && sign != '+' {
return 0
}
x, rem, err := leadingInt(value[1:])
// fail if nothing consumed by leadingInt
if err != nil || value[1:] == rem {
return 0
}
if x > 23 {
return 0
}
return len(value) - len(rem)
}
var errLeadingInt = errors.New("ISO8601: time: bad [0-9]*") // never printed.
// leadingInt consumes the leading [0-9]* from s.
//
// Language internal function.
func leadingInt(s string) (x uint64, rem string, err error) {
i := 0
for ; i < len(s); i++ {
c := s[i]
if c < '0' || c > '9' {
break
}
if x > 1<<63/10 {
// overflow
return 0, "", errLeadingInt
}
x = x*10 + uint64(c) - '0'
if x > 1<<63 {
// overflow
return 0, "", errLeadingInt
}
}
return x, s[i:], nil
}
func startUTCOffsetIndexFunc(char rune) bool {
return char == '+' || char == '-'
}