github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/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 fixedOffsetPrefix = "fixed offset:" 25 offsetBoundSecs = 167*60*60 + 59*60 26 ) 27 28 var timezoneOffsetRegex = regexp.MustCompile(`(?i)^(GMT|UTC)?([+-])?(\d{1,3}(:[0-5]?\d){0,2})$`) 29 30 // FixedOffsetTimeZoneToLocation creates a time.Location with a set offset and 31 // with a name that can be marshaled by crdb between nodes. 32 func FixedOffsetTimeZoneToLocation(offset int, origRepr string) *time.Location { 33 return time.FixedZone( 34 fmt.Sprintf("%s%d (%s)", fixedOffsetPrefix, offset, origRepr), 35 offset) 36 } 37 38 // TimeZoneStringToLocationStandard is an option for the standard to use 39 // for parsing in TimeZoneStringToLocation. 40 type TimeZoneStringToLocationStandard uint32 41 42 const ( 43 // TimeZoneStringToLocationISO8601Standard parses int UTC offsets as *east* of 44 // the GMT line, e.g. `-5` would be 'America/New_York' without daylight savings. 45 TimeZoneStringToLocationISO8601Standard TimeZoneStringToLocationStandard = iota 46 // TimeZoneStringToLocationPOSIXStandard parses int UTC offsets as *west* of the 47 // GMT line, e.g. `+5` would be 'America/New_York' without daylight savings. 48 TimeZoneStringToLocationPOSIXStandard 49 ) 50 51 // TimeZoneStringToLocation transforms a string into a time.Location. It 52 // supports the usual locations and also time zones with fixed offsets created 53 // by FixedOffsetTimeZoneToLocation(). 54 func TimeZoneStringToLocation( 55 locStr string, std TimeZoneStringToLocationStandard, 56 ) (*time.Location, error) { 57 offset, origRepr, parsed := ParseFixedOffsetTimeZone(locStr) 58 if parsed { 59 return FixedOffsetTimeZoneToLocation(offset, origRepr), nil 60 } 61 62 // The time may just be a raw int value. 63 intVal, err := strconv.ParseInt(locStr, 10, 64) 64 if err == nil { 65 // Parsing an int has different behavior for POSIX and ISO8601. 66 if std == TimeZoneStringToLocationPOSIXStandard { 67 intVal *= -1 68 } 69 return FixedOffsetTimeZoneToLocation(int(intVal)*60*60, locStr), nil 70 } 71 72 locTransforms := []func(string) string{ 73 func(s string) string { return s }, 74 strings.ToUpper, 75 strings.ToTitle, 76 } 77 for _, transform := range locTransforms { 78 if loc, err := LoadLocation(transform(locStr)); err == nil { 79 return loc, nil 80 } 81 } 82 83 tzOffset, ok := timeZoneOffsetStringConversion(locStr, std) 84 if ok { 85 return FixedOffsetTimeZoneToLocation(int(tzOffset), locStr), nil 86 } 87 return nil, errors.Newf("could not parse %q as time zone", locStr) 88 } 89 90 // ParseFixedOffsetTimeZone takes the string representation of a time.Location 91 // created by FixedOffsetTimeZoneToLocation and parses it to the offset and the 92 // original representation specified by the user. The bool returned is true if 93 // parsing was successful. 94 // 95 // The strings produced by FixedOffsetTimeZoneToLocation look like 96 // "<fixedOffsetPrefix><offset> (<origRepr>)". 97 // TODO(#42404): this is not the format given by the results in 98 // pgwire/testdata/connection_params. 99 func ParseFixedOffsetTimeZone(location string) (offset int, origRepr string, success bool) { 100 if !strings.HasPrefix(location, fixedOffsetPrefix) { 101 return 0, "", false 102 } 103 location = strings.TrimPrefix(location, fixedOffsetPrefix) 104 parts := strings.SplitN(location, " ", 2) 105 if len(parts) < 2 { 106 return 0, "", false 107 } 108 109 offset, err := strconv.Atoi(parts[0]) 110 if err != nil { 111 return 0, "", false 112 } 113 114 origRepr = parts[1] 115 if !strings.HasPrefix(origRepr, "(") || !strings.HasSuffix(origRepr, ")") { 116 return 0, "", false 117 } 118 return offset, strings.TrimSuffix(strings.TrimPrefix(origRepr, "("), ")"), true 119 } 120 121 // timeZoneOffsetStringConversion converts a time string to offset seconds. 122 // Supported time zone strings: GMT/UTC±[00:00:00 - 169:59:00]. 123 // Seconds/minutes omittable and is case insensitive. 124 // By default, anything with a UTC/GMT prefix, or with : characters are POSIX. 125 // Whole integers can be POSIX or ISO8601 standard depending on the std variable. 126 func timeZoneOffsetStringConversion( 127 s string, std TimeZoneStringToLocationStandard, 128 ) (offset int64, ok bool) { 129 submatch := timezoneOffsetRegex.FindStringSubmatch(strings.ReplaceAll(s, " ", "")) 130 if len(submatch) == 0 { 131 return 0, false 132 } 133 hasUTCPrefix := submatch[1] != "" 134 prefix := submatch[2] 135 timeString := submatch[3] 136 137 var ( 138 hoursString = "0" 139 minutesString = "0" 140 secondsString = "0" 141 ) 142 offsets := strings.Split(timeString, ":") 143 if strings.Contains(timeString, ":") { 144 hoursString, minutesString = offsets[0], offsets[1] 145 if len(offsets) == 3 { 146 secondsString = offsets[2] 147 } 148 } else { 149 hoursString = timeString 150 } 151 152 hours, _ := strconv.ParseInt(hoursString, 10, 64) 153 minutes, _ := strconv.ParseInt(minutesString, 10, 64) 154 seconds, _ := strconv.ParseInt(secondsString, 10, 64) 155 offset = (hours * 60 * 60) + (minutes * 60) + seconds 156 157 // GMT/UTC prefix, colons and POSIX standard characters have "opposite" timezones. 158 if hasUTCPrefix || len(offsets) > 1 || std == TimeZoneStringToLocationPOSIXStandard { 159 offset *= -1 160 } 161 if prefix == "-" { 162 offset *= -1 163 } 164 165 if offset > offsetBoundSecs || offset < -offsetBoundSecs { 166 return 0, false 167 } 168 return offset, true 169 }