diff --git a/sources/Text/MutableText.uc b/sources/Text/MutableText.uc index 92b4bda..6f7e44d 100644 --- a/sources/Text/MutableText.uc +++ b/sources/Text/MutableText.uc @@ -363,6 +363,62 @@ protected function int CalculateHashCode() return super(AcediaObject).GetHashCode(); } +/** + * Replaces every occurrence of the string `before` with the string `after`. + * + * @param before `Text` contents to match and then replace. + * @param after `Text` contents to replace `before` with. + * @param caseSensitivity Defines whether `before` should be matched + * in a case-sensitive manner. By default it will be. + * @param formatSensitivity Defines whether `before` should be matched + * in a way sensitive for color information. By default it is will not. + * @return Returns caller `Text`, to allow for method chaining. + */ +public final function MutableText Replace( + Text before, + Text after, + optional CaseSensitivity caseSensitivity, + optional FormatSensitivity formatSensitivity) +{ + local int index; + local bool needToInsertReplacer; + local int nextReplacementIndex; + local Text selfCopy; + if (before == none) return self; + if (before.IsEmpty()) return self; + + selfCopy = Copy(); + Clear(); + while (index < selfCopy.GetLength()) + { + nextReplacementIndex = selfCopy.IndexOf(before, index, + caseSensitivity, + formatSensitivity); + if (nextReplacementIndex < 0) + { + needToInsertReplacer = false; + nextReplacementIndex = selfCopy.GetLength(); + } + else { + needToInsertReplacer = true; + } + // Copy characters between replacements one by one + while (index < nextReplacementIndex) + { + AppendCharacter(selfCopy.GetCharacter(index)); + index += 1; + } + if (needToInsertReplacer) + { + Append(after); + // `before.GetLength() > 0` because of entry conditions + index += before.GetLength(); + } + } + selfCopy.FreeSelf(); + return self; +} + defaultproperties { } \ No newline at end of file diff --git a/sources/Text/Tests/TEST_Text.uc b/sources/Text/Tests/TEST_Text.uc index c466e3a..d86d40a 100644 --- a/sources/Text/Tests/TEST_Text.uc +++ b/sources/Text/Tests/TEST_Text.uc @@ -39,6 +39,7 @@ protected static function TESTS() Test_SeparateByCharacter(); Test_StartsEndsWith(); Test_IndexOf(); + Test_Replace(); } protected static function Test_TextCreation() @@ -878,6 +879,171 @@ protected static function SubTest_LastIndexOfFormatting() == 5); } +protected static function Test_Replace() +{ + Context("Testing `Replace()` method with non-formatted `MutableText`s."); + SubTest_ReplaceEdgeCases(SFORM_SENSITIVE); + SubTest_ReplaceEdgeCases(SFORM_INSENSITIVE); + SubTest_ReplaceMainCases(SFORM_SENSITIVE); + SubTest_ReplaceMainCases(SFORM_INSENSITIVE); + Context("Testing `Replace()` method with formatted `MutableText`s."); + SubTest_ReplaceEmptyFormatting(); + SubTest_ReplaceFullFormatting(); + SubTest_ReplacePartFormatting(); +} + +protected static function SubTest_ReplaceEdgeCases(Text.FormatSensitivity flag) +{ + local MutableText builder; + builder = __().text.Empty(); + Issue("`Replace()` works incorrectly when replacing empty `Text`s."); + TEST_ExpectTrue(builder.Replace(P(""), P(""),, flag).ToPlainString() == ""); + TEST_ExpectTrue( + builder.Replace(P(""), P("huh"),, flag).ToPlainString() == ""); + builder.AppendPlainString("word"); + TEST_ExpectTrue( + builder.Replace(P(""), P(""),, flag).ToPlainString() == "word"); + TEST_ExpectTrue( + builder.Replace(P(""), P("huh"),, flag).ToPlainString() == "word"); + + Issue("`Replace()` works incorrectly when replacing something inside" + @ "an empty `Text`s."); + builder.Clear(); + TEST_ExpectTrue( + builder.Replace(P("huh"), P(""),, flag).ToPlainString() == ""); + + Issue("`Replace()` cannot replace whole `Text`s."); + TEST_ExpectTrue(builder.Clear() + .AppendFormattedString("Just {#54af4c something}") + .Replace(F("Just {#54af4c something}"), P("Nothing really"),, flag) + .ToPlainString() + == "Nothing really"); + TEST_ExpectTrue(builder.Clear().AppendPlainString("CraZy") + .Replace(P("CRaZy"), P("calm"), SCASE_INSENSITIVE, flag) + .ToPlainString() + == "calm"); +} + +protected static function SubTest_ReplaceMainCases(Text.FormatSensitivity flag) +{ + local MutableText builder; + builder = __().text.FromStringM("Mate eight said"); + Issue("`Replace()` works incorrectly changes `Text` when replacing" + @ "non-existent sub-`Text`."); + TEST_ExpectTrue(builder.Replace(P("word"), P("another"),, flag) + .ToPlainString() + == "Mate eight said"); + TEST_ExpectTrue(builder + .Replace(P("word"), P("another"), SCASE_INSENSITIVE, flag) + .ToPlainString() + == "Mate eight said"); + + Issue("`Replace()` replaces sub-`Text` incorrectly."); + builder.Clear().AppendPlainString("Do it bay bee"); + TEST_ExpectTrue(builder.Replace(P("it"), P("this"),, flag).ToPlainString() + == "Do this bay bee"); + builder.Clear().AppendPlainString("dO It bAy BEe"); + TEST_ExpectTrue( + builder.Replace(P("it"), P("tHis"), SCASE_INSENSITIVE, flag) + .ToPlainString() + == "dO tHis bAy BEe"); + + Issue("`Replace()` replaces sub-`Text` incorrectly."); + builder.Clear().AppendPlainString("he and she and it"); + TEST_ExpectTrue(builder.Replace(P("and"), P("OR"),, flag) + .ToPlainString() + == "he OR she OR it"); + builder.Clear().AppendFormattedString("{#54af4c HE} aNd sHe aND iT"); + TEST_ExpectTrue(builder.Replace(P("AND"), P("Or"), SCASE_INSENSITIVE, flag) + .ToPlainString() + == "HE Or sHe Or iT"); +} + +protected static function SubTest_ReplaceEmptyFormatting() +{ + local MutableText builder; + builder = __().text.Empty(); + Issue("`Replace()` works incorrectly when replacing empty `MutableText`s."); + TEST_ExpectTrue(builder + .Replace(F("{rgb(4,5,6) }"), F("{rgb(76,52,161) }"),, SFORM_SENSITIVE) + .IsEmpty()); + TEST_ExpectTrue( + builder.Replace(F("{rgb(76,52,161) }"), F("huh"),, SFORM_SENSITIVE) + .IsEmpty()); + builder.AppendFormattedString("{rgb(76,52,161) wo}rd"); + TEST_ExpectTrue(builder + .Replace(F("{rgb(4,5,6) }"), F("{rgb(76,52,161) }"),, SFORM_SENSITIVE) + .ToFormattedString() == "{rgb(76,52,161) wo}rd"); + TEST_ExpectTrue(builder + .Replace(F("{rgb(76,52,161) }"), F("huh"),, SFORM_SENSITIVE) + .ToFormattedString() == "{rgb(76,52,161) wo}rd"); +} + +protected static function SubTest_ReplaceFullFormatting() +{ + local Text other; + local MutableText builder; + builder = __().text.Empty(); + Issue("`Replace()` cannot replace whole `MutableText`s."); + builder.AppendFormattedString("{rgb(76,52,161) One}, {rgb(4,5,6) two}!"); + other = F("{rgb(76,52,161) One}, {rgb(4,5,6) two}!"); + TEST_ExpectTrue(builder + .Replace(other, F("Nothing {rgb(25,0,0) really}"),, SFORM_SENSITIVE) + .ToFormattedString() + == "Nothing {rgb(25,0,0) really}"); + builder.Clear().AppendFormattedString("{rgb(76,52,161) CraZy}"); + other = __().text.FromFormattedString("{rgb(76,52,161) CraZy}"); + TEST_ExpectTrue(builder.Replace(other, F("c{rgb(25,0,0) a}lm"), + SCASE_INSENSITIVE, SFORM_SENSITIVE) + .ToFormattedString() == "c{rgb(25,0,0) a}lm"); + + Issue("`Replace()` incorrectly replaces whole `MutableText`s by matching" + @ "with a `Text` with different formatting."); + builder.Clear(); + builder.AppendFormattedString("{rgb(76,52,161) One}, {rgb(4,5,6) two}!"); + other = F("{rgb(76,52,161) One}, {rgb(5,5,6) two}!"); + TEST_ExpectTrue(builder + .Replace(other, F("Nothing {rgb(25,0,0) really}"),, SFORM_SENSITIVE) + .ToFormattedString() + == "{rgb(76,52,161) One}, {rgb(4,5,6) two}!"); +} + +protected static function SubTest_ReplacePartFormatting() +{ + local string normalCase, randomCase, complexCase; + local Text other; + local MutableText builder; + builder = __().text.Empty(); + normalCase = "He {rgb(76,52,161) and} she {rgb(204,5,6) and} it!"; + randomCase = "hE {rgb(76,52,161) aNd} SHE {rgb(76,52,161) ANd} IT!"; + complexCase = + "{rgb(76,52,161) Aba}B{rgb(76,52,161) bb{rgb(76,52,160) aB}b}a"; + Issue("`Replace()` incorrectly replaces parts of `MutableText`s."); + builder.Clear(); + builder.AppendFormattedString(normalCase); + other = F("{rgb(204,5,6) and}"); + TEST_ExpectTrue(builder + .Replace(other, F("{rgb(25,0,0) ???}"),, SFORM_SENSITIVE) + .ToFormattedString() + == "He {rgb(76,52,161) and} she {rgb(25,0,0) ???} it!"); + builder.Clear().AppendFormattedString(randomCase); + other = F("{rgb(76,52,161) and}"); + TEST_ExpectTrue(builder.Replace(other, F("c{rgb(25,0,0) o}r"), + SCASE_INSENSITIVE, SFORM_SENSITIVE) + .ToFormattedString() + == "hE c{rgb(25,0,0) o}r SHE c{rgb(25,0,0) o}r IT!"); + + builder.Clear(); + builder.AppendFormattedString(complexCase); + other = F("{rgb(76,52,161) B}"); + TEST_ExpectTrue(builder + .Replace( other, F("{rgb(4,4,4) cc}"), + SCASE_INSENSITIVE, SFORM_SENSITIVE) + .ToFormattedString() + == ("{rgb(76,52,161) A}{rgb(4,4,4) cc}{rgb(76,52,161) a}B" + $ "{rgb(4,4,4) cccc}{rgb(76,52,160) aB}{rgb(4,4,4) cc}a")); +} + defaultproperties { caseName = "Text/MutableText"