16 votes

Programming Challenge: Mini Calendar Display

It has been a while since the last time we did something like a programming challenge, so here's one for ya.

The life story of the author before you get to the recipe

I've been working on a little "today" website, showing what day it is, if it's a significant date for holiday/independence/... reasons, and one of the things I wanted was a small calendar display that showed the full month and days in each week. Like how XFCE's Clock plugin does it.

So I got to figuring it out and after finishing it up I thought this could be a nice little programming challenge. It has one input (the date) that can be in any of the rows and columns, and it's up to you to figure out all the rest.

Here's how mine looks in about 250ish lines of TypeScript (TSX technically) and SCSS.


The Recipe

Make a mini calendar display that shows all the days of the current month and at least one day of each adjacent month. So for example for May 2023: the 31 days in May, the 30th of April and the 1st of June should at least be visible.

It can be in any language with any method of rendering; simple text, TUI/GUI toolkit, web-based, raytraced in some game engine, nixie tubes, whatever.

Bonus Points

  • Highlight the current day name in the first row, if you're including day names.
  • Highlight the current day number, wherever it is.
  • Highlight the current week row, wherever it is.
  • Differentiate the days of current month and the days of the other adjacent months, wherever they are.

Some Tips

The week number

If your programming language of choice doesn't have a built-in way to get the week number, like JavaScript doesn't, this website may have you covered.

Testing

Make sure to test multiple different input dates, I thought I was finished with my display until I tried some other dates and noticed that there were still some bugs left to squash.

Starting

If you know what the first day in the calendar should be, counting up is as easy as "one two three"!

Weeks

If you use 6 weeks in the display, you will always have enough space to fit all the current month's days and the minimum 1 day of the adjacent month's too.


Showcase

If at all possible and with at least a few entries I will try to run all the submissions myself and create a little showcase website for it.

