github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/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 "fmt" 15 "regexp" 16 "time" 17 18 "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode" 19 "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" 20 "github.com/cockroachdb/cockroach/pkg/util/timeofday" 21 "github.com/cockroachdb/cockroach/pkg/util/timeutil" 22 "github.com/cockroachdb/cockroach/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{4}-`) 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 func ParseTimeTZ(now time.Time, s string, precision time.Duration) (TimeTZ, error) { 100 // Special case as we have to use `ParseTimestamp` to get the date. 101 // We cannot use `ParseTime` as it does not have timezone awareness. 102 if !timeTZHasTimeComponent.MatchString(s) { 103 return TimeTZ{}, pgerror.Newf( 104 pgcode.InvalidTextRepresentation, 105 "could not parse %q as TimeTZ", 106 s, 107 ) 108 } 109 110 // ParseTimestamp requires a date field -- append date at the beginning 111 // if a date has not been included. 112 if !timeTZIncludesDateRegex.MatchString(s) { 113 s = "1970-01-01 " + s 114 } else { 115 s = timeutil.ReplaceLibPQTimePrefix(s) 116 } 117 118 t, err := pgdate.ParseTimestamp(now, pgdate.ParseModeYMD, s) 119 if err != nil { 120 // Build our own error message to avoid exposing the dummy date. 121 return TimeTZ{}, pgerror.Newf( 122 pgcode.InvalidTextRepresentation, 123 "could not parse %q as TimeTZ", 124 s, 125 ) 126 } 127 retTime := timeofday.FromTime(t.Round(precision)) 128 // Special case on 24:00 and 24:00:00 as the parser 129 // does not handle these correctly. 130 if timeTZMaxTimeRegex.MatchString(s) { 131 retTime = timeofday.Time2400 132 } 133 134 _, offsetSecsUnconverted := t.Zone() 135 offsetSecs := int32(-offsetSecsUnconverted) 136 if offsetSecs > MaxTimeTZOffsetSecs || offsetSecs < MinTimeTZOffsetSecs { 137 return TimeTZ{}, pgerror.Newf( 138 pgcode.NumericValueOutOfRange, 139 "time zone displacement out of range: %q", 140 s, 141 ) 142 } 143 return MakeTimeTZ(retTime, offsetSecs), nil 144 } 145 146 // String implements the Stringer interface. 147 func (t *TimeTZ) String() string { 148 tTime := t.ToTime() 149 timeComponent := tTime.Format("15:04:05.999999") 150 // 24:00:00 gets returned as 00:00:00, which is incorrect. 151 if t.TimeOfDay == timeofday.Time2400 { 152 timeComponent = "24:00:00" 153 } 154 timeZoneComponent := tTime.Format("Z07:00:00") 155 // If it is UTC, .Format converts it to "Z". 156 // Fully expand this component. 157 if t.OffsetSecs == 0 { 158 timeZoneComponent = "+00:00:00" 159 } 160 // Go's time.Format functionality does not work for offsets which 161 // in the range -0s < offsetSecs < -60s, e.g. -22s offset prints as 00:00:-22. 162 // Manually correct for this. 163 if 0 < t.OffsetSecs && t.OffsetSecs < 60 { 164 timeZoneComponent = fmt.Sprintf("-00:00:%02d", t.OffsetSecs) 165 } 166 return timeComponent + timeZoneComponent 167 } 168 169 // ToTime converts a DTimeTZ to a time.Time, corrected to the given location. 170 func (t *TimeTZ) ToTime() time.Time { 171 loc := timeutil.FixedOffsetTimeZoneToLocation(-int(t.OffsetSecs), "TimeTZ") 172 return t.TimeOfDay.ToTime().Add(time.Duration(t.OffsetSecs) * time.Second).In(loc) 173 } 174 175 // Round rounds a DTimeTZ to the given duration. 176 func (t *TimeTZ) Round(precision time.Duration) TimeTZ { 177 return MakeTimeTZ(t.TimeOfDay.Round(precision), t.OffsetSecs) 178 } 179 180 // ToDuration returns the TimeTZ as an offset duration from UTC midnight. 181 func (t *TimeTZ) ToDuration() time.Duration { 182 return t.ToTime().Sub(timeutil.Unix(0, 0)) 183 } 184 185 // Before returns whether the current is before the other TimeTZ. 186 func (t *TimeTZ) Before(other TimeTZ) bool { 187 return t.ToTime().Before(other.ToTime()) || (t.ToTime().Equal(other.ToTime()) && t.OffsetSecs < other.OffsetSecs) 188 } 189 190 // After returns whether the TimeTZ is after the other TimeTZ. 191 func (t *TimeTZ) After(other TimeTZ) bool { 192 return t.ToTime().After(other.ToTime()) || (t.ToTime().Equal(other.ToTime()) && t.OffsetSecs > other.OffsetSecs) 193 } 194 195 // Equal returns whether the TimeTZ is equal to the other TimeTZ. 196 func (t *TimeTZ) Equal(other TimeTZ) bool { 197 return t.TimeOfDay == other.TimeOfDay && t.OffsetSecs == other.OffsetSecs 198 }