Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Tools

Is This Angst Really Necessary?


JUN89: STRUCTURED PROGRAMMING

I was in an art supplies store in Rochester, New York the afternoon Rochester Gas and Electric's Ginna nuclear power plant burped and spat a little wisp of radioactive steam into the wind. I'd long since learned to stop worrying and love alpha particles; between the radon from my basement and the cosmic rays one can't help but meet at 30,000 feet, I figured Ginna wasn't adding any statistically significant risk to my life. The owner of the art store, on the other hand, was obviously petrified. She had a little portable TV propped up on a pile of Strathmore pads, and was hanging on the news announcer's every word -- her hands shaking as she kept lighting one cigarette off the end of the last, going through a full pack in 20 minutes.

"The man who invented radioactivity ought to be shot," she complained as she rang up my drafting pencils. By that time you could barely see in there, and I hastened out the door while I still had my lungs.

My bitter sense of irony told me that she had the perfect response to nuclear angst: Chain smoke long enough and low-level radioactivity ceases to be any kind of threat. There is a similar (and just as pointless) angst prevalent among naive programmers: The fear that high-level languages invariably produce glacially slow code. In terror of a threat that they can't even understand, much less begin to measure, they react by the HLL equivalent of chain-smoking, which is to replace one routine after another in their programs with badly-conceived and barely-operable assembly language. Eventually the programs implode under a cancerous profusion of interrelated bugs that freeze their machines solid -- at which point slow code performance or any other kind of code performance ceases to be an issue.

Don't Worry -- Be Happy

Is this angst really necessary? Cripes almighty, no! Five years of crawling through native code HLL programs with a debugger have convinced me of this: Modern high-level languages write better assembly language than 99 percent of all the programmers who have ever lived or will ever live. Not only are your chances of writing faster code than the compiler not good, the chances are excellent that your own assembly language will be slower.

This is truer now than ever before. The battle of the titans between Bill Gates and Philippe Kahn has produced some killer code generators. (This includes TopSpeed Modula-2, which began as Turbo Modula-2 when Nils Jensen was still a Borlander.) Out of the woodwork are coming new compilers like Watcom C, which beats both Turbo and Quick C in the time trials and are raising the ante in the programmers' equivalent of the nuclear arms race.

The bottom line is this: Unless you're as good an assembly language programmer as Anders Hejlsberg, the author of Turbo Pascal, don't worry about HLL code performance. If you must do something, think hard about your algorithms, remembering that a bubble sort in assembler is almost certainly slower than a shell sort in interpreted Basic. Do what you do best -- program in your chosen HLL -- and let Anders and his rare breed do what they do best: Write programs that write assembly language.

The 20 Percent Rule

I've identified two broad categories of situations where your chances of beating the compiler rise to about 50/50: The first category includes situations when time resolution is a factor. The second category includes any movement of screen data that represents more than 20 percent of the area of your screen.

The first category is a thin one indeed. I'm speaking of things like pulling data points in from some kind of analog/digital converter. The simplest example is the PC joystick. You can poll the joystick from Pascal or Modula-2, but you get better resolution of the stick when you poll it from assembler. The resolution of the stick is directly proportional to the number of times per second that your code can grab a value from a PC I/O port. Assembler doesn't necessarily win, but it can win. Keep in mind that you have to be a good enough assembly language programmer to code the tightest possible loop, taking advantage of assumptions and special information that the compiler's code generator doesn't necessarily have.

The second category, on the other hand, is an area of critical concern, especially when the screen in question is a graphics screen. My 20 percent rule applies to text screens only; in graphics modes scale that back to 5 percent, or even 0 percent -- maybe I'm picky, but graphics screens are never fast enough to suit me. Screen refreshes that are slow enough to watch are too slow. Lines, windows, and other screen objects should appear and vanish instantly. Accept nothing less.

Blasting Lines

I've managed to build the virtual screens library through three columns without resorting to assembly language for anything but a simple INLINE macro for clearing lines in SCREENS.PAS. (DDJ, April 1989.) That macro (ClearLine) was nothing more than setup and minimal safety check for a single, powerful op-code: REP STOSW. The STOSW opcode takes whatever's in AX and blasts it out into memory at the fastest speed of which the '86 iscapable. It's a machine-code loop that executes entirely within the CPU, behind the mask of a single instruction. Compilers are not generally smart enough to identify situations in which REP STOSW is useful, so we have to code them up ourselves.

