pqtime.go (4828B)
1 package pqtime 2 3 import ( 4 "errors" 5 "fmt" 6 "math" 7 "strconv" 8 "strings" 9 "time" 10 ) 11 12 var errInvalidTimestamp = errors.New("invalid timestamp") 13 14 type timestampParser struct { 15 err error 16 } 17 18 func (p *timestampParser) expect(str string, char byte, pos int) { 19 if p.err != nil { 20 return 21 } 22 if pos+1 > len(str) { 23 p.err = errInvalidTimestamp 24 return 25 } 26 if c := str[pos]; c != char && p.err == nil { 27 p.err = fmt.Errorf("expected '%v' at position %v; got '%v'", char, pos, c) 28 } 29 } 30 31 func (p *timestampParser) mustAtoi(str string, begin int, end int) int { 32 if p.err != nil { 33 return 0 34 } 35 if begin < 0 || end < 0 || begin > end || end > len(str) { 36 p.err = errInvalidTimestamp 37 return 0 38 } 39 result, err := strconv.Atoi(str[begin:end]) 40 if err != nil { 41 if p.err == nil { 42 p.err = fmt.Errorf("expected number; got '%v'", str) 43 } 44 return 0 45 } 46 return result 47 } 48 49 func Parse(currentLocation *time.Location, str string) (time.Time, error) { 50 p := timestampParser{} 51 52 monSep := strings.IndexRune(str, '-') 53 // this is Gregorian year, not ISO Year 54 // In Gregorian system, the year 1 BC is followed by AD 1 55 year := p.mustAtoi(str, 0, monSep) 56 daySep := monSep + 3 57 month := p.mustAtoi(str, monSep+1, daySep) 58 p.expect(str, '-', daySep) 59 timeSep := daySep + 3 60 day := p.mustAtoi(str, daySep+1, timeSep) 61 62 minLen := monSep + len("01-01") + 1 63 64 isBC := strings.HasSuffix(str, " BC") 65 if isBC { 66 minLen += 3 67 } 68 69 var hour, minute, second int 70 if len(str) > minLen { 71 p.expect(str, ' ', timeSep) 72 minSep := timeSep + 3 73 p.expect(str, ':', minSep) 74 hour = p.mustAtoi(str, timeSep+1, minSep) 75 secSep := minSep + 3 76 p.expect(str, ':', secSep) 77 minute = p.mustAtoi(str, minSep+1, secSep) 78 secEnd := secSep + 3 79 second = p.mustAtoi(str, secSep+1, secEnd) 80 } 81 remainderIdx := monSep + len("01-01 00:00:00") + 1 82 // Three optional (but ordered) sections follow: the 83 // fractional seconds, the time zone offset, and the BC 84 // designation. We set them up here and adjust the other 85 // offsets if the preceding sections exist. 86 87 nanoSec := 0 88 tzOff := 0 89 90 if remainderIdx < len(str) && str[remainderIdx] == '.' { 91 fracStart := remainderIdx + 1 92 fracOff := strings.IndexAny(str[fracStart:], "-+Z ") 93 if fracOff < 0 { 94 fracOff = len(str) - fracStart 95 } 96 fracSec := p.mustAtoi(str, fracStart, fracStart+fracOff) 97 nanoSec = fracSec * (1000000000 / int(math.Pow(10, float64(fracOff)))) 98 99 remainderIdx += fracOff + 1 100 } 101 if tzStart := remainderIdx; tzStart < len(str) && (str[tzStart] == '-' || str[tzStart] == '+') { 102 // time zone separator is always '-' or '+' or 'Z' (UTC is +00) 103 var tzSign int 104 switch c := str[tzStart]; c { 105 case '-': 106 tzSign = -1 107 case '+': 108 tzSign = +1 109 default: 110 return time.Time{}, fmt.Errorf("expected '-' or '+' at position %v; got %v", tzStart, c) 111 } 112 tzHours := p.mustAtoi(str, tzStart+1, tzStart+3) 113 remainderIdx += 3 114 var tzMin, tzSec int 115 if remainderIdx < len(str) && str[remainderIdx] == ':' { 116 tzMin = p.mustAtoi(str, remainderIdx+1, remainderIdx+3) 117 remainderIdx += 3 118 } 119 if remainderIdx < len(str) && str[remainderIdx] == ':' { 120 tzSec = p.mustAtoi(str, remainderIdx+1, remainderIdx+3) 121 remainderIdx += 3 122 } 123 tzOff = tzSign * ((tzHours * 60 * 60) + (tzMin * 60) + tzSec) 124 } else if tzStart < len(str) && str[tzStart] == 'Z' { 125 // time zone Z separator indicates UTC is +00 126 remainderIdx += 1 127 } 128 129 var isoYear int 130 131 if isBC { 132 isoYear = 1 - year 133 remainderIdx += 3 134 } else { 135 isoYear = year 136 } 137 if remainderIdx < len(str) { 138 return time.Time{}, fmt.Errorf("expected end of input, got %v", str[remainderIdx:]) 139 } 140 t := time.Date(isoYear, time.Month(month), day, 141 hour, minute, second, nanoSec, 142 globalLocationCache.getLocation(tzOff)) 143 144 if currentLocation != nil { 145 // Set the location of the returned Time based on the session's 146 // TimeZone value, but only if the local time zone database agrees with 147 // the remote database on the offset. 148 lt := t.In(currentLocation) 149 _, newOff := lt.Zone() 150 if newOff == tzOff { 151 t = lt 152 } 153 } 154 155 return t, p.err 156 } 157 158 // Format into Postgres' text format for timestamps. 159 func Format(t time.Time) []byte { 160 // Need to send dates before 0001 A.D. with " BC" suffix, instead of the 161 // minus sign preferred by Go. 162 // Beware, "0000" in ISO is "1 BC", "-0001" is "2 BC" and so on 163 bc := false 164 if t.Year() <= 0 { 165 // flip year sign, and add 1, e.g: "0" will be "1", and "-10" will be "11" 166 t = t.AddDate((-t.Year())*2+1, 0, 0) 167 bc = true 168 } 169 b := []byte(t.Format("2006-01-02 15:04:05.999999999Z07:00")) 170 171 _, offset := t.Zone() 172 offset %= 60 173 if offset != 0 { 174 // RFC3339Nano already printed the minus sign 175 if offset < 0 { 176 offset = -offset 177 } 178 179 b = append(b, ':') 180 if offset < 10 { 181 b = append(b, '0') 182 } 183 b = strconv.AppendInt(b, int64(offset), 10) 184 } 185 186 if bc { 187 b = append(b, " BC"...) 188 } 189 return b 190 }