github.com/datastax/go-cassandra-native-protocol@v0.0.0-20220706104457-5e8aad05cf90/datacodec/date.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  	"math"
    19  	"time"
    20  
    21  	"github.com/datastax/go-cassandra-native-protocol/datatype"
    22  	"github.com/datastax/go-cassandra-native-protocol/primitive"
    23  )
    24  
    25  const DateLayoutDefault = "2006-01-02"
    26  
    27  var (
    28  	// DateMin is the minimum representable CQL date: -5877641-06-23; it corresponds to math.MinInt32 days before the
    29  	// Epoch.
    30  	DateMin = ConvertEpochDaysToTime(math.MinInt32)
    31  
    32  	// DateMax is the maximum representable CQL date: 5881580-07-11; it corresponds to math.MaxInt32 days after the
    33  	// Epoch.
    34  	DateMax = ConvertEpochDaysToTime(math.MaxInt32)
    35  )
    36  
    37  // ConvertTimeToEpochDays is a function that converts from a time.Time into days since the Epoch. The given time is
    38  // normalized to UTC before the computation. An error is returned if the given time value is outside the valid range
    39  // for CQL date values: from -5877641-06-23 UTC to 5881580-07-11 UTC inclusive.
    40  func ConvertTimeToEpochDays(t time.Time) (int32, error) {
    41  	// Taken from civil.Date: we convert to Unix time so that we do not have to worry about leap seconds:
    42  	// Unix time increases by exactly 86400 seconds per day.
    43  	days := floorDiv(t.UTC().Unix(), 86400)
    44  	if days < math.MinInt32 || days > math.MaxInt32 {
    45  		return 0, errValueOutOfRange(t)
    46  	}
    47  	return int32(days), nil
    48  }
    49  
    50  // ConvertEpochDaysToTime is a function that converts from days since the Epoch into a time.Time in UTC.
    51  // The returned time will have its clock part set to zero.
    52  // ConvertEpochDaysToTime(math.MinInt32) returns the minimum valid CQL date: -5877641-06-23 UTC.
    53  // ConvertEpochDaysToTime(math.MaxInt32) returns the maximum valid CQL date: 5881580-07-11 UTC.
    54  func ConvertEpochDaysToTime(days int32) time.Time {
    55  	return time.Unix(int64(days)*86400, 0).UTC()
    56  }
    57  
    58  // Date is a codec for the CQL date type with default layout. Its preferred Go type is time.Time, but it can
    59  // encode from and decode to string and to and from most numeric types as well.
    60  // When encoding from and decoding to time.Time, only the date part is considered, the clock part is ignored. Also note
    61  // that all time.Time values are normalized to UTC before encoding and after decoding. Also note that not all time.Time
    62  // values can be converted to CQL dates: the valid range is from -5877641-06-23 UTC to 5881580-07-11 UTC inclusive;
    63  // these values can be obtained with ConvertEpochDaysToTime(math.MinInt32) and ConvertEpochDaysToTime(math.MaxInt32).
    64  // When encoding from and decoding to numeric types, the numeric value represents the number of days since the Epoch.
    65  // Note that a better representation for the CQL date type can be found in the civil package
    66  // from cloud.google.com, see https://pkg.go.dev/cloud.google.com/go/civil.
    67  var Date = NewDate(DateLayoutDefault)
    68  
    69  // NewDate creates a new codec for the CQL date type, with the given layout. The Layout is used only when
    70  // encoding from or decoding to string; it is ignored otherwise. See NewDate for important notes on accepted types.
    71  func NewDate(layout string) Codec {
    72  	return &dateCodec{layout: layout}
    73  }
    74  
    75  type dateCodec struct {
    76  	layout string
    77  }
    78  
    79  func (c *dateCodec) DataType() datatype.DataType {
    80  	return datatype.Date
    81  }
    82  
    83  // Implementation note: CQL dates are encoded as a number of days since the epoch, stored in 8 bytes with 0 being
    84  // 0x80000000, that is, math.MinInt32. This is why we need to add or subtract math.MinInt32 when encoding and decoding.
    85  // Note that this relies on the fact that some additions will overflow: this is expected.
    86  
    87  func (c *dateCodec) Encode(source interface{}, version primitive.ProtocolVersion) (dest []byte, err error) {
    88  	if !version.SupportsDataType(c.DataType().Code()) {
    89  		err = errDataTypeNotSupported(c.DataType(), version)
    90  	} else {
    91  		var val int32
    92  		var wasNil bool
    93  		if val, wasNil, err = convertToInt32Date(source, c.layout); err == nil && !wasNil {
    94  			dest = writeInt32(val - math.MinInt32)
    95  		}
    96  	}
    97  	if err != nil {
    98  		err = errCannotEncode(source, c.DataType(), version, err)
    99  	}
   100  	return
   101  }
   102  
   103  func (c *dateCodec) Decode(source []byte, dest interface{}, version primitive.ProtocolVersion) (wasNull bool, err error) {
   104  	if !version.SupportsDataType(c.DataType().Code()) {
   105  		wasNull = len(source) == 0
   106  		err = errDataTypeNotSupported(c.DataType(), version)
   107  	} else {
   108  		var val int32
   109  		if val, wasNull, err = readInt32(source); err == nil {
   110  			err = convertFromInt32Date(val+math.MinInt32, wasNull, c.layout, dest)
   111  		}
   112  	}
   113  	if err != nil {
   114  		err = errCannotDecode(dest, c.DataType(), version, err)
   115  	}
   116  	return
   117  }
   118  
   119  func convertToInt32Date(source interface{}, layout string) (val int32, wasNil bool, err error) {
   120  	switch s := source.(type) {
   121  	case time.Time:
   122  		val, err = ConvertTimeToEpochDays(s)
   123  	case *time.Time:
   124  		if wasNil = s == nil; !wasNil {
   125  			val, err = ConvertTimeToEpochDays(*s)
   126  		}
   127  	case string:
   128  		val, err = stringToEpochDays(s, layout)
   129  	case *string:
   130  		if wasNil = s == nil; !wasNil {
   131  			val, err = stringToEpochDays(*s, layout)
   132  		}
   133  	case nil:
   134  		wasNil = true
   135  	default:
   136  		return convertToInt32(source)
   137  	}
   138  	if err != nil {
   139  		err = errSourceConversionFailed(source, val, err)
   140  	}
   141  	return
   142  }
   143  
   144  func convertFromInt32Date(val int32, wasNull bool, layout string, dest interface{}) (err error) {
   145  	switch d := dest.(type) {
   146  	case *interface{}:
   147  		if d == nil {
   148  			err = ErrNilDestination
   149  		} else if wasNull {
   150  			*d = nil
   151  		} else {
   152  			*d = ConvertEpochDaysToTime(val)
   153  		}
   154  	case *time.Time:
   155  		if d == nil {
   156  			err = ErrNilDestination
   157  		} else if wasNull {
   158  			*d = time.Time{}
   159  		} else {
   160  			*d = ConvertEpochDaysToTime(val)
   161  		}
   162  	case *string:
   163  		if d == nil {
   164  			err = ErrNilDestination
   165  		} else if wasNull {
   166  			*d = ""
   167  		} else {
   168  			*d = ConvertEpochDaysToTime(val).Format(layout)
   169  		}
   170  	default:
   171  		return convertFromInt32(val, wasNull, dest)
   172  	}
   173  	if err != nil {
   174  		err = errDestinationConversionFailed(val, dest, err)
   175  	}
   176  	return
   177  }