_STRUCTURED PROGRAMMING COLUMN_ by Jeff Duntemann [LISTING ONE] { Calendar unit demo program } { Jeff Duntemann -- 2/3/89 } PROGRAM CalTest; USES DOS,Crt, { Standard Borland units } Screens, { Given in DDJ 4/89 } Calendar; { Given in DDJ 6/89 } CONST YellowOnBlue = $1E; { Text attribute; yellow chars on blue background } CalX = 25; CalY = 5; VAR MyScreen : ScreenPtr; { Type exported by Screens unit } WorkScreen : Screen; { Type exported by Screens unit } Ch : Char; Quit : Boolean; ShowFor : DateTime; { Type exported by DOS unit } I : Word; { Dummy; picks up dayofweek field in GetDate } BEGIN MyScreen := @WorkScreen; { Create a pointer to WorkScreen } InitScreen(MyScreen,True); ClrScreen(MyScreen,ClearAtom); { Clear the entire screen } Quit := False; WITH ShowFor DO { Start with clock date } GetDate(Year,Month,Day,I); ShowCalendar(MyScreen,ShowFor,CalX,CalY,YellowOnBlue); REPEAT { Until Enter is pressed: } IF Keypressed THEN { If a keystroke is detected } BEGIN Ch := ReadKey; { Pick up the keystroke } IF Ord(Ch) = 0 THEN { See if it's an extended keystroke } BEGIN Ch := ReadKey; { If so, pick up scan code } CASE Ord(Ch) OF { and parse it } 72 : Pan(MyScreen,Up,1); { Up arrow } 80 : Pan(MyScreen,Down,1); { Down arrow } 75 : BEGIN { Left arrow; "down time" } WITH ShowFor DO IF Month = 1 THEN BEGIN Month := 12; Dec(Year) END ELSE Dec(Month); ShowCalendar(MyScreen,ShowFor,CalX,CalY,YellowOnBlue); END; 77 : BEGIN { Right arrow; "up time" } WITH ShowFor DO IF Month = 12 THEN BEGIN Month := 1; Inc(Year) END ELSE Inc(Month); ShowCalendar(MyScreen,ShowFor,CalX,CalY,YellowOnBlue); END; END { CASE } END ELSE { If it's an ordinary keystroke, test for quit: } IF Ch = Chr(13) THEN Quit := True END; UNTIL Quit; ClrScreen(MyScreen,ClearAtom) { All this stuff's exported by Screens } END. [LISTING TWO] {--------------------------------------------------------------} { CALENDAR } { } { Text calendar for virtual screen platform } { } { by Jeff Duntemann KI6RA } { Turbo Pascal 5.0 } { Last modified 2/3/89 } {--------------------------------------------------------------} UNIT Calendar; INTERFACE USES DOS, { Standard Borland unit } TextInfo, { Given in DDJ 3/89 } Screens, { Given in DDJ 4/89 } CalCalc; { Given in DDJ 6/89 courtesy Michael Covington } TYPE DaysOfWeek = (Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday); Months = (January,February,March,April,May,June,July, August,September,October,November,December); PROCEDURE ShowCalendar(Target : ScreenPtr; ShowFor : DateTime; CalX,CalY : Integer; Attribute : Byte); IMPLEMENTATION TYPE String10 = STRING[10]; CONST MonthNames : ARRAY[January..December] OF String10 = ('January','February', 'March','April','May','June','July', 'August', 'September','October','November','December'); Days : ARRAY[January..December] OF Integer = (31,28,31,30,31,30,31,31,30,31,30,31); {$L CALBLKS} {$F+} PROCEDURE CalFrame; EXTERNAL; PROCEDURE Caldata; EXTERNAL; {$F-} {$L BLKBLAST} {$F+} PROCEDURE BlkBlast(ScreenEnd,StoreEnd : Pointer; ScreenX,ScreenY : Integer; ULX,ULY : Integer; Width,Height : Integer; Attribute : Byte; DeadLines : Integer; TopStop : Integer); EXTERNAL; {$F-} FUNCTION IsLeapYear(Year : Integer) : Boolean; { Works from 1901 - 2199 } BEGIN IsLeapYear := False; IF (Year MOD 4) = 0 THEN IsLeapYear := True END; PROCEDURE FrameCalendar(Target : ScreenPtr; CalX,CalY : Integer; Attribute : Byte; StartDay : DaysOfWeek; DayCount : Integer); TYPE PointerMath = RECORD CASE BOOLEAN OF True : (APointer : Pointer); False : (OfsWord : Word; SegWord : Word) END; VAR DataPtr : Pointer; FudgeIt : PointerMath; DayInset : Word; DayTopStop : Word; BEGIN { DayInset allows is to specify which day of the week the first of the } { month falls. It's an offset into the block containing day figures } DayInset := (7-Ord(StartDay))*4; { DayTopStop allows us to specify how many days to show in the month. } DayTopStop := 28+(DayCount*4)-DayInset; BlkBlast(Target,@CalFrame, { Display the calendar frame } VisibleX,VisibleY, { Genned screen size from TextInfo unit } CalX,CalY, { Show at specified coordinates } 29,17, { Size of calendar frame block } Attribute, { Attribute to use for calendar frame } 0, { No interspersed empty lines } 0); { No topstop; show the whole thing. } WITH FudgeIt DO { FudgeIt is a free union allowing pointer arithmetic } BEGIN APointer := @CalData; { Create the pointer to the days block } OfsWord := OfsWord+DayInset; { Offset into block for start day } BlkBlast(Target,APointer, { Blast the day block over the } VisibleX,VisibleY, { calendar frame } CalX+1,CalY+5, { Pos. of days relative to frame } 28,6, { Size of day block } Attribute, { Show days in same color as frame } 1, { Insert 1 line between block lines } DayTopStop) { Set limit on number of chars to } END { be copied from block to control } END; { how many days shown for a month } PROCEDURE ShowCalendar(Target : ScreenPtr; ShowFor : DateTime; CalX,CalY : Integer; Attribute : Byte); CONST NameOffset : ARRAY[January..December] OF Integer = (8,8,10,10,11,10,10,9,7,8,8,8); VAR StartDay : DaysOfWeek; TargetMonth : Months; TargetDay : Real; DaysInMonth : Integer; BEGIN { First figure day number since 1980: } WITH ShowFor DO TargetDay := DayNumber(Year,Month,1); { Then use the day number to calculate day-of-the-week: } StartDay := DaysOfWeek(WeekDay(TargetDay)-1); TargetMonth := Months(ShowFor.Month-1); DaysInMonth := Days[TargetMonth]; { Test and/or adjust for leap year: } IF TargetMonth = February THEN IF IsLeapYear(ShowFor.Year) THEN DaysInMonth := 29; { Now draw the frame on the virtual screen! } FrameCalendar(Target, CalX,CalY, Attribute, StartDay, DaysInMonth); { Add the month name and year atop the frame: } GotoXY(Target,CalX+NameOffset[TargetMonth],CalY+1); WriteTo(Target,MonthNames[TargetMonth]+' '+IntStr(ShowFor.Year,4)); END; END. [LISTING THREE] UNIT CalCalc; { --- Calendrics --- } { Long-range calendrical package in standard Pascal } { Copyright 1985 Michael A. Covington } INTERFACE function daynumber(year,month,day:integer):real; procedure caldate(date:real; var year,month,day:integer); function weekday(date:real):integer; function julian(date:real):real; IMPLEMENTATION function floor(x:real) : real; { Largest whole number not greater than x. } { Uses real data type to accommodate large numbers. } begin if (x < 0) and (frac(x) <> 0) then floor := int(x) - 1.0 else floor := int(x) end; function daynumber(year,month,day:integer):real; { Number of days elapsed since 1980 January 0 (1979 December 31). } { Note that the year should be given as (e.g.) 1985, not just 85. } { Switches from Julian to Gregorian calendar on Oct. 15, 1582. } var y,m: integer; a,b,d: real; begin if year < 0 then y := year + 1 else y := year; m := month; if month < 3 then begin m := m + 12; y := y - 1 end; d := floor(365.25*y) + int(30.6001*(m+1)) + day - 723244.0; if d < -145068.0 then { Julian calendar } daynumber := d else { Gregorian calendar } begin a := floor(y/100.0); b := 2 - a + floor(a/4.0); daynumber := d + b end end; procedure caldate(date:real; var year,month,day:integer); { Inverse of DAYNUMBER; given date, finds year, month, and day. } { Uses real arithmetic because numbers are too big for integers. } var a,aa,b,c,d,e,z: real; y: integer; begin z := int(date + 2444239.0); if date < -145078.0 then { Julian calendar } a := z else { Gregorian calendar } begin aa := floor((z-1867216.25)/36524.25); a := z + 1 + aa - floor(aa/4.0) end; b := a + 1524.0; c := int((b-122.1)/365.25); d := int(365.25*c); e := int((b-d)/30.6001); day := trunc(b - d - int(30.6001*e)); if e > 13.5 then month := trunc(e - 13.0) else month := trunc(e - 1.0); if month > 2 then y := trunc(c - 4716.0) else y := trunc(c - 4715.0); if y < 1 then year := y - 1 else year := y end; function weekday(date:real):integer; { Given day number as used in the above routines, } { finds day of week (1 = Sunday, 2 = Monday, etc.). } var dd: real; begin dd := date; while dd > 28000.0 do dd:=dd-28000.0; while dd < 0 do dd:=dd+28000.0; weekday := ((trunc(dd) + 1) mod 7) + 1 end; function julian(date:real):real; { Converts result of DAYNUMBER into a Julian date. } begin julian := date + 2444238.5 end; END. { CalCalc } [LISTING FOUR] ;=========================================================================== ; ; B L K B L A S T - Blast 2D character pattern and attributes into memory ; ;=========================================================================== ; ; by Jeff Duntemann 3 February 1989 ; ; BLKBLAST is written to be called from Turbo Pascal 5.0 using the EXTERNAL ; machine-code procedure convention. ; ; This version is written to be used with the SCREENS.PAS virtual screens ; unit for Turbo Pascal 5.0. See DDJ for 4/89. ; ; Declare the procedure itself as external using this declaration: ; ; PROCEDURE BlkBlast(ScreenEnd,StoreEnd : Pointer; ; ScreenX,ScreenY : Integer; ; ULX,ULY : Integer; ; Width,Height : Integer; ; Attribute : Byte; ; DeadLines : Integer; ; TopStop : Integer); ; EXTERNAL; ; ; The idea is to store a video pattern as an assembly-language external or ; as a typed constant, and then blast it into memory so that it isn't seen ; to "flow" down from top to bottom, even on 8088 machines. ; ; During the blast itself, the attribute byte passed in the Attribute ; parameter is written to the screen along with the character information ; pointed to by the source pointer. In effect, this means we do a byte-sized ; read from the source character data, but a word-sized write to the screen. ; ; The DeadLines parm specifies how many screen lines to skip between lines of ; the pattern. The skipped lines are not disturbed. TopStop provides a byte ; count that is the maximum number of bytes to blast in from the pattern. ; If a 0 is passed in TopStop, the value is ignored. ; ; To reassemble BLKBLAST: ; ; Assemble this file with MASM or TASM: "C>MASM BLKBLAST;" ; (The semicolon is unnecessary with TASM.) ; ; No need to relink; Turbo Pascal uses the .OBJ only. ; ;======================== ; ; STACK PROTOCOL ; ; This creature puts lots of things on the stack. Study closely: ; ONSTACK STRUC OldBP DW ? ;Caller's BP value saved on the stack RetAddr DD ? ;Full 32-bit return address. (This is a FAR proc!) TopStop DW ? ;Maximum number of chars to be copied from block pattern DeadLns DW ? ;Number of lines of dead space to insert between blasted lines Attr DW ? ;Attribute to be added to blasted pattern BHeight DW ? ;Height of block to be blasted to the screen BWidth DW ? ;Width of block to be blasted to the screen ULY DW ? ;Y coordinate of upper left corner of the block ULX DW ? ;X coordinate of the upper left corner of the block YSize DW ? ;Genned max Y dimension of current visible screen XSize DW ? ;Genned max X dimension of current visible screen Block DD ? ;32-bit pointer to block pattern somewhere in memory Screen DD ? ;32-bit pointer to an array of pointers to screen lines ENDMRK DB ? ;Dummy field for stack struct size calculation ONSTACK ENDS CODE SEGMENT PUBLIC ASSUME CS:CODE PUBLIC BlkBlast BlkBlast PROC FAR PUSH BP ;Save Turbo Pascal's BP value MOV BP,SP ;SP becomes new value in BP PUSH DS ;Save Turbo Pascal's DS value ;------------------------------------------------------------------------- ; If a zero is passed in TopStop, then we fill the TopStop field in the ; struct with the full size of the block, calculated by multiplying ; BWidth times BHeight. This makes it unnecessary for the caller to ; pass the full size of the block in the TopStop parameter if topstopping ; is not required. ;------------------------------------------------------------------------- CMP [BP].TopStop,0 ; See if zero was passed in TopStop JNZ GetPtrs ; If not, skip this operation MOV AX,[BP].BWidth ; Load block width into AX MUL [BP].BHeight ; Multiply by block height, to AX MOV [BP].TopStop,AX ; Put the product back into TopStop ;------------------------------------------------------------------------- ; The first important task is to get the first pointer in the ShowPtrs ; array into ES:DI. This involved two LES operations: The first to get ; the pointer to ShowPtrs (field Screen in the stack struct) into ES:DI, ; the second to use ES:DI to get the first ShowPtrs pointer into ES:DI. ; Remembering that ShowPtrs is an *array* of pointers, the next task is ; to index DI into the array by multiplying the top line number (ULY) ; less one (because we're one-based) by 4 using SHL and then adding that ; index to DI: ;------------------------------------------------------------------------- GetPtrs: LES DI,[BP].Screen ; Address of ShowPtrs array in ES:DI MOV CX,[BP].ULY ; Load line address of block dest. to CX DEC CX ; Subtract 1 'cause we're one-based SHL CX,1 ; Multiply CX by 4 by shifting it left... SHL CX,1 ; ...twice. ADD DI,CX ; Add the resulting index to DI. MOV BX,DI ; Copy offset of ShowPtrs into BX MOV DX,ES ; Copy segment of ShowPtrs into DX LES DI,ES:[DI] ; Load first line pointer into ES:DI ;------------------------------------------------------------------------- ; The inset from the left margin of the block's destination is given in ; struct field ULX. It's one-based, so it has to be decremented by one, ; then multiplied by two using SHL since each character atom is two bytes ; in size. The value in the stack frame is adjusted (it's not a VAR parm, ; so that's safe) and then read from the frame at the start of each line ; blast and added to the line offset in DI. ;------------------------------------------------------------------------- DEC [BP].ULX ; Subtract 1 'cause we're one-based SHL [BP].ULX,1 ; Multiply by 2 to cover word moves ADD DI,[BP].ULX ; And add the adjustment to DI ;------------------------------------------------------------------------- ; One additional adjustment must be made before we start: The Deadspace ; parm puts 1 or more lines of empty space between each line of the block ; that we're blasting onto the screen. This value is passed in the ; DEADLNS field in the struct. It's passed as the number of lines to skip, ; but we have to multiply it by 4 so that it becomes an index into the ; ShowPtrs array, each element of which is four bytes in size. Like ULX, ; the value is adjusted in the stack frame and added to the stored offset ; value we keep in DX each time we set up the pointer in ES:DI to blast the ; next line. ;------------------------------------------------------------------------- SHL [BP].DEADLNS,1 ; Shift dead space line count by 1... SHL [BP].DEADLNS,1 ; ...and again to multiply by 4 LDS SI,[BP].Block ; Load pointer to block into DS:SI ;------------------------------------------------------------------------- ; This is the loop that does the actual block-blasting. Two counters are ; kept, and share CX by being separate values in CH and CL. After ; each line blast, both pointers are adjusted and the counters swapped, ; the LOOP counter decremented and tested, and then the counters swapped ; again. ;------------------------------------------------------------------------- MovEm: MOV CX,[BP].BWidth ; Load atom counter into CH MOV AH,BYTE PTR [BP].Attr ; Load attribute into AH DoChar: LODSB ; Load char from block storage into AL STOSW ; Store AX into ES:DI; increment DI by 2 LOOP DoChar ; Go back for next char if CX > 0 ;------------------------------------------------------------------------- ; Immediately after a line is blasted from block to screen, we adjust. ; First we move the pointer in ES:DI to the next pointer in the ; Turbo Pascal ShowPtrs array. Note that the source pointer does NOT ; need adjusting. After blasting through one line of the source block, ; SI is left pointing at the first character of the next line of the ; source block. Also note the addition of the deadspace adjustment to ; BX *before* BX is copied into DI, so that the adjustment will be ; retained through all the rest of the lines moved. Finally, we subtract ; the number of characters in a line from TopStop, and see if there are ; fewer counts left in TopStop than there are characters in a block line. ; If so, we force BWidth to the number of remaining characters, and ; BHeight to one, so that we will blast only one remaining (short) line. ;------------------------------------------------------------------------- MOV ES,DX ; Copy ShowPtrs segment from DX into ES ADD BX,4 ; Bounce BX to next pointer offset ADD BX,[BP].DeadLns ; Add deadspace adjustment to BX LES DI,ES:[BX] ; Load next pointer into ES:DI ADD DI,[BP].ULX ; Add adjustment for X offset into screen MOV AX,[BP].TopStop ; Load current TopStop value into AX SUB AX,[BP].BWidth ; Subtract BWidth from TopSTop value JBE GoHome ; If TopStop is <= zero, we're done. MOV [BP].TopStop,AX ; Put TopStop value back in stack struct CMP AX,[BP].BWidth ; Compare what remains in TopStop to BWidth JAE MovEm ; If at least one BWidth remains, loop again MOV [BP].BWidth,AX ; Otherwise, replace BWidth with remainder JMP MovEm ; and jump to last go-thru ;------------------------------------------------------------------------- ; When the outer loop is finished, the work is done. Restore registers ; and return to Turbo Pascal. ;------------------------------------------------------------------------- GoHome: POP DS ; Restore Turbo Pascal's MOV SP,BP ; Restore Turbo Pascal's stack pointer... POP BP ; ...and BP RET ENDMRK-RETADDR-4 ; Clean up stack and return as FAR proc! ; (would be ENDMRK-RETADDR-2 for NEAR...) BlkBlast ENDP CODE ENDS END [LISTING FIVE] TITLE CalBlks -- External calendar pattern blocks ; By Jeff Duntemann -- TASM 1.0 -- Last modified 3/1/89 ; ; For use with CALENDAR.PAS and BLKBLAST.ASM as described in DDJ 6/89 CODE SEGMENT WORD ASSUME CS:CODE CalFrame PROC FAR PUBLIC CalFrame DB 'ÕÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ͸' DB '³ ³' DB 'ÃÄÄÄÂÄÄÄÂÄÄÄÂÄÄÄÂÄÄÄÂÄÄÄÂÄÄÄ´' DB '³Sun³Mon³Tue³Wed³Thu³Fri³Sat³' DB 'ÃÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄ´' DB '³ ³ ³ ³ ³ ³ ³ ³' DB 'ÃÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄ´' DB '³ ³ ³ ³ ³ ³ ³ ³' DB 'ÃÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄ´' DB '³ ³ ³ ³ ³ ³ ³ ³' DB 'ÃÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄ´' DB '³ ³ ³ ³ ³ ³ ³ ³' DB 'ÃÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄ´' DB '³ ³ ³ ³ ³ ³ ³ ³' DB 'ÃÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄÅÄÄÄ´' DB '³ ³ ³ ³ ³ ³ ³ ³' DB 'ÔÍÍÍÏÍÍÍÏÍÍÍÏÍÍÍÏÍÍÍÏÍÍÍÏÍÍ;' Calframe ENDP CalData PROC FAR PUBLIC CalData DB ' ³ ³ ³ ³ ³ ³ ³' DB ' 1³ 2³ 3³ 4³ 5³ 6³ 7³' DB ' 8³ 9³ 10³ 11³ 12³ 13³ 14³' DB ' 15³ 16³ 17³ 18³ 19³ 20³ 21³' DB ' 22³ 23³ 24³ 25³ 26³ 27³ 28³' DB ' 29³ 30³ 31³ ³ ³ ³ ³' DB ' ³ ³ ³ ³ ³ ³ ³' DB ' ³ ³ ³ ³ ³ ³ ³' CalData ENDP CODE ENDS END