TempusImpl.mesa
Copyright © 1985 by Xerox Corporation. All rights reserved.
Rick Beach, April 4, 1985 8:16:16 am PST
Last Edited by: Pier, May 16, 1986 5:06:24 pm PDT bug
DIRECTORY
BasicTime USING [DayOfWeek, daysPerMonth, daysPerYear, earliestGMT, hoursPerDay, latestGMT, minutesPerHour, MonthOfYear, Now, Pack, secondsPerMinute, Unpack, Unpacked, Update],
Convert USING [Error, IntFromRope, RopeFromInt],
IO USING [RIS, Backup, Error, GetIndex, GetRopeLiteral, EndOfStream, BreakProc, GetTokenRope, STREAM],
Rope USING [ROPE, Equal, Concat, Find, Substr, Cat, Length],
SafeStorage USING [NarrowFault],
Tempus USING [ErrorCode, Packed, Precision];
TempusImpl: CEDAR PROGRAM
IMPORTS BasicTime, Convert, IO, Rope, SafeStorage
EXPORTS Tempus
= BEGIN OPEN BasicTime;
Types and Values
ROPE: TYPE = Rope.ROPE;
ErrorCode: TYPE ~ Tempus.ErrorCode;
Packed: TYPE ~ Tempus.Packed;
Precision: TYPE ~ Tempus.Precision;
Error: PUBLIC ERROR [ec: ErrorCode] = CODE;
Unintelligible: PUBLIC ERROR [rope: ROPE, vicinity: INT ← -1, ec: ErrorCode] = CODE;
defaultTime: PUBLIC Packed ← BasicTime.earliestGMT;
"Smart" Procedures
SmartPack: PUBLIC PROCEDURE [
year: [0..2050] ← 0, 
month: MonthOfYear ← unspecified, 
day: [0..daysPerMonth] ← 0,  
hour: [0..hoursPerDay] ← hoursPerDay,
minute: [0..minutesPerHour] ← minutesPerHour,
second: [0..secondsPerMinute] ← secondsPerMinute,
zone: INTLAST[INT],
secondsThisYear: INTLAST[INT],
daysThisYear: [0..daysPerYear] ← daysPerYear,
weekday: DayOfWeek ← unspecified,  
baseTime: Packed ← defaultTime]
RETURNS
[time: Packed, precision: Precision] = {
now, desired: Unpacked ← BasicTime.Unpack[IF baseTime = defaultTime THEN BasicTime.Now[] ELSE baseTime];
trueHour: [0..hoursPerDay] ← 24;
Increment: PROC = { -- increments the appropriate quantity, checks for rollover
IF year # 0 THEN NULL -- if user says April 27, 1982. allow this.
ELSE IF weekday # unspecified THEN {
IF month # unspecified OR day # 0 THEN Error[overConstrained]; -- e.g. user said Friday, April 7, 4PM and it is now 5PM. April 7 of following year would not be a Friday.
THROUGH [0..7) DO
IncrementDay[];
ENDLOOP;
}
ELSE IF month # unspecified THEN IncrementYear[]
ELSE IF day # 0 THEN IncrementMonth[]
ELSE IF hour # hoursPerDay THEN IncrementDay[]
ELSE IF minute # minutesPerHour THEN IncrementHour[]
ELSE ERROR;
};
IncrementYear: PROC = {
desired.year ← desired.year + 1;
};
IncrementMonth: PROC = {
desired.month ← SUCC[desired.month];
IF desired.month > December THEN {
IncrementYear[];
desired.month ← January;
};
};
IncrementDay: PROC = {
IF desired.day < (SELECT desired.month FROM
September, April, June, November => 30,
February => IF desired.year MOD 4 = 0 THEN 29 ELSE 28,
ENDCASE => 31) THEN desired.day ← desired.day + 1
ELSE {
IncrementMonth[];
desired.day ← 1;
};
};
IncrementHour: PROC = {
desired.hour ← desired.hour + 1;
IF desired.hour = hoursPerDay THEN {
IncrementDay[];
desired.hour ← 0;
};
};
IncrementMinute: PROC = {
desired.minute ← desired.minute + 1;
IF desired.minute = minutesPerHour THEN {
IncrementHour[];
desired.minute ← 0;
};
};
precision ← unspecified;
IF year # 0 THEN {
desired.year ← year;
precision ← years;
};
IF month # unspecified THEN {
IF precision = unspecified AND month < desired.month THEN Increment[]; -- e.g. user said March and it is now June. Note that if user said April, we may still have to increment the year later if he specified a day before the current day.
desired.month ← month;
precision ← months;
}
ELSE IF precision # unspecified THEN desired.month ← January; -- e.g. user just said 1984
IF day # 0 THEN {
SELECT precision FROM
years => Error[tooVague];
months => IF desired.month = now.month AND (day < desired.day) THEN Increment[]; -- e.g. user said April 3 and it is now April 10.;
unspecified => IF day < desired.day THEN Increment[]; -- e.g. user said the 3rd and it is now the 10th.
ENDCASE => ERROR;
desired.day ← day;
precision ← days;
}
ELSE IF precision # unspecified THEN {
IF desired.month = now.month THEN Increment[]; -- user said April (no date) and it is now already April.
desired.day ← 1; -- e.g. user just said June
};
trueHour ← desired.hour; -- save actual hour
desired.hour ← 2; -- we haven't set a time yet, but force it to 2:00 to avoid rolling back date on daylight savings time due to Pack/Unpack
desired.dst ← BasicTime.Unpack[BasicTime.Pack[desired]].dst;
IF weekday # unspecified THEN {
ComputeDay: PROC [currentWeekDay: DayOfWeek] = {
t: Unpacked ← BasicTime.Unpack[BasicTime.Pack[desired]];
n: INTEGER;
SELECT TRUE FROM
weekday > currentWeekDay => n ← LOOPHOLE[weekday, INTEGER] - LOOPHOLE[currentWeekDay, INTEGER];
weekday = currentWeekDay => n ← 0; -- this day is acceptable
weekday < currentWeekDay => n ← 7 - (LOOPHOLE[currentWeekDay, INTEGER] - LOOPHOLE[weekday, INTEGER]);
ENDCASE => ERROR;
THROUGH [0..n) DO
IncrementDay[]; -- this is not a case of incrementing because of rollover, but simply wanting to add 1 to the day
ENDLOOP;
};
desired.hour ← 2; -- avoid the daylight savings shift into the previous day!!!
SELECT precision FROM
years => Error[tooVague];
months => { -- means such day in corresponding month
t: Unpacked ← BasicTime.Unpack[BasicTime.Pack[desired]];
ComputeDay[t.weekday];
};
unspecified => {
ComputeDay[desired.weekday];
};
days => {
time ← BasicTime.Pack[desired];
desired ← BasicTime.Unpack[time];
IF desired.weekday # weekday THEN Error[overConstrained]; -- user said Friday, April 3, but April 3 is not Friday.
};
ENDCASE => ERROR;
precision ← days;
};
desired.hour ← trueHour; -- restore actual hour for remaining calculations
IF hour # hoursPerDay THEN {
SELECT precision FROM
years, months => Error[tooVague];
days => IF desired.day = now.day AND hour < desired.hour AND desired.month = now.month THEN Increment[];
e.g. if it is now 4PM on the 15th, and user said the 15th at 2PM, will increment month
if it is now 4PM on April 15th, and user said April 15th at 2PM, will increment year
unspecified => IF hour < desired.hour THEN Increment[]; -- will increment day
ENDCASE => ERROR;
desired.hour ← hour;
precision ← hours;
}
ELSE IF precision # unspecified THEN {
IF desired.day = now.day AND desired.month = now.month THEN Increment[]; -- user said April 27 (no hour) and it is now already April 27, i.e. April 27, 12AM has passed.
desired.hour ← 0;
};
IF minute # minutesPerHour THEN {
SELECT precision FROM
years, months, days => Error[tooVague];
hours => IF desired.hour = now.hour AND minute < desired.minute AND desired.day = now.day AND desired.month = now.month THEN Increment[];
if it is now 4:30PM, and user said 4 PM, increments day,
if it is now 4:30PM on the 15th, and user said the 15th at 4 PM, increments month
if it is now 4:30PM on April 15th, and user said April 15th at 4PM, increments year
unspecified => IF minute < desired.minute THEN Increment[];
ENDCASE => ERROR;
desired.minute ← minute;
precision ← minutes;
}
ELSE IF precision # unspecified THEN {
IF desired.hour = now.hour AND desired.day = now.day AND desired.month = now.month THEN Increment[]; -- user said April 27, 11AM and it is now April 27, 11:30.
desired.minute ← 0;
};
IF second # secondsPerMinute THEN {
SELECT precision FROM
years, months, days, hours => Error[tooVague];
minutes => IF desired.minute = now.minute AND second < desired.second AND desired.hour = now.hour AND desired.day = now.day AND desired.month = now.month THEN Increment[];
unspecified => IF second < desired.second THEN Increment[];
ENDCASE => ERROR;
desired.second ← second;
precision ← seconds;
}
ELSE IF precision # unspecified THEN {
IF desired.minute = now.minute AND desired.hour = now.hour AND desired.day = now.day AND desired.month = now.month THEN Increment[];
desired.second ← 0;
};
IF secondsThisYear # LAST[INT] THEN Error[notImplemented];
IF daysThisYear # daysPerYear THEN Error[notImplemented];
time ← BasicTime.Pack[desired];
};
Adjust: PUBLIC PROCEDURE [
years: INTLAST[INT], 
months: INTLAST[INT], 
days: INTLAST[INT],  
hours: INTLAST[INT],
minutes: INTLAST[INT],
seconds: INTLAST[INT],
baseTime: Packed ← defaultTime,
precisionOfResult: Precision ← unspecified]
RETURNS
[time: Packed, precision: Precision] = {
unpacked: Unpacked ← BasicTime.Unpack[IF baseTime = defaultTime 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..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..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 * minutesPerHour * secondsPerMinute)];
IF minutes # LAST[INT] THEN time ← BasicTime.Update[time, minutes * secondsPerMinute];
IF seconds # LAST[INT] THEN time ← BasicTime.Update[time, seconds];
RETURN[time, precisionOfResult];
};
Parse: PUBLIC PROCEDURE [rope: Rope.ROPE,
baseTime: Packed ← defaultTime, search: BOOLTRUE]
RETURNS [time: Packed, precision: Precision, start, length: NAT] = {
break: IO.BreakProc = {
RETURN[SELECT char FROM
',, '., '?, '(, ') => sepr, -- I have seen dates written as Tuesday (April 12)
':, '-, '/, '\" => break,
ENDCASE => WhiteSpace[char]];
};
IsAnHour: PROC [i: INT, mustBe: BOOLFALSE] RETURNS[BOOLTRUE] = {
IF i < 0 OR hour # hoursPerDay THEN GOTO No
ELSE IF i IN [0..hoursPerDay) THEN hour ← i
ELSE IF i < 100 THEN GOTO No
ELSE IF i < 2400 THEN { -- i is between 100 and 2400
IF (i MOD 100) < 60 THEN {hour ← i/100; minute ← i MOD 100}
ELSE GOTO No;
}
ELSE GOTO No;
isATime ← TRUE;
RETURN[TRUE];
EXITS No => IF mustBe THEN Error[invalid] ELSE RETURN[FALSE];
};
IsAMinute: PROC [i: INT, mustBe: BOOLFALSE] RETURNS[BOOLTRUE] = {
IF minute = minutesPerHour AND hour # hoursPerDay AND i IN [0..minutesPerHour) THEN minute ← i
ELSE IF mustBe THEN Error[invalid]
ELSE RETURN[FALSE];
isATime ← TRUE;
};
IsASecond: PROC [i: INT, mustBe: BOOLFALSE] RETURNS[BOOLTRUE] = {
IF second = secondsPerMinute AND minute # minutesPerHour AND hour # hoursPerDay AND i IN [0..secondsPerMinute) THEN second ← i
ELSE IF mustBe THEN Error[invalid]
ELSE RETURN[FALSE];
isATime ← TRUE;
};
IsAMonth: PROC [i: INT, mustBe: BOOLFALSE] RETURNS[BOOLTRUE] = {
IF month = unspecified AND i IN [1..12] THEN month ← LOOPHOLE[INTEGER[i - 1], MonthOfYear]
ELSE IF mustBe THEN Error[invalid]
ELSE RETURN[FALSE];
isADate ← TRUE;
};
IsADayOfMonth: PROC [i: INT, mustBe: BOOLFALSE] RETURNS[yes: BOOLTRUE] = {
IF day = 0 AND i IN [1..daysPerMonth] THEN day ← i
ELSE IF mustBe THEN Error[invalid]
ELSE RETURN[FALSE];
isADate ← TRUE;
};
IsAYear: PROC [i: INT, mustBe: BOOLFALSE] RETURNS[yes: BOOLTRUE] = {
IF year = 0 AND i IN [1900..2050] THEN year ← i
ELSE IF year = 0 AND i IN [minutesPerHour..99] THEN year ← 1900 + i -- to handle things like 25 apr 83
ELSE IF mustBe THEN Error[invalid]
ELSE RETURN[FALSE];
isADate ← TRUE;
};
AMPMCheck: PROC = {
IF (am OR pm) AND hour = hoursPerDay THEN Error[invalid]; -- if hours not specified when this is called, means somebody said am or pm without hour.
IF pm THEN {IF hour > 12 THEN Error[invalid] ELSE IF hour < 12 THEN hour ← hour + 12}
ELSE IF am THEN {IF hour > 12 THEN Error[invalid] ELSE IF hour = 12 THEN hour ← 0}
ELSE IF hour <= 7 THEN hour ← hour + 12 -- if no am or pm specified, treat 2 as 2PM
};
stream: IO.STREAM = IO.RIS[rope];
token, prevToken, nextToken: ROPE;
am, pm, isAnInterval, isADate, isAnImpliedDate, isATime: BOOLFALSE;
year: [0..2050] ← 0; 
month: MonthOfYear ← unspecified; 
day: [0..daysPerMonth] ← 0;  
hour: [0..hoursPerDay] ← hoursPerDay;
minute: [0..minutesPerHour] ← minutesPerHour;
second: [0..secondsPerMinute] ← secondsPerMinute;
zone: INTLAST[INT];
weekday: DayOfWeek ← unspecified;  
years, months, days, hours, minutes, seconds: INTLAST[INT];
base: Packed ← IF baseTime = defaultTime THEN BasicTime.Now[] ELSE baseTime;
now: Unpacked ← BasicTime.Unpack[base];
i: INT;
before: BOOLFALSE;
precisionOfResult: Precision ← unspecified;
prevNum: INT ← -1; -- -1 means previous token was not a number
last: INT ← -1;
why: ErrorCode ← invalid;
start ← 0;
BEGIN -- establish scope to transform Tempus.Error into Tempus.Uninteligible
ENABLE Error => Unintelligible[rope: rope, vicinity: IO.GetIndex[stream], ec: ec];
DO
prevToken ← token;
IF nextToken # NIL THEN {token ← nextToken; nextToken ← NIL}
ELSE token ← IO.GetTokenRope[stream, break ! IO.EndOfStream => EXIT].token;
{
SELECT TRUE FROM
RopeEqual[token, "\""] => {
stream.Backup['\"];
token ← prevToken;
[] ← stream.GetRopeLiteral[ ! IO.Error, IO.EndOfStream => EXIT]; --throw away anything in quotes
nextToken ← NIL;
LOOP;
};
RopeEqual[token, "years"], RopeEqual[token, "year"], RopeEqual[token, "yrs"] => {
IF prevNum # -1 THEN years ← prevNum
ELSE IF Rope.Equal[prevToken, "a"] THEN years ← 1
ELSE Error[invalid];
precisionOfResult ← years;
};
RopeEqual[token, "months"], RopeEqual[token, "month"] => {
IF prevNum # -1 THEN months ← prevNum
ELSE IF Rope.Equal[prevToken, "a"] THEN months ← 1
ELSE Error[invalid];
precisionOfResult ← months;
};
RopeEqual[token, "weeks"], RopeEqual[token, "week"] => {
IF prevNum # -1 THEN days ← prevNum * 7
ELSE IF Rope.Equal[prevToken, "a"] THEN days ← 7
ELSE Error[invalid];
precisionOfResult ← days;
};
RopeEqual[token, "days"], RopeEqual[token, "day"] => {
IF prevNum # -1 THEN days ← prevNum
ELSE IF Rope.Equal[prevToken, "a"] THEN days ← 1
ELSE Error[invalid];
precisionOfResult ← days;
};
RopeEqual[token, "hours"], RopeEqual[token, "hour"], RopeEqual[token, "hrs"] => {
IF prevNum # -1 THEN hours ← prevNum
ELSE IF Rope.Equal[prevToken, "an"] THEN hours ← 1
ELSE Error[invalid];
precisionOfResult ← hours;
};
RopeEqual[token, "minutes"], RopeEqual[token, "minute"], RopeEqual[token, "mins"] => {
IF prevNum # -1 THEN minutes ← prevNum
ELSE IF Rope.Equal[prevToken, "a"] THEN minutes ← 1
ELSE Error[invalid];
precisionOfResult ← minutes;
};
RopeEqual[token, "seconds"], RopeEqual[token, "secs"] => {
IF prevNum # -1 THEN seconds ← prevNum
ELSE IF Rope.Equal[prevToken, "a"] THEN seconds ← 1
ELSE Error[invalid];
precisionOfResult ← seconds;
};
ENDCASE =>
IF prevNum = -1 THEN GOTO NextBlock
ELSE IF x ← Spell.Choose[[minutes, seconds, etc.]
ELSE IF IsAnHour[prevNum] OR IsAMinute[prevNum] OR IsASecond[prevNum] THEN {prevNum ← -1; GOTO NextBlock}
ELSE IF isAnInterval OR isADate OR isAnImpliedDate OR isATime THEN Error[invalid]
ELSE {prevNum ← -1; GOTO NextBlock}; -- havent seen any part of a time or date yet. This is just a number that happens to be in the text, e.g. a phone number
prevNum ← -1;
isAnInterval ← TRUE;
length ← stream.GetIndex[] - start;
LOOP;
EXITS
NextBlock => NULL; -- drop through to next
};
{
w: DayOfWeek;
m: MonthOfYear;
IF (w ← IsDayOfWeek[token]) # unspecified THEN {
weekday ← w;
isADate ← TRUE;
}
ELSE IF (m ← IsMonthOfYear[token]) # unspecified THEN {
month ← m;
isADate ← TRUE;
}
ELSE SELECT TRUE FROM
RopeEqual[token, "Doomsday"] => {
base ← BasicTime.latestGMT;
isAnImpliedDate ← TRUE;
isATime ← TRUE;
};
RopeEqual[token, "Tomorrow"] => {
IF now.weekday = Sunday THEN weekday ← Monday
ELSE weekday ← SUCC[now.weekday];
isAnImpliedDate ← TRUE;
};
RopeEqual[token, "Yesterday"] => {
t: Unpacked ← BasicTime.Unpack[Adjust[days: -1, baseTime: base].time];
year ← t.year;
month ← t.month;
day ← t.day;
isAnImpliedDate ← TRUE;
};
RopeEqual[token, "today"] => {
year ← now.year; -- so 1:30 today doesn't parse as 1:30 tomorrow if it is now 3pm
month ← now.month;
day ← now.day;
isAnImpliedDate ← TRUE;
};
RopeEqual[token, "now"] => {
year ← now.year;
month ← now.month;
day ← now.day;
isAnImpliedDate ← TRUE;
hour ← now.hour;
minute ← now.minute;
second ← now.second;
IF hour < 12 THEN am ← TRUE;
isATime ← TRUE;
};
RopeEqual[token, "am"], RopeEqual[token, "a.m."] => {am ← TRUE; isATime ← TRUE};
RopeEqual[token, "pm"], RopeEqual[token, "p.m"] => {pm ← TRUE; isATime ← TRUE};
(i ← Rope.Find[s1: token, s2: "am", case: FALSE]) # -1 => {
IF (i + 2) = Rope.Length[token] THEN {
am ← TRUE;
nextToken ← "am"; -- reason for not simply setting am to TRUE and skipping this token. is that for something like 1:15PM June ... the fact that the pM wasn't seen would confuse this with something like 15 June ... On the other hand, if you don't set am to true, then something like Monday, August, 3PM would cause the 3 to get interpreted as the day.
token ← Rope.Substr[base: token, len: i];
};
GOTO NextBlock;
};
(i ← Rope.Find[s1: token, s2: "pm", case: FALSE]) # -1 => {
IF (i + 2) = Rope.Length[token] THEN {
pm ← TRUE;
nextToken ← "pm";
token ← Rope.Substr[base: token, len: i];
};
GOTO NextBlock;
};
RopeEqual[token, "noon"] => {
hour ← 12;
IF hour = hoursPerDay THEN hour ← 12
ELSE IF hour # 12 THEN Error[overConstrained];
pm ← TRUE;
isATime ← TRUE;
};
ENDCASE => GOTO NextBlock;
length ← stream.GetIndex[] - start;
LOOP;
EXITS
NextBlock => NULL; -- drop through to next
};
{
SELECT TRUE FROM
RopeEqual[token, "one"] => prevNum ← 1;
RopeEqual[token, "two"] => prevNum ← 2;
RopeEqual[token, "ten"] => prevNum ← 10;
RopeEqual[token, "fifteen"] => prevNum ← 15;
ENDCASE => GOTO NextBlock;
length ← stream.GetIndex[] - start;
LOOP;
EXITS
NextBlock => NULL; -- drop through to next
};
SELECT TRUE FROM
RopeEqual[token, "from"], RopeEqual[token, "after"], RopeEqual[token, "before"] => IF isAnInterval OR isADate OR isAnImpliedDate OR isATime THEN { -- e.g. two weeks from Thursday, Thursday after May 1
precision: Precision;
[base, precision] ← Parse[rope: IO.GetTokenRope[stream, Everything].token, baseTime: base];
now ← BasicTime.Unpack[base];
IF precisionOfResult = unspecified OR precision > precisionOfResult THEN precisionOfResult ← precision;
IF hour # hoursPerDay THEN hours ← hour; -- e.g. 11:45 a week from Thursday. base is now Thursday.
IF minute # minutesPerHour THEN minutes ← minute;
IF second # secondsPerMinute THEN seconds ← second;
IF RopeEqual[token, "before"] THEN before ← TRUE;
EXIT;
};
RopeEqual[token, "in"], RopeEqual[token, "at"] => {
length ← stream.GetIndex[] - start;
LOOP; -- if say at 3PM, and search = FALSE, it should still find the time.
};
RopeEqual[token, ":"], RopeEqual[token, "/"], RopeEqual[token, "-"] => {
length ← stream.GetIndex[] - start;
LOOP;
};
ENDCASE;
{
i ← -1;
i ← Convert.IntFromRope[token ! Convert.Error, SafeStorage.NarrowFault => CONTINUE];
{ -- test for a number followed by an ordinal abbreviation, like 1st, 2nd, 3rd, 17th
index: INT;
IF (index ← Rope.Find[s1: token, s2: "th", case: FALSE]) # -1 AND (index + 2) = Rope.Length[token] THEN token ← Rope.Substr[base: token, len: index]
ELSE IF (index ← Rope.Find[s1: token, s2: "nd", case: FALSE]) # -1 AND (index + 2) = Rope.Length[token] THEN token ← Rope.Substr[base: token, len: index]
ELSE IF (index ← Rope.Find[s1: token, s2: "rd", case: FALSE]) # -1 AND (index + 2) = Rope.Length[token] THEN token ← Rope.Substr[base: token, len: index]
ELSE IF (index ← Rope.Find[s1: token, s2: "st", case: FALSE]) # -1 AND (index + 2) = Rope.Length[token] THEN token ← Rope.Substr[base: token, len: index];
i ← Convert.IntFromRope[token ! Convert.Error, SafeStorage.NarrowFault => CONTINUE];
};
IF i = -1 THEN GOTO ExitCheck;
the current token is a number, what does it mean.
IF isAnInterval THEN prevNum ← i -- the next object should/must be "months", "hours", "minutes", etc.
ELSE IF hour = hoursPerDay AND (am OR pm) THEN [] ← IsAnHour[i, TRUE] -- e.g. 4PM, 430PM
ELSE IF month # unspecified AND day = 0 THEN [] ← IsADayOfMonth[i, TRUE] -- e.g. April 27
ELSE IF month # unspecified AND day # 0 THEN { -- month and day specified
IF year # 0 THEN { -- year specified, could be hour, minute, or second
IF hour = hoursPerDay THEN [] ← IsAnHour[i, TRUE] -- e.g. April 27, 1983, 430. Must be an hour
ELSE IF minute = minutesPerHour THEN [] ← IsAMinute[i, TRUE] -- e.g. April 27, 1983 4 30
ELSE [] ← IsASecond[i, TRUE]; -- e.g. April 27, 1983 4 30 30
}
ELSE IF hour # hoursPerDay THEN {IF IsAYear[i] OR IsAMinute[i, TRUE] THEN NULL} -- year not specified, hour was specified could be year or minute, e.g. 4PM April 27, 1983, or April 27, 4 30.
ELSE IF IsAYear[i] OR IsAnHour[i] OR IsAMinute[i] OR IsASecond[i, TRUE] THEN NULL; -- neither year nor hour specified, could be hour, minute, second, or year e.g. April 27, 430, April 27, 1983, or April 27, 430.
}
ELSE { -- saw a number with not enough context to decide whether day, month, or hour, look at next thing for a clue.
IF nextToken = NIL THEN nextToken ← IO.GetTokenRope[stream, break ! IO.EndOfStream => CONTINUE].token;
SELECT TRUE FROM
RopeEqual[nextToken, ":"] => {
[] ← IsAnHour[i, TRUE]; -- e.g. april 25, 3:40
nextToken ← NIL;
};
RopeEqual[nextToken, "/"] => {
[] ← IsAMonth[i, TRUE]; -- e.g. 4/27/83
nextToken ← NIL;
};
RopeEqual[nextToken, "-"] => { -- e.g. 25-apr-83
nextToken ← NIL;
nextToken ← IO.GetTokenRope[stream, break ! IO.EndOfStream => CONTINUE].token;
IF IsMonthOfYear[nextToken] # unspecified THEN [] ← IsADayOfMonth[i, TRUE]
ELSE GOTO ExitCheck; -- - not part of time, but don't raise error. might be a phone number.
};
nextToken = NIL => {
IF IsAnHour[i] OR IsAMinute[i] OR IsASecond[i] THEN NULL
ELSE Error[tooVague];
};
ENDCASE =>
IF IsMonthOfYear[nextToken] # unspecified THEN [] ← IsADayOfMonth[i, TRUE] -- e.g. 4 apr 83
ELSE prevNum ← i; -- e.g. 2 weeks
};
length ← stream.GetIndex[] - start;
LOOP;
EXITS
ExitCheck => -- the current token was not recognized as part of a time/date
IF isAnInterval THEN EXIT
ELSE IF isADate OR isAnImpliedDate OR isATime THEN {
IF (isADate OR isAnImpliedDate) AND isATime THEN EXIT
ELSE LOOP; -- so date and time can be separated by noise words, e.g. Thursday morning at 10:30, 10:30 on the morning of July 13.
}
ELSE IF NOT search THEN Unintelligible[rope, start, nothingSpecified]
ELSE {start ← stream.GetIndex[]; LOOP};
};
ENDLOOP;
AMPMCheck[];
IF NOT isATime AND NOT isADate AND NOT isAnImpliedDate AND NOT isAnInterval THEN Error[nothingSpecified] -- exited because ran out of characters in rope, or because search = FALSE
ELSE IF isADate AND isAnImpliedDate THEN Error[overConstrained]
ELSE IF isAnInterval THEN [time, precision] ← Adjust[
years: IF years # LAST[INT] AND before THEN -years ELSE years,
months: IF months # LAST[INT] AND before THEN -months ELSE months,
days: IF days # LAST[INT] AND before THEN -days ELSE days,
hours: IF hours # LAST[INT] AND before THEN -hours ELSE hours,
minutes: IF minutes # LAST[INT] AND before THEN -minutes ELSE minutes,
seconds: IF seconds # LAST[INT] AND before THEN -seconds ELSE seconds,
baseTime: base,
precisionOfResult: IF precisionOfResult = unspecified THEN unspecified ELSE IF precisionOfResult = seconds THEN seconds ELSE SUCC[precisionOfResult]] -- if user says in one hour, he probably means to nearest minute.
ELSE IF before THEN Error[notImplemented] -- Thursday before May 5 not implemented. two days before May 5 is.
ELSE [time, precision] ← SmartPack[year: year, month: month, day: day, hour: hour, minute: minute, second: second, weekday: weekday, baseTime: base
! Error => {why ← ec; time ← defaultTime; CONTINUE};
];
RETURN[time, precision, start, length];
END -- block for errors.
};
WhiteSpace: IO.BreakProc = {
RETURN[SELECT char FROM ' , '\n, '\t, '\l => sepr, ENDCASE => other];
};
Everything: IO.BreakProc = {RETURN[other]};
IsDayOfWeek: PROCEDURE [token: Rope.ROPE] RETURNS [DayOfWeek] = {
RETURN[SELECT TRUE FROM
RopeEqual[token, "Monday"], RopeEqual[token, "Mon"] => Monday,
RopeEqual[token, "Tuesday"], RopeEqual[token, "Tues"] => Tuesday,
RopeEqual[token, "Wednesday"], RopeEqual[token, "Wed"] => Wednesday,
RopeEqual[token, "Thursday"], RopeEqual[token, "Thurs"] => Thursday,
RopeEqual[token, "Friday"], RopeEqual[token, "Fri"] => Friday,
RopeEqual[token, "Saturday"], RopeEqual[token, "Sat"] => Saturday,
RopeEqual[token, "Sunday"], RopeEqual[token, "Sun"] => Sunday,
ENDCASE => unspecified];
};
IsMonthOfYear: PROCEDURE [token: Rope.ROPE] RETURNS [MonthOfYear] = {
RETURN[SELECT TRUE FROM
RopeEqual[token, "January"], RopeEqual[token, "Jan"] => January,
RopeEqual[token, "February"], RopeEqual[token, "Feb"] => February,
RopeEqual[token, "March"], RopeEqual[token, "Mar"] => March,
RopeEqual[token, "April"], RopeEqual[token, "Apr"] => April,
RopeEqual[token, "May"] => May,
RopeEqual[token, "June"], RopeEqual[token, "Jun"] => June,
RopeEqual[token, "July"], RopeEqual[token, "Jul"] => July,
RopeEqual[token, "August"], RopeEqual[token, "Aug"] => August,
RopeEqual[token, "September"], RopeEqual[token, "Sep"], RopeEqual[token, "Sept"] => September,
RopeEqual[token, "October"], RopeEqual[token, "Oct"] => October,
RopeEqual[token, "November"], RopeEqual[token, "Nov"] => November,
RopeEqual[token, "December"], RopeEqual[token, "Dec"] => December,
ENDCASE => unspecified];
};
Miscellaneous
RopeEqual: PROC [r1, r2: ROPE] RETURNS [BOOL] = INLINE {
IF RopeInline.InlineSize[r1] # RopeInline.InlineSize[r2] THEN RETURN[FALSE] causes compiler overflow
RETURN[Rope.Equal[r1, r2, FALSE]];
};
MakeRope: PUBLIC PROCEDURE [time: Packed ← defaultTime,
precision: Precision ← minutes,
includeDayOfWeek: BOOLFALSE,
useAMPM: BOOLTRUE]
RETURNS[value: Rope.ROPE] = {
unpacked: Unpacked ← BasicTime.Unpack[IF time = defaultTime THEN BasicTime.Now[] ELSE time];
IF includeDayOfWeek AND precision >= days THEN value ← Rope.Concat[
SELECT unpacked.weekday FROM
Monday => "Monday",
Tuesday => "Tuesday",
Wednesday => "Wednesday",
Thursday => "Thursday",
Friday => "Friday",
Saturday => "Saturday",
Sunday => "Sunday",
ENDCASE => ERROR,
", "];
IF precision >= months THEN
value ← Rope.Cat[
value,
SELECT unpacked.month FROM
January => "January ",
February => "February ",
March => "March ",
April => "April ",
May => "May ",
June => "June ",
July => "July ",
August => "August ",
September => " September ",
October => "October ",
November => " November ",
December => "December ",
ENDCASE => ERROR,
IF precision >= days THEN Rope.Concat[Convert.RopeFromInt[unpacked.day], ", "]
ELSE ""
];
value ← Rope.Concat[
value,
Convert.RopeFromInt[unpacked.year]
];
IF precision >= hours THEN {
value ← Rope.Cat[
value,
" ",
Convert.RopeFromInt[
IF NOT useAMPM THEN unpacked.hour
ELSE IF unpacked.hour < 13 THEN unpacked.hour
ELSE unpacked.hour - 12]];
IF precision >= minutes THEN {
value ← Rope.Cat[
value,
":",
IF unpacked.minute < 10 THEN "0"
ELSE "",
Convert.RopeFromInt[unpacked.minute]
];
IF precision >= seconds THEN value ← Rope.Cat[
value,
":",
IF unpacked.second < 10 THEN "0"
ELSE "",
Convert.RopeFromInt[unpacked.second],
];
};
IF useAMPM THEN value ← Rope.Cat[
value,
IF unpacked.hour >= 12 THEN " pm" ELSE " am"
];
};
};
END.
Change Log
Edited on September 26, 1984 1:09:59 am PDT, by Beach
changes to: SmartPack to avoid the daylight savings shift if the current hour is < 1:00 am and a BasicTime.Pack/Unpack shifts the weekday to be the previous day!!!, DIRECTORY
Beach, February 5, 1985 10:46:15 am PST
changes to: SmartPack, Adjust
Edited on February 11, 1985 1:06:18 pm PST, by Pier
changes to: Parse
Pier, November 4, 1985 4:51:07 pm PST
fixed "desired.hour 𡤂" to restore trueHour so parser works properly.
changes to: SmartPack