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