| 1 | unit NameDetails;
|
|---|
| 2 |
|
|---|
| 3 | interface
|
|---|
| 4 |
|
|---|
| 5 | uses
|
|---|
| 6 | Classes, SysUtils, Common, LazUTF8;
|
|---|
| 7 |
|
|---|
| 8 | type
|
|---|
| 9 | TNamePartKind = (npNone, npPrefix, npFirst, npMiddle, npLast, npSuffix);
|
|---|
| 10 |
|
|---|
| 11 | TNamePart = record
|
|---|
| 12 | Index: Integer;
|
|---|
| 13 | Text: string;
|
|---|
| 14 | PartKind: TNamePartKind;
|
|---|
| 15 | NamePart: ^TNamePart;
|
|---|
| 16 | Previous: ^TNamePart;
|
|---|
| 17 | Next: ^TNamePart;
|
|---|
| 18 | end;
|
|---|
| 19 |
|
|---|
| 20 | TNameParts = array of TNamePart;
|
|---|
| 21 |
|
|---|
| 22 | { TNameDetails }
|
|---|
| 23 |
|
|---|
| 24 | TNameDetails = class
|
|---|
| 25 | private
|
|---|
| 26 | function GetAsNameParts: TNameParts;
|
|---|
| 27 | function GetDetail(NamePartKind: TNamePartKind): string;
|
|---|
| 28 | function IsSuffix(Text: string): Boolean;
|
|---|
| 29 | public
|
|---|
| 30 | Prefix: string;
|
|---|
| 31 | First: string;
|
|---|
| 32 | Middle: string;
|
|---|
| 33 | Last: string;
|
|---|
| 34 | Suffix: string;
|
|---|
| 35 | procedure Split(FullName: string);
|
|---|
| 36 | function GetCombined: string;
|
|---|
| 37 | end;
|
|---|
| 38 |
|
|---|
| 39 |
|
|---|
| 40 | implementation
|
|---|
| 41 |
|
|---|
| 42 | function IsNumber(Text: string): Boolean;
|
|---|
| 43 | var
|
|---|
| 44 | Value: Integer;
|
|---|
| 45 | begin
|
|---|
| 46 | Result := TryStrToInt(Text, Value);
|
|---|
| 47 | end;
|
|---|
| 48 |
|
|---|
| 49 | function IsRomanNumber(Text: string): Boolean;
|
|---|
| 50 | var
|
|---|
| 51 | I: Integer;
|
|---|
| 52 | begin
|
|---|
| 53 | Result := True;
|
|---|
| 54 | for I := 1 to Length(Text) do
|
|---|
| 55 | if not (Text[I] in ['I', 'V', 'X', 'L', 'C', 'D', 'M']) then begin
|
|---|
| 56 | Result := False;
|
|---|
| 57 | Break;
|
|---|
| 58 | end;
|
|---|
| 59 | end;
|
|---|
| 60 |
|
|---|
| 61 | procedure SearchPart(var NameParts: TNameParts; var NamePart: TNamePart);
|
|---|
| 62 | var
|
|---|
| 63 | I: Integer;
|
|---|
| 64 | begin
|
|---|
| 65 | for I := 0 to Length(NameParts) - 1 do begin
|
|---|
| 66 | if (NameParts[I].PartKind = npNone) and (NameParts[I].Text = NamePart.Text) then begin
|
|---|
| 67 | NameParts[I].PartKind := NamePart.PartKind;
|
|---|
| 68 | NameParts[I].NamePart := @NamePart;
|
|---|
| 69 | NamePart.NamePart := @NameParts[I];
|
|---|
| 70 | Break;
|
|---|
| 71 | end;
|
|---|
| 72 | end;
|
|---|
| 73 | end;
|
|---|
| 74 |
|
|---|
| 75 | procedure SearchPartBackward(var NameParts: TNameParts; var NamePart: TNamePart);
|
|---|
| 76 | var
|
|---|
| 77 | I: Integer;
|
|---|
| 78 | begin
|
|---|
| 79 | for I := Length(NameParts) - 1 downto 0 do begin
|
|---|
| 80 | if (NameParts[I].PartKind = npNone) and (NameParts[I].Text = NamePart.Text) then begin
|
|---|
| 81 | NameParts[I].PartKind := NamePart.PartKind;
|
|---|
| 82 | NameParts[I].NamePart := @NamePart;
|
|---|
| 83 | NamePart.NamePart := @NameParts[I];
|
|---|
| 84 | Break;
|
|---|
| 85 | end;
|
|---|
| 86 | end;
|
|---|
| 87 | end;
|
|---|
| 88 |
|
|---|
| 89 | function UsedInNameParts(NamePart: TNamePartKind; NameParts: TNameParts): Boolean;
|
|---|
| 90 | var
|
|---|
| 91 | I: Integer;
|
|---|
| 92 | begin
|
|---|
| 93 | I := 0;
|
|---|
| 94 | while (I < Length(NameParts)) and (NameParts[I].PartKind <> NamePart) do Inc(I);
|
|---|
| 95 | Result := I < Length(NameParts);
|
|---|
| 96 | end;
|
|---|
| 97 |
|
|---|
| 98 | { TNameDetails }
|
|---|
| 99 |
|
|---|
| 100 | function TNameDetails.GetAsNameParts: TNameParts;
|
|---|
| 101 | var
|
|---|
| 102 | I: Integer;
|
|---|
| 103 | K: TNamePartKind;
|
|---|
| 104 | Parts: TStringArray;
|
|---|
| 105 | begin
|
|---|
| 106 | Result := Default(TNameParts);
|
|---|
| 107 | for K := Low(TNamePartKind) to High(TNamePartKind) do begin
|
|---|
| 108 | if GetDetail(K) <> '' then begin
|
|---|
| 109 | Parts := Explode(' ', GetDetail(K));
|
|---|
| 110 | for I := 0 to Length(Parts) - 1 do begin
|
|---|
| 111 | SetLength(Result, Length(Result) + 1);
|
|---|
| 112 | Result[Length(Result) - 1].Text := Parts[I];
|
|---|
| 113 | Result[Length(Result) - 1].PartKind := K;
|
|---|
| 114 | Result[Length(Result) - 1].Index := Length(Result) - 1;
|
|---|
| 115 | end;
|
|---|
| 116 | end;
|
|---|
| 117 | end;
|
|---|
| 118 |
|
|---|
| 119 | // Update previous and next links
|
|---|
| 120 | for I := 0 to Length(Result) - 1 do begin
|
|---|
| 121 | if I > 0 then
|
|---|
| 122 | Result[I].Previous := @Result[I - 1];
|
|---|
| 123 | if (I + 1) < Length(Result) then
|
|---|
| 124 | Result[I].Next := @Result[I + 1];
|
|---|
| 125 | end;
|
|---|
| 126 | end;
|
|---|
| 127 |
|
|---|
| 128 | function TNameDetails.GetDetail(NamePartKind: TNamePartKind): string;
|
|---|
| 129 | begin
|
|---|
| 130 | case NamePartKind of
|
|---|
| 131 | npPrefix: Result := Prefix;
|
|---|
| 132 | npFirst: Result := First;
|
|---|
| 133 | npMiddle: Result := Middle;
|
|---|
| 134 | npLast: Result := Last;
|
|---|
| 135 | npSuffix: Result := Suffix;
|
|---|
| 136 | end;
|
|---|
| 137 | end;
|
|---|
| 138 |
|
|---|
| 139 | function TNameDetails.IsSuffix(Text: string): Boolean;
|
|---|
| 140 | begin
|
|---|
| 141 | Result := (Pos('.', Text) > 0) or IsNumber(Text) or
|
|---|
| 142 | IsRomanNumber(Text);
|
|---|
| 143 | end;
|
|---|
| 144 |
|
|---|
| 145 | procedure TNameDetails.Split(FullName: string);
|
|---|
| 146 | var
|
|---|
| 147 | Parts: TStringArray;
|
|---|
| 148 | NewNameParts: TNameParts;
|
|---|
| 149 | OldNameParts: TNameParts;
|
|---|
| 150 | I: Integer;
|
|---|
| 151 | J: Integer;
|
|---|
| 152 | Text: string;
|
|---|
| 153 | NextKind: TNamePartKind;
|
|---|
| 154 | begin
|
|---|
| 155 | OldNameParts := GetAsNameParts;
|
|---|
| 156 |
|
|---|
| 157 | Text := FullName;
|
|---|
| 158 | while Pos(' ', FullName) > 0 do
|
|---|
| 159 | FullName := StringReplace(FullName, ' ', ' ', [rfReplaceAll]);
|
|---|
| 160 | Text := Trim(Text);
|
|---|
| 161 |
|
|---|
| 162 | Parts := Explode(' ', Text);
|
|---|
| 163 | NewNameParts := Default(TNameParts);
|
|---|
| 164 | SetLength(NewNameParts, Length(Parts));
|
|---|
| 165 | for I := 0 to Length(NewNameParts) - 1 do begin
|
|---|
| 166 | NewNameParts[I].Index := I;
|
|---|
| 167 | NewNameParts[I].PartKind := npNone;
|
|---|
| 168 | NewNameParts[I].Text := Parts[I];
|
|---|
| 169 | if I > 0 then
|
|---|
| 170 | NewNameParts[I].Previous := @NewNameParts[I - 1];
|
|---|
| 171 | if (I + 1) < Length(NewNameParts) then
|
|---|
| 172 | NewNameParts[I].Next := @NewNameParts[I + 1];
|
|---|
| 173 | end;
|
|---|
| 174 |
|
|---|
| 175 | // Match existing parts
|
|---|
| 176 | for I := 0 to Length(OldNameParts) - 1 do begin
|
|---|
| 177 | if OldNameParts[I].Text <> '' then
|
|---|
| 178 | SearchPart(NewNameParts, OldNameParts[I]);
|
|---|
| 179 | end;
|
|---|
| 180 |
|
|---|
| 181 | // Check incorrect matches
|
|---|
| 182 | for I := 0 to Length(OldNameParts) - 1 do begin
|
|---|
| 183 | for J := I + 1 to Length(OldNameParts) - 1 do
|
|---|
| 184 | if Assigned(OldNameParts[I].NamePart) and Assigned(OldNameParts[J].NamePart) and
|
|---|
| 185 | (OldNameParts[I].NamePart^.Index >= OldNameParts[J].NamePart^.Index) then begin
|
|---|
| 186 | if Abs(I - OldNameParts[I].NamePart^.Index) >
|
|---|
| 187 | Abs(J - OldNameParts[J].NamePart^.Index) then begin
|
|---|
| 188 | OldNameParts[I].NamePart^.PartKind := npNone;
|
|---|
| 189 | OldNameParts[I].NamePart^.NamePart := nil;
|
|---|
| 190 | OldNameParts[I].NamePart := nil;
|
|---|
| 191 | end else begin
|
|---|
| 192 | OldNameParts[J].NamePart^.PartKind := npNone;
|
|---|
| 193 | OldNameParts[J].NamePart^.NamePart := nil;
|
|---|
| 194 | OldNameParts[J].NamePart := nil;
|
|---|
| 195 | end;
|
|---|
| 196 | end;
|
|---|
| 197 | end;
|
|---|
| 198 | for I := Length(OldNameParts) - 1 downto 0 do begin
|
|---|
| 199 | for J := I - 1 downto 0 do
|
|---|
| 200 | if Assigned(OldNameParts[I].NamePart) and Assigned(OldNameParts[J].NamePart) and
|
|---|
| 201 | (OldNameParts[I].NamePart^.Index <= OldNameParts[J].NamePart^.Index) then begin
|
|---|
| 202 | if Abs(I - OldNameParts[I].NamePart^.Index) >
|
|---|
| 203 | Abs(J - OldNameParts[J].NamePart^.Index) then begin
|
|---|
| 204 | OldNameParts[I].NamePart^.PartKind := npNone;
|
|---|
| 205 | OldNameParts[I].NamePart^.NamePart := nil;
|
|---|
| 206 | OldNameParts[I].NamePart := nil;
|
|---|
| 207 | end else begin
|
|---|
| 208 | OldNameParts[J].NamePart^.PartKind := npNone;
|
|---|
| 209 | OldNameParts[J].NamePart^.NamePart := nil;
|
|---|
| 210 | OldNameParts[J].NamePart := nil;
|
|---|
| 211 | end;
|
|---|
| 212 | end;
|
|---|
| 213 | end;
|
|---|
| 214 |
|
|---|
| 215 | // Match existing parts backqards
|
|---|
| 216 | for I := Length(OldNameParts) - 1 downto 0 do begin
|
|---|
| 217 | if (OldNameParts[I].Text <> '') and not Assigned(OldNameParts[I].NamePart) then
|
|---|
| 218 | SearchPartBackward(NewNameParts, OldNameParts[I]);
|
|---|
| 219 | end;
|
|---|
| 220 |
|
|---|
| 221 | // Match uncertain parts
|
|---|
| 222 | for I := 0 to Length(OldNameParts) - 1 do
|
|---|
| 223 | if not Assigned(OldNameParts[I].NamePart) then begin
|
|---|
| 224 | if Assigned(OldNameParts[I].Next) and
|
|---|
| 225 | Assigned(OldNameParts[I].Next^.NamePart) and
|
|---|
| 226 | Assigned(OldNameParts[I].Next^.NamePart^.Previous) and
|
|---|
| 227 | (OldNameParts[I].Next^.NamePart^.Previous^.PartKind = npNone) then begin
|
|---|
| 228 | OldNameParts[I].NamePart := OldNameParts[I].Next^.NamePart^.Previous;
|
|---|
| 229 | OldNameParts[I].Next^.NamePart^.Previous^.NamePart := @OldNameParts[I];
|
|---|
| 230 | OldNameParts[I].Next^.NamePart^.Previous^.PartKind := OldNameParts[I].PartKind;
|
|---|
| 231 | end else
|
|---|
| 232 | if Assigned(OldNameParts[I].Previous) and
|
|---|
| 233 | Assigned(OldNameParts[I].Previous^.NamePart) and
|
|---|
| 234 | Assigned(OldNameParts[I].Previous^.NamePart^.Next) and
|
|---|
| 235 | (OldNameParts[I].Previous^.NamePart^.Next^.PartKind = npNone) then begin
|
|---|
| 236 | OldNameParts[I].NamePart := OldNameParts[I].Previous^.NamePart^.Next;
|
|---|
| 237 | OldNameParts[I].Previous^.NamePart^.Next^.NamePart := @OldNameParts[I];
|
|---|
| 238 | OldNameParts[I].Previous^.NamePart^.Next^.PartKind := OldNameParts[I].PartKind;
|
|---|
| 239 | end;
|
|---|
| 240 | end;
|
|---|
| 241 |
|
|---|
| 242 | // Mark new unknown parts according existing parts
|
|---|
| 243 | for I := Length(Parts) - 1 downto 0 do
|
|---|
| 244 | if (NewNameParts[I].PartKind = npNone) and
|
|---|
| 245 | Assigned(NewNameParts[I].Next) and
|
|---|
| 246 | (NewNameParts[I].Next^.PartKind <> npNone) then begin
|
|---|
| 247 | if (NewNameParts[I].Next^.PartKind = npFirst) and
|
|---|
| 248 | EndsWith(NewNameParts[I].Text, '.') then begin
|
|---|
| 249 | NewNameParts[I].PartKind := npPrefix;
|
|---|
| 250 | end else NewNameParts[I].PartKind := NewNameParts[I].Next^.PartKind;
|
|---|
| 251 | end;
|
|---|
| 252 |
|
|---|
| 253 | // Mark unknown parts according to neighbouring parts
|
|---|
| 254 | for I := 0 to Length(Parts) - 1 do
|
|---|
| 255 | if (NewNameParts[I].PartKind = npNone) and
|
|---|
| 256 | Assigned(NewNameParts[I].Previous) and
|
|---|
| 257 | (NewNameParts[I].Previous^.PartKind <> npNone) then begin
|
|---|
| 258 | if (NewNameParts[I].Previous^.PartKind in [npLast, npMiddle]) and
|
|---|
| 259 | IsSuffix(NewNameParts[I].Text) then begin
|
|---|
| 260 | NewNameParts[I].PartKind := npSuffix;
|
|---|
| 261 | end else
|
|---|
| 262 | if (NewNameParts[I].Previous^.PartKind = npFirst) and
|
|---|
| 263 | (Last = '') then begin
|
|---|
| 264 | NewNameParts[I].PartKind := npLast;
|
|---|
| 265 | end else
|
|---|
| 266 | if (NewNameParts[I].Previous^.PartKind = npLast) and
|
|---|
| 267 | (Middle = '') then begin
|
|---|
| 268 | NewNameParts[I].PartKind := npLast;
|
|---|
| 269 | NewNameParts[I].Previous^.PartKind := npMiddle;
|
|---|
| 270 | end else
|
|---|
| 271 | if (NewNameParts[I].Previous^.PartKind = npPrefix) then begin
|
|---|
| 272 | NewNameParts[I].PartKind := npFirst;
|
|---|
| 273 | end else
|
|---|
| 274 | NewNameParts[I].PartKind := NewNameParts[I].Previous^.PartKind;
|
|---|
| 275 | end;
|
|---|
| 276 |
|
|---|
| 277 | // Mark remaining unknown parts based on defined filling sequence
|
|---|
| 278 | NextKind := npFirst;
|
|---|
| 279 | for I := 0 to Length(Parts) - 1 do
|
|---|
| 280 | if NewNameParts[I].PartKind = npNone then begin
|
|---|
| 281 | if EndsWith(NewNameParts[I].Text, '.') and (NextKind = npFirst) then begin
|
|---|
| 282 | NewNameParts[I].PartKind := npPrefix;
|
|---|
| 283 | end else
|
|---|
| 284 | if (NextKind = npMiddle) and IsSuffix(NewNameParts[I].Text) then begin
|
|---|
| 285 | NewNameParts[I].PartKind := npSuffix;
|
|---|
| 286 | NextKind := npSuffix;
|
|---|
| 287 | end else
|
|---|
| 288 | if NextKind = npMiddle then begin
|
|---|
| 289 | NewNameParts[I].Previous^.PartKind := npMiddle;
|
|---|
| 290 | NewNameParts[I].PartKind := npLast;
|
|---|
| 291 | end else begin
|
|---|
| 292 | NewNameParts[I].PartKind := NextKind;
|
|---|
| 293 | if NextKind = npFirst then NextKind := npLast
|
|---|
| 294 | else if NextKind = npLast then NextKind := npMiddle;
|
|---|
| 295 | end;
|
|---|
| 296 | end;
|
|---|
| 297 |
|
|---|
| 298 | // Combine multiple parts to base parts
|
|---|
| 299 | Prefix := '';
|
|---|
| 300 | First := '';
|
|---|
| 301 | Middle := '';
|
|---|
| 302 | Last := '';
|
|---|
| 303 | Suffix := '';
|
|---|
| 304 | for I := 0 to Length(Parts) - 1 do
|
|---|
| 305 | case NewNameParts[I].PartKind of
|
|---|
| 306 | npPrefix: Prefix := Trim(Prefix + ' ' + Parts[I]);
|
|---|
| 307 | npFirst: First := Trim(First + ' ' + Parts[I]);
|
|---|
| 308 | npMiddle: Middle := Trim(Middle + ' ' + Parts[I]);
|
|---|
| 309 | npLast: Last := Trim(Last + ' ' + Parts[I]);
|
|---|
| 310 | npSuffix: Suffix := Trim(Suffix + ' ' + Parts[I]);
|
|---|
| 311 | end;
|
|---|
| 312 |
|
|---|
| 313 | {
|
|---|
| 314 | // Title Prefix
|
|---|
| 315 | while (Length(Parts) > 0) and EndsWith(Parts[0], '.') do begin
|
|---|
| 316 | Prefix := Trim(Prefix + ' ' + Parts[0]);
|
|---|
| 317 | Delete(Parts, 0, 1);
|
|---|
| 318 | end;
|
|---|
| 319 |
|
|---|
| 320 | // Title Suffix
|
|---|
| 321 | if ProcessAfter then
|
|---|
| 322 | for I := 0 to High(Parts) do
|
|---|
| 323 | if (Pos('.', Parts[I]) > 0) or IsNumber(Parts[I]) or IsRomanNumber(Parts[I]) then begin
|
|---|
| 324 | for J := I to High(Parts) do
|
|---|
| 325 | Suffix := Trim(Suffix + ' ' + Parts[J]);
|
|---|
| 326 | SetLength(Parts, I);
|
|---|
| 327 | Break;
|
|---|
| 328 | end;
|
|---|
| 329 |
|
|---|
| 330 | if Length(Parts) = 0 then begin
|
|---|
| 331 | end else
|
|---|
| 332 | if Length(Parts) = 1 then begin
|
|---|
| 333 | First := Parts[0];
|
|---|
| 334 | end else
|
|---|
| 335 | if Length(Parts) = 2 then begin
|
|---|
| 336 | First := Parts[0];
|
|---|
| 337 | Last := Parts[1];
|
|---|
| 338 | end else begin
|
|---|
| 339 | First := Parts[0];
|
|---|
| 340 | for I := 0 to Length(Parts) - 3 do
|
|---|
| 341 | Middle := Trim(Middle + ' ' + Parts[I + 1]);
|
|---|
| 342 | Last := Parts[High(Parts)];
|
|---|
| 343 | end;}
|
|---|
| 344 | end;
|
|---|
| 345 |
|
|---|
| 346 | function TNameDetails.GetCombined: string;
|
|---|
| 347 | begin
|
|---|
| 348 | Result := '';
|
|---|
| 349 | if Prefix <> '' then Result := Result + ' ' + Prefix;
|
|---|
| 350 | if First <> '' then Result := Result + ' ' + First;
|
|---|
| 351 | if Middle <> '' then Result := Result + ' ' + Middle;
|
|---|
| 352 | if Last <> '' then Result := Result + ' ' + Last;
|
|---|
| 353 | if Suffix <> '' then Result := Result + ' ' + Suffix;
|
|---|
| 354 | Result := Trim(Result);
|
|---|
| 355 | end;
|
|---|
| 356 |
|
|---|
| 357 | end.
|
|---|
| 358 |
|
|---|