hare

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

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