Marking Time: Round 2

by Steve Parker

Published 1998-02-01    Printer-friendly version

Download the code here

In Clarion, "time" is an integer in the range of 1 to 8,640,000. Each increment represents 1/100 second and time is measured from midnight. 0 indicates no time was entered, 1 is midnight and 8,640,000 is 1/100 second before midnight.

In the first installment, we used this information to derive the following key equivalences:

1 second = 100

1 minute = 6000

and

1 hour = 360,000

and used these equivalences to create four functions to calculate:

  • the number of seconds between two times
  • the number of minutes between two times
  • the number of hours between two times
  • the elapsed time

In this installment we will derive four more functions from these three equivalences:

  1. pause program execution for a given amount of time
  2. when to initiate a timeout
  3. how much time remains before timeout
  4. whether or not time is up

Wait a Sec

There are a number of reasons why you might want to pause program execution (without making the application Modal). In a recent project, Bill Helgeson and I discovered that Clarion code sometimes executes so quickly that other system resources do not update quickly enough (we offered to slow certain parts of the program down, the parts not affecting what we were doing, but the customer ... demurred).

For example, when using ClarionCopy() across a network and then checking that the target file existed, we found that we had to wait a few seconds before we could get an accurate answer. Similarly, after issuing an operating system command to map network drives, again we had to wait a few seconds for the mapping to be recognized and reported correctly.

This led to the creation of a procedure that takes a number of seconds as a parameter. Its prototype is:

WaitSecs(UShort)

We pass in the number of seconds to wait, so the code has to convert this to Clarion time. That means that the number-of-seconds parameter needs to be multiplied by 100, per the equivalences above.

The function's code looks like:

WaitSecs   PROCEDURE (pInterval)
StartTime  LONG
EndTime    LONG
  CODE
  StartTime = CLOCK()
  EndTime = StartTime + (pInterval * 100)
  LOOP
    IF CLOCK() => EndTime
      RETURN
    END
  END

First we store the current system time.

StartTime = CLOCK() is not, strictly speaking, necessary. We could skip this and the associated variable. In this case, the calculation ofEndTime would be:

EndTime = CLOCK() + (pInterval * 100)

InitializingStartTime separately really only serves to improve readability and, when you come back to your code in a few months, makes it easier to figure out what you were up to when you wrote it.

Then we calculate the desired ending time (pInterval * 100).

Knowing our starting and targeted ending time, we simplyLoop until the current time equals or exceeds the desired ending time.

A word of warning

Testing under NT, I discovered that when I passed more than two seconds into this function, processor utilization shot up to 100%. It is, therefore, a safe inference that it behaves similarly in other operating environments.

Now, there is a way of putting a program to sleep without effecting a coup d'etat on your CPU. However, it is not completely general. That is, if called in a LIB (not a DLL), there is an extra step since it involves using a window.

To pause a program for longer periods, without monopolizing every CPU cycle available, we can take advantage of some Windows built-in functionalities.

TopSpeed provides us with theTimer attribute on a window. This attribute fires theAccept() loop at the defined interval (where 1 = 1/100 second, therefore, 100 = 1 second).

If we create a window with aTimer attribute of 100, it will fire once per second. If the desired number of seconds to wait is the parameter passed in, then all we need to do is count the number of times theAccept() loop has cycled and compare it to the parameter.

So, suppose we create a local variable

Cycles BYTE

to count the number of loops and another

Remaining BYTE

to display the number of seconds remaining in the sleep period (it's one thing to put a program to sleep, it's another entirely not to let the user know how long they need to twiddle their thumbs).

Then, inside theAccept() loop, we only need to count how many timesTimer has triggeredAccept(), calculate and display the nap time remaining. I used Case Event Handling, Before Generated code:

OF Event:Timer
  Cycles += 1
  Remaining = pSeconds - Cycles
  IF Cycles => pSeconds
    DO ProcedureReturn
  END

I created the window with just the minimal information

time2_1.gif

required to keep the user informed as to the status of the program (i.e., why it does not appear to be doing anything and when s/he can expect it to begin doing something again).

If you do not want the window to show, of course, you can set it to "open" minimized. And, with a second parameter, you can control the window's opening mode (normal or minimized) in code and, therefore, from a runtime variable.

