Contributor: EARL F. GLYNN            

UNIT Clocks;

 {This UNIT provides a CLOCK OBJECT for use in Turbo Pascal 5.5.

  (C) Copyright 1989, Earl F. Glynn, Overland Park, KS.  Compuserve 73257,3527.
  All Rights Reserved.  This Turbo Pascal 5.5 UNIT may be freely distributed
  for non-commerical use.

  Clock objects can be used as individual timers, using either the CMOS
  real-time clock, or the DOS real-time clock.  As shown in the ClkDemo
  PROGRAM, the DOS clock can be shut off when interrupts are disabled.
  The resolution of the CMOS clock is only 1 second, while the DOS clock
  has 0.0549 second resolution (18.203 ticks per second).  In addition
  to real-time clocks, static time stamps can be manipulated and
  formatted.  The range for all clocks and time stamps is Jan 1, 1900
  through Jun 5, 2079.  (Sep 18, 1989 is the midpoint of this range).

  Several REXX-like FUNCTIONs provide Date/Time formatting.  [REXX,
  the Restructured Extended Executor, or sometimes called the System Product
  Interpreter, is IBM's SAA command language (now primarily for VM/CMS).
  That is, REXX EXECs are CMS's equivalent of PC .BAT files but REXX
  provides much more functionality than the PC 'BAT' language.]

  REXX-like FUNCTIONS in Pascal could be considered an oxymoron since
  REXX doesn't have any concept of TYPEd variables and obviously Pascal does.
  The Pascal functions in most cases were written to return STRINGs,
  which is similar to REXX.  In some cases, where a number was returned
  that could be used in calculations, a separate function was used.  For
  example, the REXX TIME('Elapsed') function was implemented as an object
  'Elapsed' method that returns a REAL value to be used in calculations.
  A function 'hhmmss' can be used to format elapsed seconds in a
  character string, if desired.

  See the CLKDEMO.PAS, FLOPS.PAS and TIMER.PAS programs for sample usage
  of clock objects and this UNIT.}

INTERFACE

  TYPE
    ClockValue    =
      RECORD
        year      :  1900..2079;
        month     :  1..12;
        day       :  1..31;
        hour      :  0..23;
        minute    :  0..59;
        second    :  0..59;
        hundredth :  0..99;
      END;
    ClockType     =  (CMOSClock,DOSClock);
    Clock         =
      OBJECT
        mode      :  ClockType;
        StartValue:  ClockValue;
        FUNCTION  Date(s:  STRING):  STRING;
        FUNCTION  Elapsed:  REAL;   {elapsed timer (seconds)}
        PROCEDURE Start (ct:  ClockType);
        FUNCTION  Time(s:  STRING):  STRING;
      END;

  FUNCTION  DateFormat(s:  STRING; clk:  ClockValue):  STRING;
  FUNCTION  DaysThisCentury(y, m, d:  WORD):  WORD;
  FUNCTION  hhmmss(seconds:  REAL):  STRING;
  FUNCTION  JulianDate(y{1900..}, m{1..12}, d{1..31}:  WORD):  WORD;
  PROCEDURE SetClock (yr,mo,d,h,m,s,hth:  WORD; VAR t:  ClockValue);
  FUNCTION  TimeDiff(t2,t1:  ClockValue):  REAL;  {t2 - t1 seconds}
  FUNCTION  TimeFormat(s:  STRING; clk:  ClockValue):  STRING;
  PROCEDURE UnPackTime (TurboTime:  LongInt; VAR Clk:  ClockValue);

