Given a list of activities and their start time/date, output an ASCII-art calendar showing the activities on the appropriate days. All activities are guaranteed to be in the same month, no two activities will be on the same day, and all activities are guaranteed to fit within the calendar box.

The calendar has the date in the upper-left corner of each box, the boxes are 9 spaces wide by 5 spaces tall, surrounded by - and |. The two-letter abbreviation for the day of the week is centered above the first row, and the weeks start with Sunday.

For example, given the following activities:

10/5/2018 - 9:00am - Sandbox calendar challenge
10/9/2018 - 9:00am - Post challenge to main
10/10/2018 - 10:00am - Profit
10/31/2018 - 7:30pm - Halloween party

Output this corresponding calendar:

    Su        Mo        Tu        We        Th        Fr        Sa     
-----------------------------------------------------------------------
|         |1        |2        |3        |4        |5        |6        |
|         |         |         |         |         |9:00am   |         |
|         |         |         |         |         |Sandbox  |         |
|         |         |         |         |         |calendar |         |
|         |         |         |         |         |challenge|         |
-----------------------------------------------------------------------
|7        |8        |9        |10       |11       |12       |13       |
|         |         |9:00am   |10:00am  |         |         |         |
|         |         |Post     |Profit   |         |         |         |
|         |         |challenge|         |         |         |         |
|         |         |to main  |         |         |         |         |
-----------------------------------------------------------------------
|14       |15       |16       |17       |18       |19       |20       |
|         |         |         |         |         |         |         |
|         |         |         |         |         |         |         |
|         |         |         |         |         |         |         |
|         |         |         |         |         |         |         |
-----------------------------------------------------------------------
|21       |22       |23       |24       |25       |26       |27       |
|         |         |         |         |         |         |         |
|         |         |         |         |         |         |         |
|         |         |         |         |         |         |         |
|         |         |         |         |         |         |         |
-----------------------------------------------------------------------
|28       |29       |30       |31       |         |         |         |
|         |         |         |7:30pm   |         |         |         |
|         |         |         |Halloween|         |         |         |
|         |         |         |party    |         |         |         |
|         |         |         |         |         |         |         |
-----------------------------------------------------------------------

Clarifications

  • The schedule words (matching [A-Za-z]+) will delimited by a single space between them (as in the example).
  • Splitting the text on word boundaries is sufficient. No need for hyphenating words.
  • If February starts on a Sunday in a non-leap-year, you only will have four calendar rows.
  • If a 31-day month (e.g., August) starts late in the week, you may have to output six calendar rows.

I/O and Rules

  • Your code must handle dates at least between 0001-01-01 and 9999-12-31 in the Gregorian calendar, including leap years as appropriate. For example, if given input 2016-02-13 9:00am Test, the output calendar should have February 29.
  • Input date format can be in any desired format. ISO 8601, a datetime object, a particularly-formatted string, etc. Input parsing is not the interesting part of this challenge.
  • Input and output can be by any convenient method.
  • Leading/trailing newlines or other whitespace are optional, provided that the characters line up appropriately.
  • Either a full program or a function are acceptable. If a function, you can return the output rather than printing it.
  • Output can be to the console, returned as a list of strings, returned as a single string, etc.
  • Standard loopholes are forbidden.
  • This is so all usual golfing rules apply, and the shortest code (in bytes) wins.
  • 1.) Do you have to split the activity names on the word boundaries? 2.) When you have a non-leapyear February starting on a Sunday, do you have only 4 rows? 3.) When you would need 6 rows to show the month (ex. August starting on Saturday) what happens? – nedla2004 Oct 10 at 14:48
  • Related (easier). – Arnauld Oct 10 at 14:50
  • @nedla2004 1) Yes, word boundaries will work fine. 2) That is correct, 4 rows. 3) Your calendar will need to show 6 rows. I'll edit in clarifications. – AdmBorkBork Oct 10 at 14:57
  • @Arnauld Yes, that's a fair assumption – AdmBorkBork Oct 10 at 15:08
  • 1
    1752-09-02 - 09:00am - Wife's Birthday Tomorrow (14th!) – ngm Oct 10 at 15:48

