DIRECTORY Arpa USING [Address], Basics, BasicTime, Commander, CommanderOps, Convert, FS USING [StreamOpen], IntHashTable, IO, List USING [LORA, Remove], LoganBerry USING [Attribute, BuildIndices, Close, CompactLogs, Cursor, DeleteEntry, EndGenerate, Entry, Error, GenerateEntries, NextEntry, nullDB, Open, OpenDB, ReadEntry, RegisterWriteProc, UnregisterWriteProc, WriteEntry], NetworkName, RedBlackTree, Remind, Rope, rtable3, SimpleFeedback, SunPMap, SunPMapClient, SunRPC, SunRPCAuth, SunRPCOnUDP, SymTab, SystemNames USING [UserCedarDir, UserName], TimeParse USING [Adjust], UnixSysCalls USING [GetPID], UserProfile USING [CallWhenProfileChanges, ProfileChangedProc, Token], UXTime; RemindImpl: CEDAR PROGRAM IMPORTS Basics, BasicTime, Commander, CommanderOps, Convert, FS, IntHashTable, IO, List, LoganBerry, NetworkName, RedBlackTree, Rope, rtable3, SimpleFeedback, SunPMapClient, SunRPC, SunRPCAuth, SunRPCOnUDP, SymTab, SystemNames, TimeParse, UnixSysCalls, UserProfile, UXTime EXPORTS Remind = BEGIN OPEN Remind, RT:rtable3; ROPE: TYPE ~ Rope.ROPE; RTable: TYPE ~ RT.TABLEPROG3; repetitionNames: ARRAY Repetitions OF ROPE = [ "once", "daily", "weekdays", "weekly", "biweekly", "monthly", "yearly", "nthWeekday", "everyNthDay", "everyNthWeek", "everyNthMonth", "other"]; RopeFromRepetition: PUBLIC PROC[rep: Repetitions] RETURNS [ROPE] = { RETURN[repetitionNames[rep]]; }; RepetitionFromRope: PUBLIC PROC[str: ROPE] RETURNS [Repetitions] = { FOR rep: Repetitions IN Repetitions DO IF Rope.Equal[str, repetitionNames[rep], FALSE] THEN RETURN[rep]; ENDLOOP; RETURN[once]; }; meetingTypeNames: ARRAY MeetingType OF ROPE = [ "none", "meeting", "command", "seminar", "protectedCmd"]; RopeFromMeetingType: PUBLIC PROC[type: MeetingType] RETURNS [ROPE] = { RETURN[meetingTypeNames[type]]; }; MeetingTypeFromRope: PUBLIC PROC[str: ROPE] RETURNS [MeetingType] = { FOR type: MeetingType IN MeetingType DO IF Rope.Equal[str, meetingTypeNames[type], FALSE] THEN RETURN[type]; ENDLOOP; RETURN[meeting]; }; BadData: PUBLIC ERROR = CODE; RepsToPeriod: ARRAY Repetitions OF RT.Interval ~ [once: single, daily: daily, weekdays: daily, weekly: weekly, biweekly: biweekly, monthly: monthly, yearly: yearly, nthWeekday: nthWeekday, everyNthDay: everyNthDay, everyNthWeek: everyNthWeek, everyNthMonth: everyNthMonth, other: otherPeriod]; PeriodToReps: ARRAY RT.Interval OF Repetitions ~ [single: once, daily: daily, weekly: weekly, biweekly: biweekly, monthly: monthly, yearly: yearly, nthWeekday: nthWeekday, everyNthDay: everyNthDay, everyNthWeek: everyNthWeek, everyNthMonth: everyNthMonth, otherPeriod: other]; IntFromTime: PROC[time: BasicTime.GMT] RETURNS [INT] = { RETURN [BasicTime.Period[BasicTime.earliestGMT, time]]; }; TimeFromInt: PROC[int: INT] RETURNS [BasicTime.GMT] = { RETURN[BasicTime.Update[BasicTime.earliestGMT, int]]; }; MyRopeFromInt: PROC[int: INT] RETURNS [ROPE] = { RETURN[IO.PutFR1["%010g", IO.int[int]]]; }; TimeFromUnixCmd: PROC [cmd: Commander.Handle] RETURNS [result: REF ANY ¬ NIL, msg: ROPE ¬ NIL] --Commander.CommandProc-- ~ { argv: CommanderOps.ArgumentVector ~ CommanderOps.Parse[cmd]; i: INT; t: BasicTime.GMT; IF argv.argc # 2 THEN RETURN [$Failure, Rope.Cat["Usage: ", argv[0], " "]]; i ¬ Convert.IntFromRope[argv[1]]; t ¬ UnixToBasicTime[i]; cmd.out.PutF["%g ~ %g\n", [integer[i]], [rope[Convert.RopeFromTime[from: t, end: seconds]]] ]; RETURN}; TimeFromIntCmd: PROC [cmd: Commander.Handle] RETURNS [result: REF ANY ¬ NIL, msg: ROPE ¬ NIL] --Commander.CommandProc-- ~ { argv: CommanderOps.ArgumentVector ~ CommanderOps.Parse[cmd]; i: INT; t: BasicTime.GMT; IF argv.argc # 2 THEN RETURN [$Failure, Rope.Cat["Usage: ", argv[0], " "]]; i ¬ Convert.IntFromRope[argv[1]]; t ¬ TimeFromInt[i]; cmd.out.PutF["%g ~ %g\n", [integer[i]], [rope[Convert.RopeFromTime[from: t, end: seconds]]] ]; RETURN}; GMTUpCmd: PROC [cmd: Commander.Handle] RETURNS [result: REF ANY ¬ NIL, msg: ROPE ¬ NIL] --Commander.CommandProc-- ~ { argv: CommanderOps.ArgumentVector ~ CommanderOps.Parse[cmd]; i: INT; t: BasicTime.GMT; IF argv.argc # 2 THEN RETURN [$Failure, Rope.Cat["Usage: ", argv[0], " "]]; i ¬ Convert.IntFromRope[argv[1]]; t ¬ LOOPHOLE[i]; cmd.out.PutF["%g ~ %g\n", [integer[i]], [rope[Convert.RopeFromTime[from: t, end: seconds]]] ]; RETURN}; baseGMT: BasicTime.GMT ¬ UXTime.ToGMT[UXTime.earliestUXTIME]; limitUnixTime: INT ¬ BasicToUnixTime[BasicTime.latestGMT]; BasicToUnixTime: PROC [gmt: BasicTime.GMT] RETURNS [INT] ~ { IF BasicTime.Period[from: gmt, to: baseGMT] > 0 THEN gmt ¬ baseGMT; TRUSTED {RETURN [LOOPHOLE[UXTime.FromGMT[gmt]]]}}; UnixToBasicTime: PROC [t: INT] RETURNS [BasicTime.GMT] ~ { t ¬ MIN[t, limitUnixTime]; TRUSTED {RETURN UXTime.ToGMT[LOOPHOLE[t]]}}; RegisterChangeProc: PUBLIC PROC[proc: PROC[data: REF], clientData: REF, ident: ATOM, dbName: ROPE ¬ NIL] = { block: ProcBlock ¬ NEW[ProcBlockRec]; block.proc ¬ proc; block.data ¬ clientData; block.dbName ¬ dbName; block.ident ¬ ident; changeProcList ¬ CONS[block, changeProcList]; }; UnregisterChangeProc: PUBLIC PROC[ident: ATOM, dbName: ROPE ¬ NIL] = { block: ProcBlock ¬ NEW[ProcBlockRec]; l: List.LORA; blk: ProcBlock; WHILE TRUE DO FOR l ¬ changeProcList, l.rest UNTIL l = NIL DO blk ¬ NARROW[l.first]; IF blk.ident=ident AND Rope.Equal[blk.dbName, dbName, FALSE] THEN { changeProcList ¬ List.Remove[l.first, changeProcList]; EXIT; } ENDLOOP; ENDLOOP; }; DatabaseChanged: PROC[db: LoganBerry.OpenDB, entry: LoganBerry.Entry, ident: ATOM, clientData: REF] --LoganBerryExtras.WriteProc-- = { name: ROPE; blk: ProcBlock; name ¬ NARROW[clientData]; FOR l: List.LORA ¬ changeProcList, l.rest UNTIL l = NIL DO blk ¬ NARROW[l.first]; IF Rope.Equal[blk.dbName, name, FALSE] THEN blk.proc[blk.data]; ENDLOOP; }; GetDefaultDbName: PUBLIC PROC RETURNS [ROPE] ~ {RETURN [defaultDbName]}; OpenDb: PROC[create: BOOLEAN ¬ FALSE, name: ROPE ¬ NIL] RETURNS [db: LoganBerry.OpenDB ¬ LoganBerry.nullDB, rt: RTable ¬ NIL, usedDbName: ROPE ¬ NIL] = { dbName: ROPE; gotBadIndex: BOOL ¬ FALSE; nl, atpos, retried: INT ¬ 0; IF name = NIL THEN dbName ¬ defaultDbName ELSE dbName ¬ name; usedDbName ¬ dbName; nl ¬ dbName.Length[]; IF (atpos ¬ dbName.Index[s2: "@"]) < nl THEN { hostName: ROPE ~ dbName.Substr[start: atpos+1]; hostAddrRope: ROPE ~ NetworkName.AddressFromName[$ARPA, hostName, NIL, host !NetworkName.Error => { SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "NetworkName.Error[%g] looking up host part of db name %g", LIST[[rope[msg]], [rope[dbName]]] ]; GOTO done}].addr; hostAddr: Arpa.Address ¬ Convert.ArpaAddressFromRope[hostAddrRope !Convert.Error => { SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "Convert.Error[VAL[%g], near %g] parsing address rope %g from db name %g", LIST[[cardinal[reason.ORD]], [integer[index]], [rope[hostAddrRope]], [rope[dbName]]] ]; GOTO done}]; rhPM: SunRPC.Handle ¬ SunRPCOnUDP.Create[hostAddr, Basics.HFromCard16[SunPMap.udpPort]]; conv: SunRPCAuth.Conversation ¬ SunRPCAuth.Initiate[SunRPCAuth.unixFlavor, SystemNames.UserName[]]; port: CARDINAL ¬ SunPMapClient.GetPort[rhPM, conv, RT.TABLEPROG, RT.TABLEVERS, SunPMap.ipProtocolUDP]; rh: SunRPC.Handle; IF port=0 THEN { SimpleFeedback.PutF[$RemindImpl, oneLiner, $Error, "no Sun calendar manager daemon (supporting version 3) on %g", [rope[hostName]] ]; GOTO done}; rh ¬ SunRPCOnUDP.SetRemote[rhPM, hostAddr, Basics.HFromCard16[Basics.LowHalf[port]]]; rt ¬ RT.MakeTABLEPROG3Client[rh, conv ! SunRPC.Error => { SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "SunRPC.Error[%g] importing cm V3 from %g", LIST[[atom[code]], [rope[hostName]]] ]; GOTO done}; SunRPCAuth.Error => { SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "SunRPCAuth.Error[%g] importing cm V3 from %g", LIST[[atom[code]], [rope[hostName]]] ]; GOTO done}]; rt.rtableping[rt ! SunRPC.Error => { SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "SunRPC.Error[%g] pinging cm V3 on %g", LIST[[atom[code]], [rope[hostName]]] ]; GOTO done}; SunRPCAuth.Error => { SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "SunRPCAuth.Error[%g] pinging cm V3 on %g", LIST[[atom[code]], [rope[hostName]]] ]; GOTO done}]; target ¬ dbName; } ELSE { target ¬ NIL; db ¬ LoganBerry.Open[dbName: dbName ! LoganBerry.Error => { IF ec=$BadIndex AND retried < 3 THEN { retried ¬ retried + 1; gotBadIndex ¬ TRUE; RETRY; } ELSE { IF ec = $CantOpenSchema OR ec=$CantOpenLog THEN { IF create AND retried < 3 THEN { Create[]; retried ¬ retried + 1; RETRY; } ELSE GOTO done; }; }; }]; IF gotBadIndex THEN BEGIN LoganBerry.BuildIndices[db: db]; END; LoganBerry.RegisterWriteProc[DatabaseChanged, db, $Remind, name]; }; RETURN; EXITS done => rt ¬ NIL; }; AddMeeting: PUBLIC PROC[meeting: Meeting, closeDb: BOOLEAN ¬ TRUE, dbName: ROPE ¬ NIL] RETURNS [id: INT] = { db: LoganBerry.OpenDB; rt: RTable; [db, rt] ¬ OpenDb[create: TRUE, name: dbName]; IF db#LoganBerry.nullDB THEN { entry: LoganBerry.Entry ¬ NIL; Link: PROC[atom: ATOM, str: ROPE] = { IF str # NIL AND NOT Rope.IsEmpty[str] THEN { attr: LoganBerry.Attribute; attr.type ¬ atom; attr.value ¬ str; entry ¬ CONS[attr, entry]; }; }; id ¬ IntFromTime[BasicTime.Now[]]; Link[$uniqID, Convert.RopeFromInt[id]]; Link[$type, meetingTypeNames[meeting.type]]; Link[$start, MyRopeFromInt[IntFromTime[meeting.start]]]; Link[$duration, Convert.RopeFromCard[meeting.duration]]; Link[$explanation, meeting.explanation]; Link[$repeat, repetitionNames[meeting.repeat]]; FOR rl: RemindList ¬ meeting.reminders, rl.rest WHILE rl#NIL DO WITH rl.first SELECT FROM x: REF ReminderRecord[alert] => { Link[$stopReminding, MyRopeFromInt[IntFromTime[x.stop]]]; Link[$startReminding, MyRopeFromInt[IntFromTime[x.start]]]; }; x: REF ReminderRecord[mail] => { Link[$mailTo, x.to]; Link[$mailAt, MyRopeFromInt[IntFromTime[x.when]]]; }; ENDCASE => ERROR; ENDLOOP; Link[$more, meeting.more]; Link[$iconLabel, meeting.iconLabel]; Link[$iconFlavor, meeting.iconFlavor]; Link[$public, Convert.RopeFromBool[meeting.public]]; LoganBerry.WriteEntry[db: db, entry: entry]; IF closeDb THEN CloseLoganBerry[db]; -- so autobackup will see a change } ELSE IF rt#NIL THEN { appt: REF RT.Appt; args: RT.Args; res: RT.TableRes; pd: RT.Period ¬ [period: RepsToPeriod[meeting.repeat], nth: 0]; tag: REF RT.Tag ¬ NEW [RT.Tag ¬ [tag: appointment, showtime: 1, next: NIL]]; attrs: REF RT.Attribute ¬ PushReminders[meeting, meeting.reminders, NEW[RT.Attribute ¬ [ next: NIL, attr: "cedar-more", value: meeting.more, clientdata: NIL]] ]; TRUSTED {appt ¬ NEW[RT.Appt ¬ [ apptid: [tick: BasicToUnixTime[meeting.start], key: 0], tag: tag, duration: meeting.duration*60, ntimes: IF meeting.repeat=once THEN 0 ELSE 999999999, what: meeting.explanation, period: pd, author: NIL, clientdata: NIL, exception: NIL, attr: attrs, apptstatus: active, privacy: IF meeting.public THEN public ELSE private, next: NIL]]}; args ¬ NEW [RT.ArgsObject[APPT] ¬ [APPT[appt]] ]; res ¬ rt.rtableinsert[rt, target, args, UnixSysCalls.GetPID[] ! SunRPC.Error => { SimpleFeedback.PutF[$RemindImpl, oneLiner, $Error, "insert of cm appt failed (%g)", [atom[code]] ]; GOTO dun}]; IF watchAS THEN SELECT res.status FROM accessok, accessadded => NULL; ENDCASE => { SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "Insertion into DB %g of meeting at %g failed (%g)", LIST[[rope[dbName]], [time[meeting.start]], [rope[RT.AccessStatusNames[res.status]]]] ]; GOTO dun}; WITH res.res SELECT FROM x: REF RT.TableResListObject[AP] => id ¬ x.a.apptid.key; x: REF RT.TableResListObject[ID] => id ¬ x.i.apptid.key; ENDCASE => { SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "Insertion into DB %g of meeting at %g returned unexpected variant", LIST[[rope[dbName]], [time[meeting.start]]] ]; GOTO dun}; EXITS dun => id ¬ 0; } ELSE { SimpleFeedback.PutF[$RemindImpl, oneLiner, $Error, "Unable to open DB %g", [rope[dbName]] ]; id ¬ 0}; }; PushReminders: PROC [meeting: Meeting, rl: RemindList, attr: REF RT.Attribute] RETURNS [REF RT.Attribute] ~ { FOR rl ¬ rl, rl.rest WHILE rl#NIL DO nu: REF RT.Attribute ~ NEW [RT.Attribute ¬ WITH rl.first SELECT FROM x: REF ReminderRecord[alert] => [ next: attr, attr: "op", value: Convert.RopeFromInt[BasicTime.Period[from: x.start, to: meeting.start]], clientdata: NIL], x: REF ReminderRecord[mail] => [ next: attr, attr: "ml", value: Convert.RopeFromInt[BasicTime.Period[from: x.when, to: meeting.start]], clientdata: x.to], ENDCASE => ERROR]; attr ¬ nu; ENDLOOP; RETURN [attr]}; breakStart: INT ¬ 0; breakUID: INT ¬ 0; breaksSeen: INT ¬ 0; InterpLbAttrs: PROC [aList: LIST OF LoganBerry.Attribute] RETURNS [meeting: Meeting] ~ { break: BOOL ¬ FALSE; meeting ¬ NEW[MeetingRec]; WHILE aList#NIL DO atom: ATOM ~ aList.first.type; value: ROPE ~ aList.first.value; aList ¬ aList.rest; SELECT atom FROM $uniqID => { meeting.uniqID ¬ Convert.IntFromRope[value]; IF meeting.uniqID=breakUID THEN break ¬ TRUE}; $type => meeting.type ¬ MeetingTypeFromRope[value]; $start => {i: INT ~ Convert.IntFromRope[value]; meeting.start ¬ TimeFromInt[i]; IF i=breakStart THEN break ¬ TRUE}; $duration => meeting.duration ¬ Convert.CardFromRope[value]; $explanation => meeting.explanation ¬ value; $repeat => meeting.repeat ¬ RepetitionFromRope[value]; $startReminding => { x: REF ReminderRecord[alert] ~ NEW [ReminderRecord[alert]]; x.start ¬ TimeFromInt[Convert.IntFromRope[value]]; IF aList#NIL AND aList.first.type=$stopReminding THEN { x.stop ¬ TimeFromInt[Convert.IntFromRope[aList.first.value]]; aList ¬ aList.rest; meeting.reminders ¬ CONS[x, meeting.reminders]} ELSE ERROR BadData[]}; $stopReminding => { x: REF ReminderRecord[alert] ~ NEW [ReminderRecord[alert]]; x.stop ¬ TimeFromInt[Convert.IntFromRope[value]]; IF aList#NIL AND aList.first.type=$startReminding THEN { x.start ¬ TimeFromInt[Convert.IntFromRope[aList.first.value]]; aList ¬ aList.rest; meeting.reminders ¬ CONS[x, meeting.reminders]} ELSE ERROR BadData[]}; $more => meeting.more ¬ value; $iconLabel => meeting.iconLabel ¬ value; $iconFlavor => meeting.iconFlavor ¬ value; $public => meeting.public ¬ Rope.Equal[value, "TRUE"]; $mailAt => { x: REF ReminderRecord[mail] ~ NEW [ReminderRecord[mail]]; x.when ¬ TimeFromInt[Convert.IntFromRope[value]]; IF aList#NIL AND aList.first.type=$mailTo THEN { x.to ¬ aList.first.value; aList ¬ aList.rest} ELSE x.to ¬ "user@host"; meeting.reminders ¬ CONS[x, meeting.reminders]}; ENDCASE => ERROR BadData[]; ENDLOOP; IF break THEN breaksSeen ¬ breaksSeen + 1; RETURN}; ListMeetings: PUBLIC PROC [ from, to: BasicTime.GMT, all: BOOLEAN ¬ FALSE, dbName: ROPE ¬ NIL ] RETURNS [ls: LIST OF Meeting ¬ NIL] = { cursor: LoganBerry.Cursor; entry: LoganBerry.Entry; meeting, meeting1: Meeting; lim: INT; db: LoganBerry.OpenDB; rt: RTable; usedDbName: ROPE; [db, rt, usedDbName] ¬ OpenDb[name: dbName]; IF BasicTime.Period[from: BasicTime.earliestGMT, to: from] < BasicTime.secondsPerYear THEN from ¬ BasicTime.Update[BasicTime.earliestGMT, BasicTime.secondsPerYear]; lim ¬ IntFromTime[to]; IF db#LoganBerry.nullDB THEN { cursor ¬ LoganBerry.GenerateEntries[ db: db, start: MyRopeFromInt[IntFromTime[from]], end: MyRopeFromInt[IntFromTime[to]], key: $start ]; WHILE (entry ¬ LoganBerry.NextEntry[cursor: cursor]) # NIL DO meeting ¬ InterpLbAttrs[entry]; IF meeting.repeat = once THEN ls ¬ CONS[meeting, ls]; ENDLOOP; LoganBerry.EndGenerate[cursor: cursor]; FOR rep: Repetitions IN [daily..LAST[Repetitions]] DO cursor ¬ LoganBerry.GenerateEntries[ db: db, start: repetitionNames[rep], -- RopeFromRepetition[rep]: end: repetitionNames[rep], key: $repeat]; WHILE (entry ¬ LoganBerry.NextEntry[cursor: cursor]) # NIL DO newReminders: BOOL ¬ FALSE; newStart: BasicTime.GMT; meeting ¬ InterpLbAttrs[entry]; newStart ¬ FirstOccurrenceAfter[rep, meeting.nth, meeting.start, from]; WHILE IntFromTime[newStart] <= lim DO UpdateMeeting[meeting, newStart, TRUE, newReminders]; ls ¬ CONS[meeting, ls]; IF NOT all THEN EXIT; meeting1 ¬ meeting; meeting ¬ NEW[MeetingRec ¬ meeting1­]; newStart ¬ NextOccurrence[rep, meeting.nth, meeting.start]; newReminders ¬ TRUE; ENDLOOP; ENDLOOP; LoganBerry.EndGenerate[cursor: cursor]; ENDLOOP; } ELSE IF rt#NIL THEN { range: RT.Range ~ [ key1: BasicToUnixTime[from], key2: BasicToUnixTime[to], next: NIL]; seen: IntHashTable.Table ~ IF all THEN NIL ELSE IntHashTable.Create[]; PassAppt: PROC [appt: REF RT.Appt] RETURNS [BOOL] ~ { IF range.key1<=appt.apptid.tick AND appt.apptid.tick<=range.key2 AND (all OR seen.Insert[appt.apptid.key, $T]) THEN ls ¬ CONS[ApptToMtg[appt], ls]; RETURN [FALSE]}; ScanAppts[rt, usedDbName, PassAppt, "ListMeetings", range.key1, range.key2]; RETURN}; }; ApptToMtg: PROC [appt: REF RT.Appt] RETURNS [mtg: Meeting] ~ { mtg ¬ NEW [MeetingRec ¬ [ uniqID: appt.apptid.key, type: meeting, start: UnixToBasicTime[appt.apptid.tick], duration: MAX[(appt.duration+30)/60, 1], explanation: appt.what, repeat: PeriodToReps[appt.period.period], public: appt.privacy = public, other: NIL]]; FOR attr: REF RT.Attribute ¬ appt.attr, attr.next WHILE attr#NIL DO IF attr.attr.Equal["cedar-more"] THEN mtg.more ¬ attr.value ELSE IF attr.attr.Length>0 THEN SELECT attr.attr.Fetch[0] FROM 'b, 'f, 'm, 'o => {r: ROPE ¬ attr.attr; alert, mail: BOOL ¬ FALSE; IF Rope.IsPrefix["bp", r] THEN {r ¬ r.Substr[2]; alert ¬ TRUE}; IF Rope.IsPrefix["fl", r] THEN {r ¬ r.Substr[2]; alert ¬ TRUE}; IF Rope.IsPrefix["op", r] THEN {r ¬ r.Substr[2]; alert ¬ TRUE}; IF Rope.IsPrefix["ml", r] THEN {r ¬ r.Substr[2]; mail ¬ TRUE}; IF r.Length[] = 0 THEN { adv: INT ¬ Convert.IntFromRope[attr.value !Convert.Error => GOTO Not]; start: BasicTime.GMT ¬ BasicTime.Update[mtg.start, -adv]; IF alert THEN { x: Reminder ~ NEW [ReminderRecord ¬ [NIL, alert[start, mtg.start]]]; mtg.reminders ¬ CONS[x, mtg.reminders]}; IF mail THEN { x: Reminder ~ NEW [ReminderRecord ¬ [NIL, mail[start, attr.clientdata]]]; mtg.reminders ¬ CONS[x, mtg.reminders]}; }; EXITS Not => mtg ¬ mtg}; ENDCASE => NULL; ENDLOOP; RETURN}; EnumerateAppts: PROC [rt: RTable, res: RT.TableResList, PerAppt: PROC [REF RT.Appt] RETURNS [BOOL]] RETURNS [empty: BOOL ¬ TRUE] ~ { TryLookup: PROC [of: RT.Args, apptid: RT.Id] RETURNS [stop: BOOL ¬ FALSE] ~ { ires: RT.TableRes; ires ¬ rt.rtablelookup[rt, target, of, UnixSysCalls.GetPID[] !SunRPC.Error => { SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "lookup of abbreviated appointment (tick=%x=%g, key=%g) => RPC.Error[%g]", LIST[[integer[apptid.tick]], [time[UnixToBasicTime[apptid.tick]]], [integer[apptid.key]], [atom[code]]] ]; GOTO Fale}]; IF watchAS THEN SELECT ires.status FROM accessok, accessexists => NULL; ENDCASE => { SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "lookup of abbreviated appointment (tick=%x=%g, key=%g) failed, with status=%g", LIST[[integer[apptid.tick]], [time[UnixToBasicTime[apptid.tick]]], [integer[apptid.key]], [rope[RT.AccessStatusNames[ires.status]]]] ]; RETURN [TRUE]}; WITH ires.res SELECT FROM y: REF RT.TableResListObject[AP] => stop ¬ PerAppt[y.a]; ENDCASE => { SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "lookup of abbreviated appointment (tick=%x=%g, key=%g) returned unexpected variant (%g)", LIST[[integer[apptid.tick]], [time[UnixToBasicTime[apptid.tick]]], [integer[apptid.key]], [rope[RT.TableResTypeNames[ires.res.tag]]]] ]; RETURN [TRUE]}; RETURN; EXITS Fale => stop ¬ TRUE}; WITH res SELECT FROM x: REF RT.TableResListObject[AP] => { FOR appt: REF RT.Appt ¬ x.a, appt.next WHILE appt#NIL DO empty ¬ FALSE; SELECT appt.tag.tag FROM appointment, otherTag, toDo => { IF debug THEN SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Debug, "Enum appt[tick=%x=%g, key=%g, what=%g]", LIST[[integer[appt.apptid.tick]], [time[UnixToBasicTime[appt.apptid.tick]]], [integer[appt.apptid.key]], [rope[appt.what]]] ]; IF PerAppt[appt] THEN EXIT; }; reminder => { IF debug THEN SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Debug, "Enum reminder[tick=%x=%g, key=%g, what=%g]", LIST[[integer[appt.apptid.tick]], [time[UnixToBasicTime[appt.apptid.tick]]], [integer[appt.apptid.key]], [rope[appt.what]]] ]; }; holiday => { IF debug THEN SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Debug, "Enum holiday[tick=%x=%g, key=%g, what=%g]", LIST[[integer[appt.apptid.tick]], [time[UnixToBasicTime[appt.apptid.tick]]], [integer[appt.apptid.key]], [rope[appt.what]]] ]; }; ENDCASE => ERROR; ENDLOOP}; x: REF RT.TableResListObject[AB] => { FOR aba: REF RT.AbbAppt ¬ x.b, aba.next WHILE aba#NIL DO empty ¬ FALSE; IF debug THEN SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Debug, "Enum abb-appt[tick=%x=%g, key=%g, what=%g]", LIST[[integer[aba.apptid.tick]], [time[UnixToBasicTime[aba.apptid.tick]]], [integer[aba.apptid.key]], [rope[aba.what]]] ]; IF TryLookup[NEW[RT.ArgsObject[UID] ¬ [UID[NEW[RT.Uid ¬ [aba.apptid, NIL]] ]]], aba.apptid ] THEN EXIT; ENDLOOP}; x: REF RT.TableResListObject[ID] => { FOR uid: REF RT.Uid ¬ x.i, uid.next WHILE uid#NIL DO empty ¬ FALSE; IF debug THEN SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Debug, "Enum uid[tick=%x=%g, key=%g]", LIST[[integer[uid.apptid.tick]], [time[UnixToBasicTime[uid.apptid.tick]]], [integer[uid.apptid.key]]] ]; IF TryLookup[NEW[RT.ArgsObject[UID] ¬ [UID[uid]]], uid.apptid ] THEN EXIT; ENDLOOP}; ENDCASE => SimpleFeedback.PutF[$RemindImpl, oneLiner, $Error, "enumerating unexpected variant (%g)", [rope[RT.TableResTypeNames[res.tag]]] ]; }; secsPerWeek: INT ¬ 3600*168; watchAS: BOOL ¬ FALSE; debug: BOOL ¬ FALSE; scanStyle: {dualRanges, cachedIntervals, nextLarger} ¬ dualRanges; useAbb: BOOL ¬ FALSE; ScanAppts: PROC [rt: RTable, usedDbName: ROPE, PerAppt: PROC [REF RT.Appt] RETURNS [BOOL], for: ROPE, min: INT ¬ 0, max: INT ¬ limitUnixTime-1] ~ { SELECT scanStyle FROM dualRanges => { now: INT ~ BasicToUnixTime[BasicTime.Now[]]; delt: INT ¬ secsPerWeek; sofar: INT ¬ MIN[now-1, max]+1; WHILE min < sofar DO -- [sofar, MIN[now-1, max]] already done qmin: INT ¬ MAX[MAX[min, sofar-delt]-1, 0]; qmax: INT ¬ sofar; args: RT.Args ¬ NEW [RT.ArgsObject[RANGE] ¬ [RANGE[NEW[RT.Range ¬ [qmin, qmax, NIL]] ]]]; res: RT.TableRes ¬ (IF useAbb THEN rt.rtableabbreviatedlookuprange ELSE rt.rtablelookuprange) [rt, target, args, UnixSysCalls.GetPID[] !SunRPC.Error => { SimpleFeedback.Append[$RemindImpl, oneLiner, $Error, IO.PutFLR["cm lookuprange[%x=%g, %x=%g] for %g on %g => RPC.Error[%g]", LIST[ [integer[qmin]], [time[UnixToBasicTime[qmin]]], [integer[qmax]], [time[UnixToBasicTime[qmax]]], [rope[for]], [rope[usedDbName]], [atom[code]] ]]]; EXIT}]; IF watchAS THEN SELECT res.status FROM accessok => IF debug THEN SimpleFeedback.Append[$RemindImpl, oneLiner, $Debug, IO.PutFLR["cm lookuprange[%x=%g, %x=%g] for %g succeeds", LIST[ [integer[qmin]], [time[UnixToBasicTime[qmin]]], [integer[qmax]], [time[UnixToBasicTime[qmax]]], [rope[for]] ]]]; ENDCASE => { SimpleFeedback.Append[$RemindImpl, oneLiner, $Error, IO.PutFLR["cm lookuprange[%x=%g, %x=%g] for %g on %g => status = %g", LIST[ [integer[qmin]], [time[UnixToBasicTime[qmin]]], [integer[qmax]], [time[UnixToBasicTime[qmax]]], [rope[for]], [rope[usedDbName]], [rope[RT.AccessStatusNames[res.status]]] ]]]; EXIT}; IF EnumerateAppts[rt, res.res, PerAppt] THEN delt ¬ 2*MIN[delt, LAST[INT]/4]; sofar ¬ qmin; ENDLOOP; NULL -- [min, MIN[now-1, max]] already queried--; sofar ¬ MAX[now, min]-1; delt ¬ secsPerWeek; WHILE sofar < max DO --[MAX[now, min], sofar] already done qmin: INT ¬ sofar; qmax: INT ¬ MIN[max, sofar+delt]+1; args: RT.Args ¬ NEW [RT.ArgsObject[RANGE] ¬ [RANGE[NEW[RT.Range ¬ [qmin, qmax, NIL]] ]]]; res: RT.TableRes ¬ (IF useAbb THEN rt.rtableabbreviatedlookuprange ELSE rt.rtablelookuprange) [rt, target, args, UnixSysCalls.GetPID[] !SunRPC.Error => { SimpleFeedback.Append[$RemindImpl, oneLiner, $Error, IO.PutFLR["cm lookuprange[%x=%g, %x=%g] for %g on %g => RPC.Error[%g]", LIST[ [integer[qmin]], [time[UnixToBasicTime[qmin]]], [integer[qmax]], [time[UnixToBasicTime[qmax]]], [rope[for]], [rope[usedDbName]], [atom[code]] ]]]; EXIT}]; IF watchAS THEN SELECT res.status FROM accessok => IF debug THEN SimpleFeedback.Append[$RemindImpl, oneLiner, $Debug, IO.PutFLR["cm lookuprange[%x=%g, %x=%g] for %g succeeds", LIST[ [integer[qmin]], [time[UnixToBasicTime[qmin]]], [integer[qmax]], [time[UnixToBasicTime[qmax]]], [rope[for]] ]]]; ENDCASE => { SimpleFeedback.Append[$RemindImpl, oneLiner, $Error, IO.PutFLR["cm lookuprange[%x=%g, %x=%g] for %g on %g => status = %g", LIST[ [integer[qmin]], [time[UnixToBasicTime[qmin]]], [integer[qmax]], [time[UnixToBasicTime[qmax]]], [rope[for]], [rope[usedDbName]], [rope[RT.AccessStatusNames[res.status]]] ]]]; EXIT}; IF EnumerateAppts[rt, res.res, PerAppt] THEN delt ¬ 2*MIN[delt, (LAST[INT]-sofar)/4]; sofar ¬ qmax; ENDLOOP; NULL--[MAX[now, min], max] queried--}; cachedIntervals => { stack: REF RT.Range ¬ NEW [RT.Range ¬ [min, max, NIL]]; WHILE stack#NIL DO probe: REF RT.Range ¬ stack; bad: BOOL ¬ IntervalIsBad[usedDbName, probe]; stack ¬ stack.next; probe.next ¬ NIL; IF NOT bad THEN { args: RT.Args ~ NEW [RT.ArgsObject[RANGE] ¬ [RANGE[probe]]]; res: RT.TableRes ~ rt.rtableabbreviatedlookuprange[rt, target, args, UnixSysCalls.GetPID[] !SunRPC.Error => IF probe.key2-probe.key1 <= 86400 THEN { SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "cm lookuprange[%g, %g] for %g on %g => RPC.Error[%g]", LIST[[integer[probe.key1]], [integer[probe.key2]], [rope[for]], [rope[usedDbName]], [atom[code]]] ]; EXIT} ELSE { SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Warning, "cm lookuprange[%g, %g] for %g on %g -> RPC.Error[%g], trying smaller range", LIST[[integer[probe.key1]], [integer[probe.key2]], [rope[for]], [rope[usedDbName]], [atom[code]]] ]; GOTO Bad}]; IF watchAS THEN SELECT res.status FROM accessok => NULL; accesspartial => GOTO Bad; ENDCASE => IF probe.key2-probe.key1 <= 86400 THEN { SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "cm lookuprange[%g, %g] for %g on %g => status = %g", LIST[[integer[probe.key1]], [integer[probe.key2]], [rope[for]], [rope[usedDbName]], [rope[RT.AccessStatusNames[res.status]]]] ]; EXIT} ELSE { mid: CARD ~ (CARD[probe.key1]+CARD[probe.key2])/2; SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Warning, "cm lookuprange[%g, %g] for %g on %g -> status = %g, trying smaller range", LIST[[integer[probe.key1]], [integer[probe.key2]], [rope[for]], [rope[usedDbName]], [rope[RT.AccessStatusNames[res.status]]]] ]; GOTO Bad}; [] ¬ EnumerateAppts[rt, res.res, PerAppt]; EXITS Bad => { AssertBad[usedDbName, probe]; bad ¬ TRUE}}; IF bad THEN { mid: CARD ~ (CARD[probe.key1]+CARD[probe.key2])/2; stack ¬ NEW[RT.Range ¬ [probe.key1, mid, NEW[RT.Range ¬ [mid, probe.key2, stack]] ]]}; ENDLOOP}; nextLarger => { seekTick: INT ¬ min; args: RT.Args ¬ NEW [RT.ArgsObject[TICK3] ¬ [TICK3[min]]]; DO res: RT.TableRes ~ rt.rtablelookupnextlarger[rt, target, args, UnixSysCalls.GetPID[] !SunRPC.Error => { SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "lookup[%x=%g] for scan (for %g) failed (%g)", LIST[[integer[seekTick]], [time[UnixToBasicTime[seekTick]]], [rope[for]], [atom[code]]] ]; EXIT}]; IF watchAS THEN SELECT res.status FROM accessok => NULL; ENDCASE => { IF debug THEN SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Debug, "LookupNextLarger[tick=%x=%g] => status=%g", LIST[[integer[seekTick]], [time[UnixToBasicTime[seekTick]]], [rope[RT.AccessStatusNames[res.status]]]] ]; EXIT}; WITH res.res SELECT FROM x: REF RT.TableResListObject[AP] => IF x.a=NIL THEN { IF debug THEN SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Debug, "LookupNextLarger[tick=%x=%g] => NIL", LIST[[integer[seekTick]], [time[UnixToBasicTime[seekTick]]]] ]; EXIT} ELSE { IF debug THEN SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Debug, "LookupNextLarger[tick=%x=%g] => tick=%x=%g key=%g", LIST[[integer[seekTick]], [time[UnixToBasicTime[seekTick]]], [integer[x.a.apptid.tick]], [time[UnixToBasicTime[x.a.apptid.tick]]], [integer[x.a.apptid.key]]] ]; IF x.a.apptid.tick > max THEN EXIT; IF PerAppt[x.a] THEN EXIT; seekTick ¬ x.a.apptid.tick; args ¬ NEW[RT.ArgsObject[TICK3] ¬ [TICK3[x.a.apptid.tick]]]}; ENDCASE => { SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "scanning unexpected variant (%g) (for %g)", LIST[[rope[RT.TableResTypeNames[res.res.tag]]], [rope[for]]] ]; EXIT}; ENDLOOP}; ENDCASE => ERROR; RETURN}; DeleteMeeting: PUBLIC PROC[start: BasicTime.GMT, id: INT, closeDb: BOOLEAN ¬ TRUE, dbName: Rope.ROPE ¬ NIL] RETURNS [found: BOOLEAN ¬ FALSE] = {RETURN DeleteMeetingInRange[start, start, id, closeDb, dbName]}; DeleteMeetingInRange: PUBLIC PROC[from, to: BasicTime.GMT, id: INT, closeDb: BOOLEAN ¬ TRUE, dbName: Rope.ROPE ¬ NIL] RETURNS [found: BOOLEAN ¬ FALSE] = { db: LoganBerry.OpenDB; rt: RTable; usedDbName: ROPE; DeleteThis: PROC [start: BasicTime.GMT, unixStart: INT] RETURNS [found: BOOL ¬ FALSE] ~ { args: RT.Args ¬ NEW [RT.ArgsObject[UID] ¬ [UID[NEW[RT.Uid ¬ [[tick: unixStart, key: id], NIL]] ]]]; res: RT.TableRes ¬ rt.rtabledelete[rt, target, args, UnixSysCalls.GetPID[] !SunRPC.Error => { SimpleFeedback.PutF[$RemindImpl, oneLiner, $Error, "delete rpc failed (%g)", [atom[code]] ]; GOTO Bail}]; SELECT res.status FROM accessok, accessremoved => found ¬ TRUE; ENDCASE => { found ¬ FALSE; IF watchAS THEN SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "Delete (by id) of message (tick=%xH=%g, key=%g) failed (status = %g)", LIST[[integer[unixStart]], [time[UnixToBasicTime[unixStart]]], [integer[id]], [rope[RT.AccessStatusNames[res.status]]]] ]; }; EXITS Bail => found ¬ FALSE}; [db, rt, usedDbName] ¬ OpenDb[name: dbName]; IF db # LoganBerry.nullDB THEN { entry: LoganBerry.Entry; idVal: ROPE ~ Convert.RopeFromInt[id]; others: BOOL; [entry, others] ¬ LoganBerry.ReadEntry[db: db, key: $uniqID, value: idVal]; IF others THEN ERROR; IF entry#NIL THEN { LoganBerry.DeleteEntry[db: db, key: $uniqID, value: idVal]; found ¬ TRUE; }; IF closeDb THEN CloseLoganBerry[db]; -- so autobackup will see a change RETURN [found]} ELSE IF rt=NIL THEN RETURN [FALSE] ELSE IF from=to THEN RETURN DeleteThis[from, BasicToUnixTime[from]] ELSE { unixFrom: INT ~ BasicToUnixTime[from]; unixTo: INT ~ BasicToUnixTime[to]; args: RT.Args ¬ NEW [RT.ArgsObject[KEYRANGE] ¬ [KEYRANGE[NEW[RT.Keyrange ¬ [key: id, tick1: unixFrom, tick2: unixTo, next: NIL]] ]]]; res: RT.TableRes ¬ rt.rtableabbreviatedlookupkeyrange[rt, target, args, UnixSysCalls.GetPID[] !SunRPC.Error => { SimpleFeedback.PutF[$RemindImpl, oneLiner, $Error, "abb-lookup-keyrange rpc failed (%g)", [atom[code]] ]; GOTO Bail}]; IF watchAS THEN SELECT res.status FROM accessok => NULL; ENDCASE => { IF debug THEN SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Debug, "abb-lookup-keyrange[key=%g, tick1=%g, tick2=%g] => status=%g", LIST[[integer[id]], [time[from]], [time[to]], [rope[RT.AccessStatusNames[res.status]]]] ]; GOTO Bail}; WITH res.res SELECT FROM x: REF RT.TableResListObject[AB] => { IF x.b=NIL THEN found ¬ FALSE ELSE found ¬ DeleteThis[UnixToBasicTime[x.b.apptid.tick], x.b.apptid.tick]}; ENDCASE => SimpleFeedback.PutF[$RemindImpl, oneLiner, $Error, "delete-in-range got unexpected variant (%g)", [rope[RT.TableResTypeNames[res.res.tag]]] ]; RETURN; EXITS Bail => found ¬ FALSE}; }; < id ¬ aList.first.value; $start => IF TimeFromInt[Convert.IntFromRope[aList.first.value]] = meeting.start THEN startMatch ¬ TRUE; $explanation => IF Rope.Equal[meeting.explanation, aList.first.value] THEN explMatch ¬ TRUE; ENDCASE => {}; ENDLOOP; IF id.Equal[stopID] THEN stops ¬ stops+1; IF explMatch AND startMatch AND id#NIL THEN { LoganBerry.DeleteEntry[db: db, key: $uniqID, value: id]; found ¬ TRUE; }; ENDLOOP; LoganBerry.EndGenerate[cursor: cursor]; IF closeDb THEN CloseLoganBerry[db]; -- so autobackup will see a change RETURN [found]} ELSE IF rt#NIL THEN { doomedTicks: INT ¬ BasicToUnixTime[meeting.start]; doomed: LIST OF REF RT.Appt ¬ NIL; NoteAppt: PROC [appt: REF RT.Appt] RETURNS [BOOL] ~ { IF appt.apptid.key = meeting.uniqID THEN { doomed ¬ CONS[appt, doomed]; found ¬ TRUE}; RETURN[FALSE]}; ScanAppts[rt, usedDbName, NoteAppt, "DeleteMeeting", doomedTicks, doomedTicks]; FOR dl: LIST OF REF RT.Appt ¬ doomed, dl.rest WHILE dl#NIL DO args: RT.Args ¬ NEW [RT.ArgsObject[UID] ¬ [UID[NEW[RT.Uid ¬ [dl.first.apptid, NIL]] ]]]; res: RT.TableRes ¬ rt.rtabledelete[rt, target, args, UnixSysCalls.GetPID[] !SunRPC.Error => { SimpleFeedback.PutF[$RemindImpl, oneLiner, $Error, "delete rpc failed (%g)", [atom[code]] ]; EXIT}]; IF watchAS THEN SELECT res.status FROM accessok, accessremoved => NULL; ENDCASE => SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "Delete of message (tick=%xH=%g, key=%g) failed (status = %g)", LIST[[integer[dl.first.apptid.tick]], [time[UnixToBasicTime[dl.first.apptid.tick]]], [integer[dl.first.apptid.key]], [rope[RT.AccessStatusNames[res.status]]]] ]; ENDLOOP; } ELSE RETURN [FALSE]}; stopID: ROPE ¬ NIL; stops: CARD ¬ 0; DeleteMeetingByID: PUBLIC PROC[from, to: BasicTime.GMT, id: INT, closeDb: BOOLEAN ¬ TRUE, dbName: Rope.ROPE ¬ NIL] RETURNS [found: BOOLEAN ¬ FALSE] = { db: LoganBerry.OpenDB; rt: RTable; usedDbName: ROPE; [db, rt, usedDbName] ¬ OpenDb[name: dbName]; IF db # LoganBerry.nullDB THEN { cursor: LoganBerry.Cursor; entry: LoganBerry.Entry; cursor ¬ LoganBerry.GenerateEntries[db: db, key: $uniqID]; BEGIN -- establish scope for EXIT clause WHILE (entry ¬ LoganBerry.NextEntry[cursor: cursor]) # NIL DO FOR aList: LIST OF LoganBerry.Attribute ¬ entry, aList.rest UNTIL aList = NIL DO IF aList.first.type = $uniqID THEN { IF stopID.Equal[aList.first.value] THEN stops ¬ stops+1; IF Convert.IntFromRope[aList.first.value] = id THEN { LoganBerry.DeleteEntry[db: db, key: $uniqID, value: Convert.RopeFromInt[id]]; found ¬ TRUE; GOTO done; } ELSE EXIT; }; ENDLOOP; ENDLOOP; EXITS done => {}; END; LoganBerry.EndGenerate[cursor: cursor]; IF closeDb THEN CloseLoganBerry[db]; -- so autobackup will see a change RETURN [found]} ELSE IF rt#NIL THEN { qmin: INT ~ BasicToUnixTime[from]; qmax: INT ~ BasicToUnixTime[to]; doomed: LIST OF REF RT.Appt ¬ NIL; NoteAppt: PROC [appt: REF RT.Appt] RETURNS [BOOL] ~ { IF appt.apptid.key = id THEN doomed ¬ CONS[appt, doomed]; RETURN[FALSE]}; ScanAppts[rt, usedDbName, NoteAppt, "DeleteMeetingByID", qmin, qmax]; FOR dl: LIST OF REF RT.Appt ¬ doomed, dl.rest WHILE dl#NIL DO args: RT.Args ¬ NEW [RT.ArgsObject[UID] ¬ [UID[NEW[RT.Uid ¬ [dl.first.apptid, NIL]] ]]]; res: RT.TableRes ¬ rt.rtabledelete[rt, target, args, UnixSysCalls.GetPID[] !SunRPC.Error => { SimpleFeedback.PutF[$RemindImpl, oneLiner, $Error, "delete rpc failed (%g)", [atom[code]] ]; EXIT}]; IF watchAS THEN SELECT res.status FROM accessok, accessremoved => NULL; ENDCASE => SimpleFeedback.PutFL[$RemindImpl, oneLiner, $Error, "Delete (by id) of message (tick=%xH=%g, key=%g) failed (status = %g)", LIST[[integer[dl.first.apptid.tick]], [time[UnixToBasicTime[dl.first.apptid.tick]]], [integer[dl.first.apptid.key]], [rope[RT.AccessStatusNames[res.status]]]] ]; ENDLOOP; } ELSE RETURN [FALSE]};>> Create: PROC = { userCred: ROPE ¬ SystemNames.UserName[]; s: IO.STREAM; baseName, logName: ROPE; credIndex: INT; ind: INT ¬ Rope.FindBackward[defaultDbName, "/"]; SimpleFeedback.PutF[$Remind, oneLiner, $Info, "Creating CalendarTool database file: %g\n", [rope[defaultDbName]] ]; IF ind # -1 THEN baseName ¬ Rope.Substr[defaultDbName, ind+1] ELSE baseName ¬ defaultDbName; baseName ¬ Rope.Substr[baseName, 0, Rope.Index[baseName, 0, "."]]; s ¬ FS.StreamOpen[defaultDbName, $create]; s.PutF1["-- %g\n\n", IO.rope[defaultDbName]]; credIndex ¬ Rope.FindBackward[userCred, ".pa"]; IF credIndex # -1 THEN userCred ¬ Rope.Substr[userCred, 0, credIndex]; s.PutF1["Directory [User]<%g>Top>\n", IO.rope[userCred]]; s.PutF1["\t\t%g\n", IO.rope[Rope.Substr[defaultDbName, Rope.FindBackward[defaultDbName, "/"]+1]]]; s.PutF["Directory [User]<%g>%g>\n", IO.rope[userCred], IO.rope[baseName]]; s.PutF1[" --> log 0 readwrite\n\t\t%g.lblog\n", IO.rope[baseName]]; s.PutF1[" --> index \"uniqID\" primary\n\t\t%g.uniqID.lbindex\n", IO.rope[baseName]]; s.PutF1[" --> index \"start\" secondary\n\t\t%g.start.lbindex\n", IO.rope[baseName]]; s.PutF1[" --> index \"repeat\" secondary\n\t\t%g.repeat.lbindex\n", IO.rope[baseName]]; s.PutF1[" --> index \"stopReminding\" secondary\n\t\t%g.stopReminding.lbindex\n", IO.rope[baseName]]; IO.Close[s]; logName ¬ Rope.Cat[Rope.Substr[defaultDbName, 0, ind+1], baseName, ".lblog"]; s ¬ FS.StreamOpen[logName, $create]; s.PutRope["\377"]; IO.Close[s]; }; NextOccurrence: PUBLIC PROC[repeat: Repetitions, nth: INT, time: BasicTime.GMT] RETURNS [BasicTime.GMT] = { SELECT repeat FROM once => RETURN[time]; daily, everyNthDay => RETURN[TimeParse.Adjust[baseTime: time, precisionOfResult: seconds, days: IF repeat=daily THEN 1 ELSE nth].time]; weekly, everyNthWeek => RETURN[TimeParse.Adjust[baseTime: time, precisionOfResult: seconds, days: IF repeat=weekly THEN 7 ELSE (nth*7)].time]; biweekly => RETURN[TimeParse.Adjust[baseTime: time, precisionOfResult: seconds, days: 14].time]; monthly, everyNthMonth => RETURN[TimeParse.Adjust[baseTime: time, precisionOfResult: seconds, months: IF repeat=monthly THEN 1 ELSE nth].time]; yearly => RETURN[TimeParse.Adjust[baseTime: time, precisionOfResult: seconds, years: 1].time]; weekdays, nthWeekday => { t: BasicTime.GMT ¬ time; IF repeat=weekdays THEN nth ¬ 1; WHILE TRUE DO t ¬ TimeParse.Adjust[baseTime: t, precisionOfResult: seconds, days: 1].time; IF BasicTime.Unpack[t].weekday IN [Monday .. Friday] THEN { nth ¬ nth - 1; IF nth=0 THEN EXIT}; ENDLOOP; RETURN[t]; }; ENDCASE => RETURN[time]; -- should never get here }; FirstOccurrenceAfter: PUBLIC PROC[repeat: Repetitions, nth: INT, init, now: BasicTime.GMT] RETURNS [BasicTime.GMT] = { initMinus: BasicTime.GMT; secondsPerDay: INT = BasicTime.secondsPerMinute*BasicTime.minutesPerHour*BasicTime.hoursPerDay; secondsPerWeek: INT = secondsPerDay*7; secondsPerTwoWeeks: INT = secondsPerWeek*2; secondsPerYear: INT = secondsPerDay*365; diff: INT; ByDay: PROC [days: INT] RETURNS [BasicTime.GMT] ~ { secsPerPeriod: INT ~ secondsPerDay*days; diff: INT ¬ BasicTime.Period[init, now]/secsPerPeriod + 1; init ¬ TimeParse.Adjust[baseTime: init, days: diff*days, precisionOfResult: seconds].time; IF BasicTime.Period[now, init] < 0 THEN init ¬ TimeParse.Adjust[baseTime: init, days: days, precisionOfResult: seconds].time ELSE { initMinus ¬ TimeParse.Adjust[baseTime: init, days: -days, precisionOfResult: seconds].time; IF BasicTime.Period[now, initMinus] >= 0 THEN init ¬ initMinus; }; RETURN[init]}; SELECT repeat FROM once => RETURN[init]; daily => RETURN ByDay[1]; everyNthDay => RETURN ByDay[nth]; weekly => RETURN ByDay[7]; biweekly => RETURN ByDay[14]; everyNthWeek => RETURN ByDay[7*nth]; monthly, everyNthMonth => { uInit, uNow: BasicTime.Unpacked; secsInit, secsNow: INT; IF repeat=monthly THEN nth ¬ 1; uInit ¬ BasicTime.Unpack[init]; uNow ¬ BasicTime.Unpack[now]; diff ¬ (uNow.year - uInit.year)*12 + ORD[uNow.month] - ORD[uInit.month]; diff ¬ ((diff+nth-1)/nth)*nth; secsInit ¬ uInit.second + 60*uInit.minute + 60*60*uInit.hour + secondsPerDay*ORD[uInit.day]; secsNow ¬ uNow.second + 60*uNow.minute + 60*60*uNow.hour + secondsPerDay*ORD[uNow.day]; IF secsInit < secsNow THEN diff ¬ diff + nth; init ¬ TimeParse.Adjust[baseTime: init, months: diff, precisionOfResult: seconds].time; RETURN[init]; }; yearly => { diff ¬ BasicTime.Period[init, now]/secondsPerYear + 1; init ¬ TimeParse.Adjust[baseTime: init, years: diff, precisionOfResult: seconds].time; IF BasicTime.Period[now, init] < 0 THEN init ¬ TimeParse.Adjust[baseTime: init, years: 1, precisionOfResult: seconds].time ELSE { initMinus ¬ TimeParse.Adjust[baseTime: init, years: -1, precisionOfResult: seconds].time; IF BasicTime.Period[now, initMinus] >= 0 THEN init ¬ initMinus; }; RETURN[init]; }; weekdays, nthWeekday => { u: BasicTime.Unpacked; IF repeat=weekdays THEN nth ¬ 1; DO init ¬ ByDay[1]; u ¬ BasicTime.Unpack[init]; IF u.weekday= Saturday THEN init ¬ TimeParse.Adjust[baseTime: init, days: 2, precisionOfResult: seconds].time ELSE IF u.weekday= Sunday THEN init ¬ TimeParse.Adjust[baseTime: init, days: 1, precisionOfResult: seconds].time; IF (nth ¬ nth-1) = 0 THEN RETURN[init]; ENDLOOP; }; ENDCASE => RETURN[init]; -- should never get here }; UpdateMeeting: PROC[meeting: Meeting, newTime: BasicTime.GMT, remindersToo, newReminders: BOOL] = { diff: INT ¬ BasicTime.Period[meeting.start, newTime]; UpdateReminders: PROC [rl: RemindList] RETURNS [RemindList] ~ { IF rl=NIL THEN RETURN [NIL]; WITH rl.first SELECT FROM x: AlertReminder => RETURN [CONS[ NEW [ReminderRecord[alert] ¬ [x.other, alert[ start: BasicTime.Update[x.start, diff], stop: BasicTime.Update[x.stop, diff] ]]], UpdateReminders[rl.rest] ]]; x: MailReminder => RETURN [CONS[ NEW [ReminderRecord[mail] ¬ [x.other, mail[ when: BasicTime.Update[x.when, diff], to: x.to ]]], UpdateReminders[rl.rest] ]]; ENDCASE => ERROR}; meeting.start ¬ newTime; SELECT TRUE FROM NOT remindersToo => NULL; NOT newReminders => FOR rl: RemindList ¬ meeting.reminders, rl.rest WHILE rl#NIL DO WITH rl.first SELECT FROM x: AlertReminder => { x.start ¬ BasicTime.Update[x.start, diff]; x.stop ¬ BasicTime.Update[x.stop, diff]}; x: MailReminder => { x.when ¬ BasicTime.Update[x.when, diff]}; ENDCASE => ERROR; ENDLOOP; ENDCASE => meeting.reminders ¬ UpdateReminders[meeting.reminders]; RETURN}; CompactDb: PUBLIC PROC[dbName: ROPE ¬ NIL] = { db: LoganBerry.OpenDB; rt: RTable; [db, rt, ] ¬ OpenDb[name: dbName]; IF db = LoganBerry.nullDB THEN RETURN; LoganBerry.CompactLogs[db: db]; }; dbnToBadIntervals : SymTab.Ref--usedDbName => badIntervals: RedBlackTree.Table-- ~ SymTab.Create[case: TRUE]; GetTable: PROC [usedDbName: ROPE] RETURNS [bads: RedBlackTree.Table ¬ NIL] ~ { AddIfNeeded: PROC [found: BOOL, val: REF ANY] RETURNS [op: SymTab.UpdateOperation ¬ none, new: REF ANY ¬ NIL] ~ { IF found THEN {bads ¬ NARROW[val]; op ¬ none} ELSE { new ¬ bads ¬ RedBlackTree.Create[GetIntervalKey, CompareIntervals]; op ¬ store}}; dbnToBadIntervals.Update[usedDbName, AddIfNeeded]; RETURN}; AssertBad: PROC [usedDbName: ROPE, i: REF RT.Range] ~ { badIntervals: RedBlackTree.Table ~ GetTable[usedDbName]; --Assert: i.next = NIL-- badIntervals.Insert[i, i]; RETURN}; IntervalIsBad: PROC [usedDbName: ROPE, i: REF RT.Range] RETURNS [BOOL] ~ { badIntervals: RedBlackTree.Table ~ GetTable[usedDbName]; data, eqData: REF ANY; [, eqData, data] ¬ badIntervals.Lookup3[i]; IF eqData#NIL THEN RETURN [TRUE]; WHILE data#NIL DO fr: REF RT.Range ~ NARROW[data]; IF fr.key1 > i.key2 THEN RETURN [FALSE]; IF fr.key2 <= i.key2 THEN RETURN [TRUE]; data ¬ badIntervals.LookupNextLarger[data]; ENDLOOP; RETURN [FALSE]}; GetIntervalKey: PROC [data: REF ANY] RETURNS [REF ANY] ~ {RETURN [data]}; CompareIntervals: PROC [k, data: REF ANY] RETURNS [Basics.Comparison] ~ { k1: REF RT.Range ~ NARROW[k]; k2: REF RT.Range ~ NARROW[data]; SELECT TRUE FROM k1.key1 RETURN [less]; k1.key1>k2.key1 => RETURN [greater]; k1.key2>k2.key2 => RETURN [less]; k1.key2 RETURN [greater]; ENDCASE => RETURN [equal]}; CloseDb: PUBLIC PROC[dbName: ROPE ¬ NIL] = { db: LoganBerry.OpenDB; rt: RTable; [db, rt, ] ¬ OpenDb[name: dbName]; IF db = LoganBerry.nullDB THEN RETURN; CloseLoganBerry[db]; }; CloseLoganBerry: PROC[db: LoganBerry.OpenDB] = { LoganBerry.Close[db: db]; }; ChangeLoganBerryDbName: UserProfile.ProfileChangedProc = { newDbName: ROPE ¬ UserProfile.Token["Remind.DbName", UserProfile.Token["Remind.DfFile", NIL]]; IF Rope.IsEmpty[newDbName] THEN newDbName ¬ Rope.Concat[SystemNames.UserCedarDir[NIL], defaultName]; IF NOT Rope.Equal[s1: defaultDbName, s2: newDbName, case: FALSE] THEN { IF (NOT Rope.IsEmpty[defaultDbName]) AND (defaultDbName.Find["@"] < 0) THEN { db: LoganBerry.OpenDB ¬ OpenDb[name: defaultDbName].db; IF db # LoganBerry.nullDB THEN { LoganBerry.UnregisterWriteProc[db, $Remind]; LoganBerry.Close[db: db]; }; }; defaultDbName ¬ newDbName; DatabaseChanged[LoganBerry.nullDB, NIL, NIL, NIL]; }; }; defaultDbName: ROPE ¬ NIL; target: ROPE ¬ NIL; defaultName: ROPE ¬ "CalendarBerry.df"; ProcBlockRec: TYPE = RECORD[ proc: PROC[data: REF], dbName: ROPE, -- perhaps should instead have blocklist per database? data: REF ANY, ident: ATOM]; ProcBlock: TYPE = REF ProcBlockRec; changeProcList: List.LORA ¬ NIL; -- LIST OF ProcBlock Commander.Register["RemindImpl.TimeFromUnix", TimeFromUnixCmd, " -- invokes RemindImpl.UnixToBasicTime"]; Commander.Register["RemindImpl.TimeFromInt", TimeFromIntCmd, " -- invokes RemindImpl.TimeFromInt"]; Commander.Register["­GMT", GMTUpCmd, " -- the abstraction function of the BasicTime.GMT representation scheme"]; UserProfile.CallWhenProfileChanges[ChangeLoganBerryDbName]; END. Œ RemindImpl.mesa Copyright Ó 1990, 1992 by Xerox Corporation. All rights reserved. David Goldberg November 13, 1989 7:59:49 pm PST Peter B. Kessler, January 17, 1990 9:11:14 pm PST Last changed by Pavel on February 12, 1990 5:40:45 pm PST Kenneth A. Pier, April 19, 1990 10:56 am PDT Theimer, September 17, 1990 11:22 pm PDT Last tweaked by Mike Spreitzer August 27, 1993 8:19 am PDT Willie-s, May 6, 1992 1:11 pm PDT Implements the remind interface, and also contains the PostReminders proc, which runs in the background and pops up reminders - Next 3 routines are candidates for INLINE Meetings are stored with meeting.start converted to a 0-padded INT in the start field. The zero-padding is so that loganberry compares yield time order. convert time to an integer and use this as a unique record id XXX: will changing timezones goof this up? -- I think the check for DBClosed is obsolete LoganBerry.WriteEntry[db: db, entry: entry ! LoganBerry.Error => {IF ec = $DBClosed THEN {OpenLoganBerry[name: dbName]; RETRY} ELSE REJECT} ]; XXX:XXX make two passes -XXX: this is a royal kludge: FirstOccurrenceAfter substracts by 1 year, so add 1 year here to avoid BasicTimeImpl.OutOfRange ERROR (note that TimeParse.Adjust won't work, because it interprets earliestGMT as Now). first pass: find all one time meetings second pass: find all repeated meetings XXX:XXX XXX:XXX because of daylights savings time, might be off by 1 compute seconds since beginning of month because of leap years, might be off by 1 No interval in a badIntervals is a sub-interval of another. LoganBerryExtras.UnregisterWriteProc[db, $Remind]; PROC [reason: UserProfile.ProfileChangeReason] Ê1®•NewlineDelimiter –(cedarcode) style™codešœ™Iproc– "Cedar" stylešœ Ïeœ7™BK™/K™1K™9K™,K™(K™:K™!K™—šÏk ˜ Kšœžœ ˜Kšœ˜Kšœ ˜ K˜Kšœ˜Kšžœžœ˜K˜ Kšžœ˜Kšœžœžœ ˜Kšœ žœÐ˜àK˜ K˜ Kšœ˜Kšœ˜K˜K˜Kšœ˜Kšœ˜K˜K˜ K˜ K˜Kšœ žœ˜+Kšœ žœ ˜Kšœ žœ ˜Kšœ žœ5˜FK˜K˜—KšÐln œžœž˜Kšžœ6žœžœ¿˜‘šžœ˜K˜Kšœ}™}K™—K˜Kšœžœžœ žœ ˜ K˜Kšžœžœžœ˜Kšœžœžœž œ˜K˜šœžœ žœžœ˜.Kšœ˜—K˜š Ïnœžœžœžœžœ˜DKšžœ˜Kšœ˜—K˜š  œžœžœžœžœ˜Dšžœžœ ž˜&K•StartOfExpansion-[s1: ROPE, s2: ROPE, case: BOOL _ TRUE]šžœ'žœžœžœ˜AKšžœ˜—Kšžœ˜ Kšœ˜—K˜Kšœžœ žœžœ>˜iK˜š  œžœžœžœžœ˜FKšžœ˜Kšœ˜—K˜š  œžœžœžœžœ˜Ešžœžœ ž˜'K–-[s1: ROPE, s2: ROPE, case: BOOL _ TRUE]šžœ)žœžœžœ˜DKšžœ˜—Kšžœ ˜Kšœ˜—K˜Kš œžœžœžœ˜K˜Kš  œžœ žœžœ€˜¥Kš  œžœžœ žœò˜”K˜Kšœ%Ïs™+š   œžœžœžœžœ˜8Kšžœ1˜7Kšœ˜—K˜š   œžœžœžœ žœ˜7Kšžœ/˜5Kšœ˜—K˜š   œžœžœžœžœ˜0Kšžœžœžœ ˜(Kšœ˜—K˜š œžœžœ žœžœžœžœžœÏcœ˜|K˜K˜K˜šžœ&žœ˜.Kšœ žœ!˜/šœžœ0žœ˜cKšœpžœ ˜”Kšžœ ˜—˜Ašœ˜Kšœžœžœ>˜ÖKšžœ˜ ——K˜XK˜cKš œžœ%žœž œžœž œ˜fKšœ˜šžœžœ˜K˜…Kšžœ˜ —K˜Ušœžœ ˜'šœ˜Kšœ`žœ#˜‡Kšžœ˜ —šœ˜Kšœdžœ#˜‹Kšžœ˜ ——˜šœ˜Kšœ\žœ#˜ƒKšžœ˜ —šœ˜Kšœ`žœ#˜‡Kšžœ˜ ——K˜K˜—šžœ˜Kšœ žœ˜ ˜;šžœžœ ˜ šžœ˜K˜Kšœžœ˜Kšžœ˜Kšœ˜—šžœ˜šžœžœžœ˜1šžœžœ ˜šžœ˜Kšœ ˜ K˜Kšžœ˜Kšœ˜—Kšžœžœ˜—K˜—K˜——K˜—šžœ žœ˜Kšž˜Kšœ ˜ Kšžœ˜—K˜AKšœ˜—Kšžœ˜šž˜Kšœ žœ˜—Kšœ˜—K˜š  œžœžœžœžœ žœžœžœžœ˜lKšœ˜K˜ Kšœžœ˜.šžœžœ˜Kšœ?¡œX™šKšœžœ˜š œžœžœžœ˜%š žœžœžœžœžœ˜-Kšœ˜K˜K˜Kšœžœ˜K˜—Kšœ˜—Kšœ=™=K˜"Kšœ'˜'Kšœ*™*Kšœ,˜,Kšœ8˜8Kšœ8˜8Kšœ(˜(Kšœ/˜/šžœ-žœžœž˜?šžœ žœž˜šœžœ˜!Kšœ9˜9Kšœ;˜;K˜—šœžœ˜ Kšœ˜Kšœ2˜2K˜—Kšžœžœ˜—Kšžœ˜—Kšœ˜Kšœ$˜$Kšœ&˜&K˜4K˜K– 4 in tabStopsšœ-™-– 4 in tabStopsšœ@™@K– 4 in tabStopsš œžœžœ žœžœžœ™JK– 4 in tabStopsšœ™—K– 4 in tabStopsšœ,˜,Kšœ™Kšžœ žœ¢"˜GKšœ˜—šžœžœžœžœ˜Kšœžœžœ˜Kšœžœ˜Kšœžœ ˜Kšœžœ9˜?Kš œžœžœžœžœ-žœ˜Lšœžœžœ6˜Cšžœžœ˜Kšœžœ˜ K˜K˜Kšœ žœ˜——šžœ žœžœ ˜Kšœ7˜7Kšœ ˜ Kšœ˜Kšœžœžœžœ ˜5Kšœ˜Kšœ ˜ Kšœžœ˜ Kšœ žœ˜Kšœ žœ˜Kšœ ˜ K˜Kšœ žœžœžœ ˜4Kšœžœ˜ —Kš œžœžœ žœžœ ˜1˜QK˜cKšžœ˜ —šžœ žœžœ ž˜&Kšœžœ˜šžœ˜ Kšœižœ.žœ$˜ÁKšžœ˜ ——šžœ žœž˜Kšœžœžœ/˜8Kšœžœžœ/˜8šžœ˜ Kšœyžœ*˜§Kšžœ˜ ——Kšžœ˜K˜—šžœ˜Kšœ\˜\K˜—K˜—K˜š  œžœ*žœžœ žœžœžœ˜mšžœžœžœž˜$šœžœžœ žœžœ žœ žœž˜Dšœžœ˜!Kšœ ˜ K˜ KšœO˜OKšœ žœ˜—šœžœ˜ Kšœ ˜ K˜ KšœN˜NKšœ˜—Kšžœžœ˜—K˜ Kšžœ˜—Kšžœ ˜—K˜Kšœ žœ˜Kšœ žœ˜Kšœ žœ˜K˜š   œžœ žœžœžœ˜XKšœžœžœ˜Kšœ žœ ˜šžœžœž˜Kšœžœ˜Kšœžœ˜ K˜šžœž˜˜ K˜,Kšžœžœ žœ˜.—K˜3šœžœ˜/K˜Kšžœžœ žœ˜#—K˜K˜Kšœžœ˜/—Kšžœžœ ˜—K˜K˜(K˜*K˜6˜ Kšœžœžœ˜9K˜1šžœžœžœžœ˜0K˜K˜—Kšžœ˜Kšœžœ˜0—Kšžœžœ ˜—Kšžœ˜—Kšžœžœ˜*Kšžœ˜—K˜Kšœ™š  œžœžœ˜Kšœžœ˜Kšœžœžœ˜Kšœžœž˜—š œžœžœžœ žœ˜)Kšœ˜K˜Kšœ˜Kšœžœ˜ Kšœ˜K˜ Kšœ žœ˜K˜K˜,Kšœ œ¤™ÖKšžœTžœJ˜¤K˜šžœžœ˜Kšœ'™'˜$Kšœ˜K˜)K˜%Kšœ ˜ Kšœ˜—šžœ2žœž˜=K˜Kšžœžœžœ˜5Kšžœ˜—Kšœ'˜'Kšœ(™(šžœžœ žœž˜5˜$Kšœ˜Kšœ¢˜9Kšœ˜Kšœ˜—šžœ2žœž˜=Kšœžœžœ˜Kšœžœ˜K˜K˜Gšžœž˜%Kšœ!žœ˜5Kšœžœ˜Kšžœžœžœžœ˜K˜Kšœ žœ˜&K˜;Kšœžœ˜Kšžœ˜—Kšžœ˜—Kšœ'˜'Kšžœ˜—K˜—šžœžœžœžœ˜šœžœ ˜Kšœ˜Kšœ˜Kšœžœ˜ —Kš œžœžœžœžœ˜Fš  œžœžœžœžœžœ˜5šžœžœžœžœ"˜nKšžœžœ˜$—Kšžœžœ˜—KšœL˜LKšžœ˜—Kšœ˜—K™š   œžœžœžœžœ˜>šœžœ˜Kšœ˜K˜Kšœ)˜)Kšœ žœ˜(Kšœ˜Kšœ)˜)K˜Kšœžœ˜ —š žœžœžœ"žœžœž˜CKšžœžœ˜;š žœžœžœžœž˜>šœžœ ˜'Kšœ žœžœ˜Kšžœžœžœ˜@Kšžœžœžœ˜@Kšžœžœžœ˜@Kšžœžœžœ˜?šžœžœ˜Kšœžœ4žœ˜FKšœžœ%˜9šžœžœ˜Kšœžœžœ˜DKšœžœ˜(—šžœžœ˜Kšœžœžœ!˜IKšœžœ˜(—Kšœ˜—Kšžœ˜—Kšžœžœ˜—Kšžœ˜—Kšžœ˜—K˜š œžœžœ œžœžœžœžœžœžœ žœžœ˜„š  œžœžœžœžœžœžœ˜MKšœžœ ˜˜OKšœžœf˜éKšžœ˜ —šžœ žœžœ ž˜'Kšœžœ˜šžœ˜ Kšœ…žœ\žœ%˜ŒKšžœžœ˜——šžœ žœž˜Kšœžœžœ/˜8šžœ˜ Kšœžœ\žœ&˜—Kšžœžœ˜——Kšžœ˜Kšžœžœ˜—šžœžœž˜šœžœžœ˜%š žœžœžœžœžœž˜8Kšœžœ˜šžœž˜˜ Kšžœžœ_žœz˜êKšžœžœžœ˜K˜—˜ Kšžœžœcžœz˜îK˜—˜ Kšžœžœbžœz˜íK˜—Kšžœžœ˜—Kšžœ˜ ——šœžœžœ˜%š žœžœžœžœžœž˜8Kšœžœ˜Kšžœžœcžœv˜êKšžœ žœžœžœžœžœžœžœ˜gKšžœ˜ ——šœžœžœ˜%š žœžœžœžœžœž˜4Kšœžœ˜KšžœžœUžœd˜ÊKš žœ žœžœ-žœžœ˜JKšžœ˜ ——Kšžœ†˜—K˜—K˜Kšœ žœ ˜K˜Kšœ žœžœ˜Kšœžœžœ˜K˜BKšœžœžœ˜K˜š  œžœžœ œžœžœžœžœžœžœžœ žœ˜“Kšžœ ž˜˜Kšœžœ$˜,Kšœžœ˜Kšœžœžœ˜šžœ žœ¢(˜=Kšœžœžœžœ˜+Kšœžœ ˜Kš œžœžœžœžœžœžœ˜Yšœžœ ˜Kšœžœžœ!žœ˜JKšœ(˜(šœ˜Kšœ5žœFžœ”˜•Kšžœ˜——šžœ žœžœ ž˜&Kš œ žœžœ6žœ8žœr˜ÿšžœ˜ Kšœ5žœDžœ‰žœ%˜¯Kšžœ˜——Kš žœ&žœ žœžœžœ˜MK˜ Kšžœ˜—Kšžœ¢+œ˜1Kšœžœ ˜K˜šžœ žœ¢%˜:Kšœžœ ˜Kšœžœžœ˜#Kš œžœžœžœžœžœžœ˜Yšœžœ ˜Kšœžœžœ!žœ˜JKšœ(˜(šœ˜Kšœ5žœFžœ”˜•Kšžœ˜——šžœ žœžœ ž˜&Kš œ žœžœ6žœ8žœr˜ÿšžœ˜ Kšœ5žœDžœ‰žœ%˜¯Kšžœ˜——Kš žœ&žœ žœžœžœ ˜UK˜ Kšžœ˜—Kšž¢ œ˜&—˜Kš œžœžœ žœžœžœ˜7šžœžœž˜Kšœžœžœ˜Kšœžœ$˜-K˜Kšœ žœ˜šžœžœžœ˜Kšœžœžœžœ%˜<šœžœS˜Zšœžœ˜2šžœ˜Kšœlžœ`˜ÐKšžœ˜—šžœ˜Kšœ„žœ`˜èKšžœ˜ ———šžœ žœžœ ž˜&Kšœ žœ˜Kšœžœ˜šžœžœ˜,šžœ˜KšœjžœVžœ$˜êKšžœ˜—šžœ˜Kšœžœžœ žœ˜2Kšœ‚žœVžœ$˜‚Kšžœ˜ ———K˜*šžœ ˜Kšœ˜Kšœžœ˜ ——šžœžœ˜ Kšœžœžœ žœ˜2šœžœžœ˜(Kšžœžœ'˜-——Kšžœ˜ ——˜Kšœ žœ˜Kšœžœžœžœ#˜:šž˜šœžœM˜Tšœ˜KšœcžœV˜½Kšžœ˜——šžœ žœžœ ž˜&Kšœ žœ˜šžœ˜ Kšžœžœcžœ?žœ$˜ÙKšžœ˜——šžœ žœž˜šœžœžœžœž˜.šžœ˜Kšžœžœ]žœ;˜©Kšžœ˜—šžœ˜Kšžœžœjžœœ˜—Kšžœžœžœ˜#Kšžœžœžœ˜K˜Kšœžœžœ0˜=——šžœ˜ Kšœažœžœ2˜ Kšžœ˜——Kšžœ˜ ——Kšžœžœ˜Kšžœ˜—K˜š  œžœžœžœžœ žœžœžœžœžœ žœžœ˜ŒKšœžœ:˜C—K˜š œžœžœžœžœ žœžœžœžœžœ žœžœ˜šKšœ˜Kšœ ˜ Kšœ žœ˜š  œžœžœ žœžœ žœžœ˜YKšœžœžœžœ žœžœžœžœ$žœ˜cšœžœV˜]K˜\Kšžœ˜ —šžœ ž˜Kšœ#žœ˜(šžœ˜ Kšœžœ˜Kšžœ žœ}žœPžœ$˜†K˜——Kšžœžœ˜—K˜K˜,šžœžœ˜ K˜Kšœžœ˜&Kšœžœ˜ K˜KKšžœžœžœ˜šžœžœžœ˜K˜;Kšœžœ˜ K˜—Kšœ™Kšžœ žœ¢"˜GKšžœ ˜—Kš žœžœžœžœžœžœ˜"Kšžœžœ žœžœ(˜Cšžœ˜Kšœ žœ˜&Kšœžœ˜"Kšœžœžœžœ žœžœžœžœ<žœ˜…šœžœi˜pK˜iKšžœ˜ —šžœ žœžœ ž˜&Kšœ žœ˜šžœ˜ Kšžœžœvžœ1žœ$˜ÞKšžœ˜ ——šžœ žœž˜šœžœžœžœ˜%Kšžœžœžœ ž˜KšžœH˜L—Kšžœlžœ$˜™—Kšžœ˜Kšžœžœ˜—K˜—K˜šœ  œžœžœžœžœ žœžœžœ žœžœ˜K˜Kšœžœ˜ K˜Kšœ˜Kšœžœ˜K˜K˜,šžœžœ˜ K˜:šžœ2žœž˜=Kšœ žœžœ˜Kšœ žœ$˜3Kšœžœ˜ š žœžœžœ*žœ žœž˜Pšžœž˜K˜"Kšœ žœEžœžœ˜hKšœžœ4žœ žœ˜\Kšžœ˜—Kšžœ˜—Kšžœžœ˜)š žœ žœ žœžœžœ˜-Kšœ8˜8Kšœžœ˜ K˜—Kšžœ˜—Kšœ'˜'Kšžœ žœ¢"˜GKšžœ ˜—šžœžœžœžœ˜Kšœ žœ"˜2Kš œžœžœžœžœžœ˜"š  œžœžœžœžœžœ˜5šžœ"žœ˜*Kšœ žœ˜Kšœžœ˜—Kšžœžœ˜—KšœO˜Ošžœžœžœžœžœžœžœž˜=Kš œžœžœžœžœžœžœ˜XšœžœV˜]K˜\Kšžœ˜—šžœ žœžœ ž˜&Kšœžœ˜ Kšžœxžœwžœ$˜ —Kšžœ˜—K˜—Kšžœžœžœ˜—K˜Kšœžœžœ˜Kšœžœ˜K˜š œžœžœžœžœ žœžœžœžœžœ žœžœ˜—Kšœ˜Kšœ ˜ Kšœ žœ˜K˜K˜,šžœžœ˜ K˜K˜K˜:Kšžœ¢"˜(šžœ2žœž˜=š žœžœžœ*žœ žœž˜Pšžœžœ˜$Kšžœ!žœ˜8šžœ-žœ˜5KšœM˜MKšœžœ˜ Kšžœ˜ K˜—šž˜Kšžœ˜—K˜—Kšžœ˜—Kšžœ˜—šž˜K˜ —Kšžœ˜Kšœ'˜'Kšœ™Kšžœ žœ¢"˜GKšžœ ˜—šžœžœžœžœ˜Kšœžœ˜"Kšœžœ˜ Kš œžœžœžœžœžœ˜"š  œžœžœžœžœžœ˜5Kšžœžœ žœ˜9Kšžœžœ˜—KšœE˜Ešžœžœžœžœžœžœžœž˜=Kš œžœžœžœžœžœžœ˜XšœžœV˜]K˜\Kšžœ˜—šžœ žœžœ ž˜&Kšœžœ˜ Kšžœ€žœwžœ$˜¨—Kšžœ˜—K˜—Kšžœžœžœ˜—K˜š œžœ˜Kšœ žœ˜(Kšœžœžœ˜ Kšœžœ˜Kšœ žœ˜Kšœžœ)˜1K˜K˜sK˜Kšžœ žœ.žœ˜\K˜BKšœžœ$˜*Kšœžœ˜-K˜K˜/Kšžœžœ0˜FKšœ&žœ˜9KšœžœL˜bKšœ$žœžœ˜JKšœ0žœ˜CKšœBžœ˜UKšœBžœ˜UKšœDžœ˜WKšœRžœ˜eKšžœ ˜ K˜MKšœžœ˜$K˜Kšžœ ˜ K– 4 in tabStops˜—K˜š œžœžœžœžœžœ žœ˜kK˜šžœž˜Kšœžœ˜šœ˜KšžœDžœžœžœ ˜q—šœ˜KšžœDžœžœžœ˜v—šœ ˜ KšžœN˜T—šœ˜KšžœFžœžœžœ ˜u—šœ ˜ KšžœN˜T—šœ˜Kšœ žœ˜Kšžœžœ ˜ šžœžœž˜ K˜Lšžœžœžœ˜;K˜Kšžœžœžœ˜—Kšžœ˜—Kšžœ˜ K˜—Kšžœžœ¢˜1—Kšœ˜—K˜š œžœžœžœžœžœ žœ˜vKšœžœ˜KšœžœM˜_Kšœžœ˜&Kšœžœ˜+Kšœžœ˜(Kšœžœ˜ š  œžœžœžœ žœ˜3Kšœžœ˜(Kšœžœ1˜:K˜ZKšœ4™4Kšžœ!žœU˜|šžœ˜K˜[Kšžœ'žœ˜?K˜—Kšžœ˜—K˜šžœž˜Kšœžœ˜Kšœ žœ ˜Kšœžœ ˜!Kšœ žœ ˜Kšœ žœ ˜Kšœžœ˜$šœ˜Kšœ ˜ Kšœžœ˜K˜Kšžœžœ ˜K˜K˜Kšœ'žœžœ˜JK˜Kšœ(™(KšœMžœ ˜\KšœJžœ ˜XKšžœžœ˜-K˜WKšžœ˜ K˜—šœ ˜ K˜6K˜VKšœ(™(Kšžœ!žœS˜zšžœ˜K˜YKšžœ'ž˜-K˜K˜—Kšžœ˜ K˜—šœ˜Kšœ˜Kšžœžœ ˜ šž˜K˜K˜šžœž˜K˜Q—šžœžœž˜K˜R—Kšžœžœžœ˜'Kšžœ˜—K˜—Kšžœžœ¢˜1—Kšœ˜—K˜š  œžœ&žœžœ˜cKšœžœ,˜5š œžœžœ˜?Kš žœžœžœžœžœ˜šžœ žœž˜šœžœžœ˜!šžœ*˜-K˜'K˜)—K˜—šœžœžœ˜ šžœ(˜+K˜%K˜ —K˜—Kšžœžœ˜——K˜šžœžœž˜Kšžœžœ˜š žœžœ-žœžœž˜Sšžœ žœž˜˜K˜*K˜)—˜K˜)—Kšžœžœ˜—Kšžœ˜—Kšžœ;˜B—Kšžœ˜—K˜š   œžœžœ žœžœ˜.Kšœ˜K˜ K˜K˜"Kšžœžœžœ˜&Kšœ˜K˜—K˜šœ˜Kšœ ¢2˜>Kšœžœ˜Kšœ;™;—K˜š  œžœžœžœžœ˜Nš  œžœ žœžœžœžœ*žœžœžœ˜qšžœ˜Kšžœ žœ˜$KšžœT˜X——Kšœ2˜2Kšžœ˜—K˜š   œžœžœžœžœ ˜7Kšœ8˜8Kš¢œ˜3Kšžœ˜—K˜š  œžœžœžœžœžœžœ˜JKšœ8˜8Kšœžœžœ˜K˜+Kš žœžœžœžœžœ˜!šžœžœž˜Kšœžœžœ žœ˜ Kšžœžœžœžœ˜(Kšžœžœžœžœ˜(K˜+Kšžœ˜—Kšžœžœ˜—K˜š œžœžœžœžœžœžœ˜6Kšœžœ ˜—K˜š  œžœ žœžœžœ˜IKšœžœžœ žœ˜Kšœžœžœ žœ˜ šžœžœž˜Kšœžœ˜!Kšœžœ ˜$Kšœžœ˜!Kšœžœ ˜$Kšžœžœ ˜——code2š  œžœžœ žœžœ˜,Kšœ˜K˜ K˜K˜"Kšžœžœžœ˜&Kšœ˜Mšœ˜—š œžœ˜0Kšœ2™2Kšœ˜Kšœ˜—š œ$˜:Kšžœ*™.Kšœ žœIžœ˜^K˜Kšžœžœ2žœ˜dK–-[s1: ROPE, s2: ROPE, case: BOOL _ TRUE]˜šžœžœ4žœžœ˜Gšžœžœžœžœ˜MK˜7šžœžœ˜ K˜,Kšœ˜Kšœ˜—K˜—K˜Kšœ#žœžœžœ˜2K˜—Kšœ˜—K˜Kšœžœžœ˜Kšœžœžœ˜Kšœ žœ˜'šœžœžœ˜Kšœžœžœ˜Kšœžœ¢6˜DKšœžœžœ˜Kšœžœ˜ —Kšœ žœžœ˜#Kšœžœžœ¢Ðcs¢ ˜5K˜K˜uK˜oK˜|K˜Kšœ;˜;K˜Kšžœ˜—…—¯²çì