@@ 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);
+};
+