This program generates a perpetual calendar for any date between 1801 and 2399, displaying the day of the week and a formatted monthly calendar page with the target date highlighted using inverse video characters. A comma-delimited string is used as a compact data store for day names, month names, and month lengths, parsed at startup by a reusable GOSUB 1000 tokenizer routine. The day-of-week calculation uses a Zeller-style formula adjusted for century boundaries, with special cases for years 1900–1999, year 2000, and other centuries. Leap year detection at line 480 correctly handles the Gregorian calendar’s century exception, though the year-2000 special case is hardcoded separately rather than derived from the general rule.
Program Structure
The program divides cleanly into five phases:
- Initialisation (lines 10–240): Arrays
D$(7,10)(day names),M$(12,9)(month names), andL(12)(month lengths) are populated by repeatedly calling the string-tokenizer at line 1000, which walks the comma-delimited master stringI$. - Input (lines 250–450): The user types a date in
MM,DD,YYYYformat; the same tokenizer routine re-usesI$(now overwritten with the user’s input) to extract month, day, and year. - Validation (lines 460–490): Checks month range, day range against
L(M), and year range 1801–2399. Leap year is computed and stored inL(2)at line 480. - Day-of-week calculation (lines 500–542): A Zeller-style formula computes
FOM(first-of-month offset) andDOW. - Display (lines 550–830): Prints the month/year header, abbreviated day-of-week row, and the calendar grid. The queried date is highlighted in inverse video before the next-date prompt is shown.
String Tokenizer (GOSUB 1000)
Lines 1000–1060 implement a simple comma-delimiter parser. Two pointers P1 and P2 are maintained globally. The routine scans forward from P2 until a comma is found, extracts I$(P1 TO P2-1) into R$, then advances both pointers past the comma ready for the next call. This single subroutine serves double duty: first during initialisation (parsing the built-in data string) and again during input parsing (applied to the user-supplied date string).
Array Length Encoding
Because ZX81 string arrays have fixed-width rows, variable-length strings are padded with spaces. The actual string length is stored as a single character in the last cell of each row: D$(I,10)=CHR$(LEN R$) and M$(I,9)=CHR$(LEN R$). To retrieve the string, the program reads back the length with CODE D$(DOW,10) or CODE M$(M,9) and uses a TO slice, e.g. D$(DOW,1 TO CODE D$(DOW,10)). This avoids trailing-space problems without needing a separate length array.
Day-of-Week Formula
Lines 500–542 use a modified Zeller congruence. January and February are treated as months 13 and 14 of the previous year (lines 500–510). The core formula at line 520 is:
FOM = INT(Y*1.25) + (Y<1900) + (Y>2000)*INT((Y-2000)/100) + INT((M-2)*2.59)
The term (Y<1900) adds 1 for 19th-century years and (Y>2000)*INT((Y-2000)/100) adjusts for each completed century beyond 2000, compensating for century non-leap years. DOW is then computed modulo 7. After that, FOM is separately reduced modulo 7 and incremented by 1 to give the 1-based column for the first day of the month.
Leap Year Detection
Line 480 computes February’s length:
L(2) = 28 + ((Y=INT(Y/4)*4 AND Y<>INT(Y/100)*100) OR Y=2000)
This correctly identifies leap years divisible by 4 but not 100, with year 2000 hardcoded as an exception (it is divisible by 400). However, other years divisible by 400 within the supported range (2400 is excluded; 1600 is below the lower bound) are not handled by the general rule—for this program’s stated range of 1801–2399 the logic is correct.
Inverse-Video Date Highlighting
Lines 680–720 convert the day number string for the selected date to inverse video by adding 128 to each character code:
LET P$(J) = CHR$(CODE P$(J) + 128)
This exploits the ZX81’s character set, where codes 128–255 are the inverse-video equivalents of codes 0–127, providing highlighting without any additional PRINT control codes.
Calendar Grid Layout
The grid uses PRINT AT VP, HP+(I<10) (line 730) to right-align single-digit dates within their four-character column. HP starts at (FOM-1)*4+2 and advances by 4 each day; when it reaches or exceeds 30, both VP is incremented by 2 and HP resets to 2 (lines 750–760).
Notable Idioms and Anomalies
FAST/SLOWare used strategically: array initialisation and calculation run inFASTmode; input and display run inSLOWmode.- Line 785 prints a row of
▀block graphics (zmakebas\''sequences) as a visual separator before the quit prompt. - Line 2000 (
SAVE "CALENDA%R") and line 3000 (GOTO 10) are utility lines, not part of the normal execution path—the%in the filename is unusual and may be a typo for the intended nameCALENDAR. - Line 1800 contains an unreachable
STOP; it appears to be a leftover stub. - The re-use of
I$for both the initialisation data string and the user’s date input means that after line 340, the original day/month data is overwritten. This is intentional—the arrays have already been populated by then—but it is a subtle side-effect that could confuse maintenance. - The tokenizer advances
P2by 2 and setsP1=P2-1(lines 1040–1050), which correctly positionsP1one character ahead of the just-consumed comma, ready for the next token.
Variable Summary
| Variable | Purpose |
|---|---|
I$ | Master data string (init), then user date input |
P1, P2 | Tokenizer start/end pointers into I$ |
R$ | Token returned by GOSUB 1000 |
D$(7,10) | Day names; column 10 stores name length as CHR$ |
M$(12,9) | Month names; column 9 stores name length as CHR$ |
L(12) | Days in each month (L(2) updated for leap year) |
M, D, Y | Input month, day, year |
FOM | First-of-month day offset (1–7) |
DOW | Day of week for the input date (1–7) |
VP, HP | Vertical/horizontal print position for calendar grid |
P$ | String form of day number, optionally inverse-video |
Content
Source Code
1 REM "CALENDAR"
10 FAST
20 DIM D$(7,10)
30 DIM M$(12,9)
40 DIM L(12)
49 REM --INITIALIZE VARIABLES AND ARRAYS--
50 REM
51 LET P1=1
60 LET P2=7
70 LET I$="SUNDAY,MONDAY,TUESDAY,WEDNESDAY,"
80 LET I$=I$+"THURSDAY,FRIDAY,SATURDAY,"
90 LET I$=I$+"JANUARY,31,FEBRUARY,28,MARCH,31,"
100 LET I$=I$+"APRIL,30,MAY,31,JUNE,30,JULY,31,"
110 LET I$=I$+"AUGUST,31,SEPTEMBER,30,OCTOBER,31,"
120 LET I$=I$+"NOVEMBER,30,DECEMBER,31,"
130 FOR I=1 TO 7
140 GOSUB 1000
150 LET D$(I)=R$
160 LET D$(I,10)=CHR$ (LEN R$)
170 NEXT I
180 FOR I=1 TO 12
190 GOSUB 1000
200 LET M$(I)=R$
210 LET M$(I,9)=CHR$ (LEN R$)
220 GOSUB 1000
230 LET L(I)=VAL (R$)
240 NEXT I
249 REM --ASK FOR,ACCEPT,AND CHECK INPUT--
250 SLOW
260 CLS
270 PRINT AT 0,7;"PERPETUAL CALENDAR"
280 PRINT AT 1,0;"TYPE IN DATE IN ANY YEAR"
290 PRINT "AFTER 1800 AND BEFORE 2400;"
300 PRINT "THEN PRESS <ENTER>."
310 PRINT AT 5,0;"USE THIS FORMAT:"
320 PRINT AT 7,0;"12,22,1984"
330 PRINT AT 9,0;"DATE? ";
340 INPUT I$
350 PRINT I$
360 LET I$=I$+","
370 LET P1=1
380 LET P2=2
390 GOSUB 1000
400 LET M=VAL R$
410 IF M<1 OR M>12 THEN GOTO 260
420 GOSUB 1000
430 LET D=VAL R$
440 GOSUB 1000
450 LET Y=VAL R$
460 FAST
470 CLS
480 LET L(2)=28+((Y=INT (Y/4)*4 AND Y<>INT (Y/100)*100) OR Y=2000)
490 IF D<1 OR D>L(M) OR Y<1801 OR Y>2399 THEN GOTO 250
495 REM
496 REM ***********************
498 REM --COMPUTE WHAT DAY THE DATE FALLS ON (DOW)-
499 REM ***********************
500 IF M<3 THEN LET Y=Y-1
510 IF M<3 THEN LET M=M+12
520 LET FOM=INT (Y*1.25)+(Y<1900)+(Y>2000)*INT ((Y-2000)/100)+INT ((M-2)*2.59)
530 LET DOW=FOM+D-INT ((FOM+D-1)/7)*7
537 REM
538 REM ***********************
539 REM --FOM IS DAY THAT FIRST OF MONTH M FALLS ON--
540 REM ***********************
541 REM
542 LET FOM=FOM-INT (FOM/7)*7+1
550 IF M>12 THEN LET Y=Y+1
560 IF M>12 THEN LET M=M-12
565 REM
566 REM ***********************
567 REM --PRINT DAY OF WEEK AND TOP OF CALENDAR PAGE--
568 REM ***********************
569 REM
570 PRINT M$(M,1 TO CODE M$(M,9));" ";D;", ";Y;", IS A"
580 PRINT D$(DOW,1 TO CODE D$(DOW,10));"."
590 PRINT AT 4,(25-CODE M$(M,9))/2;M$(M,1 TO CODE M$(M,9));" ";Y
600 PRINT AT 6,2;
610 FOR I=1 TO 7
620 PRINT D$(I,1 TO 3);" ";
630 NEXT I
640 PRINT
650 LET VP=8
660 LET HP=(FOM-1)*4+2
670 FOR I=1 TO L(M)
680 LET P$=STR$ (I)
690 IF I<>D THEN GOTO 730
700 FOR J=1 TO LEN P$
710 LET P$(J)=CHR$ (CODE P$(J)+128)
720 NEXT J
730 PRINT AT VP,HP+(I<10);P$
740 LET HP=HP+4
750 IF HP>=30 THEN LET VP=VP+2
760 IF HP>=30 THEN LET HP=2
770 NEXT I
780 SLOW
781 REM
782 REM **********************
783 REM -- ANOTHER DATE OR STOP
784 REM **********************
785 PRINT AT 19,0;"\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''"
790 PRINT AT 20,3;"PRESS <Q> TO QUIT OR ANY";AT 21,0;"OTHER KEY TO TRY ANOTHER DATE."
800 LET K$=INKEY$
810 IF K$="" THEN GOTO 800
820 IF K$<>"Q" THEN GOTO 260
830 STOP
\n1000 IF I$(P2)="," THEN GOTO 1030
\n1010 LET P2=P2+1
\n1020 GOTO 1000
\n1030 LET R$=I$(P1 TO P2-1)
\n1040 LET P2=P2+2
\n1050 LET P1=P2-1
\n1060 RETURN
\n1800 STOP
\n2000 SAVE "CALENDA%R"
\n3000 GOTO 10
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.
