From 9cb465ffe1116b1c4f38e6d8d858ed47e7020212 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Wed, 7 Apr 2021 17:00:35 +0700 Subject: [PATCH] Add methods for finding substrings in `Text` --- sources/Text/Tests/TEST_Text.uc | 183 ++++++++++++++++++++++++++++++++ sources/Text/Text.uc | 113 +++++++++++++++++++- 2 files changed, 294 insertions(+), 2 deletions(-) diff --git a/sources/Text/Tests/TEST_Text.uc b/sources/Text/Tests/TEST_Text.uc index f205738..c466e3a 100644 --- a/sources/Text/Tests/TEST_Text.uc +++ b/sources/Text/Tests/TEST_Text.uc @@ -38,6 +38,7 @@ protected static function TESTS() Test_Substring(); Test_SeparateByCharacter(); Test_StartsEndsWith(); + Test_IndexOf(); } protected static function Test_TextCreation() @@ -695,6 +696,188 @@ protected static function SubTest_EndsWith_Formatting() .EndsWith(F("{#454545 r}aZy"),, SFORM_SENSITIVE)); } +protected static function Test_IndexOf() +{ + Context("Testing `IndexOf()` method with non-formatted `Text`s."); + SubTest_IndexOfSuccess(SFORM_SENSITIVE); + SubTest_IndexOfSuccess(SFORM_INSENSITIVE); + SubTest_IndexOfFail(SFORM_SENSITIVE); + SubTest_IndexOfFail(SFORM_INSENSITIVE); + Context("Testing `IndexOf()` method with formatted `Text`s."); + SubTest_IndexOfFormatting(); + Context("Testing `LastIndexOf()` method with non-formatted `Text`s."); + SubTest_LastIndexOfSuccess(SFORM_SENSITIVE); + SubTest_LastIndexOfSuccess(SFORM_INSENSITIVE); + SubTest_LastIndexOfFail(SFORM_SENSITIVE); + SubTest_LastIndexOfFail(SFORM_INSENSITIVE); + Context("Testing `LastIndexOf()` method with formatted `Text`s."); + SubTest_LastIndexOfFormatting(); +} + +protected static function SubTest_IndexOfSuccess(Text.FormatSensitivity flag) +{ + Issue("`IndexOf()` works incorrectly with empty `Text`s."); + TEST_ExpectTrue(P("").IndexOf(F("{rgb(4,5,6) }"),,, flag) == 0); + TEST_ExpectTrue(P("something").IndexOf(P(""),,, flag) == 0); + TEST_ExpectFalse( + F("{rgb(23,342,32) }").IndexOf(P("something"),,, flag) == 0); + + Issue("`IndexOf()` returns non-zero index for identical `Text`s."); + TEST_ExpectTrue(P("Just something").IndexOf(P("Just something")) == 0); + TEST_ExpectTrue( + P("CraZy").IndexOf(P("CRaZy"),, SCASE_INSENSITIVE, flag) == 0); + + Issue("`IndexOf()` returns wrong index for correct sub-`Text`s."); + TEST_ExpectTrue(P("Just something").IndexOf(P("some"),,, flag) == 5); + TEST_ExpectTrue(P("Just something").IndexOf(P("some"), 3,, flag) == 5); + TEST_ExpectTrue(P("Just something").IndexOf(P("some"), 5,, flag) == 5); + TEST_ExpectTrue( + P("Just some-something").IndexOf(P("some"), 7,, flag) == 10); + + Issue("`IndexOf()` returns wrong index for correct case-insensitive" @ + "sub-`Text`s."); + TEST_ExpectTrue(P("JUSt sOmEtHiNG") + .IndexOf(P("sOME"),, SCASE_INSENSITIVE, flag) + == 5); + TEST_ExpectTrue(P("JUSt sOmEtHiNG") + .IndexOf(P("SoMe"), 3, SCASE_INSENSITIVE, flag) + == 5); + TEST_ExpectTrue(P("JUSt sOmEtHiNG") + .IndexOf(P("sOMe"), 5, SCASE_INSENSITIVE, flag) + == 5); + TEST_ExpectTrue(P("JUSt soME-sOmEtHiNG") + .IndexOf(P("SomE"), 7, SCASE_INSENSITIVE, flag) + == 10); +} + +protected static function SubTest_IndexOfFail(Text.FormatSensitivity flag) +{ + Issue("`IndexOf()` returns non-negative index for longer `Text`s."); + TEST_ExpectTrue( + P("text").IndexOf(P("image"),, SCASE_INSENSITIVE, flag) < 0); + TEST_ExpectTrue(P("++text").IndexOf(P("text+"), 2,, flag) < 0); + + Issue("`IndexOf()` returns non-negative index when looking for `Text`s that" + @ "are not a substring."); + TEST_ExpectTrue(P("text").IndexOf(P("exd"),, SCASE_INSENSITIVE, flag) < 0); + TEST_ExpectTrue(P("A string").IndexOf(P(" string"),,, flag) < 0); + TEST_ExpectTrue(P("A string").IndexOf(P(" string"),,, flag) < 0); + TEST_ExpectTrue(P("A string").IndexOf(P("str"), 3,, flag) < 0); + TEST_ExpectTrue(P("A string").IndexOf(P("str"), 20,, flag) < 0); +} + +protected static function SubTest_IndexOfFormatting() +{ + Issue("`IndexOf()` returns non-zero index for identical `Text`s."); + TEST_ExpectTrue(F("Just {#4fe2ac some}thing") + .IndexOf(F("Just {#4fe2ac some}thing"),,, SFORM_SENSITIVE) + == 0); + TEST_ExpectTrue(F("Cra{#ff0000 Zy}") + .IndexOf(F("Cra{#ff0000 Zy}"),,, SFORM_SENSITIVE) + == 0); + + Issue("`IndexOf()` returns wrong index for correct sub-`Text`s."); + TEST_ExpectTrue(F("Just so{#4f632dc me-some}thing") + .IndexOf(F("so{#4f632dc me}"),,, SFORM_SENSITIVE) + == 5); + TEST_ExpectTrue(F("Just so{#4f632dc me-some}thing") + .IndexOf(F("so{#4f632dc me}"), 3,, SFORM_SENSITIVE) + == 5); + TEST_ExpectTrue(F("Just so{#4f632dc me-some}thing") + .IndexOf(F("so{#4f632dc me}"), 5,, SFORM_SENSITIVE) + == 5); + TEST_ExpectTrue(F("Just so{#4f632dc me-some}thing") + .IndexOf(F("{#4f632dc some}"),,, SFORM_SENSITIVE) + == 10); + TEST_ExpectTrue(F("Just so{#4f632dc me-some}thing") + .IndexOf(F("{#4f632dc some}"), 7,, SFORM_SENSITIVE) + == 10); +} + +protected static function SubTest_LastIndexOfSuccess( + Text.FormatSensitivity flag) +{ + Issue("`LastIndexOf()` works incorrectly with empty `Text`s."); + TEST_ExpectTrue(P("").LastIndexOf(F("{rgb(4,5,6) }"),,, flag) == 0); + TEST_ExpectTrue(P("something").LastIndexOf(P(""),,, flag) == 0); + TEST_ExpectFalse( + F("{rgb(23,342,32) }").LastIndexOf(P("something"),,, flag) == 0); + + Issue("`LastIndexOf()` returns non-zero index for identical `Text`s."); + TEST_ExpectTrue(P("Just something").LastIndexOf(P("Just something")) == 0); + TEST_ExpectTrue( + P("CraZy").LastIndexOf(P("CRaZy"),, SCASE_INSENSITIVE, flag) == 0); + + Issue("`LastIndexOf()` returns wrong index for correct sub-`Text`s."); + TEST_ExpectTrue(P("Just something").LastIndexOf(P("some"),,, flag) == 5); + TEST_ExpectTrue(P("Just something").LastIndexOf(P("some"), 3,, flag) == 5); + TEST_ExpectTrue(P("Just something").LastIndexOf(P("some"), 5,, flag) == 5); + TEST_ExpectTrue( + P("Just some-something").LastIndexOf(P("some"),,, flag) == 10); + TEST_ExpectTrue( + P("Just some-something").LastIndexOf(P("some"), 6,, flag) == 5); + + Issue("`LastIndexOf()` returns wrong index for correct case-insensitive" @ + "sub-`Text`s."); + TEST_ExpectTrue(P("JUSt sOmEtHiNG") + .LastIndexOf(P("sOME"),, SCASE_INSENSITIVE, flag) + == 5); + TEST_ExpectTrue(P("JUSt sOmEtHiNG") + .LastIndexOf(P("SoMe"), 3, SCASE_INSENSITIVE, flag) + == 5); + TEST_ExpectTrue(P("JUSt soME-sOmEtHiNG") + .LastIndexOf(P("sOMe"), 5, SCASE_INSENSITIVE, flag) + == 10); + TEST_ExpectTrue(P("JUSt soME-sOmEtHiNG") + .LastIndexOf(P("SomE"), 6, SCASE_INSENSITIVE, flag) + == 5); +} + +protected static function SubTest_LastIndexOfFail(Text.FormatSensitivity flag) +{ + Issue("`LastIndexOf()` returns non-negative index for longer `Text`s."); + TEST_ExpectTrue( + P("text").LastIndexOf(P("image"),, SCASE_INSENSITIVE, flag) < 0); + TEST_ExpectTrue(P("++text").LastIndexOf(P("text+"), 2,, flag) < 0); + + Issue("`LastIndexOf()` returns non-negative index when looking for `Text`s that" + @ "are not a substring."); + TEST_ExpectTrue( + P("text").LastIndexOf(P("exd"),, SCASE_INSENSITIVE, flag) < 0); + TEST_ExpectTrue(P("A string").LastIndexOf(P(" string"),,, flag) < 0); + TEST_ExpectTrue(P("A string").LastIndexOf(P(" string"),,, flag) < 0); + TEST_ExpectTrue(P("A string").LastIndexOf(P("str"), 4,, flag) < 0); + TEST_ExpectTrue(P("A string").LastIndexOf(P("str"), 20,, flag) < 0); +} + +protected static function SubTest_LastIndexOfFormatting() +{ + Issue("`LastIndexOf()` returns non-zero index for identical `Text`s."); + TEST_ExpectTrue(F("Just {#4fe2ac some}thing") + .LastIndexOf(F("Just {#4fe2ac some}thing"),,, SFORM_SENSITIVE) + == 0); + TEST_ExpectTrue(F("Cra{#ff0000 Zy}") + .LastIndexOf(F("Cra{#ff0000 Zy}"),,, SFORM_SENSITIVE) + == 0); + + Issue("`LastIndexOf()` returns wrong index for correct sub-`Text`s."); + TEST_ExpectTrue(F("Just so{#4f632dc me-some}thing") + .LastIndexOf(F("{#4f632dc some}"),,, SFORM_SENSITIVE) + == 10); + TEST_ExpectTrue(F("Just so{#4f632dc me-some}thing") + .LastIndexOf(F("{#4f632dc some}"), 3,, SFORM_SENSITIVE) + == 10); + TEST_ExpectTrue(F("Just so{#4f632dc me-some}thing") + .LastIndexOf(F("{#4f632dc some}"), 5,, SFORM_SENSITIVE) + == 10); + TEST_ExpectTrue(F("Just so{#4f632dc me-some}thing") + .LastIndexOf(F("so{#4f632dc me}"),,, SFORM_SENSITIVE) + == 5); + TEST_ExpectTrue(F("Just so{#4f632dc me-some}thing") + .LastIndexOf(F("so{#4f632dc me}"), 7,, SFORM_SENSITIVE) + == 5); +} + defaultproperties { caseName = "Text/MutableText" diff --git a/sources/Text/Text.uc b/sources/Text/Text.uc index 8296a97..f0d0507 100644 --- a/sources/Text/Text.uc +++ b/sources/Text/Text.uc @@ -573,7 +573,6 @@ public final function bool StartsWith( if (GetLength() < otherText.GetLength()) { return false; } - // Copy once to avoid doing it each iteration for (i = 0; i < otherText.GetLength(); i += 1) { char1 = GetCharacter(i); @@ -619,7 +618,6 @@ public final function bool EndsWith( if (GetLength() < otherText.GetLength()) { return false; } - // Copy once to avoid doing it each iteration index = GetLength() - 1; otherIndex = otherText.GetLength() - 1; while (otherIndex >= 0) @@ -1193,6 +1191,117 @@ public final function array SplitByCharacter(Character separator) return result; } +/** + * Returns the index position of the first occurrence of the `otherText` in + * the caller `Text`, searching forward from index position `fromIndex`. + * + * @param otherText `Text` data to find inside the caller `Text`. + * @param fromIndex Index from which we should start searching. + * @param caseSensitivity Defines whether comparison should be + * case-sensitive. By default it is. + * @param formatSensitivity Defines whether comparison should be + * sensitive for color information. By default it is not. + * @return `-1` if `otherText` is not found after `fromIndex`. + */ +public final function int IndexOf( + Text otherText, + optional int fromIndex, + optional CaseSensitivity caseSensitivity, + optional FormatSensitivity formatSensitivity) +{ + local int startCandidate; + local int index, otherIndex; + local Character char1, char2; + if (otherText == none) return -1; + if (fromIndex > GetLength()) return -1; + if (GetLength() - fromIndex < otherText.GetLength()) return -1; + if (otherText.IsEmpty()) return fromIndex; + + startCandidate = fromIndex; + for (index = fromIndex; index < GetLength(); index += 1) + { + char1 = GetCharacter(index); + char2 = otherText.GetCharacter(otherIndex); + if ( NormalizeCodePoint(char1.codePoint, caseSensitivity) + != NormalizeCodePoint(char2.codePoint, caseSensitivity)) + { + startCandidate = index + 1; + otherIndex = 0; + continue; + } + if ( formatSensitivity == SFORM_SENSITIVE + && !_.text.IsFormattingEqual(char1.formatting, char2.formatting)) + { + startCandidate = index + 1; + otherIndex = 0; + continue; + } + otherIndex += 1; + if (otherIndex == otherText.GetLength()) { + return startCandidate; + } + } + return -1; +} + +/** + * Returns the index position of the last occurrence of the `otherText` in + * the caller `Text`, searching backward from index position `fromIndex`, + * counted the end of the caller `Text`. + * + * @param otherText `Text` data to find inside the caller `Text`. + * @param fromIndex Index from which we should start searching, but + * it's counted from the end of the caller `Text`: `0` means starting from + * the last character, `1` means next to last, etc. + * @param caseSensitivity Defines whether comparison should be + * case-sensitive. By default it is. + * @param formatSensitivity Defines whether comparison should be + * sensitive for color information. By default it is not. + * @return `-1` if `otherText` is not found starting `fromIndex` + * (this index is counted from the end of the caller `Text`). + */ +public final function int LastIndexOf( + Text otherText, + optional int fromIndex, + optional CaseSensitivity caseSensitivity, + optional FormatSensitivity formatSensitivity) +{ + local int startCandidate; + local int index, otherIndex; + local Character char1, char2; + if (otherText == none) return -1; + if (fromIndex > GetLength()) return -1; + if (GetLength() - fromIndex < otherText.GetLength()) return -1; + if (otherText.IsEmpty()) return fromIndex; + + otherIndex = otherText.GetLength() - 1; + startCandidate = GetLength() - fromIndex - 1; + for (index = startCandidate; index >= 0; index -= 1) + { + char1 = GetCharacter(index); + char2 = otherText.GetCharacter(otherIndex); + if ( NormalizeCodePoint(char1.codePoint, caseSensitivity) + != NormalizeCodePoint(char2.codePoint, caseSensitivity)) + { + startCandidate = index - 1; + otherIndex = otherText.GetLength() - 1; + continue; + } + if ( formatSensitivity == SFORM_SENSITIVE + && !_.text.IsFormattingEqual(char1.formatting, char2.formatting)) + { + startCandidate = index - 1; + otherIndex = otherText.GetLength() - 1; + continue; + } + otherIndex -= 1; + if (otherIndex < 0) { + return startCandidate - (otherText.GetLength() - 1); + } + } + return -1; +} + defaultproperties { STRING_SEPARATOR_FORMAT = " "