427 lines
8.3 KiB
Go
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
|
|
}
|