TimeParseImpl.mesa
Copyright Ó 1989, 1992 by Xerox Corporation. All rights reserved.
David Goldberg April 28, 1989 2:37:11 pm PDT
Chauser, April 3, 1991 11:48 am PST
Willie-s, May 5, 1992 2:09 pm PDT
DIRECTORY
Ascii USING [Digit, Letter, Lower],
BasicTime USING [daysPerMonth, DayOfWeek, earliestGMT, GMT, minutesPerHour, secondsPerMinute, MonthOfYear, Now, Pack, Unpack, Unpacked, Update],
Convert USING [IntFromRope, Error],
Rope USING [Fetch, Find, IsEmpty, Length, ROPE, SkipTo, Substr],
TimeParse;
TimeParseImpl: CEDAR PROGRAM
IMPORTS
Ascii, BasicTime, Convert, Rope
EXPORTS
TimeParse
= BEGIN OPEN TimeParse;
AmPm: TYPE = {am, pm};
shortMonths: ARRAY[0..11] OF Rope.ROPE;
months: ARRAY[0..11] OF Rope.ROPE = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
shortWeekdays: ARRAY[0..6] OF Rope.ROPE;
otherWeekdays: ARRAY[0..6] OF Rope.ROPE ¬ ALL[""];
weekdays: ARRAY[0..6] OF Rope.ROPE = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
ParseError: PUBLIC SIGNAL[errorType: ParseErrorType] = CODE;
MakeArrays: PROC[] = {
FOR i: INT IN [0..11] DO
shortMonths[i] ¬ Rope.Substr[months[i], 0, 3];
ENDLOOP;
FOR i: INT IN [0..6] DO
shortWeekdays[i] ¬ Rope.Substr[weekdays[i], 0, 3];
ENDLOOP;
otherWeekdays[1] ¬ "Tues";
otherWeekdays[3] ¬ "Thurs";
};
Parse scans str for a date and returns the time and a list of 'pieces' that specify where in the string the date appeared. Thus a calendar program (for example) could strip the date part out of the string
If date is ambiguous and direction = forward(backward), then times will be interpreted in the future(past) relative to now. If direction=heuristic, then something (hopefully logical) is done.
If insistTime is true, Parse assumes a time is specified somewhere in str.

