hare

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

format.ha (8400B)


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