JavaScript (ES8), 380321 320 bytes

Takes input as (y,m,e) where:

  • y is the year
  • m is the month, 0-indexed
  • e is an object whose keys are the days and whose values are the events in [hour, task] format
(y,m,e)=>`SuMoTuWeThFrSa
`.split(/(..)/).join`    `+(p='-'.repeat(d=71)+`
`)+(g=i=>++d<1|(x=G`getMonth`==m)|i%7?`|${[h,t]=e[d]||E,o=[x*d,h],q=E,t&&t.split` `.map(s=>q=(u=q?q+' '+s:s)[9]?o.push(q)&&s:u),[...o,q][r%5]||E}`.padEnd(10)+(++i%7?E:`|
`+(++r%5?(d-=7,E):p))+g(i):E)(E=r='',d=-(G=s=>new Date(y,m,d)[s]())`getDay`)

Try it online!

How?

Below are some important parts in the code.

Header

The header line is generated with:

'SuMoTuWeThFrSa\n'.split(/(..)/).join`    `

When split() is used with a regular expression containing a capturing group, this group is included in the output array. In this case, it gives:

[ '', 'Su', '', 'Mo', '', 'Tu', '', 'We', '', 'Th', '', 'Fr', '', 'Sa', '\n' ]

We join this array with 4 spaces, leading to:

'    Su        Mo        Tu        We        Th        Fr        Sa    \n'

which is exactly what we want.

Month structure

The helper function \$G\$ builds a date from the input year \$y\$ and input month \$m\$ (which are constant) and the day \$d\$ (which is dynamic). Its parameter is a method name, which allows to extract either the day of the week or the month.

G = s => new Date(y, m, d)[s]()

It is first used to identify the day of the week of the first day of the month, so that we can move back to the last Sunday from there. It is then used to detect when we enter back the correct month and when we leave it (this information is stored in the Boolean \$x\$).

Event formatting

The following code is used to format an event.

[h, t] = e[d] || E,           // split the content of the event into h and t
o = [x * d, h],               // set the first 2 entries of o[]: day and hour
q = E,                        // we start with q = empty string
t &&                          // skip this .map() loop entirely if t is not defined
t.split` `                    // split the string on spaces
.map(s =>                     // for each word s:
  q =                         //   update q:
    (u = q ? q + ' ' + s : s) //     u is the concatenation of the previous string with
                              //     the new word, separated by a space if needed
    [9] ?                     //     if u is more than 9 character long:
      o.push(q)               //       push the previous string in o[]
      && s                    //       and reset q to s
    :                         //     else:
      u                       //       update q to u
),                            // end of map()
[...o, q][r % 5]              // append the last pending part to o[] and extract the
|| E                          // relevant row; or use an empty string by default

Python 2, 326 324 315 312 307 bytes

import calendar as c,textwrap as t
c.setfirstweekday(6)
y,m,e=input()
print' Su Mo Tu We Th Fr Sa\n'.replace(' ',' '*8)[4:]+'-'*71
for w in c.monthcalendar(y,m):
 for l in zip(*[[d or'',a]+(t.wrap(b,9)+['']*3)[:3]for d in w for a,b in[e.get(d,'  ')]]):print'|'+'|'.join('%-9s'%v for v in l)+'|'
 print'-'*71

Try it online!

Same input as Arnauld's JS answer:

Takes input as (y,m,e) where:

  • y is the year
  • m is the month, 1-indexed
  • e is an object whose keys are the days and whose values are the events in (hour, task) format

Charcoal, 215 206 bytes

