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 }