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
|
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package utils
import (
"fmt"
"math"
"strconv"
"strings"
"time"
)
var _ = fmt.Print
func is_digit(x byte) bool {
return '0' <= x && x <= '9'
}
// The following is copied from the Go standard library to implement date range validation logic
// equivalent to the behaviour of Go's time.Parse.
func isLeap(year int) bool {
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
}
// daysInMonth is the number of days for non-leap years in each calendar month starting at 1
var daysInMonth = [13]int{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
func daysIn(m time.Month, year int) int {
if m == time.February && isLeap(year) {
return 29
}
return daysInMonth[int(m)]
}
func ISO8601Parse(raw string) (time.Time, error) {
orig := raw
raw = strings.TrimSpace(raw)
required_number := func(num_digits int) (int, error) {
if len(raw) < num_digits {
return 0, fmt.Errorf("Insufficient digits")
}
text := raw[:num_digits]
raw = raw[num_digits:]
ans, err := strconv.ParseUint(text, 10, 32)
if err == nil && ans <= math.MaxInt {
return int(ans), nil
}
return math.MaxInt, err
}
optional_separator := func(x byte) bool {
if len(raw) > 0 && raw[0] == x {
raw = raw[1:]
}
return len(raw) > 0 && is_digit(raw[0])
}
errf := func(msg string) (time.Time, error) {
return time.Time{}, fmt.Errorf("Invalid ISO8601 timestamp: %#v. %s", orig, msg)
}
optional_separator('+')
year, err := required_number(4)
if err != nil {
return errf("timestamp does not start with a 4 digit year")
}
var month int = 1
var day int = 1
if optional_separator('-') {
month, err = required_number(2)
if err != nil {
return errf("timestamp does not have a valid 2 digit month")
}
if optional_separator('-') {
day, err = required_number(2)
if err != nil {
return errf("timestamp does not have a valid 2 digit day")
}
}
}
var hour, minute, second int
var nsec int64
if len(raw) > 0 && (raw[0] == 'T' || raw[0] == ' ') {
raw = raw[1:]
hour, err = required_number(2)
if err != nil {
return errf("timestamp does not have a valid 2 digit hour")
}
if optional_separator(':') {
minute, err = required_number(2)
if err != nil {
return errf("timestamp does not have a valid 2 digit minute")
}
if optional_separator(':') {
second, err = required_number(2)
if err != nil {
return errf("timestamp does not have a valid 2 digit second")
}
}
}
if len(raw) > 0 && (raw[0] == '.' || raw[0] == ',') {
raw = raw[1:]
num_digits := 0
for len(raw) > num_digits && is_digit(raw[num_digits]) {
num_digits++
}
text := raw[:num_digits]
raw = raw[num_digits:]
extra := 9 - len(text)
if extra < 0 {
text = text[:9]
}
if text != "" {
if nsec, err = strconv.ParseInt(text, 10, 0); err != nil {
return errf("timestamp does not have a valid nanosecond field")
}
for ; extra > 0; extra-- {
nsec *= 10
}
}
}
}
switch {
case month < 1 || month > 12:
return errf("timestamp has invalid month value")
case day < 1 || day > 31 || day > daysIn(time.Month(month), year):
return errf("timestamp has invalid day value")
case hour < 0 || hour > 23:
return errf("timestamp has invalid hour value")
case minute < 0 || minute > 59:
return errf("timestamp has invalid minute value")
case second < 0 || second > 59:
return errf("timestamp has invalid second value")
}
loc := time.UTC
tzsign, tzhour, tzminute := 0, 0, 0
if len(raw) > 0 {
switch raw[0] {
case '+':
tzsign = 1
case '-':
tzsign = -1
}
}
if tzsign != 0 {
raw = raw[1:]
tzhour, err = required_number(2)
if err != nil {
return errf("timestamp has invalid timezone hour")
}
optional_separator(':')
tzminute, err = required_number(2)
if err != nil {
tzminute = 0
}
seconds := tzhour*3600 + tzminute*60
loc = time.FixedZone("", tzsign*seconds)
}
return time.Date(year, time.Month(month), day, hour, minute, second, int(nsec), loc), err
}
func ISO8601Format(x time.Time) string {
return x.Format(time.RFC3339Nano)
}
|