github.com/qioalice/ekago/v3@v3.3.2-0.20221202205325-5c262d586ee4/ekatime/date_encode.go (about) 1 // Copyright © 2020. All rights reserved. 2 // Author: Ilya Stroy. 3 // Contacts: iyuryevich@pm.me, https://github.com/qioalice 4 // License: https://opensource.org/licenses/MIT 5 6 package ekatime 7 8 import ( 9 "errors" 10 "strconv" 11 12 "github.com/qioalice/ekago/v3/internal/ekaenc" 13 ) 14 15 //goland:noinspection GoSnakeCaseUsage 16 var ( 17 _ERR_NIL_DATE_RECEIVER = errors.New("nil ekatime.Date receiver") 18 _ERR_NOT_ISO8601_DATE = errors.New("incorrect ISO8601 date format (must be YYYYMMDD or YYYY-MM-DD)") 19 _ERR_BAD_YEAR = errors.New("year must be in the range [0..4095]") 20 _ERR_BAD_MONTH = errors.New("month must be in the range [1..12]") 21 _ERR_BAD_DAY = errors.New("day must be in the range [1..31]") 22 _ERR_BAD_CORRESP_DATE = errors.New("date must represent valid date (check year, month, day corresponding)") 23 _ERR_BAD_JSON_DATE_QUO = errors.New("bad JSON ISO8601 date representation (forgotten quotes?)") 24 ) 25 26 // AppendTo generates a string representation of Date and adds it to the b, 27 // returning a new slice (if it has grown) or the same if there was enough 28 // space to store 10 bytes (of string representation). 29 // 30 // Uses separator as Date's parts separator: 31 // "YYYY<separator>MM<separator>DD". 32 // 33 // Up to 6x faster than fmt.Sprintf(). 34 func (dd Date) AppendTo(b []byte, separator byte) []byte { 35 36 f := func(b []byte) { 37 y, m, d := normalizeDate(dd.Split()) 38 39 if y >= _YEAR_AS_NUM_STR_MIN && y <= _YEAR_AS_NUM_STR_MAX { 40 copy(b[:4], _YEAR_AS_NUM_STR[y-_YEAR_AS_NUM_STR_MIN]) 41 } else { 42 // normalizeDate never return y > 4095, so it's safe 43 _ = append(b[:0], strconv.Itoa(int(y))...) 44 } 45 46 copy(b[5:7], _DAY_AS_NUM_STR[m-MONTH_JANUARY]) // use day's array, it's ok 47 copy(b[8:10], _DAY_AS_NUM_STR[d-1]) 48 49 b[4] = separator 50 b[7] = separator 51 } 52 53 if c, l := cap(b), len(b); c-l >= 10 { 54 b = b[:l+10] 55 f(b[l:]) 56 return b 57 } else { 58 // One more allocation 59 b2 := make([]byte, 10) 60 f(b2) 61 b = append(b, b2...) 62 return b 63 } 64 } 65 66 // ParseFrom tries to parse b considering with the following format: 67 // "YYYY<separator>MM<separator>DD", <separator> may be any 1 byte, 68 // or not presented at all. But if separator was presented between in "YYYY<sep>MM" 69 // it must be also between other parts and vice-versa. 70 // 71 // Read more: 72 // https://en.wikipedia.org/wiki/ISO_8601 73 // 74 // If success, returns nil and saves date into the current Date object. 75 // Year must be <= 4095, instead it will be overwritten by 4095. 76 // 77 // Skips leading spaces, ignores all next data after the day has been scanned. 78 func (dd *Date) ParseFrom(b []byte) error { 79 80 if dd == nil { 81 return _ERR_NIL_DATE_RECEIVER 82 } 83 84 var i = 0 85 for n := len(b); i < n && b[i] <= ' '; i++ { 86 } 87 88 // Minimum required len: 8 (YYYYMMDD - w/o separators). 89 if len(b[i:]) < 8 { 90 return _ERR_NOT_ISO8601_DATE 91 } 92 93 x1, valid1 := batoi(b[i], b[i+1]) 94 x2, valid2 := batoi(b[i+2], b[i+3]) 95 96 x1 *= 100 97 x1 += x2 98 99 if !(valid1 && valid2) || x1 < 0 || x2 < 0 || x1 > 4095 { 100 return _ERR_BAD_YEAR 101 } 102 103 i += 4 104 y := Year(x1) 105 wasSeparator := false 106 107 // Skip separator 108 if !(b[i] >= '0' && b[i] <= '9') { 109 i++ 110 wasSeparator = true 111 } 112 113 x1, valid1 = batoi(b[i], b[i+1]) 114 m := Month(x1) 115 if !valid1 || m < MONTH_JANUARY || m > MONTH_DECEMBER { 116 return _ERR_BAD_MONTH 117 } 118 119 i += 2 120 if wasSeparator { 121 i++ 122 } 123 124 if len(b[i:]) < 2 { 125 // Looks like user has incorrect data like: 126 // "YYYY-MM-D", "YYYY-MM-" 127 return _ERR_NOT_ISO8601_DATE 128 } 129 130 x1, valid1 = batoi(b[i], b[i+1]) 131 if x1 < 1 || x1 > 31 { 132 return _ERR_BAD_DAY 133 } 134 135 if !IsValidDate(y, m, Day(x1)) { 136 return _ERR_BAD_CORRESP_DATE 137 } 138 139 *dd = NewDate(y, m, Day(x1)) 140 return nil 141 } 142 143 // String returns the current Date's string representation in the following format: 144 // "YYYY/MM/DD". 145 func (dd Date) String() string { 146 return string(dd.AppendTo(make([]byte, 0, 10), '/')) 147 } 148 149 // MarshalJSON encodes the current Date in the following format (quoted) 150 // "YYYY-MM-DD", and returns it. Always returns nil as error. 151 // 152 // JSON null supporting: 153 // - Writes JSON null if current Date receiver is nil 154 // - Writes JSON null if current Date == 0. 155 func (dd *Date) MarshalJSON() ([]byte, error) { 156 157 if dd == nil || dd.ToCmp() == 0 { 158 return ekaenc.NULL_JSON_BYTES_SLICE, nil 159 } 160 161 b := make([]byte, 12) 162 _ = dd.AppendTo(b[1:1:12], '-') 163 164 b[0] = '"' 165 b[11] = '"' 166 167 return b, nil 168 } 169 170 // UnmarshalJSON decodes b into the current Date object expecting b contains 171 // ISO8601 quoted date (only date, not date w/ time) in the one of the following 172 // formats: "YYYYMMDD", "YYYY-MM-DD". 173 // 174 // JSON null supporting: 175 // - It's ok if there is JSON null and receiver == nil (nothing changes) 176 // - Zeroes Date if there is JSON null and receiver != nil. 177 // 178 // In other cases JSON parsing error or Date.ParseFrom() error is returned. 179 func (dd *Date) UnmarshalJSON(b []byte) error { 180 181 if ekaenc.IsNullJSON(b) { 182 if dd != nil { 183 *dd = 0 184 } 185 return nil 186 } 187 188 switch l := len(b); { 189 190 case l != 10 && l != 12: 191 // The length must be 10 (8+quotes) or 12 (8+separators+quotes). 192 return _ERR_NOT_ISO8601_DATE 193 194 case b[0] != '"' || b[l-1] != '"': 195 // Forgotten quotes? Incorrect JSON? 196 return _ERR_BAD_JSON_DATE_QUO 197 198 default: 199 return dd.ParseFrom(b[1 : l-1]) 200 } 201 }