{ ANAGRAM. -------------------------------------------------------------------- Rapha‰l Vanney, 01/93 Purpose : Reads a list of Words 4 to 10 Characters long from a File named 'LIST.#1', outputs a list of anagrams founds in a specified format to a File named 'ANAGRAM.RES'. Note : I commented-out the source using a langage, say English, which I'm not Really fluent in ; please forgive mistakes. ------------------------------------------------------------------------------} {$m 8192,65536,655360} {$a+,d+,e-,f-,g+,i+,l+,n-,o-,q-,r-,s-,v+} {$b-} { Turns off complete Boolean evaluation ; this allows easiest combined Boolean tests. } Uses Crt, Objects ; Const MaxWordLen = 10 ; { Offically specified by GP ! } CntAnagrams : Word = 0 ; { Actually, this counter shows the } { number of Words found in the } { output File. } OutFileName = 'ANAGRAM.RES' ; Type TWordString = String[MaxWordLen] ; { TWordCollection. This Object will be used to store the Words in a sorted fashion. As long as the input list is already sorted, it could have inherited from TCollection, put there is no big penalty using a sorted one. } TWordCollection = Object (TSortedCollection) Function KeyOf(Item : Pointer) : Pointer ; Virtual ; Function Compare(Key1, Key2 : Pointer) : Integer ; Virtual ; Procedure FreeItem(Item : Pointer) ; Virtual ; end ; PWordCollection = ^TWordCollection ; { TWord. This is the Object we'll use to store a Word. Each Word knows : - it's 'Textual form' : It - the first of it's anagrams, if it has been found to be the anagram of another Word, - the next of it's anagrams, in the same condition. } PWord = ^TWord ; TWord = Object It : TWordString ; FirstAng : PWord ; NextAng : PWord ; Constructor Init(Var Wrd : TWordString) ; Destructor Done ; end ; Var WordsList : PWordCollection ; { The main list of Words } OrgMem : LongInt ; { Original MemAvail } UsedMem : LongInt ; { Amount of RAM used } {-------------------------------------- TWord --------------------------------} Constructor TWord.Init ; begin It:=Wrd ; FirstAng:=Nil ; NextAng:=Nil ; end ; Destructor TWord.Done ; begin end ; {-------------------------------------- TWordCollection ----------------------} { The following methods are not commented out, since they already are in Turbo-Pascal's documentations, and they do nothing unusual. } Function TWordCollection.KeyOf ; begin KeyOf:=Addr(PWord(Item)^.It) ; end ; Function TWordCollection.Compare ; Var k1 : PString Absolute Key1 ; k2 : PString Absolute Key2 ; begin If k1^>k2^ Then Compare:=1 Else If k1^lengthes, not anagrams } LDS SI, WordB { Let's make a local copy of WordB ; enhanced version of TP's "Move" } ClD { Clear direction flag } Push SS Pop ES { Segment part of WordC's address } LEA DI, WordC { Offset part of it } Mov CL, DS:[SI] { Get length Byte } XOr CH, CH { Make it a Word } Mov DL, CL { Save length For later use } Inc CX { # of Bytes to store the String } ShR CX, 1 { We'll copy Words ; CF is importt } Rep MovSW { Copy WordB to WordC } JNC @NoByte MovSB { Copy last Byte } @NoByte: LDS SI, WordA { DS:SI contains WordA's address } Inc SI { SI points to first Char of WordA } Mov DH, DL { Use DH as a loop counter } LEA BX, WordC { Load offset of WordC in BX } Inc BX { Skip length Byte } { For each letter in WordA, search it in WordB ; if found, mark it as 'used' in WordB, then proceed With next. If a letter is not found, Words are not anagrams ; if all are found, Words are anagrams. } { Registers usage : AL : scratch For SCAS AH : unused BX : offset part of WordC's address CX : will be used as a counter For SCAS DL : contains length of Strings ; 'll be used to reset CX DH : loop counter ; initially =DL ES : segment part of WordC's address DI : scratch For SCAS DS:SI : Pointer to next Char to process in WordA } @Bcle: LodSB { Load next Char of WordA in AL } Mov CL, DL { Load length of String in CX } Mov DI, BX { Copy offset of WordC to DI } RepNE ScaSB { Scan WordC For AL 'till found } JNE @NotAng { Char not found, not anagrams } Dec DI { Back-up to matching Char } Mov Byte Ptr ES:[DI], '*' { Mark the Character as 'used' } Dec DH { Dec loop counter } Or DH, DH { Done all Chars ? } JNZ @Bcle { No, loop } { All Chars done, the Words are anagrams } Mov AL, 1 { Result=True } Or AL, AL { Set accordingly the ZF } Jmp @Done @NotAng: XOr AL, AL { Result=False } @Done: Pop DS { Restore DS } end ; Function ReadWordsFrom(FName : String) : Boolean ; Var InF : Text ; { Input File } Buf : Array[1..2048] Of Byte ; { Speed-up Text buffer } Lig : String ; { Read line } Wrd : String ; { Word gotten from parsed Lig } WSt : TWordString ; { Checked version of Wrd } p : Integer ; { Work } Cnt : LongInt ; { Line counter } begin ReadWordsFrom:=False ; { 'till now, at least ! } WordsList:=New(PWordCollection, Init(20, 20)) ; Assign(InF, FName) ; {$i-} ReSet(InF) ; {$i+} If IOResult<>0 Then Exit ; SetTextBuf(InF, Buf, SizeOf(Buf)) ; Cnt:=0 ; While Not EOF(InF) Do begin Inc(Cnt) ; ReadLn(InF, Lig) ; While Lig<>'' Do begin { Let's parse the read line into Words } p:=Pos(',', Lig) ; If p=0 Then p:=Length(Lig)+1 ; Wrd:=Copy(Lig, 1, p-1) ; { Check of overflowing Word length } If Length(Wrd)>MaxWordLen Then WriteLn('Word length > ', MaxWordLen, ' : ', Wrd) ; WSt:=Wrd ; CleanUp(WSt) ; If WSt<>'' Then WordsList^.Insert(New(PWord, Init(WSt))) ; Delete(Lig, 1, p) ; end ; end ; {$i-} Close(InF) ; {$i+} If IOResult<>0 Then ; ReadWordsFrom:=True ; WriteLn(Cnt, ' lines, ', WordsList^.Count, ' Words found.') ; end ; Procedure CheckAnagrams(i : Integer) ; { This Procedure builds, if necessary (i.e. not already done), the anagrams list For Word #i of the list. } Var Org : PWord ; { Original Word (1st of list) } j : Integer ; { Work } Last : PWord ; { Last anagram found } begin Org:=WordsList^.Items^[i] ; If Org^.FirstAng<>Nil Then begin { This Word is already known to be the anagram of at least another one ; don't re-do the job. } { _or_ this Word is known to have no anagrams in the list } Exit ; end ; { Search anagrams } Last:=Org ; Org^.FirstAng:=Org ; { This Word is the first of it's } { own anagrams list ; normal, no ? } For j:=Succ(i) To Pred(WordsList^.Count) Do { Don't search the begining of the list, of course ! } begin { Let's skip anagram checking if lengths are <> } If Org^.It[0]=PWord(WordsList^.Items^[j])^.It[0] Then If AreAnagrams(Org^.It, PWord(WordsList^.Items^[j])^.It) Then begin { Build chained list of anagrams } Last^.NextAng:=WordsList^.Items^[j] ; Last:=WordsList^.Items^[j] ; Last^.FirstAng:=Org ; end ; end ; Last^.NextAng:=Nil ; { Unusefull, but keep carefull } end ; Procedure ScanForAnagrams ; { This Procedure scans the list of Words For anagrams, and do the outputing to the 'ANAGRAM.RES' File. } Var i : Integer ; { Work } Tmp : PWord ; { Temporary Word } Out : Text ; { Output File } Comma : Boolean ; { Helps dealing With commas } Current : PWord ; { Currently handled Word } begin Assign(Out, OutFileName) ; ReWrite(Out) ; With WordsList^ Do For i:=0 To Pred(Count) Do begin Current:=Items^[i] ; CheckAnagrams(i) ; { We're now gonna scan the chained list of known anagrams for this Word. } If (Current^.NextAng<>Nil) Or (Current^.FirstAng<>Current) Then { This Word has at least an anagram other than itself } begin Write(Out, PadStr(Current^.It, 12)) ; Inc(CntAnagrams) ; Comma:=False ; Tmp:=Current^.FirstAng ; While Tmp<>Nil Do begin If Tmp<>Current Then { Don't reWrite it... } begin If Comma Then Write(Out, ', ') ; Comma:=True ; Write(Out, Tmp^.It) ; Inc(CntAnagrams) ; end ; Tmp:=Tmp^.NextAng ; end ; WriteLn(Out) ; end ; end ; Close(Out) ; end ; Var Tmp : LongInt ; begin { Check command line parameter } If ParamCount<>1 Then begin WriteLn('Anagram. Rapha‰l Vanney, 01/93 - Anagram''s contest entry.'); WriteLn ; WriteLn('Anagram ') ; WriteLn ; WriteLn('Please specify input File name.') ; Halt(1) ; end ; OrgMem:=MemAvail ; { Read Words list from input File } If Not ReadWordsFrom(ParamStr(1)) Then begin WriteLn('Error reading Words from input File.') ; Halt(1) ; end ; { Display statistics stuff } WriteLn('Reading and sorting done.') ; UsedMem:=OrgMem-MemAvail ; WriteLn('Used RAM : ', UsedMem, ' Bytes') ; Tmp := Trunc(1.0 * MemAvail / (1.0 * UsedMem / WordsList^.Count)) ; If Tmp > 16383 Then Tmp := 16383 ; WriteLn('Potential Words manageable : ', Tmp) ; { Scan For anagrams, create output File } ScanForAnagrams ; WriteLn('Anagrams scanning & output done.') ; WriteLn(CntAnagrams, ' Words written to ', OutFileName) ; { Clean-up } Dispose(WordsList, Done) ; end. { ------------------------------------------------------------------------------ Okay, this is my entry For the 'anagram contest' ! The few things I'd like to point-out about it : . I chosed to use OOP, in contrast to seeking speed. I wouldn't say my Program is Really slow (7.25 secs on my 386-33), but speed was not my first concern. . It fully Uses one of the interresting points of OOP in TP, i.e. reusability, through inheritance, . When a Word (A) has been found to be an anagram of another (B), the Program never searches again For the anagrams of (A) ; this highly reduces computing time... but I believe anybody does the same. . I also quite like the assembly langage Function 'AreAnagrams'. ------------------------------------------------------------------------------ The Words list is stored in memory in the following maner : . A collection (say, a list) of the Words, . Within this list, anagrams are chained as a list . Each Word knows the first and the next of its anagrams ------------------------------------------------------------------------------ For the sake of speed, I did something I'm quite ashamed of ; but it saves 32% of execution time, so... The usual way to access element #i of a TCollection is to call Function At with parameter i (i.e. At(i)) ; there is also another way, which is not Really clean, but which I chosed to use : access it directly through Items^[i].