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