Sθ≔I⪪§⪪θ ⁰/η≔⁻⁺×¹²⊟η⊟η²η≔EE²⁻ηι﹪Σ⟦÷ι⁴⁸⁰⁰±÷ι¹²⁰⁰÷ι⁴⁸÷ι¹²÷×¹³⁺⁴﹪ι¹²¦⁵⟧⁷η≔±⊟ηζ≔⁺²⁸﹪⁺⊟ηζ⁷ε⭆⪪SuMoTuWeThFrSa²◨◧ι⁶χ↓←⁷¹W‹ζε«↘F⁷«P↓⁵→≦⊕ζF⁼Iζ§⪪θ/⁰«≔⪪θ - θ≔⟦ω◨§θ¹¦⁹⟧δF⪪⊟θ ⊞δ⎇‹⁺L§δ±¹Lμ⁹⁺⁺⊟δ μμP⪫δ¶Sθ»◨×››ζ⁰›ζεIζ⁹»↓⁵←⁷¹

Try it online! Link is to verbose version of code. Takes dates in d/m/yyyy format. Explanation:

Sθ

Input the first event.

≔I⪪§⪪θ ⁰/η

Extract the date and split on /s.

≔⁻⁺×¹²⊟η⊟η²η

Convert to months since March, 1 BC. I want to calculate the day of week of the first of both next month and the current month, and working in months is easier than keeping the months and years separate and carrying at the end of the year, plus it also allows me to start counting months beginning at March instead of January, which is required by Zeller's congruence.

≔EE²⁻ηι﹪Σ⟦÷ι⁴⁸⁰⁰±÷ι¹²⁰⁰÷ι⁴⁸÷ι¹²÷×¹³⁺⁴﹪ι¹²¦⁵⟧⁷η

Use a modified Zeller's congruence to extract the day of the week of the first day of next month and this month. The basic part relies on the fact that the number of days from October 30th of the previous year to the 1st of a given month where m = 4 for March and m = 14 for January of the following year is given by the formula m * 153 / 5, however we can subtract 140 because we only care about the day of the week. It then remains to make adjustments due to the year; each year adds a day, each 4th year adds an extra day, each 100th year subtracts a day, and each 400th year adds a day again. (As I'm working in months these values are all multiplied by 12.) Rather conveniently this directly gives me the answer in terms of a Sunday-indexed week (normally you would add the day of month and start counting on Saturday).

≔±⊟ηζ

Negate the day of the week and save it as the current day of the month.

≔⁺²⁸﹪⁺⊟ηζ⁷ε

Calculate the number of days in the month from the day of the week of the two months.

⭆⪪SuMoTuWeThFrSa²◨◧ι⁶χ

Output the day headers.

↓←⁷¹

Print the top row of -s.

W‹ζε«

Loop until the last day of the month has been output.

Move the cursor to the start of the next row.

F⁷«

Process 7 days at a time.

P↓⁵→

Print the column of |s to the left.

≦⊕ζ

Increment the current day of the month.

F⁼Iζ§⪪θ/⁰«

If the current day of the month is the day of the current event, ...

≔⪪θ - θ

... extract the other parts of the event, ...

≔⟦ω◨§θ¹¦⁹⟧δ

... pad the time to 9 spaces and save it and an empty string as a list, ...

F⪪⊟θ 

... split the description on spaces and loop over them, ...

⊞δ⎇‹⁺L§δ±¹Lμ⁹⁺⁺⊟δ μμ

... adding each word to the previous word if it will fit; ...

P⪫δ¶

