github.com/cockroachdb/cockroachdb-parser@v0.23.3-0.20240213214944-911057d40c9a/pkg/util/timetz/timetz.go (about) 1 // Copyright 2019 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package timetz 12 13 import ( 14 "regexp" 15 "time" 16 17 "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgcode" 18 "github.com/cockroachdb/cockroachdb-parser/pkg/sql/pgwire/pgerror" 19 "github.com/cockroachdb/cockroachdb-parser/pkg/util/strutil" 20 "github.com/cockroachdb/cockroachdb-parser/pkg/util/timeofday" 21 "github.com/cockroachdb/cockroachdb-parser/pkg/util/timeutil" 22 "github.com/cockroachdb/cockroachdb-parser/pkg/util/timeutil/pgdate" 23 ) 24 25 var ( 26 // MaxTimeTZOffsetSecs is the maximum offset TimeTZ allows in seconds. 27 // NOTE: postgres documentation mentions 14:59, but up to 15:59 is accepted. 28 MaxTimeTZOffsetSecs = int32((15*time.Hour + 59*time.Minute) / time.Second) 29 // MinTimeTZOffsetSecs is the minimum offset TimeTZ allows in seconds. 30 // NOTE: postgres documentation mentions -14:59, but up to -15:59 is accepted. 31 MinTimeTZOffsetSecs = -1 * MaxTimeTZOffsetSecs 32 33 // timeTZMaxTimeRegex is a compiled regex for parsing the 24:00 timetz value. 34 timeTZMaxTimeRegex = regexp.MustCompile(`^([0-9-]*T?)?\s*24:`) 35 36 // timeTZIncludesDateRegex is a regex to check whether there is a date 37 // associated with the given string when attempting to parse it. 38 timeTZIncludesDateRegex = regexp.MustCompile(`^\d+[-/]`) 39 // timeTZHasTimeComponent determines whether there is a time component at all 40 // in a given string. 41 timeTZHasTimeComponent = regexp.MustCompile(`\d:`) 42 ) 43 44 // TimeTZ is an implementation of postgres' TimeTZ. 45 // Note that in this implementation, if time is equal in terms of UTC time 46 // the zone offset is further used to differentiate. 47 type TimeTZ struct { 48 // TimeOfDay is the time since midnight in a given zone 49 // dictated by OffsetSecs. 50 timeofday.TimeOfDay 51 // OffsetSecs is the offset of the zone, with the sign reversed. 52 // e.g. -0800 (PDT) would have OffsetSecs of +8*60*60. 53 // This is in line with the postgres implementation. 54 // This means timeofday.Secs() + OffsetSecs = UTC secs. 55 OffsetSecs int32 56 } 57 58 // MakeTimeTZ creates a TimeTZ from a TimeOfDay and offset. 59 func MakeTimeTZ(t timeofday.TimeOfDay, offsetSecs int32) TimeTZ { 60 return TimeTZ{TimeOfDay: t, OffsetSecs: offsetSecs} 61 } 62 63 // MakeTimeTZFromLocation creates a TimeTZ from a TimeOfDay and time.Location. 64 func MakeTimeTZFromLocation(t timeofday.TimeOfDay, loc *time.Location) TimeTZ { 65 _, zoneOffsetSecs := timeutil.Now().In(loc).Zone() 66 return TimeTZ{TimeOfDay: t, OffsetSecs: -int32(zoneOffsetSecs)} 67 } 68 69 // MakeTimeTZFromTime creates a TimeTZ from a time.Time. 70 // It will be trimmed to microsecond precision. 71 // 2400 time will overflow to 0000. If 2400 is needed, use 72 // MakeTimeTZFromTimeAllow2400. 73 func MakeTimeTZFromTime(t time.Time) TimeTZ { 74 return MakeTimeTZFromLocation( 75 timeofday.New(t.Hour(), t.Minute(), t.Second(), t.Nanosecond()/1000), 76 t.Location(), 77 ) 78 } 79 80 // MakeTimeTZFromTimeAllow2400 creates a TimeTZ from a time.Time, 81 // but factors in that Time2400 may be possible. 82 // This assumes either a lib/pq time or unix time is set. 83 // This should be used for storage and network deserialization, where 84 // 2400 time is allowed. 85 func MakeTimeTZFromTimeAllow2400(t time.Time) TimeTZ { 86 if t.Day() != 1 { 87 return MakeTimeTZFromLocation(timeofday.Time2400, t.Location()) 88 } 89 return MakeTimeTZFromTime(t) 90 } 91 92 // Now returns the TimeTZ of the current location. 93 func Now() TimeTZ { 94 return MakeTimeTZFromTime(timeutil.Now()) 95 } 96 97 // ParseTimeTZ parses and returns the TimeTZ represented by the 98 // provided string, or an error if parsing is unsuccessful. 99 // 100 // The dependsOnContext return value indicates if we had to consult the given 101 // `now` value (either for the time or the local timezone). 102 func ParseTimeTZ( 103 now time.Time, dateStyle pgdate.DateStyle, s string, precision time.Duration, 104 ) (_ TimeTZ, dependsOnContext bool, _ error) { 105 // Special case as we have to use `ParseTimestamp` to get the date. 106 // We cannot use `ParseTime` as it does not have timezone awareness. 107 if !timeTZHasTimeComponent.MatchString(s) { 108 return TimeTZ{}, false, pgerror.Newf( 109 pgcode.InvalidTextRepresentation, 110 "could not parse %q as TimeTZ", 111 s, 112 ) 113 } 114 115 // ParseTimestamp requires a date field -- append date at the beginning 116 // if a date has not been included. 117 if !timeTZIncludesDateRegex.MatchString(s) { 118 s = "1970-01-01 " + s 119 } else { 120 s = timeutil.ReplaceLibPQTimePrefix(s) 121 } 122 123 t, dependsOnContext, err := pgdate.ParseTimestamp(now, dateStyle, s) 124 if err != nil { 125 // Build our own error message to avoid exposing the dummy date. 126 return TimeTZ{}, false, pgerror.Newf( 127 pgcode.InvalidTextRepresentation, 128 "could not parse %q as TimeTZ", 129 s, 130 ) 131 } 132 retTime := timeofday.FromTime(t.Round(precision)) 133 // Special case on 24:00 and 24:00:00 as the parser 134 // does not handle these correctly. 135 if timeTZMaxTimeRegex.MatchString(s) { 136 retTime = timeofday.Time2400 137 } 138 139 _, offsetSecsUnconverted := t.Zone() 140 offsetSecs := int32(-offsetSecsUnconverted) 141 if offsetSecs > MaxTimeTZOffsetSecs || offsetSecs < MinTimeTZOffsetSecs { 142 return TimeTZ{}, false, pgerror.Newf( 143 pgcode.NumericValueOutOfRange, 144 "time zone displacement out of range: %q", 145 s, 146 ) 147 } 148 return MakeTimeTZ(retTime, offsetSecs), dependsOnContext, nil 149 } 150 151 // String implements the Stringer interface. 152 func (t *TimeTZ) String() string { 153 return string(t.AppendFormat(nil)) 154 } 155 156 // AppendFormat appends TimeTZ to the buffer, and returns modified buffer. 157 func (t *TimeTZ) AppendFormat(buf []byte) []byte { 158 tTime := t.ToTime() 159 if t.TimeOfDay == timeofday.Time2400 { 160 // 24:00:00 gets returned as 00:00:00, which is incorrect. 161 buf = append(buf, "24:00:00"...) 162 } else { 163 buf = tTime.AppendFormat(buf, "15:04:05.999999") 164 } 165 166 if t.OffsetSecs == 0 { 167 // If it is UTC, .Format converts it to "Z". 168 // Fully expand this component. 169 buf = append(buf, "+00:00:00"...) 170 } else if 0 < t.OffsetSecs && t.OffsetSecs < 60 { 171 // Go's time.Format functionality does not work for offsets which 172 // in the range -0s < offsetSecs < -60s, e.g. -22s offset prints as 00:00:-22. 173 // Manually correct for this. 174 buf = append(buf, "-00:00:"...) 175 buf = strutil.AppendInt(buf, int(t.OffsetSecs), 2) 176 } else { 177 buf = tTime.AppendFormat(buf, "Z07:00:00") 178 } 179 return buf 180 } 181 182 // ToTime converts a DTimeTZ to a time.Time, corrected to the given location. 183 func (t *TimeTZ) ToTime() time.Time { 184 loc := timeutil.TimeZoneOffsetToLocation(-int(t.OffsetSecs)) 185 return t.TimeOfDay.ToTime().Add(time.Duration(t.OffsetSecs) * time.Second).In(loc) 186 } 187 188 // Round rounds a DTimeTZ to the given duration. 189 func (t *TimeTZ) Round(precision time.Duration) TimeTZ { 190 return MakeTimeTZ(t.TimeOfDay.Round(precision), t.OffsetSecs) 191 } 192 193 // ToDuration returns the TimeTZ as an offset duration from UTC midnight. 194 func (t *TimeTZ) ToDuration() time.Duration { 195 return t.ToTime().Sub(timeutil.Unix(0, 0)) 196 } 197 198 // Before returns whether the current is before the other TimeTZ. 199 func (t *TimeTZ) Before(other TimeTZ) bool { 200 return t.ToTime().Before(other.ToTime()) || (t.ToTime().Equal(other.ToTime()) && t.OffsetSecs < other.OffsetSecs) 201 } 202 203 // After returns whether the TimeTZ is after the other TimeTZ. 204 func (t *TimeTZ) After(other TimeTZ) bool { 205 return t.ToTime().After(other.ToTime()) || (t.ToTime().Equal(other.ToTime()) && t.OffsetSecs > other.OffsetSecs) 206 } 207 208 // Equal returns whether the TimeTZ is equal to the other TimeTZ. 209 func (t *TimeTZ) Equal(other TimeTZ) bool { 210 return t.TimeOfDay == other.TimeOfDay && t.OffsetSecs == other.OffsetSecs 211 }