{ unit sdStringTable An optimized table of *unique* strings, using two separate sorted indices: - by (string) ID - by sdCompareRefString method The sdCompareRefString method does not use common alphabetical compare, but rather a comparison from first character, then last character, then 2nd, then before-last, etc. until all characters are compared, or a mismatch is found. Since many (programmer) strings have numbers at the end of the string, (e.g. "MyNewNode1", "MyNewNode2", etc), the comparison terminates earlier than with a common alphabetical compare. sdStringTable is used by NativeXml but can also be used independently in your projects. Author: Nils Haeck M.Sc. (n.haeck@simdesign.nl) Original Date: 28 May 2007 Modified: 05jan2011: enhancement, no longer uses stringrec 17jun2011: changed TStringTable ancestor from TDebugPersistent to TDebugComponent 24jun2011: "find" fix 18jul2011: renamed TsdStringTable to TsdSymbolTable and added TsdSymbolStyle It is NOT allowed under ANY circumstances to publish or copy this code without accepting the license conditions in accompanying LICENSE.txt first! This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. Please visit http://www.simdesign.nl/xml.html for more information. Copyright (c) 2007 - 2011 Simdesign BV } unit sdStringTable; {$ifdef lcl}{$MODE Delphi}{$endif} interface uses Classes, SysUtils, Contnrs, sdDebug; // symbol styles (cardinal) // Default symbol style is ssUnknown, but highlevel code can // distinguish between symbol styles. TsdSymbolTable just stores // the symbol as counted Utf8String. const ssUnknown = 0; // data not determined yet ssString = 1; // data is a string ssBase64Binary = 2; // data is binary and will be handled by Base64 funcs ssHexBinary = 3; // data is binary and will be handled by BinHex funcs ssBoolean = 4; // boolean (stored in a byte, just 0 and 1 of cardinal) ssCardinal = 5; // cardinal (1..N bytes, see TBinaryXml.ReadCardinal) ssInteger = 6; // integer (1..N bytes) ssDecimal = 7; // decimal value (see TNativeXml.EncodeDecimalSymbol) ssDate = 8; // date (see TNativeXml.EncodeDateSymbol) ssTime = 9; // time (see TNativeXml.EncodeTimeSymbol) ssDateTime = 10; // datetime (see TNativeXml.EncodeDateTimeSymbol) // These were the default symbol styles as used by NativeXml. Other units may // define more symbols after the last default symbol type // A symbol table, holding a collection of unique strings, sorted in 2 ways // for fast access. Strings can be added with AddString or AddStringRec. // When a string is added or updated, an ID is returned which the application // can use to retrieve the string, using GetString. TsdSymbolTable = class(TDebugComponent) private FByID: TObjectList; FBySymbol: TObjectList; FPluralSymbolCount: integer; function GetSymbolCount: integer; protected public constructor Create(AOwner: TComponent); override; destructor Destroy; override; // Clear the string table procedure Clear; // Add a potentially new string S to the table, the function // returns its string ID. function AddString(const S: Utf8String): integer; // retrieve the string based on its string ID. The string ID is only unique // within this string table, so do not use IDs from other tables. function GetString(ID: integer): Utf8String; // total number of symbols in the table property SymbolCount: integer read GetSymbolCount; // plural symbols in the table. plural symbols are symbols that have // a frequency > 1. ie the symbol is found more than once in the app. // PluralCount is only valid after method SortByFrequency. property PluralSymbolCount: integer read FPluralSymbolCount; procedure LoadFromFile(const AFileName: string); procedure LoadFromStream(S: TStream); function LoadSymbol(S: TStream): Cardinal; procedure SaveToFile(const AFileName: string); procedure SaveToStream(S: TStream; ACount: integer); procedure SaveSymbol(S: TStream; ASymbolID: Cardinal); procedure ClearFrequency; procedure IncrementFrequency(ID: integer); procedure SortByFrequency(var ANewIDs: array of Cardinal); end; {utility functions} // compare two bytes function sdCompareByte(Byte1, Byte2: byte): integer; // compare two integers function sdCompareInteger(Int1, Int2: integer): integer; // unicode UTF8 <> UTF16LE coversion functions function sdUtf16ToUtf8Mem(Src: Pword; Dst: Pbyte; Count: integer): integer; function sdUtf8ToUtf16Mem(var Src: Pbyte; Dst: Pword; Count: integer): integer; // stream methods function sdStreamReadCardinal(S: TStream): Cardinal; function sdStreamReadString(S: TStream; ACharCount: Cardinal): Utf8String; procedure sdStreamWriteCardinal(S: TStream; ACardinal: Cardinal); procedure sdStreamWriteString(S: TStream; const AString: Utf8String); implementation type // A symbol item used in symbol lists (do not use directly) TsdSymbol = class private FID: integer; FFreq: Cardinal; FSymbolStyle: Cardinal; FFirst: Pbyte; FCharCount: integer; public destructor Destroy; override; function AsString: Utf8String; property SymbolStyle: Cardinal read FSymbolStyle; property CharCount: integer read FCharCount; end; // A list of symbols (do not use directly) TsdSymbolList = class(TObjectList) private function GetItems(Index: integer): TsdSymbol; protected // Assumes list is sorted by refstring function Find(ASymbol: TsdSymbol; var Index: integer): boolean; public property Items[Index: integer]: TsdSymbol read GetItems; default; end; // compare two symbols. This is NOT an alphabetic compare. symbols are first // compared by length, then by first byte, then last byte then second, then // N-1, until all bytes are compared. function sdCompareSymbol(Symbol1, Symbol2: TsdSymbol): integer; var CharCount: integer; First1, First2, Last1, Last2: Pbyte; IsEqual: boolean; begin // Compare string length first Result := sdCompareInteger(Symbol1.CharCount, Symbol2.CharCount); if Result <> 0 then exit; // Compare FFirst Result := sdCompareByte(Symbol1.FFirst^, Symbol2.FFirst^); if Result <> 0 then exit; // CharCount of RS1 (and RS2, since they are equal) CharCount := Symbol1.CharCount; // Setup First & Last pointers First1 := Symbol1.FFirst; First2 := Symbol2.FFirst; // compare memory (boolean op). CompareMem might have optimized code depending // on memory manager (ASM, MMX, SSE etc) to binary compare the block. // Since sdCompareRefString may be used to compare relatively large blocks of // text, which are often exact copies, using CompareMem before special comparison // is warrented. IsEqual := CompareMem(First1, First2, CharCount); if IsEqual then begin Result := 0; exit; end; // finally the special conparison: Compare each time last ptrs then first ptrs, // until they meet in the middle Last1 := First1; inc(Last1, CharCount); Last2 := First2; inc(Last2, CharCount); repeat dec(Last1); dec(Last2); if First1 = Last1 then exit; Result := sdCompareByte(Last1^, Last2^); if Result <> 0 then exit; inc(First1); inc(First2); if First1 = Last1 then exit; Result := sdCompareByte(First1^, First2^); if Result <> 0 then exit; until False; end; { TsdSymbol } function TsdSymbol.AsString: Utf8String; begin SetString(Result, PAnsiChar(FFirst), FCharCount); end; destructor TsdSymbol.Destroy; begin FreeMem(FFirst); inherited; end; { TsdSymbolList } function TsdSymbolList.GetItems(Index: integer): TsdSymbol; begin Result := Get(Index); end; function TsdSymbolList.Find(ASymbol: TsdSymbol; var Index: integer): boolean; var AMin, AMax: integer; begin Result := False; // Find position - binary method AMin := 0; AMax := Count; while AMin < AMax do begin Index := (AMin + AMax) div 2; case sdCompareSymbol(Items[Index], ASymbol) of -1: AMin := Index + 1; 0: begin Result := True; exit; end; 1: AMax := Index; end; end; Index := AMin; end; { TsdSymbolTable } function TsdSymbolTable.AddString(const S: Utf8String): integer; var Found: boolean; L, BySymbolIndex: integer; ASymbol, Item: TsdSymbol; begin Result := 0; L := length(S); // zero-length string if L = 0 then exit; ASymbol := TsdSymbol.Create; try ASymbol.FFirst := PByte(@S[1]); ASymbol.FCharCount := L; // Try to find the new string Found := TsdSymbolList(FBySymbol).Find(ASymbol, BySymbolIndex); if Found then begin // yes it is found Item := TsdSymbol(FBySymbol[BySymbolIndex]); Result := Item.FID; exit; end; // Not found.. must make new item Item := TsdSymbol.Create; Item.FCharCount := ASymbol.FCharCount; // reallocate memory and copy the string data ReallocMem(Item.FFirst, Item.FCharCount); Move(S[1], Item.FFirst^, Item.FCharCount); // add to the ByID objectlist FByID.Add(Item); Item.FID := FByID.Count; Result := Item.FID; // insert into the ByRS list FBySymbol.Insert(BySymbolIndex, Item); finally // this ensures we do not deallocate the memory that may be in use elsewhere ASymbol.FFirst := nil; ASymbol.Free; end; end; procedure TsdSymbolTable.Clear; begin FByID.Clear; FBySymbol.Clear; end; procedure TsdSymbolTable.ClearFrequency; var i: integer; begin for i := 0 to FByID.Count - 1 do TsdSymbol(FByID[i]).FFreq := 0; end; constructor TsdSymbolTable.Create(AOwner: TComponent); begin inherited Create(AOwner); FByID := TObjectList.Create(True); FBySymbol := TsdSymbolList.Create(False); end; destructor TsdSymbolTable.Destroy; begin FreeAndNil(FBySymbol); FreeAndNil(FByID); inherited; end; function TsdSymbolTable.GetSymbolCount: integer; begin Result := FByID.Count; end; function TsdSymbolTable.GetString(ID: integer): Utf8String; begin // Find the ID // zero string if ID <= 0 then begin Result := ''; exit; end; // out of bounds? if ID > FByID.Count then begin // output warning DoDebugOut(Self, wsWarn, 'string ID not found'); Result := ''; end; Result := TsdSymbol(FByID[ID - 1]).AsString; end; procedure TsdSymbolTable.IncrementFrequency(ID: integer); var RS: TsdSymbol; begin RS := TsdSymbol(FByID[ID - 1]); inc(RS.FFreq); end; procedure TsdSymbolTable.LoadFromFile(const AFileName: string); var S: TMemoryStream; begin S := TMemoryStream.Create; try S.LoadFromFile(AFileName); LoadFromStream(S); finally S.Free; end; end; procedure TsdSymbolTable.LoadFromStream(S: TStream); var i: integer; TableCount: Cardinal; begin Clear; // DoDebugOut(Self, wsInfo, format('stream position: %d', [S.Position])); // table count TableCount := sdStreamReadCardinal(S); if TableCount = 0 then exit; for i := 0 to TableCount - 1 do begin LoadSymbol(S); end; end; function TsdSymbolTable.LoadSymbol(S: TStream): Cardinal; var Symbol: TsdSymbol; BySymbolIndex: integer; Found: boolean; begin Symbol := TsdSymbol.Create; // For now, we just use ssString uniquely as symbol style,. // In updates, different symbol styles can be added. Symbol.FSymbolStyle := sdStreamReadCardinal(S); Symbol.FCharCount := sdStreamReadCardinal(S); if Symbol.FCharCount > 0 then begin // reallocate memory and copy the string data ReallocMem(Symbol.FFirst, Symbol.FCharCount); S.Read(Symbol.FFirst^, Symbol.FCharCount); end; // add to the ByID objectlist FByID.Add(Symbol); Symbol.FID := FByID.Count; Result := Symbol.FID; // find the symbol Found := TsdSymbolList(FBySymbol).Find(Symbol, BySymbolIndex); if Found then begin DoDebugOut(Self, wsFail, 'duplicate symbol!'); exit; end; // insert into the ByRS list FBySymbol.Insert(BySymbolIndex, Symbol); end; procedure TsdSymbolTable.SaveToFile(const AFileName: string); var S: TMemoryStream; begin S := TMemoryStream.Create; try SaveToStream(S, SymbolCount); S.SaveToFile(AFileName); finally S.Free; end; end; procedure TsdSymbolTable.SaveToStream(S: TStream; ACount: integer); var i: integer; begin // write (part of the) symbol table sdStreamWriteCardinal(S, ACount); for i := 0 to ACount - 1 do begin SaveSymbol(S, i + 1); end; end; procedure TsdSymbolTable.SaveSymbol(S: TStream; ASymbolID: Cardinal); var RS: TsdSymbol; StringVal: Utf8String; CharCount: Cardinal; begin if ASymbolID <= 0 then DoDebugOut(Self, wsFail, 'symbol ID <= 0'); RS := TsdSymbol(FByID[ASymbolID - 1]); // For now, we just use ssString uniquely as symbol style. // In updates, different symbol styles can be added. sdStreamWriteCardinal(S, RS.SymbolStyle); StringVal := RS.AsString; CharCount := length(StringVal); sdStreamWriteCardinal(S, CharCount); sdStreamWriteString(S, StringVal); end; procedure TsdSymbolTable.SortByFrequency(var ANewIDs: array of Cardinal); // local function CompareFreq(Pos1, Pos2: integer): integer; var RS1, RS2: TsdSymbol; begin RS1 := TsdSymbol(FByID[Pos1]); RS2 := TsdSymbol(FByID[Pos2]); if RS1.FFreq > RS2.FFreq then Result := -1 else if RS1.FFreq < RS2.FFreq then Result := 1 else Result := 0; end; // local procedure QuickSort(iLo, iHi: Integer); var Lo, Hi, Mid: longint; begin Lo := iLo; Hi := iHi; Mid:= (Lo + Hi) div 2; repeat while CompareFreq(Lo, Mid) < 0 do Inc(Lo); while CompareFreq(Hi, Mid) > 0 do Dec(Hi); if Lo <= Hi then begin // Swap pointers; FByID.Exchange(Lo, Hi); if Mid = Lo then Mid := Hi else if Mid = Hi then Mid := Lo; Inc(Lo); Dec(Hi); end; until Lo > Hi; if Hi > iLo then QuickSort(iLo, Hi); if Lo < iHi then QuickSort(Lo, iHi); end; // main var i: integer; begin // sort by frequency QuickSort(0, FByID.Count - 1); // plural count FPluralSymbolCount := 0; i := 0; while i < FByID.Count do begin if TsdSymbol(FByID[i]).FFreq >= 2 then inc(FPluralSymbolCount) else break; inc(i); end; // tell app about new ID for i := 0 to FByID.Count - 1 do begin ANewIDs[TsdSymbol(FByID[i]).FID] := i + 1; end; // then rename IDs for i := 0 to FByID.Count - 1 do begin TsdSymbol(FByID[i]).FID := i + 1; end; end; {utility functions} function sdCompareByte(Byte1, Byte2: byte): integer; begin if Byte1 < Byte2 then Result := -1 else if Byte1 > Byte2 then Result := 1 else Result := 0; end; function sdCompareInteger(Int1, Int2: integer): integer; begin if Int1 < Int2 then Result := -1 else if Int1 > Int2 then Result := 1 else Result := 0; end; function sdUtf16ToUtf8Mem(Src: Pword; Dst: Pbyte; Count: integer): integer; // Convert an Unicode (UTF16 LE) memory block to UTF8. This routine will process // Count wide characters (2 bytes size) to Count UTF8 characters (1-3 bytes). // Therefore, the block at Dst must be at least 1.5 the size of the source block. // The function returns the number of *bytes* written. var W: word; DStart: Pbyte; begin DStart := Dst; while Count > 0 do begin W := Src^; inc(Src); if W <= $7F then begin Dst^ := byte(W); inc(Dst); end else begin if W > $7FF then begin Dst^ := byte($E0 or (W shr 12)); inc(Dst); Dst^ := byte($80 or ((W shr 6) and $3F)); inc(Dst); Dst^ := byte($80 or (W and $3F)); inc(Dst); end else begin // $7F < W <= $7FF Dst^ := byte($C0 or (W shr 6)); inc(Dst); Dst^ := byte($80 or (W and $3F)); inc(Dst); end; end; dec(Count); end; Result := integer(Dst) - integer(DStart); end; function sdUtf8ToUtf16Mem(var Src: Pbyte; Dst: Pword; Count: integer): integer; // Convert an UTF8 memory block to Unicode (UTF16 LE). This routine will process // Count *bytes* of UTF8 (each character 1-3 bytes) into UTF16 (each char 2 bytes). // Therefore, the block at Dst must be at least 2 times the size of Count, since // many UTF8 characters consist of just one byte, and are mapped to 2 bytes. The // function returns the number of *wide chars* written. Note that the Src block must // have an exact number of UTF8 characters in it, if Count doesn't match then // the last character will be converted anyway (going past the block boundary!) var W: word; C: byte; DStart: Pword; SClose: Pbyte; begin DStart := Dst; SClose := Src; inc(SClose, Count); while integer(Src) < integer(SClose) do begin // 1st byte W := Src^; inc(Src); if W and $80 <> 0 then begin W := W and $3F; if W and $20 <> 0 then begin // 2nd byte C := Src^; inc(Src); if C and $C0 <> $80 then // malformed trail byte or out of range char Continue; W := (W shl 6) or (C and $3F); end; // 2nd or 3rd byte C := Src^; inc(Src); if C and $C0 <> $80 then // malformed trail byte Continue; Dst^ := (W shl 6) or (C and $3F); inc(Dst); end else begin Dst^ := W; inc(Dst); end; end; Result := (integer(Dst) - integer(DStart)) div 2; end; { stream methods } function sdStreamReadCardinal(S: TStream): Cardinal; var C: byte; Bits: integer; begin Result := 0; Bits := 0; repeat S.Read(C, 1); if C > 0 then begin inc(Result, (C and $7F) shl Bits); inc(Bits, 7) end; until(C and $80) = 0; end; function sdStreamReadString(S: TStream; ACharCount: Cardinal): Utf8String; begin SetLength(Result, ACharCount); if ACharCount = 0 then exit; S.Read(Result[1], ACharCount); end; procedure sdStreamWriteCardinal(S: TStream; ACardinal: Cardinal); var C: byte; begin repeat if ACardinal <= $7F then begin C := ACardinal; S.Write(C, 1); exit; end else C := (ACardinal and $7F) or $80; S.Write(C, 1); ACardinal := ACardinal shr 7; until ACardinal = 0; end; procedure sdStreamWriteString(S: TStream; const AString: Utf8String); var L: integer; begin L := Length(AString); if L > 0 then begin S.Write(AString[1], L); end; end; end.