Algorithm used by Parse:
Look for noon, midnight
else look for digit:digit: .
else Look for digit followed by pm, p.m., PM, P.M. am, a.m., AM, A.M.
else if there must be a time (insistTime = TRUE), look for number between 1 and 12
otherwise error
Heuristic: unqualified times < 8 are assumed to be pm, otherwise am (direction doesn't apply to time: should it?)
Look for yesterday, today, tomorrow. If so, done
Look for 19xx: If so this is the year
Look for mm/dd where 1 <= mm <= 12 and 1 <= dd <= 31
If no month, look for Jan, January, Feb, February, etc..... If so, this is the month.
Look for Mon, Monday, Tue, Tuesday, etc...: (tues, th, thur, ??) if so, this is the day
If no mm/dd, Look for digit or digitdigit in range between 1 and 31. if so, this is the day: If more than 1 such number, pick the one adjacent to another time element.
Finally, compute date (see below for this algorithm)
Parse: PUBLIC PROC[
str: Rope.ROPE,
now: BasicTime.GMT,
direction: DirectionType ¬ heuristic,
insistTime: BOOLEAN ¬ TRUE,
insistDay: BOOLEAN ¬ TRUE]
RETURNS [time: BasicTime.GMT, pieces: PiecesType ¬ NIL] = {
offset: INT;
hr: INT ¬ -1;
min: INT ¬ 0;
year, tmpYr: INT;
month: BasicTime.MonthOfYear;
weekday: BasicTime.DayOfWeek;
day, wday: INT ¬ -1;
past, maybe: BOOLEAN ¬ FALSE;
noAmPm: BOOLEAN ¬ FALSE;
unp: BasicTime.Unpacked;
Look for a.m. or p.m.
MatchAmPm: PROC [str: Rope.ROPE, k: INT ¬ 0]
RETURNS [found: BOOL, which: AmPm, start, stop: INT] = {
ln: INT;
ch: CHAR;
found ¬ FALSE;
ln ¬ Rope.Length[str];
WHILE k < Rope.Length[str] DO
start ¬ k ¬ Rope.SkipTo[str, k, "aApP"];
IF k < ln-1 AND (ch ¬ Rope.Fetch[str, k+1]) = '. THEN k ¬ k + 1;
IF k < ln-1 AND (ch ¬ Rope.Fetch[str, k+1]) = 'm OR ch = 'M THEN {
IF (ch ¬ Rope.Fetch[str, start]) = 'a OR ch = 'A THEN which ¬ am ELSE which ¬ pm;
found ¬ TRUE;
stop ¬ k+1;
IF Rope.Fetch[str, start+1] = '. THEN stop ¬ stop + 1;
RETURN;
};
k ¬ k + 1;
ENDLOOP;
};
look for mm/dd or mm/dd/yy. American convention, where month comes first
SlashFormat: PROC[] RETURNS [
month: BasicTime.MonthOfYear ¬ unspecified,
day, yr: INT ¬ -1
] = {
i, j, k, ln: INT;
tmpDay, tmpMonth: INT;
ln ¬ Rope.Length[str];
FOR i ¬ 0, Rope.Find[str, "/", i] UNTIL i = -1 DO
IF i > 0 AND Ascii.Digit[Rope.Fetch[str, i-1]] AND i < ln + 1 AND Ascii.Digit[Rope.Fetch[str, i+1]] THEN {
j ¬ i-1;
IF j > 0 AND Ascii.Digit[Rope.Fetch[str, j-1]] THEN j ¬ j - 1;
IF j > 0 AND AlNum[Rope.Fetch[str, j-1]] THEN {i ¬ i + 1; LOOP;};
k ¬ i + 1;
IF k < ln - 1 AND Ascii.Digit[Rope.Fetch[str, k+1]] THEN k ¬ k + 1;
IF k < ln-1 AND AlNum[Rope.Fetch[str, k+1]] THEN {i ¬ i + 1; LOOP;};
tmpDay ¬ Convert.IntFromRope[Rope.Substr[str, i+1, k - i]];
tmpMonth ¬ Convert.IntFromRope[Rope.Substr[str, j, i - j]];
IF tmpDay < 1 OR tmpDay > 31 OR tmpMonth < 1 OR tmpMonth > 12 THEN
{i ¬ i + 1; LOOP;};
day ¬ tmpDay;
month ¬ VAL[CARDINAL[tmpMonth-1]]; -- mm=1 means month is January
IF k < ln - 3 AND Rope.Fetch[str, k+1] = '/ AND Ascii.Digit[Rope.Fetch[str, k+2]] AND Ascii.Digit[Rope.Fetch[str, k+3]] THEN {
yr ¬ Convert.IntFromRope[Rope.Substr[str, k+2, 2]];
IF yr < 80 OR yr > 99 THEN SIGNAL ParseError[badYearInSlash];
yr ¬ 1900 + yr;
k ¬ k + 3;
};
pieces ¬ CONS[NEW[PieceType ¬ [j, k-j+1]], pieces];
RETURN;
}
ELSE i ¬ i + 1;
ENDLOOP;
};
look for things like 4:45, as well as optional trailing am/pm
MatchColonTime: PROC[] RETURNS [hr: INT ¬ -1, min: INT ¬ 0] = {
ln, i, j: INT;
ok: BOOLEAN;
start, stop: INT;
which: AmPm;
i ¬ 0;
ln ¬ Rope.Length[str];
WHILE (i ¬ Rope.Find[str, ":", i]) # -1 DO
IF i >= 1 AND Ascii.Digit[Rope.Fetch[str, i-1]] AND i < ln-2 AND Ascii.Digit[Rope.Fetch[str, i+1]] AND Ascii.Digit[Rope.Fetch[str, i+2]] THEN {
IF i >= 2 AND Ascii.Digit[Rope.Fetch[str, i-2]] THEN {
hr ¬ Convert.IntFromRope[Rope.Substr[str, i-2, 2]];
j ¬ i-2;
ln ¬ 5;
}
ELSE {
hr ¬ Convert.IntFromRope[Rope.Substr[str, i-1, 1]];
j ¬ i-1;
ln ¬ 4;
};
min ¬ Convert.IntFromRope[Rope.Substr[str, i+1, 2]];
pieces ¬ CONS[NEW[PieceType ¬ [j, ln]], pieces];
[ok, which, start, stop] ¬ MatchAmPm[str, i+3 - ln];
IF ok THEN {
pieces ¬ CONS[NEW[PieceType ¬ [start, stop - start + 1]], pieces];
IF hr = 12 THEN hr ¬ 0;
IF which = pm THEN hr ¬ hr + 12;
}
ELSE
noAmPm ¬ TRUE;
};
i ¬ i + 1;
ENDLOOP;
};
Next look for things like 4pm
MatchAmPmTime: PROC[] RETURNS [hr: INT ¬ -1] = {
ok: BOOLEAN;
which: AmPm;
i, start, stop: INT;
[ok, which, start, stop] ¬ MatchAmPm[str];
IF ok THEN {
i ¬ start-1;
WHILE i >= 0 AND Rope.Fetch[str,i] = ' DO i ¬ i - 1; ENDLOOP;
WHILE i >= 0 AND Ascii.Digit[Rope.Fetch[str,i]] DO i ¬ i - 1; ENDLOOP;
hr ¬ Convert.IntFromRope[Rope.Substr[str, i+1, (start-1) - (i+1) + 1]
! Convert.Error => {GOTO done;}];
IF hr >= 24 THEN hr ¬ -1
ELSE IF which = pm THEN hr ¬ hr + 12; --XXX: check to see if hr <= 12?
IF hr >= 0 THEN pieces ¬ CONS[NEW[PieceType ¬ [i+1, stop - i]], pieces];
EXITS
done => {};
};
};
look for word as a token in str
MatchWord: PROC[word: Rope.ROPE] RETURNS [BOOLEAN] = {
ln, k: INT;
ln ¬ Rope.Length[word];
k ¬ 0;
WHILE TRUE DO
k ¬ Rope.Find[str, word, k, FALSE];
IF k = -1 THEN RETURN[FALSE];
IF (k = 0 OR ~Ascii.Letter[Rope.Fetch[str, k-1]]) AND (k + ln = Rope.Length[str] OR ~Ascii.Letter[Rope.Fetch[str, k+ln]]) THEN {
pieces ¬ CONS[NEW[PieceType ¬ [k, ln]], pieces];
RETURN[TRUE];
};
k ¬ k + 1;
ENDLOOP;
RETURN[FALSE]; -- this is here to keep compiler happy
};
AlNum : PROC [ch: CHAR] RETURNS [BOOL]
= INLINE { RETURN [ch IN ['A..'Z] OR ch IN ['a..'z] OR ch IN ['0..'9]]; };
Search for 19xx
MatchYear: PROC[] RETURNS [year: INT] = {
k, ln: INT;
year ¬ -1;
k ¬ 0;
ln ¬ Rope.Length[str];
WHILE TRUE DO
k ¬ Rope.Find[str, "19", k, FALSE];
IF k = -1 THEN RETURN;
IF (k = 0 OR ~AlNum[Rope.Fetch[str, k-1]]) AND k < ln - 3 AND Ascii.Digit[Rope.Fetch[str, k+2]] AND Ascii.Digit[Rope.Fetch[str, k+3]] AND (k + 4 = ln OR ~Ascii.Letter[Rope.Fetch[str, k+4]]) THEN {
year ¬ Convert.IntFromRope[Rope.Substr[str, k, 4]];
pieces ¬ CONS[NEW[PieceType ¬ [k, 4]], pieces];
RETURN;
};
k ¬ k + 2;
ENDLOOP;
RETURN; -- this is here to keep compiler happy
};
MatchMonth: PROC[] RETURNS [mnth: BasicTime.MonthOfYear] = {
i: CARDINAL;
FOR i IN [0..11] DO
IF MatchWord[shortMonths[i]] THEN RETURN[VAL[i]];
ENDLOOP;
FOR i IN [0..11] DO
IF MatchWord[months[i]] THEN RETURN[VAL[i]];
ENDLOOP;
RETURN[unspecified];
};
MatchWeekday: PROC[] RETURNS [BasicTime.DayOfWeek] = {
i: CARDINAL;
FOR i IN [0..6] DO
IF MatchWord[shortWeekdays[i]] THEN RETURN[VAL[i]];
ENDLOOP;
FOR i IN [0..6] DO
IF MatchWord[weekdays[i]] THEN RETURN[VAL[i]];
ENDLOOP;
FOR i IN [0..6] DO
IF ~Rope.IsEmpty[otherWeekdays[i]] AND MatchWord[otherWeekdays[i]] THEN RETURN[VAL[i]];
ENDLOOP;
RETURN[unspecified];
};
if can't find colontime (e.g. 4:45) or number followed by am/pm (e.g. 2pm) look for a single number (like "go home at 4").
MatchTime: PROC[] RETURNS [INT] = {
k, k1, ln, cnt, val: INT;
ch: CHAR;
ln ¬ Rope.Length[str];
k ¬ 0;
WHILE TRUE DO
k1 ¬ k ¬ Rope.SkipTo[str, k, "0123456789"];
cnt ¬ 1;
IF k = ln THEN RETURN[-1];
IF k > 0 AND (AlNum[ch ¬ Rope.Fetch[str, k-1]] OR ch = ':) THEN {k ¬ k + 1; LOOP};
IF k < ln-1 AND Ascii.Digit[Rope.Fetch[str, k+1]] THEN {k ¬ k + 1; cnt ¬ 2;};
IF k = ln-1 OR (~AlNum[ch ¬ Rope.Fetch[str, k+1]] AND ch # ':) THEN {
val ¬ Convert.IntFromRope[Rope.Substr[str, k1, cnt]];
IF val >= 1 AND val <= 12 THEN {
pieces ¬ CONS[NEW[PieceType ¬ [k1, cnt]], pieces];
noAmPm ¬ TRUE;
RETURN[val];
};
};
k ¬ k + 1;
ENDLOOP;
RETURN[-1]; -- this is here to keep compiler happy
};
InPieces: PROC[i: INT] RETURNS [BOOLEAN] = {
plist: PiecesType;
plist ¬ pieces;
WHILE plist # NIL DO
IF i >= plist.first.start AND i < plist.first.start + plist.first.len THEN RETURN[TRUE];
plist ¬ plist.rest;
ENDLOOP;
RETURN[FALSE];
};
watch out for x:yy, in which case neither x nor yy is a date. However 9th and 23rd are dates
This is the one place where we check that the substr we are hunting for has not been already found and thus in pieces. This is because digits like 4 are ambiguous: "Sep 4" vs "Meet today at 4"
MatchDay: PROC[] RETURNS [INT] = {
k, k1, ln, cnt, val: INT;
ch: CHAR;
b: BOOLEAN ¬ FALSE;
return true for 1st, 2nd 3rd, nth
Th: PROC[k: INT] RETURNS [b: BOOLEAN] = {
IF k >= ln-2 THEN RETURN[FALSE];
SELECT Rope.Fetch[str, k] FROM
'1 => b ¬ Ascii.Lower[Rope.Fetch[str, k+1]] = 's AND Ascii.Lower[Rope.Fetch[str, k+2]] = 't;
'2 => b ¬ Ascii.Lower[Rope.Fetch[str, k+1]] = 'n AND Ascii.Lower[Rope.Fetch[str, k+2]] = 'd;
'3 => b ¬ Ascii.Lower[Rope.Fetch[str, k+1]] = 'r AND Ascii.Lower[Rope.Fetch[str, k+2]] = 'd;
ENDCASE =>b ¬ Ascii.Lower[Rope.Fetch[str, k+1]] = 't AND Ascii.Lower[Rope.Fetch[str, k+2]] # 'h;
RETURN[b AND (k+2 = ln-1 OR ~AlNum[Rope.Fetch[str,k+3]])];
};
ln ¬ Rope.Length[str];
k ¬ 0;
WHILE TRUE DO
k1 ¬ k ¬ Rope.SkipTo[str, k, "0123456789"];
cnt ¬ 1;
IF k = ln THEN RETURN[-1];
IF k > 0 AND (AlNum[ch ¬ Rope.Fetch[str, k-1]] OR ch = ':) THEN {k ¬ k + 1; LOOP};
IF InPieces[k] THEN {k ¬ k + 1; LOOP};
IF k < ln-1 AND Ascii.Digit[Rope.Fetch[str, k+1]] THEN {k ¬ k + 1; cnt ¬ 2;};
IF k = ln-1 OR (~AlNum[ch ¬ Rope.Fetch[str, k+1]] AND ch # ':) OR (b ¬ Th[k]) THEN {
val ¬ Convert.IntFromRope[Rope.Substr[str, k1, cnt]];
IF val >= 1 AND val <= 31 THEN {
IF b THEN {cnt ¬ cnt + 2;};
pieces ¬ CONS[NEW[PieceType ¬ [k1, cnt]], pieces];
RETURN[val];
};
};
k ¬ k + 1;
ENDLOOP;
RETURN[-1]; -- this is here to keep compiler happy
};
set time to hr:min, and then adjust by days
MyAdjust: PROC[time: BasicTime.GMT, min, hr: INT, days: INT] RETURNS [BasicTime.GMT] = {
unp: BasicTime.Unpacked;
unp ¬ BasicTime.Unpack[time];
unp.minute ¬ min;
unp.hour ¬ hr;
time ¬ BasicTime.Pack[unp];
RETURN[Adjust[days: days, baseTime: time, precisionOfResult: minutes].time];
};
MonthDistance: PROC[a, b: BasicTime.MonthOfYear] RETURNS [k: INT] = {
k ¬ 0;
WHILE a # b DO
MonthOfYear.Last isn't December, but rather is unspecified
IF a = December THEN a ¬ FIRST[BasicTime.MonthOfYear] ELSE a ¬ SUCC[a];
k ¬ k + 1;
ENDLOOP;
};
unp ¬ BasicTime.Unpack[now];
unp.second ¬ 0;
unp.dst ¬ unspecified; -- we want it to float to the correct value for the result time
Look for a time. First check for 'noon'
IF MatchWord["noon"] THEN hr ¬ 12
Next check for things like 4:45
ELSE [hr, min] ¬ MatchColonTime[];
Next look for things like 4pm
IF hr < 0 THEN hr ¬ MatchAmPmTime[];
Finally check for things like "go home at 4"
IF hr < 0 AND (hr ¬ MatchTime[]) # -1 THEN maybe ¬ TRUE;
IF hr = -1 THEN {
IF insistTime THEN SIGNAL ParseError[noTime] ELSE hr ¬ 0;
};
If there is a relative day (yesterday, today, tomorrow) then date is complete
IF MatchWord["today"] THEN {
IF noAmPm AND hr # 0 AND hr < 12 THEN {
IF direction # backward AND hr < unp.hour THEN hr ¬ hr + 12
ELSE IF direction = backward AND hr + 12 > unp.hour THEN NULL
ELSE IF hr < 8 THEN hr ¬ hr + 12;
};
time ¬ MyAdjust[now, min, hr, 0];
RETURN;
}
ELSE IF MatchWord["yesterday"] THEN {
IF noAmPm AND hr # 0 AND hr < 8 THEN hr ¬ hr + 12;
time ¬ MyAdjust[now, min, hr, -1];
RETURN;
}
ELSE IF MatchWord["tomorrow"] THEN {
IF noAmPm AND hr # 0 AND hr < 8 THEN hr ¬ hr + 12;
time ¬ MyAdjust[now, min, hr, 1];
RETURN;
};
check for a year, month, weekday, and day
year ¬ MatchYear[];
[month, day, tmpYr] ¬ SlashFormat[];
IF month = unspecified THEN month ¬ MatchMonth[];
IF tmpYr # -1 AND year # -1 THEN SIGNAL ParseError[twoYears];
IF year = -1 THEN year ¬ tmpYr;
weekday ¬ MatchWeekday[];
IF day = -1 THEN day ¬ MatchDay[];
If string was "sep 4", the 4 was interpreted as a time above (at the point that maybe ← TRUE). Check for this situation now
IF day = -1 AND maybe THEN {
IF ~insistTime THEN {
day ¬ hr;
hr ¬ 0;
}
ELSE {
duplicate error check here to get more intelligible error return (otherwise yearOrMonthButNoDay will be raised later).
IF (month # unspecified OR year # -1) AND insistDay THEN SIGNAL ParseError[noTime];
};
};
BasicTime.Now compute date. Algorithm is
if day and weekday unspecified then
if month or yr specified then error
if time < now then day ← today + 1 else day ← today
(unless direction=backward)
done
if day unspecified then
if month or yr specified then error
day is smallest such that (time, week) > now ( or < now if direction = backward)
done
if month unspecified then
if year specified then error
if (day, time) < today then month ← thismonth + 1;
(or thismonth -1 if direction=backward)
done
if year unspecified then
if direction=heuristic: if month >=11 months past now, assume last month
If weekday specified, check that it is consistent with date
BEGIN-- establish scope for EXITS clause
IF weekday = unspecified AND day = -1 THEN {
IF (month # unspecified OR year # -1) THEN {
IF insistDay THEN SIGNAL ParseError[yearOrMonthButNoDay] ELSE day ¬ 1;
}
ELSE {
IF noAmPm AND hr # 0 AND hr < 12 THEN {
IF direction # backward AND hr < unp.hour THEN hr ¬ hr + 12
ELSE IF direction = backward AND hr + 12 > unp.hour THEN NULL
ELSE IF hr < 8 THEN hr ¬ hr + 12;
};
past ¬ 60*hr + min < 60*unp.hour + unp.minute;
unp.minute ¬ min;
unp.hour ¬ hr;
time ¬ BasicTime.Pack[unp];
SELECT direction FROM
forward, heuristic =>
IF past THEN
time ¬ Adjust[baseTime: time, days: 1, precisionOfResult: minutes].time;
backward =>
IF ~past THEN
time ¬ Adjust[baseTime: time, days: -1, precisionOfResult: minutes].time;
ENDCASE => {};
GOTO out;
};
};
IF noAmPm THEN {
XXX: plausibility rule: unqaulified hr < 8 must mean pm.
IF hr < 8 AND hr # 0 THEN hr ¬ hr + 12;
};
IF day = -1 AND weekday # unspecified THEN {
IF month # unspecified OR year # -1 THEN SIGNAL ParseError[yearOrMonthButNoDay];
If it's now 2 pm Wed and somebody says 10:00 am Wed, do they really mean next wed?
BEGIN-- establish scope for GOTO
SELECT direction FROM
forward, heuristic =>
IF unp.weekday # weekday OR 60*hr + min < 60*unp.hour + unp.minute THEN
offset ¬ 1;
backward =>
IF unp.weekday # weekday OR 60*hr + min > 60*unp.hour + unp.minute THEN
offset ¬ -1;
ENDCASE => {GOTO nochange;};
WHILE unp.weekday # weekday DO
now ¬ Adjust[baseTime: now, days: offset, precisionOfResult: minutes].time;
unp ¬ BasicTime.Unpack[now];
ENDLOOP;
EXITS
nochange => {};
END;
unp.minute ¬ min;
unp.hour ¬ hr;
time ¬ BasicTime.Pack[unp];
GOTO out;
};
IF month = unspecified THEN {
IF year # -1 THEN SIGNAL ParseError[yearButNoMonth];
past ¬ day < unp.day OR (day = unp.day AND 60*hr + min < 60*unp.hour + unp.minute);
offset ¬ 0;
SELECT direction FROM
forward, heuristic =>
IF past THEN
offset ¬ 1;
backward =>
IF ~past THEN
offset ¬ -1;
ENDCASE => {};
IF offset = 0 THEN {
unp.minute ¬ min;
unp.hour ¬ hr;
unp.day ¬ day;
}
ELSE {
time ¬ Adjust[baseTime: now, months: offset, precisionOfResult: minutes].time;
unp ¬ BasicTime.Unpack[time];
unp.minute ¬ min;
unp.hour ¬ hr;
unp.day ¬ day;
};
time ¬ BasicTime.Pack[unp];
GOTO out;
};
IF year = -1 THEN {
offset ¬ MonthDistance[month, unp.month];
SELECT direction FROM
forward =>
time ¬ Adjust[baseTime: now, months: 12 - offset, precisionOfResult: minutes].time;
heuristic =>
IF offset <= 1 THEN
go back a month
time ¬ Adjust[baseTime: now, months: -offset, precisionOfResult: minutes].time
ELSE
go forward
time ¬ Adjust[baseTime: now, months: 12 - offset, precisionOfResult: minutes].time;
backward =>
time ¬ Adjust[baseTime: now, months: -offset, precisionOfResult: minutes].time
ENDCASE => {};
unp ¬ BasicTime.Unpack[time];
unp.minute ¬ min;
unp.hour ¬ hr;
unp.day ¬ day;
time ¬ BasicTime.Pack[unp];
GOTO out;
};
IF year # -1 THEN {
unp.minute ¬ min;
unp.hour ¬ hr;
unp.day ¬ day;
unp.month ¬ month;
unp.year ¬ year;
time ¬ BasicTime.Pack[unp];
GOTO out;
};
EXITS
out => {};
END;
IF weekday # unspecified THEN {
unp ¬ BasicTime.Unpack[time];
IF unp.weekday # weekday THEN SIGNAL ParseError[dayWeekdayMismatch];
};
};
TempusParse: PUBLIC PROC[
rope: Rope.ROPE,
baseTime: BasicTime.GMT,
search: BOOLEAN ¬ TRUE]
RETURNS [time: BasicTime.GMT, precision: Precision, start, length: NAT] = {
time ¬ Parse[rope, baseTime].time;
start ¬ length ¬ 0;
precision ¬ unspecified;
};
Adjust: PUBLIC PROCEDURE [
years: INT ¬ LAST[INT], 
months: INT ¬ LAST[INT], 
days: INT ¬ LAST[INT],  
hours: INT ¬ LAST[INT],
minutes: INT ¬ LAST[INT],
seconds: INT ¬ LAST[INT],
baseTime: BasicTime.GMT ¬ BasicTime.earliestGMT,
precisionOfResult: Precision ¬ unspecified]
RETURNS
[time: BasicTime.GMT, precision: Precision] = {
unpacked: BasicTime.Unpacked ¬ BasicTime.Unpack[IF baseTime = BasicTime.earliestGMT THEN BasicTime.Now[] ELSE baseTime];
IncrementYears: PROC[by: INT ¬ 1] = {
unpacked.year ¬ unpacked.year + by;
unpacked.day ¬ MIN[unpacked.day, DaysInMonth[]]; -- one year from February 29 is February 28
};
IncrementMonths: PROC [by: INT ¬ 1] = {
IF by >= 0 THEN FOR i: INT IN [0..by) DO
IF unpacked.month = December THEN {IncrementYears[]; unpacked.month ¬ January}
ELSE unpacked.month ¬ SUCC[unpacked.month];
REPEAT
FINISHED => unpacked.day ¬ MIN[unpacked.day, DaysInMonth[]]; -- if it is now March 31 and client says increment one month, that is April 30. If it is now January 29, 30 or 31 and client says increment one month, goes to February 28/29.
ENDLOOP
ELSE FOR i: INT IN [0..-by) DO
IF unpacked.month = January THEN {IncrementYears[-1]; unpacked.month ¬ December}
ELSE unpacked.month ¬ PRED[unpacked.month];
ENDLOOP;
};
DaysInMonth: PROC RETURNS[[0 .. BasicTime.daysPerMonth]] = {
RETURN[SELECT unpacked.month FROM
September, April, June, November => 30,
February => IF unpacked.year MOD 4 = 0 THEN 29 ELSE 28,
ENDCASE => 31];
};
IncrementDays: PROC [by: INT ¬ 1] = {
daysInMonth: [0 .. BasicTime.daysPerMonth];
daysInMonth ¬ DaysInMonth[];
IF by >= 0 THEN
FOR i: INT IN [0..by) DO
IF unpacked.day = daysInMonth THEN {
IncrementMonths[];
daysInMonth ¬ DaysInMonth[];
unpacked.day ¬ 1;
}
ELSE unpacked.day ¬ unpacked.day + 1;
ENDLOOP
ELSE
FOR i: INT IN [0..-by) DO
IF unpacked.day = 1 THEN {
IncrementMonths[-1];
daysInMonth ¬ DaysInMonth[];
unpacked.day ¬ daysInMonth;
}
ELSE unpacked.day ¬ unpacked.day - 1;
ENDLOOP;
};
IncrementHours: PROC [by: INT ¬ 1] = {
IF by >= 0 THEN FOR i: INT IN [0..by) DO
IF unpacked.hour = 23 THEN {IncrementDays[]; unpacked.hour ¬ 0}
ELSE unpacked.hour ¬ unpacked.hour + 1;
ENDLOOP
ELSE FOR i: INT IN [0..-by) DO
IF unpacked.hour = 0 THEN {IncrementDays[-1]; unpacked.hour ¬ 23}
ELSE unpacked.hour ¬ unpacked.hour - 1;
ENDLOOP;
};
IncrementMinutes: PROC [by: INT ¬ 1] = {
IF by >= 0 THEN FOR i: INT IN [0..by) DO
IF unpacked.minute = 59 THEN {IncrementHours[]; unpacked.minute ¬ 0}
ELSE unpacked.minute ¬ unpacked.minute + 1;
ENDLOOP
ELSE FOR i: INT IN [0..-by) DO
IF unpacked.minute = 0 THEN {IncrementHours[-1]; unpacked.minute ¬ 59}
ELSE unpacked.minute ¬ unpacked.minute - 1;
ENDLOOP;
};
IncrementSeconds: PROC [by: INT ¬ 1] = {
IF by >= 0 THEN FOR i: INT IN [0..by) DO
IF unpacked.second = 59 THEN {IncrementMinutes[]; unpacked.second ¬ 0}
ELSE unpacked.second ¬ unpacked.second + 1;
ENDLOOP
ELSE FOR i: INT IN [0..-by) DO
IF unpacked.second = 0 THEN {IncrementMinutes[-1]; unpacked.second ¬ 59}
ELSE unpacked.second ¬ unpacked.second - 1;
ENDLOOP;
};
IF years # LAST[INT] THEN {IncrementYears[years]; precision ¬ years};
IF months # LAST[INT] THEN {IncrementMonths[months]; precision ¬ months};
IF days # LAST[INT] THEN {IncrementDays[days]; precision ¬ days};
IF hours # LAST[INT] THEN {-- IncrementHours[hours]; -- precision ¬ hours};
IF minutes # LAST[INT] THEN {-- IncrementMinutes[minutes]; -- precision ¬ minutes};
IF seconds # LAST[INT] THEN {-- IncrementSeconds[seconds]; -- precision ¬ seconds};
IF precisionOfResult = unspecified THEN precisionOfResult ¬ precision;
IF precisionOfResult < seconds THEN unpacked.second ¬ 0;
IF precisionOfResult < minutes THEN unpacked.minute ¬ 0;
IF precisionOfResult < hours THEN unpacked.hour ¬ 0;
IF precisionOfResult < days THEN unpacked.day ¬ 1;
IF precisionOfResult < months THEN unpacked.month ¬ January;
unpacked.dst ¬ BasicTime.Unpack[BasicTime.Pack[unpacked]].dst; --set dst properly before actual packing
time ¬ BasicTime.Pack[unpacked];
The reason for the asymmetry between months/days and hours/minutes/seconds, i.e. the reason why don't use IncrementHours for hours, is because of daylight savings time. If the baseTime is midnight on Sunday of the day that daylight savings time changes, and you say increment 3 hours, I claim the user means 3 elapsed hours, i.e. 4AM. If you simply add 3 to unpacked.hour and call BasicTime.Pack, it will happily return 3AM, because that is a valid time.
IF hours # LAST[INT] THEN time ¬ BasicTime.Update[time, (hours * BasicTime.minutesPerHour * BasicTime.secondsPerMinute)];
IF minutes # LAST[INT] THEN time ¬ BasicTime.Update[time, minutes * BasicTime.secondsPerMinute];
IF seconds # LAST[INT] THEN time ¬ BasicTime.Update[time, seconds];
RETURN[time, precisionOfResult];
};
MakeArrays[];
END.