~sircmpwn/hautils

284c2f68573a8479a995df08f25ae7305ce6a73b — Curtis Arthaud 26 days ago 0558967 master
cal: new util

Signed-off-by: Curtis Arthaud <uku82@gmx.fr>
2 files changed, 276 insertions(+), 0 deletions(-)

M Makefile
A cal.ha
M Makefile => Makefile +2 -0
@@ 5,6 5,7 @@ HAREFLAGS=

utils=\
	basename \
	cal \
	cat \
	cut \
	dirname \


@@ 38,6 39,7 @@ clean:
	$(HARE) build -q $(HAREFLAGS) -o $@ $<

basename: basename.ha main/main.ha
cal: cal.ha main/main.ha
cat: cat.ha main/main.ha
dirname: dirname.ha main/main.ha
env: env.ha main/main.ha

A cal.ha => cal.ha +274 -0
@@ 0,0 1,274 @@
// Algorithm for finding weekday based on Openbsd's cal (usr.bin/cal/cal.c)
// Posix (and this util) defines
// the gregorian calendar to start in 1752.
// This is only true for Britain: https://norbyhus.dk/calendar.php
use fmt;
use getopt;
use main;
use os;
use strconv;
use time;
use time::date;

type config = struct {
	month: int,
	year: int
};

export fn utilmain() (void | main::error) = {
	const help: []getopt::help = [
		"print calendar",
		"[[month] year]",
	];
	const cmd = getopt::parse(os::args, help...);
	defer getopt::finish(&cmd);

	let conf = config{ ... };

	switch (len(cmd.args)) {
	case 0 => // display current month
		const now = date::localnow();
		conf.month = date::month(&now);
		conf.year = date::year(&now);
	case 1 => // display all months of given year
		conf.month = 0;
		conf.year = parse_arg(cmd.args[0]);
		if (conf.year < 1 || conf.year > 9999) {
			fmt::fatal("Invalid year. Valid range: 1-9999");
		};
	case 2 => // display given month of given year
		conf.month = parse_arg(cmd.args[0]);
		if (conf.month < 1 || conf.month > 12) {
			fmt::fatal("Invalid month. Valid range: 1-12");
		};
		conf.year = parse_arg(cmd.args[1]);
		if (conf.year < 1 || conf.year > 9999) {
			fmt::fatal("Invalid year. Valid range: 1-9999");
		};
	case =>
		main::usage(help);
	};

	if (conf.month == 0) {
		yearly(conf.year);
	} else {
		monthly(conf.month, conf.year);
	};
};

fn parse_arg(arg: str) int = {
	return match (strconv::stoi(arg)) {
	case let i: int => yield i;
	case strconv::error =>
		fmt::fatal("Invalid date argument.");
	};
};

def DAY_HEADINGS = "Su Mo Tu We Th Fr Sa";

const month_names: [12]str = [
	"January", "February", "March",
	"April", "May", "June",
	"July", "August", "September",
	"October", "November", "December",
];

