hare

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

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