Task: Calendar - for _REAL_ programmers








VB: 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
                    
                    _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)

        
        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))
        
        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

            
            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))

            
            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

                
                
                HASNEXT = EN.MOVENEXT()
                YIELD IF(HASNEXT, CURRENT, CURRENT & ENVIRONMENT.NEWLINE)
            LOOP

            MONTH += MONTHSPERROW
        LOOP
    END FUNCTION

    
    
    
    
    
    
    
    
    
    
    
    
    
    
    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
                
                DIM FIRSTACTIVE = -1, LASTACTIVE = -1

                
                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

                
                
                DIM THISITERHASRESULTS AS BOOLEAN = IF(WHILEANY, FIRSTACTIVE <> -1, FIRSTACTIVE = 0 ANDALSO LASTACTIVE = NUMSOURCES - 1)
                IF NOT THISITERHASRESULTS THEN EXIT DO

                
                IF ANYPREVITERS THEN
                    IF USEOUTERSEPARATOR THEN YIELD OUTERSEPARATOR
                ELSE
                    ANYPREVITERS = TRUE
                END IF

                
                FOR I = 0 TO NUMSOURCES - 1
                    IF ENUMERATORSTATES(I) THEN
                        
                        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

    
    
    
    
    
    
    
    
    
    
    
    
    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 
        CONST ALLDAYS_WDT = DAY_WDT * 7 

        
        DT = NEW DATE(DT.YEAR, DT.MONTH, 1)

        
        DIM DAYSEP AS NEW STRING(" "C, MATH.MIN((WIDTH - ALLDAYS_WDT) \ 6, IF(HORIZSTRETCH, INTEGER.MAXVALUE, 1)))
        
        DIM VERTBLANKCOUNT = IF(NOT VERTSTRETCH, 0, (HEIGHT - 8) \ 7)

        
        DIM BLOCKWIDTH = ALLDAYS_WDT + DAYSEP.LENGTH * 6

        
        DIM LEFTPAD AS NEW STRING(" "C, (WIDTH - BLOCKWIDTH) \ 2)
        
        DIM FULLPAD AS NEW STRING(" "C, WIDTH)

        
        DIM SB AS NEW STRINGBUILDER(LEFTPAD)
        DIM NUMLINES = 0

        
        
        
        DIM ENDLINE =
         FUNCTION() AS IENUMERABLE(OF STRING)
             DIM FINISHEDLINE AS STRING = SB.TOSTRING().PADRIGHT(WIDTH)
             SB.CLEAR()
             SB.APPEND(LEFTPAD)

             
             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

        
        SB.APPEND(PADCENTER(DT.TOSTRING("MMMM", CULTUREINFO.INVARIANTCULTURE), BLOCKWIDTH).TOUPPER())
        FOR EACH L IN ENDLINE()
            YIELD L
        NEXT

        
        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

        
        DIM STARTWKDY = CINT(DT.DAYOFWEEK)

        
        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)

            
            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

        
        DIM NEXTLINES AS IENUMERABLE(OF STRING)
        DO
            NEXTLINES = ENDLINE()
            FOR EACH L IN NEXTLINES
                YIELD L
            NEXT
        LOOP WHILE NEXTLINES.ANY()
    END FUNCTION

    
    
    
    
    
    
    
    <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