github.com/qioalice/ekago/v3@v3.3.2-0.20221202205325-5c262d586ee4/ekatime/timestamp_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 11 "github.com/qioalice/ekago/v3/internal/ekaenc" 12 ) 13 14 //goland:noinspection GoSnakeCaseUsage 15 var ( 16 _ERR_NIL_TIMESTAMP_RECEIVER = errors.New("nil ekatime.Timestamp receiver") 17 _ERR_NOT_ISO8601_TIMESTAMP = errors.New("incorrect ISO8601 timestamp format (must be YYYY-MM-DDThh:mm:ss)") 18 _ERR_BAD_TIMESTAMP_SEPARATOR = errors.New("ISO8601 require using 'T' as date time separator") 19 _ERR_BAD_JSON_TIMESTAMP_QUO = errors.New("bad JSON ISO8601 timestamp representation (forgotten quotes?)") 20 ) 21 22 // AppendTo generates a string representation of Timestamp and adds it to the b, 23 // returning a new slice (if it has grown) or the same if there was enough 24 // space to store 19 bytes (of string representation). 25 // 26 // Uses separator as Date and Time's parts separator: 27 // "YYYY<separatorDate>MM<separatorDate>DD hh<separatorTime>mm<separatorTime>ss". 28 // 29 // Up to 10x faster than fmt.Sprintf(). 30 func (ts Timestamp) AppendTo(b []byte, separatorDate, separatorTime byte) []byte { 31 32 b = NewDate(dateFromUnix(ts)).AppendTo(b, separatorDate) 33 b = append(b, ' ') 34 b = NewTime(timeFromUnix(ts)).AppendTo(b, separatorTime) 35 36 return b 37 } 38 39 // ParseFrom tries to parse b considering with the following format: 40 // "YYYY<sep>MM<sep>DD<reqSep>hh<sep>mm". 41 // - <sep> may be any 1 byte, or not presented at all. 42 // But if separator was presented between in "YYYY<sep>MM" it must be also 43 // between other parts and vice-versa. 44 // - <reqSep> required separator: space or 'T' char. 45 // 46 // Read more: 47 // https://en.wikipedia.org/wiki/ISO_8601 48 // 49 // If success, returns nil and saves date into the current Timestamp object. 50 // Year must be <= 4095, instead it will be overwritten by 4095. 51 // 52 // Skips leading spaces, ignores all next data after the day has been scanned. 53 func (ts *Timestamp) ParseFrom(b []byte) error { 54 55 if ts == nil { 56 return _ERR_NIL_TIMESTAMP_RECEIVER 57 } 58 59 var ( 60 d Date 61 t Time 62 ) 63 64 // Regardless Date.ParseFrom() has the loop that skips spaces, 65 // we need to know we must start parsing Time from. 66 // So, skip spaces manually. 67 68 var i = 0 69 for n := len(b); i < n && b[i] <= ' '; i++ { 70 } 71 72 if err := d.ParseFrom(b[i:]); err != nil { 73 return err 74 } 75 76 // Date.ParseFrom() finished w/ no errors. 77 // So, it was be either 8 chars (YYYYMMDD) or 10 (YYYY-MM-DD). 78 // 79 // YYYYMMDD 80 // YYYY-MM-DD 81 // ^ we need to check this char. 82 // If it's a digit, it was 6 char format. Otherwise - 8. 83 84 i += 7 85 if b[i] >= '0' && b[i] <= '9' { 86 // Was 8 char format. 87 i += 1 88 } else { 89 // Was 10 char format. 90 i += 3 91 } 92 93 // It's OK if time is not presented. 94 b = b[i:] 95 i = 0 96 if len(b) > 0 { 97 // ISO8601 requires a separator between date and time. And it must be 'T' char. 98 // We allows also a space. Why not? 99 if b[i] != ' ' && b[i] != 't' && b[i] != 'T' { 100 return _ERR_BAD_TIMESTAMP_SEPARATOR 101 } 102 i++ 103 if err := t.ParseFrom(b[i:]); err != nil { 104 return err 105 } 106 } 107 108 *ts = d.WithTime(t.Split()) 109 return nil 110 } 111 112 // String returns the current Timestamp's human-readable string representation 113 // in the following format: "YYYY/MM/DD hh:mm:ss". 114 func (ts Timestamp) String() string { 115 return string(ts.AppendTo(make([]byte, 0, 19), '/', ':')) 116 } 117 118 // MarshalJSON encodes the current Time in the following format (quoted) 119 // "YYYY-MM-DDThh:mm:ss", and returns it. Always returns nil as error. 120 // 121 // JSON null supporting: 122 // - Writes JSON null if current Timestamp receiver == nil. 123 // - Writes JSON null if current Timestamp == 0 (*). 124 // 125 // ----- 126 // 127 // (*): Yes, it's kinda weird that 01 Jan 1970 00:00:00 is JSON null, 128 // but let's be honest. When the last time you'll need to marshal EXACTLY 129 // this date and it's not a null-like value? 130 // And, of course, you always may use 01 Jan 1970 00:00:01 and it will be marshalled 131 // correctly. 132 func (ts *Timestamp) MarshalJSON() ([]byte, error) { 133 134 if ts == nil || *ts == 0 { 135 return ekaenc.NULL_JSON_BYTES_SLICE, nil 136 } 137 138 // Date: 10 chars (YYYY-MM-DD) 139 // Clock: 8 chars (hh:mm:ss) 140 // Quotes: 2 chars ("") 141 // Date clock separator: 1 char (T) 142 // Summary: 21 char. 143 b := make([]byte, 21) 144 145 _ = ts.Date().AppendTo(b[1:1:20], '-') 146 _ = ts.Time().AppendTo(b[12:12:20], ':') 147 148 b[0] = '"' 149 b[11] = 'T' 150 b[20] = '"' 151 152 return b, nil 153 } 154 155 // UnmarshalJSON decodes b into the current Timestamp object expecting b contains 156 // ISO8601 quoted date with time in the one of the following formats: 157 // "YYYY-MM-DDThh:mm:ss" (recommended), 158 // "YYYYMMDDThh:mm:ss", "YYYY-MM-DDThhmmss", "YYYYMMDDThhmmss" 159 // 160 // JSON null supporting: 161 // - It's ok if there is JSON null and receiver == nil (nothing changes) 162 // - Zeroes Timestamp if there is JSON null and receiver != nil 163 // (yes, sets to 01 Jan 1970 00:00:00). 164 // 165 // In other cases JSON parsing error or Date.ParseFrom() error is returned. 166 func (ts *Timestamp) UnmarshalJSON(b []byte) error { 167 168 if ekaenc.IsNullJSON(b) { 169 if ts != nil { 170 *ts = 0 171 } 172 return nil 173 } 174 175 // Length variants: 176 // Date length variants: 10, 8 177 // Clock length variants: 4, 5, 6, 8, 178 // Quotes: 2, 179 // Date time separator: 1. 180 // 181 // So, summary variants: 182 // - 15 (8+4+2+1), 183 // - 16 (8+5+2+1), 184 // - 17 (8+6+2+1, 10+4+2+1), 185 // - 18 (10+5+2+1), 186 // - 19 (8+8+2+1, 10+6+2+1), 187 // - 21 (10+8+2+1). 188 189 switch l := len(b); { 190 191 case !(l >= 15 && l <= 19) && l != 21: 192 return _ERR_NOT_ISO8601_TIMESTAMP 193 194 case b[0] != '"' && b[l-1] != '"': 195 return _ERR_BAD_JSON_TIMESTAMP_QUO 196 197 default: 198 return ts.ParseFrom(b[1 : l-1]) 199 } 200 }