github.com/datastax/go-cassandra-native-protocol@v0.0.0-20220706104457-5e8aad05cf90/datacodec/time.go (about)

     1  // Copyright 2021 DataStax
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package datacodec
    16  
    17  import (
    18  	"time"
    19  
    20  	"github.com/datastax/go-cassandra-native-protocol/datatype"
    21  	"github.com/datastax/go-cassandra-native-protocol/primitive"
    22  )
    23  
    24  const (
    25  	TimeLayoutDefault = "15:04:05.999999999"
    26  
    27  	// TimeMaxDuration is the maximum duration that can be stored in a CQL time value. Any int64 value that is lesser
    28  	// than zero or grater than this value will be rejected.
    29  	TimeMaxDuration = 24*time.Hour - 1
    30  )
    31  
    32  // ConvertTimeToNanosOfDay is a function that converts from a time.Time into nanos since the beginning of the day.
    33  // The given time is normalized to UTC before the computation.
    34  func ConvertTimeToNanosOfDay(t time.Time) int64 {
    35  	t = t.UTC()
    36  	return int64(t.Nanosecond()) +
    37  		int64(t.Second())*int64(time.Second) +
    38  		int64(t.Minute())*int64(time.Minute) +
    39  		int64(t.Hour())*int64(time.Hour)
    40  }
    41  
    42  // ConvertDurationToNanosOfDay is a function that converts from a time.Duration into nanos since the beginning of the
    43  // day. An error is returned if the given time value is outside the valid range for CQL time values: from 0 to
    44  // TimeMaxDuration inclusive.
    45  func ConvertDurationToNanosOfDay(d time.Duration) (int64, error) {
    46  	if d < 0 || d > TimeMaxDuration {
    47  		return 0, errValueOutOfRange(d)
    48  	} else {
    49  		return d.Nanoseconds(), nil
    50  	}
    51  }
    52  
    53  // ConvertNanosOfDayToTime is a function that converts from nanos since the beginning of the day into a time.Time in UTC.
    54  // The returned time will have its date part set to 0001-01-01 and its time zone will be UTC. An error is returned if
    55  // the given time value is outside the valid range for CQL time values: from 0 to TimeMaxDuration inclusive.
    56  func ConvertNanosOfDayToTime(nanos int64) (time.Time, error) {
    57  	if d := time.Duration(nanos); d < 0 || d > TimeMaxDuration {
    58  		return time.Time{}, errValueOutOfRange(d)
    59  	} else {
    60  		return time.Date(0, time.January, 1, 0, 0, 0, 0, time.UTC).Add(d), nil
    61  	}
    62  }
    63  
    64  // ConvertNanosOfDayToDuration is a function that converts from nanos since the beginning of the day into a
    65  // time.Duration. An error is returned if the given time value is outside the valid range for CQL time values: from 0 to
    66  // TimeMaxDuration inclusive.
    67  func ConvertNanosOfDayToDuration(nanos int64) (time.Duration, error) {
    68  	if d := time.Duration(nanos); d < 0 || d > TimeMaxDuration {
    69  		return 0, errValueOutOfRange(d)
    70  	} else {
    71  		return d, nil
    72  	}
    73  }
    74  
    75  // Time is a codec for the CQL time type with default layout. Its preferred Go type is time.Duration, but it
    76  // can encode from and decode to time.Time, string and to most numeric types as well.
    77  // When encoding from and decoding to time.Duration, the duration must be >= 0 and <= TimeMaxDuration, otherwise an
    78  // error is returned.
    79  // When encoding from and decoding to time.Time, only the clock part is considered, the date part is ignored. Also note
    80  // that all time.Time values are normalized to UTC before encoding and after decoding.
    81  // When encoding from and decoding to numeric types, the numeric value represents the number of nanoseconds since the
    82  // beginning of the day.
    83  // Note that a better representation for the CQL date type can be found in the civil package
    84  // from cloud.google.com, see https://pkg.go.dev/cloud.google.com/go/civil.
    85  var Time = NewTime(TimeLayoutDefault)
    86  
    87  // NewTime creates a new codec for CQL time type, with the given layout. The Layout is used only when
    88  // encoding from or decoding to string; it is ignored otherwise. See NewTime for important notes on accepted types.
    89  func NewTime(layout string) Codec {
    90  	return &timeCodec{layout: layout}
    91  }
    92  
    93  type timeCodec struct {
    94  	layout string
    95  }
    96  
    97  func (c *timeCodec) DataType() datatype.DataType {
    98  	return datatype.Time
    99  }
   100  
   101  func (c *timeCodec) Encode(source interface{}, version primitive.ProtocolVersion) (dest []byte, err error) {
   102  	if !version.SupportsDataType(c.DataType().Code()) {
   103  		err = errDataTypeNotSupported(c.DataType(), version)
   104  	} else {
   105  		var val int64
   106  		var wasNil bool
   107  		if val, wasNil, err = convertToInt64Time(source, c.layout); err == nil && !wasNil {
   108  			dest = writeInt64(val)
   109  		}
   110  	}
   111  	if err != nil {
   112  		err = errCannotEncode(source, c.DataType(), version, err)
   113  	}
   114  	return
   115  }
   116  
   117  func (c *timeCodec) Decode(source []byte, dest interface{}, version primitive.ProtocolVersion) (wasNull bool, err error) {
   118  	if !version.SupportsDataType(c.DataType().Code()) {
   119  		wasNull = len(source) == 0
   120  		err = errDataTypeNotSupported(c.DataType(), version)
   121  	} else {
   122  		var val int64
   123  		if val, wasNull, err = readInt64(source); err == nil {
   124  			err = convertFromInt64Time(val, wasNull, dest, c.layout)
   125  		}
   126  	}
   127  	if err != nil {
   128  		err = errCannotDecode(dest, c.DataType(), version, err)
   129  	}
   130  	return
   131  }
   132  
   133  func convertToInt64Time(source interface{}, layout string) (val int64, wasNil bool, err error) {
   134  	switch s := source.(type) {
   135  	case time.Duration:
   136  		val, err = ConvertDurationToNanosOfDay(s)
   137  	case *time.Duration:
   138  		if wasNil = s == nil; !wasNil {
   139  			val, err = ConvertDurationToNanosOfDay(*s)
   140  		}
   141  	case time.Time:
   142  		val = ConvertTimeToNanosOfDay(s)
   143  	case *time.Time:
   144  		if wasNil = s == nil; !wasNil {
   145  			val = ConvertTimeToNanosOfDay(*s)
   146  		}
   147  	case string:
   148  		val, err = stringToNanosOfDay(s, layout)
   149  	case *string:
   150  		if wasNil = s == nil; !wasNil {
   151  			val, err = stringToNanosOfDay(*s, layout)
   152  		}
   153  	case nil:
   154  		wasNil = true
   155  	default:
   156  		return convertToInt64(source)
   157  	}
   158  	if err != nil {
   159  		err = errSourceConversionFailed(source, val, err)
   160  	}
   161  	return
   162  }
   163  
   164  func convertFromInt64Time(val int64, wasNull bool, dest interface{}, layout string) (err error) {
   165  	switch d := dest.(type) {
   166  	case *interface{}:
   167  		if d == nil {
   168  			err = ErrNilDestination
   169  		} else if wasNull {
   170  			*d = nil
   171  		} else {
   172  			*d, err = ConvertNanosOfDayToDuration(val)
   173  		}
   174  	case *time.Duration:
   175  		if d == nil {
   176  			err = ErrNilDestination
   177  		} else if wasNull {
   178  			*d = 0
   179  		} else {
   180  			*d, err = ConvertNanosOfDayToDuration(val)
   181  		}
   182  	case *time.Time:
   183  		if d == nil {
   184  			err = ErrNilDestination
   185  		} else if wasNull {
   186  			*d = time.Time{}
   187  		} else {
   188  			*d, err = ConvertNanosOfDayToTime(val)
   189  		}
   190  	case *string:
   191  		if d == nil {
   192  			err = ErrNilDestination
   193  		} else if wasNull {
   194  			*d = ""
   195  		} else {
   196  			var t time.Time
   197  			if t, err = ConvertNanosOfDayToTime(val); err == nil {
   198  				*d = t.Format(layout)
   199  			}
   200  		}
   201  	default:
   202  		return convertFromInt64(val, wasNull, dest)
   203  	}
   204  	if err != nil {
   205  		err = errDestinationConversionFailed(val, dest, err)
   206  	}
   207  	return
   208  }