... output the time and description (Pδ doesn't work, might be a Charcoal bug?), ...

Sθ»

... and input the next event.

◨×››ζ⁰›ζεIζ⁹»

If the current day of the month is between 1 and the last day of the month then output it, otherwise just output enough spaces to move to the next day.

↓⁵←⁷¹

At the end of the week, print the right column of |s and the bottom row of -s.

  • Maybe I skipped over it in your verbose TIO code, but are you sure your Zeller's congruence implementation is complete? It seems to be correct for the months March through December, but for the months January and February year-1 should be used instead of year and month+12 should be used instead of month. Or did you somehow simplify the algorithm that I mentioned in this 05AB1E answer of mine which is equal to the one from Wikipedia? – Kevin Cruijssen Oct 12 at 12:39
  • @KevinCruijssen This is basically why I calculate the number of months since March, 1BC, but it's too complicated to explain further in a comment. – Neil Oct 12 at 13:04
  • 1
    @KevinCruijssen I've updated my explanation; I hope you find it helpful. – Neil Oct 13 at 10:36
  • Thanks! That's indeed a nice modified formula, and I now understand the reasoning behind it. Thanks a lot for adding it to the explanation. +1 from me. – Kevin Cruijssen Oct 13 at 15:25

Java (JDK), 538 439 428 425 bytes

Quite possibly the longest Code Golf solution I've ever posted. Still trying to golf it down from here but it's a struggle.

Managed to knock off 99 bytes by changing the input format and using some regex parsing, and another 11 from miscellaneous bits.

3 extra bytes off thanks to Kevin!

Taking inspiration from other answers, it takes input as the year, month and a Map of days to a String representing the time and event in the format <time>-<event>.

(y,m,M)->{var C=java.util.Calendar.getInstance();C.set(y,m-1,1);String r=",Su,,Mo,,Tu,,We,,Th,,Fr,,Sa\n".replace(",","    "),e;for(int x=C.getActualMaximum(5),l=0,b=0,j,c,i=0;i<7;r+="\n",l+=b<=x&++i>6?7*(i=1):0)for(j=0;j<71;b=l+j/10+2-C.get(7),e=(e=M.get(b))!=null?e.replaceAll("([^-]{1,9})(-| |$)","$1-")+" - ":null,r+=e=i%6<1?"-":c<1?"|":c*i<2&b>0&b<=x?b+"":c<2&e!=null?e.split("-")[i-2]:" ",j+=e.length())c=j%10;return r;}

Try it online!


Ungolfed

(y,m,M)->{                                              // Lambda taking input as a year, month and map
  var C=java.util.Calendar.getInstance();               // Creates a new Calendar instance
  C.set(y,m-1,1);                                       // Sets the calendar to the first of the month in the given year    
  String r=",Su,,Mo,,Tu,,We,,Th,,Fr,,Sa\n"              // Creates the header row by replacing
    .replace(",","    "),e;                             // commas with 4 sets of spaces

  for(                                                  // Creates 7 rows for a calendar row
      int x=C.getActualMaximum(5)                       // Stores last day of the month
      ,l=0,b=0,j,c,i=0;i<7;                             // Initialises other integers
      r+="\n",                                          // Add new line each row
      l+=b<=x&++i>6                                     // If end of a calendar row is reached, and current day is less than max
        ?7*(i=1)                                        // Set i to 1 and add 7 to line count to create another calendar row
        :0)                                             // Otherwise do nothing

    for(j=0;j<71;                                       // Loop 71 times, appending characters to create a row
        b=l+j/10+2-C.get(7),                            // Determine the day of the box we're in
        e=(e=M.get(b))!=null?                           // Get the event for this day from the map and if not null
            e.replaceAll("([^-]{1,9})(-| |$)","$1-")      // Do some regex to separate the line entries by hyphens
            +" - "                                      // Append extra hyphen to prevent array out of bounds
            :null,                                      // Otherwise do nothing
        r+=e=i%6<1?"-":                                 // If it's the first line of a calendar row, append -
           c<1?"|":                                     // If it's the first column of a box, append |
           c*i<2&b>0&b<=x?b+"":                         // If it's the second column of a box, the second row, 
                                                        // and less than the max day, append the day
           c<2&e!=null?e.split("-")[i-2]:               // If it's any other row and there is an event then split and append correct line
           " ",                                         // Else just append a space
        j+=e.length())                                  // Increase the row character count by the length to append
          c=j%10;                                       // Set the column of box (this is the only thing in the loop so happens first)

  return r;                                             // return the calendar string!
}
  • &&(i=1)<2?7:0 can be ?7*(i=1):0 to save 3 bytes. – Kevin Cruijssen Oct 12 at 11:51
  • @KevinCruijssen Nice one thanks! – Luke Stevens Oct 12 at 13:33

Red, 674 651 bytes

func[a][z: func[a b c][to-tuple reduce[a b c]]c: a/1 c/4: 1 d: c + 31
d/4: 1 d: d - 1 e: 1 + c/weekday % 7 if e = 0[e: 7]foreach
t[:Su:Mo:Tu:We:Th:Fr:Sa][prin pad pad/left t 6 10]h:
pad/with"-"71 #"-"print["^/"h]m: copy[]foreach[i b]a[put m z r:(t: e - 1)+
i/4 / 7 + 1 n: i/4 % 7 + t 2 b/1 t: split b/2" "l: 0
until[if t/2[if 10 >((length? t/1)+ length? t/2)[t/1:
rejoin reduce[t/1" "take next t]]]put m z r n 2 + l: l + 1 take t
tail? t]i: 0]n: k: 0 repeat v(g: e - 1 + d/4)/ 7 + sign? g % 7[repeat
r 5[repeat i 7[t: copy"|"if i = e[k: 1]if all[k = 1 r = 1 n < d/4][append t
n: n + 1]if s: select m z v i r[append t s]prin pad t 10]print"|"]print h]]

