hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

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 };