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 }