This way of pausing a program typically uses 1-2% of the available cycles and I never saw it use more than 14% (and that was at the momentAccept() actually cycled).

Again, this is not completely general when the procedure is placed in a LIB. You will also have to include the resource file for the window in your project.

When is Timeout?

If you plan to timeout a user, it is helpful to know when to do it. This simple function takes the number of minutes of activity to allow and returns the time of day at which timeout will occur. Therefore, its prototype will be:

CalcEndTime(REAL),LONG

Why take a Real as a parameter? This allows passing fractions of a minute. It returns aLong because Clarion standard time is usually aLong (aLong is the smallest unit of storage that can store any Clarion time).

CalcEndTime FUNCTION (pTimeAllowed)
  CODE
  RETURN(CLOCK() + (pTimeAllowed * 6000) )

IfpTimeAllowed contains the number of permitted minutes, thenpTimeAllowed * 6000 converts it from minutes to Clarion standard time. Adding that toClock(), the current time, is the time at which timeout will occur.

A typical call would look like:

LOC:TimeOutTime = CalcEndTime(10)

If this function is called before theAccept loop begins, the user will have only the passed number of minutes and no more. If called within theAccept loop, it functions just like anIdle() call, recalculating each timeAccept cycles.

There is an important implication of the last point. If you have set theTimer attribute on a window and you calculate CalcEndTime() within theAccept loop, that value will be recalculated whether or not the user is active. SinceIdle() calculates based on user inactivity, an Idle procedure may be the better choice.

A Countdown Timer

The original reason for undertaking the creation of these functions was to be able to display the amount of time remaining until a user was timed out. Virtually all of the functions created here revolve around this display.

The prototype for this function is:

TimeRemaining(LONG),STRING

We pass the time of day at which timeout will occur and get back a string in the form of:

hh:mm:ss

We already have functions that will calculate the hours, minutes and seconds between two times. One of the times passed into the function will be the current time as given byClock() and the other will be the timeout time as previously calculated byCalcEndTime. So, this function will call the functions for elapsed hours, minutes and seconds and format the returned values as a single string.

TimeRemaining  FUNCTION (pTimeOutTime)
ReturnString   STRING(20)
Hours          LONG
Minutes        LONG
Seconds        LONG
  CODE
  IF pTimeOutTime
    Hours = CalcHours(CLOCK(),pTimeOutTime)
    Minutes = CalcMinutes(CLOCK(),pTimeOutTime)
    Seconds = CalcSeconds(CLOCK(),pTimeOutTime)
    IF Hours
      ReturnString = CLIP(Hours) & ':'
    END
    ReturnString = CLIP(ReturnString) & |
    CLIP(Minutes) & ':' & CLIP(Seconds)
  ELSE
    ReturnString = ''
  END
  RETURN(ReturnString)

TimeRemaining would typically be called inside theAccept loop and assigned it to a display variable. For example:

AppFrame{Prop:StatusText,3} = 'Time Remaining: ' & |
Clip (TimeRemaining(TimeOutTime))

Why do I allow the parameter to be empty (i.e., why do I check whetherpTimeOutTime has a non-zero value)? After all, the prototype indicates that the parameter is not optional.

Because the parameter is a numeric, it will always have a value, even if zero. So the function will always receive a value.

The problem addressed by this is the fact that a non-zero value may not have been calculated when the procedure was first called. That is, the first time through theAccept loop, pTimeOutTime may not yet have been computed. Until implementing theIf ... Else, the function returned a complete string, but with zeros for the minutes and seconds. I considered this undesirable.

Is It Time To Do Something?

The final function is used to determine whether the user is to be timed out.

Assuming that we know the time of day at which timeout is to occur, we need to test whether that time has come.

The prototype for this function is:

IsTimeUp(LONG),SHORT

and a typical call, inside theAccept loop, would look like:

IF IsTimeUp(TimeOutTime)
 !code or procedure to execute
END

The actual calculation is quite simple:

IsTimeUp   FUNCTION (pTimeOutTime)
  CODE
  IF pTimeOutTime > CLOCK() OR ~pTimeOutTime
    RETURN(False)
  ELSE
    RETURN(True)
  END