Turbo Pascal does have the FillChar procedure, which uses the REP STOSB, which is close, but not quite what we need to clear a screen line with both a chosen clear character and a chosen attribute character. Also, there is considerable fooling around that the more general FillChar routine must go through to set itself up to perform the REP STOSB. We don't need that in this case, but the compiler can't help generating it. If I needed a maximum-speed buffer filler, I might in fact be tempted to code it up because there's not that much to it.

The guidance here is to keep it simple, and know your assembly language. I can't teach you assembly language in this column, lordy, but if you're going to try it the least I can do is counsel you to read up on it and try to understand what it's about. Tom Swan has an excellent book: Mastering Turbo Assembler, on the stands right now, and my own introduction to assembly language, Assembling From Square One (which speaks of both TASM and MASM) will be out this fall. Also well into production is Michael Abrash's two-volume opus The Zen of Assembler, which is definitely the category-killer book on advanced 86-family assembly language. When both volumes and all 1,600 pages of Zen are in print, I feel confident that the work will stand as the definitive reference to performance programming in assembler well into the Twenty-First Century. Master Zen and you'll be qualified to outthink the sharpest compiler.

On the other hand, having come away from the technical edit of Volume 1: Knowledge with smoking eyeballs, I will advise you that the learning process could take awhile.

Cloning a Calendar

So -- to show you a situation where assembly language is both necessary and effective, let's clone the Sidekick calendar for virtual screens.

The CALTEST.PAS demo program (Listing One) puts a calendar in the middle of your visible screen window. The right arrow takes you "up-time" and bumps the month by one into the future; the left arrow takes you "down-time" and bumps the month by one into the past. Pound on the arrow keys and try to catch any "flow" as the calendar comes in. You won't, not even on a 4.77MHz-8088 machine.

There are three important elements comprising the calendar. The CALENDAR.PAS unit (Listing Two) handles the display of the calendar. The calendar calculations are done in CALCALC.PAS (Listing Three). CALCALC.PAS comes from Michael Covington at the University of Georgia, and I use it here with his gracious permission. The key routine for this discussion, however, lies in file BLKBLAST.ASM (Listing Four). BlkBlast is the assembly language external to Turbo Pascal that actually moves the calendar patterns to the virtual screen. BlkBlast was placed in CALENDAR.PAS for convenience's sake; as a virtual screens primitive it rightfully belongs in SCREENS.PAS and I recommend you put it there.

The patterns themselves can be placed as arrays of STRING in typed constants, but I've chosen to gather them into a separate assembly language file, CALBLKS.ASM ( Listing Five) so that you can see how it's done. Note that the patterns stored in CALBLKS.ASM are called procedures, but (pretty obviously) are not meant to be executed. They need to be packaged as procedures to force the Turbo Pascal linker into loading and linking them to your program. They are, in fact, data, and are accessed by deriving pointers to them and passing them as pointer parameters to BlkBlast.

Blasting Blocks

