Task

Provide an algorithm as per the Calendar task, except the entire code for the algorithm must be presented   entirely without lowercase.

Also - as per many 1969 era line printers - format the calendar to nicely fill a page that is 132 characters wide.

(Hint: manually convert the code from the Calendar task to all UPPERCASE)

This task also is inspired by Real Programmers Don't Use PASCAL by Ed Post, Datamation, volume 29 number 7, July 1983.

THE REAL PROGRAMMER'S NATURAL HABITAT
"Taped to the wall is a line-printer Snoopy calender for the year 1969."

Moreover this task is further inspired by the long lost corollary article titled:

"Real programmers think in UPPERCASE"!

Note: Whereas today we only need to worry about ASCII, UTF-8, UTF-16, UTF-32, UTF-7 and UTF-EBCDIC encodings, in the 1960s having code in UPPERCASE was often mandatory as characters were often stuffed into 36-bit words as 6 lots of 6-bit characters. More extreme words sizes include 60-bit words of the CDC 6000 series computers. The Soviets even had a national character set that was inclusive of all
4-bit,
5-bit,
6-bit &
7-bit depending on how the file was opened... And one rogue Soviet university went further and built a 1.5-bit based computer.

Of course... as us Boomers have turned into Geezers we have become HARD OF HEARING,
and suffer from chronic Presbyopia, hence programming in UPPERCASE
is less to do with computer architecture and more to do with practically. :-)

For economy of size, do not actually include Snoopy generation
in either the code or the output, instead just output a place-holder.

FYI: a nice ASCII art file of Snoopy can be found at textfiles.com. Save with a .txt extension.

Trivia: The terms uppercase and lowercase date back to the early days of the mechanical printing press. Individual metal alloy casts of each needed letter, or punctuation symbol, were meticulously added to a press block, by hand, before rolling out copies of a page. These metal casts were stored and organized in wooden cases. The more often needed minuscule letters were placed closer to hand, in the lower cases of the work bench. The less often needed, capitalized, majuscule letters, ended up in the harder to reach upper cases.




OPTION COMPARE BINARY
OPTION EXPLICIT ON
OPTION INFER ON
OPTION STRICT ON
 
IMPORTS SYSTEM.GLOBALIZATION
IMPORTS SYSTEM.TEXT
IMPORTS SYSTEM.RUNTIME.INTEROPSERVICES
IMPORTS SYSTEM.RUNTIME.COMPILERSERVICES
 