const days_in_month: [2][13]int = [
	[0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
	[0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
];

def MAXDAYS = 42; // max sloths in a 6x7 array
def WEEK_LEN = 20;
def SPACE = -1;

const sep1752: [MAXDAYS]int = [
	SPACE, SPACE, 1,     2,     14,    15,    16,
	17,    18,    19,    20,    21,    22,    23,
	24,    25,    26,    27,    28,    29,    30,
	SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE,
	SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE,
	SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE,
];

const empty: [MAXDAYS]int = [
	SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE,
	SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE,
	SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE,
	SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE,
	SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE,
	SPACE, SPACE, SPACE, SPACE, SPACE, SPACE, SPACE,
];

fn ascii_day(day: int, trail: bool) void = {
	if (day == SPACE) {
		fmt::print("  ")!;
		if (trail) {
			fmt::print(" ")!;
		};
		return;
	};
	fmt::printf("{:2}", day)!;
	if (trail) {
		fmt::print(" ")!;
	};
};

fn monthly(month: int, year: int) void = {
	const days = day_array(month, year);

	// 14 is len of longest month name (september) + space + year
	let buf: [14]u8 = [0...];
	const head = fmt::bsprintf(buf, "{} {}", month_names[month - 1], year);
	fmt::printf("{%}\n{}\n",
		head, &fmt::mods {
			pad = ' ',
			width = WEEK_LEN,
			alignment = fmt::alignment::CENTER,
			...
		},
		DAY_HEADINGS
	)!;
	for (let row = 0; row < 6; row += 1) {
		const firstday = SPACE;
		for (let col = 0; col < 6; col += 1) {
			if (firstday == SPACE && days[row * 7 + col] != SPACE) {
				firstday = days[row * 7 + col];
			};
			ascii_day(days[row * 7 + col], true);
		};
		ascii_day(days[row * 7 + 6], false);
		fmt::println()!;
	};
};

fn yearly(year: int) void = {
	def HEAD_SEP = 2; // space between day headings

	fmt::printf("{%}\n\n", year, &fmt::mods {
			pad = ' ', width = WEEK_LEN * 3 + HEAD_SEP * 2,
			alignment = fmt::alignment::CENTER, ...
	})!;

	let days = []: [][MAXDAYS]int;
	for (let i = 0; i < 12; i += 1) {
		append(days, day_array(i + 1, year));
	};

	for (let month = 0; month < 12; month += 3) {
		printfwidth(month_names[month], WEEK_LEN);
		printfwidth(month_names[month + 1], WEEK_LEN + HEAD_SEP * 2);
		printfwidth(month_names[month + 2], WEEK_LEN);
		fmt::println()!;
		fmt::print(DAY_HEADINGS)!;
		printfwidth(" ", HEAD_SEP);
		fmt::print(DAY_HEADINGS)!;
		printfwidth(" ", HEAD_SEP);
		fmt::println(DAY_HEADINGS)!;

		for (let row = 0; row < 6; row += 1) {
			for (let which_cal = 0; which_cal < 3; which_cal += 1) {
				let firstday = SPACE;
				for (let col = 0; col < 6; col += 1) {
					let dp = days[month + which_cal][row * 7 + col];
					if (firstday == SPACE && dp != SPACE) {
						firstday = dp;
					};
					ascii_day(dp, true);
				};
				ascii_day(days[month + which_cal][row * 7 + 6], false);
				fmt::print("  ")!;
			};
			fmt::println()!;
		};
	};
};

fn printfwidth(s: str, width: size) size = {
	return fmt::printf("{%}",
		s,
		&fmt::mods {
			pad = ' ',
			width = width,
			alignment = fmt::alignment::CENTER,
			...
		},
	)!;
};

fn day_array(month: int, year: int) [MAXDAYS]int = {
	if (month == 9 && year == 1752) {
		return sep1752;
	};

	let days = empty;
	let dm = days_in_month[leap_year(year)][month];
	let dw = day_in_week(month, year);
	let day = 1;
	for (dm > 0; dm -= 1) {
		days[dw] = day;
		day += 1;
		dw += 1;
	};

	return days;
};

fn day_in_week(month: int, year: int) int = {
	def SATURDAY = 6; // 1 Jan 1
	def FIRST_MISSING_DAY = 639799; // 3 Sep 1752
	def NUMBER_MISSING_DAYS = 11;

	const d = (year - 1) * 365 + count_leap_years(year - 1)
		+ day_in_year(month, year);
	if (d < FIRST_MISSING_DAY) {
		return ((d - 1 + SATURDAY) % 7);
	};
	if (d >= (FIRST_MISSING_DAY + NUMBER_MISSING_DAYS)) {
		return (((d - 1 + SATURDAY) - NUMBER_MISSING_DAYS) % 7);
	};
	return 4; // Thursday
};

fn day_in_year(month: int, year: int) int = {
	const leap = leap_year(year);
	let day = 1;
	for (let i = 1; i < month; i += 1) {
		day += days_in_month[leap][i];
	};
	return day;
};

// leap year -- account for gregorian reformation in 1752
fn leap_year(yr: int) int = {
	if (yr <= 1752) {
		if (yr % 4 == 0) {
			return 1;
		} else {
			return 0;
		};
	} else {
		if (yr % 4 == 0 && ((yr % 100) != 0 || yr % 400 == 0)) {
			return 1;
		} else {
			return 0;
		};
	};
};

// number of leap years between year 1 and this year, not inclusive
fn count_leap_years(yr: int) int = {
	const centuries = if (yr > 1700) {
		yield yr / 100 - 17;
	} else {
		yield 0;
	};
	const quad_centuries = if (yr > 1600) {
		yield (yr - 1600) / 400;
	} else {
		yield 0;
	};

	return (yr / 4 - centuries + quad_centuries);
};