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  }