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