



Who am I to argue with a cute, curly-haired orphan, but… Annie was wrong. "Tomorrow, tomorrow, I love ya, tomorrow, you're only a day away" she sang (and at the slightest provocation).
To those of us who have worked on systems that are time-sensitive, we know that "tomorrow" is only sometimes a day away. For example:
The code to handle all of this is not particularly obscure, and many of us have written it-over and over and over again.
From the earliest days on the Mac, we have had very good date and time manipulation routines available in the toolbox. Recently, the Script Manager incorporated some rather nifty text-parsing routines that it combines with new date routines to make everything transparent, whether you are in Japan or Egypt, and whether you are interested in this era or in one distant by several millennia.
I decided that once and for all I would take the toolbox routines and combine them into some MacApp objects that could be used (and overridden) for almost any purpose involving date manipulation. And thus was the UDates unit born.
From a user's point of view, the two most important objects in UDates are the TDateCluster and the TElapsedTimeCluster. Both are descendants of TCluster and are designed to be placed in TDialogViews.
Here's a step-by-step description of their behavior. Note that both are initialized to a "today" date which will be described later. In addition, assume that Saturday and Sunday are weekends, although UDates allows you to specify any weekend days that you want.
Now, as you might expect, the two boxes at the left are editable. In fact, they belong to a class called TDateEditText which is a descendant of TEditText. TDateEditText objects are basically TEditTexts but with the added functionality that their Validate methods expect the contents to be a date which is parsable by the Script Manager routines. If the date doesn't pass the Script Manager parsing, Validate fails and MacApp restores the previous value. The programmer can thus always assume that there's a valid date in a TDateEditText.
Finally, the TDateCluster provides the information as to whether the date values it returns are the result of user data entry or of clicking on the radio buttons. In some cases, the program is only interested in the start and stop dates shown in the TDateCluster. In other cases, it is important to know whether the user is after this week's data (regardless of date) or the data for 3/12 – 3/16 specifically.
Here's the interface to TDateCluster:
TDateCluster = OBJECT (TCluster)
fDateObj: TDateObj; {a TDateObj, probably set to today}
fFrom, fTo: TDateEditText; {private - use GetStartStop}
FUNCTION TDateCluster.GetStartStop(
VAR d1, d2: LongDateTime;
VAR rChoice: IDType): BOOLEAN;
PROCEDURE TDateCluster.IDateCluster(aDate: TDateObj);
PROCEDURE TDateCluster.DoChoice(
origView: TView;
itsChoice: INTEGER); OVERRIDE;
PROCEDURE TDateCluster.Fields(PROCEDURE DoToField(
fieldName: STR255;
feldAddr: Ptr;
fieldType: INTEGER)); OVERRIDE;
PROCEDURE TDateCluster.Free; OVERRIDE;
END;
Only IDateCluster and GetStartStop are normally used.
{IElapsedTimeCluster can handle a 0 for d2 (stop time) and/or duration. If duration is 0, it is calculated. If d2 (stop time) is 0, it is calculated using duration. You might want to do your own error-checking to make sure that you are passing in good values. TElapsedTimeCluster makes sure that all three fields are consistent: change fFrom or fTo, and fDuration is updated. Change fDuration and fTo is changed. (Yes, it could have been coded the other way, but it wasn't. If you want duration to count backwards from fTo and modify fFrom, modify the object.) GetStartStop gives you the start and stop times.}
Here is the interface to TElapsedTimeCluster:
TElapsedTimeCluster = OBJECT (TCluster)
fDateObj: TDateObj; {probably today}
fFrom, fTo: TValDateEditText; {private - use StartStop}
fDuration: TValEditText; {private - use GetStartStop}
PROCEDURE TElapsedTimeCluster.Free; OVERRIDE;
FUNCTION TElapsedTimeCluster.GetStartStop(
VAR d1,d2:LongDateTime):BOOLEAN;
PROCEDURE TElapsedTimeCluster.IElapsedTimeCluster(
aDate: TDateObj;
d1,d2: LongDateTime;
duration: comp;
aStyle: TextStyle);
FUNCTION TElapsedTimeCluster.Validate:LONGINT; OVERRIDE;
PROCEDURE TElapsedTimeCluster.Fields(PROCEDUREDoToField(
fieldName:STR255;
fieldAddr: Ptr;
FieldType:INTEGER)); OVERRIDE;
END;
Again, GetStartStop and IElapsedTimeCluster are likely to be the only methods which you'll call directly. Validate is called for you by TDialogView, but nothing prevents you from calling it yourself at some other time.
{The IEditText and IRes methods initialize all fields. You may want to subsequently reset fWantDate or fWantTime. Resetting fDidEdit is undefined (polite for "stupid"). fDate is obtainable in alternate formats by calling GetLongDateTime or GetLongDateRec. }
Here's the interface to TDateEditText:
TDateEditText = OBJECT (TEditText)
fWantDate, fWantTime, fDidEdit, fZeroBlank: BOOLEAN;
fDate: LongDateRec;
PROCEDURE TDateEditText.Fields(PROCEDURE DoToField(
fieldName:STR255;
fieldAddr: Ptr;
FieldType: INTEGER)); OVERRIDE;
FUNCTION TDateEditText.GetLongDateTime(
VAR aDate: LongDateTime):BOOLEAN;
FUNCTION TDateEditText.GetLongDateRec(
VAR aDateRec: LongDateRec): BOOLEAN;
PROCEDURE TDateEditText.IEditText(
itsSuperView: TView;
itsLocation, itsSize:VPoint;
itsMaxChars: INTEGER);
OVERRIDE;
PROCEDURE TDateEditText.IRes(
itsDocument: TDocument;
itsSuperView: TView;
VAR itsParams: Ptr); OVERRIDE;
PROCEDURE TDateEditText.SetDate(
aDate: LongDateTime;
reDraw: BOOLEAN);
FUNCTION TDateEditText.Validate: LONGINT; OVERRIDE;
END;
Once again, the methods shown in bold are the ones which you are likely to call directly. Note one point about IEditText: it does NOT set the initial value; you have to call SetDate. It is generally agreed that the IYourObject methods should leave all fields set to some value (e.g., handles to NIL if not actually allocated). In our recent projects we have tended to separate the setting of values from the initialization of the object. Thus, in a project that uses UDates, we have three methods that handle the fields:
Similar triplets of methods are used for other types of data entry fields. This works very nicely for cases where one view is used to show and update data from various database records.
The interface for TDateObj is not provided here, since it is fairly lengthy and is provided in the code which follows.
In addition, the TDateCluster and TElapsedTimeCluster are views that are editable in ViewEdit to allow a developer to use a specific application's standard fonts and graphic styles. TElapsedTimeCluster is about as sparse as you can get in terms of text, because in those cases where it's been used, we have always modified the resource to incorporate additional text fields.
The code which follows is for UDates itself as well as for DateSample, a small application which uses UDates and to show the results of various commands via messages in the Debug window.
The code is (I hope) fairly clear and well-annotated, so there's no point in going through it in detail. I will, however, mention a few points which may be of interest.
Whenever code is duplicated, that's a clue that it's in the wrong place and should be moved to a location where it's written once and done with. In UDates, not every variable is accessible to the outside world; providing Get… and Set… methods for all fields of an object is unnecessary (in my humble opinion). What is necessary is to decide which fields and which common transformations of them are likely to be necessary, and to provide those.
Normally, one has a choice of creating views either from templates or programmatically. In UDates, the clusters are designed to be created ONLY from templates-and in fact the appropriate ICluster methods are missing. This is deliberate and should be considered as an advertisement for ViewEdit. The TDateCluster contains eight subviews, each of which must be placed, sized, and identified properly in order for the TDateCluster to work. In addition, the six radio buttons must be named with appropriate base names-and no one would consider hard-coding words like "Today" or "Yesterday," so those would have to be stored in a string resource. The code for doing all of this initialization is about 50 lines long. In a case like this, I do not think that template and programmatic creation are equally appropriate: ViewEdit wins hands down in such a case (even with some of its bugs-which I'm sure will be gone shortly).



