format.ha (8400B)
1 // License: MPL-2.0 2 // (c) 2021-2022 Byron Torres <b@torresjrjr.com> 3 // (c) 2022 Drew DeVault <sir@cmpwn.com> 4 // (c) 2021-2022 Vlad-Stefan Harbuz <vlad@vladh.net> 5 use ascii; 6 use errors; 7 use fmt; 8 use io; 9 use strings; 10 use strio; 11 use time; 12 use time::chrono; 13 14 // [[datetime::format]] layout for the email date format. 15 export def EMAIL: str = "%a, %d %b %Y %H:%M:%S %z"; 16 17 // [[datetime::format]] layout for the email date format, with zone offset and 18 // zone abbreviation. 19 export def EMAILZ: str = "%a, %d %b %Y %H:%M:%S %z %Z"; 20 21 // [[datetime::format]] layout partly compatible with the default layout format 22 // for the POSIX locale. %d is used in place of POSIX %e. 23 export def POSIX: str = "%a %b %d %H:%M:%S %Z %Y"; 24 // TODO: Actually implement '%e' and thus the POSIX layout format? 25 26 // [[datetime::format]] layout compatible with RFC 3339. 27 export def RFC3339: str = "%Y-%m-%dT%H:%M:%S%z"; 28 29 // [[datetime::format]] layout for a simple timestamp. 30 export def STAMP: str = "%Y-%m-%d %H:%M:%S"; 31 32 // [[datetime::format]] layout for a simple timestamp with nanoseconds. 33 export def STAMP_NANO: str = "%Y-%m-%d %H:%M:%S.%N"; 34 35 // [[datetime::format]] layout for a simple timestamp with nanoseconds and zone 36 // offset. 37 export def STAMP_ZOFF: str = "%Y-%m-%d %H:%M:%S.%N %z"; 38 39 // [[datetime::format]] layout for a simple timestamp with nanoseconds, 40 // zone offset, zone abbreviation, and locality. 41 export def STAMP_NOZL: str = "%Y-%m-%d %H:%M:%S.%N %z %Z %L"; 42 43 def WEEKDAYS: [_]str = [ 44 "Monday", 45 "Tuesday", 46 "Wednesday", 47 "Thursday", 48 "Friday", 49 "Saturday", 50 "Sunday", 51 ]; 52 53 def WEEKDAYS_SHORT: [_]str = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; 54 55 def MONTHS: [_]str = [ 56 "January", 57 "February", 58 "March", 59 "April", 60 "May", 61 "June", 62 "July", 63 "August", 64 "September", 65 "October", 66 "November", 67 "December", 68 ]; 69 70 def MONTHS_SHORT: [_]str = [ 71 "Jan", "Feb", "Mar", 72 "Apr", "May", "Jun", 73 "Jul", "Aug", "Sep", 74 "Oct", "Nov", "Dec", 75 ]; 76 77 // TODO: Make format() accept parameters of type (datetime | period), using the 78 // "intervals" standard representation provided by ISO 8601? 79 // 80 // See https://en.wikipedia.org/wiki/ISO_8601#Time_intervals 81 // 82 // Ticket: https://todo.sr.ht/~sircmpwn/hare/650 83 84 // Formats a [[datetime]] and writes it into a caller supplied buffer. 85 // The returned string is borrowed from this buffer. 86 export fn bsformat( 87 buf: []u8, 88 layout: str, 89 dt: *datetime, 90 ) (str | io::error) = { 91 let sink = strio::fixed(buf); 92 format(&sink, layout, dt)?; 93 return strio::string(&sink); 94 }; 95 96 // Formats a [[datetime]] and writes it into a heap-allocated string. 97 // The caller must free the return value. 98 export fn asformat(layout: str, dt: *datetime) (str | io::error) = { 99 let sink = strio::dynamic(); 100 format(&sink, layout, dt)?; 101 return strio::string(&sink); 102 }; 103 104 fn fmtout(out: io::handle, r: rune, dt: *datetime) (size | io::error) = { 105 switch (r) { 106 case 'a' => 107 return fmt::fprint(out, WEEKDAYS_SHORT[_weekday(dt)]); 108 case 'A' => 109 return fmt::fprint(out, WEEKDAYS[_weekday(dt)]); 110 case 'b' => 111 return fmt::fprint(out, MONTHS_SHORT[_month(dt) - 1]); 112 case 'B' => 113 return fmt::fprint(out, MONTHS[_month(dt) - 1]); 114 case 'd' => 115 return fmt::fprintf(out, "{:02}", _day(dt)); 116 case 'F' => 117 return fmt::fprintf(out, "{:04}-{:02}-{:02}", _year(dt), _month(dt), _day(dt)); 118 case 'H' => 119 return fmt::fprintf(out, "{:02}", _hour(dt)); 120 case 'I' => 121 return fmt::fprintf(out, "{:02}", (_hour(dt) + 11) % 12 + 1); 122 case 'j' => 123 return fmt::fprintf(out, "{:03}", _yearday(dt)); 124 case 'L' => 125 return fmt::fprint(out, dt.loc.name); 126 case 'm' => 127 return fmt::fprintf(out, "{:02}", _month(dt)); 128 case 'M' => 129 return fmt::fprintf(out, "{:02}", _minute(dt)); 130 case 'N' => 131 return fmt::fprintf(out, "{:09}", _nanosecond(dt)); 132 case 'p' => 133 return fmt::fprint(out, if (_hour(dt) < 12) "AM" else "PM"); 134 case 's' => 135 return fmt::fprintf(out, "{:02}", time::unix(*(dt: *time::instant))); 136 case 'S' => 137 return fmt::fprintf(out, "{:02}", _second(dt)); 138 case 'T' => 139 return fmt::fprintf(out, "{:02}:{:02}:{:02}", _hour(dt), _minute(dt), _second(dt)); 140 case 'u' => 141 return fmt::fprintf(out, "{}", _weekday(dt) + 1); 142 case 'U' => 143 return fmt::fprintf(out, "{:02}", _sundayweek(dt)); 144 case 'w' => 145 return fmt::fprintf(out, "{}", (_weekday(dt) + 1) % 7); 146 case 'W' => 147 return fmt::fprintf(out, "{:02}", _week(dt)); 148 case 'y' => 149 return fmt::fprintf(out, "{:02}", _year(dt) % 100); 150 case 'Y' => 151 return fmt::fprintf(out, "{:04}", _year(dt)); 152 case 'z' => 153 const (sign, zo) = if (chrono::mzone(dt).zoff >= 0) { 154 yield ('+', calc_hmsn(chrono::mzone(dt).zoff)); 155 } else { 156 yield ('-', calc_hmsn(-chrono::mzone(dt).zoff)); 157 }; 158 const (hr, mi) = (zo.0, zo.1); 159 return fmt::fprintf(out, "{}{:02}{:02}", sign, hr, mi); 160 case 'Z' => 161 return fmt::fprint(out, chrono::mzone(dt).abbr); 162 case '%' => 163 return fmt::fprint(out, "%"); 164 case => 165 abort("Invalid format string provided to datetime::format"); 166 }; 167 }; 168 169 // Formats a [[datetime]] according to a layout and writes to an [[io::handle]]. 170 // 171 // The layout may contain any of the following format specifiers listed below. 172 // Implemented are a subset of the POSIX strftime(3) format specifiers, as well 173 // as some others. Use of unimplemented specifiers or an otherwise invalid 174 // layout will cause an abort. 175 // 176 // %% -- A literal '%' character. 177 // %a -- The abbreviated name of the day of the week. 178 // %A -- The full name of the day of the week. 179 // %b -- The abbreviated name of the month. 180 // %B -- The full name of the month. 181 // %d -- The day of the month (decimal, range 01 to 31). 182 // %F -- The full date, equivalent to %Y-%m-%d 183 // %H -- The hour of the day as from a 24-hour clock (range 00 to 23). 184 // %I -- The hour of the day as from a 12-hour clock (range 01 to 12). 185 // %j -- The ordinal day of the year (range 001 to 366). 186 // %L -- The locality's name (the timezone's identifier). 187 // %m -- The month (decimal, range 01 to 12). 188 // %M -- The minute (decimal, range 00 to 59). 189 // %N -- The nanosecond of the second (range 000000000 to 999999999). 190 // %p -- Either "AM" or "PM" according to the current time. 191 // "AM" includes midnight, and "PM" includes noon. 192 // %s -- Number of seconds since 1970-01-01 00:00:00, the Unix epoch 193 // %S -- The second of the minute (range 00 to 60). 194 // %T -- The full time, equivalent to %H:%M:%S 195 // %u -- The day of the week (decimal, range 1 to 7). 1 represents Monday. 196 // %U -- The week number of the current year (range 00 to 53), 197 // starting with the first Sunday as the first day of week 01. 198 // %w -- The day of the week (decimal, range 0 to 6). 0 represents Sunday. 199 // %W -- The week number of the current year (range 00 to 53), 200 // starting with the first Monday as the first day of week 01. 201 // %y -- The year without the century digits (range 00 to 99). 202 // %Y -- The year. 203 // %z -- The observed zone offset. 204 // %Z -- The observed zone abbreviation. 205 // 206 export fn format( 207 h: io::handle, 208 layout: str, 209 dt: *datetime 210 ) (size | io::error) = { 211 const iter = strings::iter(layout); 212 let escaped = false; 213 let n = 0z; 214 for (true) { 215 let r: rune = match (strings::next(&iter)) { 216 case void => 217 break; 218 case let r: rune => 219 yield r; 220 }; 221 222 if (escaped) { 223 escaped = false; 224 n += fmtout(h, r, dt)?; 225 } else { 226 if (r == '%') { 227 escaped = true; 228 } else { 229 strio::appendrune(h, r)?; 230 }; 231 }; 232 }; 233 return n; 234 }; 235 236 @test fn format() void = { 237 const dt = new(chrono::UTC, 0, 1994, 1, 1, 2, 17, 5, 24)!; 238 239 const cases = [ 240 // special characters 241 ("%%", "%"), 242 // hour 243 ("%H", "02"), 244 ("%I", "02"), 245 // minute 246 ("%M", "17"), 247 // second 248 ("%S", "05"), 249 // nanosecond 250 ("%N", "000000024"), 251 // am/pm 252 ("%p", "AM"), 253 // day 254 ("%d", "01"), 255 // month 256 ("%m", "01"), 257 // year 258 ("%Y", "1994"), 259 ("%y", "94"), 260 // month name 261 ("%b", "Jan"), 262 ("%B", "January"), 263 // weekday 264 ("%u", "6"), 265 ("%w", "6"), 266 ("%a", "Sat"), 267 ("%A", "Saturday"), 268 // yearday 269 ("%j", "001"), 270 // week 271 ("%W", "00"), 272 // full date 273 ("%F", "1994-01-01"), 274 // full time 275 ("%T", "02:17:05"), 276 // Unix timestamp 277 ("%s", "757390625"), 278 ]; 279 280 for (let i = 0z; i < len(cases); i += 1) { 281 const layout = cases[i].0; 282 const expected = cases[i].1; 283 const actual = asformat(layout, &dt)!; 284 defer free(actual); 285 if (actual != expected) { 286 fmt::printfln( 287 "expected format({}, &dt) to be {} but was {}", 288 layout, expected, actual 289 )!; 290 abort(); 291 }; 292 }; 293 };