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  }