IMPLEMENTATION

  USES
    DOS; {INTR}

  VAR
    c  :  CHAR;

  FUNCTION L2C(L:  LONGINT):  STRING;  {LONGINT-to-character}
    {L2C and W2C are intended to be similar to the standard D2C
     (decimal-to-character) REXX function.}
    VAR t:  STRING[11];
  BEGIN
    STR (L,t);
    L2C := t
  END {L2C};

  FUNCTION W2C(w:  WORD):  STRING;     {word-to-character}
    VAR t:  STRING[5];
  BEGIN
    STR (w,t);
    W2C := t
  END {W2C};

  FUNCTION TwoDigits (w:  WORD):  STRING;
    CONST Digit:  ARRAY[0..9] OF CHAR = '0123456789';
  BEGIN
    w := w MOD 100;  {just to be safe}
    TwoDigits := Digit[w DIV 10] + Digit[w MOD 10]
  END {TwoDigits};

  FUNCTION DateFormat(s:  STRING; clk:  ClockValue):  STRING;
    CONST
      days  :  ARRAY[0..6] OF STRING[9]
                         =('Sunday','Monday','Tuesday','Wednesday',
                           'Thursday','Friday','Saturday');
      months:  ARRAY[1..12] OF STRING[9]
                         =('January','February','March',
                           'April',  'May',     'June',
                           'July',   'August',  'September',
                           'October','November','December');
  BEGIN
    IF   LENGTH(s) = 0
    THEN c := 'N' {NORMAL}
    ELSE c := UpCase(s[1]);
    CASE c OF
            {Normal (default):  dd Mmm yyyy -- no leading zero or blank}
      'N':  DateFormat := W2C(clk.day) + ' ' + COPY(months[clk.month],1,3)
                                       + ' ' + W2C(clk.year);

            {Century:  ddddd -- no leading zeros or blanks}
      'C':  DateFormat := W2C( DaysThisCentury(clk.year,clk.month,clk.day) );

            {Julian date:  ddd -- no leading 0s or blanks}
      'D':  DateFormat := W2C(JulianDate(clk.year,clk.month,clk.day));

            {European:  dd/mm/yy}
      'E':  DateFormat := TwoDigits(clk.day  )  + '/' +
              TwoDigits(clk.month)  + '/' + TwoDigits(clk.year MOD 100);

            {Month:  current month name in mixed case}
      'M':  DateFormat := months[clk.month];

            {Ordered:  yy/mm/dd suitable for sorting}
      'O':  DateFormat := TwoDigits(clk.year MOD 100)  + '/' +
              TwoDigits(clk.month)  + '/' + TwoDigits(clk.day);

            {Standard:  yyyymmdd -- suitable for sorting (ISO/R 2014-1971)}
      'S':  DateFormat := W2C(clk.year) + TwoDigits(clk.month) +
              TwoDigits(clk.day);

            {USA:  mm/dd/yy}
      'U':  DateFormat := TwoDigits(clk.month)  + '/' +
              TwoDigits(clk.day  )  + '/' + TwoDigits(clk.year MOD 100);

            {Weekday:  returns day of the week in mixed case}
      'W':  DateFormat :=  {January 1, 1900 was a Monday}
              days[DaysThisCentury(clk.year,clk.month,clk.day) MOD 7 ]

      ELSE DateFormat := ''
    END
  END {DateFormat};

  FUNCTION DaysThisCentury(y, m, d:  WORD):  WORD;

  {This function was written to be equivalent to the REXX language
   DATE('Century') function.  See DateFormat FUNCTION in this UNIT.

   Jan 1, 1900 = 1, Jan 2, 1900 = 2, ..., Jun 5, 2079 = 65535 (largest word).
   Jan 1, 1989 = 32508, Jan 1, 1990 = 32873, Sep 18, 1989 = 32768.

   "The Astronomical Almanac" defines the astronomical julian date
   to be the numbers of mean solar days since 4713 BC.  In this system
   Jan 1, 1900 = 2415020.5, Jan 1, 2000 = 2451544.5,
   Jan 1, 1989 = 2447527.5, Jan 1, 1990 = 2447892.5,
   Jun 5, 2079 = 2480554.5.  This data was used to validate the function.

   (Note:  DaysThisCentry(y,m,d) MOD 7  returns day-of-week index, i.e.,
   0=Sunday, 1=Monday, etc. since January 1, 1900 was a Monday.)}
  BEGIN
    DaysThisCentury := 365*(y-1900) + INTEGER(y-1901) DIV 4 + JulianDate(y,m,d)
  END {DaysThisCentury};

  FUNCTION  hhmmss(seconds:  REAL):  STRING;
    {Convert elapsed times/time differences to [hh:]mm:ss format}
    VAR
      h,h1,h2:  LONGINT;
      s      :  STRING;
      t      :  LONGINT;
  BEGIN
    IF   seconds < 0.0
    THEN BEGIN
      seconds := ABS(seconds);
      s := '-'
    END
    ELSE s:= '';
    h1 := 0;
    WHILE seconds > 2147483647.0 DO BEGIN  {fixup real-to-LONGINT problem}
      seconds := seconds - 1576800000.0;   {subtract about 50 years}
      h1 := h1 + 438000 {hours}            {add about 50 years}
    END;
    t := TRUNC(seconds);
    h2 := t DIV 3600;  {hours}
    h := h1 + h2;
    IF   h > 0
    THEN s := s + L2C(h) + ':';
    t := t - h2*3600;  {minutes and seconds left}
    hhmmss := s + TwoDigits(t DIV 60) + ':' + TwoDigits(t MOD 60)
  END {hhmmss};

  FUNCTION JulianDate(y{1900..}, m{1..12}, d{1..31}:  WORD):  WORD;
    CONST
      julian:  ARRAY[0..12] OF WORD =
               (0,31,59,90,120,151,181,212,243,273,304,334,365);
    VAR
      jd:  WORD;
  BEGIN
    jd := julian[m-1] + d;
    IF   (m > 2) AND (y MOD 4 = 0) AND
         (y <> 1900) {AND (y <> 2100)}
    THEN INC (jd);   {1900 and 2100 are not leap years; 2000 is}
    JulianDate := jd
  END {JulianDate};

  PROCEDURE SetClock (yr,mo,d,h,m,s,hth:  WORD; VAR t:  ClockValue);
  BEGIN
    t.year      := yr;
    t.month     := mo;
    t.day       := d;
    t.hour      := h;
    t.minute    := m;
    t.second    := s;
    t.hundredth := hth
  END {SetClock};

  FUNCTION  TimeDiff(t2,t1:  ClockValue):  REAL;
  BEGIN  {REAL arithmetic is used to avoid INTEGER/LONGINT overflows}
    TimeDiff :=   0.01*INTEGER(t2.hundredth - t1.hundredth) +
                       INTEGER(t2.second - t1.second      ) +
                  60.0*INTEGER(t2.minute - t1.minute      ) +
                3600.0*INTEGER(t2.hour   - t1.hour        ) +
               86400.0*LONGINT(DaysThisCentury(t2.year,t2.month,t2.day) -
                       LONGINT(DaysThisCentury(t1.year,t1.month,t1.day)))
  END {TimeDiff};

  FUNCTION  TimeFormat(s:  STRING; clk:  ClockValue):  STRING;
    VAR
      meridian:  STRING[2];
  BEGIN
    IF   LENGTH(s) = 0
    THEN c := 'N' {NORMAL}
    ELSE c := UpCase(s[1]);
    CASE c OF

            {Normal (default):  hh:mm:ss}
      'N':  TimeFormat := TwoDigits(clk.hour  )  + ':' +
              TwoDigits(clk.minute)  + ':' + TwoDigits(clk.second);

            {Civil:  hh:mxx, for example:  11:59pm}
      'C':  BEGIN
              IF   clk.hour < 12
              THEN BEGIN
                meridian := 'am';  {anti meridiem}
                IF   clk.hour = 0
                THEN clk.hour := 12;  {12:00am is midnight}
              END                     {12:00pm is noon}
              ELSE BEGIN
                meridian := 'pm';  {post meridiem}
                IF   clk.hour > 12
                THEN clk.hour := clk.hour - 12
              END;
              TimeFormat := W2C(clk.hour)  + ':' +
                TwoDigits(clk.minute)  + meridian
            END;

            {Hours:  hh -- number of hours since midnight}
      'H':  TimeFormat := W2C(clk.hour);

            {Long:  hh.mm:ss.xx (real REXX requires microseconds here)}
      'L':  TimeFormat := TwoDigits(clk.hour  )  + ':' +
              TwoDigits(clk.minute)  + ':' + TwoDigits(clk.second)  + '.' +
              TwoDigits(clk.hundredth);

            {Minutes:  mmmm -- number of minutes since midnight}
      'M':  TimeFormat := W2C(60*clk.hour + clk.minute);

            {Seconds:  sssss -- number of seconds since midnight}
      'S':  TimeFormat := L2C( 3600*LONGINT(clk.hour)
               + 60*LONGINT(clk.minute) + LONGINT(clk.second) )

      ELSE TimeFormat := ''
    END
  END {TimeFormat};

  PROCEDURE UnPackTime (TurboTime:  LongInt; VAR Clk:  ClockValue);
    {The DOS.DateTime TYPE does not have hundredths of a second in its
     definition.  Clocks.UnPackTime allows the use of Clocks.DateFormat
     and Clocks.TimeFormat with time stamps, especially with SearchRec
     TYPEed variables defined by FindFirst/FindNext.}
    VAR
      DT:  DateTime;
  BEGIN
    DOS.UnPackTime (TurboTime, DT);
    SetClock (DT.year,DT.month,DT.day,DT.hour,DT.min,DT.sec,0, Clk)
  END {UnPackTime};

  PROCEDURE GetDateTime (VAR c:  ClockValue; ct:  ClockType);
    VAR r1,r2:  Registers;

    FUNCTION BCD (k:  BYTE):  WORD;    {convert binary-coded decimal}
    BEGIN
      BCD := 10*(k DIV 16) + (k MOD 16)
    END {BCD};

  BEGIN
    CASE ct OF
      CMOSClock:
        BEGIN
          r1.AH := $04;
          INTR ($1A,r1);      {BIOS call:  read date from real-time clock}
          r2.AH := $02;
          Intr ($1A,r2);      {BIOS call:  read real-time clock}
          SetClock (100*BCD(r1.CH) + BCD(r1.CL) {yr},
                    BCD(r1.DH) {mo}, BCD(r1.DL) {day},
                    BCD(r2.CH) {h},  BCD(r2.CL) {m}, BCD(r2.DH) {s},
                    0 {.00}, c)
        END;
      DOSClock:
        BEGIN
          r1.AH := $2A;       {could use GetDate and GetTime from DOS UNIT}
          INTR ($21,r1);      {DOS call:  get system date}
          r2.AH := $2C;
          Intr ($21,r2);      {DOS call:  get system time}
          SetClock (r1.CX,r1.DH,r1.DL, r2.CH,r2.CL,r2.DH,r2.DL, c)
        END
    END
  END {GetDateTime};

  FUNCTION Clock.Date(s:  STRING):  STRING;
  BEGIN
    Date := DateFormat(s,StartValue)
  END {Date};

  FUNCTION  Clock.Elapsed:  REAL;
    VAR now:  ClockValue;
  BEGIN
    GetDateTime (now,mode);
    Elapsed := TimeDiff(now,StartValue)
  END {Clock.Elapsed};

  PROCEDURE Clock.Start (ct:  ClockType);
  BEGIN
    mode := ct;
    GetDateTime (StartValue, ct)
  END {Clock.Start};

  FUNCTION Clock.Time(s:  STRING):  STRING;
  BEGIN
    Time := TimeFormat(s,StartValue)
  END {Time};

