gojs/goja/date_parser.go
2024-10-10 17:08:01 +08:00

427 lines
8.3 KiB
Go

package goja
import (
"strings"
)
type date struct {
year, month, day int
hour, min, sec, msec int
timeZoneOffset int // time zone offset in minutes
isLocal bool
}
func skip(s string, c byte) (string, bool) {
if len(s) > 0 && s[0] == c {
return s[1:], true
}
return s, false
}
func skipSpaces(s string) string {
for len(s) > 0 && s[0] == ' ' {
s = s[1:]
}
return s
}
func skipUntil(s string, stopList string) string {
for len(s) > 0 && !strings.ContainsRune(stopList, rune(s[0])) {
s = s[1:]
}
return s
}
func match(s string, lower string) (string, bool) {
if len(s) < len(lower) {
return s, false
}
for i := 0; i < len(lower); i++ {
c1 := s[i]
c2 := lower[i]
if c1 != c2 {
// switch to lower-case; 'a'-'A' is known to be a single bit
c1 |= 'a' - 'A'
if c1 != c2 || c1 < 'a' || c1 > 'z' {
return s, false
}
}
}
return s[len(lower):], true
}
func getDigits(s string, minDigits, maxDigits int) (int, string, bool) {
var i, v int
for i < len(s) && i < maxDigits && s[i] >= '0' && s[i] <= '9' {
v = v*10 + int(s[i]-'0')
i++
}
if i < minDigits {
return 0, s, false
}
return v, s[i:], true
}
func getMilliseconds(s string) (int, string) {
mul, v := 100, 0
if len(s) > 0 && (s[0] == '.' || s[0] == ',') {
const I_START = 1
i := I_START
for i < len(s) && i-I_START < 9 && s[i] >= '0' && s[i] <= '9' {
v += int(s[i]-'0') * mul
mul /= 10
i++
}
if i > I_START {
// only consume the separator if digits are present
return v, s[i:]
}
}
return 0, s
}
// [+-]HH:mm or [+-]HHmm or Z
func getTimeZoneOffset(s string, strict bool) (int, string, bool) {
if len(s) == 0 {
return 0, s, false
}
sign := s[0]
if sign == '+' || sign == '-' {
var hh, mm, v int
var ok bool
t := s[1:]
n := len(t)
if hh, t, ok = getDigits(t, 1, 9); !ok {
return 0, s, false
}
n -= len(t)
if strict && n != 2 && n != 4 {
return 0, s, false
}
for n > 4 {
n -= 2
hh /= 100
}
if n > 2 {
mm = hh % 100
hh = hh / 100
} else if t, ok = skip(t, ':'); ok {
if mm, t, ok = getDigits(t, 2, 2); !ok {
return 0, s, false
}
}
if hh > 23 || mm > 59 {
return 0, s, false
}
v = hh*60 + mm
if sign == '-' {
v = -v
}
return v, t, true
} else if sign == 'Z' {
return 0, s[1:], true
}
return 0, s, false
}
var tzAbbrs = []struct {
nameLower string
offset int
}{
{"gmt", 0}, // Greenwich Mean Time
{"utc", 0}, // Coordinated Universal Time
{"ut", 0}, // Universal Time
{"z", 0}, // Zulu Time
{"edt", -4 * 60}, // Eastern Daylight Time
{"est", -5 * 60}, // Eastern Standard Time
{"cdt", -5 * 60}, // Central Daylight Time
{"cst", -6 * 60}, // Central Standard Time
{"mdt", -6 * 60}, // Mountain Daylight Time
{"mst", -7 * 60}, // Mountain Standard Time
{"pdt", -7 * 60}, // Pacific Daylight Time
{"pst", -8 * 60}, // Pacific Standard Time
{"wet", +0 * 60}, // Western European Time
{"west", +1 * 60}, // Western European Summer Time
{"cet", +1 * 60}, // Central European Time
{"cest", +2 * 60}, // Central European Summer Time
{"eet", +2 * 60}, // Eastern European Time
{"eest", +3 * 60}, // Eastern European Summer Time
}
func getTimeZoneAbbr(s string) (int, string, bool) {
for _, tzAbbr := range tzAbbrs {
if s, ok := match(s, tzAbbr.nameLower); ok {
return tzAbbr.offset, s, true
}
}
return 0, s, false
}
var monthNamesLower = []string{
"jan",
"feb",
"mar",
"apr",
"may",
"jun",
"jul",
"aug",
"sep",
"oct",
"nov",
"dec",
}
func getMonth(s string) (int, string, bool) {
for i, monthNameLower := range monthNamesLower {
if s, ok := match(s, monthNameLower); ok {
return i + 1, s, true
}
}
return 0, s, false
}
func parseDateISOString(s string) (date, bool) {
if len(s) == 0 {
return date{}, false
}
var d = date{month: 1, day: 1}
var ok bool
// year is either yyyy digits or [+-]yyyyyy
sign := s[0]
if sign == '-' || sign == '+' {
s = s[1:]
if d.year, s, ok = getDigits(s, 6, 6); !ok {
return date{}, false
}
if sign == '-' {
if d.year == 0 {
// reject -000000
return date{}, false
}
d.year = -d.year
}
} else if d.year, s, ok = getDigits(s, 4, 4); !ok {
return date{}, false
}
if s, ok = skip(s, '-'); ok {
if d.month, s, ok = getDigits(s, 2, 2); !ok || d.month < 1 {
return date{}, false
}
if s, ok = skip(s, '-'); ok {
if d.day, s, ok = getDigits(s, 2, 2); !ok || d.day < 1 {
return date{}, false
}
}
}
if s, ok = skip(s, 'T'); ok {
if d.hour, s, ok = getDigits(s, 2, 2); !ok {
return date{}, false
}
if s, ok = skip(s, ':'); !ok {
return date{}, false
}
if d.min, s, ok = getDigits(s, 2, 2); !ok {
return date{}, false
}
if s, ok = skip(s, ':'); ok {
if d.sec, s, ok = getDigits(s, 2, 2); !ok {
return date{}, false
}
d.msec, s = getMilliseconds(s)
}
d.isLocal = true
}
// parse the time zone offset if present
if len(s) > 0 {
if d.timeZoneOffset, s, ok = getTimeZoneOffset(s, true); !ok {
return date{}, false
}
d.isLocal = false
}
// error if extraneous characters
return d, len(s) == 0
}
func parseDateOtherString(s string) (date, bool) {
var d = date{
year: 2001,
month: 1,
day: 1,
isLocal: true,
}
var nums [3]int
var numIndex int
var hasYear, hasMon, hasTime, ok bool
for {
s = skipSpaces(s)
if len(s) == 0 {
break
}
c := s[0]
n, val := len(s), 0
if c == '+' || c == '-' {
if hasTime {
if val, s, ok = getTimeZoneOffset(s, false); ok {
d.timeZoneOffset = val
d.isLocal = false
}
}
if !hasTime || !ok {
s = s[1:]
if val, s, ok = getDigits(s, 1, 9); ok {
d.year = val
if c == '-' {
if d.year == 0 {
return date{}, false
}
d.year = -d.year
}
hasYear = true
}
}
} else if val, s, ok = getDigits(s, 1, 9); ok {
if s, ok = skip(s, ':'); ok {
// time part
d.hour = val
if d.min, s, ok = getDigits(s, 1, 2); !ok {
return date{}, false
}
if s, ok = skip(s, ':'); ok {
if d.sec, s, ok = getDigits(s, 1, 2); !ok {
return date{}, false
}
d.msec, s = getMilliseconds(s)
}
hasTime = true
if t := skipSpaces(s); len(t) > 0 {
if t, ok = match(t, "pm"); ok {
if d.hour < 12 {
d.hour += 12
}
s = t
continue
} else if t, ok = match(t, "am"); ok {
if d.hour == 12 {
d.hour = 0
}
s = t
continue
}
}
} else if n-len(s) > 2 {
d.year = val
hasYear = true
} else if val < 1 || val > 31 {
d.year = val
if val < 100 {
d.year += 1900
}
if val < 50 {
d.year += 100
}
hasYear = true
} else {
if numIndex == 3 {
return date{}, false
}
nums[numIndex] = val
numIndex++
}
} else if val, s, ok = getMonth(s); ok {
d.month = val
hasMon = true
s = skipUntil(s, "0123456789 -/(")
} else if val, s, ok = getTimeZoneAbbr(s); ok {
d.timeZoneOffset = val
if len(s) > 0 {
if c := s[0]; (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') {
return date{}, false
}
}
d.isLocal = false
continue
} else if c == '(' {
// skip parenthesized phrase
level := 1
s = s[1:]
for len(s) > 0 && level != 0 {
if s[0] == '(' {
level++
} else if s[0] == ')' {
level--
}
s = s[1:]
}
if level > 0 {
return date{}, false
}
} else if c == ')' {
return date{}, false
} else {
if hasYear || hasMon || hasTime || numIndex > 0 {
return date{}, false
}
// skip a word
s = skipUntil(s, " -/(")
}
for len(s) > 0 && strings.ContainsRune("-/.,", rune(s[0])) {
s = s[1:]
}
}
n := numIndex
if hasYear {
n++
}
if hasMon {
n++
}
if n > 3 {
return date{}, false
}
switch numIndex {
case 0:
if !hasYear {
return date{}, false
}
case 1:
if hasMon {
d.day = nums[0]
} else {
d.month = nums[0]
}
case 2:
if hasYear {
d.month = nums[0]
d.day = nums[1]
} else if hasMon {
d.year = nums[1]
if nums[1] < 100 {
d.year += 1900
}
if nums[1] < 50 {
d.year += 100
}
d.day = nums[0]
} else {
d.month = nums[0]
d.day = nums[1]
}
case 3:
d.year = nums[2]
if nums[2] < 100 {
d.year += 1900
}
if nums[2] < 50 {
d.year += 100
}
d.month = nums[0]
d.day = nums[1]
default:
return date{}, false
}
return d, d.month > 0 && d.day > 0
}