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