DIRECTORY
Atom USING [GetPName],
BlackCherry USING [AddDisplayerProc, MsgHandle, MsgSetInfo, GetMsgContents, GetMsgID, ProcessNewMailProc, InsertMsgsProc, MsgButtonTextProc, CustomProcs, CustomProcsRec, RegisterCustomProcs, Report],
Convert USING [IntFromRope],
IO USING [atom, int, PutFR, RIS, rope, RopeFromROS, ROS, STREAM],
LoganBerryEntry USING [GetAllAttrs, GetAttr],
LoganQuery USING [AttributePattern, AttributePatternRec, AttributePatterns, WriteAttributePatterns],
Menus USING [MenuProc],
PopUpSelection USING [Request],
Rope USING [Cat, Concat, Find, Substr, ROPE],
SimMatch USING [Tokenize],
TapFilter USING [AddFilter, Agent, Annotation, CreateAgent, DeleteFilter, Error, ExistsFilter, GetAnnotations, IsAgentIdle, LookupFilter, MonitorAgent, MonitorProc, ParseMsgIntoFields, Query, WakeupAgent],
TapMsgQueue USING [EntryFromMsg, Msg, MsgQueue, Put, Create],
UserProfile USING [CallWhenProfileChanges, ProfileChangedProc, Token],
ViewerOps USING [FetchProp];
Menu operations
menuItems: LIST OF ROPE ← LIST["InterestLevel?", "DropConv", "BoostConv", "DropSim", "BoostSim"];
menuItemsDoc:
LIST
OF
ROPE ←
LIST[
"Explain why msg has given interest level",
"Drop msg's conversation to a low interest level",
"Raise msg's conversation to a high interest level",
"Drop similar msgs to a low interest level",
"Raise similar msgs to a high interest level"
];
subMenuItems: LIST OF ROPE ← LIST["5", "10", "15", "20", "25", "30", "35", "40", "45",
"50", "55", "60", "65", "70", "75", "80", "85", "90", "95", "default", "original"];
subMenuItemsDoc:
LIST
OF
ROPE ←
LIST["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
"Set to default value?", "Set to original value?"];
dropLevel: ROPE ← "25";
boostLevel: ROPE ← "75";
defaultSimInterest: ROPE ← "50";
defaultSimThreshold: ROPE ← "50";
FiltersMenuProc: Menus.MenuProc ~ {
[parent: ViewerClasses.Viewer, clientData: REF ANY ← NIL, mouseButton: ViewerClasses.MouseButton ← red, shift: BOOL ← FALSE, control: BOOL ← FALSE]
ENABLE {
UNWIND => NULL;
TapFilter.Error => {BlackCherry.Report["Filters problem: %g - %g.\n", IO.atom[ec], IO.rope[explanation]]; CONTINUE;};
};
dropSimInterest: ROPE;
dropSimThreshold: ROPE;
msInfo: MsgSetInfo ~ NARROW[ViewerOps.FetchProp[NARROW[parent], $BlackCherry]];
which: INT ← PopUpSelection.Request[header: "Filters", choice: menuItems, headerDoc: NIL, choiceDoc: menuItemsDoc, default: 0, timeOut: 15];
IF which <= 0 THEN RETURN; -- no selection
dropSimInterest ← dropLevel;
dropSimThreshold ← dropLevel;
SELECT which
FROM
1 => ExplainILevel[msInfo];
2 => DropConv[msInfo];
3 => BoostConv[msInfo];
4 => DropSim[msInfo];
5 => BoostSim[msInfo]
ENDCASE;
};
AnnotationToRope:
PROC [note: TapFilter.Annotation]
RETURNS [rope: Rope.
ROPE] = {
rope ← NIL;
FOR l: TapFilter.Annotation ← note, l.rest
WHILE l #
NIL
DO
rope ← IO.PutFR["%g %g: \"%g\"", IO.rope[rope], IO.rope[Atom.GetPName[l.first.type]], IO.rope[l.first.value]];
ENDLOOP;
};
ExplainILevel:
PROC [msInfo: MsgSetInfo] ~ {
note: TapFilter.Annotation;
IF msInfo.selected =
NIL
THEN {
BlackCherry.Report["\nNo message selected.\n"];
RETURN;
};
GetProfileInfo[];
BlackCherry.Report["\nAnnotations for message %g: ", IO.rope[msInfo.selected.gvID]];
note ← TapFilter.GetAnnotations[annotDB: annotationDBName, msgID: msInfo.selected.gvID];
BlackCherry.Report["%g\n", IO.rope[IF note # NIL THEN AnnotationToRope[note] ELSE "none"]];
};
DropConv:
PROC [msInfo: MsgSetInfo] ~ {
filterID: ROPE;
data: MsgData;
msg: MsgHandle;
subject, filterName, user: ROPE;
query: TapFilter.Query;
annot: TapFilter.Annotation;
whichSimInterest: INT;
oldInterestLevel: ROPE;
dropSimInterest: ROPE;
msg ← msInfo.selected;
data ← NARROW[msg.data];
IF data.parsedMsg =
NIL
THEN {
contents: ROPE ← BlackCherry.GetMsgContents[msInfo, msg].contents;
data.parsedMsg ← TapFilter.ParseMsgIntoFields[contents];
};
Get subject
subject ← LoganBerryEntry.GetAttr[entry: TapMsgQueue.EntryFromMsg[data.parsedMsg], type: $subject];
IF msInfo.selected =
NIL
THEN {
BlackCherry.Report["\nNo message selected.\n"];
RETURN;
};
oldInterestLevel ← NIL;
Construct filterID, lookup filter in database, and extract annotation of interest level.
filterID ← Rope.Cat[userName, "$", "Subject=", subject];
[filterName, user, query, annot] ← TapFilter.LookupFilter[filterDB: filterDBName, filterID: filterID];
IF filterName #
NIL
THEN {
Assign old interest level.
FOR anno: TapFilter.Annotation ← annot, anno.rest
WHILE anno #
NIL
DO
IF anno.first.type = $Level THEN oldInterestLevel ← anno.first.value;
ENDLOOP;
};
User picks the interest level for conversation messages. No need to check old interest level against new interest level.
whichSimInterest ← PopUpSelection.Request[header: "Interest", choice: subMenuItems, headerDoc: NIL, choiceDoc: subMenuItemsDoc, default: 0, timeOut: 15];
IF whichSimInterest > 0
THEN {
SELECT whichSimInterest
FROM
IN [1..19] => dropSimInterest ← ListNth[list: subMenuItems, itemNum: whichSimInterest];
= 20 => dropSimInterest ← defaultSimInterest; -- set default value
= 21 =>
IF oldInterestLevel #
NIL
THEN dropSimInterest ← oldInterestLevel
ELSE {
BlackCherry.Report["\nNo original value because filter does not already exist.\n"];
RETURN
};
ENDCASE;
}
ELSE RETURN; --user picked nothing, so abort entire operation
Check to see whether user's supplied interest is greater than the old one; if so, then we're done, else, report error.
IF oldInterestLevel #
NIL
THEN {
IF Convert.IntFromRope[dropSimInterest] >= Convert.IntFromRope[oldInterestLevel]
THEN {
BlackCherry.Report["\nCannot drop interest level from %g to new level %g.\n", IO.rope[oldInterestLevel], IO.rope[dropSimInterest]];
RETURN;
};
};
filterID ← DeleteSubjectFilter[msInfo: msInfo, msg: msInfo.selected];
BlackCherry.Report["\nDeleted old filter %g.", IO.rope[filterID]];
filterID ← AddSubjectFilter[msInfo: msInfo, msg: msInfo.selected, note: LIST[[$Level, dropSimInterest]]];
BlackCherry.Report["\nAdded new filter %g to set conversation's interest level to %g.\n", IO.rope[filterID], IO.rope[dropSimInterest]];
};
BoostConv:
PROC [msInfo: MsgSetInfo] ~ {
filterID: ROPE;
data: MsgData;
msg: MsgHandle;
subject, filterName, user: ROPE;
query: TapFilter.Query;
annot: TapFilter.Annotation;
whichSimInterest: INT;
oldInterestLevel: ROPE;
boostSimInterest, dropSimInterest: ROPE;
msg ← msInfo.selected;
data ← NARROW[msg.data];
IF data.parsedMsg =
NIL
THEN {
contents: ROPE ← BlackCherry.GetMsgContents[msInfo, msg].contents;
data.parsedMsg ← TapFilter.ParseMsgIntoFields[contents];
};
subject ← LoganBerryEntry.GetAttr[entry: TapMsgQueue.EntryFromMsg[data.parsedMsg], type: $subject];
IF msInfo.selected =
NIL
THEN {
BlackCherry.Report["\nNo message selected.\n"];
RETURN;
};
oldInterestLevel ← NIL;
Construct filterID, lookup filter in database, and extract annotation of interest level.
filterID ← Rope.Cat[userName, "$", "Subject=", subject];
[filterName, user, query, annot] ← TapFilter.LookupFilter[filterDB: filterDBName, filterID: filterID];
IF filterName #
NIL
THEN {
Assign old interest level.
FOR anno: TapFilter.Annotation ← annot, anno.rest
WHILE anno #
NIL
DO
IF anno.first.type = $Level THEN oldInterestLevel ← anno.first.value;
ENDLOOP;
};
User picks the interest level for conversation messages. No need to check old interest level against new interest level.
whichSimInterest ← PopUpSelection.Request[header: "Interest", choice: subMenuItems, headerDoc: NIL, choiceDoc: subMenuItemsDoc, default: 0, timeOut: 15];
IF whichSimInterest > 0
THEN {
SELECT whichSimInterest
FROM
IN [1..19] => boostSimInterest ← ListNth[list: subMenuItems, itemNum: whichSimInterest];
= 20 => boostSimInterest ← defaultSimInterest; -- set default value
= 21 =>
IF oldInterestLevel #
NIL
THEN boostSimInterest ← oldInterestLevel
ELSE {
BlackCherry.Report["\nNo original value because filter does not already exist.\n"];
RETURN
};
ENDCASE;
}
ELSE RETURN; --user picked nothing, so abort entire operation
Check to see whether user's supplied interest is less than the old one; if so, then we're done, else, report error.
IF oldInterestLevel #
NIL
THEN {
IF Convert.IntFromRope[boostSimInterest] <= Convert.IntFromRope[oldInterestLevel]
THEN {
BlackCherry.Report["\nCannot boost interest level from %g to new level %g.\n", IO.rope[oldInterestLevel], IO.rope[boostSimInterest]];
RETURN;
};
};
filterID ← AddSubjectFilter[msInfo: msInfo, msg: msInfo.selected, note: LIST[[$Level, boostSimInterest]]];
BlackCherry.Report["\nAdded filter %g to set conversation's interest level to %g.\n", IO.rope[filterID], IO.rope[boostSimInterest]];
};
DropSim:
PROC [msInfo: MsgSetInfo] ~ {
Effects: Drops the similarity threshold of a filter to a value less than or equal to the original value. Used to set both the interest level and the similarity threshold for the selected message. User must choose values for both, using default values if necessary; otherwise, this operation has no effect, that is, the old filter is not changed or the new filter is not added. Clicking outside the menu causes the operation to return. User can select a value between 5 and 95 inclusive, or choose "default" or "original" values.
filterID: ROPE;
name: ROPE;
filterName, user: ROPE;
query: TapFilter.Query;
annot: TapFilter.Annotation;
oldInterestLevel, oldThreshold: ROPE;
whichSimThreshold: INT;
dropSimThreshold, dropSimInterest: ROPE;
whichSimInterest: INT;
IF msInfo.selected =
NIL
THEN {
BlackCherry.Report["\nNo message selected.\n"];
RETURN;
};
Pre-condition: filter must already exist.
oldInterestLevel ← NIL;
name ← BlackCherry.GetMsgID[msInfo: msInfo, msgH: msInfo.selected];
Construct filterID, lookup filter in database, and extract annotation of similarity threshold.
filterID ← Rope.Cat[userName, "$", "SimTo:", name];
[filterName, user, query, annot] ← TapFilter.LookupFilter[filterDB: filterDBName, filterID: filterID];
IF filterName #
NIL
THEN {
Assign old interest level and old similarity threshold.
FOR anno: TapFilter.Annotation ← annot, anno.rest
WHILE anno #
NIL
DO
IF anno.first.type = $SimThreshold
THEN oldThreshold ← anno.first.value
ELSE IF anno.first.type = $Level THEN oldInterestLevel ← anno.first.value;
ENDLOOP;
};
User picks the new threshold for similarity matching, above which he's interested in.
whichSimThreshold ← PopUpSelection.Request[header: "Sim Threshold", choice: subMenuItems, headerDoc: NIL, choiceDoc: subMenuItemsDoc, default: 0, timeOut: 15];
IF whichSimThreshold <= 0 THEN RETURN; -- no selection
SELECT whichSimThreshold
FROM
IN [1..19] => dropSimThreshold ← ListNth[list: subMenuItems, itemNum: whichSimThreshold];
= 20 => dropSimThreshold ← defaultSimThreshold; --set default value
= 21 =>
IF oldThreshold #
NIL
THEN dropSimThreshold ← oldThreshold
ELSE {
BlackCherry.Report["\nNo original value because filter does not exist.\n"];
RETURN
};
ENDCASE;
Check to see whether user's supplied threshold is greater than the old one; if so, then we're done, else, report error.
IF oldThreshold #
NIL
THEN {
IF Convert.IntFromRope[dropSimThreshold] >= Convert.IntFromRope[oldThreshold]
THEN { BlackCherry.Report["\n Cannot drop old similarity threshold %g to new, higher threshold %g.\n",
IO.rope[oldThreshold],
IO.rope[dropSimThreshold]];
RETURN;
};
};
User picks the interest level for similar messages. No need to check old interest level against new interest level.
whichSimInterest ← PopUpSelection.Request[header: "Interest", choice: subMenuItems, headerDoc: NIL, choiceDoc: subMenuItemsDoc, default: 0, timeOut: 15];
IF whichSimInterest > 0
THEN {
SELECT whichSimInterest
FROM
IN [1..19] => dropSimInterest ← ListNth[list: subMenuItems, itemNum: whichSimInterest];
= 20 => dropSimInterest ← defaultSimInterest; -- set default value
= 21 =>
IF oldInterestLevel #
NIL
THEN dropSimInterest ← oldInterestLevel
ELSE {
BlackCherry.Report["\nNo original value because filter does not exist.\n"];
RETURN
};
ENDCASE;
}
ELSE RETURN;
BlackCherry.Report["\nReplacing old filter %g.\n", IO.rope[filterID]];
filterID ← DeleteTextFilter[msInfo: msInfo, msg: msInfo.selected];
filterID ← AddTextFilter[msInfo: msInfo, msg: msInfo.selected, note: LIST[[$Level, dropSimInterest], [$SimThreshold, dropSimThreshold]]];
BlackCherry.Report["\nAdding new filter %g to drop interest level of similar msgs to %g. Similarity threshold dropped to %g.\n", IO.rope[filterID], IO.rope[dropSimInterest], IO.rope[dropSimThreshold]];
};
BoostSim:
PROC [msInfo: MsgSetInfo] ~ {
Effects: This routine boosts the similarity threshold for a selected message to a higher value, as determinted by the user, and changes the interest level for similar messages. User must choose values for both, using default values if necessary; otherwise, this operation has no effect, that is, the old filter is not changed or the new filter is not added. Clicking outside the menu causes the operation to return. User can select a value between 5 and 95 inclusive, or choose "default" or "original" values.
filterID: ROPE;
whichSimInterest, whichSimThreshold: INT;
boostSimInterest, boostSimThreshold: ROPE;
name: ROPE;
oldThreshold, oldInterestLevel: ROPE;
filterName, user: ROPE;
query: TapFilter.Query;
annot: TapFilter.Annotation;
IF msInfo.selected =
NIL
THEN {
BlackCherry.Report["\nNo message selected.\n"];
RETURN;
};
oldThreshold ← NIL;
name ← BlackCherry.GetMsgID[msInfo: msInfo, msgH: msInfo.selected];
Construct filterID, lookup filter in database, and extract annotation of similarity threshold.
filterID ← Rope.Cat[userName, "$", "SimTo:", name];
[filterName, user, query, annot] ← TapFilter.LookupFilter[filterDB: filterDBName, filterID: filterID];
IF filterName #
NIL
THEN {
Assign old interest level and old similarity threshold.
FOR anno: TapFilter.Annotation ← annot, anno.rest
WHILE anno #
NIL
DO
IF anno.first.type = $SimThreshold
THEN oldThreshold ← anno.first.value
ELSE IF anno.first.type = $Level THEN oldInterestLevel ← anno.first.value;
ENDLOOP;
};
User picks the threshold for similarity matching, above which he's interested in.
whichSimThreshold ← PopUpSelection.Request[header: "Threshold", choice: subMenuItems, headerDoc: NIL, choiceDoc: subMenuItemsDoc, default: 0, timeOut: 15];
IF whichSimThreshold > 0
THEN {
SELECT whichSimThreshold
FROM
IN [1..19] => boostSimThreshold ← ListNth[list: subMenuItems, itemNum: whichSimThreshold];
= 20 => boostSimThreshold ← defaultSimThreshold; --set default value
= 21 =>
IF oldThreshold #
NIL
THEN boostSimThreshold ← oldThreshold
ELSE {
BlackCherry.Report["\nNo original value because filter does not exist.\n"];
RETURN
};
ENDCASE;
}
ELSE RETURN; --user picked nothing, so abort entire operation
Check to see whether user's supplied threshold is less than or equal to the old one; if so, then we're done, else, report error.
IF oldThreshold #
NIL
THEN {
IF Convert.IntFromRope[boostSimThreshold] <= Convert.IntFromRope[oldThreshold]
THEN {
BlackCherry.Report["\nCannot boost old similarity threshold %g to new, lower threshold %g.\n", IO.rope[oldThreshold], IO.rope[boostSimThreshold]];
RETURN;
};
};
User picks the interest level for similar messages. No need to check old interest level against new interest level.
whichSimInterest ← PopUpSelection.Request[header: "Interest", choice: subMenuItems, headerDoc: NIL, choiceDoc: subMenuItemsDoc, default: 0, timeOut: 15];
IF whichSimInterest > 0
THEN {
SELECT whichSimInterest
FROM
IN [1..19] => boostSimInterest ← ListNth[list: subMenuItems, itemNum: whichSimInterest];
= 20 => boostSimInterest ← defaultSimInterest; -- set default value
= 21 =>
IF oldInterestLevel #
NIL
THEN boostSimInterest ← oldInterestLevel
ELSE {
BlackCherry.Report["\nnNo original value because filter does not exist.\n"];
RETURN
};
ENDCASE;
}
ELSE RETURN;
filterID ← AddTextFilter[msInfo: msInfo, msg: msInfo.selected, note: LIST[[$Level, boostSimInterest], [$SimThreshold, boostSimThreshold]]];
BlackCherry.Report["\nAdded filter %g to boost interest level of similar msgs to %g. Similarity threshold boosted to %g.\n", IO.rope[filterID], IO.rope[boostSimInterest], IO.rope[boostSimThreshold]];
};
AddSubjectFilter:
PROC [msInfo: MsgSetInfo, msg: MsgHandle, note: TapFilter.Annotation]
RETURNS [filterID:
ROPE] ~ {
subject, query: ROPE;
data: MsgData ← NARROW[msg.data];
IF data.parsedMsg =
NIL
THEN {
contents: ROPE ← BlackCherry.GetMsgContents[msInfo, msg].contents;
data.parsedMsg ← TapFilter.ParseMsgIntoFields[contents];
};
subject ← LoganBerryEntry.GetAttr[entry: TapMsgQueue.EntryFromMsg[data.parsedMsg], type: $subject];
IF Rope.Find[s1: subject, s2: "Re: ", pos1: 0, case:
FALSE] = 0
THEN
subject ← Rope.Substr[base: subject, start: 4]; -- strip off "re: "
query ← IO.PutFR["subject(re): \"(Re\':| )*%g\"", IO.rope[subject]];
GetProfileInfo[];
filterID ← Rope.Cat[userName, "$", "Subject=", subject];
IF TapFilter.ExistsFilter[filterDB: filterDBName, filterID: filterID]
THEN {
[] ← TapFilter.DeleteFilter[filterDB: filterDBName, filterID: filterID];
};
filterID ← TapFilter.AddFilter[filterDB: filterDBName, user: userName, filterName: Rope.Concat["Subject=", subject], query: query, annot: note, agent: filteringAgent];
};
DeleteSubjectFilter:
PROC [msInfo: MsgSetInfo, msg: MsgHandle]
RETURNS [filterID:
ROPE] ~ {
subject, query: ROPE;
data: MsgData ← NARROW[msg.data];
IF data.parsedMsg =
NIL
THEN {
contents: ROPE ← BlackCherry.GetMsgContents[msInfo, msg].contents;
data.parsedMsg ← TapFilter.ParseMsgIntoFields[contents];
};
subject ← LoganBerryEntry.GetAttr[entry: TapMsgQueue.EntryFromMsg[data.parsedMsg], type: $subject];
IF Rope.Find[s1: subject, s2: "Re: ", pos1: 0, case:
FALSE] = 0
THEN
subject ← Rope.Substr[base: subject, start: 4]; -- strip off "re: "
query ← IO.PutFR["subject(re): \"(Re\':| )*%g\"", IO.rope[subject]];
GetProfileInfo[];
filterID ← Rope.Cat[userName, "$", "Subject=", subject];
TapFilter.DeleteFilter[filterDB: filterDBName, filterID: filterID];
};
Add a text filter for similarity matching.
AddTextFilter:
PROC [msInfo: MsgSetInfo, msg: MsgHandle, note: TapFilter.Annotation]
RETURNS [filterID:
ROPE] ~ {
name, query, text: ROPE;
attrs: LIST OF ROPE;
stream: IO.STREAM;
threshold: ROPE;
data: MsgData ← NARROW[msg.data];
aps: LoganQuery.AttributePatterns;
ap: LoganQuery.AttributePattern ← NEW[LoganQuery.AttributePatternRec];
ap.attr.type ← $text;
ap.ptype ← IO.PutFR["sim"];
IF data.parsedMsg =
NIL
THEN {
contents: ROPE ← BlackCherry.GetMsgContents[msInfo, msg].contents;
data.parsedMsg ← TapFilter.ParseMsgIntoFields[contents];
};
Get all text fields and concatenate into one rope
attrs ← LoganBerryEntry.GetAllAttrs[entry: TapMsgQueue.EntryFromMsg[data.parsedMsg], type: $text];
WHILE attrs #
NIL
DO
text ← Rope.Concat[text, attrs.first];
attrs ← attrs.rest;
ENDLOOP;
Tokenize text (remove punctuation, etc.)
stream ← IO.RIS[text];
stream ← SimMatch.Tokenize[stream];
text ← IO.RopeFromROS[stream];
SimMatch.UpdateDFList[text];
Get threshold value from annotation.
FOR anno: TapFilter.Annotation ← note, anno.rest
WHILE anno #
NIL
DO
IF anno.first.type = $SimThreshold THEN threshold ← anno.first.value;
ENDLOOP;
text ← Rope.Concat[threshold, text]; -- Prepend the threshold
ap.attr.value ← text;
aps ← LIST[ap];
stream ← IO.ROS[];
LoganQuery.WriteAttributePatterns[s: stream, ap: aps];
query ← IO.RopeFromROS[stream];
BlackCherry.Report["query is %g\n", IO.rope[query]]; -- for debugging
name ← BlackCherry.GetMsgID[msInfo: msInfo, msgH: msg];
GetProfileInfo[];
IF TapFilter.ExistsFilter[filterDB: filterDBName, filterID: Rope.Cat[userName, "$", "SimTo:", name]]
THEN {
BlackCherry.Report["\Replacing old filter %g.\n", IO.rope[filterID]];
[] ← DeleteTextFilter[msInfo, msg];
};
filterID ← TapFilter.AddFilter[filterDB: filterDBName, user: userName, filterName: Rope.Concat["SimTo:", name], query: query, annot: note, agent: filteringAgent];
};
DeleteTextFilter:
PROC [msInfo: MsgSetInfo, msg: MsgHandle]
RETURNS [filterID:
ROPE] ~ {
name: ROPE;
stream: IO.STREAM;
name ← BlackCherry.GetMsgID[msInfo: msInfo, msgH: msg];
GetProfileInfo[];
filterID ← Rope.Cat[userName, "$", "SimTo:", name];
TapFilter.DeleteFilter[filterDB: filterDBName, filterID: filterID];
};
ListNth:
PROC [list:
LIST
OF
ROPE, itemNum:
INT]
RETURNS [nth:
ROPE] ~ {
Effects: This procedure returns the nth element in a list of ropes.
item: ROPE;
counter: INT ← 0;
FOR element:
LIST
OF
ROPE ← list, element.rest
WHILE element #
NIL
DO
counter ← counter + 1;
item ← element.first;
IF counter = itemNum THEN RETURN[item];
ENDLOOP;
};