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