// Copyright 2025 Dolthub, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package functions import ( "fmt" "math" "strconv" "strings" "time" "unicode" "github.com/dolthub/doltgresql/postgres/parser/duration" "github.com/cockroachdb/errors" "github.com/dolthub/go-mysql-server/sql" ) // This formatting implementation comes from postgres/src/backend/utils/adt/formatting.c var ( monthsFull = []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"} monthsShort = []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} wdaysFull = []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"} wdaysShort = []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"} rmMonthsLower = []string{"xii", "xi", "x", "ix", "viii", "vii", "vi", "v", "iv", "iii", "ii", "i"} ) // Time and date constants for formatting calculations const ( monthsPerYear = 12 hoursPerDay = 24 minsPerHour = 60 usecsPerSecs = 1000000 maxTzdispHour = 15 // Maximum allowed hour part for timezone display ) // formatNode represents a single element in a parsed format string type formatNode struct { typ formatNodeType // Type of the node (action, character, separator, space) characters []uint8 // Characters for literal text nodes suffix int key *keyword // Format keyword for action nodes } // formatNodeType defines the type of formatting node type formatNodeType uint const ( nodeTypeACTION formatNodeType = iota + 1 // Format action (e.g., YYYY, MM, DD) nodeTypeCHAR // Literal character nodeTypeSEPARATOR // Separator character nodeTypeSPACE // Whitespace ) // fromCharDateMode defines the date parsing mode type fromCharDateMode uint const ( fromCharDateNONE fromCharDateMode = iota // Value does not affect date mode fromCharDateGREGORIAN // Gregorian (day, month, year) style date fromCharDateISOWEEK // ISO 8601 week date ) // keywordID defines the keyword ID type keywordID uint const ( DCH_A_D keywordID = iota DCH_A_M DCH_AD DCH_AM DCH_B_C DCH_BC DCH_CC DCH_DAY DCH_DDD DCH_DD DCH_DY DCH_Day DCH_Dy DCH_D DCH_FF1 DCH_FF2 DCH_FF3 DCH_FF4 DCH_FF5 DCH_FF6 DCH_FX DCH_HH24 DCH_HH12 DCH_HH DCH_IDDD DCH_ID DCH_IW DCH_IYYY DCH_IYY DCH_IY DCH_I DCH_J DCH_MI DCH_MM DCH_MONTH DCH_MON DCH_MS DCH_Month DCH_Mon DCH_OF DCH_P_M DCH_PM DCH_Q DCH_RM DCH_SSSSS DCH_SSSS DCH_SS DCH_TZH DCH_TZM DCH_TZ DCH_US DCH_WW DCH_W DCH_Y_YYY DCH_YYYY DCH_YYY DCH_YY DCH_Y DCH_a_d DCH_a_m DCH_ad DCH_am DCH_b_c DCH_bc DCH_cc DCH_day DCH_ddd DCH_dd DCH_dy DCH_d DCH_ff1 DCH_ff2 DCH_ff3 DCH_ff4 DCH_ff5 DCH_ff6 DCH_fx DCH_hh24 DCH_hh12 DCH_hh DCH_iddd DCH_id DCH_iw DCH_iyyy DCH_iyy DCH_iy DCH_i DCH_j DCH_mi DCH_mm DCH_month DCH_mon DCH_ms DCH_of DCH_p_m DCH_pm DCH_q DCH_rm DCH_sssss DCH_ssss DCH_ss DCH_tzh DCH_tzm DCH_tz DCH_us DCH_ww DCH_w DCH_y_yyy DCH_yyyy DCH_yyy DCH_yy DCH_y NUM_COMMA NUM_DEC NUM_0 NUM_9 NUM_B NUM_C NUM_D NUM_E NUM_FM NUM_G NUM_L NUM_MI NUM_PL NUM_PR NUM_RN NUM_SG NUM_SP NUM_S NUM_TH NUM_V NUM_b NUM_c NUM_d NUM_e NUM_fm NUM_g NUM_l NUM_mi NUM_pl NUM_pr NUM_rn NUM_sg NUM_sp NUM_s NUM_th NUM_v ) // tmFromChar holds parsed date/time components from input string // For char->date/time conversion type tmFromChar struct { mode fromCharDateMode // Date parsing mode (Gregorian, ISO week or neither) hh int // Hour (12 or 24 hour format) pm int // 0 if AM; 1 if PM mi int // Minutes ss int // Seconds ssss int // Seconds since midnight d int // Day of week (1-7, Sunday = 1, 0 means missing) dd int // Day of month ddd int // Day of year mm int // Month ms int // Milliseconds year int // Year bc int // BC/AD indicator ww int // Week of year w int // Week of month cc int // Century j int // Julian day us int // Microseconds yysz int // Year size (YY=2, YYYY=4) is12clock bool // True for 12-hour clock format tzsign int // Timezone sign: +1, -1, or 0 if no TZH/TZM fields tzh int // Timezone hours tzm int // Timezone minutes ff int // Fractional precision // TODO: use these when we support TZ //has_tz bool // True if TZ field was present //gmtoffset int // GMT offset of fixed-offset zone abbreviation } // keywordSuffix represents format modifiers like FM (fill mode) or TH (ordinal) type keywordSuffix struct { name string // Name of the suffix/prefix len int // Length of the modifier id int isPostFix bool // True if this is a postfix modifier } const ( DCH_S_FM int = 0x01 DCH_S_TH int = 0x02 DCH_S_th int = 0x04 DCH_S_SP int = 0x08 DCH_S_TM int = 0x10 ) var orderedKeywordSuffixes = []*keywordSuffix{ {"FM", 2, DCH_S_FM, false}, {"fm", 2, DCH_S_FM, false}, {"TM", 2, DCH_S_TM, false}, {"tm", 2, DCH_S_TM, false}, {"TH", 2, DCH_S_TH, true}, {"th", 2, DCH_S_th, true}, {"SP", 2, DCH_S_SP, true}, } // keyword represents a format specifier (like YYYY, MM, DD, etc.) type keyword struct { name string // Name of the keyword (e.g., "YYYY", "MM") len int // Length of the keyword id keywordID isDigit bool // True if this keyword expects digits fcdMode fromCharDateMode // Date mode this keyword belongs to } var orderedDCHKeywords = []*keyword{ {"A.D.", 4, DCH_A_D, false, fromCharDateNONE}, {"A.M.", 4, DCH_A_M, false, fromCharDateNONE}, {"AD", 2, DCH_AD, false, fromCharDateNONE}, {"AM", 2, DCH_AM, false, fromCharDateNONE}, {"B.C.", 4, DCH_B_C, false, fromCharDateNONE}, {"BC", 2, DCH_BC, false, fromCharDateNONE}, {"CC", 2, DCH_CC, true, fromCharDateNONE}, {"DAY", 3, DCH_DAY, false, fromCharDateNONE}, {"DDD", 3, DCH_DDD, true, fromCharDateGREGORIAN}, {"DD", 2, DCH_DD, true, fromCharDateGREGORIAN}, {"DY", 2, DCH_DY, false, fromCharDateNONE}, {"Day", 3, DCH_Day, false, fromCharDateNONE}, {"Dy", 2, DCH_Dy, false, fromCharDateNONE}, {"D", 1, DCH_D, true, fromCharDateGREGORIAN}, {"FF1", 3, DCH_FF1, true, fromCharDateNONE}, {"FF2", 3, DCH_FF2, true, fromCharDateNONE}, {"FF3", 3, DCH_FF3, true, fromCharDateNONE}, {"FF4", 3, DCH_FF4, true, fromCharDateNONE}, {"FF5", 3, DCH_FF5, true, fromCharDateNONE}, {"FF6", 3, DCH_FF6, true, fromCharDateNONE}, {"FX", 2, DCH_FX, false, fromCharDateNONE}, {"HH24", 4, DCH_HH24, true, fromCharDateNONE}, {"HH12", 4, DCH_HH12, true, fromCharDateNONE}, {"HH", 2, DCH_HH, true, fromCharDateNONE}, {"IDDD", 4, DCH_IDDD, true, fromCharDateISOWEEK}, {"ID", 2, DCH_ID, true, fromCharDateISOWEEK}, {"IW", 2, DCH_IW, true, fromCharDateISOWEEK}, {"IYYY", 4, DCH_IYYY, true, fromCharDateISOWEEK}, {"IYY", 3, DCH_IYY, true, fromCharDateISOWEEK}, {"IY", 2, DCH_IY, true, fromCharDateISOWEEK}, {"I", 1, DCH_I, true, fromCharDateISOWEEK}, {"J", 1, DCH_J, true, fromCharDateNONE}, {"MI", 2, DCH_MI, true, fromCharDateNONE}, {"MM", 2, DCH_MM, true, fromCharDateGREGORIAN}, {"MONTH", 5, DCH_MONTH, false, fromCharDateGREGORIAN}, {"MON", 3, DCH_MON, false, fromCharDateGREGORIAN}, {"MS", 2, DCH_MS, true, fromCharDateNONE}, {"Month", 5, DCH_Month, false, fromCharDateGREGORIAN}, {"Mon", 3, DCH_Mon, false, fromCharDateGREGORIAN}, {"OF", 2, DCH_OF, false, fromCharDateNONE}, {"P.M.", 4, DCH_P_M, false, fromCharDateNONE}, {"PM", 2, DCH_PM, false, fromCharDateNONE}, {"Q", 1, DCH_Q, true, fromCharDateNONE}, {"RM", 2, DCH_RM, false, fromCharDateGREGORIAN}, {"SSSSS", 5, DCH_SSSS, true, fromCharDateNONE}, {"SSSS", 4, DCH_SSSS, true, fromCharDateNONE}, {"SS", 2, DCH_SS, true, fromCharDateNONE}, {"TZH", 3, DCH_TZH, false, fromCharDateNONE}, {"TZM", 3, DCH_TZM, true, fromCharDateNONE}, {"TZ", 2, DCH_TZ, false, fromCharDateNONE}, {"US", 2, DCH_US, true, fromCharDateNONE}, {"WW", 2, DCH_WW, true, fromCharDateGREGORIAN}, {"W", 1, DCH_W, true, fromCharDateGREGORIAN}, {"Y,YYY", 5, DCH_Y_YYY, true, fromCharDateGREGORIAN}, {"YYYY", 4, DCH_YYYY, true, fromCharDateGREGORIAN}, {"YYY", 3, DCH_YYY, true, fromCharDateGREGORIAN}, {"YY", 2, DCH_YY, true, fromCharDateGREGORIAN}, {"Y", 1, DCH_Y, true, fromCharDateGREGORIAN}, {"a.d.", 4, DCH_a_d, false, fromCharDateNONE}, {"a.m.", 4, DCH_a_m, false, fromCharDateNONE}, {"ad", 2, DCH_ad, false, fromCharDateNONE}, {"am", 2, DCH_am, false, fromCharDateNONE}, {"b.c.", 4, DCH_b_c, false, fromCharDateNONE}, {"bc", 2, DCH_bc, false, fromCharDateNONE}, {"cc", 2, DCH_CC, true, fromCharDateNONE}, {"day", 3, DCH_day, false, fromCharDateNONE}, {"ddd", 3, DCH_DDD, true, fromCharDateGREGORIAN}, {"dd", 2, DCH_DD, true, fromCharDateGREGORIAN}, {"dy", 2, DCH_dy, false, fromCharDateNONE}, {"d", 1, DCH_D, true, fromCharDateGREGORIAN}, {"ff1", 3, DCH_FF1, true, fromCharDateNONE}, {"ff2", 3, DCH_FF2, true, fromCharDateNONE}, {"ff3", 3, DCH_FF3, true, fromCharDateNONE}, {"ff4", 3, DCH_FF4, true, fromCharDateNONE}, {"ff5", 3, DCH_FF5, true, fromCharDateNONE}, {"ff6", 3, DCH_FF6, true, fromCharDateNONE}, {"fx", 2, DCH_FX, false, fromCharDateNONE}, {"hh24", 4, DCH_HH24, true, fromCharDateNONE}, {"hh12", 4, DCH_HH12, true, fromCharDateNONE}, {"hh", 2, DCH_HH, true, fromCharDateNONE}, {"iddd", 4, DCH_IDDD, true, fromCharDateISOWEEK}, {"id", 2, DCH_ID, true, fromCharDateISOWEEK}, {"iw", 2, DCH_IW, true, fromCharDateISOWEEK}, {"iyyy", 4, DCH_IYYY, true, fromCharDateISOWEEK}, {"iyy", 3, DCH_IYY, true, fromCharDateISOWEEK}, {"iy", 2, DCH_IY, true, fromCharDateISOWEEK}, {"i", 1, DCH_I, true, fromCharDateISOWEEK}, {"j", 1, DCH_J, true, fromCharDateNONE}, {"mi", 2, DCH_MI, true, fromCharDateNONE}, {"mm", 2, DCH_MM, true, fromCharDateGREGORIAN}, {"month", 5, DCH_month, false, fromCharDateGREGORIAN}, {"mon", 3, DCH_mon, false, fromCharDateGREGORIAN}, {"ms", 2, DCH_MS, true, fromCharDateNONE}, {"of", 2, DCH_OF, false, fromCharDateNONE}, {"p.m.", 4, DCH_p_m, false, fromCharDateNONE}, {"pm", 2, DCH_pm, false, fromCharDateNONE}, {"q", 1, DCH_Q, true, fromCharDateNONE}, {"rm", 2, DCH_rm, false, fromCharDateGREGORIAN}, {"sssss", 5, DCH_SSSS, true, fromCharDateNONE}, {"ssss", 4, DCH_SSSS, true, fromCharDateNONE}, {"ss", 2, DCH_SS, true, fromCharDateNONE}, {"tzh", 3, DCH_TZH, false, fromCharDateNONE}, {"tzm", 3, DCH_TZM, true, fromCharDateNONE}, {"tz", 2, DCH_tz, false, fromCharDateNONE}, {"us", 2, DCH_US, true, fromCharDateNONE}, {"ww", 2, DCH_WW, true, fromCharDateGREGORIAN}, {"w", 1, DCH_W, true, fromCharDateGREGORIAN}, {"y,yyy", 5, DCH_Y_YYY, true, fromCharDateGREGORIAN}, {"yyyy", 4, DCH_YYYY, true, fromCharDateGREGORIAN}, {"yyy", 3, DCH_YYY, true, fromCharDateGREGORIAN}, {"yy", 2, DCH_YY, true, fromCharDateGREGORIAN}, {"y", 1, DCH_Y, true, fromCharDateGREGORIAN}, } // isSpace returns true if the character is a space. func isSpace(s uint8) bool { return s == ' ' || s == '\t' } // isSeperateChar returns true if the character is a printable ASCII separator (not letter/digit) func isSeperateChar(s uint8) bool { return s > 0x20 && s < 0x7F && !unicode.IsUpper(rune(s)) && !unicode.IsLower(rune(s)) && !unicode.IsDigit(rune(s)) } // suffSearch searches for format modifiers prefixes or postfixes. // suffix id and length func suffSearch(str string, isPostfix bool) (int, int) { l := len(str) for _, s := range orderedKeywordSuffixes { if s.isPostFix != isPostfix { continue } if l >= s.len && str[:s.len] == s.name { return s.id, s.len } } return 0, 0 } // keywordSearch searches for format keywords at the beginning of the string func keywordSearch(str string) *keyword { l := len(str) for _, k := range orderedDCHKeywords { if l >= k.len && str[:k.len] == k.name { return k } } return nil } // parseFormat breaks down a format string into individual FormatNodes for processing func parseFormat(format string) ([]*formatNode, error) { var f []*formatNode formatPos := 0 for formatPos < len(format) { suffix := 0 // prefix if suffixId, suffixLen := suffSearch(format[formatPos:], false); suffixId != 0 { suffix |= suffixId formatPos += suffixLen } // keyword if k := keywordSearch(format[formatPos:]); k != nil { newNode := &formatNode{ typ: nodeTypeACTION, suffix: suffix, key: k, } formatPos += k.len // postfix if suffixId, suffixLen := suffSearch(format[formatPos:], true); suffixId != 0 { newNode.suffix |= suffixId formatPos += suffixLen } f = append(f, newNode) } else { if format[formatPos] == '"' { // Process double-quoted literal string, if any formatPos++ for formatPos < len(format) { if format[formatPos] == '"' { formatPos++ break } // backslash quotes the next character, if any if format[formatPos] == '\\' && formatPos+1 < len(format) { formatPos++ } newNode := &formatNode{ typ: nodeTypeCHAR, characters: []uint8{format[formatPos]}, } f = append(f, newNode) formatPos++ } } else { // Outside double-quoted strings, backslash is only special if // it immediately precedes a double quote. if format[formatPos] == '\\' && formatPos+1 < len(format) && format[formatPos+1] == '"' { formatPos++ } newNode := &formatNode{} if isSeperateChar(format[formatPos]) { newNode.typ = nodeTypeSEPARATOR } else if isSpace(format[formatPos]) { newNode.typ = nodeTypeSPACE } else { newNode.typ = nodeTypeCHAR } newNode.characters = []uint8{format[formatPos]} f = append(f, newNode) formatPos++ } } } return f, nil } // getDateTimeFromFormat parses an input string according to a format specification and returns a time.Time. func getDateTimeFromFormat(ctx *sql.Context, input, format string) (time.Time, error) { formatNodes, err := parseFormat(format) if err != nil { return time.Time{}, err } inputPos := 0 extraSkip := 0 fxMode := false var tfc tmFromChar for i, n := range formatNodes { if inputPos >= len(input) { break } if !fxMode && (n.typ != nodeTypeACTION || (n.key.name != "fx" && n.key.name != "FX")) && (n.typ == nodeTypeACTION || n == formatNodes[0]) { for isSpace(input[inputPos]) { inputPos++ extraSkip++ } } if n.typ == nodeTypeSPACE || n.typ == nodeTypeSEPARATOR { if !fxMode { // In non FX (fixed format) mode one format string space or // separator match to one space or separator in input string. // Or match nothing if there is no space or separator in the // current position of input string. extraSkip-- if isSpace(input[inputPos]) || isSeperateChar(input[inputPos]) { inputPos++ extraSkip++ } } else { // In FX mode, on format string space or separator we consume // exactly one character from input string. Notice we don't // insist that the consumed character match the format's // character. inputPos++ } continue } else if n.typ != nodeTypeACTION { if !fxMode { // In non FX mode we might have skipped some extra characters // (more than specified in format string) before. In this // case we don't skip input string character, because it might // be part of field. if extraSkip > 0 { extraSkip-- } else { inputPos++ } } else { inputPos += len(n.characters) } continue } if n.key.fcdMode != fromCharDateNONE { if tfc.mode == fromCharDateNONE { tfc.mode = n.key.fcdMode } else if tfc.mode != n.key.fcdMode { return time.Time{}, errors.Errorf(`invalid combination of date conventions`) } } switch n.key.id { case DCH_FX: fxMode = true case DCH_A_M, DCH_P_M, DCH_a_m, DCH_p_m: v, l, err := fromCharSeqSearch(n.key.name, input[inputPos:], []string{"a.m.", "p.m.", "A.M.", "P.M."}, tfc.pm, func(i int) int { return i % 2 }) if err != nil { return time.Time{}, err } tfc.pm = v inputPos += l tfc.is12clock = true case DCH_AM, DCH_PM, DCH_am, DCH_pm: v, l, err := fromCharSeqSearch(n.key.name, input[inputPos:], []string{"am", "pm", "AM", "PM"}, tfc.pm, func(i int) int { return i % 2 }) if err != nil { return time.Time{}, err } tfc.pm = v inputPos += l tfc.is12clock = true case DCH_HH, DCH_HH12: v, l, err := fromCharParseIntLen(input[inputPos:], 2, formatNodes[i:], tfc.hh) if err != nil { return time.Time{}, err } tfc.hh = v inputPos += l tfc.is12clock = true inputPos += skipTh(n.suffix) case DCH_HH24: v, l, err := fromCharParseIntLen(input[inputPos:], 2, formatNodes[i:], tfc.hh) if err != nil { return time.Time{}, err } tfc.hh = v inputPos += l inputPos += skipTh(n.suffix) case DCH_MI: v, l, err := fromCharParseIntLen(input[inputPos:], n.key.len, formatNodes[i:], tfc.mi) if err != nil { return time.Time{}, err } tfc.mi = v inputPos += l inputPos += skipTh(n.suffix) case DCH_SS: v, l, err := fromCharParseIntLen(input[inputPos:], n.key.len, formatNodes[i:], tfc.ss) if err != nil { return time.Time{}, err } tfc.ss = v inputPos += l inputPos += skipTh(n.suffix) case DCH_MS: // millisecond v, l, err := fromCharParseIntLen(input[inputPos:], 3, formatNodes[i:], tfc.ms) if err != nil { return time.Time{}, err } // 25 is 0.25 and 250 is 0.25 too; 025 is 0.025 and not 0.25 switch l { case 1: tfc.ms = v * 100 case 2: tfc.ms = v * 10 default: tfc.ms = v } inputPos += l inputPos += skipTh(n.suffix) case DCH_FF1, DCH_FF2, DCH_FF3, DCH_FF4, DCH_FF5, DCH_FF6: switch n.key.id { case DCH_FF1: tfc.ff = 1 case DCH_FF2: tfc.ff = 2 case DCH_FF3: tfc.ff = 3 case DCH_FF4: tfc.ff = 4 case DCH_FF5: tfc.ff = 5 case DCH_FF6: tfc.ff = 6 } fallthrough case DCH_US: l := 6 if tfc.ff != 0 { l = tfc.ff } // microsecond v, l, err := fromCharParseIntLen(input[inputPos:], l, formatNodes[i:], tfc.us) if err != nil { return time.Time{}, err } switch l { case 1: tfc.us = v * 100000 case 2: tfc.us = v * 10000 case 3: tfc.us = v * 1000 case 4: tfc.us = v * 100 case 5: tfc.us = v * 10 default: tfc.us = v } inputPos += l inputPos += skipTh(n.suffix) case DCH_SSSS: v, l, err := fromCharParseIntLen(input[inputPos:], n.key.len, formatNodes[i:], tfc.ssss) if err != nil { return time.Time{}, err } tfc.ssss = v inputPos += l inputPos += skipTh(n.suffix) case DCH_tz, DCH_TZ: // TODO: implement this return time.Time{}, errors.Errorf(`formatting TZ is not supported yet`) case DCH_OF: // OF is equivalent to TZH or TZH:TZM // see TZH comments below if input[inputPos] == '-' { tfc.tzsign = -1 inputPos++ } else if input[inputPos] == '+' || input[inputPos] == ' ' { tfc.tzsign = +1 inputPos++ } else { if extraSkip > 0 && input[inputPos-1] == '-' { tfc.tzsign = -1 } else { tfc.tzsign = +1 } } v, l, err := fromCharParseIntLen(input[inputPos:], 2, formatNodes[i:], tfc.tzh) if err != nil { return time.Time{}, err } tfc.tzh = v inputPos += l if inputPos < len(input[inputPos:]) && input[inputPos] == ':' { inputPos++ v, l, err = fromCharParseIntLen(input[inputPos:], 2, formatNodes[i:], tfc.tzm) if err != nil { return time.Time{}, err } tfc.tzm = v inputPos += l } case DCH_TZH: if input[inputPos] == '-' { tfc.tzsign = -1 inputPos++ } else if input[inputPos] == '+' || input[inputPos] == ' ' { tfc.tzsign = +1 inputPos++ } else { if extraSkip > 0 && input[inputPos-1] == '-' { tfc.tzsign = -1 } else { tfc.tzsign = +1 } } v, l, err := fromCharParseIntLen(input[inputPos:], 2, formatNodes[i:], tfc.tzh) if err != nil { return time.Time{}, err } tfc.tzh = v inputPos += l case DCH_TZM: // assign positive timezone sign if TZH was not seen before if tfc.tzsign == 0 { tfc.tzsign = +1 } v, l, err := fromCharParseIntLen(input[inputPos:], 2, formatNodes[i:], tfc.tzm) if err != nil { return time.Time{}, err } tfc.tzm = v inputPos += l case DCH_A_D, DCH_B_C, DCH_a_d, DCH_b_c: v, l, err := fromCharSeqSearch(n.key.name, input[inputPos:], []string{"a.d.", "b.c.", "A.D.", "B.C."}, tfc.bc, func(i int) int { return i % 2 }) if err != nil { return time.Time{}, err } tfc.bc = v inputPos += l case DCH_AD, DCH_BC, DCH_ad, DCH_bc: v, l, err := fromCharSeqSearch(n.key.name, input[inputPos:], []string{"ad", "bc", "AD", "BC"}, tfc.bc, func(i int) int { return i % 2 }) if err != nil { return time.Time{}, err } tfc.bc = v inputPos += l case DCH_MONTH, DCH_Month, DCH_month: v, l, err := fromCharSeqSearch(n.key.name, input[inputPos:], monthsFull, tfc.mm, func(i int) int { return i }) if err != nil { return time.Time{}, err } tfc.mm = v + 1 inputPos += l case DCH_MON, DCH_Mon, DCH_mon: v, l, err := fromCharSeqSearch(n.key.name, input[inputPos:], monthsShort, tfc.mm, func(i int) int { return i }) if err != nil { return time.Time{}, err } tfc.mm = v + 1 inputPos += l case DCH_MM: val, l, err := fromCharParseIntLen(input[inputPos:], n.key.len, formatNodes[i:], tfc.mm) if err != nil { return time.Time{}, err } tfc.mm = val inputPos += l inputPos += skipTh(n.suffix) case DCH_DAY, DCH_Day, DCH_day: v, l, err := fromCharSeqSearch(n.key.name, input[inputPos:], wdaysFull, tfc.d, func(i int) int { return i }) if err != nil { return time.Time{}, err } tfc.d = v + 1 inputPos += l case DCH_DY, DCH_Dy, DCH_dy: v, l, err := fromCharSeqSearch(n.key.name, input[inputPos:], wdaysShort, tfc.d, func(i int) int { return i }) if err != nil { return time.Time{}, err } tfc.d = v + 1 inputPos += l case DCH_DDD: v, l, err := fromCharParseIntLen(input[inputPos:], n.key.len, formatNodes[i:], tfc.ddd) if err != nil { return time.Time{}, err } tfc.ddd = v inputPos += l inputPos += skipTh(n.suffix) case DCH_IDDD: v, l, err := fromCharParseIntLen(input[inputPos:], 3, formatNodes[i:], tfc.ddd) if err != nil { return time.Time{}, err } tfc.ddd = v inputPos += l inputPos += skipTh(n.suffix) case DCH_DD: v, l, err := fromCharParseIntLen(input[inputPos:], n.key.len, formatNodes[i:], tfc.dd) if err != nil { return time.Time{}, err } tfc.dd = v inputPos += l inputPos += skipTh(n.suffix) case DCH_D: v, l, err := fromCharParseIntLen(input[inputPos:], n.key.len, formatNodes[i:], tfc.d) if err != nil { return time.Time{}, err } tfc.d = v inputPos += l inputPos += skipTh(n.suffix) case DCH_ID: v, l, err := fromCharParseIntLen(input[inputPos:], 1, formatNodes[i:], tfc.dd) if err != nil { return time.Time{}, err } // Shift numbering to match Gregorian where Sunday = 1 if v+1 > 7 { tfc.d = 1 } else { tfc.d = v } inputPos += l inputPos += skipTh(n.suffix) case DCH_WW, DCH_IW: v, l, err := fromCharParseIntLen(input[inputPos:], n.key.len, formatNodes[i:], tfc.ww) if err != nil { return time.Time{}, err } tfc.ww = v inputPos += l inputPos += skipTh(n.suffix) case DCH_Q: // we parse but ignore _, l, err := fromCharParseIntLen(input[inputPos:], n.key.len, formatNodes[i:], 0) if err != nil { return time.Time{}, err } inputPos += l inputPos += skipTh(n.suffix) case DCH_CC: v, l, err := fromCharParseIntLen(input[inputPos:], n.key.len, formatNodes[i:], tfc.cc) if err != nil { return time.Time{}, err } tfc.cc = v inputPos += l inputPos += skipTh(n.suffix) case DCH_Y_YYY: var millennia, years int parts := strings.Split(input[inputPos:], ",") if len(parts) < 2 { return time.Time{}, errors.Errorf(`invalid input string for "Y,YYY"`) } millenniaStr := strings.TrimSpace(parts[0]) yearsStr := strings.TrimSpace(parts[1]) matched := 0 if m, err := strconv.Atoi(millenniaStr); err == nil { millennia = m matched++ } used := 0 for _, l := range yearsStr { if unicode.IsDigit(l) { used += 1 } else { break } } if used != 3 { return time.Time{}, errors.Errorf(`invalid input string for "Y,YYY"`) } if y, err := strconv.Atoi(yearsStr[:used]); err == nil { years = y matched++ } if matched < 2 { return time.Time{}, errors.Errorf(`invalid input string for "Y,YYY"`) } // Check for overflow on multiplication if millennia > math.MaxInt32/1000 || millennia < math.MinInt32/1000 { return time.Time{}, errors.Errorf(`value for "Y,YYY" in source string is out of range`) } millennia *= 1000 // Check for overflow on addition if (years > 0 && millennia > math.MaxInt32-years) || (years < 0 && millennia < math.MinInt32-years) { return time.Time{}, errors.Errorf(`value for "Y,YYY" in source string is out of range`) } years += millennia tfc.year = years tfc.yysz = 4 inputPos += 5 inputPos += skipTh(n.suffix) case DCH_YYYY, DCH_IYYY: v, l, err := fromCharParseIntLen(input[inputPos:], n.key.len, formatNodes[i:], tfc.year) if err != nil { return time.Time{}, err } tfc.year = v tfc.yysz = 4 inputPos += l inputPos += skipTh(n.suffix) case DCH_YYY, DCH_IYY: v, l, err := fromCharParseIntLen(input[inputPos:], n.key.len, formatNodes[i:], tfc.year) if err != nil { return time.Time{}, err } if l < 4 { tfc.year = adjustPartialYearTo2020(v) } else { tfc.year = v } tfc.yysz = 3 inputPos += l inputPos += skipTh(n.suffix) case DCH_YY, DCH_IY: v, l, err := fromCharParseIntLen(input[inputPos:], n.key.len, formatNodes[i:], tfc.year) if err != nil { return time.Time{}, err } if l < 4 { tfc.year = adjustPartialYearTo2020(v) } else { tfc.year = v } tfc.yysz = 2 inputPos += l inputPos += skipTh(n.suffix) case DCH_Y, DCH_I: v, l, err := fromCharParseIntLen(input[inputPos:], n.key.len, formatNodes[i:], tfc.year) if err != nil { return time.Time{}, err } if l < 4 { tfc.year = adjustPartialYearTo2020(v) } else { tfc.year = v } tfc.yysz = 1 inputPos += l inputPos += skipTh(n.suffix) case DCH_RM, DCH_rm: v, l, err := fromCharSeqSearch(n.key.name, input[inputPos:], rmMonthsLower, tfc.mm, func(i int) int { return monthsPerYear - i }) if err != nil { return time.Time{}, err } tfc.mm = v inputPos += l case DCH_W: v, l, err := fromCharParseIntLen(input[inputPos:], n.key.len, formatNodes[i:], tfc.w) if err != nil { return time.Time{}, err } tfc.w = v inputPos += l inputPos += skipTh(n.suffix) case DCH_J: v, l, err := fromCharParseIntLen(input[inputPos:], n.key.len, formatNodes[i:], tfc.j) if err != nil { return time.Time{}, err } tfc.j = v inputPos += l inputPos += skipTh(n.suffix) default: } /* Ignore all spaces after fields */ if !fxMode { extraSkip = 0 for inputPos < len(input) && isSpace(input[inputPos]) { inputPos++ extraSkip++ } } } for inputPos < len(input) && isSpace(input[inputPos]) { inputPos++ } if inputPos < len(input) { return time.Time{}, errors.Errorf(`trailing characters remain in input string after datetime format`) } return getTime(ctx, tfc) } // getTime converts parsed time components from tmFromChar into a time.Time value func getTime(ctx *sql.Context, t tmFromChar) (time.Time, error) { var ( year int month int day int hour int minute int second int nanosecond int fsec int gmtOffset int has_tz bool ) if t.ssss != 0 { x := t.ssss hour = x / duration.SecsPerHour x %= duration.SecsPerHour minute = x / duration.SecsPerMinute x %= duration.SecsPerMinute second = x } if t.ss != 0 { second = t.ss } if t.mi != 0 { minute = t.mi } if t.hh != 0 { hour = t.hh } if t.is12clock { if hour < 1 || hour > hoursPerDay/2 { return time.Time{}, errors.Errorf(`hour "%d" is invalid for the 12-hour clock`, hour) } if t.pm != 0 && hour < hoursPerDay/2 { hour += hoursPerDay / 2 } else if t.pm == 0 && hour == hoursPerDay/2 { hour = 0 } } if t.year != 0 { // If CC and YY are provided, combine them considering century boundaries if t.cc != 0 && t.yysz <= 2 { if t.bc != 0 { t.cc = -t.cc } year = t.year % 100 if year != 0 { if t.cc >= 0 { // year += (t.cc - 1) * 100 tmp, err := checkForOverflow((t.cc - 1) * 100) if err != nil { return time.Time{}, err } year, err = checkForOverflow(year + tmp) if err != nil { return time.Time{}, err } } else { // year = (t.cc + 1) * 100 - year + 1 tmp, err := checkForOverflow((t.cc + 1) * 100) if err != nil { return time.Time{}, err } tmp, err = checkForOverflow(tmp - year) if err != nil { return time.Time{}, err } year, err = checkForOverflow(tmp + 1) if err != nil { return time.Time{}, err } } } else { // find century year for dates ending in "00" year = t.cc * 100 if t.cc < 0 { year = year + 1 } } } else { // If a 4-digit year is provided, we use that and ignore CC. year = t.year if t.bc != 0 { year = -year } // correct for our representation of BC years if year < 0 { year++ } } } else if t.cc != 0 { // use first year of century if t.bc != 0 { t.cc = -t.cc } if t.cc >= 0 { // Add 1 because 21st century started in 2001, not 2000 // year = (t.cc - 1) * 100 + 1 tmp, err := checkForOverflow((t.cc - 1) * 100) if err != nil { return time.Time{}, err } t.year, err = checkForOverflow(tmp + 1) if err != nil { return time.Time{}, err } } else { // Add 1 because year 599 represents 600 BC // year = t.cc * 100 + 1; tmp, err := checkForOverflow(t.cc * 100) if err != nil { return time.Time{}, err } t.year, err = checkForOverflow(tmp + 1) if err != nil { return time.Time{}, err } } } if t.j != 0 { year, month, day = j2Date(t.j) } if t.ww != 0 { if t.mode == fromCharDateISOWEEK { // If t.d is not set, then the date is left at the beginning of // the ISO week (Monday). if t.d != 0 { year, month, day = isoWeekDate2Date(year, t.ww, t.d) } else { year, month, day = isoWeek2Date(year, t.ww) } } else { // t.ddd = (t.ww - 1) * 7 + 1 var err error if t.ddd, err = checkForOverflow(t.ww - 1); err != nil { return time.Time{}, err } if t.ddd, err = checkForOverflow(t.ddd * 7); err != nil { return time.Time{}, err } if t.ddd, err = checkForOverflow(t.ddd + 1); err != nil { return time.Time{}, err } } } if t.w != 0 { // t.dd = (t.w - 1) * 7 + 1 var err error if t.dd, err = checkForOverflow(t.w - 1); err != nil { return time.Time{}, err } if t.dd, err = checkForOverflow(t.dd * 7); err != nil { return time.Time{}, err } if t.dd, err = checkForOverflow(t.dd + 1); err != nil { return time.Time{}, err } } if t.dd != 0 { day = t.dd } if t.mm != 0 { month = t.mm } if t.ddd != 0 && (month <= 1 || day <= 1) { if year == 0 && t.bc == 0 { return time.Time{}, errors.Errorf(`cannot calculate day of year without year information`) } if t.mode == fromCharDateISOWEEK { // zeroth day of the ISO year, in Julian j0 := isoWeek2J(year, 1) - 1 year, month, day = j2Date(j0 + t.ddd) } else { ysum := [2][13]int{ {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}, {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}, } var y []int if isLeapYear(year) { y = ysum[1][:] } else { y = ysum[0][:] } var i int for i = 1; i <= monthsPerYear; i++ { if t.ddd <= y[i] { break } } if month <= 1 { month = i // mon is 0-11, so it might need adjustment later } if day <= 1 { day = t.ddd - y[i-1] } } } if t.ms != 0 { // *fsec += t.ms * 1000 var err error fsec, err = checkForOverflow(t.ms * 1000) if err != nil { return time.Time{}, err } } if t.us != 0 { fsec += t.us } if hour < 0 || hour >= hoursPerDay || minute < 0 || minute >= 60 || second < 0 || second >= duration.SecsPerMinute || fsec < 0 || fsec > usecsPerSecs { return time.Time{}, errors.Errorf(`date/time field value out of range`) } if t.tzsign != 0 { // TZH and/or TZM fields if t.tzh < 0 || t.tzh > maxTzdispHour || t.tzm < 0 || t.tzm >= minsPerHour { return time.Time{}, errors.Errorf(`date/time field value out of range`) } has_tz = true gmtOffset = (t.tzh*minsPerHour + t.tzm) * duration.SecsPerMinute if t.tzsign > 0 { gmtOffset = -gmtOffset } } if fsec != 0 { nanosecond = fsec * int(time.Microsecond) } // Get server timezone to interpret the parsed timestamp serverLoc, err := GetServerLocation(ctx) if err != nil { return time.Time{}, err } if year == 0 { year = 1970 } if month == 0 { month = 1 } if day == 0 { day = 1 } // Create the time in the server timezone result := time.Date(year, time.Month(month), day, hour, minute, second, nanosecond, serverLoc) // Validate the created time (handles invalid dates like Feb 30) if result.Year() != year || int(result.Month()) != month || result.Day() != day { return time.Time{}, errors.Errorf(`date/time field value out of range`) } // Convert the result to specified timezone if has_tz { _, serverLocOffset := result.Zone() result = result.Add(time.Duration(gmtOffset+serverLocOffset) * time.Second) } return result, nil } // fromCharSeqSearch finds a matching string from an array and returns its index and length func fromCharSeqSearch(keyName string, input string, arr []string, curVal int, calculateVal func(int) int) (int, int, error) { var v int matchFound := false l := 0 for i, elem := range arr { // TODO: support collation when matching if strings.HasPrefix(strings.ToLower(input), strings.ToLower(elem)) { v = i l += len(elem) matchFound = true break } } if !matchFound { s := strings.Split(input, " ") if len(s) > 0 { l = len(s[0]) } return 0, 0, errors.Errorf(`invalid value "%s" for "%s"`, input[:l], keyName) } val, err := verifyVal(keyName, curVal, calculateVal(v)) if err != nil { return 0, 0, err } return val, l, nil } // verifyVal checks if the current value conflicts with a new value for the same field func verifyVal(keyName string, curVal, newVal int) (int, error) { if curVal != 0 && curVal != newVal { return 0, errors.Errorf(`conflicting values for "%s" field in formatting string`, keyName) } return newVal, nil } // checkForOverflow validates that a value fits within int32 range func checkForOverflow(v int) (int, error) { if v < math.MinInt32 || v > math.MaxInt32 { return 0, errors.Errorf(`date/time field value out of range`) } return v, nil } // isoWeekDate2Date converts an ISO week date (year, week, weekday) to a Gregorian date using PostgreSQL's algorithm func isoWeekDate2Date(year, isoweek, wday int) (int, int, int) { jday := isoWeek2J(year, isoweek) if wday > 1 { jday += wday - 2 } else { jday += 6 } return j2Date(jday) } // isoWeek2Date converts a year and ISO week to the Gregorian date of the first day of that week func isoWeek2Date(year, woy int) (int, int, int) { // 1. Calculate the Julian day for the start of the ISO week. jday := isoWeek2J(year, woy) // 2. Convert that Julian day number to a Gregorian date. return j2Date(jday) } // isoWeek2J returns the Julian day corresponding to the first day (Monday) of an ISO 8601 year and week func isoWeek2J(year, week int) int { var day0, day4 int // fourth day of current year day4 = date2J(year, 1, 4) // day0 == offset to first day of week (Monday) day0 = j2Day(day4 - 1) return (week-1)*7 + (day4 - day0) } // date2J converts a Gregorian date to a Julian day number using PostgreSQL's algorithm func date2J(year, month, day int) int { var julian int var century int if month > 2 { month++ year += 4800 } else { month += 13 year += 4799 } century = year / 100 julian = year*365 - 32167 julian += year/4 - century + century/4 julian += 7834*month/256 + day return julian } // j2Day converts a Julian day number to a day of the week (0=Sunday, 6=Saturday) // This function implements the specific PostgreSQL algorithm. func j2Day(date int) int { date++ date %= 7 if date < 0 { date += 7 } return date } // j2Date converts a Julian day number back to a Gregorian date. // This function implements the inverse of the date2J algorithm. func j2Date(jd int) (year, month, day int) { var julian uint var quad uint var extra uint var y int julian = uint(jd) julian += 32044 quad = julian / 146097 extra = (julian-quad*146097)*4 + 3 julian += 60 + quad*3 + extra/146097 quad = julian / 1461 julian -= quad * 1461 y = int(julian*4) / 1461 if y != 0 { julian = (julian+305)%365 + 123 } else { julian = (julian+306)%366 + 123 } y += int(quad * 4) year = y - 4800 quad = julian * 2141 / 65536 day = int(julian) - 7834*int(quad)/256 month = int(quad+10)%monthsPerYear + 1 return year, month, day } // isLeapYear returns true if the given year is a leap year. func isLeapYear(year int) bool { // A year is a leap year if it has 366 days. // We can check this by getting the Day of the Year for March 1st // of the given year. If it's day 61, it means February had 29 days. return time.Date(year, time.March, 1, 0, 0, 0, 0, time.UTC).YearDay() == 61 } // adjustPartialYearTo2020 adjusts partial years to reasonable 4-digit years based on PostgreSQL rules func adjustPartialYearTo2020(year int) int { /* Force 0-69 into the 2000's */ if year < 70 { return year + 2000 } else if year < 100 { /* Force 70-99 into the 1900's */ return year + 1900 } else if year < 520 { /* Force 100-519 into the 2000's */ return year + 2000 } else if year < 1000 { /* Force 520-999 into the 1000's */ return year + 1000 } else { return year } } // skipTh returns 2 if there is a "TH" or "th" postfix; otherwise returns 0 func skipTh(suff int) int { if suff&DCH_S_TH != 0 || suff&DCH_S_th != 0 { return 2 } return 0 } // fromCharParseIntLen parses an integer from input string with specified length constraints func fromCharParseIntLen(input string, length int, nodes []*formatNode, curVal int) (int, int, error) { // Skip whitespace input = strings.TrimLeftFunc(input, unicode.IsSpace) n := nodes[0] isFMorIsNextSeperator := n.suffix&DCH_S_FM != 0 || isNextSeperator(nodes) if !isFMorIsNextSeperator && len(input) < length { return 0, 0, errors.Errorf(`source string too short for "%s" formatting field`, n.key.name) } used := 0 negative := false for i, l := range input { // if fill mode is true, slurp as many characters as we can get // otherwise, get only given length many if i == 0 && l == '-' { negative = true } else if unicode.IsDigit(l) && (isFMorIsNextSeperator || used < length) { used++ } else { break } } s := input[:used] if negative { s = input[:used+1] } res, err := strconv.ParseInt(s, 10, 64) if err != nil || used == 0 || (!isFMorIsNextSeperator && used > 0 && used < length) { if used < length && len(input) >= length { used = length } return 0, 0, errors.Errorf(`invalid value "%s" for "%s"`, input[:used], n.key.name) } if res < math.MinInt32 || res > math.MaxInt32 { return 0, 0, errors.Errorf(`value for "%s" in source string is out of range`, n.key.name) } v, err := verifyVal(n.key.name, curVal, int(res)) if err != nil { return 0, 0, err } // result, length of result return v, len(s), nil } // isNextSeperator checks if the next node in the format is a separator or end of format func isNextSeperator(nodes []*formatNode) bool { n := nodes[0] if len(nodes) == 1 { return true } if n.typ == nodeTypeACTION && skipTh(n.suffix) > 0 { return true } // next node n = nodes[1] if n.typ == nodeTypeACTION { return !n.key.isDigit } else if len(n.characters) != 0 && unicode.IsDigit(rune(n.characters[0])) { return false } return true } type tmToChar struct { sec int min int hour int mday int mon int year int wday int yday int gmtoff int fsec int64 /* fractional seconds */ tzn string /* timezone */ } func tsToChar(t *tmToChar, format string, isInterval bool) (string, error) { formatNodes, err := parseFormat(format) if err != nil { return "", err } var s string for i, n := range formatNodes { if n.typ != nodeTypeACTION { s += string(n.characters) continue } switch n.key.id { case DCH_A_M, DCH_P_M: if (t.hour % hoursPerDay) >= hoursPerDay/2 { s += "P.M." } else { s += "A.M." } case DCH_AM, DCH_PM: if (t.hour % hoursPerDay) >= hoursPerDay/2 { s += "PM" } else { s += "AM" } case DCH_a_m, DCH_p_m: if (t.hour % hoursPerDay) >= hoursPerDay/2 { s += "p.m." } else { s += "a.m." } case DCH_am, DCH_pm: if (t.hour % hoursPerDay) >= hoursPerDay/2 { s += "pm" } else { s += "am" } case DCH_HH, DCH_HH12: // display time as shown on a 12-hour clock, even for intervals width := 3 if n.suffix&DCH_S_FM != 0 { width = 0 } else if t.hour >= 0 { width = 2 } h := t.hour % (hoursPerDay / 2) if h == 0 { h = hoursPerDay / 2 } s += makeNumTh(getStringFromIntWithWidth(width, h), n.suffix) case DCH_HH24: width := 3 if n.suffix&DCH_S_FM != 0 { width = 0 } else if t.hour >= 0 { width = 2 } s += makeNumTh(getStringFromIntWithWidth(width, t.hour), n.suffix) case DCH_MI: width := 3 if n.suffix&DCH_S_FM != 0 { width = 0 } else if t.min >= 0 { width = 2 } s += makeNumTh(getStringFromIntWithWidth(width, t.min), n.suffix) case DCH_SS: width := 3 if n.suffix&DCH_S_FM != 0 { width = 0 } else if t.sec >= 0 { width = 2 } s += makeNumTh(getStringFromIntWithWidth(width, t.sec), n.suffix) case DCH_FF1: s += makeNumTh(getStringFromIntWithWidth(1, int(t.fsec/1000)), n.suffix) case DCH_FF2: s += makeNumTh(getStringFromIntWithWidth(2, int(t.fsec/1000)), n.suffix) case DCH_FF3, DCH_MS: // millisecond s += makeNumTh(getStringFromIntWithWidth(3, int(t.fsec/1000)), n.suffix) case DCH_FF4: s += makeNumTh(getStringFromIntWithWidth(4, int(t.fsec/1000)), n.suffix) case DCH_FF5: s += makeNumTh(getStringFromIntWithWidth(5, int(t.fsec/1000)), n.suffix) case DCH_FF6, DCH_US: // microsecond s += makeNumTh(getStringFromIntWithWidth(6, int(t.fsec)), n.suffix) case DCH_SSSS: s += makeNumTh(fmt.Sprintf("%d", t.hour*duration.SecsPerHour+t.min*duration.SecsPerMinute+t.sec), n.suffix) case DCH_tz: if isInterval { return "", errors.Errorf("invalid format specification for an interval value") } s += strings.ToLower(t.tzn) case DCH_TZ: if isInterval { return "", errors.Errorf("invalid format specification for an interval value") } s += strings.ToUpper(t.tzn) case DCH_TZH: if isInterval { return "", errors.Errorf("invalid format specification for an interval value") } sign := "+" if t.gmtoff < 0 { sign = "-" } s += fmt.Sprintf("%s%s", sign, getStringFromIntWithWidth(2, int(math.Abs(float64(t.gmtoff/duration.SecsPerHour))))) case DCH_TZM: if isInterval { return "", errors.Errorf("invalid format specification for an interval value") } s += getStringFromIntWithWidth(2, int(math.Abs(float64(t.gmtoff%duration.SecsPerHour)/duration.SecsPerMinute))) case DCH_OF: if isInterval { return "", errors.Errorf("invalid format specification for an interval value") } width := 2 if n.suffix&DCH_S_FM != 0 { width = 0 } sign := "+" if t.gmtoff < 0 { sign = "-" } s += fmt.Sprintf("%s%s:%s", sign, getStringFromIntWithWidth(width, int(math.Abs(float64(t.gmtoff/duration.SecsPerHour)))), getStringFromIntWithWidth(2, int(math.Abs(float64(t.gmtoff%duration.SecsPerHour)/duration.SecsPerMinute)))) case DCH_A_D, DCH_B_C: if isInterval { return "", errors.Errorf("invalid format specification for an interval value") } if t.year <= 0 { s += "B.C." } else { s += "A.D." } case DCH_AD, DCH_BC: if isInterval { return "", errors.Errorf("invalid format specification for an interval value") } if t.year <= 0 { s += "BC" } else { s += "AD" } case DCH_a_d, DCH_b_c: if isInterval { return "", errors.Errorf("invalid format specification for an interval value") } if t.year <= 0 { s += "BC" } else { s += "AD" } case DCH_ad, DCH_bc: if isInterval { return "", errors.Errorf("invalid format specification for an interval value") } if t.year <= 0 { s += "bc" } else { s += "ad" } case DCH_MONTH: m, err := getFromArray(monthsFull, t.mon-1, n.suffix, isInterval, -9) if err != nil { return "", err } s += strings.ToUpper(m) case DCH_Month: m, err := getFromArray(monthsFull, t.mon-1, n.suffix, isInterval, -9) if err != nil { return "", err } s += m case DCH_month: m, err := getFromArray(monthsFull, t.mon-1, n.suffix, isInterval, -9) if err != nil { return "", err } s += strings.ToLower(m) case DCH_MON: m, err := getFromArray(monthsShort, t.mon-1, n.suffix, isInterval, 0) if err != nil { return "", err } s += strings.ToUpper(m) case DCH_Mon: m, err := getFromArray(monthsShort, t.mon-1, n.suffix, isInterval, 0) if err != nil { return "", err } s += m case DCH_mon: m, err := getFromArray(monthsShort, t.mon-1, n.suffix, isInterval, 0) if err != nil { return "", err } s += strings.ToLower(m) case DCH_MM: width := 3 if n.suffix&DCH_S_FM != 0 { width = 0 } else if t.min >= 0 { width = 2 } s += makeNumTh(getStringFromIntWithWidth(width, t.mon), n.suffix) case DCH_DAY: m, err := getFromArray(wdaysFull, t.wday, n.suffix, isInterval, -9) if err != nil { return "", err } s += strings.ToUpper(m) case DCH_Day: m, err := getFromArray(wdaysFull, t.wday, n.suffix, isInterval, -9) if err != nil { return "", err } s += m case DCH_day: m, err := getFromArray(wdaysFull, t.wday, n.suffix, isInterval, -9) if err != nil { return "", err } s += strings.ToLower(m) case DCH_DY: m, err := getFromArray(wdaysShort, t.wday, n.suffix, isInterval, 0) if err != nil { return "", err } s += strings.ToUpper(m) case DCH_Dy: m, err := getFromArray(wdaysShort, t.wday, n.suffix, isInterval, 0) if err != nil { return "", err } s += m case DCH_dy: m, err := getFromArray(wdaysShort, t.wday, n.suffix, isInterval, 0) if err != nil { return "", err } s += strings.ToLower(m) case DCH_DDD: width := 3 if n.suffix&DCH_S_FM != 0 { width = 0 } s += makeNumTh(getStringFromIntWithWidth(width, t.yday), n.suffix) case DCH_IDDD: width := 3 if n.suffix&DCH_S_FM != 0 { width = 0 } s += makeNumTh(getStringFromIntWithWidth(width, date2IsoYearDay(t.year, t.mon, t.mday)), n.suffix) case DCH_DD: width := 2 if n.suffix&DCH_S_FM != 0 { width = 0 } s += makeNumTh(getStringFromIntWithWidth(width, t.mday), n.suffix) case DCH_D: if isInterval { return "", errors.Errorf("invalid format specification for an interval value") } s += makeNumTh(getStringFromInt(t.wday+1), n.suffix) case DCH_ID: if isInterval { return "", errors.Errorf("invalid format specification for an interval value") } wday := t.wday if t.wday == 0 { wday = 7 } s += makeNumTh(getStringFromInt(wday), n.suffix) case DCH_WW: width := 2 if n.suffix&DCH_S_FM != 0 { width = 0 } s += makeNumTh(getStringFromIntWithWidth(width, (t.yday-1)/7+1), n.suffix) case DCH_IW: width := 2 if n.suffix&DCH_S_FM != 0 { width = 0 } s += makeNumTh(getStringFromIntWithWidth(width, date2IsoWeek(t.year, t.mon, t.mday)), n.suffix) case DCH_Q: if t.mon == 0 { break } s += makeNumTh(fmt.Sprintf("%d", (t.mon-1)/3+1), n.suffix) case DCH_CC: //var i int if isInterval { // straight calculation i = t.year / 100 } else { if t.year > 0 { // Century 20 == 1901 - 2000 i = (t.year-1)/100 + 1 } else { // Century 6BC == 600BC - 501BC i = t.year/100 - 1 } } if i <= 99 && i >= -99 { width := 3 if n.suffix&DCH_S_FM != 0 { width = 0 } else if i >= 0 { width = 2 } s += makeNumTh(getStringFromIntWithWidth(width, i), n.suffix) } else { s += makeNumTh(getStringFromInt(i), n.suffix) } case DCH_Y_YYY: ay := adjustYear(t.year, isInterval) i = ay / 1000 s += makeNumTh(fmt.Sprintf("%d,%03d", i, ay-(i*1000)), n.suffix) case DCH_YYYY: width := 5 ay := adjustYear(t.year, isInterval) if n.suffix&DCH_S_FM != 0 { width = 0 } else if ay >= 0 { width = 4 } s += makeNumTh(getStringFromIntWithWidth(width, ay), n.suffix) case DCH_IYYY: width := 5 ay := adjustYear(t.year, isInterval) if n.suffix&DCH_S_FM != 0 { width = 0 } else if ay >= 0 { width = 4 } s += makeNumTh(getStringFromIntWithWidth(width, adjustYear(date2IsoYear(t.year, t.mon, t.mday), isInterval)), n.suffix) case DCH_YYY: width := 4 ay := adjustYear(t.year, isInterval) if n.suffix&DCH_S_FM != 0 { width = 0 } else if ay >= 0 { width = 3 } s += makeNumTh(getStringFromIntWithWidth(width, ay), n.suffix) case DCH_IYY: width := 4 ay := adjustYear(t.year, isInterval) if n.suffix&DCH_S_FM != 0 { width = 0 } else if ay >= 0 { width = 3 } s += makeNumTh(getStringFromIntWithWidth(width, adjustYear(date2IsoYear(t.year, t.mon, t.mday), isInterval)%1000), n.suffix) case DCH_YY: width := 3 ay := adjustYear(t.year, isInterval) if n.suffix&DCH_S_FM != 0 { width = 0 } else if ay >= 0 { width = 2 } s += makeNumTh(getStringFromIntWithWidth(width, ay), n.suffix) case DCH_IY: width := 3 ay := adjustYear(t.year, isInterval) if n.suffix&DCH_S_FM != 0 { width = 0 } else if ay >= 0 { width = 2 } s += makeNumTh(getStringFromIntWithWidth(width, adjustYear(date2IsoYear(t.year, t.mon, t.mday), isInterval)%100), n.suffix) case DCH_Y: s += makeNumTh(getStringFromIntWithWidth(1, adjustYear(t.year, isInterval)), n.suffix) case DCH_I: s += makeNumTh(getStringFromIntWithWidth(1, adjustYear(date2IsoYear(t.year, t.mon, t.mday), isInterval)%10), n.suffix) case DCH_RM, DCH_rm: // For intervals, values like '12 month' will be reduced to 0 // month and some years. These should be processed. if t.mon != 0 || t.year != 0 { mon := 0 //const char *const *months; // Compute the position in the roman-numeral array. Note // that the contents of the array are reversed, December // being first and January last. if t.mon == 0 { // This case is special, and tracks the case of full // interval years. if t.year < 0 { mon = monthsPerYear - 1 } } else if t.mon < 0 { // Negative case. In this case, the calculation is // reversed, where -1 means December, -2 November, // etc. mon = -1 * (t.mon + 1) } else { // Common case, with a strictly positive value. The // position in the array matches with the value of // tm_mon. mon = monthsPerYear - t.mon } width := -4 if n.suffix&DCH_S_FM != 0 { width = 0 } if n.key.id == DCH_rm { s += fmt.Sprintf("%*s", width, rmMonthsLower[mon]) } else { s += fmt.Sprintf("%*s", width, strings.ToUpper(rmMonthsLower[mon])) } } case DCH_W: s += makeNumTh(getStringFromInt((t.mday-1)/7+1), n.suffix) case DCH_J: s += makeNumTh(getStringFromInt(date2J(t.year, t.mon, t.mday)), n.suffix) default: } } return s, nil } func getFromArray(arr []string, v int, suffix int, isInterval bool, width int) (string, error) { if isInterval { return "", errors.Errorf("invalid format specification for an interval value") } if v == 0 { // TODO return "", nil } if suffix&DCH_S_TM != 0 { return "", errors.Errorf("TM suffix is not supported yet.") } if suffix&DCH_S_FM != 0 { width = 0 } return fmt.Sprintf("%*s", width, arr[v]), nil } // date2IsoYear returns ISO 8601 year number. // Note: zero or negative results follow the year-zero-exists convention. func date2IsoYear(year, mon, mday int) int { /* current day */ dayn := date2J(year, mon, mday) /* fourth day of current year */ day4 := date2J(year, 1, 4) /* day0 == offset to first day of week (Monday) */ day0 := j2Day(day4 - 1) // We need the first week containing a Thursday, otherwise this day falls // into the previous year for purposes of counting weeks if dayn < day4-day0 { day4 = date2J(year-1, 1, 4) /* day0 == offset to first day of week (Monday) */ day0 = j2Day(day4 - 1) year-- } result := float64(dayn-(day4-day0))/7 + 1 // Sometimes the last few days in a year will fall into the first week of // the next year, so check for this. if result >= 52 { day4 = date2J(year+1, 1, 4) /* day0 == offset to first day of week (Monday) */ day0 = j2Day(day4 - 1) if dayn >= day4-day0 { year++ } } return year } // date2IsoYearDay returns the ISO 8601 day-of-year, given a Gregorian year, month and day. // Possible return values are 1 through 371 (364 in non-leap years). func date2IsoYearDay(year, mon, mday int) int { return date2J(year, mon, mday) - isoWeek2J(date2IsoYear(year, mon, mday), 1) + 1 } // date2IsoWeek returns ISO week number of year. func date2IsoWeek(year, mon, mday int) int { // current day */ dayn := date2J(year, mon, mday) // fourth day of current year day4 := date2J(year, 1, 4) // day0 == offset to first day of week (Monday) day0 := j2Day(day4 - 1) // We need the first week containing a Thursday, otherwise this day falls // into the previous year for purposes of counting weeks if dayn < day4-day0 { day4 = date2J(year-1, 1, 4) // day0 == offset to first day of week (Monday) day0 = j2Day(day4 - 1) } result := (dayn-(day4-day0))/7 + 1 // Sometimes the last few days in a year will fall into the first week of // the next year, so check for this. if result >= 52 { day4 = date2J(year+1, 1, 4) // day0 == offset to first day of week (Monday) day0 = j2Day(day4 - 1) if dayn >= day4-day0 { result = (dayn-(day4-day0))/7 + 1 } } return result } // adjustYear return the year if it's interval. Otherwise, // there is no 0 AD, and years go from 1 BC to 1 AD, so we make it // positive and map year == -1 to year zero, and shift all negative // years up one. For interval years, we just return the year. func adjustYear(year int, isInterval bool) int { if isInterval { return year } if year <= 0 { return -year + 1 } return year } // getStringFromIntWithWidth returns int in string format with given width // If the int is of length less than width, it's padded with zeroes. // If the int is of length more than width, it's trimmed. // Zero width is no width restriction. func getStringFromIntWithWidth(width int, v int) string { if width == 0 { return fmt.Sprintf("%d", v) } s := fmt.Sprintf("%0*d", width, v) l := len(s) if l > width { return s[l-width:] } return s } // getStringFromInt returns int in string format. func getStringFromInt(v int) string { return getStringFromIntWithWidth(0, v) } func makeNumTh(s string, suffix int) string { if suffix&DCH_S_TH != 0 { return s + strings.ToUpper(numth(s[len(s)-1])) } else if suffix&DCH_S_th != 0 { return s + numth(s[len(s)-1]) } else { return s } } func numth(s uint8) string { if s == '1' { return "st" } else if s == '2' { return "nd" } else if s == '3' { return "rd" } else { return "th" } }