format.ha (9771B)
1 // SPDX-License-Identifier: MPL-2.0 2 // (c) Hare authors <https://harelang.org> 3 4 use fmt; 5 use io; 6 use memio; 7 use strings; 8 use time::chrono; 9 10 // [[format]] layout for the email date format. 11 export def EMAIL: str = "%a, %d %b %Y %H:%M:%S %z"; 12 13 // [[format]] layout for the email date format, with zone offset and 14 // zone abbreviation. 15 export def EMAILZONE: str = "%a, %d %b %Y %H:%M:%S %z %Z"; 16 17 // [[format]] layout for the POSIX locale's default date & time representation. 18 export def POSIX: str = "%a %b %e %H:%M:%S %Y"; 19 20 // [[format]] layout compatible with RFC 3339. 21 export def RFC3339: str = "%Y-%m-%dT%H:%M:%S%z"; 22 23 // [[format]] layout for a standard, collatable timestamp. 24 export def STAMP: str = "%Y-%m-%d %H:%M:%S"; 25 26 // [[format]] layout for a standard, collatable timestamp with nanoseconds. 27 export def STAMPNANO: str = "%Y-%m-%d %H:%M:%S.%N"; 28 29 // [[format]] layout for a standard, collatable timestamp with nanoseconds 30 // and zone offset. 31 export def STAMPZOFF: str = "%Y-%m-%d %H:%M:%S.%N %z"; 32 33 // [[format]] layout for a standard, collatable timestamp with nanoseconds, 34 // zone offset, and zone abbreviation. 35 export def STAMPZONE: str = "%Y-%m-%d %H:%M:%S.%N %z %Z"; 36 37 // [[format]] layout for a standard, collatable timestamp with nanoseconds, 38 // zone offset, zone abbreviation, and locality. 39 export def STAMPLOC: str = "%Y-%m-%d %H:%M:%S.%N %z %Z %L"; 40 41 // [[format]] layout for an ISO week-numbering timestamp. 42 export def ISOWKSTAMP: str = "%G-W%V-%u %H:%M:%S"; 43 44 // [[format]] layout for a friendly, comprehensive datetime. 45 export def JOURNAL: str = "%Y %b %d, %a %H:%M:%S %z %Z %L"; 46 47 // [[format]] layout for a friendly, terse datetime. 48 export def WRIST: str = "%b-%d %a %H:%M %Z"; 49 50 // [[format]] layout for a precise timescalar second and nanosecond. 51 export def QUARTZ: str = "%s.%N"; 52 53 // [[format]] layout for a precise timescalar second, nanosecond, 54 // and zone offset. 55 export def QUARTZZOFF: str = "%s.%N%z"; 56 57 // [[format]] layout for a precise timescalar second, nanosecond, 58 // and locality. 59 export def QUARTZLOC: str = "%s.%N:%L"; 60 61 def WEEKDAYS: [_]str = [ 62 "Monday", 63 "Tuesday", 64 "Wednesday", 65 "Thursday", 66 "Friday", 67 "Saturday", 68 "Sunday", 69 ]; 70 71 def WEEKDAYS_SHORT: [_]str = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; 72 73 def MONTHS: [_]str = [ 74 "January", 75 "February", 76 "March", 77 "April", 78 "May", 79 "June", 80 "July", 81 "August", 82 "September", 83 "October", 84 "November", 85 "December", 86 ]; 87 88 def MONTHS_SHORT: [_]str = [ 89 "Jan", "Feb", "Mar", 90 "Apr", "May", "Jun", 91 "Jul", "Aug", "Sep", 92 "Oct", "Nov", "Dec", 93 ]; 94 95 // TODO: Make format() accept parameters of type (date | period), using the 96 // "intervals" standard representation provided by ISO 8601? 97 // 98 // See https://en.wikipedia.org/wiki/ISO_8601#Time_intervals 99 // 100 // Ticket: https://todo.sr.ht/~sircmpwn/hare/650 101 102 // Formats a [[date]] and writes it into a caller supplied buffer. 103 // The returned string is borrowed from this buffer. 104 export fn bsformat( 105 buf: []u8, 106 layout: str, 107 d: *date, 108 ) (str | io::error) = { 109 let sink = memio::fixed(buf); 110 format(&sink, layout, d)?; 111 return memio::string(&sink)!; 112 }; 113 114 // Formats a [[date]] and writes it into a heap-allocated string. 115 // The caller must free the return value. 116 export fn asformat(layout: str, d: *date) (str | io::error) = { 117 let sink = memio::dynamic(); 118 format(&sink, layout, d)?; 119 return memio::string(&sink)!; 120 }; 121 122 // Formats a [[date]] according to a layout and writes to an [[io::handle]]. 123 // 124 // The layout may contain any of the following format specifiers listed below. 125 // These specifiers emit 2 digit zero-padded decimals unless stated otherwise. 126 // Use of unimplemented specifiers or an invalid layout will cause an abort. 127 // 128 // - %% : A single literal '%' character. 129 // - %a : The day of the week, abbreviated name. ("Sun"). 130 // - %A : The day of the week, full name. ("Sunday"). 131 // - %b : The month, abbreviated name. ("Jan"). 132 // - %B : The month, full name. ("January"). 133 // - %C : The century (the year without the last 2 digits). ("20"). 134 // - %d : The day of the month. Range 01 to 31. ("02"). 135 // - %e : The day of the month. Range 1 to 31, 136 // right-aligned, space-padded. (" 2"). 137 // - %F : The full Gregorian calendar date. 138 // Alias for "%Y-%m-%d". ("2000-01-02"). 139 // - %G : The ISO week-numbering year. At least 4 digits. 140 // ISO-years before the Common Era have a minus sign prefix. ("1999"). 141 // - %H : The hour of the day of a 24-hour clock. Range 00 to 23. ("15"). 142 // - %I : The hour of the day of a 12-hour clock. Range 01 to 12. ("03"). 143 // - %j : The ordinal day of the year. 3 digits, range 001 to 366. ("002"). 144 // - %L : The locality's name (the timezone identifier). ("Europe/Amsterdam"). 145 // - %m : The month of the year. Range 01 to 12. ("01"). 146 // - %M : The minute of the hour. Range 00 to 59. ("04"). 147 // - %N : The nanosecond of the second. 9 digits, 148 // range 000000000 to 999999999. ("600000000"). 149 // - %p : The meridian indicator, either "AM" or "PM". 150 // "AM" includes midnight, and "PM" includes noon. 151 // - %s : The number of seconds since the locality's epoch. ("946821845"). 152 // - %S : The second of the minute. Range 00 to 59. ("05"). 153 // - %T : The wall-time of a 24-hour clock without nanoseconds. 154 // Alias for "%H:%M:%S". ("15:04:05"). 155 // - %u : The day of the week. 1 digit, range 1 to 7, Monday to Sunday. ("7"). 156 // - %U : The sunday-week of the year. Range 00 to 53. 157 // The year's first Sunday is the first day of week 01. ("01"). 158 // - %V : The week of the ISO week-numbering year. Range 01 to 53. ("52"). 159 // - %w : The day of the sunday-week. 160 // 1 digit, range 0 to 6, Sunday to Saturday. ("0"). 161 // - %W : The week of the year. Range 00 to 53. 162 // The year's first Monday is the first day of week 01. ("00"). 163 // - %y : The year's last 2 digits, no century digits. Range 00 to 99. ("00"). 164 // - %Y : The year. At least 4 digits. 165 // Years before the Common Era have a minus sign prefix. ("2000"). 166 // - %z : The observed zone offset. ("+0100"). 167 // - %Z : The observed zone abbreviation. ("CET"). 168 export fn format( 169 h: io::handle, 170 layout: str, 171 d: *date 172 ) (size | io::error) = { 173 let iter = strings::iter(layout); 174 let z = 0z; 175 for (let r => strings::next(&iter)) { 176 if (r == '%') { 177 match (strings::next(&iter)) { 178 case let spec: rune => 179 z += fmtspec(h, spec, d)?; 180 case done => 181 abort("layout has dangling '%'"); 182 }; 183 } else { 184 z += memio::appendrune(h, r)?; 185 }; 186 }; 187 return z; 188 }; 189 190 fn fmtspec(out: io::handle, r: rune, d: *date) (size | io::error) = { 191 switch (r) { 192 case 'a' => 193 return fmt::fprint(out, WEEKDAYS_SHORT[_weekday(d)]); 194 case 'A' => 195 return fmt::fprint(out, WEEKDAYS[_weekday(d)]); 196 case 'b' => 197 return fmt::fprint(out, MONTHS_SHORT[_month(d) - 1]); 198 case 'B' => 199 return fmt::fprint(out, MONTHS[_month(d) - 1]); 200 case 'C' => 201 return fmt::fprintf(out, "{:.2}", _year(d) / 100); 202 case 'd' => 203 return fmt::fprintf(out, "{:.2}", _day(d)); 204 case 'e' => 205 return fmt::fprintf(out, "{: 2}", _day(d)); 206 case 'F' => 207 return fmt::fprintf(out, "{:.4}-{:.2}-{:.2}", 208 _year(d), _month(d), _day(d)); 209 case 'G' => 210 return fmt::fprintf(out, "{:.4}", _isoweekyear(d)); 211 case 'H' => 212 return fmt::fprintf(out, "{:.2}", _hour(d)); 213 case 'I' => 214 return fmt::fprintf(out, "{:.2}", (_hour(d) + 11) % 12 + 1); 215 case 'j' => 216 return fmt::fprintf(out, "{:.3}", _yearday(d)); 217 case 'L' => 218 return fmt::fprint(out, d.loc.name); 219 case 'm' => 220 return fmt::fprintf(out, "{:.2}", _month(d)); 221 case 'M' => 222 return fmt::fprintf(out, "{:.2}", _minute(d)); 223 case 'N' => 224 return fmt::fprintf(out, "{:.9}", _nanosecond(d)); 225 case 'p' => 226 return fmt::fprint(out, if (_hour(d) < 12) "AM" else "PM"); 227 case 's' => 228 return fmt::fprintf(out, "{:.2}", d.sec); 229 case 'S' => 230 return fmt::fprintf(out, "{:.2}", _second(d)); 231 case 'T' => 232 return fmt::fprintf(out, "{:.2}:{:.2}:{:.2}", 233 _hour(d), _minute(d), _second(d)); 234 case 'u' => 235 return fmt::fprintf(out, "{}", _weekday(d) + 1); 236 case 'U' => 237 return fmt::fprintf(out, "{:.2}", _sundayweek(d)); 238 case 'V' => 239 return fmt::fprintf(out, "{:.2}", _isoweek(d)); 240 case 'w' => 241 return fmt::fprintf(out, "{}", (_weekday(d) + 1) % 7); 242 case 'W' => 243 return fmt::fprintf(out, "{:.2}", _week(d)); 244 case 'y' => 245 return fmt::fprintf(out, "{:.2}", _year(d) % 100); 246 case 'Y' => 247 return fmt::fprintf(out, "{:.4}", _year(d)); 248 case 'z' => 249 const (sign, zo) = if (chrono::ozone(d).zoff >= 0) { 250 yield ('+', calc_hmsn(chrono::ozone(d).zoff)); 251 } else { 252 yield ('-', calc_hmsn(-chrono::ozone(d).zoff)); 253 }; 254 const (hr, mi) = (zo.0, zo.1); 255 return fmt::fprintf(out, "{}{:.2}{:.2}", sign, hr, mi); 256 case 'Z' => 257 return fmt::fprint(out, chrono::ozone(d).abbr); 258 case '%' => 259 return fmt::fprint(out, "%"); 260 case => 261 abort("layout has unrecognised specifier"); 262 }; 263 }; 264 265 @test fn format() void = { 266 const d = new(chrono::UTC, 0, 1994, 1, 1, 2, 17, 5, 24)!; 267 268 const cases = [ 269 // special characters 270 ("%%", "%"), 271 // hour 272 ("%H", "02"), 273 ("%I", "02"), 274 // minute 275 ("%M", "17"), 276 // second 277 ("%S", "05"), 278 // nanosecond 279 ("%N", "000000024"), 280 // am/pm 281 ("%p", "AM"), 282 // day 283 ("%d", "01"), 284 // day 285 ("%e", " 1"), 286 // month 287 ("%m", "01"), 288 // year 289 ("%Y", "1994"), 290 ("%y", "94"), 291 ("%C", "19"), 292 // month name 293 ("%b", "Jan"), 294 ("%B", "January"), 295 // weekday 296 ("%u", "6"), 297 ("%w", "6"), 298 ("%a", "Sat"), 299 ("%A", "Saturday"), 300 // yearday 301 ("%j", "001"), 302 // week 303 ("%W", "00"), 304 // full date 305 ("%F", "1994-01-01"), 306 // full time 307 ("%T", "02:17:05"), 308 // Unix timestamp 309 ("%s", "757390625"), 310 ]; 311 312 for (let (layout, expected) .. cases) { 313 const actual = asformat(layout, &d)!; 314 defer free(actual); 315 if (actual != expected) { 316 fmt::printfln( 317 "expected format({}, &d) to be {} but was {}", 318 layout, expected, actual 319 )!; 320 abort(); 321 }; 322 }; 323 };