END {Clocks}.

{---------------------------  DEMO --------------------------}

PROGRAM ClkDemo;

 {This PROGRAM demonstates how to use the CLOCKS UNIT, including a
  clock object, its methods, and related FUNCTIONs and PROCEDUREs.
  Differences between CMOS and DOS clocks are shown.

  (C) Copyright 1989, Earl F. Glynn, Overland Park, KS.  Compuserve 73257,3527.
  All Rights Reserved.  This Turbo Pascal 5.5 PROGRAM may be freely distributed
  for non-commerical use.

  Several of the examples were derived from "The REXX Language" by
  M.F. Cowlishaw, Prentice Hall, 1985.}

  USES
    CRT,
    Clocks,
    DOS;    {FindFirst,FindNext,SearchRec,AnyFile,DOSError}

  VAR
    Clk1,Clk2,Clk3:  Clock;       {clock objects -- real time clocks}
    stamp1,stamp2 :  ClockValue;  {static clocks -- time stamps}
    stamp3,stamp4 :  ClockValue;
    stamp5        :  ClockValue;
    DirInfo       :  SearchRec;

  PROCEDURE ShowClocks;
  BEGIN
    Clk2.Start (CMOSClock);
    Clk3.Start (DOSClock);
    WRITELN ('  CMOS Clock:  ',Clk2.date('u'),' ',Clk2.time('N') );
    WRITELN ('   DOS Clock:  ',Clk3.date('u'),' ',Clk3.time('L') );
    WRITELN ('  Difference:  ',TimeDiff(Clk2.StartValue,Clk3.StartValue):8:2,
             ' second(s)');
  END {ShowClocks};

  PROCEDURE DisableInterrupts;
    INLINE ($FA);

  PROCEDURE EnableInterrupts;
    INLINE ($FB);

  PROCEDURE KillTime;
    {The following could be used for a 5-second delay, but it re-enables
     interrupts when they are disabled:

        WHILE clk1.elapsed < 5.0 DO (* nothing *);

     So,time will be wasted with a few calculations.}

    VAR
      i:  WORD;
      x:  REAL;
  BEGIN
    WRITELN ('''Kill'' some time ...');
    FOR i := 1 TO 10000 DO
      x := SQRT(i)
  END;

BEGIN
  Clk1.Start (CMOSClock);
  WRITELN ('CMOS/DOS Clock Differences');
  WRITELN ('--------------------------');
  WRITELN ('Start Clocks');
  ShowClocks;
  KillTime;
  ShowClocks;
  WRITELN ('Disable Interrupts (DOS clock will stop):');
  DisableInterrupts;
  KillTime;
  ShowClocks;
  WRITELN ('Enable Interrupts');
  EnableInterrupts;

  SetClock (1985,8,27, 16,54,22, 12, stamp1);  {These are not real-time clocks.}
  SetClock (1900,1, 1,  0, 0, 0,  0, stamp2);
  SetClock (2079,6, 5, 23,59,59, 99, stamp3);

  WRITELN ('Cowlishaw''s':52);
  WRITELN ('now':39,'REXX Book':13,'First':13,'Last':13);
  WRITELN ('Date/DateFormat Examples');
  WRITELN ('------------------------');
  WRITELN ('day this century - C':26,Clk2.Date('Century'):13,
    DateFormat('C',stamp1):13, DateFormat('C',stamp2):13,
    DateFormat('C',stamp3):13);
  WRITELN ('day this year - D':26,   Clk2.Date('Days'):13,
    DateFormat('D',stamp1):13, DateFormat('D',stamp2):13,
    DateFormat('D',stamp3):13);
  WRITELN ('dd/mm/yy - E':26,        Clk2.Date('European'):13,
    DateFormat('E',stamp1):13, DateFormat('E',stamp2):13,
    DateFormat('E',stamp3):13);
  WRITELN ('month name - M':26,      Clk2.Date('MONTH'):13,
    DateFormat('M',stamp1):13, DateFormat('M',stamp2):13,
    DateFormat('M',stamp3):13);
  WRITELN ('dd Mmm yyyy - N':26,     Clk2.Date('normal'):13,
    DateFormat('N',stamp1):13, DateFormat('N',stamp2):13,
    DateFormat('N',stamp3):13);
  WRITELN ('yy/mm/dd - O':26,        Clk2.Date('Ordered'):13,
     DateFormat('O',stamp1):13,DateFormat('O',stamp2):13,
     DateFormat('O',stamp3):13);
  WRITELN ('yyyymmdd - S':26,        Clk2.Date('standard'):13,
    DateFormat('S',stamp1):13, DateFormat('S',stamp2):13,
    DateFormat('S',stamp3):13);
  WRITELN ('mm/dd/yy - U':26,        Clk2.Date('USA'):13,
    DateFormat('U',stamp1):13, DateFormat('U',stamp2):13,
    DateFormat('U',stamp3):13);
  WRITELN ('day of week - W':26,     Clk2.Date('weekday'):13,
    DateFormat('W',stamp1):13, DateFormat('W',stamp2):13,
    DateFormat('W',stamp3):13);

  WRITELN;
  WRITELN ('Time/TimeFormat Examples');
  WRITELN ('------------------------');
  WRITELN ('hh:mmxm - C':26,             Clk2.Time('Civil'):13,
    TimeFormat('C',stamp1):13, TimeFormat('C',stamp2):13,
    TimeFormat('C',stamp3):13);
  WRITELN ('hours since midnight - H':26,Clk2.Time('Hours'):13,
    TimeFormat('h',stamp1):13, TimeFormat('h',stamp2):13,
    TimeFormat('h',stamp3):13);
  WRITELN ('hh:mm:ss.xx - L':26,         Clk2.Time('long'):13,
    TimeFormat('L',stamp1):13, TimeFormat('L',stamp2):13,
    TimeFormat('L',stamp3):13);
  WRITELN ('minutes since midnight - M', Clk2.Time('minutes'):13,
    TimeFormat('m',stamp1):13, TimeFormat('m',stamp2):13,
    TimeFormat('m',stamp3):13);
  WRITELN ('hh:mm:ss - N':26,            Clk2.Time('normal'):13,
    TimeFormat('n',stamp1):13, TimeFormat('n',stamp2):13,
    TimeFormat('n',stamp3):13);
  WRITELN ('seconds since midnight - S', Clk2.Time('seconds'):13,
    TimeFormat('s',stamp1):13, TimeFormat('s',stamp2):13,
    TimeFormat('s',stamp3):13);

  WRITELN;
  WRITELN ('Time Differences/Elapsed Time');
  WRITELN ('-----------------------------');
  WRITELN (' ':20,'seconds':12,'hh:mm:ss':16);
  WRITELN ('CMOS - DOS Clock:':20,
    TimeDiff(Clk2.StartValue,Clk3.StartValue):12:2,
    hhmmss(TimeDiff(Clk2.StartValue,Clk3.StartValue)):16);
  SetClock (1989,1, 1,  0, 0, 0,  0, stamp4);
  SetClock (1990,1, 1,  0, 0, 0,  0, stamp5);
  WRITELN ('Jan 1-Dec 31 1989:':20,TimeDiff(stamp5,stamp4):12:0,
    hhmmss(TimeDiff(stamp5,stamp4)):16);
  WRITELN ('Dec 31-Jan 1 1989:':20,TimeDiff(stamp4,stamp5):12:0,
    hhmmss(TimeDiff(stamp4,stamp5)):16);
  SetClock (1992,1, 1,  0, 0, 0,  0, stamp4);
  SetClock (1993,1, 1,  0, 0, 0,  0, stamp5);
  WRITELN ('1992 (leap year):':20,TimeDiff(stamp5,stamp4):12:0,
    hhmmss(TimeDiff(stamp5,stamp4)):16);
  SetClock (2000,1, 1,  0, 0, 0,  0, stamp5);
  WRITELN ('20th century:':20,TimeDiff(stamp5,stamp2):12:0,
    hhmmss(TimeDiff(stamp5,stamp2)):16,' (100*365 days + 24 leap days)');
  WRITELN ('Maximum Clock Range:':20,TimeDiff(stamp3,stamp2):12:0,
    hhmmss(TimeDiff(stamp3,stamp2)):16,' (January 1, 1900 midnight -');
  WRITELN ('June 5, 2079 23:59:59.99)':78);
  WRITELN ('Elapsed time:':20,Clk1.Elapsed:12:0,
    hhmmss(Clk1.Elapsed):16);

  Readkey;
  WRITELN;
  WRITELN ('Clocks.UnPackTime');
  WRITELN ('-----------------');
  FindFirst ('*.*',AnyFile,DirInfo);
  WHILE DOSError = 0 DO BEGIN  {Note:  seconds on files are even numbers}
    Clocks.UnPackTime (DirInfo.Time, stamp5);
    WRITELN (DirInfo.Name:12,'  ',DirInfo.size:7,'  ',
      COPY(DateFormat('Weekday',stamp5),1,3),' ',
      DateFormat('USA',stamp5),' ',TimeFormat('Normal',stamp5));
    FindNext (DirInfo)
  END;
  Readkey;
END {ClkDemo}.