hare

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

format.ha (11785B)


      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 strconv;
     10 use strings;
     11 use strio;
     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,
     36 // zone offset, zone abbreviation, and locality.
     37 export def STAMP_NOZL: str = "%Y-%m-%d %H:%M:%S.%N %z %Z %L";
     38 
     39 def WEEKDAYS: [_]str = [
     40 	"Monday",
     41 	"Tuesday",
     42 	"Wednesday",
     43 	"Thursday",
     44 	"Friday",
     45 	"Saturday",
     46 	"Sunday",
     47 ];
     48 
     49 def WEEKDAYS_SHORT: [_]str = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
     50 
     51 def MONTHS: [_]str = [
     52 	"January",
     53 	"February",
     54 	"March",
     55 	"April",
     56 	"May",
     57 	"June",
     58 	"July",
     59 	"August",
     60 	"September",
     61 	"October",
     62 	"November",
     63 	"December",
     64 ];
     65 
     66 def MONTHS_SHORT: [_]str = [
     67 	"Jan", "Feb", "Mar",
     68 	"Apr", "May", "Jun",
     69 	"Jul", "Aug", "Sep",
     70 	"Oct", "Nov", "Dec",
     71 ];
     72 
     73 // TODO: Make format() accept parameters of type (datetime | period), using the
     74 // "intervals" standard representation provided by ISO 8601?
     75 //
     76 // See https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
     77 //
     78 // Ticket: https://todo.sr.ht/~sircmpwn/hare/650
     79 
     80 // Formats a [[datetime]] and writes it into a caller supplied buffer.
     81 // The returned string is borrowed from this buffer.
     82 export fn bsformat(
     83 	buf: []u8,
     84 	layout: str,
     85 	dt: *datetime,
     86 ) (str | invalid | io::error) = {
     87 	let sink = strio::fixed(buf);
     88 	format(&sink, layout, dt)?;
     89 	return strio::string(&sink);
     90 };
     91 
     92 // Formats a [[datetime]] and writes it into a heap-allocated string.
     93 // The caller must free the return value.
     94 export fn asformat(layout: str, dt: *datetime) (str | invalid | io::error) = {
     95 	let sink = strio::dynamic();
     96 	format(&sink, layout, dt)?;
     97 	return strio::string(&sink);
     98 };
     99 
    100 fn fmtout(out: io::handle, r: rune, dt: *datetime) (size | io::error) = {
    101 	switch (r) {
    102 	case 'a' =>
    103 		return fmt::fprint(out, WEEKDAYS_SHORT[weekday(dt) - 1]);
    104 	case 'A' =>
    105 		return fmt::fprint(out, WEEKDAYS[weekday(dt) - 1]);
    106 	case 'b' =>
    107 		return fmt::fprint(out, MONTHS_SHORT[month(dt) - 1]);
    108 	case 'B' =>
    109 		return fmt::fprint(out, MONTHS[month(dt) - 1]);
    110 	case 'd' =>
    111 		return fmt::fprintf(out, "{:02}", day(dt));
    112 	case 'H' =>
    113 		return fmt::fprintf(out, "{:02}", hour(dt));
    114 	case 'I' =>
    115 		return fmt::fprintf(out, "{:02}", hour12(dt));
    116 	case 'j' =>
    117 		return fmt::fprint(out, strconv::itos(yearday(dt)));
    118 	case 'L' =>
    119 		return fmt::fprint(out, dt.loc.name);
    120 	case 'm' =>
    121 		return fmt::fprintf(out, "{:02}", month(dt));
    122 	case 'M' =>
    123 		return fmt::fprintf(out, "{:02}", min(dt));
    124 	case 'N' =>
    125 		return fmt::fprintf(out, "{:09}", strconv::itos(nsec(dt)));
    126 	case 'p' =>
    127 		const s = if (hour(dt) < 12) {
    128 			yield "AM";
    129 		} else {
    130 			yield "PM";
    131 		};
    132 		return fmt::fprint(out, s);
    133 	case 'S' =>
    134 		return fmt::fprintf(out, "{:02}", sec(dt));
    135 	case 'u' =>
    136 		return fmt::fprint(out, strconv::itos(weekday(dt)));
    137 	case 'U' =>
    138 		return fmt::fprintf(out, "{:02}", _sundayweek(dt));
    139 	case 'w' =>
    140 		return fmt::fprint(out, strconv::itos(weekday(dt) % 7));
    141 	case 'W' =>
    142 		return fmt::fprintf(out, "{:02}", week(dt));
    143 	case 'y' =>
    144 		let year_str = strconv::itos(year(dt));
    145 		year_str = strings::sub(year_str, len(year_str) - 2, strings::end);
    146 		return fmt::fprint(out, year_str);
    147 	case 'Y' =>
    148 		return fmt::fprint(out, strconv::itos(year(dt)));
    149 	case 'z' =>
    150 		// TODO: test me
    151 		let pm = '+';
    152 		const z = if (dt.zone.zoffset >= 0) {
    153 			yield calc_hmsn(dt.zone.zoffset);
    154 		} else {
    155 			pm = '-';
    156 			yield calc_hmsn(-dt.zone.zoffset);
    157 		};
    158 		return fmt::fprintf(out, "{}{:02}{:02}", pm, z.0, z.1);
    159 	case 'Z' =>
    160 		return fmt::fprint(out, dt.zone.abbr);
    161 	case '%' =>
    162 		return fmt::fprint(out, "%");
    163 	case =>
    164 		abort("Invalid format string provided to datetime::format");
    165 	};
    166 };
    167 
    168 // Formats a [[datetime]] according to a layout and writes to an [[io::handle]].
    169 //
    170 // The layout may contain any of the following format specifiers listed below.
    171 // Implemented are a subset of the POSIX strftime(3) format specifiers, as well
    172 // as some others. Use of unimplemented specifiers or an otherwise invalid
    173 // layout will cause an abort.
    174 //
    175 // 	%% -- A literal '%' character.
    176 // 	%a -- The abbreviated name of the day of the week.
    177 // 	%A -- The full name of the day of the week.
    178 // 	%b -- The abbreviated name of the month.
    179 // 	%B -- The full name of the month.
    180 // 	%d -- The day of the month (decimal, range 01 to 31).
    181 // 	%H -- The hour of the day as from a 24-hour clock (range 00 to 23).
    182 // 	%I -- The hour of the day as from a 12-hour clock (range 01 to 12).
    183 // 	%j -- The ordinal day of the year (range 001 to 366).
    184 // 	%L -- The locality's name (the timezone's identifier).
    185 // 	%m -- The month (decimal, range 01 to 12).
    186 // 	%M -- The minute (decimal, range 00 to 59).
    187 // 	%N -- The nanosecond of the second (range 000000000 to 999999999).
    188 // 	%p -- Either "AM" or "PM" according to the current time.
    189 // 	      "AM" includes midnight, and "PM" includes noon.
    190 // 	%S -- The second of the minute (range 00 to 60).
    191 // 	%u -- The day of the week (decimal, range 1 to 7). 1 represents Monday.
    192 // 	%U -- The week number of the current year (range 00 to 53),
    193 // 	      starting with the first Sunday as the first day of week 01.
    194 // 	%w -- The day of the week (decimal, range 0 to 6). 0 represents Sunday.
    195 // 	%W -- The week number of the current year (range 00 to 53),
    196 // 	      starting with the first Monday as the first day of week 01.
    197 // 	%y -- The year without the century digits (range 00 to 99).
    198 // 	%Y -- The year.
    199 // 	%z -- The observed zone offset.
    200 // 	%Z -- The observed zone abbreviation.
    201 //
    202 export fn format(
    203 	h: io::handle,
    204 	layout: str,
    205 	dt: *datetime
    206 ) (size | invalid | io::error) = {
    207 	const iter = strings::iter(layout);
    208 	let escaped = false;
    209 	let n = 0z;
    210 	for (true) {
    211 		let r: rune = match (strings::next(&iter)) {
    212 		case void =>
    213 			break;
    214 		case let r: rune =>
    215 			yield r;
    216 		};
    217 
    218 		if (escaped) {
    219 			escaped = false;
    220 			n += fmtout(h, r, dt)?;
    221 		} else {
    222 			if (r == '%') {
    223 				escaped = true;
    224 			} else {
    225 				strio::appendrune(h, r)?;
    226 			};
    227 		};
    228 	};
    229 	return n;
    230 };
    231 
    232 fn get_default_locale_string_index(iter: *strings::iterator, list: []str) (int | invalid) = {
    233 	const name = strings::iterstr(iter);
    234 	if (len(name) == 0) {
    235 		return invalid;
    236 	};
    237 	for(let i = 0z; i < len(list); i += 1) {
    238 		if (strings::hasprefix(name, list[i])) {
    239 			// Consume name
    240 			for (let j = 0z; j < len(list[i]); j += 1) {
    241 				strings::next(iter);
    242 			};
    243 			return (i: int) + 1;
    244 		};
    245 	};
    246 	return invalid;
    247 };
    248 
    249 fn get_max_n_digits(iter: *strings::iterator, n: uint) (int | invalid) = {
    250 	let buf: [64]u8 = [0...];
    251 	let bufstr = strio::fixed(buf);
    252 	for (let i = 0z; i < n; i += 1) {
    253 		let r: rune = match (strings::next(iter)) {
    254 			case void =>
    255 				break;
    256 			case let r: rune =>
    257 				yield r;
    258 		};
    259 		if (!ascii::isdigit(r)) {
    260 			strings::prev(iter);
    261 			break;
    262 		};
    263 		match (strio::appendrune(&bufstr, r)) {
    264 		case io::error =>
    265 			return invalid;
    266 		case =>
    267 			void;
    268 		};
    269 	};
    270 	return match (strconv::stoi(strio::string(&bufstr))) {
    271 	case let res: int =>
    272 		yield res;
    273 	case =>
    274 		yield invalid;
    275 	};
    276 };
    277 
    278 fn eat_one_rune(iter: *strings::iterator, needle: rune) (uint | invalid) = {
    279 	let s_r = match (strings::next(iter)) {
    280 	case void =>
    281 		return invalid;
    282 	case let r: rune =>
    283 		yield r;
    284 	};
    285 	if (s_r == needle) {
    286 		return 1;
    287 	} else {
    288 		strings::prev(iter);
    289 		return 0;
    290 	};
    291 };
    292 
    293 fn clamp_int(i: int, min: int, max: int) int = {
    294 	return if (i < min) {
    295 		yield min;
    296 	} else if (i > max) {
    297 		yield max;
    298 	} else {
    299 		yield i;
    300 	};
    301 };
    302 
    303 fn hour12(dt: *datetime) int = {
    304 	let mod_hour = hour(dt) % 12;
    305 	if (mod_hour == 0) {
    306 		mod_hour = 12;
    307 	};
    308 	return mod_hour;
    309 };
    310 
    311 @test fn format() void = {
    312 	const dt = new(chrono::UTC, 0, 1994, 01, 01, 02, 17, 05, 24)!;
    313 
    314 	const cases = [
    315 		// special characters
    316 		("%%", "%"),
    317 		// hour
    318 		("%H", "02"),
    319 		("%I", "02"),
    320 		// minute
    321 		("%M", "17"),
    322 		// second
    323 		("%S", "05"),
    324 		// nanosecond
    325 		("%N", "000000024"),
    326 		// am/pm
    327 		("%p", "AM"),
    328 		// day
    329 		("%d", "01"),
    330 		// month
    331 		("%m", "01"),
    332 		// year
    333 		("%Y", "1994"),
    334 		("%y", "94"),
    335 		// month name
    336 		("%b", "Jan"),
    337 		("%B", "January"),
    338 		// weekday
    339 		("%u", "6"),
    340 		("%w", "6"),
    341 		("%a", "Sat"),
    342 		("%A", "Saturday"),
    343 		// yearday
    344 		("%j", "1"),
    345 		// week
    346 		("%W", "00"),
    347 	];
    348 
    349 	for (let i = 0z; i < len(cases); i += 1) {
    350 		const layout = cases[i].0;
    351 		const expected = cases[i].1;
    352 		const actual = asformat(layout, &dt)!;
    353 		defer free(actual);
    354 		if (actual != expected) {
    355 			fmt::printfln(
    356 				"expected format({}, &dt) to be {} but was {}",
    357 				layout, expected, actual
    358 			)!;
    359 			abort();
    360 		};
    361 	};
    362 };
    363 
    364 // TODO: Refactor this once the rest of the parse() refactoring is done
    365 // Ticket: https://todo.sr.ht/~sircmpwn/hare/648
    366 
    367 // @test fn parse() void = {
    368 // 	let dt = datetime {...};
    369 
    370 // 	// General tests
    371 // 	parse("%Y-%m-%d %H:%M:%S.%N", "1994-08-27 11:01:02.123", &dt)!;
    372 // 	assert(dt.year as int == 1994 &&
    373 // 		dt.month as int == 08 &&
    374 // 		dt.day as int == 27 &&
    375 // 		dt.hour as int == 11 &&
    376 // 		dt.min as int == 01 &&
    377 // 		dt.sec as int == 02 &&
    378 // 		dt.nsec as int == 123, "invalid parsing results");
    379 
    380 // 	// General errors
    381 // 	assert(parse("%Y-%m-%d", "1a94-08-27", &dt) is invalid,
    382 // 		"invalid datetime string did not throw error");
    383 
    384 // 	assert(parse("%Y-%m-%d", "1994-123-27", &dt) is invalid,
    385 // 		"invalid datetime string did not throw error");
    386 
    387 // 	assert(parse("%Y-%m-%d", "a994-08-27", &dt) is invalid,
    388 // 		"invalid datetime string did not throw error");
    389 
    390 // 	// Basic specifiers
    391 // 	parse("%a", "Tue", &dt)!;
    392 // 	assert(dt.weekday as int == 2, "invalid parsing results");
    393 
    394 // 	parse("%a %d", "Tue 27", &dt)!;
    395 // 	assert(dt.weekday as int == 2 &&
    396 // 		dt.day as int == 27, "invalid parsing results");
    397 
    398 // 	parse("%A", "Tuesday", &dt)!;
    399 // 	assert(dt.weekday as int == 2, "invalid parsing results");
    400 
    401 // 	parse("%b", "Feb", &dt)!;
    402 // 	assert(dt.month as int == 2, "invalid parsing results");
    403 
    404 // 	parse("%B", "February", &dt)!;
    405 // 	assert(dt.month as int == 2, "invalid parsing results");
    406 
    407 // 	parse("%I", "14", &dt)!;
    408 // 	assert(dt.hour as int == 2, "invalid parsing results");
    409 
    410 // 	parse("%j", "123", &dt)!;
    411 // 	assert(dt.yearday as int == 123, "invalid parsing results");
    412 
    413 // 	parse("%H %p", "6 AM", &dt)!;
    414 // 	assert(dt.hour as int == 6, "invalid parsing results");
    415 
    416 // 	parse("%H %p", "6 PM", &dt)!;
    417 // 	assert(dt.hour as int == 18, "invalid parsing results");
    418 
    419 // 	assert(parse("%H %p", "13 PM", &dt) is invalid,
    420 // 		"invalid parsing results");
    421 
    422 // 	assert(parse("%H %p", "PM 6", &dt) is invalid,
    423 // 		"invalid parsing results");
    424 
    425 // 	parse("%u", "7", &dt)!;
    426 // 	assert(dt.weekday as int == 7, "invalid parsing results");
    427 
    428 // 	parse("%U", "2", &dt)!;
    429 // 	assert(dt.week as int == 2, "invalid parsing results");
    430 
    431 // 	parse("%U", "99", &dt)!;
    432 // 	assert(dt.week as int == 53, "invalid parsing results");
    433 
    434 // 	parse("%w", "0", &dt)!;
    435 // 	assert(dt.weekday as int == 7, "invalid parsing results");
    436 
    437 // 	parse("%W", "2", &dt)!;
    438 // 	assert(dt.week as int == 2, "invalid parsing results");
    439 // };