Files
2026-07-13 12:32:25 +08:00

2183 lines
56 KiB
Go

// 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"
}
}