Try it online!

More readable:

func [ a ] [
    c: d: a/1
    c/4: d/4: 1
    d: d + 31
    d/4: 1
    d: d - 1
    e: 1 + c/weekday % 7
    if e = 0[e: 7]
    g: e - 1 + d/4
    w: g / 7 + sign? g % 7
    foreach t [ :Su :Mo :Tu :We :Th :Fr :Sa ] [
        prin pad pad/left t 6 10
    ]
    h: pad/with "-" 71 #"-"
    print[ "^/" h ]
    m: copy #()
    foreach[ i b ] a [
        n: i/4 % 7 + t: e - 1
        r: t + i/4 / 7 + 1
        put m to-tuple reduce[ r n 2 ] b/1
        t: split b/2" "
        i: 0
        until [
            if t/2 [ if 9 >= ((length? t/1) + (length? t/2)) [
                t/1: rejoin reduce[t/1" "take next t]
                ]
            ]
            put m to-tuple reduce [ r n 2 + i: i + 1 ] take t
            tail? t
        ]

    ]
    n: 0
    g: off
    repeat v w [
        repeat r 5 [
           repeat i 7 [
                t: copy "|"
                if i = e[ g: on ]
                if all [ g r = 1 n < d/4 ] [ append t n: n + 1 ]
                if s: select m to-tuple reduce [ v i r ]
                    [ append t s ]
                prin pad t 10
            ]
            print "|"
        ]
        print h
    ]
]
  • if e = 0[e: 7] can be removed, right? You use e: 1 + c/weekday % 7, so e will always be in the range [1, 7]. – Kevin Cruijssen Oct 12 at 11:59
  • @KevinCruijssen: Maybe I'm missing something, but I think that I need it. Red indexing is 1-based. Please take a look at this: >> c: now/time/date == 12-Oct-2018 >> c: c + 1 == 13-Oct-2018 >> 1 + c/weekday % 7 == 0; >> 1 + 2 * 3 is 9 in Red, not 7 – Galen Ivanov Oct 12 at 12:09
  • 1
    EDIT: Ah, nvm.. The 1 + happens first.. Ok, I see my error. I'm used to % and / having operator precedence over +. – Kevin Cruijssen Oct 12 at 12:12
  • 1
    @KevinCruijssen Yes, exactly. There's no operator precedence in Red, one needs to use () instead – Galen Ivanov Oct 12 at 12:21

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.