12 comments

  1. [3]
    wirelyre
    Link
    Fun little challenge. I got a solution in BQN — it should work for any POSIX date. ⟨ 2023 6 4 ⟩ 1 2 3 4 5 6 │ 1 2 3 │ 1 7 8 9 10 11 12 13 │ 4 5 6 7 8 9 10 │ 2 3 4 5 6 7 8 14 15 16 17 18 19 20 │ 11...

    Fun little challenge. I got a solution in BQN — it should work for any POSIX date.

                                    ⟨ 2023 6 4 ⟩
           1   2   3   4   5   6  │                   1   2   3  │                           1
       7   8   9  10  11  12  13  │   4   5   6   7   8   9  10  │   2   3   4   5   6   7   8
      14  15  16  17  18  19  20  │  11  12  13  14  15  16  17  │   9  10  11  12  13  14  15
      21  22  23  24  25  26  27  │  18  19  20  21  22  23  24  │  16  17  18  19  20  21  22
      28  29  30  31              │  25  26  27  28  29  30      │  23  24  25  26  27  28  29
                                  │                              │  30  31                    
    
    Full source code
    # Print a calendar of this month and the two surrounding months.
    
    
    # current date and weekday
    y‿m‿d‿wd ← •BQN 1⊑ •SH ⟨"date", "+%Y‿%m‿%d‿%w"⟩
    m‿d -↩ 1
    
    # number of days in a month
    Leap ← ≠´0=4‿100‿400|¨⊣
    Days ← ⊢◶31‿(28+Leap)‿31‿30‿31‿30‿31‿31‿30‿31‿30‿31
    
    # number of days and starting weekday of these three months
    Offset ← {y‿m𝕊o: x←o+m+12×y ⋄ ⟨⌊x÷12,12|x⟩}
    ⟨prev,cur,·⟩ ← counts ← Days´¨ y‿m⊸Offset¨ ¯1‿0‿1
    starts ← 7 | ⟨wd-d+prev, wd-d, cur+wd-d⟩
    
    # format a single month as rank-2 character array
    S ← ⥊⟜⟨"    "⟩
    Chart ← { ∾˘ 6‿7⥊ (S𝕨)∾((¯4↑•Fmt)¨1+↕𝕩)∾(S 42) }
    
    
    # output
    •Out (32⥊" ")∾•Fmt⟨y,m+1,d+1⟩
    •Out˘ (∾⟜"  │"⊸∾)˘˝> starts Chart¨ counts
    

    Tildes's font has surprisingly nice BQN symbols!

    This was a good exercise for boiling down the problem to its essential parts. I decided those should be:

    • Print today's date
    • Print three calendar months

    In order to print one month you need to know how many days it has and which day of the week it starts on. I think the Days function (number of days in a month) is really beautiful. Take the index of the month into a list. If it's February, figure out whether it's a Leap year (modulus 4 and 100 and 400, xor together) and add 28 to the boolean. One weird note: xor is written , so "xor of a boolean list" is written ≠´.

    Then I figured out how to print a single month. This is string formatting and it's ugly. But the interface 2 Chart 29 for printing a 29-day month starting on Tuesday (2s-day) is nice. You can then splice those together. Each chart is just a list of strings for each day, padded on the front by the starting weekday, padded on the back by loads of spaces, cut into a 6×7 grid, and merged into an array of lines.

    Then it's just a matter of finding the number of days in each month and the weekday they start on. Boring math but we got Offset out of it: (2023‿6 Offset 3) ≡ 2023‿9, forward 3 months; (2023‿6 Offset ¯8) ≡ 2022‿10, backward 8 months. Finally, a little modular math to find the weekdays (using 0-based month and day indexes).

    I decided to print the date in native BQN array format, ⟨surrounded by angle brackets⟩, as a nod to the language! I think it looks cute.

    5 votes
    1. [2]
      Bauke
      Link Parent
      Nicely done! I've always wondered what the development workflow looks like with this kind of programming language that uses all these different symbols. Half the characters used I didn't even know...

      Nicely done! I've always wondered what the development workflow looks like with this kind of programming language that uses all these different symbols. Half the characters used I didn't even know existed let alone how I would go about typing them. Do you use some kind of special keyboard layout to get access to those? Or are you copying and pasting them manually? Or something else?

      2 votes
      1. wirelyre
        Link Parent
        I installed a keyboard layout! It's just QWERTY but with a backslash dead key. So I type \r for ↑ and 1\ 2 for 1‿2. It sounds hard but the symbols are good mnemonics for the functions and the keys...

        I installed a keyboard layout! It's just QWERTY but with a backslash dead key. So I type \r for and 1\ 2 for 1‿2. It sounds hard but the symbols are good mnemonics for the functions and the keys are good mnemonics for the symbols. \z\Z makes ⥊⋈ for example, which both look kind of like z. And hjkl ist the dot zone, so \h\j\k\K\l makes ⊸∘○⌾⟜. (Neither of those is meaningful code.)

        Right now I do most of the drafting in the online interpreter because my terminal editor feels weird for it. Over there you can see all the primitives at the top, click them to insert, hover to see the key binding, and shift/control/command-click them to see the documentation.

        Yesterday I was brainstorming and was like "hmm, what's the symbol for this?" So I fumbled around the keyboard trying to find it, and eventually I gave up and looked at the top bar. I was looking for a regular ASCII slash.

        Honestly I'm no expert and the biggest challenge is usually figuring out what not to care about, finding the right place of abstraction. For example I needed to pad the end of each month with spaces, otherwise it would cycle back to 1. How many spaces do you need? Well, actually it doesn't matter, just pad with 42 cells and chop it back to size.

        It feels like you should be able to extract 7 and make it more general. But, like, who on Earth is going to change the length of the week? (And there's only two 7s in the code anyway.)

        And even if it does matter and you do need the generalization — well, it's only 11 lines so you can just go in there and do whatever you want.

        3 votes
  2. [2]
    TangibleLight
    (edited )
    Link
    Python actually includes calendar formatting in the standard library, producing output like: June 2023 Mon Tue Wed Thu Fri Sat Sun 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25...

    Python actually includes calendar formatting in the standard library, producing output like:

             June 2023
    Mon Tue Wed Thu Fri Sat Sun
                  1   2   3   4
      5   6   7   8   9  10  11
     12  13  14  15  16  17  18
     19  20  21  22  23  24  25
     26  27  28  29  30
    

    I think it'd be fun to try to complete this challenge without performing any date/time arithmetic, so I'll instead use regex substitutions and string manipulation to decorate that output with ANSI escape sequences to satisfy the challenge rules.

    • Underline the day of week
    • Bold the current week
    • Highlight the current date

    https://i.imgur.com/gjr02vM.png

    I do need a little date/time arithmetic to find the preceeding or following month; for example a date in January should include December from the previous year and vice versa. I do this via datetime.timedelta to at least hide that arithmetic even though it's still there.

    https://i.imgur.com/3SIET7v.png

    For a more compact view I can insert only the last week of the previous month and the first week of the next month by string manipulation.

    https://i.imgur.com/bL3amTZ.png

    I want to merge those lines into the current month, but I need to do this conditionally in case the month starts on the first day of the week, or ends on the last day of the week. I could determine this by date arithmetic... but I will do it via string manipulation instead. The ANSI sequences present in the content make this tricky, but I can check line.startswith(' ') to tell if a line should be merged into the previous.

    With all that we have the last feature

    • Include at least one day from the adjacent months
    • Dim dates of adjacent months

    import calendar
    import datetime
    import re
    import sys
    
    
    def _ansi(s, e):
        return lambda string: f"\033[{s}m{string}\033[{e}m"
    
    
    _bold = _ansi(1, 0)
    _dim = _ansi(2, 22)
    _uline = _ansi(4, 24)
    _inv = _ansi(7, 27)
    
    if len(sys.argv) > 1:
        date = datetime.datetime.strptime(sys.argv[1], "%Y-%m-%d").date()
    else:
        date = datetime.date.today()
    
    curr = calendar.month(date.year, date.month, w=3)
    
    # underline the current day in the header
    day = calendar.day_abbr[date.weekday()]
    curr = re.sub(rf"\b{day}\b", _uline(r"\g<0>"), curr)
    
    # bold the current week
    # be careful to place the bold tag _after_ any spaces at the start of the line.
    # this makes it easier to do line merging later on.
    curr = re.sub(rf'(?m)[^ \n].*?\b{date.day}\b.*$', _bold(r'\g<0>'), curr)
    
    # highlight the current date
    curr = re.sub(rf" ?\b{date.day}\b ?", _inv(r"\g<0>"), curr)
    
    lines = curr.splitlines()
    
    # merge last week of previous month
    prev_month = date.replace(day=1) - datetime.timedelta(days=1)
    prev_week = calendar.month(prev_month.year, prev_month.month, w=3).splitlines()[-1]
    if lines[2].startswith("   "):
        # prev_week has no ansi codes yet. no problems.
        lines[2] = _dim(prev_week) + lines[2][len(prev_week) :]
    else:
        lines.insert(2, _dim(prev_week))
    
    # merge first week of next month
    next_month = date.replace(day=1) + datetime.timedelta(days=32)
    next_week = calendar.month(next_month.year, next_month.month, w=3).splitlines()[2]
    if next_week.startswith("   "):
        # lines[-1] already has ansi codes. be careful.
        part = '   ' + next_week.lstrip(' ')
        lines[-1] += _dim(part)
    else:
        lines.append(_dim(next_week))
    
    print("\n".join(lines))
    

    Today's date: https://i.imgur.com/1ZhYPhe.png

    Some edge cases: https://i.imgur.com/EGiJJDm.png

    5 votes
    1. Bauke
      Link Parent
      The Python Standard Library never ceases to amaze, nice find!

      The Python Standard Library never ceases to amaze, nice find!

      2 votes
  3. [3]
    lily
    (edited )
    Link
    I wrote a terminal program, like some of the others here. I started this a couple days ago but only got around to finishing it now. It's in C and uses ANSI escapes for formatting. The logic is a...

    I wrote a terminal program, like some of the others here. I started this a couple days ago but only got around to finishing it now. It's in C and uses ANSI escapes for formatting. The logic is a little hacky, but it works and that's what counts, I guess!

    calendar.c
    /* calendar.c
     * by Lily
     *
     * A small calendar display for the current month.
     *
     * For the "Mini Calendar Display" challenge on Tildes:
     * https://tildes.net/~comp/15l9/programming_challenge_mini_calendar_display
     */
    
    #include <stdbool.h>
    #include <stdio.h>
    #include <time.h>
    
    int main(void) {
        // get current date
        // (only the year needs extra calculation)
        time_t t = time(NULL);
        struct tm tm = *localtime(&t);
        int year = tm.tm_year + 1900;
        
        // display date
        printf("\x1b[1;4m%d-%02d-%02d                 \x1b[24m\n", year, tm.tm_mon,
               tm.tm_mday);
    
        // print weekdays, highlighting the current one
        for (int weekday = 0; weekday < 7; ++weekday) {
            char *name;
            switch (weekday) {
                case 0: name = "Sun"; break;
                case 1: name = "Mon"; break;
                case 2: name = "Tue"; break;
                case 3: name = "Wed"; break;
                case 4: name = "Thu"; break;
                case 5: name = "Fri"; break;
                case 6: name = "Sat"; break;
            }
    
            if (weekday == tm.tm_wday) {
                printf("\x1b[7m%s\x1b[27m ", name);
            } else {
                printf("%s ", name);
            }
        }
        puts("\x1b[0m");
    
        // hardcoding seems like the best option here
        int days_per_month[12] = {
            31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
        };
    
        // account for leap years
        if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
            days_per_month[1] = 29;
        }
    
        // precalculate adjacent months
        int previous_month = (tm.tm_mon - 1) % 12;
        int next_month = (tm.tm_mon + 1) % 12;
    
        // start on the last Monday of the previous month
        int month = previous_month;
        int weekday = 0;
        
        // find what that Monday is (very hacky)
        int day = days_per_month[month]
                  - (7 + (tm.tm_wday - (tm.tm_mday - 1)) % 7 - 1);
    
        // print days until we reach the first Friday in next month
        // days not in the current month are dimmed
        // today is highlighted
        fputs("\x1b[2m", stdout);
        while (month != next_month || weekday != 0) {
            if (day == tm.tm_mday && month == tm.tm_mon) {
                printf("\x1b[7m%3d\x1b[27m ", day);
            } else {
                printf("%3d ", day);
            }
            
            // move to next day, and month if needed
            ++day;
            if (day > days_per_month[month]) {
                ++month;
                day = 1;
    
                // set correct formatting for the new month
                if (month == tm.tm_mon) {
                    fputs("\x1b[0m", stdout);
                } else {
                    fputs("\x1b[2m", stdout);
                }
            }
    
            // move to next weekday
            ++weekday;
            if (weekday > 6) {
                weekday = 0;
                putchar('\n');
            }
        }
    
        // disable all formatting as to not mess up the shell
        fputs("\x1b[0m", stdout);
    }
    
    3 votes
    1. [2]
      Bauke
      Link Parent
      Good work! When I compiled with gcc I did get a warning about something but the calendar still outputs correctly so I don't know, I'm not familiar enough with C to tell. :P The warning if you're...

      Good work! When I compiled with gcc I did get a warning about something but the calendar still outputs correctly so I don't know, I'm not familiar enough with C to tell. :P

      The warning if you're interested
      lily-calendar.c: In function ‘main’:
      lily-calendar.c:52:5: warning: ‘and’ of mutually exclusive equal-tests is always 0
         52 |     if ((year % 4 == 0 && year & 100 != 0) || year % 400 == 0) {
            |     ^~
      
      2 votes
      1. lily
        (edited )
        Link Parent
        Looks like I made a typo, oops! year & 100 should be year % 100. It only affects the leap year test, though, so I guess that's why it still works right now. I'll go ahead and edit the original...

        Looks like I made a typo, oops! year & 100 should be year % 100. It only affects the leap year test, though, so I guess that's why it still works right now.

        I'll go ahead and edit the original post to fix that. I didn't notice it because I didn't have all the warnings on.

        2 votes
  4. [4]
    talklittle
    Link
    Cool idea. Your calendar turned out quite nicely. I happen to be seeking a mini project to try out Svelte, so think I'll try an entry.

    Cool idea. Your calendar turned out quite nicely.

    I happen to be seeking a mini project to try out Svelte, so think I'll try an entry.

    2 votes
    1. [3]
      talklittle
      Link Parent
      I spent some hours and whipped up something: https://github.com/talklittle/mini-calendar-2023 Screenshot on the GitHub page. No polish and I didn't do all the extras like day-of-week headings and...

      I spent some hours and whipped up something: https://github.com/talklittle/mini-calendar-2023

      Screenshot on the GitHub page. No polish and I didn't do all the extras like day-of-week headings and week numbers, nor a date picker.

      Learning to setup a Svelte project
      Stumbling blocks
      • Being new to Svelte and TypeScript slowed me down; had to keep referring to the Svelte tutorial and TypeScript cheatsheets.
      • Took some time to figure out how to define/export the Date prototype functions when the code is encapsulated in a module. Found the answer for TypeScript and for JavaScript.
      • Took me a while to figure out how to export a TypeScript class defined in a script tag in a .svelte file. Answer: I didn't; I moved the class to a new .ts file instead.

      So after these learnings I was satisfied and decided to leave it at that. Thanks for the challenge idea @Bauke!

      3 votes
      1. [2]
        Bauke
        (edited )
        Link Parent
        Great job! The way you got there is pretty much the same way I did it: Spoilies Set the start as the first day of the week (for the first column), calculate back to the previous month (for the...

        Great job! The way you got there is pretty much the same way I did it:

        Spoilies

        Set the start as the first day of the week (for the first column), calculate back to the previous month (for the first row), and finally loop de loop up 6 * 7 days.

        Thanks for participating!

        2 votes
        1. talklittle
          Link Parent
          Oh yeah, I used your hints right away for a head start. Even so, it did take a while of staring at the screen and a couple false starts on the core logic until I got a straightforward algorithm in...

          Oh yeah, I used your hints right away for a head start. Even so, it did take a while of staring at the screen and a couple false starts on the core logic until I got a straightforward algorithm in place.

          2 votes