MODULE ARGHELPER
    READONLY _ARGDICT AS NEW DICTIONARY(OF STRING, STRING)()
 
    DELEGATE FUNCTION TRYPARSE(OF T, TRESULT)(VALUE AS T, <OUT> BYREF RESULT AS TRESULT) AS BOOLEAN
 
    SUB INITIALIZEARGUMENTS(ARGS AS STRING())
        FOR EACH ITEM IN ARGS
            ITEM = ITEM.TOUPPERINVARIANT()
 
            IF ITEM.LENGTH > 0 ANDALSO ITEM(0) <> """"C THEN
                DIM COLONPOS = ITEM.INDEXOF(":"C, STRINGCOMPARISON.ORDINAL)
 
                IF COLONPOS <> -1 THEN
                    ' SPLIT ARGUMENTS WITH COLUMNS INTO KEY(PART BEFORE COLON) / VALUE(PART AFTER COLON) PAIRS.
                    _ARGDICT.ADD(ITEM.SUBSTRING(0, COLONPOS), ITEM.SUBSTRING(COLONPOS + 1, ITEM.LENGTH - COLONPOS - 1))
                END IF
            END IF
        NEXT
    END SUB
 
    SUB FROMARGUMENT(OF T)(
            KEY AS STRING,
            <OUT> BYREF VAR AS T,
            GETDEFAULT AS FUNC(OF T),
            TRYPARSE AS TRYPARSE(OF STRING, T),
            OPTIONAL VALIDATE AS PREDICATE(OF T) = NOTHING)
 
        DIM VALUE AS STRING = NOTHING
        IF _ARGDICT.TRYGETVALUE(KEY.TOUPPERINVARIANT(), VALUE) THEN
            IF NOT (TRYPARSE(VALUE, VAR) ANDALSO (VALIDATE IS NOTHING ORELSE VALIDATE(VAR))) THEN
                CONSOLE.WRITELINE($"INVALID VALUE FOR {KEY}: {VALUE}")
                ENVIRONMENT.EXIT(-1)
            END IF
        ELSE
            VAR = GETDEFAULT()
        END IF
    END SUB
END MODULE
 
MODULE PROGRAM
    SUB MAIN(ARGS AS STRING())
        DIM DT AS DATE
        DIM COLUMNS, ROWS, MONTHSPERROW AS INTEGER
        DIM VERTSTRETCH, HORIZSTRETCH, RESIZEWINDOW AS BOOLEAN
 
        INITIALIZEARGUMENTS(ARGS)
        FROMARGUMENT("DATE", DT, FUNCTION() NEW DATE(1969, 1, 1), ADDRESSOF DATE.TRYPARSE)
        FROMARGUMENT("COLS", COLUMNS, FUNCTION() 80, ADDRESSOF INTEGER.TRYPARSE, FUNCTION(V) V >= 20)
        FROMARGUMENT("ROWS", ROWS, FUNCTION() 43, ADDRESSOF INTEGER.TRYPARSE, FUNCTION(V) V >= 0)
        FROMARGUMENT("MS/ROW", MONTHSPERROW, FUNCTION() 0, ADDRESSOF INTEGER.TRYPARSE, FUNCTION(V) V <= 12 ANDALSO V <= COLUMNS \ 20)
        FROMARGUMENT("VSTRETCH", VERTSTRETCH, FUNCTION() TRUE, ADDRESSOF BOOLEAN.TRYPARSE)
        FROMARGUMENT("HSTRETCH", HORIZSTRETCH, FUNCTION() TRUE, ADDRESSOF BOOLEAN.TRYPARSE)
        FROMARGUMENT("WSIZE", RESIZEWINDOW, FUNCTION() TRUE, ADDRESSOF BOOLEAN.TRYPARSE)
 
        ' THE SCROLL BAR IN COMMAND PROMPT SEEMS TO TAKE UP PART OF THE LAST COLUMN.
        IF RESIZEWINDOW THEN
            CONSOLE.WINDOWWIDTH = COLUMNS + 1
            CONSOLE.WINDOWHEIGHT = ROWS
        END IF
 
        IF MONTHSPERROW < 1 THEN MONTHSPERROW = MATH.MAX(COLUMNS \ 22, 1)
 
        FOR EACH ROW IN GETCALENDARROWS(DT:=DT, WIDTH:=COLUMNS, HEIGHT:=ROWS, MONTHSPERROW:=MONTHSPERROW, VERTSTRETCH:=VERTSTRETCH, HORIZSTRETCH:=HORIZSTRETCH)
            CONSOLE.WRITE(ROW)
        NEXT
    END SUB
 
    ITERATOR FUNCTION GETCALENDARROWS(
            DT AS DATE,
            WIDTH AS INTEGER,
            HEIGHT AS INTEGER,
            MONTHSPERROW AS INTEGER,
            VERTSTRETCH AS BOOLEAN,
            HORIZSTRETCH AS BOOLEAN) AS IENUMERABLE(OF STRING)
 
        DIM YEAR = DT.YEAR
        DIM CALENDARROWCOUNT AS INTEGER = CINT(MATH.CEILING(12 / MONTHSPERROW))
        ' MAKE ROOM FOR THE THREE EMPTY LINES ON TOP.
        DIM MONTHGRIDHEIGHT AS INTEGER = HEIGHT - 3
 
        YIELD "[SNOOPY]".PADCENTER(WIDTH) & ENVIRONMENT.NEWLINE
        YIELD YEAR.TOSTRING(CULTUREINFO.INVARIANTCULTURE).PADCENTER(WIDTH) & ENVIRONMENT.NEWLINE
        YIELD ENVIRONMENT.NEWLINE
 
        DIM MONTH = 0
        DO WHILE MONTH < 12
            DIM ROWHIGHESTMONTH = MATH.MIN(MONTH + MONTHSPERROW, 12)
 
            DIM CELLWIDTH = WIDTH \ MONTHSPERROW
            DIM CELLCONTENTWIDTH = IF(MONTHSPERROW = 1, CELLWIDTH, (CELLWIDTH * 19) \ 20)
 
            DIM CELLHEIGHT = MONTHGRIDHEIGHT \ CALENDARROWCOUNT
            DIM CELLCONTENTHEIGHT = (CELLHEIGHT * 19) \ 20
 
            ' CREATES A MONTH CELL FOR THE SPECIFIED MONTH (1-12).
            DIM GETMONTHFROM =
                FUNCTION(M AS INTEGER) BUILDMONTH(
                    DT:=NEW DATE(DT.YEAR, M, 1),
                    WIDTH:=CELLCONTENTWIDTH,
                    HEIGHT:=CELLCONTENTHEIGHT,
                    VERTSTRETCH:=VERTSTRETCH,
                    HORIZSTRETCH:=HORIZSTRETCH).SELECT(FUNCTION(X) X.PADCENTER(CELLWIDTH))
 
            ' THE MONTHS IN THIS ROW OF THE CALENDAR.
            DIM MONTHSTHISROW AS IENUMERABLE(OF IENUMERABLE(OF STRING)) =
                ENUMERABLE.SELECT(ENUMERABLE.RANGE(MONTH + 1, ROWHIGHESTMONTH - MONTH), GETMONTHFROM)
 
            DIM CALENDARROW AS IENUMERABLE(OF STRING) =
                INTERLEAVED(
                    MONTHSTHISROW,
                    USEINNERSEPARATOR:=FALSE,
                    USEOUTERSEPARATOR:=TRUE,
                    OUTERSEPARATOR:=ENVIRONMENT.NEWLINE)
 
            DIM EN = CALENDARROW.GETENUMERATOR()
            DIM HASNEXT = EN.MOVENEXT()
            DO WHILE HASNEXT
 
                DIM CURRENT AS STRING = EN.CURRENT
 
                ' TO MAINTAIN THE (NOT STRICTLY NEEDED) CONTRACT OF YIELDING COMPLETE ROWS, KEEP THE NEWLINE AFTER
                ' THE CALENDAR ROW WITH THE LAST TERMINAL ROW OF THE ROW.
                HASNEXT = EN.MOVENEXT()
                YIELD IF(HASNEXT, CURRENT, CURRENT & ENVIRONMENT.NEWLINE)
            LOOP
 
            MONTH += MONTHSPERROW
        LOOP
    END FUNCTION
 
    ''' <SUMMARY>
    ''' INTERLEAVES THE ELEMENTS OF THE SPECIFIED SUB-SOURCES BY MAKING SUCCESSIVE PASSES THROUGH THE SOURCE
    ''' ENUMERABLE, YIELDING A SINGLE ELEMENT FROM EACH SUB-SOURCE IN SEQUENCE IN EACH PASS, OPTIONALLY INSERTING A
    ''' SEPARATOR BETWEEN ELEMENTS OF ADJACENT SUB-SOURCES AND OPTIONALLY A DIFFERENT SEPARATOR AT THE END OF EACH
    ''' PASS THROUGH ALL THE SOURCES. (I.E., BETWEEN ELEMENTS OF THE LAST AND FIRST SOURCE)
    ''' </SUMMARY>
    ''' <TYPEPARAM NAME="T">THE TYPE OF THE ELEMENTS OF THE SUB-SOURCES.</TYPEPARAM>
    ''' <PARAM NAME="SOURCES">A SEQUENCE OF THE SEQUENCES WHOSE ELEMENTS ARE TO BE INTERLEAVED.</PARAM>
    ''' <PARAM NAME="USEINNERSEPARATOR">WHETHER TO INSERT <PARAMREF NAME="USEINNERSEPARATOR"/> BETWEEN THE ELEMENTS OFADJACENT SUB-SOURCES.</PARAM>
    ''' <PARAM NAME="INNERSEPARATOR">THE SEPARATOR BETWEEN ELEMENTS OF ADJACENT SUB-SOURCES.</PARAM>
    ''' <PARAM NAME="USEOUTERSEPARATOR">WHETHER TO INSERT <PARAMREF NAME="OUTERSEPARATOR"/> BETWEEN THE ELEMENTS OF THE LAST AND FIRST SUB-SOURCES.</PARAM>
    ''' <PARAM NAME="OUTERSEPARATOR">THE SEPARATOR BETWEEN ELEMENTS OF THE LAST AND FIRST SUB-SOURCE.</PARAM>
    ''' <PARAM NAME="WHILEANY">IF <SEE LANGWORD="TRUE"/>, THE ENUMERATION CONTINUES UNTIL EVERY GIVEN SUBSOURCE IS EMPTY;
    ''' IF <SEE LANGWORD="FALSE"/>, THE ENUMERATION STOPS AS SOON AS ANY ENUMERABLE NO LONGER HAS AN ELEMENT TO SUPPLY FOR THE NEXT PASS.</PARAM>
    ITERATOR FUNCTION INTERLEAVED(OF T)(
            SOURCES AS IENUMERABLE(OF IENUMERABLE(OF T)),
            OPTIONAL USEINNERSEPARATOR AS BOOLEAN = FALSE,
            OPTIONAL INNERSEPARATOR AS T = NOTHING,
            OPTIONAL USEOUTERSEPARATOR AS BOOLEAN = FALSE,
            OPTIONAL OUTERSEPARATOR AS T = NOTHING,
            OPTIONAL WHILEANY AS BOOLEAN = TRUE) AS IENUMERABLE(OF T)
        DIM SOURCEENUMERATORS AS IENUMERATOR(OF T)() = NOTHING
 
        TRY
            SOURCEENUMERATORS = SOURCES.SELECT(FUNCTION(X) X.GETENUMERATOR()).TOARRAY()
            DIM NUMSOURCES = SOURCEENUMERATORS.LENGTH
            DIM ENUMERATORSTATES(NUMSOURCES - 1) AS BOOLEAN
 
            DIM ANYPREVITERS AS BOOLEAN = FALSE
            DO
                ' INDICES OF FIRST AND LAST SUB-SOURCES THAT HAVE ELEMENTS.
                DIM FIRSTACTIVE = -1, LASTACTIVE = -1
 
                ' DETERMINE WHETHER EACH SUB-SOURCE THAT STILL HAVE ELEMENTS.
                FOR I = 0 TO NUMSOURCES - 1
                    ENUMERATORSTATES(I) = SOURCEENUMERATORS(I).MOVENEXT()
                    IF ENUMERATORSTATES(I) THEN
                        IF FIRSTACTIVE = -1 THEN FIRSTACTIVE = I
                        LASTACTIVE = I
                    END IF
                NEXT
 
                ' DETERMINE WHETHER TO YIELD ANYTHING IN THIS ITERATION BASED ON WHETHER WHILEANY IS TRUE.
                ' NOT YIELDING ANYTHING THIS ITERATION IMPLIES THAT THE ENUMERATION HAS ENDED.
                DIM THISITERHASRESULTS AS BOOLEAN = IF(WHILEANY, FIRSTACTIVE <> -1, FIRSTACTIVE = 0 ANDALSO LASTACTIVE = NUMSOURCES - 1)
                IF NOT THISITERHASRESULTS THEN EXIT DO
 
                ' DON'T INSERT A SEPARATOR ON THE FIRST PASS.
                IF ANYPREVITERS THEN
                    IF USEOUTERSEPARATOR THEN YIELD OUTERSEPARATOR
                ELSE
                    ANYPREVITERS = TRUE
                END IF
 
                ' GO THROUGH AND YIELD FROM THE SUB-SOURCES THAT STILL HAVE ELEMENTS.
                FOR I = 0 TO NUMSOURCES - 1
                    IF ENUMERATORSTATES(I) THEN
                        ' DON'T INSERT A SEPARATOR BEFORE THE FIRST ELEMENT.
                        IF I > FIRSTACTIVE ANDALSO USEINNERSEPARATOR THEN YIELD INNERSEPARATOR
                        YIELD SOURCEENUMERATORS(I).CURRENT
                    END IF
                NEXT
            LOOP
 
        FINALLY
            IF SOURCEENUMERATORS ISNOT NOTHING THEN
                FOR EACH EN IN SOURCEENUMERATORS
                    EN.DISPOSE()
                NEXT
            END IF
        END TRY
    END FUNCTION
 
    ''' <SUMMARY>
    ''' RETURNS THE ROWS REPRESENTING ONE MONTH CELL WITHOUT TRAILING NEWLINES. APPROPRIATE LEADING AND TRAILING
    ''' WHITESPACE IS ADDED SO THAT EVERY ROW HAS THE LENGTH OF WIDTH.
    ''' </SUMMARY>
    ''' <PARAM NAME="DT">A DATE WITHIN THE MONTH TO REPRESENT.</PARAM>
    ''' <PARAM NAME="WIDTH">THE WIDTH OF THE CELL.</PARAM>
    ''' <PARAM NAME="HEIGHT">THE HEIGHT.</PARAM>
    ''' <PARAM NAME="VERTSTRETCH">IF <SEE LANGWORD="TRUE" />, BLANK ROWS ARE INSERTED TO FIT THE AVAILABLE HEIGHT.
    ''' OTHERWISE, THE CELL HAS A CONSTANT HEIGHT OF </PARAM>
    ''' <PARAM NAME="HORIZSTRETCH">IF <SEE LANGWORD="TRUE" />, THE SPACING BETWEEN INDIVIDUAL DAYS IS INCREASED TO
    ''' FIT THE AVAILABLE WIDTH. OTHERWISE, THE CELL HAS A CONSTANT WIDTH OF 20 CHARACTERS AND IS PADDED TO BE IN
    ''' THE CENTER OF THE EXPECTED WIDTH.</PARAM>
    ITERATOR FUNCTION BUILDMONTH(DT AS DATE, WIDTH AS INTEGER, HEIGHT AS INTEGER, VERTSTRETCH AS BOOLEAN, HORIZSTRETCH AS BOOLEAN) AS IENUMERABLE(OF STRING)
        CONST DAY_WDT = 2 ' WIDTH OF A DAY.
        CONST ALLDAYS_WDT = DAY_WDT * 7 ' WIDTH OF AL LDAYS COMBINED.
 
        ' NORMALIZE THE DATE TO JANUARY 1.
        DT = NEW DATE(DT.YEAR, DT.MONTH, 1)
 
        ' HORIZONTAL WHITESPACE BETWEEN DAYS OF THE WEEK. CONSTANT OF 6 REPRESENTS 6 SEPARATORS PER LINE.
        DIM DAYSEP AS NEW STRING(" "C, MATH.MIN((WIDTH - ALLDAYS_WDT) \ 6, IF(HORIZSTRETCH, INTEGER.MAXVALUE, 1)))
        ' NUMBER OF BLANK LINES BETWEEN ROWS.
        DIM VERTBLANKCOUNT = IF(NOT VERTSTRETCH, 0, (HEIGHT - 8) \ 7)
 
        ' WIDTH OF EACH DAY * 7 DAYS IN ONE ROW + DAY SEPARATOR LENGTH * 6 SEPARATORS PER LINE.
        DIM BLOCKWIDTH = ALLDAYS_WDT + DAYSEP.LENGTH * 6
 
        ' THE WHITESPACE AT THE BEGINNING OF EACH LINE.
        DIM LEFTPAD AS NEW STRING(" "C, (WIDTH - BLOCKWIDTH) \ 2)
        ' THE WHITESPACE FOR BLANK LINES.
        DIM FULLPAD AS NEW STRING(" "C, WIDTH)
 
        ' LINES ARE "STAGED" IN THE STRINGBUILDER.
        DIM SB AS NEW STRINGBUILDER(LEFTPAD)
        DIM NUMLINES = 0
 
        ' GET THE CURRENT LINE SO FAR FORM THE STRINGBUILDER AND BEGIN A NEW LINE.
        ' RETURNS THE CURRENT LINE AND TRAILING BLANK LINES USED FOR VERTICAL PADDING (IF ANY).
        ' RETURNS EMPTY ENUMERABLE IF THE HEIGHT REQUIREMENT HAS BEEN REACHED.
        DIM ENDLINE =
         FUNCTION() AS IENUMERABLE(OF STRING)
             DIM FINISHEDLINE AS STRING = SB.TOSTRING().PADRIGHT(WIDTH)
             SB.CLEAR()
             SB.APPEND(LEFTPAD)
 
             ' USE AN INNER ITERATOR TO PREVENT LAZY EXECUTION OF SIDE EFFECTS OF OUTER FUNCTION.
             RETURN IF(NUMLINES >= HEIGHT,
                 ENUMERABLE.EMPTY(OF STRING)(),
                 ITERATOR FUNCTION() AS IENUMERABLE(OF STRING)
                     YIELD FINISHEDLINE
                     NUMLINES += 1
 
                     FOR I = 1 TO VERTBLANKCOUNT
                         IF NUMLINES >= HEIGHT THEN RETURN
                         YIELD FULLPAD
                         NUMLINES += 1
                     NEXT
                 END FUNCTION())
         END FUNCTION
 
        ' YIELD THE MONTH NAME.
        SB.APPEND(PADCENTER(DT.TOSTRING("MMMM", CULTUREINFO.INVARIANTCULTURE), BLOCKWIDTH).TOUPPER())
        FOR EACH L IN ENDLINE()
            YIELD L
        NEXT
 
        ' YIELD THE HEADER OF WEEKDAY NAMES.
        DIM WEEKNMABBREVS = [ENUM].GETNAMES(GETTYPE(DAYOFWEEK)).SELECT(FUNCTION(X) X.SUBSTRING(0, 2).TOUPPER())
        SB.APPEND(STRING.JOIN(DAYSEP, WEEKNMABBREVS))
        FOR EACH L IN ENDLINE()
            YIELD L
        NEXT
 
        ' DAY OF WEEK OF FIRST DAY OF MONTH.
        DIM STARTWKDY = CINT(DT.DAYOFWEEK)
 
        ' INITIALIZE WITH EMPTY SPACE FOR THE FIRST LINE.
        DIM FIRSTPAD AS NEW STRING(" "C, (DAY_WDT + DAYSEP.LENGTH) * STARTWKDY)
        SB.APPEND(FIRSTPAD)
 
        DIM D = DT
        DO WHILE D.MONTH = DT.MONTH
            SB.APPENDFORMAT(CULTUREINFO.INVARIANTCULTURE, $"{{0,{DAY_WDT}}}", D.DAY)
 
            ' EACH ROW ENDS ON SATURDAY.
            IF D.DAYOFWEEK = DAYOFWEEK.SATURDAY THEN
                FOR EACH L IN ENDLINE()
                    YIELD L
                NEXT
            ELSE
                SB.APPEND(DAYSEP)
            END IF
 
            D = D.ADDDAYS(1)
        LOOP
 
        ' KEEP ADDING EMPTY LINES UNTIL THE HEIGHT QUOTA IS MET.
        DIM NEXTLINES AS IENUMERABLE(OF STRING)
        DO
            NEXTLINES = ENDLINE()
            FOR EACH L IN NEXTLINES
                YIELD L
            NEXT
        LOOP WHILE NEXTLINES.ANY()
    END FUNCTION
 
    ''' <SUMMARY>
    ''' RETURNS A NEW STRING THAT CENTER-ALIGNS THE CHARACTERS IN THIS STRING BY PADDING TO THE LEFT AND RIGHT WITH
    ''' THE SPECIFIED CHARACTER TO A SPECIFIED TOTAL LENGTH.
    ''' </SUMMARY>
    ''' <PARAM NAME="S">THE STRING TO CENTER-ALIGN.</PARAM>
    ''' <PARAM NAME="TOTALWIDTH">THE NUMBER OF CHARACTERS IN THE RESULTING STRING.</PARAM>
    ''' <PARAM NAME="PADDINGCHAR">THE PADDING CHARACTER.</PARAM>
    <EXTENSION()>
    PRIVATE FUNCTION PADCENTER(S AS STRING, TOTALWIDTH AS INTEGER, OPTIONAL PADDINGCHAR AS CHAR = " "C) AS STRING
        RETURN S.PADLEFT(((TOTALWIDTH - S.LENGTH) \ 2) + S.LENGTH, PADDINGCHAR).PADRIGHT(TOTALWIDTH, PADDINGCHAR)
    END FUNCTION
END MODULE