332 lines
14 KiB
VB.net
332 lines
14 KiB
VB.net
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
|