Why do I accept no value inpTimeOutTime (~pTimeOutTime)? I want to allow a configurable time value, and the system administrator may decide not to use a timeout. Even if timeout is being used, this function could be called beforeCalcEndTime() is called. In either of these cases,pTimeOutTime will be zero and, therefore, zero must be accepted as a legitimate value.

Midnight Rollover Revisited

Again, I did not provide for midnight rollover. My applications are not used in such a way that this can be a problem. However, ifWaitSecs is invoked just before midnight and the expiration of the period is after midnight,WaitSecs will never terminate (or, at least, won't for almost 24 hours). Also,IsTimeUp will immediately return True when it is before midnight and the timeout time is after.

A major revision of the logic inWaitSecs can adapt it to handle midnight rollovers (since we are only concerned with a period equal to the parameter passed toWaitSecs, hence, we could calculate it in two parts: that before and that after midnight):

WaitSecs   PROCEDURE (pInterval)
StartTime  LONG
EndTime    LONG
  CODE
  StartTime = CLOCK()
  EndTime = StartTime + (pInterval * 100)
  IF EndTime =< 8640000 !before midnight
    LOOP
      IF CLOCK() => EndTime
        RETURN
      END
    END
  ELSE
    EndTime -= 8640000 !decrement time
    LOOP
      IF CLOCK() =< 8640000
        CYCLE
      ELSIF CLOCK() => EndTime
        RETURN
      END
    END
  END

butIsTimeUp cannot, I think, be so "easily" revised.

If your apps can be active across midnight and the rollover, therefore, is an issue, Susan Alchesay's algorithm comes to the rescue.

Ms. Alchesay's algorithm "concatenates" the Clarion Standard Date (which is also aLong) with the Clarion Standard Time, storing the result in aReal. The Clarion Standard Date is used as the integer portion and the Clarion Standard Time as the decimal part (by dividing it by 8,640,000, resulting in a percentage of a day).

To fully implement this, the only additional thing needed would be a same-date check:

Days = INT(DateTime1) - INT(DateTime2)
IF Days
  !processing code
END

and, if this value is not zero, extraction of the decimal. Multiplying the decimal portion by 8,640,000 will restore the time-of-day (within rounding errors, i.e. a few thousandths).

Idle Procedures

What recommends these functions over using an Idle procedure which appears to serve similar purposes? Indeed, do these functions accomplish anything that you cannot accomplish with anIdle()?

In fact these functions do several little things that cannot be done with anIdle(). The primary thing that you cannot do in an Idle procedure is display the amount of time left until timeout.Idle() cannot give you the expected timeout time. Therefore, you can implement neither a countdown timer nor an elapsed time display from anIdle().

If you do want to display the amount of time until timeout, you also need to know the time of day at which that will occur. Again, you cannot get this value fromIdle().

Well, there are a few other things these functions can do that an Idle() cannot. An Idle procedure activates after the number of seconds, passed as the second parameter of theIdle() statement, of no keyboard or mouse activity. This means that execution of the Idle procedure is inherently relative. Action is always relative to the time of the last keystroke/mouse movement. The functions created here can be used to define either a relative timeout (by recalculating the trigger time inside theAccept() loop) or an absolute amount of time (i.e., allowing the user not more than, say, 7.25 minutes of use or whatever by calculating it before theAccept loop).

I suppose you could structure an Idle procedure to display a countdown timer or act on an absolute interval but, to do so, not only would you have to conform to the module requirements ofIdle() (an Idle procedure must be in the caller's module or the main module), you would have to use these functions anyway.

Thus, these functions supplement the capabilities ofIdle(). If all you want to do is to take some action if a user does nothing for a defined period of time, nothing is more straightforward or simple to implement than anIdle(). By the way,Idle() works with CWIC.

Summary

From two facts, that Clarion measures time in increments of 1/100 second and that midnight = 1, we can derive arithmetic equivalents for "second," "minute" and "hour." From these, we are able to derive a host of useful functions to measure elapsed times, calculate times and act on those calculations.

Amazing how far a little knowledge can go.

I have attached two sample applications (2003) demonstrating all the functions described in these articles, as well as copies of my C4 beta 5 .LIBs and .DLLs for your use (2003 versions were attached to last month's article).

Printer-friendly version