kitty/tools/utils/iso8601.go

167 lines
4.0 KiB
Go

// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package utils
import (
"fmt"
"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)
return int(ans), 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, nsec int
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 != "" {
n, err := strconv.ParseUint(text, 10, 64)
if err != nil {
return errf("timestamp does not have a valid nanosecond field")
}
nsec = int(n)
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, nsec, loc), err
}
func ISO8601Format(x time.Time) string {
return x.Format(time.RFC3339Nano)
}