github.com/cockroachdb/cockroachdb-parser@v0.23.3-0.20240213214944-911057d40c9a/pkg/util/timeutil/time_zone_util.go (about) 1 // Copyright 2017 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 timeutil 12 13 import ( 14 "fmt" 15 "regexp" 16 "strconv" 17 "strings" 18 "time" 19 20 "github.com/cockroachdb/errors" 21 ) 22 23 const ( 24 offsetBoundSecs = 167*60*60 + 59*60 25 // PG supports UTC hour offsets in the range [-167, 167]. 26 maxUTCHourOffset = 167 27 maxUTCHourOffsetInSeconds = maxUTCHourOffset * 60 * 60 28 ) 29 30 var timezoneOffsetRegex = regexp.MustCompile(`(?i)^(GMT|UTC)?([+-])?(\d{1,3}(:[0-5]?\d){0,2})$`) 31 32 // FixedTimeZoneOffsetToLocation creates a time.Location with an offset and a 33 // time zone string. 34 func FixedTimeZoneOffsetToLocation(offset int, origRepr string) *time.Location { 35 // The offset name always should be normalized to upper-case for UTC/GMT. 36 return time.FixedZone(strings.ToUpper(origRepr), offset) 37 } 38 39 // TimeZoneOffsetToLocation takes an offset and name that can be marshaled by 40 // crdb between nodes and creates a time.Location. 41 // Note that the display time zone is always shown with ISO sign convention. 42 func TimeZoneOffsetToLocation(offset int) *time.Location { 43 origRepr := secondsToHoursMinutesSeconds(offset) 44 if offset <= 0 { 45 origRepr = fmt.Sprintf("<-%s>+%s", origRepr, origRepr) 46 } else { 47 origRepr = fmt.Sprintf("<+%s>-%s", origRepr, origRepr) 48 } 49 50 return time.FixedZone(origRepr, offset) 51 } 52 53 // TimeZoneStringToLocationStandard is an option for the standard to use 54 // for parsing in TimeZoneStringToLocation. 55 type TimeZoneStringToLocationStandard uint32 56 57 const ( 58 // TimeZoneStringToLocationISO8601Standard parses int UTC offsets as *east* of 59 // the GMT line, e.g. `-5` would be 'America/New_York' without daylight savings. 60 TimeZoneStringToLocationISO8601Standard TimeZoneStringToLocationStandard = iota 61 // TimeZoneStringToLocationPOSIXStandard parses int UTC offsets as *west* of the 62 // GMT line, e.g. `+5` would be 'America/New_York' without daylight savings. 63 TimeZoneStringToLocationPOSIXStandard 64 ) 65 66 // TimeZoneStringToLocation transforms a string into a time.Location. It 67 // supports the usual locations and also time zones with fixed offsets created 68 // by FixedTimeZoneOffsetToLocation(). 69 func TimeZoneStringToLocation( 70 locStr string, std TimeZoneStringToLocationStandard, 71 ) (*time.Location, error) { 72 offset, _, parsed := ParseTimeZoneOffset(locStr, std) 73 if parsed { 74 if offset < -maxUTCHourOffsetInSeconds || offset > maxUTCHourOffsetInSeconds { 75 return nil, errors.New("UTC timezone offset is out of range.") 76 } 77 return TimeZoneOffsetToLocation(offset), nil 78 } 79 80 // The time may just be a raw int value. 81 intVal, err := strconv.ParseInt(locStr, 10, 64) 82 if err == nil { 83 // Parsing an int has different behavior for POSIX and ISO8601. 84 if std == TimeZoneStringToLocationPOSIXStandard { 85 intVal *= -1 86 } 87 if intVal < -maxUTCHourOffset || intVal > maxUTCHourOffset { 88 return nil, errors.New("UTC timezone offset is out of range.") 89 } 90 return TimeZoneOffsetToLocation(int(intVal) * 60 * 60), nil 91 } 92 93 locTransforms := []func(string) string{ 94 func(s string) string { return s }, 95 strings.ToUpper, 96 strings.ToTitle, 97 } 98 for _, transform := range locTransforms { 99 if loc, err := LoadLocation(transform(locStr)); err == nil { 100 return loc, nil 101 } 102 } 103 104 tzOffset, ok := timeZoneOffsetStringConversion(locStr, std) 105 if !ok { 106 return nil, errors.Newf("could not parse %q as time zone", locStr) 107 } 108 return FixedTimeZoneOffsetToLocation(int(tzOffset), locStr), nil 109 } 110 111 // ParseTimeZoneOffset takes the string representation of a time.Location 112 // created by TimeZoneOffsetToLocation and parses it to the offset and the 113 // original representation specified by the user. The bool returned is true if 114 // parsing was successful. 115 // The offset is formatted <-%s>+%s or <+%s>+%s. 116 // A string with whitespace padding optionally followed by a (+/-) 117 // and a float should be able to be parsed. Example: " +10.5" is parsed in 118 // PG and displayed as <+10:06:36>-10:06:36. 119 func ParseTimeZoneOffset( 120 location string, standard TimeZoneStringToLocationStandard, 121 ) (offset int, origRepr string, success bool) { 122 if strings.HasPrefix(location, "<") { 123 // The string has the format <+HH:MM:SS>-HH:MM:SS or <-HH:MM:SS>+HH:MM:SS. 124 // Parse the time between the < >. 125 // Grab the time from between the < >. 126 regexPattern, err := regexp.Compile(`\<[+-].*\>`) 127 if err != nil { 128 return 0, "", false 129 } 130 origRepr = regexPattern.FindString(location) 131 origRepr = strings.TrimPrefix(origRepr, "<") 132 origRepr = strings.TrimSuffix(origRepr, ">") 133 134 offsetMultiplier := 1 135 if strings.HasPrefix(origRepr, "-") { 136 offsetMultiplier = -1 137 } 138 139 origRepr = strings.Trim(origRepr, "+") 140 origRepr = strings.Trim(origRepr, "-") 141 142 // Parse HH:MM:SS time. 143 offset = hoursMinutesSecondsToSeconds(origRepr) 144 offset *= offsetMultiplier 145 146 return offset, location, true 147 } 148 149 // Try parsing the string in the format whitespaces optionally followed by 150 // (+/-) followed immediately by a float. 151 origRepr = strings.TrimSpace(location) 152 origRepr = strings.TrimPrefix(origRepr, "+") 153 154 multiplier := 1 155 if strings.HasPrefix(origRepr, "-") { 156 multiplier = -1 157 origRepr = strings.TrimPrefix(origRepr, "-") 158 } 159 160 if standard == TimeZoneStringToLocationPOSIXStandard { 161 multiplier *= -1 162 } 163 164 f, err := strconv.ParseFloat(origRepr, 64) 165 if err != nil { 166 return 0, "", false 167 } 168 169 origRepr = floatToHoursMinutesSeconds(f) 170 offset = hoursMinutesSecondsToSeconds(origRepr) 171 return multiplier * offset, origRepr, true 172 } 173 174 // timeZoneOffsetStringConversion converts a time string to offset seconds. 175 // Supported time zone strings: GMT/UTC±[00:00:00 - 169:59:00]. 176 // Seconds/minutes omittable and is case insensitive. 177 // By default, anything with a UTC/GMT prefix, or with : characters are POSIX. 178 // Whole integers can be POSIX or ISO8601 standard depending on the std variable. 179 func timeZoneOffsetStringConversion( 180 s string, std TimeZoneStringToLocationStandard, 181 ) (offset int64, ok bool) { 182 submatch := timezoneOffsetRegex.FindStringSubmatch(strings.ReplaceAll(s, " ", "")) 183 if len(submatch) == 0 { 184 return 0, false 185 } 186 hasUTCPrefix := submatch[1] != "" 187 prefix := submatch[2] 188 timeString := submatch[3] 189 190 offsets := strings.Split(timeString, ":") 191 offset = int64(hoursMinutesSecondsToSeconds(timeString)) 192 193 // GMT/UTC prefix, colons and POSIX standard characters have "opposite" timezones. 194 if hasUTCPrefix || len(offsets) > 1 || std == TimeZoneStringToLocationPOSIXStandard { 195 offset *= -1 196 } 197 if prefix == "-" { 198 offset *= -1 199 } 200 201 if offset > offsetBoundSecs || offset < -offsetBoundSecs { 202 return 0, false 203 } 204 return offset, true 205 } 206 207 // The timestamp must be of one of the following formats: 208 // 209 // HH 210 // HH:MM 211 // HH:MM:SS 212 func hoursMinutesSecondsToSeconds(timeString string) int { 213 var ( 214 hoursString = "0" 215 minutesString = "0" 216 secondsString = "0" 217 ) 218 offsets := strings.Split(timeString, ":") 219 if strings.Contains(timeString, ":") { 220 hoursString, minutesString = offsets[0], offsets[1] 221 if len(offsets) == 3 { 222 secondsString = offsets[2] 223 } 224 } else { 225 hoursString = timeString 226 } 227 228 hours, _ := strconv.ParseInt(hoursString, 10, 64) 229 minutes, _ := strconv.ParseInt(minutesString, 10, 64) 230 seconds, _ := strconv.ParseInt(secondsString, 10, 64) 231 return int((hours * 60 * 60) + (minutes * 60) + seconds) 232 } 233 234 // secondsToHoursMinutesSeconds converts seconds to a timestamp of the format 235 // 236 // HH 237 // HH:MM 238 // HH:MM:SS 239 func secondsToHoursMinutesSeconds(totalSeconds int) string { 240 secondsPerHour := 60 * 60 241 secondsPerMinute := 60 242 if totalSeconds < 0 { 243 totalSeconds = totalSeconds * -1 244 } 245 hours := totalSeconds / secondsPerHour 246 minutes := (totalSeconds - hours*secondsPerHour) / secondsPerMinute 247 seconds := totalSeconds - hours*secondsPerHour - minutes*secondsPerMinute 248 249 if seconds == 0 && minutes == 0 { 250 return fmt.Sprintf("%02d", hours) 251 } else if seconds == 0 { 252 return fmt.Sprintf("%d:%d", hours, minutes) 253 } else { 254 // PG doesn't round, truncate precision. 255 return fmt.Sprintf("%d:%d:%2.0d", hours, minutes, seconds) 256 } 257 } 258 259 // floatToHoursMinutesSeconds converts a float to a HH:MM:SS. 260 // The minutes and seconds sections are only included in the precision is 261 // necessary. 262 // For example: 263 // 264 // 11.00 -> 11 265 // 11.5 -> 11:30 266 // 11.51 -> 11:30:36 267 func floatToHoursMinutesSeconds(f float64) string { 268 hours := int(f) 269 remaining := f - float64(hours) 270 271 secondsPerHour := float64(60 * 60) 272 totalSeconds := remaining * secondsPerHour 273 minutes := int(totalSeconds / 60) 274 seconds := totalSeconds - float64(minutes*60) 275 276 if seconds == 0 && minutes == 0 { 277 return fmt.Sprintf("%02d", hours) 278 } else if seconds == 0 { 279 return fmt.Sprintf("%d:%d", hours, minutes) 280 } else { 281 // PG doesn't round, truncate precision. 282 return fmt.Sprintf("%d:%d:%2.0f", hours, minutes, seconds) 283 } 284 }