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