Called from a Turbo Pascal program, BlkBlast copies a linear array of bytes from one location of memory to a rectangular region in a virtual screen. The pointer to the array of bytes where the pattern is stored is passed in StoreEnd, whereas the pointer to the virtual screen is passed in ScreenEnd. ScreenX and ScreenY are passed the current X and Y size of the visible screen (that is, the display adapter's buffer), respectively. My TextInfo unit (DDJ, March 1989) exports two preinitialized variables, VisibleX and VisibleY, that I use for that purpose.

ULX and ULY are passed the X,Y position of the upper left corner of the rectangular region within the virtual screen where the pattern is to be placed. Width and Height are passed the width and height of the pattern stored at the address passed in StoreEnd. Attribute is passed the attribute byte to be used for the display of the pattern.

DeadLines and TopStop are special parameters that serve the calendar application. A calendar is displayed by first blasting the CalFrame pattern (stored in CALBLKS.ASM) up to the virtual screen. The CalCalc unit is used to calculate an offset into the CalData pattern. This offset will vary depending on what day of the week the first of any given month falls on. The sooner in the week the first falls, the farther into CalData you must move for a starting point. Starting at this offset position, CalData is blasted atop CalFrame. Bingo! You have a calendar.

There is some trickiness involving the blast of the data onto the calendar frame. If you'll notice from CALBLKS.ASM, the numbers for the calendar are stored in a single-spaced array, when in fact they must be overlaid upon the calendar frame as double-spaced, in order to leave undisturbed the frame's lines that separate the rows of day blocks. DeadLines specifies some number of virtual screen lines that will be skipped between lines of the pattern blasted to the screen. A line of CalData is blasted to the screen; then a screen line is skipped; the next line of CalData is blasted to the screen, and so on. It is not a matter of inserting empty lines, but a matter of "spreading out" the lines of the pattern on the screen.

TopStop addresses another calendar gotcha: The months have different numbers of days. To avoid having Februaries with 31 days, BlkBlast must stop blasting CalData's data onto a February calendar frame before it reaches the characters for the 29th (except in leap years) 30th, and 31st days. If only a specific number of bytes of the pattern are to be blasted to the screen, that number is passed in TopStop. If the entire pattern is to be sent to the screen (as for the calendar frame) then TopStop is passed a 0.

Modelling a Stack Frame

I won't do an instruction-by-instruction explanation of how BlkBlast works. There simply isn't room here. What I do want to do is point to some general techniques that may help you create your own assembly language external modules.

First of all, model the stack frame in an assembly language structure. This is the item named ONSTACK in BlkBlast. The structure allows you to access fields by "dotting," in a fashion similar to what we do with Pascal records. Make sure you understand what Turbo Pascal pushes on the stack for your parameters, particularly in areas, like pointers, where it may not be immediately clear whether the segment or offset portion of the pointer is pushed first. (The segment is pushed first.)

The identifier ONSTACK is not actually referenced in the routine; it's only there because all structures must have names. Instead, the structure is referenced as the referent of SS:[BP], and individual fields within the structure resolve to offsets from BP. In other words, a structure reference like MOV AX,[BP]. Attr is equivalent to MOV AX,[BP+10]. The idea is to avoid having to calculate offsets from BP, especially when you find yourself inserting new fields into the structure or rearranging the ones that are there.

Notice the field named ENDMRK at the end of the structure. ENDMRK is not actually the name of anything pushed onto the stack. Instead, it allows us to use an assembly language expression to calculate the number of bytes to be removed from the stack when the external procedure returns control to Turbo Pascal. The expression comes at the very end of BlkBlast:

  RET ENDMRK-RETADDR-4

The operand to the RET instruction is what "cleans up the stack." When it executes, RET <n> adds <n> bytes to the stack pointer (remember, this is a push-down stack), at one stroke wiping the stack frame clean from the stack. <n> can be calculated by subtracting the start of the stack frame from its end, minus 4 bytes for the return address. It's important to exclude the size of the return address from the calculation, because by the time the RET instruction gets around to adjusting the stack pointer, it has already removed the return address from the stack.

Also, remember that BlkBlast is a FAR procedure. Had it been written as a NEAR procedure, the return address would have been a 16-bit offset, rather than a full 32-bit address, and the expression would have been:

RET ENDMRK-RETADDR-2

Other Assembly Language Pointers

Be sure to declare the name of the external routine as PUBLIC, or Turbo Pascal's linker will not be able to find the name in the .OBJ file's symbol table. Any identifier that is to be known outside the .ASM file must be explicitly declared as PUBLIC. This is in addition to adding the PUBLIC directive to the declaration of the external's code segment.

The key to fast assembly language work is to identify the tight loops that are executed dozens or hundreds of times (usually in moving data from one place to another) and to keep those loops as simple and empty as possible. If you can, use the 8086 string instructions MOVSB, STOSB, SCASB, and CMPS with the REP prefix. As I mentioned earlier, this allows you to (in effect) code a loop that runs entirely within the CPU, fetching no instructions and losing no time to unnecessary memory accesses. The string instructions can't be used to solve every problem, but when they can, the results will scorch your eyeballs.

Notice BlkBlast's innermost loop:

     DoChar: LODSB
      STOSW
      LOOP DoChar

Because the attribute must be "mixed" with the pattern on the way from storage to the virtual screen, this is one case where the REP prefix can't be used. However, the loop has been pared to its barest essentials: You load a byte from the pattern into AL (the attribute is already loaded into AH) and then store AX to the virtual screen as a character/attribute pair. The LODSB and STOSB instructions increment the SI and DI registers after each pass through the loop, relieving your own code from that burden.

From The Book CASE

If, like me, you have a fondness for taking your machine by the throat and shaking it, do not fail to obtain Ray Duncan's new quick reference guide from Microsoft Press, IBM ROM BIOS. I've used various sources of information on ROM BIOS over the years, starting with the original 1981 IBM PC Technical Reference, and most recently Peter Norton's still excellent Programmer's Guide to the IBM PC. Ray's new book is lean, lucid, and eminently thumbable. It summarizes all BIOS services and is current through the PS/2. Furthermore, it includes summaries of interrupt and I/O port usage on all machines through PS/2. My only gripe is that it doesn't summarize ROM BIOS memory usage, but that's a small matter. For $5.95, it's a must-have.

One of the multitude of little stupidities that has kept Microsoft from being richer than it is is their obsession with keeping Windows and PM development an affair for C programmers. I don't use an environment I can't program, and the C-flavored Windows API is about as organized and easy to grasp as a gauze curtain in a Chicago gale. Microsoft's insistence that Windows development is "not for beginners" insults me. The Windows API wastes my time, and I have very little patience with things like that. Until Whitewater Group's Actor language came along, Windows simply gathered dust on my disk.

If for whatever reason you must confront Windows development under C (shudder), you could do worse than pick up Introduction to Windows Programming by Guy Quedens and Pamela Beason. It's not the entire story by any means, but it's as good an entry point into the chaos of the Windows API as you're likely to find. You'd better know C before opening it, but if you do, the rest will come naturally. The treatment of the Windows GDI is particularly good, with plenty of screen shots and figures to make the technical prose gel. And even if you're using Actor, the background on the Windows environment is well worth reading.

OOPS!

One of my crustier correspondents sent me a letter asking (among other things) "Who needs OOPS?" I've been soaking my tail in object-oriented programming miscellany for the past couple of months, and my response is this: If you've ever said it, you need it.

We'll begin taking a look at your OOPS options in my next column. The best OOP tools have nothing to do with C. Nice thought, isn't it?

Availability

All source code for articles in this issue is available on a single disk. To order, send $14.95 (Calif. residents add sales tax) to Dr. Dobb's Journal, 501 Galveston Dr., Redwood City, CA 94063; or call 800-356-2002 (from inside Calif.) or 800-533-4372 (from outside Calif.). Please specify the issue number and format (MS-DOS, Macintosh, Kaypro).

Products Mentioned

Mastering Turbo Assembler by Tom Swan Howard Sams & Sons, 1989 ISBN 0672-48435-8 $24.95

Programmer's Quick Reference Series: IBM ROM BIOS by Ray Duncan Microsoft Press, 1988 ISBN 1-55615-135-7 $5.95

Introduction to Windows Programming by Guy Quedens and Pamela Beason Scott, Foresman & Company, 1988 ISBN 0-673-38058-0 $21.95 Listings disks (2) available from Guy Quedens for $19.95

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.


<a name="012e_0010"><a name="012e_0010">
<a name="012e_0011">
[LISTING Two]
<a name="012e_0011">

{--------------------------------------------------------------}
{                         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.


<a name="012e_0012"><a name="012e_0012">
<a name="012e_0013">
[LISTING Three]
<a name="012e_0013">

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 }




<a name="012e_0014"><a name="012e_0014">
<a name="012e_0015">
[LISTING Four]
<a name="012e_0015">

;===========================================================================
;
; 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




<a name="012e_0016"><a name="012e_0016">
<a name="012e_0017">
[LISTING Five]
<a name="012e_0017">


         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   'SunMonTueWedThuFriSat'
         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












Copyright © 1989, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.