From ebc3c078298713b2a3bce14c9e5f25f0075f1adf Mon Sep 17 00:00:00 2001 From: Claudio Maggioni Date: Tue, 25 Apr 2023 21:28:29 +0200 Subject: [PATCH] Copied commons-text sources --- docker-start.sh | 11 + sources/.asf.yaml | 29 + sources/.github/GH-ROBOTS.txt | 19 + sources/.github/dependabot.yml | 27 + sources/.github/workflows/codeql-analysis.yml | 85 + sources/.github/workflows/coverage.yml | 52 + sources/.github/workflows/maven.yml | 47 + .../.github/workflows/scorecards-analysis.yml | 69 + sources/.gitignore | 20 + sources/CODE_OF_CONDUCT.md | 17 + sources/CONTRIBUTING.md | 115 + sources/LICENSE.txt | 202 ++ sources/NOTICE.txt | 5 + sources/README.md | 106 + sources/RELEASE-NOTES.txt | 618 ++++ sources/SECURITY.md | 17 + sources/pom.xml | 522 +++ sources/src/assembly/bin.xml | 44 + sources/src/assembly/src.xml | 44 + sources/src/changes/changes.xml | 344 ++ sources/src/changes/release-notes.vm | 139 + sources/src/conf/checkstyle-header.txt | 16 + sources/src/conf/checkstyle-suppressions.xml | 48 + sources/src/conf/checkstyle.xml | 186 + sources/src/conf/spotbugs-exclude-filter.xml | 60 + .../commons/text/AlphabetConverter.java | 512 +++ .../java/org/apache/commons/text/Builder.java | 86 + .../org/apache/commons/text/CaseUtils.java | 130 + .../commons/text/CharacterPredicate.java | 37 + .../commons/text/CharacterPredicates.java | 111 + .../apache/commons/text/CompositeFormat.java | 117 + .../commons/text/ExtendedMessageFormat.java | 573 +++ .../apache/commons/text/FormatFactory.java | 41 + .../apache/commons/text/FormattableUtils.java | 159 + .../commons/text/RandomStringGenerator.java | 459 +++ .../org/apache/commons/text/StrBuilder.java | 3075 ++++++++++++++++ .../org/apache/commons/text/StrLookup.java | 209 ++ .../org/apache/commons/text/StrMatcher.java | 445 +++ .../apache/commons/text/StrSubstitutor.java | 1279 +++++++ .../org/apache/commons/text/StrTokenizer.java | 1122 ++++++ .../commons/text/StringEscapeUtils.java | 865 +++++ .../commons/text/StringSubstitutor.java | 1548 ++++++++ .../apache/commons/text/StringTokenizer.java | 1139 ++++++ .../commons/text/TextRandomProvider.java | 49 + .../commons/text/TextStringBuilder.java | 3141 +++++++++++++++++ .../org/apache/commons/text/WordUtils.java | 900 +++++ .../commons/text/diff/CommandVisitor.java | 147 + .../commons/text/diff/DeleteCommand.java | 56 + .../apache/commons/text/diff/EditCommand.java | 89 + .../apache/commons/text/diff/EditScript.java | 133 + .../commons/text/diff/InsertCommand.java | 58 + .../apache/commons/text/diff/KeepCommand.java | 58 + .../commons/text/diff/ReplacementsFinder.java | 126 + .../text/diff/ReplacementsHandler.java | 54 + .../commons/text/diff/StringsComparator.java | 328 ++ .../commons/text/diff/package-info.java | 25 + .../text/io/StringSubstitutorReader.java | 316 ++ .../apache/commons/text/io/package-info.java | 31 + .../text/lookup/AbstractStringLookup.java | 92 + .../text/lookup/BiFunctionStringLookup.java | 105 + .../commons/text/lookup/BiStringLookup.java | 75 + .../text/lookup/ConstantStringLookup.java | 152 + .../commons/text/lookup/DateStringLookup.java | 94 + .../text/lookup/DefaultStringLookup.java | 190 + .../commons/text/lookup/DnsStringLookup.java | 105 + .../commons/text/lookup/FileStringLookup.java | 92 + .../text/lookup/FunctionStringLookup.java | 99 + .../lookup/IllegalArgumentExceptions.java | 58 + .../commons/text/lookup/InetAddressKeys.java | 47 + .../text/lookup/InterpolatorStringLookup.java | 146 + .../text/lookup/JavaPlatformStringLookup.java | 216 ++ .../text/lookup/LocalHostStringLookup.java | 75 + .../text/lookup/PropertiesStringLookup.java | 103 + .../lookup/ResourceBundleStringLookup.java | 121 + .../text/lookup/ScriptStringLookup.java | 93 + .../commons/text/lookup/StringLookup.java | 62 + .../text/lookup/StringLookupFactory.java | 1318 +++++++ .../text/lookup/UrlDecoderStringLookup.java | 63 + .../text/lookup/UrlEncoderStringLookup.java | 62 + .../commons/text/lookup/UrlStringLookup.java | 93 + .../commons/text/lookup/XmlStringLookup.java | 84 + .../commons/text/lookup/package-info.java | 40 + .../text/matcher/AbstractStringMatcher.java | 451 +++ .../commons/text/matcher/StringMatcher.java | 159 + .../text/matcher/StringMatcherFactory.java | 258 ++ .../commons/text/matcher/package-info.java | 26 + .../commons/text/numbers/DoubleFormat.java | 775 ++++ .../commons/text/numbers/ParsedDecimal.java | 763 ++++ .../commons/text/numbers/package-info.java | 24 + .../org/apache/commons/text/package-info.java | 22 + .../text/similarity/CosineDistance.java | 49 + .../text/similarity/CosineSimilarity.java | 107 + .../commons/text/similarity/Counter.java | 57 + .../commons/text/similarity/EditDistance.java | 59 + .../text/similarity/EditDistanceFrom.java | 108 + .../commons/text/similarity/FuzzyScore.java | 143 + .../text/similarity/HammingDistance.java | 78 + .../text/similarity/IntersectionResult.java | 121 + .../similarity/IntersectionSimilarity.java | 237 ++ .../text/similarity/JaccardDistance.java | 50 + .../text/similarity/JaccardSimilarity.java | 91 + .../text/similarity/JaroWinklerDistance.java | 88 + .../similarity/JaroWinklerSimilarity.java | 167 + .../LevenshteinDetailedDistance.java | 522 +++ .../text/similarity/LevenshteinDistance.java | 399 +++ .../text/similarity/LevenshteinResults.java | 129 + .../similarity/LongestCommonSubsequence.java | 314 ++ .../LongestCommonSubsequenceDistance.java | 59 + .../text/similarity/RegexTokenizer.java | 63 + .../text/similarity/SimilarityScore.java | 63 + .../text/similarity/SimilarityScoreFrom.java | 109 + .../commons/text/similarity/Tokenizer.java | 35 + .../commons/text/similarity/package-info.java | 45 + .../text/translate/AggregateTranslator.java | 66 + .../translate/CharSequenceTranslator.java | 139 + .../text/translate/CodePointTranslator.java | 51 + .../text/translate/CsvTranslators.java | 93 + .../commons/text/translate/EntityArrays.java | 450 +++ .../text/translate/JavaUnicodeEscaper.java | 103 + .../text/translate/LookupTranslator.java | 104 + .../text/translate/NumericEntityEscaper.java | 114 + .../translate/NumericEntityUnescaper.java | 157 + .../text/translate/OctalUnescaper.java | 81 + .../text/translate/SinglePassTranslator.java | 62 + .../text/translate/UnicodeEscaper.java | 138 + .../text/translate/UnicodeUnescaper.java | 65 + .../UnicodeUnpairedSurrogateRemover.java | 38 + .../commons/text/translate/package-info.java | 24 + sources/src/media/logo.xcf | Bin 0 -> 17288 bytes sources/src/site/resources/download_text.cgi | 4 + sources/src/site/resources/images/logo.png | Bin 0 -> 10143 bytes sources/src/site/resources/profile.jacoco | 0 sources/src/site/site.xml | 46 + sources/src/site/xdoc/developerguide.xml | 150 + sources/src/site/xdoc/download_text.xml | 156 + sources/src/site/xdoc/index.xml | 84 + sources/src/site/xdoc/issue-tracking.xml | 102 + sources/src/site/xdoc/mail-lists.xml | 215 ++ sources/src/site/xdoc/proposal.xml | 89 + sources/src/site/xdoc/security.xml | 118 + sources/src/site/xdoc/userguide.xml | 290 ++ .../commons/text/AlphabetConverterTest.java | 300 ++ .../apache/commons/text/CaseUtilsTest.java | 76 + .../commons/text/CharacterPredicatesTest.java | 113 + .../commons/text/CompositeFormatTest.java | 87 + .../text/ExtendedMessageFormatTest.java | 564 +++ .../commons/text/FormattableUtilsTest.java | 183 + .../text/RandomStringGeneratorTest.java | 279 ++ .../text/StrBuilderAppendInsertTest.java | 1429 ++++++++ .../apache/commons/text/StrBuilderTest.java | 2024 +++++++++++ .../apache/commons/text/StrLookupTest.java | 133 + .../apache/commons/text/StrMatcherTest.java | 201 ++ .../commons/text/StrSubstitutorTest.java | 793 +++++ .../apache/commons/text/StrTokenizerTest.java | 915 +++++ .../commons/text/StringEscapeUtilsTest.java | 644 ++++ .../commons/text/StringSubstitutorTest.java | 1112 ++++++ ...tutorWithInterpolatorStringLookupTest.java | 263 ++ .../commons/text/StringTokenizerTest.java | 935 +++++ .../TextStringBuilderAppendInsertTest.java | 1181 +++++++ .../commons/text/TextStringBuilderTest.java | 2287 ++++++++++++ .../apache/commons/text/WordUtilsTest.java | 550 +++ .../text/diff/ReplacementsFinderTest.java | 95 + .../text/diff/StringsComparatorTest.java | 139 + .../io/StringSubstitutorFilterReaderTest.java | 257 ++ .../text/jmh/DoubleFormatPerformance.java | 273 ++ .../LongestCommonSubsequencePerformance.java | 167 + .../text/lookup/AbstractStringLookupTest.java | 57 + .../lookup/Base64DecoderStringLookupTest.java | 44 + .../lookup/Base64EncoderStringLookupTest.java | 44 + .../lookup/BiFunctionStringLookupTest.java | 110 + .../text/lookup/BiStringLookupTest.java | 33 + .../lookup/ConstantStringLookupBasicTest.java | 89 + .../text/lookup/ConstantStringLookupTest.java | 141 + .../text/lookup/DateStringLookupTest.java | 68 + .../text/lookup/DefaultStringLookupTest.java | 71 + .../text/lookup/DnsStringLookupTest.java | 83 + .../EnvironmentVariableStringLookupTest.java | 51 + .../text/lookup/FileStringLookupTest.java | 67 + .../text/lookup/FunctionStringLookupTest.java | 63 + .../lookup/InterpolatorStringLookupTest.java | 126 + .../lookup/JavaPlatformStringLookupTest.java | 59 + .../lookup/LocalHostStringLookupTest.java | 65 + .../text/lookup/NullStringLookupTest.java | 40 + .../lookup/PropertiesStringLookupTest.java | 113 + .../ResourceBundleStringLookupTest.java | 95 + .../lookup/ScriptEngineFactoryHelper.java | 41 + .../text/lookup/ScriptStringLookupTest.java | 81 + .../text/lookup/StringLookupFactoryTest.java | 231 ++ .../SystemPropertyStringLookupTest.java | 45 + .../lookup/UrlDecoderStringLookupTest.java | 71 + .../lookup/UrlEncoderStringLookupTest.java | 61 + .../text/lookup/UrlStringLookupTest.java | 83 + .../text/lookup/XmlStringLookupTest.java | 59 + .../matcher/StringMatcherFactoryTest.java | 187 + .../matcher/StringMatcherOnCharArrayTest.java | 362 ++ ...StringMatcherOnCharSequenceStringTest.java | 353 ++ .../matcher/StringSubstitutorGetSetTest.java | 99 + .../text/numbers/DoubleFormatTest.java | 595 ++++ .../text/numbers/ParsedDecimalTest.java | 732 ++++ .../text/similarity/CosineDistanceTest.java | 69 + .../text/similarity/CosineSimilarityTest.java | 62 + .../text/similarity/FuzzyScoreTest.java | 73 + .../text/similarity/HammingDistanceTest.java | 59 + .../similarity/IntersectionResultTest.java | 143 + .../IntersectionSimilarityTest.java | 330 ++ .../text/similarity/JaccardDistanceTest.java | 71 + .../similarity/JaccardSimilarityTest.java | 71 + .../similarity/JaroWinklerDistanceTest.java | 72 + .../similarity/JaroWinklerSimilarityTest.java | 98 + .../LevenshteinDetailedDistanceTest.java | 462 +++ .../similarity/LevenshteinDistanceTest.java | 145 + .../similarity/LevenshteinResultsTest.java | 71 + .../LongestCommonSubsequenceDistanceTest.java | 69 + .../LongestCommonSubsequenceTest.java | 140 + .../ParameterizedEditDistanceFromTest.java | 64 + .../ParameterizedLevenshteinDistanceTest.java | 104 + .../ParameterizedSimilarityScoreFromTest.java | 56 + .../similarity/SimilarityScoreFromTest.java | 41 + .../text/similarity/StringMetricFromTest.java | 66 + .../translate/AggregateTranslatorTest.java | 66 + .../translate/CodePointTranslatorTest.java | 39 + .../text/translate/CsvTranslatorsTest.java | 130 + .../text/translate/EntityArraysTest.java | 132 + .../translate/JavaUnicodeEscaperTest.java | 67 + .../text/translate/LookupTranslatorTest.java | 77 + .../translate/NumericEntityEscaperTest.java | 68 + .../translate/NumericEntityUnescaperTest.java | 98 + .../text/translate/OctalUnescaperTest.java | 83 + .../translate/SinglePassTranslatorTest.java | 66 + .../text/translate/UnicodeEscaperTest.java | 58 + .../text/translate/UnicodeUnescaperTest.java | 55 + .../UnicodeUnpairedSurrogateRemoverTest.java | 47 + .../apache/commons/text/document.properties | 16 + .../org/apache/commons/text/document.xml | 24 + .../testResourceBundleLookup.properties | 19 + .../commons/text/lcs-perf-analysis-inputs.csv | 5 + .../text/stringEscapeUtilsTestData.txt | 1 + 237 files changed, 55091 insertions(+) create mode 100755 docker-start.sh create mode 100644 sources/.asf.yaml create mode 100644 sources/.github/GH-ROBOTS.txt create mode 100644 sources/.github/dependabot.yml create mode 100644 sources/.github/workflows/codeql-analysis.yml create mode 100644 sources/.github/workflows/coverage.yml create mode 100644 sources/.github/workflows/maven.yml create mode 100644 sources/.github/workflows/scorecards-analysis.yml create mode 100644 sources/.gitignore create mode 100644 sources/CODE_OF_CONDUCT.md create mode 100644 sources/CONTRIBUTING.md create mode 100644 sources/LICENSE.txt create mode 100644 sources/NOTICE.txt create mode 100644 sources/README.md create mode 100644 sources/RELEASE-NOTES.txt create mode 100644 sources/SECURITY.md create mode 100644 sources/pom.xml create mode 100644 sources/src/assembly/bin.xml create mode 100644 sources/src/assembly/src.xml create mode 100644 sources/src/changes/changes.xml create mode 100644 sources/src/changes/release-notes.vm create mode 100644 sources/src/conf/checkstyle-header.txt create mode 100644 sources/src/conf/checkstyle-suppressions.xml create mode 100644 sources/src/conf/checkstyle.xml create mode 100644 sources/src/conf/spotbugs-exclude-filter.xml create mode 100644 sources/src/main/java/org/apache/commons/text/AlphabetConverter.java create mode 100644 sources/src/main/java/org/apache/commons/text/Builder.java create mode 100644 sources/src/main/java/org/apache/commons/text/CaseUtils.java create mode 100644 sources/src/main/java/org/apache/commons/text/CharacterPredicate.java create mode 100644 sources/src/main/java/org/apache/commons/text/CharacterPredicates.java create mode 100644 sources/src/main/java/org/apache/commons/text/CompositeFormat.java create mode 100644 sources/src/main/java/org/apache/commons/text/ExtendedMessageFormat.java create mode 100644 sources/src/main/java/org/apache/commons/text/FormatFactory.java create mode 100644 sources/src/main/java/org/apache/commons/text/FormattableUtils.java create mode 100644 sources/src/main/java/org/apache/commons/text/RandomStringGenerator.java create mode 100644 sources/src/main/java/org/apache/commons/text/StrBuilder.java create mode 100644 sources/src/main/java/org/apache/commons/text/StrLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/StrMatcher.java create mode 100644 sources/src/main/java/org/apache/commons/text/StrSubstitutor.java create mode 100644 sources/src/main/java/org/apache/commons/text/StrTokenizer.java create mode 100644 sources/src/main/java/org/apache/commons/text/StringEscapeUtils.java create mode 100644 sources/src/main/java/org/apache/commons/text/StringSubstitutor.java create mode 100644 sources/src/main/java/org/apache/commons/text/StringTokenizer.java create mode 100644 sources/src/main/java/org/apache/commons/text/TextRandomProvider.java create mode 100644 sources/src/main/java/org/apache/commons/text/TextStringBuilder.java create mode 100644 sources/src/main/java/org/apache/commons/text/WordUtils.java create mode 100644 sources/src/main/java/org/apache/commons/text/diff/CommandVisitor.java create mode 100644 sources/src/main/java/org/apache/commons/text/diff/DeleteCommand.java create mode 100644 sources/src/main/java/org/apache/commons/text/diff/EditCommand.java create mode 100644 sources/src/main/java/org/apache/commons/text/diff/EditScript.java create mode 100644 sources/src/main/java/org/apache/commons/text/diff/InsertCommand.java create mode 100644 sources/src/main/java/org/apache/commons/text/diff/KeepCommand.java create mode 100644 sources/src/main/java/org/apache/commons/text/diff/ReplacementsFinder.java create mode 100644 sources/src/main/java/org/apache/commons/text/diff/ReplacementsHandler.java create mode 100644 sources/src/main/java/org/apache/commons/text/diff/StringsComparator.java create mode 100644 sources/src/main/java/org/apache/commons/text/diff/package-info.java create mode 100644 sources/src/main/java/org/apache/commons/text/io/StringSubstitutorReader.java create mode 100644 sources/src/main/java/org/apache/commons/text/io/package-info.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/AbstractStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/BiFunctionStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/BiStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/ConstantStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/DateStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/DefaultStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/DnsStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/FileStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/FunctionStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/IllegalArgumentExceptions.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/InetAddressKeys.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/InterpolatorStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/JavaPlatformStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/LocalHostStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/PropertiesStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/ResourceBundleStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/ScriptStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/StringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/StringLookupFactory.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/UrlDecoderStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/UrlEncoderStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/UrlStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/XmlStringLookup.java create mode 100644 sources/src/main/java/org/apache/commons/text/lookup/package-info.java create mode 100644 sources/src/main/java/org/apache/commons/text/matcher/AbstractStringMatcher.java create mode 100644 sources/src/main/java/org/apache/commons/text/matcher/StringMatcher.java create mode 100644 sources/src/main/java/org/apache/commons/text/matcher/StringMatcherFactory.java create mode 100644 sources/src/main/java/org/apache/commons/text/matcher/package-info.java create mode 100644 sources/src/main/java/org/apache/commons/text/numbers/DoubleFormat.java create mode 100644 sources/src/main/java/org/apache/commons/text/numbers/ParsedDecimal.java create mode 100644 sources/src/main/java/org/apache/commons/text/numbers/package-info.java create mode 100644 sources/src/main/java/org/apache/commons/text/package-info.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/CosineDistance.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/CosineSimilarity.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/Counter.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/EditDistance.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/EditDistanceFrom.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/FuzzyScore.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/HammingDistance.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/IntersectionResult.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/IntersectionSimilarity.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/JaccardDistance.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/JaccardSimilarity.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/JaroWinklerDistance.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/JaroWinklerSimilarity.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/LevenshteinDetailedDistance.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/LevenshteinDistance.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/LevenshteinResults.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/LongestCommonSubsequence.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/LongestCommonSubsequenceDistance.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/RegexTokenizer.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/SimilarityScore.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/SimilarityScoreFrom.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/Tokenizer.java create mode 100644 sources/src/main/java/org/apache/commons/text/similarity/package-info.java create mode 100644 sources/src/main/java/org/apache/commons/text/translate/AggregateTranslator.java create mode 100644 sources/src/main/java/org/apache/commons/text/translate/CharSequenceTranslator.java create mode 100644 sources/src/main/java/org/apache/commons/text/translate/CodePointTranslator.java create mode 100644 sources/src/main/java/org/apache/commons/text/translate/CsvTranslators.java create mode 100644 sources/src/main/java/org/apache/commons/text/translate/EntityArrays.java create mode 100644 sources/src/main/java/org/apache/commons/text/translate/JavaUnicodeEscaper.java create mode 100644 sources/src/main/java/org/apache/commons/text/translate/LookupTranslator.java create mode 100644 sources/src/main/java/org/apache/commons/text/translate/NumericEntityEscaper.java create mode 100644 sources/src/main/java/org/apache/commons/text/translate/NumericEntityUnescaper.java create mode 100644 sources/src/main/java/org/apache/commons/text/translate/OctalUnescaper.java create mode 100644 sources/src/main/java/org/apache/commons/text/translate/SinglePassTranslator.java create mode 100644 sources/src/main/java/org/apache/commons/text/translate/UnicodeEscaper.java create mode 100644 sources/src/main/java/org/apache/commons/text/translate/UnicodeUnescaper.java create mode 100644 sources/src/main/java/org/apache/commons/text/translate/UnicodeUnpairedSurrogateRemover.java create mode 100644 sources/src/main/java/org/apache/commons/text/translate/package-info.java create mode 100644 sources/src/media/logo.xcf create mode 100755 sources/src/site/resources/download_text.cgi create mode 100644 sources/src/site/resources/images/logo.png create mode 100644 sources/src/site/resources/profile.jacoco create mode 100644 sources/src/site/site.xml create mode 100644 sources/src/site/xdoc/developerguide.xml create mode 100644 sources/src/site/xdoc/download_text.xml create mode 100644 sources/src/site/xdoc/index.xml create mode 100644 sources/src/site/xdoc/issue-tracking.xml create mode 100644 sources/src/site/xdoc/mail-lists.xml create mode 100644 sources/src/site/xdoc/proposal.xml create mode 100644 sources/src/site/xdoc/security.xml create mode 100644 sources/src/site/xdoc/userguide.xml create mode 100644 sources/src/test/java/org/apache/commons/text/AlphabetConverterTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/CaseUtilsTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/CharacterPredicatesTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/CompositeFormatTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/ExtendedMessageFormatTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/FormattableUtilsTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/RandomStringGeneratorTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/StrBuilderAppendInsertTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/StrBuilderTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/StrLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/StrMatcherTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/StrSubstitutorTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/StrTokenizerTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/StringEscapeUtilsTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/StringSubstitutorTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/StringSubstitutorWithInterpolatorStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/StringTokenizerTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/TextStringBuilderAppendInsertTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/TextStringBuilderTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/WordUtilsTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/diff/ReplacementsFinderTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/diff/StringsComparatorTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/io/StringSubstitutorFilterReaderTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/jmh/DoubleFormatPerformance.java create mode 100644 sources/src/test/java/org/apache/commons/text/jmh/LongestCommonSubsequencePerformance.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/AbstractStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/Base64DecoderStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/Base64EncoderStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/BiFunctionStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/BiStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/ConstantStringLookupBasicTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/ConstantStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/DateStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/DefaultStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/DnsStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/EnvironmentVariableStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/FileStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/FunctionStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/InterpolatorStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/JavaPlatformStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/LocalHostStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/NullStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/PropertiesStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/ResourceBundleStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/ScriptEngineFactoryHelper.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/ScriptStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/StringLookupFactoryTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/SystemPropertyStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/UrlDecoderStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/UrlEncoderStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/UrlStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/lookup/XmlStringLookupTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/matcher/StringMatcherFactoryTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/matcher/StringMatcherOnCharArrayTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/matcher/StringMatcherOnCharSequenceStringTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/matcher/StringSubstitutorGetSetTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/numbers/DoubleFormatTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/numbers/ParsedDecimalTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/CosineDistanceTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/CosineSimilarityTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/FuzzyScoreTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/HammingDistanceTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/IntersectionResultTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/IntersectionSimilarityTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/JaccardDistanceTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/JaccardSimilarityTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/JaroWinklerDistanceTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/JaroWinklerSimilarityTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/LevenshteinDetailedDistanceTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/LevenshteinDistanceTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/LevenshteinResultsTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/LongestCommonSubsequenceDistanceTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/LongestCommonSubsequenceTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/ParameterizedEditDistanceFromTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/ParameterizedLevenshteinDistanceTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/ParameterizedSimilarityScoreFromTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/SimilarityScoreFromTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/similarity/StringMetricFromTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/translate/AggregateTranslatorTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/translate/CodePointTranslatorTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/translate/CsvTranslatorsTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/translate/EntityArraysTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/translate/JavaUnicodeEscaperTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/translate/LookupTranslatorTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/translate/NumericEntityEscaperTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/translate/NumericEntityUnescaperTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/translate/OctalUnescaperTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/translate/SinglePassTranslatorTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/translate/UnicodeEscaperTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/translate/UnicodeUnescaperTest.java create mode 100644 sources/src/test/java/org/apache/commons/text/translate/UnicodeUnpairedSurrogateRemoverTest.java create mode 100644 sources/src/test/resources/org/apache/commons/text/document.properties create mode 100644 sources/src/test/resources/org/apache/commons/text/document.xml create mode 100644 sources/src/test/resources/org/apache/commons/text/example/testResourceBundleLookup.properties create mode 100644 sources/src/test/resources/org/apache/commons/text/lcs-perf-analysis-inputs.csv create mode 100644 sources/src/test/resources/org/apache/commons/text/stringEscapeUtilsTestData.txt diff --git a/docker-start.sh b/docker-start.sh new file mode 100755 index 0000000..bb4fa55 --- /dev/null +++ b/docker-start.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +mkdir -pv "${SCRIPT_DIR}/.m2" + +docker run --rm -it \ + -v "${SCRIPT_DIR}:/tools/home" \ + -v "${SCRIPT_DIR}/.m2:/root/.m2" \ + bugcounting/satools:y23 + diff --git a/sources/.asf.yaml b/sources/.asf.yaml new file mode 100644 index 0000000..4cc5f8a --- /dev/null +++ b/sources/.asf.yaml @@ -0,0 +1,29 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +github: + description: "Apache Commons Text" + homepage: https://commons.apache.org/text/ + +notifications: + commits: commits@commons.apache.org + issues: issues@commons.apache.org + pullrequests: issues@commons.apache.org + jira_options: link label + jobs: notifications@commons.apache.org + issues_bot_dependabot: notifications@commons.apache.org + pullrequests_bot_dependabot: notifications@commons.apache.org + issues_bot_codecov-commenter: notifications@commons.apache.org + pullrequests_bot_codecov-commenter: notifications@commons.apache.org diff --git a/sources/.github/GH-ROBOTS.txt b/sources/.github/GH-ROBOTS.txt new file mode 100644 index 0000000..e3329e5 --- /dev/null +++ b/sources/.github/GH-ROBOTS.txt @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Keeps on creating FUD PRs in test code +# Does not follow Apache disclosure policies +User-agent: JLLeitschuh/security-research +Disallow: * diff --git a/sources/.github/dependabot.yml b/sources/.github/dependabot.yml new file mode 100644 index 0000000..9ebcd0e --- /dev/null +++ b/sources/.github/dependabot.yml @@ -0,0 +1,27 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +version: 2 +updates: + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "weekly" + day: "friday" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "friday" diff --git a/sources/.github/workflows/codeql-analysis.yml b/sources/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..2e4cbac --- /dev/null +++ b/sources/.github/workflows/codeql-analysis.yml @@ -0,0 +1,85 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '33 9 * * 4' + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3.5.2 + with: + persist-credentials: false + - uses: actions/cache@v3.3.1 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/sources/.github/workflows/coverage.yml b/sources/.github/workflows/coverage.yml new file mode 100644 index 0000000..ce7402a --- /dev/null +++ b/sources/.github/workflows/coverage.yml @@ -0,0 +1,52 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Coverage + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + java: [ 8 ] + + steps: + - uses: actions/checkout@v3.5.2 + with: + persist-credentials: false + - uses: actions/cache@v3.3.1 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v3.11.0 + with: + distribution: 'temurin' + java-version: ${{ matrix.java }} + - name: Build with Maven + run: mvn -V test jacoco:report --file pom.xml --no-transfer-progress + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./target/site/jacoco/jacoco.xml diff --git a/sources/.github/workflows/maven.yml b/sources/.github/workflows/maven.yml new file mode 100644 index 0000000..d7ed7b9 --- /dev/null +++ b/sources/.github/workflows/maven.yml @@ -0,0 +1,47 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Java CI + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental }} + strategy: + matrix: + java: [ 8, 11, 17 ] + experimental: [false] +# include: +# - java: 18-ea +# experimental: true + + steps: + - uses: actions/checkout@v3.5.2 + with: + persist-credentials: false + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v3.11.0 + with: + distribution: 'temurin' + java-version: ${{ matrix.java }} + cache: 'maven' + - name: Build with Maven + run: mvn -Dpolyglot.engine.WarnInterpreterOnly=false diff --git a/sources/.github/workflows/scorecards-analysis.yml b/sources/.github/workflows/scorecards-analysis.yml new file mode 100644 index 0000000..672a3e4 --- /dev/null +++ b/sources/.github/workflows/scorecards-analysis.yml @@ -0,0 +1,69 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache license, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the license for the specific language governing permissions and +# limitations under the license. + +name: "Scorecards supply-chain security" + +on: + branch_protection_rule: + schedule: + - cron: "30 1 * * 6" # Weekly on Saturdays + push: + branches: [ "master" ] + +permissions: read-all + +jobs: + + analysis: + + name: "Scorecards analysis" + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to the code-scanning dashboard. + security-events: write + actions: read + id-token: write # This is required for requesting the JWT + contents: read # This is required for actions/checkout + + steps: + + - name: "Checkout code" + uses: actions/checkout@v3.5.2 # 3.1.0 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@80e868c13c90f172d68d1f4501dee99e2479f7af # 2.1.3 + with: + results_file: results.sarif + results_format: sarif + # A read-only PAT token, which is sufficient for the action to function. + # The relevant discussion: https://github.com/ossf/scorecard-action/issues/188 + repo_token: ${{ secrets.GITHUB_TOKEN }} + # Publish the results for public repositories to enable scorecard badges. + # For more details: https://github.com/ossf/scorecard-action#publishing-results + publish_results: true + + - name: "Upload artifact" + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # 3.1.2 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@b398f525a5587552e573b247ac661067fafa920b # 2.1.22 + with: + sarif_file: results.sarif diff --git a/sources/.gitignore b/sources/.gitignore new file mode 100644 index 0000000..92a1b2a --- /dev/null +++ b/sources/.gitignore @@ -0,0 +1,20 @@ +# Maven build files +target +*.log +maven-eclipse.xml +build.properties +site-content +*~ + +# IntelliJ IDEA files +.idea +.iws +*.iml +*.ipr + +# Eclipse files +.settings +.classpath +.project +.externalToolBuilders +/.checkstyle diff --git a/sources/CODE_OF_CONDUCT.md b/sources/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..3ed5015 --- /dev/null +++ b/sources/CODE_OF_CONDUCT.md @@ -0,0 +1,17 @@ + +The Apache code of conduct page is [https://www.apache.org/foundation/policies/conduct.html](https://www.apache.org/foundation/policies/conduct.html). diff --git a/sources/CONTRIBUTING.md b/sources/CONTRIBUTING.md new file mode 100644 index 0000000..7683e3f --- /dev/null +++ b/sources/CONTRIBUTING.md @@ -0,0 +1,115 @@ + + +Contributing to Apache Commons Text +====================== + +You have found a bug or you have an idea for a cool new feature? Contributing code is a great way to give something back to +the open source community. Before you dig right into the code there are a few guidelines that we need contributors to +follow so that we can have a chance of keeping on top of things. + +Getting Started +--------------- + ++ Make sure you have a [JIRA account](https://issues.apache.org/jira/). ++ Make sure you have a [GitHub account](https://github.com/signup/free). ++ If you're planning to implement a new feature it makes sense to discuss your changes on the [dev list](https://commons.apache.org/mail-lists.html) first. This way you can make sure you're not wasting your time on something that isn't considered to be in Apache Commons Text's scope. ++ Submit a [Jira Ticket][jira] for your issue, assuming one does not already exist. + + Clearly describe the issue including steps to reproduce when it is a bug. + + Make sure you fill in the earliest version that you know has the issue. ++ Find the corresponding [repository on GitHub](https://github.com/apache/?query=commons-), +[fork](https://help.github.com/articles/fork-a-repo/) and check out your forked repository. + +Making Changes +-------------- + ++ Create a _topic branch_ for your isolated work. + * Usually you should base your branch on the `master` branch. + * A good topic branch name can be the JIRA bug id plus a keyword, e.g. `TEXT-123-InputStream`. + * If you have submitted multiple JIRA issues, try to maintain separate branches and pull requests. ++ Make commits of logical units. + * Make sure your commit messages are meaningful and in the proper format. Your commit message should contain the key of the JIRA issue. + * e.g. `TEXT-123: Close input stream earlier` ++ Respect the original code style: + + Only use spaces for indentation. + + Create minimal diffs - disable _On Save_ actions like _Reformat Source Code_ or _Organize Imports_. If you feel the source code should be reformatted create a separate PR for this change first. + + Check for unnecessary whitespace with `git diff` -- check before committing. ++ Make sure you have added the necessary tests for your changes, typically in `src/test/java`. ++ Run all the tests with `mvn clean verify` to assure nothing else was accidentally broken. + +Making Trivial Changes +---------------------- + +The JIRA tickets are used to generate the changelog for the next release. + +For changes of a trivial nature to comments and documentation, it is not always necessary to create a new ticket in JIRA. +In this case, it is appropriate to start the first line of a commit with '(doc)' instead of a ticket number. + + +Submitting Changes +------------------ + ++ Sign and submit the Apache [Contributor License Agreement][cla] if you haven't already. + * Note that small patches & typical bug fixes do not require a CLA as + clause 5 of the [Apache License](https://www.apache.org/licenses/LICENSE-2.0.html#contributions) + covers them. ++ Push your changes to a topic branch in your fork of the repository. ++ Submit a _Pull Request_ to the corresponding repository in the `apache` organization. + * Verify _Files Changed_ shows only your intended changes and does not + include additional files like `target/*.class` ++ Update your JIRA ticket and include a link to the pull request in the ticket. + +If you prefer to not use GitHub, then you can instead use +`git format-patch` (or `svn diff`) and attach the patch file to the JIRA issue. + + +Additional Resources +-------------------- + ++ [Contributing patches](https://commons.apache.org/patches.html) ++ [Apache Commons Text JIRA project page][jira] ++ [Contributor License Agreement][cla] ++ [General GitHub documentation](https://help.github.com/) ++ [GitHub pull request documentation](https://help.github.com/articles/creating-a-pull-request/) ++ [Apache Commons Twitter Account](https://twitter.com/ApacheCommons) ++ `#apache-commons` IRC channel on `irc.freenode.net` + +[cla]:https://www.apache.org/licenses/#clas +[jira]:https://issues.apache.org/jira/browse/TEXT diff --git a/sources/LICENSE.txt b/sources/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/sources/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/sources/NOTICE.txt b/sources/NOTICE.txt new file mode 100644 index 0000000..a4c26c8 --- /dev/null +++ b/sources/NOTICE.txt @@ -0,0 +1,5 @@ +Apache Commons Text +Copyright 2014-2023 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (https://www.apache.org/). diff --git a/sources/README.md b/sources/README.md new file mode 100644 index 0000000..c4c9c77 --- /dev/null +++ b/sources/README.md @@ -0,0 +1,106 @@ + + +Apache Commons Text +=================== + +[![GitHub Actions Status](https://github.com/apache/commons-text/workflows/Java%20CI/badge.svg)](https://github.com/apache/commons-text/actions) +[![Coverage Status](https://codecov.io/gh/apache/commons-text/branch/master/graph/badge.svg)](https://app.codecov.io/gh/apache/commons-text) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.apache.commons/commons-text/badge.svg?gav=true)](https://maven-badges.herokuapp.com/maven-central/org.apache.commons/commons-text/?gav=true) +[![Javadocs](https://javadoc.io/badge/org.apache.commons/commons-text/1.10.0.svg)](https://javadoc.io/doc/org.apache.commons/commons-text/1.10.0) +[![CodeQL](https://github.com/apache/commons-text/workflows/CodeQL/badge.svg)](https://github.com/apache/commons-text/actions/workflows/codeql-analysis.yml?query=workflow%3ACodeQL) + +Apache Commons Text is a library focused on algorithms working on strings. + +Documentation +------------- + +More information can be found on the [Apache Commons Text homepage](https://commons.apache.org/proper/commons-text). +The [Javadoc](https://commons.apache.org/proper/commons-text/apidocs) can be browsed. +Questions related to the usage of Apache Commons Text should be posted to the [user mailing list][ml]. + +Where can I get the latest release? +----------------------------------- +You can download source and binaries from our [download page](https://commons.apache.org/proper/commons-text/download_text.cgi). + +Alternatively you can pull it from the central Maven repositories: + +```xml + + org.apache.commons + commons-text + 1.10.0 + +``` + +Contributing +------------ + +We accept Pull Requests via GitHub. The [developer mailing list][ml] is the main channel of communication for contributors. +There are some guidelines which will make applying PRs easier for us: ++ No tabs! Please use spaces for indentation. ++ Respect the code style. ++ Create minimal diffs - disable on save actions like reformat source code or organize imports. If you feel the source code should be reformatted create a separate PR for this change. ++ Provide JUnit tests for your changes and make sure your changes don't break any existing tests by running ```mvn```. + +If you plan to contribute on a regular basis, please consider filing a [contributor license agreement](https://www.apache.org/licenses/#clas). +You can learn more about contributing via GitHub in our [contribution guidelines](CONTRIBUTING.md). + +License +------- +This code is under the [Apache Licence v2](https://www.apache.org/licenses/LICENSE-2.0). + +See the `NOTICE.txt` file for required notices and attributions. + +Donations +--------- +You like Apache Commons Text? Then [donate back to the ASF](https://www.apache.org/foundation/contributing.html) to support the development. + +Additional Resources +-------------------- + ++ [Apache Commons Homepage](https://commons.apache.org/) ++ [Apache Issue Tracker (JIRA)](https://issues.apache.org/jira/browse/TEXT) ++ [Apache Commons Twitter Account](https://twitter.com/ApacheCommons) ++ `#apache-commons` IRC channel on `irc.freenode.org` + +[ml]:https://commons.apache.org/mail-lists.html diff --git a/sources/RELEASE-NOTES.txt b/sources/RELEASE-NOTES.txt new file mode 100644 index 0000000..e5a5f6b --- /dev/null +++ b/sources/RELEASE-NOTES.txt @@ -0,0 +1,618 @@ +Apache Commons Text +Version 1.10.0 +Release Notes + +INTRODUCTION: + +This document contains the release notes for the 1.10.0 version of Apache Commons Text. +Commons Text is a set of utility functions and reusable components for the purpose of processing +and manipulating text that should be of use in a Java environment. + +Apache Commons Text is a library focused on algorithms working on strings. + +Release 1.10.0. Requires Java 8. + +Changes in this version include: + +New features: +o TEXT-207: Add DoubleFormat utility. +o TEXT-190: Document negative limit for WordUtils abbreviate method Thanks to Benjamin Bing. +o TEXT-188: Speed up LevenshteinDistance with threshold by exiting early Thanks to Jakob Vesterstrm. +o TEXT-185: Release Notes page hasn't been updated for 1.9 release yet. Thanks to Larry West, Gary Gregory. +o Add StrBuilder.isNotEmpty(). Thanks to Gary Gregory. + +Fixed Bugs: +o TEXT-189: Fix CaseUtils when the input string contains only delimiters Thanks to Gongpu Zhu. +o TEXT-187: Add GraalVM test dependencies to fix test failures with Java 15. +o TEXT-158: Incorrect values for Jaccard similarity with empty strings. +o TEXT-186: StringSubstitutor map constructor throws NPE on 1.9 with null map. Thanks to Gautam Korlam, Gary Gregory. +o TEXT-191: JaroWinklerDistance returns the same values as JaroWinklerSimilarity. Thanks to Bradley David Rumball. +o Correct Javadoc in FileStringLookup. Thanks to Gary Gregory. +o Minor Improvements #192, #196. Thanks to Arturo Bernal. +o TEXT-194: Use StringUtils.INDEX_NOT_FOUND constant. Thanks to Arturo Bernal. +o TEXT-199: Remove redundant local variable. Thanks to Arturo Bernal. +o TEXT-198: Replace lambda with method reference. Thanks to Arturo Bernal. +o TEXT-200: Simplify statements. Thanks to Arturo Bernal. +o TEXT-197: Replace statement lambda with expression lambda. Thanks to Arturo Bernal. +o TEXT-204: Use static class inner class in tests. Thanks to Arturo Bernal. +o TEXT-201: Simplify assertion. Thanks to Arturo Bernal. +o TEXT-202: Extract duplicate code. Thanks to Arturo Bernal. +o TEXT-205: Set void return method. Thanks to Arturo Bernal. +o Remove unused exception from TextStringBuilder.readFrom(CharBuffer). This preserves binary compatibility but not source compatibility. Thanks to Gary Gregory. +o StrBuilder.StrBuilderReader.skip(long): Throw an exception when an implicit narrowing conversion in a compound assignment would result in information loss or a numeric error such as an overflows. Thanks to CodeQL, Gary Gregory. +o TextStringBuilder.TextStringBuilderReader.skip(long): Throw an exception when an implicit narrowing conversion in a compound assignment would result in information loss or a numeric error such as an overflows. Thanks to CodeQL, Gary Gregory. +o TEXT-211: TextStringBuilder.equals whatever the capacity is #281. Thanks to sebx59. +o TEXT=212: A More Efficient Implementation for Calculating Size of Longest Common Subsequence. Thanks to Ali Ghanbari. +o TEXT-209: LookupTranslator returns count of chars consumed, not of codepoints consumed. Thanks to fourAjeff. +o TEXT-209: Use Math.min() call instead of doing it manually. #335. Thanks to Arturo Bernal. +o TextStringBuilder: Throw OutOfMemoryError instead of NegativeArraySizeException. Thanks to ValentijnvdBeek, Gary Gregory. +o TextStringBuilder: Can't grow to sizes up to Integer.MAX_VALUE. Thanks to ValentijnvdBeek, Gary Gregory. +o Make default string lookups configurable via system property. Remove dns, url, and script lookups from defaults. If these lookups are required for use in StringSubstitutor.createInterpolator(), they must be enabled via system property. See StringLookupFactory for details. + +Changes: +o Bump actions/setup-java from v1.4.0 to 3 #147, #156, #155, #172, #215, #314. Thanks to Dependabot. +o Bump github/codeql-action from 1 to 2 #319. Thanks to Dependabot. +o Bump checkstyle from 8.34 to 9.3, #141, #168, #182, #188, #193, #201, #208, #211, #228, #235, #245, #253, #255, #262, #270, #280, #287, #299, #315, #321. Thanks to Dependabot. +o Bump spotbugs-maven-plugin from 4.0.0 to 4.7.2.0, #144, #150, #167, #176, #194, #210, #223, #250, #268, #273, #277, #278, #286, #293, #303, #320, #325, #338, #344, #354. Thanks to Gary Gregory, Dependabot. +o Bump spotbugs from 4.1.3 to 4.7.2 #175, 189, #209, #218, #247, #256, #264, #275, #284, #289, #296, #306, #355. Thanks to Gary Gregory, Dependabot. +o Bump mockito-inline from 3.4.4 to 4.8.0, #143, #148, #149, #152, #153, #154, #158, #159, #166, #177, #180, #187, #195, #197, #207, #216, #231, #236, #237, #243, #258, #259, #260, #261, #272, #285, #291, #305, #317, #330, #331, #347, #352. Thanks to Dependabot. +o Bump junit-jupiter from 5.6.2 to 5.9.1 #163, #204, #232, #265, #269, #288, #343, #357. Thanks to Dependabot. +o Bump assertj-core from 3.16.1 to 3.23.1 #151, #157, #160, #178, #184, #199, #244, #267, #294. Thanks to Dependabot, Gary Gregory. +o Bump commons-io from 2.7 to 2.11.0 #161 #251. Thanks to Dependabot, Gary Gregory. +o Bump commons-parent from 51 to 54 #145, #358. Thanks to Dependabot, Gary Gregory. +o Bump maven-pmd-plugin from 3.13.0 to 3.19.0 #186, #263, #302, #334, #349, #353. Thanks to Dependabot. +o Bump pmd from 6.42.0 to 6.46.0. Thanks to Gary Gregory. +o Bump graalvm.version from 20.2.0 to 22.0.0.2 #185, #198, #206, #227, #252, #276, #295, #300. Thanks to Dependabot. +o Bump commons.japicmp.version from 0.14.3 to 0.16.0. Thanks to Gary Gregory. +o Bump commons.jacoco.version 0.8.5 to 0.8.8; fixes Java 15 build. Thanks to Gary Gregory. +o Bump maven-checkstyle-plugin from 3.1.1 to 3.2.0 #202, #348. Thanks to Dependabot. +o Bump commons-lang3 3.11 -> 3.12.0. Thanks to Gary Gregory. +o Bump commons.javadoc.version from 3.2.0 to 3.4.1. Thanks to Gary Gregory. +o Bump commons.project-info.version from 3.1.0 to 3.1.2. Thanks to Gary Gregory. +o Bump jmh.version from 1.32 to 1.35 #254, #292, #313. Thanks to Dependabot. +o Bump commons-rng-simple from 1.3 to 1.4 #266. Thanks to Dependabot. +o Bump taglist-maven-plugin from 2.4 to 3.0.0 #297. Thanks to Dependabot. +o Bump commons.pmd-impl.version from 6.44.0 to 6.49.0 #323, #336, #345, #350. Thanks to Dependabot. +o Bump exec-maven-plugin from 3.0.0 to 3.1.0 #340. Thanks to Dependabot. + + +Historical list of changes: https://commons.apache.org/proper/commons-text/changes-report.html + +For complete information on Apache Commons Text, including instructions on how to submit bug reports, +patches, or suggestions for improvement, see the Apache Commons Text website: + +https://commons.apache.org/proper/commons-text + +Download page: https://commons.apache.org/proper/commons-text/download_text.cgi + +Have fun! +-Apache Commons Team + +============================================================================= + +Apache Commons Text +Version 1.9 +Release Notes + + +INTRODUCTION: + +This document contains the release notes for the 1.9 version of Apache Commons Text. +Commons Text is a set of utility functions and reusable components for the purpose of processing +and manipulating text that should be of use in a Java environment. + + +Apache Commons Text is a library focused on algorithms working on strings. + +Release 1.9. Requires Java 8. + +Changes in this version include: + +New features: +o Add StringMatcher.size(). Thanks to Gary Gregory. +o Refactor TextStringBuilder.readFrom(Readable), extracting readFrom(CharBuffer) and readFrom(Reader). Thanks to Gary Gregory. +o Add BiStringLookup and implementation BiFunctionStringLookup. Thanks to Gary Gregory. +o Add org.apache.commons.text.StringSubstitutor.StringSubstitutor(StringSubstitutor). Thanks to Gary Gregory. +o Add org.apache.commons.text.TextStringBuilder.TextStringBuilder(CharSequence). Thanks to Gary Gregory. +o Add org.apache.commons.text.TextStringBuilder.drainChar(int). Thanks to Gary Gregory. +o Add org.apache.commons.text.TextStringBuilder.drainChars(int, int, char[]. int). Thanks to Gary Gregory. +o Add org.apache.commons.text.TextStringBuilder.isNotEmpty(). Thanks to Gary Gregory. +o Add org.apache.commons.text.TextStringBuilder.isReallocated(). Thanks to Gary Gregory. +o Add org.apache.commons.text.TextStringBuilder.readFrom(Reader, int). Thanks to Gary Gregory. +o Add org.apache.commons.text.TextStringBuilder.set(String). Thanks to Gary Gregory. +o Add org.apache.commons.text.TextStringBuilder.wrap(char[]). Thanks to Gary Gregory. +o Add org.apache.commons.text.TextStringBuilder.wrap(char[], int). Thanks to Gary Gregory. +o Add org.apache.commons.text.io.StringSubstitutorReader. Thanks to Gary Gregory. +o Add org.apache.commons.text.lookup.StringLookupFactory.functionStringLookup(Function). Thanks to Gary Gregory. +o Add org.apache.commons.text.matcher.StringMatcher.isMatch(CharSequence, int). Thanks to Gary Gregory. +o Add org.apache.commons.text.matcher.StringMatcher.isMatch(CharSequence, int, int, int). Thanks to Gary Gregory. +o Add org.apache.commons.text.matcher.StringMatcherFactory.andMatcher(StringMatcher...). Thanks to Gary Gregory. +o Add org.apache.commons.text.matcher.StringMatcherFactory.stringMatcher(char...). Thanks to Gary Gregory. + +Fixed Bugs: +o TEXT-166: Removed non-existing parameter from Javadocs and spelled out parameters in throws. Thanks to Mikko Maunu. +o TEXT-149: StringEscapeUtils.unescapeCsv doesn't remove quotes at begin and end of string. Thanks to Yuji Konishi. +o TEXT-174: ScriptStringLookup does not accept ":" #126. Thanks to furkilic. +o TEXT-178: StringSubstitutor incorrectly removes some escape characters. Thanks to Gary Gregory. +o TEXT-181: Fix Javadocs #135. Thanks to XenoAmess. +o TEXT-182: Fix typos #137. Thanks to XenoAmess. +o TEXT-183: Make ConstantStringLookup.constantCache final #136. Thanks to XenoAmess. +o TEXT-184: Simplify if in CaseUtils #134. Thanks to XenoAmess. + +Changes: +o [javadoc] Fix compiler warnings in Java code example in Javadoc #124. Thanks to Johan Hammar. +o TEXT-177: Update from Apache Commons Lang 3.9 to 3.11. Thanks to Gary Gregory. +o [build] Skip clirr since we use JApiCmp. Thanks to Gary Gregory. +o [test] junit-jupiter 5.5.1 -> 5.5.2. Thanks to Gary Gregory. +o [test] org.assertj:assertj-core 3.13.2 -> 3.16.1. Thanks to Gary Gregory. +o [build] com.puppycrawl.tools:checkstyle 8.23 -> 8.34. Thanks to Gary Gregory. +o [build] Update JUnit from 5.5.2 to 5.6.2. Thanks to Gary Gregory. +o [build] commons.jacoco.version 0.8.4 -> 0.8.5. Thanks to Gary Gregory. +o [build] commons.javadoc.version 3.1.1 -> 3.2.0. Thanks to Gary Gregory. +o [build] commons.japicmp.version 0.14.1 -> 0.14.3. Thanks to Gary Gregory. +o [build] checkstyle.plugin.version 3.1.0 -> 3.1.1. Thanks to Gary Gregory. +o [build] checkstyle.version 8.27 -> 8.33. Thanks to Gary Gregory. +o [build] org.apache.commons:commons-parent 48 -> 51. Thanks to Gary Gregory. +o [build] maven-pmd-plugin 3.12.0 -> 3.13.0. Thanks to Gary Gregory. +o [build] org.mockito 3.3.3 -> 3.4.4. Thanks to Gary Gregory. + + +Historical list of changes: https://commons.apache.org/proper/commons-text/changes-report.html + +For complete information on Apache Commons Text, including instructions on how to submit bug reports, +patches, or suggestions for improvement, see the Apache Commons Text website: + +https://commons.apache.org/proper/commons-text + +Download page: https://commons.apache.org/proper/commons-text/download_text.cgi + +Have fun! +-Apache Commons Team + +============================================================================= + +Apache Commons Text +Version 1.8 +Release Notes + + +INTRODUCTION: + +This document contains the release notes for the 1.8 version of Apache Commons Text. +Commons Text is a set of utility functions and reusable components for the purpose of processing +and manipulating text that should be of use in a Java environment. + + +Apache Commons Text is a library focused on algorithms working on strings. + +Release 1.8 + +Changes in this version include: + +- New Features + o TEXT-169: Add helper factory method org.apache.commons.text.StringSubstitutor.createInterpolator(). Thanks to Gary Gregory. + o TEXT-170: Add String lookup for host names and IP addresses (DnsStringLookup). Thanks to Gary Gregory. + +- Fixed Bugs + o TEXT-167: commons-text web page missing "RELEASE-NOTES-1.7.txt". Thanks to Larry West. + o TEXT-168: (doc) Fixed wrong value for Jaro-Winkler example #117. Thanks to luksan47. + o TEXT-171: StringLookupFactory.addDefaultStringLookups(Map) does not convert keys to lower case. Thanks to Gary Gregory. + +- Changes + o Expand Javadoc for StringSubstitutor and friends. Thanks to Gary Gregory. + o [site] checkstyle.version 8.21 -> 8.23. Thanks to Gary Gregory. + + +Historical list of changes: https://commons.apache.org/proper/commons-textchanges-report.html + +For complete information on Apache Commons Text, including instructions on how to submit bug reports, +patches, or suggestions for improvement, see the Apache Commons Text website: + +https://commons.apache.org/proper/commons-text + +Download it from https://commons.apache.org/proper/commons-text/download_text.cgi + +============================================================================= + +Apache Commons Text +Version 1.7 +Release Notes + +INTRODUCTION: + +This document contains the release notes for the 1.7 version of Apache Commons Text. +Commons Text is a set of utility functions and reusable components for the purpose of processing +and manipulating text that should be of use in a Java environment. + +Apache Commons Text is a library focused on algorithms working on strings. + +Changes in this version include: + +New features: + +o TEXT-148: Add an enum to the lookup package that lists all StringLookups +o TEXT-127: Add a toggle to throw an exception when a variable is unknown in StringSubstitutor Thanks to Jean-Baptiste REICH, Sebb, Don Jeba, Gary Gregory. +o TEXT-138: TextStringBuilder append sub-sequence not consistent with Appendable. Thanks to Neal Johnson, Don Jeba. +o TEXT-152: Fix possible infinite loop in WordUtils.wrap for a regex pattern that would trigger on a match of 0 length Thanks to @CAPS50. +o TEXT-155: Add a generic IntersectionSimilarity measure + +Fixed Bugs: + +o TEXT-111: WordUtils.wrap must calculate offset increment from wrapOn pattern length Thanks to @CAPS50. +o TEXT-151: Fix the JaroWinklerSimilarity to use StringUtils.equals to test for CharSequence equality +o TEXT-165: ResourceBundleStringLookup.lookup(String) throws MissingResourceException instead of returning null. + +Changes: + +o TEXT-104: Jaro Winkler Distance refers to similarity Thanks to Sascha Szott. +o TEXT-153: Make prefixSet in LookupTranslator a BitSet Thanks to amirhadadi. +o TEXT-156: Fix the RegexTokenizer to use a static Pattern +o TEXT-157: Remove rounding from JaccardDistance and JaccardSimilarity +o TEXT-162: Update Apache Commons Lang from 3.8.1 to 3.9. +o Update tests from org.assertj:assertj-core 3.12.1 to 3.12.2. +o Update site from com.puppycrawl.tools:checkstyle 8.18 to 8.21. + +Historical list of changes: https://commons.apache.org/proper/commons-text/changes-report.html + +For complete information on Apache Commons Text, including instructions on how to submit bug reports, +patches, or suggestions for improvement, see the Apache Commons Text website: + +https://commons.apache.org/proper/commons-text + +Download it from https://commons.apache.org/proper/commons-text/download_text.cgi + +============================================================================= + + Apache Commons Text + Version 1.6 + Release Notes + +INTRODUCTION +============ + +This document contains the release notes for the 1.6 version of Apache Commons +Text. Commons Text is a set of utility functions and reusable components for +the purpose of processing and manipulating text that should be of use in a Java +environment. + +This component requires Java 8. + +CHANGES +======= + +o TEXT-144: Add the resource string bundle string lookup to the default set of lookups +o TEXT-145: Add StringLookupFactory methods for the URL encoder and decoder string lookups +o TEXT-146: org.apache.commons.text.lookup.StringLookupFactory.interpolatorStringLookup() should reuse a singleton instance +o TEXT-147: Add a Base64 encoder string lookup. + +Historical list of changes: https://commons.apache.org/proper/commons-text/changes-report.html + +For complete information on Apache Commons Text, including instructions on how to submit bug reports, +patches, or suggestions for improvement, see the Apache Commons Text website: + +https://commons.apache.org/proper/commons-text + +============================================================================= + + Apache Commons Text + Version 1.5 + Release Notes + +INTRODUCTION +============ + +This document contains the release notes for the 1.5 version of Apache Commons +Text. Commons Text is a set of utility functions and reusable components for +the purpose of processing and manipulating text that should be of use in a Java +environment. + +This component requires Java 8. + +NEW FEATURES +============ + +o TEXT-133: Add a XML file XPath string lookup. +o TEXT-134: Add a Properties file string lookup. +o TEXT-135: Add a script string lookup. +o TEXT-136: Add a file string lookup. +o TEXT-137: Add a URL string lookup. +o TEXT-140: Add a Base64 string lookup. +o TEXT-141: Add org.apache.commons.text.lookup.StringLookupFactory.resourceBundleStringLookup(String). +o TEXT-142: Add URL encoder and decoder string lookups. +o TEXT-143: Add constant string lookup like the one in Apache Commons Configuration. + +FIXED BUGS +========== + +o TEXT-139: Improve JaccardSimilarity computational cost Thanks to Nick Wong. +o TEXT-118: JSON escaping incorrect for the delete control character Thanks to Nandor Kollar. +o TEXT-130: Fixes JaroWinklerDistance: Wrong results due to precision of transpositions Thanks to Jan Martin Keil. +o TEXT-131: JaroWinklerDistance: Calculation deviates from definition Thanks to Jan Martin Keil. + +CHANGES +======= + +o TEXT-132: Update Apache Commons Lang from 3.7 to 3.8.1 + +============================================================================= + + Apache Commons Text + Version 1.4 + Release Notes + +INTRODUCTION +============ + +This document contains the release notes for the 1.4 version of Apache Commons +Text. Commons Text is a set of utility functions and reusable components for +the purpose of processing and manipulating text that should be of use in a Java +environment. + +This component requires Java 8. + + +Changes in this version include: + +Fixed Bugs: +o TEXT-120: StringEscapeUtils#unescapeJson does not unescape double quotes and forward slash. +o TEXT-119: Remove mention of SQL escaping from user guide. +o TEXT-123: WordUtils.wrap throws StringIndexOutOfBoundsException when wrapLength is Integer.MAX_VALUE. Thanks to Takanobu Asanuma. + +Changes: +o TEXT-121: Update Java requirement from version 7 to 8. Thanks to pschumacher. +o TEXT-122: Allow full customization with new API org.apache.commons.text.lookup.StringLookupFactory.interpolatorStringLookup(Map, StringLookup, boolean). + +============================================================================= + + Apache Commons Text + Version 1.3 + Release Notes + +INTRODUCTION +============ + +This document contains the release notes for the 1.3 version of Apache Commons +Text. Commons Text is a set of utility functions and reusable components for +the purpose of processing and manipulating text that should be of use in a Java +environment. + +This component requires Java 7. + +NEW FEATURES +============= + +o Add Automatic-Module-Name MANIFEST entry for Java 9 compatibility Issue: TEXT-110. +o Add an interpolator string lookup: StringLookupFactory#interpolatorStringLookup() Issue: TEXT-113. +o Add a StrSubstitutor replacement based on interfaces: StringSubstitutor Issue: TEXT-114. +o Add a StrBuilder replacement based on the StringMatcher interface: TextStringBuilder Issue: TEXT-115. +o Add a StrTokenizer replacement based on the StringMatcher interface: StringTokenizer Issue: TEXT-116. +o Add a local host string lookup: LocalHostStringLookup Issue: TEXT-117. + +FIXED BUGS +========== + +o Build failure with java 9-ea+159 Issue: TEXT-70. +o StrLookup API confusing Issue: TEXT-80. + +============================================================================= + + Apache Commons Text + Version 1.2 + Release Notes + +INTRODUCTION +============ + +This document contains the release notes for the 1.2 version of Apache Commons +Text. Commons Text is a set of utility functions and reusable components for +the purpose of processing and manipulating text that should be of use in a Java +environment. + +This component requires Java 7. + +JAVA 9 SUPPORT +============== + +At our time of release of 1.1, our build succeeds with Java 9-ea build 159, +and we believe all of our features to be Java 9 compatible. However, when we +run "mvn clean site" we have failures. + +NEW FEATURES +============= + +o TEXT-74: StrSubstitutor: Ability to turn off substitution in values. Thanks to Ioannis Sermetziadis. +o TEXT-97: RandomStringGenerator able to pass multiple ranges to .withinRange(). Thanks to Amey Jadiye. +o TEXT-89: WordUtils.initials support for UTF-16 surrogate pairs. Thanks to Arun Vinud S S. +o TEXT-90: Add CharacterPredicates for ASCII letters (uppercase/lowercase) and arabic numerals. +o TEXT-85: Added CaseUtils class with camel case conversion support. Thanks to Arun Vinud S S. +o TEXT-91: RandomStringGenerator should be able to generate a String with a random length. +o TEXT-102: Add StrLookup.resourceBundleLookup(ResourceBundle). + +FIXED BUGS +========== + +o TEXT-106: Exception thrown in ExtendedMessageFormat using quotes with custom registry. Thanks to Benoit Moreau. +o TEXT-100: StringEscapeUtils#UnEscapeJson doesn't recognize escape signs correctly. Thanks to Don Jeba. +o TEXT-105: Typo in LongestCommonSubsequence#logestCommonSubsequence. Thanks to Abrasha. + +CHANGES +======= + +o TEXT-107: Upversion commons-lang to 3.7. +o TEXT-98: Deprecate isDelimiter and use HashSets for delimiter checks. Thanks to Arun Vinud S S. +o TEXT-88: WordUtils should treat an empty delimiter array as no delimiters. Thanks to Amey Jadiye. +o TEXT-93: Update RandomStringGenerator to accept a list of valid characters. Thanks to Amey Jadiye. +o TEXT-92: Update commons-lang dependency to version 3.6. +o TEXT-83: Document that commons-csv should be used in preference to CsvTranslators. Thanks to Amey Jadiye. +o TEXT-67: NumericEntityUnescaper.options - fix TODO. +o TEXT-84: RandomStringGenerator claims to be immutable, but isn't. + +============================================================================= + + Release Notes for version 1.1 + +JAVA 9 SUPPORT +============== + +At our time of release of 1.1, our build succeeds with Java 9-ea build 159, +and we believe all of our features to be Java 9 compatible. However, when we +run "mvn clean site" we have failures. + +NEW FEATURES +============ + +o TEXT-41: WordUtils.abbreviate support Thanks to Amey Jadiye. +o TEXT-82: Putting WordUtils back in to the codebase Thanks to Amey Jadiye. +o TEXT-81: Add RandomStringGenerator Thanks to djones. +o TEXT-36: RandomStringGenerator: allow users to provide source of randomness + Thanks to Raymond DeCampo. + +FIXED BUGS +========== + +o TEXT-76: Correct round issue in Jaro Winkler implementation +o TEXT-72: Similar to LANG-1025, clirr fails site build. + +CHANGES +======= + +o TEXT-39: WordUtils should use toXxxxCase(int) rather than toXxxxCase(char) + Thanks to Amey Jadiye. + +============================================================================= + + Release Notes for version 1.0 + +INCOMPATIBLE CHANGES +==================== + +All package names changed from org.apache.commons.text.beta in 1.0-beta-1 to +org.apache.commons.text in 1.0. + +Methods StringEscapeUtils#escapeHtml3Once and StringEscapeUtils#escapeHtml4Once +have been removed; see TEXT-40 + +JAVA 9 SUPPORT +============== + +At our time of release of 1.0, our build succeeds with Java 9-ea build 158, +and we believe all of our features to be Java 9 compatible. However, when we run +"mvn clean site" we have failures. + +FIXED BUGS +========== + +o TEXT-64: Investigate locale issue in ExtendedMessageFormatTest. Thanks to + chtompki. +o TEXT-69: Resolve PMD/CMD Violations +o TEXT-65: Fixing the 200 checkstyle errors present in 1.0-beta-1. +o TEXT-63: Mutable fields should be private. + +REMOVED +======= + +o TEXT-40: Escape HTML characters only once: revert. + +============================================================================= + + Release Notes for version 1.0-beta-1 + + +A NOTE ON THE HISTORY OF THE CODE +================================= + +The codebase began in the fall of 2014 as a location for housing algorithms for +operating on Strings that seemed to have a more complex nature than those which +would be considered a needed extension to java.lang. Thus, a new component, +different from Apache Commons Lang was warranted. As the project evolved, it was +noticed that Commons Lang had considerable more text manipulation tools than +the average Java application developer would need or even want. So, we have +decided to move the more esoteric String processing algorithms out of Commons +Lang into Commons Text. + +JAVA 9 SUPPORT +============== + +At our time of release of 1.0-beta-1, our build succeeds with Java 9-ea build 153, +and we believe all of our features to be Java 9 compatible. + +NEW FEATURES +============ + +o TEXT-56: Move CvsTranslators out of StringEscapeUtils and make them DRY + Thanks to Jarek Strzeleck. +o TEXT-40: Escape HTML characters only once Thanks to Sampanna Kahu. +o TEXT-32: Add LCS similarity and distance +o TEXT-34: Add class to generate random strings +o TEXT-29: Add a builder to StringEscapeUtils +o TEXT-28: Add shell/XSI escape/unescape support +o TEXT-2: Add Jaccard Index and Jaccard Distance Thanks to Don Jeba. +o TEXT-27: Move org.apache.commons.lang3.StringEscapeUtils.java into text +o TEXT-23: Moving from commons-lang, the package org.apache.commons.lang3.text +o TEXT-10: A more complex Levenshtein distance Thanks to Don Jeba. +o TEXT-24: Add coveralls and Travis.ci integration +o TEXT-19: Add alphabet converter Thanks to Eyal Allweil. +o TEXT-13: Create Commons Text logo +o TEXT-7: Write user guide +o TEXT-15: Human name parser +o TEXT-3: Add Cosine Similarity and Cosine Distance +o TEXT-4: Port Myers algorithm from [collections] +o TEXT-1: Add Hamming distance +o TEXT-9: Incorporate String algorithms from Commons Lang Thanks to britter. + +FIXED BUGS +========== + +Note. We recognize the curiosity of a new component having "fixed bugs," but a +considerable number of files were migrated over from Commons Lang, some of which +needed fixes. + +o TEXT-62: Incorporate suggestions from RC2 into 1.0 release. +o TEXT-60: Upgrading Jacoco for Java 9-ea compatibility. Thanks to Lee Adcock. +o TEXT-52: Possible attacks through StringEscapeUtils.escapeEcmaScrip better + javadoc +o TEXT-37: Global vs local source of randomness +o TEXT-38: Fluent API in "RandomStringBuilder" +o TEXT-26: Fix JaroWinklerDistance in the manner of LUCENE-1297 +o TEXT-35: Unfinished class Javadoc for CosineDistance +o TEXT-22: LevenshteinDistance reduce memory consumption +o TEXT-5: IP clearance for the names package +o TEXT-11: Work on the string metric, distance, and similarity definitions for + the project +o TEXT-12: Create StringDistanceFrom class that contains a StringMetric and + the "left" side string. This would have a method that accepts the + "right" side string to test. Thanks to Jonathan baker. +o TEXT-8: Change (R) StringMetric.compare(CS left, CS right) to "apply" so + that it is consistent with BiFunction. Thanks to Jonathan Baker. +o TEXT-6: Allow extra information (e.g. Levenshtein threshold) to be stored + as (final) fields in the StringMetric instance. Thanks to Jonathan + Baker. + +CHANGES +======= + +o TEXT-61: Naming packages org.apache.commons.text.beta Thanks to Lee Adcock. +o TEXT-58: Refactor EntityArrays to have unmodifiableMaps in leu of String[][] +o TEXT-53: Prepare site for 1.0 release +o TEXT-50: Upgrade from commons-parent version 41 to version 42 +o TEXT-33: Consolidating since tags at 1.0, removing deprecated methods +o TEXT-16: Improve HumanNameParser + +REMOVED +======= + +o TEXT-55: Remove WordUtils to be added back in an upcoming 1.X release +o TEXT-51: Remove RandomStringGenerator to be added back in the 1.1 release +o TEXT-31: Remove org.apache.commons.text.names, for later release than 1.0 + + +Historical list of changes: https://commons.apache.org/text/changes-report.html + +For complete information on Apache Commons Text, including instructions on how +to submit bug reports, patches, or suggestions for improvement, see the Apache +Apache Commons Text website: + +https://commons.apache.org/text/ + +Have fun! +-Apache Commons Text team \ No newline at end of file diff --git a/sources/SECURITY.md b/sources/SECURITY.md new file mode 100644 index 0000000..51943ba --- /dev/null +++ b/sources/SECURITY.md @@ -0,0 +1,17 @@ + +The Apache Commons security page is [https://commons.apache.org/security.html](https://commons.apache.org/security.html). diff --git a/sources/pom.xml b/sources/pom.xml new file mode 100644 index 0000000..e39603b --- /dev/null +++ b/sources/pom.xml @@ -0,0 +1,522 @@ + + + + 4.0.0 + + org.apache.commons + commons-parent + 56 + + commons-text + 1.10.1-SNAPSHOT + Apache Commons Text + Apache Commons Text is a library focused on algorithms working on strings. + https://commons.apache.org/proper/commons-text + + + ISO-8859-1 + UTF-8 + 1.8 + 1.8 + + text + text + org.apache.commons.text + + 1.10.0 + (Java 8+) + + TEXT + 12318221 + + text + https://svn.apache.org/repos/infra/websites/production/commons/content/proper/commons-text + site-content + + 4.11.0 + + + 3.10.0 + + + 22.0.0.2 + 1.5 + + false + + 1.36 + + + 1.9 + RC1 + true + scm:svn:https://dist.apache.org/repos/dist/dev/commons/${commons.componentid} + Gary Gregory + 86fdc7e2a11262cb + + + + + org.apache.commons + commons-lang3 + 3.12.0 + + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + 3.24.2 + test + + + commons-io + commons-io + 2.11.0 + test + + + org.mockito + + mockito-inline + ${commons.mockito.version} + test + + + org.graalvm.js + js + ${graalvm.version} + test + + + org.graalvm.js + js-scriptengine + ${graalvm.version} + test + + + org.apache.commons + commons-rng-simple + ${commons.rng.version} + test + + + org.openjdk.jmh + jmh-core + ${jmh.version} + test + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + test + + + + + clean verify apache-rat:check japicmp:cmp checkstyle:check spotbugs:check javadoc:javadoc + + + + org.apache.rat + apache-rat-plugin + + + site-content/** + src/site/resources/download_lang.cgi + src/test/resources/org/apache/commons/text/stringEscapeUtilsTestData.txt + src/test/resources/org/apache/commons/text/lcs-perf-analysis-inputs.csv + src/site/resources/release-notes/RELEASE-NOTES-*.txt + + + + + maven-pmd-plugin + ${commons.pmd.version} + + ${maven.compiler.target} + + + + + + + maven-checkstyle-plugin + + false + src/conf/checkstyle.xml + src/conf/checkstyle-header.txt + src/conf/checkstyle-suppressions.xml + src/conf/checkstyle-suppressions.xml + true + **/generated/**.java,**/jmh_generated/**.java + + + + com.github.spotbugs + spotbugs-maven-plugin + + src/conf/spotbugs-exclude-filter.xml + + + + maven-assembly-plugin + + + src/assembly/bin.xml + src/assembly/src.xml + + gnu + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + ${commons.module.name} + + + + + + org.apache.maven.plugins + maven-scm-publish-plugin + + + javadocs + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + ${maven.compiler.source} + + + + + + + + + maven-checkstyle-plugin + + false + src/conf/checkstyle.xml + src/conf/checkstyle-header.txt + src/conf/checkstyle-suppressions.xml + src/conf/checkstyle-suppressions.xml + true + **/generated/**.java,**/jmh_generated/**.java + + + + + checkstyle + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + + src/conf/spotbugs-exclude-filter.xml + + + + com.github.siom79.japicmp + japicmp-maven-plugin + + + maven-pmd-plugin + + ${maven.compiler.target} + + + + + pmd + cpd + + + + + + org.codehaus.mojo + taglist-maven-plugin + 3.0.0 + + + + + Needs Work + + + TODO + exact + + + FIXME + exact + + + XXX + exact + + + + + Noteable Markers + + + NOTE + exact + + + NOPMD + exact + + + NOSONAR + exact + + + + + + + + + + + 2014 + + + + kinow + Bruno P. Kinoshita + kinow@apache.org + + + britter + Benedikt Ritter + britter@apache.org + + + chtompki + Rob Tompkins + chtompki@apache.org + + + ggregory + Gary Gregory + ggregory at apache.org + https://www.garygregory.com + The Apache Software Foundation + https://www.apache.org/ + + PMC Member + + America/New_York + + https://people.apache.org/~ggregory/img/garydgregory80.png + + + + djones + Duncan Jones + djones@apache.org + + + + + + Don Jeba + donjeba@yahoo.com + + + Sampanna Kahu + + + Jarek Strzelecki + + + Lee Adcock + + + Amey Jadiye + ameyjadiye@gmail.com + + + Arun Vinud S S + + + Ioannis Sermetziadis + + + Jostein Tveit + + + Luciano Medallia + + + Jan Martin Keil + + + Nandor Kollar + + + Nick Wong + + + Ali Ghanbari + https://ali-ghanbari.github.io/ + + + + + scm:git:https://gitbox.apache.org/repos/asf/commons-text + scm:git:https://gitbox.apache.org/repos/asf/commons-text + https://gitbox.apache.org/repos/asf?p=commons-text.git + + + + jira + https://issues.apache.org/jira/browse/TEXT + + + + + apache.website + Apache Commons Site + scm:svn:https://svn.apache.org/repos/infra/websites/production/commons/content/proper/commons-text/ + + + + + + setup-checkout + + + site-content + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + prepare-checkout + + run + + pre-site + + + + + + + + + + + + + + + + + + + + + + + + java9+ + + [9,) + + + + true + + + + benchmark + + true + org.apache + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + benchmark + test + + exec + + + test + java + + -classpath + + org.openjdk.jmh.Main + -rf + json + -rff + target/jmh-result.${benchmark}.json + ${benchmark} + + + + + + + + + + diff --git a/sources/src/assembly/bin.xml b/sources/src/assembly/bin.xml new file mode 100644 index 0000000..5762291 --- /dev/null +++ b/sources/src/assembly/bin.xml @@ -0,0 +1,44 @@ + + + bin + + tar.gz + zip + + false + + + + LICENSE.txt + NOTICE.txt + RELEASE-NOTES.txt + + + + target + + + *.jar + + + + target/site/apidocs + apidocs + + + diff --git a/sources/src/assembly/src.xml b/sources/src/assembly/src.xml new file mode 100644 index 0000000..0afc458 --- /dev/null +++ b/sources/src/assembly/src.xml @@ -0,0 +1,44 @@ + + + src + + tar.gz + zip + + ${project.artifactId}-${commons.release.version}-src + + + + checkstyle.xml + checkstyle-suppressions.xml + CONTRIBUTING.md + sb-excludes.xml + LICENSE.txt + checkstyle-header.txt + NOTICE.txt + pom.xml + PROPOSAL.html + README.md + RELEASE-NOTES.txt + + + + src + + + diff --git a/sources/src/changes/changes.xml b/sources/src/changes/changes.xml new file mode 100644 index 0000000..5a8f57b --- /dev/null +++ b/sources/src/changes/changes.xml @@ -0,0 +1,344 @@ + + + + + + + + + Apache Commons Text Changes + + + + + Fix StringTokenizer.getTokenList to return an independent modifiable list + Fix Javadoc for StringEscapeUtils.escapeHtml4 #382 + TextStringBuidler#hashCode() allocates a String on each call #387. + Fix Bundle-SymbolicName to use the package name org.apache.commons.text + Add and use a package-private singleton for RegexTokenizer. + Add and use a package-private singleton for CosineSimilarity. + Add and use a package-private singleton for LongestCommonSubsequence. + Add and use a package-private singleton for JaroWinklerSimilarity. + Add and use a package-private singleton for JaccardSimilarity. + + + Bump actions/cache from 3.0.8 to 3.0.10 #361, #365. + Bump actions/setup-java from 3 to 3.5.1. + Bump actions/checkout from 3.0.2 to 3.1.0 #366. + Bump pmd from 6.49.0 to 6.52.0 #364. + Bump commons-rng-simple from 1.4 to 1.5 #370. + Bump spotbugs-maven-plugin from 4.7.2.0 to 4.7.3.0 #371, #385. + Bump spotbugs from 4.7.2 to 4.7.3 #373. + Bump mockito-inline from 4.8.0 to 4.11.0 #380, #389, #396, #400. + Bump jmh.version from 1.35 to 1.36 #388. + Bump commons-parent from 54 to 56 #392, #401. + Bump assertj-core from 3.23.1 to 3.24.2 #405, #410. + Bump maven-checkstyle-plugin from 3.2.0 to 3.2.1 #407. + + + + Fix CaseUtils when the input string contains only delimiters + Add GraalVM test dependencies to fix test failures with Java 15. + Incorrect values for Jaccard similarity with empty strings. + StringSubstitutor map constructor throws NPE on 1.9 with null map. + JaroWinklerDistance returns the same values as JaroWinklerSimilarity. + Correct Javadoc in FileStringLookup. + Minor Improvements #192, #196. + Use StringUtils.INDEX_NOT_FOUND constant. + Remove redundant local variable. + Replace lambda with method reference. + Simplify statements. + Replace statement lambda with expression lambda. + Use static class inner class in tests. + Simplify assertion. + Extract duplicate code. + Set void return method. + Remove unused exception from TextStringBuilder.readFrom(CharBuffer). This preserves binary compatibility but not source compatibility. + StrBuilder.StrBuilderReader.skip(long): Throw an exception when an implicit narrowing conversion in a compound assignment would result in information loss or a numeric error such as an overflows. + TextStringBuilder.TextStringBuilderReader.skip(long): Throw an exception when an implicit narrowing conversion in a compound assignment would result in information loss or a numeric error such as an overflows. + TextStringBuilder.equals whatever the capacity is #281. + A More Efficient Implementation for Calculating Size of Longest Common Subsequence. + LookupTranslator returns count of chars consumed, not of codepoints consumed. + Use Math.min() call instead of doing it manually. #335. + TextStringBuilder: Throw OutOfMemoryError instead of NegativeArraySizeException. + TextStringBuilder: Can't grow to sizes up to Integer.MAX_VALUE. + Make default string lookups configurable via system property. Remove dns, url, and script lookups from defaults. If these lookups are required for use in StringSubstitutor.createInterpolator(), they must be enabled via system property. See StringLookupFactory for details. + + Add DoubleFormat utility. + Document negative limit for WordUtils abbreviate method + Speed up LevenshteinDistance with threshold by exiting early + Release Notes page hasn't been updated for 1.9 release yet. + Add StrBuilder.isNotEmpty(). + + Bump actions/setup-java from v1.4.0 to 3 #147, #156, #155, #172, #215, #314. + Bump github/codeql-action from 1 to 2 #319. + Bump checkstyle from 8.34 to 9.3, #141, #168, #182, #188, #193, #201, #208, #211, #228, #235, #245, #253, #255, #262, #270, #280, #287, #299, #315, #321. + Bump spotbugs-maven-plugin from 4.0.0 to 4.7.2.0, #144, #150, #167, #176, #194, #210, #223, #250, #268, #273, #277, #278, #286, #293, #303, #320, #325, #338, #344, #354. + Bump spotbugs from 4.1.3 to 4.7.2 #175, 189, #209, #218, #247, #256, #264, #275, #284, #289, #296, #306, #355. + Bump mockito-inline from 3.4.4 to 4.8.0, #143, #148, #149, #152, #153, #154, #158, #159, #166, #177, #180, #187, #195, #197, #207, #216, #231, #236, #237, #243, #258, #259, #260, #261, #272, #285, #291, #305, #317, #330, #331, #347, #352. + Bump junit-jupiter from 5.6.2 to 5.9.1 #163, #204, #232, #265, #269, #288, #343, #357. + Bump assertj-core from 3.16.1 to 3.23.1 #151, #157, #160, #178, #184, #199, #244, #267, #294. + Bump commons-io from 2.7 to 2.11.0 #161 #251. + Bump commons-parent from 51 to 54 #145, #358. + Bump maven-pmd-plugin from 3.13.0 to 3.19.0 #186, #263, #302, #334, #349, #353. + Bump pmd from 6.42.0 to 6.46.0. + Bump graalvm.version from 20.2.0 to 22.0.0.2 #185, #198, #206, #227, #252, #276, #295, #300. + Bump commons.japicmp.version from 0.14.3 to 0.16.0. + Bump commons.jacoco.version 0.8.5 to 0.8.8; fixes Java 15 build. + Bump maven-checkstyle-plugin from 3.1.1 to 3.2.0 #202, #348. + Bump commons-lang3 3.11 -> 3.12.0. + Bump commons.javadoc.version from 3.2.0 to 3.4.1. + Bump commons.project-info.version from 3.1.0 to 3.1.2. + Bump jmh.version from 1.32 to 1.35 #254, #292, #313. + Bump commons-rng-simple from 1.3 to 1.4 #266. + Bump taglist-maven-plugin from 2.4 to 3.0.0 #297. + Bump commons.pmd-impl.version from 6.44.0 to 6.49.0 #323, #336, #345, #350. + Bump exec-maven-plugin from 3.0.0 to 3.1.0 #340. + + + Removed non-existing parameter from Javadocs and spelled out parameters in throws. + StringEscapeUtils.unescapeCsv doesn't remove quotes at begin and end of string. + ScriptStringLookup does not accept ":" #126. + StringSubstitutor incorrectly removes some escape characters. + Fix Javadocs #135. + Fix typos #137. + Make ConstantStringLookup.constantCache final #136. + Simplify if in CaseUtils #134. + [javadoc] Fix compiler warnings in Java code example in Javadoc #124. + Update from Apache Commons Lang 3.9 to 3.11. + Add StringMatcher.size(). + Refactor TextStringBuilder.readFrom(Readable), extracting readFrom(CharBuffer) and readFrom(Reader). + Add BiStringLookup and implementation BiFunctionStringLookup. + Add org.apache.commons.text.StringSubstitutor.StringSubstitutor(StringSubstitutor). + Add org.apache.commons.text.TextStringBuilder.TextStringBuilder(CharSequence). + Add org.apache.commons.text.TextStringBuilder.drainChar(int). + Add org.apache.commons.text.TextStringBuilder.drainChars(int, int, char[]. int). + Add org.apache.commons.text.TextStringBuilder.isNotEmpty(). + Add org.apache.commons.text.TextStringBuilder.isReallocated(). + Add org.apache.commons.text.TextStringBuilder.readFrom(Reader, int). + Add org.apache.commons.text.TextStringBuilder.set(String). + Add org.apache.commons.text.TextStringBuilder.wrap(char[]). + Add org.apache.commons.text.TextStringBuilder.wrap(char[], int). + Add org.apache.commons.text.io.StringSubstitutorReader. + Add org.apache.commons.text.lookup.StringLookupFactory.functionStringLookup(Function<String, V>). + Add org.apache.commons.text.matcher.StringMatcher.isMatch(CharSequence, int). + Add org.apache.commons.text.matcher.StringMatcher.isMatch(CharSequence, int, int, int). + Add org.apache.commons.text.matcher.StringMatcherFactory.andMatcher(StringMatcher...). + Add org.apache.commons.text.matcher.StringMatcherFactory.stringMatcher(char...). + [build] Skip clirr since we use JApiCmp. + [test] junit-jupiter 5.5.1 -> 5.5.2. + [test] org.assertj:assertj-core 3.13.2 -> 3.16.1. + [build] com.puppycrawl.tools:checkstyle 8.23 -> 8.34. + [build] Update JUnit from 5.5.2 to 5.6.2. + [build] commons.jacoco.version 0.8.4 -> 0.8.5. + [build] commons.javadoc.version 3.1.1 -> 3.2.0. + [build] commons.japicmp.version 0.14.1 -> 0.14.3. + [build] checkstyle.plugin.version 3.1.0 -> 3.1.1. + [build] checkstyle.version 8.27 -> 8.33. + [build] org.apache.commons:commons-parent 48 -> 51. + [build] maven-pmd-plugin 3.12.0 -> 3.13.0. + [build] org.mockito 3.3.3 -> 3.4.4. + + + + commons-text web page missing "RELEASE-NOTES-1.7.txt" + (doc) Fixed wrong value for Jaro-Winkler example #117 + Add helper factory method org.apache.commons.text.StringSubstitutor.createInterpolator(). + Add String lookup for host names and IP addresses (DnsStringLookup). + StringLookupFactory.addDefaultStringLookups(Map) does not convert keys to lower case. + Expand Javadoc for StringSubstitutor and friends. + [site] checkstyle.version 8.21 -> 8.23. + + + + WordUtils.wrap must calculate offset increment from wrapOn pattern length + Jaro Winkler Distance refers to similarity + Add an enum to the lookup package that lists all StringLookups + Add a toggle to throw an exception when a variable is unknown in StringSubstitutor + TextStringBuilder append sub-sequence not consistent with Appendable. + Fix possible infinite loop in WordUtils.wrap for a regex pattern that would trigger on a match of 0 length + Make prefixSet in LookupTranslator a BitSet + Fix the RegexTokenizer to use a static Pattern + Remove rounding from JaccardDistance and JaccardSimilarity + Fix the JaroWinklerSimilarity to use StringUtils.equals to test for CharSequence equality + Add a generic IntersectionSimilarity measure + Update Apache Commons Lang from 3.8.1 to 3.9. + ResourceBundleStringLookup.lookup(String) throws MissingResourceException instead of returning null. + Update tests from org.assertj:assertj-core 3.12.1 to 3.12.2. + Update site from com.puppycrawl.tools:checkstyle 8.18 to 8.21. + + + + Add the resource string bundle string lookup to the default set of lookups + Add StringLookupFactory methods for the URL encoder and decoder string lookups + org.apache.commons.text.lookup.StringLookupFactory.interpolatorStringLookup() should reuse a singleton instance + Add a Base64 encoder string lookup. + + + + Improve JaccardSimilarity computational cost + JSON escaping incorrect for the delete control character + Fixes JaroWinklerDistance: Wrong results due to precision of transpositions + JaroWinklerDistance: Calculation deviates from definition + Update Apache Commons Lang from 3.7 to 3.8.1 + Add a XML file XPath string lookup. + Add a Properties file string lookup. + Add a script string lookup. + Add a file string lookup. + Add a URL string lookup. + Add a Base64 string lookup. + Add org.apache.commons.text.lookup.StringLookupFactory.resourceBundleStringLookup(String). + Add URL encoder and decoder string lookups. + Add constant string lookup like the one in Apache Commons Configuration. + + + + StringEscapeUtils#unescapeJson does not unescape double quotes and forward slash + Remove mention of SQL escaping from user guide + Update Java requirement from version 7 to 8. + Allow full customization with new API org.apache.commons.text.lookup.StringLookupFactory.interpolatorStringLookup(Map<String, StringLookup>, StringLookup, boolean). + WordUtils.wrap throws StringIndexOutOfBoundsException when wrapLength is Integer.MAX_VALUE. + + + + Add Automatic-Module-Name MANIFEST entry for Java 9 compatibility + Build failure with java 9-ea+159 + Add an interpolator string lookup: StringLookupFactory#interpolatorStringLookup() + Add a StrSubstitutor replacement based on interfaces: StringSubstitutor + Add a StrBuilder replacement based on the StringMatcher interface: TextStringBuilder + Add a StrTokenizer replacement based on the StringMatcher interface: StringTokenizer + Add a local host string lookup: LocalHostStringLookup + StrLookup API confusing + + + + Upversion commons-lang to 3.7 + Exception thrown in ExtendedMessageFormat using quotes with custom registry + StringEscapeUtils#UnEscapeJson doesn't recognize escape signs correctly + StrSubstitutor: Ability to turn off substitution in values + RandomStringGenerator able to pass multiple ranges to .withinRange() + Deprecate isDelimiter and use HashSets for delimiter checks + WordUtils.initials support for UTF-16 surrogate pairs + WordUtils should treat an empty delimiter array as no delimiters + Update RandomStringGenerator to accept a list of valid characters + Add CharacterPredicates for ASCII letters (uppercase/lowercase) and arabic numerals + Added CaseUtils class with camel case conversion support + RandomStringGenerator should be able to generate a String with a random length + Update commons-lang dependency to version 3.6 + Document that commons-csv should be used in preference to CsvTranslators + NumericEntityUnescaper.options - fix TODO + RandomStringGenerator claims to be immutable, but isn't + Add StrLookup.resourceBundleLookup(ResourceBundle) + Typo in LongestCommonSubsequence#logestCommonSubsequence + + + + WordUtils should use toXxxxCase(int) rather than toXxxxCase(char) + WordUtils.abbreviate support + Putting WordUtils back in to the codebase + Add RandomStringGenerator + RandomStringGenerator: allow users to provide source of randomness + Correct round issue in Jaro Winkler implementation + Similar to LANG-1025, clirr fails site build. + + + + Investigate locale issue in ExtendedMessageFormatTest + Resolve PMD/CMD Violations + Escape HTML characters only once: revert + Fixing the 200 checkstyle errors present in 1.0-beta-1 + Mutable fields should be private + + + + Incorporate suggestions from RC2 into 1.0 release + Naming packages org.apache.commons.text.beta + Upgrading Jacoco for Java 9-ea compatibility. + Refactor EntityArrays to have unmodifiableMaps in leu of String[][] + Prepare site for 1.0 release + Move CvsTranslators out of StringEscapeUtils and make them DRY + Remove WordUtils to be added back in an upcoming 1.X release + Possible attacks through StringEscapeUtils.escapeEcmaScrip better javadoc + Remove RandomStringGenerator to be added back in the 1.1 release + Upgrade from commons-parent version 41 to version 42 + Escape HTML characters only once + Global vs local source of randomness + Fluent API in "RandomStringBuilder" + Fix JaroWinklerDistance in the manner of LUCENE-1297 + Add LCS similarity and distance + Add class to generate random strings + Unfinished class Javadoc for CosineDistance + Consolidating since tags at 1.0, removing deprecated methods + Add a builder to StringEscapeUtils + Add shell/XSI escape/unescape support + LevenshteinDistance reduce memory consumption + Remove org.apache.commons.text.names, for later release than 1.0 + Add Jaccard Index and Jaccard Distance + Move org.apache.commons.lang3.StringEscapeUtils.java into text + Moving from commons-lang, the package org.apache.commons.lang3.text + A more complex Levenshtein distance + Add coveralls and Travis.ci integration + Add alphabet converter + Create Commons Text logo + Improve HumanNameParser + IP clearance for the names package + Write user guide + Work on the string metric, distance, and similarity definitions for the project + Human name parser + Create StringDistanceFrom class that contains a StringMetric and the "left" side string. This would have a method that accepts the "right" side string to test. + Add Cosine Similarity and Cosine Distance + Change (R) StringMetric.compare(CS left, CS right) to "apply" so that it is consistent with BiFunction. + Allow extra information (e.g. Levenshtein threshold) to be stored as (final) fields in the StringMetric instance. + Port Myers algorithm from [collections] + Add Hamming distance + Incorporate String algorithms from Commons Lang + + + + diff --git a/sources/src/changes/release-notes.vm b/sources/src/changes/release-notes.vm new file mode 100644 index 0000000..8639399 --- /dev/null +++ b/sources/src/changes/release-notes.vm @@ -0,0 +1,139 @@ +## Licensed to the Apache Software Foundation (ASF) under one +## or more contributor license agreements. See the NOTICE file +## distributed with this work for additional information +## regarding copyright ownership. The ASF licenses this file +## to you under the Apache License, Version 2.0 (the +## "License"); you may not use this file except in compliance +## with the License. You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, +## software distributed under the License is distributed on an +## "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +## KIND, either express or implied. See the License for the +## specific language governing permissions and limitations +## under the License. +## + +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + + +${project.name} +Version ${version} +Release Notes + + +INTRODUCTION: + +This document contains the release notes for the ${version} version of Apache Commons Text. +Commons Text is a set of utility functions and reusable components for the purpose of processing +and manipulating text that should be of use in a Java environment. + + +$introduction.replaceAll("(? + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sources/src/conf/checkstyle.xml b/sources/src/conf/checkstyle.xml new file mode 100644 index 0000000..f0c3258 --- /dev/null +++ b/sources/src/conf/checkstyle.xml @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sources/src/conf/spotbugs-exclude-filter.xml b/sources/src/conf/spotbugs-exclude-filter.xml new file mode 100644 index 0000000..7798a4f --- /dev/null +++ b/sources/src/conf/spotbugs-exclude-filter.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sources/src/main/java/org/apache/commons/text/AlphabetConverter.java b/sources/src/main/java/org/apache/commons/text/AlphabetConverter.java new file mode 100644 index 0000000..033ede1 --- /dev/null +++ b/sources/src/main/java/org/apache/commons/text/AlphabetConverter.java @@ -0,0 +1,512 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; + +/** + *

+ * Convert from one alphabet to another, with the possibility of leaving certain + * characters unencoded. + *

+ * + *

+ * The target and do not encode languages must be in the Unicode BMP, but the + * source language does not. + *

+ * + *

+ * The encoding will all be of a fixed length, except for the 'do not encode' + * chars, which will be of length 1 + *

+ * + *

Sample usage

+ * + *
+ * Character[] originals;   // a, b, c, d
+ * Character[] encoding;    // 0, 1, d
+ * Character[] doNotEncode; // d
+ *
+ * AlphabetConverter ac = AlphabetConverter.createConverterFromChars(originals,
+ * encoding, doNotEncode);
+ *
+ * ac.encode("a");    // 00
+ * ac.encode("b");    // 01
+ * ac.encode("c");    // 0d
+ * ac.encode("d");    // d
+ * ac.encode("abcd"); // 00010dd
+ * 
+ * + *

+ * #ThreadSafe# AlphabetConverter class methods are thread-safe as they do not + * change internal state. + *

+ * + * @since 1.0 + */ +public final class AlphabetConverter { + + /** + * Arrow constant, used for converting the object into a string. + */ + private static final String ARROW = " -> "; + + /** + * Creates new String that contains just the given code point. + * + * @param i code point + * @return a new string with the new code point + * @see "http://www.oracle.com/us/technologies/java/supplementary-142654.html" + */ + private static String codePointToString(final int i) { + if (Character.charCount(i) == 1) { + return String.valueOf((char) i); + } + return new String(Character.toChars(i)); + } + + /** + * Converts characters to integers. + * + * @param chars array of characters + * @return an equivalent array of integers + */ + private static Integer[] convertCharsToIntegers(final Character[] chars) { + if (ArrayUtils.isEmpty(chars)) { + return ArrayUtils.EMPTY_INTEGER_OBJECT_ARRAY; + } + final Integer[] integers = new Integer[chars.length]; + Arrays.setAll(integers, i -> (int) chars[i]); + return integers; + } + + /** + * Creates an alphabet converter, for converting from the original alphabet, + * to the encoded alphabet, while leaving + * the characters in doNotEncode as they are (if possible). + * + *

Duplicate letters in either original or encoding will be ignored.

+ * + * @param original an array of ints representing the original alphabet in + * code points + * @param encoding an array of ints representing the alphabet to be used for + * encoding, in code points + * @param doNotEncode an array of ints representing the chars to be encoded + * using the original alphabet - every char + * here must appear in both the previous params + * @return The AlphabetConverter + * @throws IllegalArgumentException if an AlphabetConverter cannot be + * constructed + */ + public static AlphabetConverter createConverter( + final Integer[] original, + final Integer[] encoding, + final Integer[] doNotEncode) { + final Set originalCopy = new LinkedHashSet<>(Arrays.asList(original)); + final Set encodingCopy = new LinkedHashSet<>(Arrays.asList(encoding)); + final Set doNotEncodeCopy = new LinkedHashSet<>(Arrays.asList(doNotEncode)); + + final Map originalToEncoded = new LinkedHashMap<>(); + final Map encodedToOriginal = new LinkedHashMap<>(); + final Map doNotEncodeMap = new HashMap<>(); + + final int encodedLetterLength; + + for (final int i : doNotEncodeCopy) { + if (!originalCopy.contains(i)) { + throw new IllegalArgumentException( + "Can not use 'do not encode' list because original " + + "alphabet does not contain '" + + codePointToString(i) + "'"); + } + + if (!encodingCopy.contains(i)) { + throw new IllegalArgumentException( + "Can not use 'do not encode' list because encoding alphabet does not contain '" + + codePointToString(i) + "'"); + } + + doNotEncodeMap.put(i, codePointToString(i)); + } + + if (encodingCopy.size() >= originalCopy.size()) { + encodedLetterLength = 1; + + final Iterator it = encodingCopy.iterator(); + + for (final int originalLetter : originalCopy) { + final String originalLetterAsString = + codePointToString(originalLetter); + + if (doNotEncodeMap.containsKey(originalLetter)) { + originalToEncoded.put(originalLetter, + originalLetterAsString); + encodedToOriginal.put(originalLetterAsString, + originalLetterAsString); + } else { + Integer next = it.next(); + + while (doNotEncodeCopy.contains(next)) { + next = it.next(); + } + + final String encodedLetter = codePointToString(next); + + originalToEncoded.put(originalLetter, encodedLetter); + encodedToOriginal.put(encodedLetter, + originalLetterAsString); + } + } + + return new AlphabetConverter(originalToEncoded, + encodedToOriginal, + encodedLetterLength); + + } + if (encodingCopy.size() - doNotEncodeCopy.size() < 2) { + throw new IllegalArgumentException( + "Must have at least two encoding characters (excluding " + + "those in the 'do not encode' list), but has " + + (encodingCopy.size() - doNotEncodeCopy.size())); + } + // we start with one which is our minimum, and because we do the + // first division outside the loop + int lettersSoFar = 1; + + // the first division takes into account that the doNotEncode + // letters can't be in the leftmost place + int lettersLeft = (originalCopy.size() - doNotEncodeCopy.size()) + / (encodingCopy.size() - doNotEncodeCopy.size()); + + while (lettersLeft / encodingCopy.size() >= 1) { + lettersLeft = lettersLeft / encodingCopy.size(); + lettersSoFar++; + } + + encodedLetterLength = lettersSoFar + 1; + + final AlphabetConverter ac = + new AlphabetConverter(originalToEncoded, + encodedToOriginal, + encodedLetterLength); + + ac.addSingleEncoding(encodedLetterLength, + StringUtils.EMPTY, + encodingCopy, + originalCopy.iterator(), + doNotEncodeMap); + + return ac; + } + + /** + * Creates an alphabet converter, for converting from the original alphabet, + * to the encoded alphabet, while leaving the characters in + * doNotEncode as they are (if possible). + * + *

Duplicate letters in either original or encoding will be ignored.

+ * + * @param original an array of chars representing the original alphabet + * @param encoding an array of chars representing the alphabet to be used + * for encoding + * @param doNotEncode an array of chars to be encoded using the original + * alphabet - every char here must appear in + * both the previous params + * @return The AlphabetConverter + * @throws IllegalArgumentException if an AlphabetConverter cannot be + * constructed + */ + public static AlphabetConverter createConverterFromChars( + final Character[] original, + final Character[] encoding, + final Character[] doNotEncode) { + return AlphabetConverter.createConverter( + convertCharsToIntegers(original), + convertCharsToIntegers(encoding), + convertCharsToIntegers(doNotEncode)); + } + + /** + * Creates a new converter from a map. + * + * @param originalToEncoded a map returned from getOriginalToEncoded() + * @return The reconstructed AlphabetConverter + * @see AlphabetConverter#getOriginalToEncoded() + */ + public static AlphabetConverter createConverterFromMap(final Map originalToEncoded) { + final Map unmodifiableOriginalToEncoded = Collections.unmodifiableMap(originalToEncoded); + final Map encodedToOriginal = new LinkedHashMap<>(); + + int encodedLetterLength = 1; + + for (final Entry e : unmodifiableOriginalToEncoded.entrySet()) { + final String originalAsString = codePointToString(e.getKey()); + encodedToOriginal.put(e.getValue(), originalAsString); + + if (e.getValue().length() > encodedLetterLength) { + encodedLetterLength = e.getValue().length(); + } + } + + return new AlphabetConverter(unmodifiableOriginalToEncoded, encodedToOriginal, encodedLetterLength); + } + + /** + * Original string to be encoded. + */ + private final Map originalToEncoded; + + /** + * Encoding alphabet. + */ + private final Map encodedToOriginal; + + /** + * Length of the encoded letter. + */ + private final int encodedLetterLength; + + /** + * Hidden constructor for alphabet converter. Used by static helper methods. + * + * @param originalToEncoded original string to be encoded + * @param encodedToOriginal encoding alphabet + * @param encodedLetterLength length of the encoded letter + */ + private AlphabetConverter(final Map originalToEncoded, + final Map encodedToOriginal, + final int encodedLetterLength) { + + this.originalToEncoded = originalToEncoded; + this.encodedToOriginal = encodedToOriginal; + this.encodedLetterLength = encodedLetterLength; + } + + /** + * Recursive method used when creating encoder/decoder. + * + * @param level at which point it should add a single encoding + * @param currentEncoding current encoding + * @param encoding letters encoding + * @param originals original values + * @param doNotEncodeMap map of values that should not be encoded + */ + private void addSingleEncoding(final int level, + final String currentEncoding, + final Collection encoding, + final Iterator originals, + final Map doNotEncodeMap) { + + if (level > 0) { + for (final int encodingLetter : encoding) { + if (!originals.hasNext()) { + return; // done encoding all the original alphabet + } + // this skips the doNotEncode chars if they are in the + // leftmost place + if (level != encodedLetterLength + || !doNotEncodeMap.containsKey(encodingLetter)) { + addSingleEncoding(level - 1, + currentEncoding + + codePointToString(encodingLetter), + encoding, + originals, + doNotEncodeMap + ); + } + } + } else { + Integer next = originals.next(); + + while (doNotEncodeMap.containsKey(next)) { + final String originalLetterAsString = codePointToString(next); + + originalToEncoded.put(next, originalLetterAsString); + encodedToOriginal.put(originalLetterAsString, + originalLetterAsString); + + if (!originals.hasNext()) { + return; + } + + next = originals.next(); + } + + final String originalLetterAsString = codePointToString(next); + + originalToEncoded.put(next, currentEncoding); + encodedToOriginal.put(currentEncoding, originalLetterAsString); + } + } + + /** + * Decodes a given string. + * + * @param encoded a string that has been encoded using this + * AlphabetConverter + * @return The decoded string, {@code null} if the given string is null + * @throws UnsupportedEncodingException if unexpected characters that + * cannot be handled are encountered + */ + public String decode(final String encoded) + throws UnsupportedEncodingException { + if (encoded == null) { + return null; + } + + final StringBuilder result = new StringBuilder(); + + for (int j = 0; j < encoded.length();) { + final int i = encoded.codePointAt(j); + final String s = codePointToString(i); + + if (s.equals(originalToEncoded.get(i))) { + result.append(s); + j++; // because we do not encode in Unicode extended the + // length of each encoded char is 1 + } else { + if (j + encodedLetterLength > encoded.length()) { + throw new UnsupportedEncodingException("Unexpected end " + + "of string while decoding " + encoded); + } + final String nextGroup = encoded.substring(j, + j + encodedLetterLength); + final String next = encodedToOriginal.get(nextGroup); + if (next == null) { + throw new UnsupportedEncodingException( + "Unexpected string without decoding (" + + nextGroup + ") in " + encoded); + } + result.append(next); + j += encodedLetterLength; + } + } + + return result.toString(); + } + + /** + * Encodes a given string. + * + * @param original the string to be encoded + * @return The encoded string, {@code null} if the given string is null + * @throws UnsupportedEncodingException if chars that are not supported are + * encountered + */ + public String encode(final String original) + throws UnsupportedEncodingException { + if (original == null) { + return null; + } + + final StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < original.length();) { + final int codePoint = original.codePointAt(i); + + final String nextLetter = originalToEncoded.get(codePoint); + + if (nextLetter == null) { + throw new UnsupportedEncodingException( + "Couldn't find encoding for '" + + codePointToString(codePoint) + + "' in " + + original + ); + } + + sb.append(nextLetter); + + i += Character.charCount(codePoint); + } + + return sb.toString(); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (obj == this) { + return true; + } + if (!(obj instanceof AlphabetConverter)) { + return false; + } + final AlphabetConverter other = (AlphabetConverter) obj; + return originalToEncoded.equals(other.originalToEncoded) + && encodedToOriginal.equals(other.encodedToOriginal) + && encodedLetterLength == other.encodedLetterLength; + } + + /** + * Gets the length of characters in the encoded alphabet that are necessary + * for each character in the original + * alphabet. + * + * @return The length of the encoded char + */ + public int getEncodedCharLength() { + return encodedLetterLength; + } + + /** + * Gets the mapping from integer code point of source language to encoded + * string. Use to reconstruct converter from + * serialized map. + * + * @return The original map + */ + public Map getOriginalToEncoded() { + return Collections.unmodifiableMap(originalToEncoded); + } + + @Override + public int hashCode() { + return Objects.hash(originalToEncoded, + encodedToOriginal, + encodedLetterLength); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + // @formatter:off + originalToEncoded.forEach((k, v) -> + sb.append(codePointToString(k)) + .append(ARROW) + .append(k) + .append(System.lineSeparator())); + // @formatter:on + return sb.toString(); + } +} diff --git a/sources/src/main/java/org/apache/commons/text/Builder.java b/sources/src/main/java/org/apache/commons/text/Builder.java new file mode 100644 index 0000000..ca4c2d3 --- /dev/null +++ b/sources/src/main/java/org/apache/commons/text/Builder.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +/** + * The Builder interface is designed to designate a class as a builder + * object in the Builder design pattern. Builders are capable of creating and + * configuring objects or results that normally take multiple steps to construct + * or are very complex to derive. + * + *

+ * The builder interface defines a single method, {@link #build()}, that + * classes must implement. The result of this method should be the final + * configured object or result after all building operations are performed. + *

+ * + *

+ * It is a recommended practice that the methods supplied to configure the + * object or result being built return a reference to {@code this} so that + * method calls can be chained together. + *

+ * + *

+ * Example Builder: + *

+ *

+ * class FontBuilder implements Builder<Font> {
+ *     private Font font;
+ *
+ *     public FontBuilder(String fontName) {
+ *         this.font = new Font(fontName, Font.PLAIN, 12);
+ *     }
+ *
+ *     public FontBuilder bold() {
+ *         this.font = this.font.deriveFont(Font.BOLD);
+ *         return this; // Reference returned so calls can be chained
+ *     }
+ *
+ *     public FontBuilder size(float pointSize) {
+ *         this.font = this.font.deriveFont(pointSize);
+ *         return this; // Reference returned so calls can be chained
+ *     }
+ *
+ *     // Other Font construction methods
+ *
+ *     public Font build() {
+ *         return this.font;
+ *     }
+ * }
+ * 
+ * + * Example Builder Usage: + *

+ * Font bold14ptSansSerifFont = new FontBuilder(Font.SANS_SERIF).bold()
+ *                                                              .size(14.0f)
+ *                                                              .build();
+ * 
+ * + * + * @param the type of object that the builder will construct or compute. + * @since 1.0 + */ +public interface Builder { + + /** + * Returns a reference to the object being constructed or result being + * calculated by the builder. + * + * @return The object constructed or result calculated by the builder. + */ + T build(); +} diff --git a/sources/src/main/java/org/apache/commons/text/CaseUtils.java b/sources/src/main/java/org/apache/commons/text/CaseUtils.java new file mode 100644 index 0000000..c3a7591 --- /dev/null +++ b/sources/src/main/java/org/apache/commons/text/CaseUtils.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import java.util.HashSet; +import java.util.Set; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; + +/** + * Case manipulation operations on Strings that contain words. + * + *

This class tries to handle {@code null} input gracefully. + * An exception will not be thrown for a {@code null} input. + * Each method documents its behavior in more detail.

+ * + * @since 1.2 + */ +public class CaseUtils { + + /** + * Converts all the delimiter separated words in a String into camelCase, + * that is each word is made up of a title case character and then a series of + * lowercase characters. + * + *

The delimiters represent a set of characters understood to separate words. + * The first non-delimiter character after a delimiter will be capitalized. The first String + * character may or may not be capitalized and it's determined by the user input for capitalizeFirstLetter + * variable.

+ * + *

A {@code null} input String returns {@code null}.

+ * + *

A input string with only delimiter characters returns {@code ""}.

+ * + * Capitalization uses the Unicode title case, normally equivalent to + * upper case and cannot perform locale-sensitive mappings. + * + *
+     * CaseUtils.toCamelCase(null, false)                                 = null
+     * CaseUtils.toCamelCase("", false, *)                                = ""
+     * CaseUtils.toCamelCase(*, false, null)                              = *
+     * CaseUtils.toCamelCase(*, true, new char[0])                        = *
+     * CaseUtils.toCamelCase("To.Camel.Case", false, new char[]{'.'})     = "toCamelCase"
+     * CaseUtils.toCamelCase(" to @ Camel case", true, new char[]{'@'})   = "ToCamelCase"
+     * CaseUtils.toCamelCase(" @to @ Camel case", false, new char[]{'@'}) = "toCamelCase"
+     * CaseUtils.toCamelCase(" @", false, new char[]{'@'})                = ""
+     * 
+ * + * @param str the String to be converted to camelCase, may be null + * @param capitalizeFirstLetter boolean that determines if the first character of first word should be title case. + * @param delimiters set of characters to determine capitalization, null and/or empty array means whitespace + * @return camelCase of String, {@code null} if null String input + */ + public static String toCamelCase(String str, final boolean capitalizeFirstLetter, final char... delimiters) { + if (StringUtils.isEmpty(str)) { + return str; + } + str = str.toLowerCase(); + final int strLen = str.length(); + final int[] newCodePoints = new int[strLen]; + int outOffset = 0; + final Set delimiterSet = toDelimiterSet(delimiters); + boolean capitalizeNext = capitalizeFirstLetter; + for (int index = 0; index < strLen;) { + final int codePoint = str.codePointAt(index); + + if (delimiterSet.contains(codePoint)) { + capitalizeNext = outOffset != 0; + index += Character.charCount(codePoint); + } else if (capitalizeNext || outOffset == 0 && capitalizeFirstLetter) { + final int titleCaseCodePoint = Character.toTitleCase(codePoint); + newCodePoints[outOffset++] = titleCaseCodePoint; + index += Character.charCount(titleCaseCodePoint); + capitalizeNext = false; + } else { + newCodePoints[outOffset++] = codePoint; + index += Character.charCount(codePoint); + } + } + + return new String(newCodePoints, 0, outOffset); + } + + /** + * Converts an array of delimiters to a hash set of code points. Code point of space(32) is added + * as the default value. The generated hash set provides O(1) lookup time. + * + * @param delimiters set of characters to determine capitalization, null means whitespace + * @return Set + */ + private static Set toDelimiterSet(final char[] delimiters) { + final Set delimiterHashSet = new HashSet<>(); + delimiterHashSet.add(Character.codePointAt(new char[]{' '}, 0)); + if (ArrayUtils.isEmpty(delimiters)) { + return delimiterHashSet; + } + + for (int index = 0; index < delimiters.length; index++) { + delimiterHashSet.add(Character.codePointAt(delimiters, index)); + } + return delimiterHashSet; + } + + /** + * {@code CaseUtils} instances should NOT be constructed in + * standard programming. Instead, the class should be used as + * {@code CaseUtils.toCamelCase("foo bar", true, new char[]{'-'});}. + * + *

This constructor is public to permit tools that require a JavaBean + * instance to operate.

+ */ + public CaseUtils() { + } +} + diff --git a/sources/src/main/java/org/apache/commons/text/CharacterPredicate.java b/sources/src/main/java/org/apache/commons/text/CharacterPredicate.java new file mode 100644 index 0000000..164432b --- /dev/null +++ b/sources/src/main/java/org/apache/commons/text/CharacterPredicate.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +/** + * A predicate for selecting code points. Implementations of this interface must + * be thread safe. + * + * @since 1.0 + */ +public interface CharacterPredicate { + + /** + * Tests the code point with this predicate. + * + * @param codePoint + * the code point to test + * @return {@code true} if the code point matches the predicate, + * {@code false} otherwise + * @since 1.0 + */ + boolean test(int codePoint); +} diff --git a/sources/src/main/java/org/apache/commons/text/CharacterPredicates.java b/sources/src/main/java/org/apache/commons/text/CharacterPredicates.java new file mode 100644 index 0000000..c174b74 --- /dev/null +++ b/sources/src/main/java/org/apache/commons/text/CharacterPredicates.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +/** + * Commonly used implementations of {@link CharacterPredicate}. Per the interface + * requirements, all implementations are thread safe. + * + * @since 1.0 + */ +public enum CharacterPredicates implements CharacterPredicate { + + /** + * Tests code points against {@link Character#isLetter(int)}. + * + * @since 1.0 + */ + LETTERS { + @Override + public boolean test(final int codePoint) { + return Character.isLetter(codePoint); + } + }, + + /** + * Tests code points against {@link Character#isDigit(int)}. + * + * @since 1.0 + */ + DIGITS { + @Override + public boolean test(final int codePoint) { + return Character.isDigit(codePoint); + } + }, + + /** + * Tests if the code points represents a number between 0 and 9. + * + * @since 1.2 + */ + ARABIC_NUMERALS { + @Override + public boolean test(final int codePoint) { + return codePoint >= '0' && codePoint <= '9'; + } + }, + + /** + * Tests if the code points represents a letter between a and z. + * + * @since 1.2 + */ + ASCII_LOWERCASE_LETTERS { + @Override + public boolean test(final int codePoint) { + return codePoint >= 'a' && codePoint <= 'z'; + } + }, + + /** + * Tests if the code points represents a letter between A and Z. + * + * @since 1.2 + */ + ASCII_UPPERCASE_LETTERS { + @Override + public boolean test(final int codePoint) { + return codePoint >= 'A' && codePoint <= 'Z'; + } + }, + + /** + * Tests if the code points represents a letter between a and Z. + * + * @since 1.2 + */ + ASCII_LETTERS { + @Override + public boolean test(final int codePoint) { + return ASCII_LOWERCASE_LETTERS.test(codePoint) || ASCII_UPPERCASE_LETTERS.test(codePoint); + } + }, + + /** + * Tests if the code points represents a letter between a and Z or a number between 0 and 9. + * + * @since 1.2 + */ + ASCII_ALPHA_NUMERALS { + @Override + public boolean test(final int codePoint) { + return ASCII_LOWERCASE_LETTERS.test(codePoint) || ASCII_UPPERCASE_LETTERS.test(codePoint) + || ARABIC_NUMERALS.test(codePoint); + } + } +} diff --git a/sources/src/main/java/org/apache/commons/text/CompositeFormat.java b/sources/src/main/java/org/apache/commons/text/CompositeFormat.java new file mode 100644 index 0000000..24328ff --- /dev/null +++ b/sources/src/main/java/org/apache/commons/text/CompositeFormat.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import java.text.FieldPosition; +import java.text.Format; +import java.text.ParseException; +import java.text.ParsePosition; + +/** + * Formats using one formatter and parses using a different formatter. An + * example of use for this would be a webapp where data is taken in one way and + * stored in a database another way. + * + * @since 1.0 + */ +public class CompositeFormat extends Format { + + /** + * Required for serialization support. + * + * @see java.io.Serializable + */ + private static final long serialVersionUID = -4329119827877627683L; + + /** The parser to use. */ + private final Format parser; + + /** The formatter to use. */ + private final Format formatter; + + /** + * Constructs a format that points its parseObject method to one implementation + * and its format method to another. + * + * @param parser implementation + * @param formatter implementation + */ + public CompositeFormat(final Format parser, final Format formatter) { + this.parser = parser; + this.formatter = formatter; + } + + /** + * Formats the input. + * + * @param obj the object to format + * @param toAppendTo the {@link StringBuffer} to append to + * @param pos the FieldPosition to use (or ignore). + * @return {@code toAppendTo} + * @see Format#format(Object, StringBuffer, FieldPosition) + */ + @Override // Therefore has to use StringBuffer + public StringBuffer format(final Object obj, final StringBuffer toAppendTo, + final FieldPosition pos) { + return formatter.format(obj, toAppendTo, pos); + } + + /** + * Gets the parser Format implementation. + * + * @return formatter Format implementation + */ + public Format getFormatter() { + return this.formatter; + } + + /** + * Gets the parser Format implementation. + * + * @return parser Format implementation + */ + public Format getParser() { + return this.parser; + } + + /** + * Parses the input. + * + * @param source the String source + * @param pos the ParsePosition containing the position to parse from, will + * be updated according to parsing success (index) or failure + * (error index) + * @return The parsed Object + * @see Format#parseObject(String, ParsePosition) + */ + @Override + public Object parseObject(final String source, final ParsePosition pos) { + return parser.parseObject(source, pos); + } + + /** + * Parses and then reformats a String. + * + * @param input String to reformat + * @return A reformatted String + * @throws ParseException thrown by parseObject(String) call + */ + public String reformat(final String input) throws ParseException { + return format(parseObject(input)); + } + +} diff --git a/sources/src/main/java/org/apache/commons/text/ExtendedMessageFormat.java b/sources/src/main/java/org/apache/commons/text/ExtendedMessageFormat.java new file mode 100644 index 0000000..1e14940 --- /dev/null +++ b/sources/src/main/java/org/apache/commons/text/ExtendedMessageFormat.java @@ -0,0 +1,573 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import java.text.Format; +import java.text.MessageFormat; +import java.text.ParsePosition; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Locale.Category; +import java.util.Map; +import java.util.Objects; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.matcher.StringMatcherFactory; + +/** + * Extends {@code java.text.MessageFormat} to allow pluggable/additional formatting + * options for embedded format elements. Client code should specify a registry + * of {@code FormatFactory} instances associated with {@code String} + * format names. This registry will be consulted when the format elements are + * parsed from the message pattern. In this way custom patterns can be specified, + * and the formats supported by {@code java.text.MessageFormat} can be overridden + * at the format and/or format style level (see MessageFormat). A "format element" + * embedded in the message pattern is specified (()? signifies optionality):
+ * {@code {}argument-number({@code ,}format-name + * ({@code ,}format-style)?)?{@code }} + * + *

+ * format-name and format-style values are trimmed of surrounding whitespace + * in the manner of {@code java.text.MessageFormat}. If format-name denotes + * {@code FormatFactory formatFactoryInstance} in {@code registry}, a {@code Format} + * matching format-name and format-style is requested from + * {@code formatFactoryInstance}. If this is successful, the {@code Format} + * found is used for this format element. + *

+ * + *

NOTICE: The various subformat mutator methods are considered unnecessary; they exist on the parent + * class to allow the type of customization which it is the job of this class to provide in + * a configurable fashion. These methods have thus been disabled and will throw + * {@code UnsupportedOperationException} if called. + *

+ * + *

Limitations inherited from {@code java.text.MessageFormat}:

+ *
    + *
  • When using "choice" subformats, support for nested formatting instructions is limited + * to that provided by the base class.
  • + *
  • Thread-safety of {@code Format}s, including {@code MessageFormat} and thus + * {@code ExtendedMessageFormat}, is not guaranteed.
  • + *
+ * + * @since 1.0 + */ +public class ExtendedMessageFormat extends MessageFormat { + + /** + * Serializable Object. + */ + private static final long serialVersionUID = -2362048321261811743L; + + /** + * Our initial seed value for calculating hashes. + */ + private static final int HASH_SEED = 31; + + /** + * The empty string. + */ + private static final String DUMMY_PATTERN = StringUtils.EMPTY; + + /** + * A comma. + */ + private static final char START_FMT = ','; + + /** + * A right side squiggly brace. + */ + private static final char END_FE = '}'; + + /** + * A left side squiggly brace. + */ + private static final char START_FE = '{'; + + /** + * A properly escaped character representing a single quote. + */ + private static final char QUOTE = '\''; + + /** + * To pattern string. + */ + private String toPattern; + + /** + * Our registry of FormatFactory. + */ + private final Map registry; + + /** + * Constructs a new ExtendedMessageFormat for the default locale. + * + * @param pattern the pattern to use, not null + * @throws IllegalArgumentException in case of a bad pattern. + */ + public ExtendedMessageFormat(final String pattern) { + this(pattern, Locale.getDefault(Category.FORMAT)); + } + + /** + * Constructs a new ExtendedMessageFormat. + * + * @param pattern the pattern to use, not null + * @param locale the locale to use, not null + * @throws IllegalArgumentException in case of a bad pattern. + */ + public ExtendedMessageFormat(final String pattern, final Locale locale) { + this(pattern, locale, null); + } + + /** + * Constructs a new ExtendedMessageFormat. + * + * @param pattern the pattern to use, not null + * @param locale the locale to use, not null + * @param registry the registry of format factories, may be null + * @throws IllegalArgumentException in case of a bad pattern. + */ + public ExtendedMessageFormat(final String pattern, + final Locale locale, + final Map registry) { + super(DUMMY_PATTERN); + setLocale(locale); + this.registry = registry != null + ? Collections.unmodifiableMap(new HashMap<>(registry)) + : null; + applyPattern(pattern); + } + + /** + * Constructs a new ExtendedMessageFormat for the default locale. + * + * @param pattern the pattern to use, not null + * @param registry the registry of format factories, may be null + * @throws IllegalArgumentException in case of a bad pattern. + */ + public ExtendedMessageFormat(final String pattern, + final Map registry) { + this(pattern, Locale.getDefault(Category.FORMAT), registry); + } + + /** + * Consumes a quoted string, adding it to {@code appendTo} if + * specified. + * + * @param pattern pattern to parse + * @param pos current parse position + * @param appendTo optional StringBuilder to append + */ + private void appendQuotedString(final String pattern, final ParsePosition pos, + final StringBuilder appendTo) { + assert pattern.toCharArray()[pos.getIndex()] == QUOTE + : "Quoted string must start with quote character"; + + // handle quote character at the beginning of the string + if (appendTo != null) { + appendTo.append(QUOTE); + } + next(pos); + + final int start = pos.getIndex(); + final char[] c = pattern.toCharArray(); + for (int i = pos.getIndex(); i < pattern.length(); i++) { + switch (c[pos.getIndex()]) { + case QUOTE: + next(pos); + if (appendTo != null) { + appendTo.append(c, start, pos.getIndex() - start); + } + return; + default: + next(pos); + } + } + throw new IllegalArgumentException( + "Unterminated quoted string at position " + start); + } + + /** + * Applies the specified pattern. + * + * @param pattern String + */ + @Override + public final void applyPattern(final String pattern) { + if (registry == null) { + super.applyPattern(pattern); + toPattern = super.toPattern(); + return; + } + final ArrayList foundFormats = new ArrayList<>(); + final ArrayList foundDescriptions = new ArrayList<>(); + final StringBuilder stripCustom = new StringBuilder(pattern.length()); + + final ParsePosition pos = new ParsePosition(0); + final char[] c = pattern.toCharArray(); + int fmtCount = 0; + while (pos.getIndex() < pattern.length()) { + switch (c[pos.getIndex()]) { + case QUOTE: + appendQuotedString(pattern, pos, stripCustom); + break; + case START_FE: + fmtCount++; + seekNonWs(pattern, pos); + final int start = pos.getIndex(); + final int index = readArgumentIndex(pattern, next(pos)); + stripCustom.append(START_FE).append(index); + seekNonWs(pattern, pos); + Format format = null; + String formatDescription = null; + if (c[pos.getIndex()] == START_FMT) { + formatDescription = parseFormatDescription(pattern, + next(pos)); + format = getFormat(formatDescription); + if (format == null) { + stripCustom.append(START_FMT).append(formatDescription); + } + } + foundFormats.add(format); + foundDescriptions.add(format == null ? null : formatDescription); + if (foundFormats.size() != fmtCount) { + throw new IllegalArgumentException("The validated expression is false"); + } + if (foundDescriptions.size() != fmtCount) { + throw new IllegalArgumentException("The validated expression is false"); + } + if (c[pos.getIndex()] != END_FE) { + throw new IllegalArgumentException( + "Unreadable format element at position " + start); + } + //$FALL-THROUGH$ + default: + stripCustom.append(c[pos.getIndex()]); + next(pos); + } + } + super.applyPattern(stripCustom.toString()); + toPattern = insertFormats(super.toPattern(), foundDescriptions); + if (containsElements(foundFormats)) { + final Format[] origFormats = getFormats(); + // only loop over what we know we have, as MessageFormat on Java 1.3 + // seems to provide an extra format element: + int i = 0; + for (final Format f : foundFormats) { + if (f != null) { + origFormats[i] = f; + } + i++; + } + super.setFormats(origFormats); + } + } + + /** + * Tests whether the specified Collection contains non-null elements. + * @param coll to check + * @return {@code true} if some Object was found, {@code false} otherwise. + */ + private boolean containsElements(final Collection coll) { + if (coll == null || coll.isEmpty()) { + return false; + } + return coll.stream().anyMatch(Objects::nonNull); + } + + /** + * Tests if this extended message format is equal to another object. + * + * @param obj the object to compare to + * @return true if this object equals the other, otherwise false + */ + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + if (obj == null) { + return false; + } + if (!Objects.equals(getClass(), obj.getClass())) { + return false; + } + final ExtendedMessageFormat rhs = (ExtendedMessageFormat) obj; + if (!Objects.equals(toPattern, rhs.toPattern)) { + return false; + } + if (!super.equals(obj)) { + return false; + } + return Objects.equals(registry, rhs.registry); + } + + /** + * Gets a custom format from a format description. + * + * @param desc String + * @return Format + */ + private Format getFormat(final String desc) { + if (registry != null) { + String name = desc; + String args = null; + final int i = desc.indexOf(START_FMT); + if (i > 0) { + name = desc.substring(0, i).trim(); + args = desc.substring(i + 1).trim(); + } + final FormatFactory factory = registry.get(name); + if (factory != null) { + return factory.getFormat(name, args, getLocale()); + } + } + return null; + } + + /** + * Consumes quoted string only. + * + * @param pattern pattern to parse + * @param pos current parse position + */ + private void getQuotedString(final String pattern, final ParsePosition pos) { + appendQuotedString(pattern, pos, null); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + int result = super.hashCode(); + result = HASH_SEED * result + Objects.hashCode(registry); + result = HASH_SEED * result + Objects.hashCode(toPattern); + return result; + } + + /** + * Inserts formats back into the pattern for toPattern() support. + * + * @param pattern source + * @param customPatterns The custom patterns to re-insert, if any + * @return full pattern + */ + private String insertFormats(final String pattern, final ArrayList customPatterns) { + if (!containsElements(customPatterns)) { + return pattern; + } + final StringBuilder sb = new StringBuilder(pattern.length() * 2); + final ParsePosition pos = new ParsePosition(0); + int fe = -1; + int depth = 0; + while (pos.getIndex() < pattern.length()) { + final char c = pattern.charAt(pos.getIndex()); + switch (c) { + case QUOTE: + appendQuotedString(pattern, pos, sb); + break; + case START_FE: + depth++; + sb.append(START_FE).append(readArgumentIndex(pattern, next(pos))); + // do not look for custom patterns when they are embedded, e.g. in a choice + if (depth == 1) { + fe++; + final String customPattern = customPatterns.get(fe); + if (customPattern != null) { + sb.append(START_FMT).append(customPattern); + } + } + break; + case END_FE: + depth--; + //$FALL-THROUGH$ + default: + sb.append(c); + next(pos); + } + } + return sb.toString(); + } + + /** + * Advances parse position by 1. + * + * @param pos ParsePosition + * @return {@code pos} + */ + private ParsePosition next(final ParsePosition pos) { + pos.setIndex(pos.getIndex() + 1); + return pos; + } + + /** + * Parses the format component of a format element. + * + * @param pattern string to parse + * @param pos current parse position + * @return Format description String + */ + private String parseFormatDescription(final String pattern, final ParsePosition pos) { + final int start = pos.getIndex(); + seekNonWs(pattern, pos); + final int text = pos.getIndex(); + int depth = 1; + while (pos.getIndex() < pattern.length()) { + switch (pattern.charAt(pos.getIndex())) { + case START_FE: + depth++; + next(pos); + break; + case END_FE: + depth--; + if (depth == 0) { + return pattern.substring(text, pos.getIndex()); + } + next(pos); + break; + case QUOTE: + getQuotedString(pattern, pos); + break; + default: + next(pos); + break; + } + } + throw new IllegalArgumentException( + "Unterminated format element at position " + start); + } + + /** + * Reads the argument index from the current format element. + * + * @param pattern pattern to parse + * @param pos current parse position + * @return argument index + */ + private int readArgumentIndex(final String pattern, final ParsePosition pos) { + final int start = pos.getIndex(); + seekNonWs(pattern, pos); + final StringBuilder result = new StringBuilder(); + boolean error = false; + for (; !error && pos.getIndex() < pattern.length(); next(pos)) { + char c = pattern.charAt(pos.getIndex()); + if (Character.isWhitespace(c)) { + seekNonWs(pattern, pos); + c = pattern.charAt(pos.getIndex()); + if (c != START_FMT && c != END_FE) { + error = true; + continue; + } + } + if ((c == START_FMT || c == END_FE) && result.length() > 0) { + try { + return Integer.parseInt(result.toString()); + } catch (final NumberFormatException e) { // NOPMD + // we've already ensured only digits, so unless something + // outlandishly large was specified we should be okay. + } + } + error = !Character.isDigit(c); + result.append(c); + } + if (error) { + throw new IllegalArgumentException( + "Invalid format argument index at position " + start + ": " + + pattern.substring(start, pos.getIndex())); + } + throw new IllegalArgumentException( + "Unterminated format element at position " + start); + } + + /** + * Consumes whitespace from the current parse position. + * + * @param pattern String to read + * @param pos current position + */ + private void seekNonWs(final String pattern, final ParsePosition pos) { + int len = 0; + final char[] buffer = pattern.toCharArray(); + do { + len = StringMatcherFactory.INSTANCE.splitMatcher().isMatch(buffer, pos.getIndex(), 0, buffer.length); + pos.setIndex(pos.getIndex() + len); + } while (len > 0 && pos.getIndex() < pattern.length()); + } + + /** + * Throws UnsupportedOperationException - see class Javadoc for details. + * + * @param formatElementIndex format element index + * @param newFormat the new format + * @throws UnsupportedOperationException always thrown since this isn't + * supported by ExtendMessageFormat + */ + @Override + public void setFormat(final int formatElementIndex, final Format newFormat) { + throw new UnsupportedOperationException(); + } + + /** + * Throws UnsupportedOperationException - see class Javadoc for details. + * + * @param argumentIndex argument index + * @param newFormat the new format + * @throws UnsupportedOperationException always thrown since this isn't + * supported by ExtendMessageFormat + */ + @Override + public void setFormatByArgumentIndex(final int argumentIndex, + final Format newFormat) { + throw new UnsupportedOperationException(); + } + + /** + * Throws UnsupportedOperationException - see class Javadoc for details. + * + * @param newFormats new formats + * @throws UnsupportedOperationException always thrown since this isn't + * supported by ExtendMessageFormat + */ + @Override + public void setFormats(final Format[] newFormats) { + throw new UnsupportedOperationException(); + } + + /** + * Throws UnsupportedOperationException - see class Javadoc for details. + * + * @param newFormats new formats + * @throws UnsupportedOperationException always thrown since this isn't + * supported by ExtendMessageFormat + */ + @Override + public void setFormatsByArgumentIndex(final Format[] newFormats) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public String toPattern() { + return toPattern; + } +} diff --git a/sources/src/main/java/org/apache/commons/text/FormatFactory.java b/sources/src/main/java/org/apache/commons/text/FormatFactory.java new file mode 100644 index 0000000..dcf0e22 --- /dev/null +++ b/sources/src/main/java/org/apache/commons/text/FormatFactory.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import java.text.Format; +import java.util.Locale; + +/** + * Format factory. + * + * @since 1.0 + */ +public interface FormatFactory { + + /** + * Gets or creates a format instance. + * + * @param name The format type name + * @param arguments Arguments used to create the format instance. This allows the + * {@code FormatFactory} to implement the "format style" + * concept from {@code java.text.MessageFormat}. + * @param locale The locale, may be null + * @return The format instance + */ + Format getFormat(String name, String arguments, Locale locale); + +} diff --git a/sources/src/main/java/org/apache/commons/text/FormattableUtils.java b/sources/src/main/java/org/apache/commons/text/FormattableUtils.java new file mode 100644 index 0000000..525b76a --- /dev/null +++ b/sources/src/main/java/org/apache/commons/text/FormattableUtils.java @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import static java.util.FormattableFlags.LEFT_JUSTIFY; + +import java.util.Formattable; +import java.util.Formatter; + +import org.apache.commons.lang3.StringUtils; + +/** + * Provides utilities for working with the {@code Formattable} interface. + * + *

The {@link Formattable} interface provides basic control over formatting + * when using a {@code Formatter}. It is primarily concerned with numeric precision + * and padding, and is not designed to allow generalised alternate formats.

+ * + * @since 1.0 + */ +public class FormattableUtils { + + /** + * A format that simply outputs the value as a string. + */ + private static final String SIMPLEST_FORMAT = "%s"; + + /** + * Handles the common {@code Formattable} operations of truncate-pad-append, + * with no ellipsis on precision overflow, and padding width underflow with + * spaces. + * + * @param seq the string to handle, not null + * @param formatter the destination formatter, not null + * @param flags the flags for formatting, see {@code Formattable} + * @param width the width of the output, see {@code Formattable} + * @param precision the precision of the output, see {@code Formattable} + * @return The {@code formatter} instance, not null + */ + public static Formatter append(final CharSequence seq, final Formatter formatter, final int flags, final int width, + final int precision) { + return append(seq, formatter, flags, width, precision, ' ', null); + } + + /** + * Handles the common {@link Formattable} operations of truncate-pad-append, + * with no ellipsis on precision overflow. + * + * @param seq the string to handle, not null + * @param formatter the destination formatter, not null + * @param flags the flags for formatting, see {@code Formattable} + * @param width the width of the output, see {@code Formattable} + * @param precision the precision of the output, see {@code Formattable} + * @param padChar the pad character to use + * @return The {@code formatter} instance, not null + */ + public static Formatter append(final CharSequence seq, final Formatter formatter, final int flags, final int width, + final int precision, final char padChar) { + return append(seq, formatter, flags, width, precision, padChar, null); + } + + /** + * Handles the common {@link Formattable} operations of truncate-pad-append. + * + * @param seq the string to handle, not null + * @param formatter the destination formatter, not null + * @param flags the flags for formatting, see {@code Formattable} + * @param width the width of the output, see {@code Formattable} + * @param precision the precision of the output, see {@code Formattable} + * @param padChar the pad character to use + * @param truncateEllipsis the ellipsis to use when precision dictates truncation, null or + * empty causes a hard truncation + * @return The {@code formatter} instance, not null + * @throws IllegalArgumentException if {@code ellipsis.length() > precision}, + * given that {@code ellipsis} is not null and {@code precision >= 0} + */ + public static Formatter append(final CharSequence seq, final Formatter formatter, final int flags, final int width, + final int precision, final char padChar, final CharSequence truncateEllipsis) { + if (!(truncateEllipsis == null || precision < 0 || truncateEllipsis.length() <= precision)) { + throw new IllegalArgumentException( + String.format("Specified ellipsis '%s' exceeds precision of %s", + truncateEllipsis, + precision)); + } + final StringBuilder buf = new StringBuilder(seq); + if (precision >= 0 && precision < seq.length()) { + final CharSequence ellipsis; + if (truncateEllipsis == null) { + ellipsis = StringUtils.EMPTY; + } else { + ellipsis = truncateEllipsis; + } + buf.replace(precision - ellipsis.length(), seq.length(), ellipsis.toString()); + } + final boolean leftJustify = (flags & LEFT_JUSTIFY) == LEFT_JUSTIFY; + for (int i = buf.length(); i < width; i++) { + buf.insert(leftJustify ? i : 0, padChar); + } + formatter.format(buf.toString()); + return formatter; + } + + /** + * Handles the common {@link Formattable} operations of truncate-pad-append, + * padding width underflow with spaces. + * + * @param seq the string to handle, not null + * @param formatter the destination formatter, not null + * @param flags the flags for formatting, see {@code Formattable} + * @param width the width of the output, see {@code Formattable} + * @param precision the precision of the output, see {@code Formattable} + * @param ellipsis the ellipsis to use when precision dictates truncation, null or + * empty causes a hard truncation + * @return The {@code formatter} instance, not null + * @throws IllegalArgumentException if {@code ellipsis.length() > precision}, + * given that {@code ellipsis} is not null and {@code precision >= 0} + */ + public static Formatter append(final CharSequence seq, final Formatter formatter, final int flags, final int width, + final int precision, final CharSequence ellipsis) { + return append(seq, formatter, flags, width, precision, ' ', ellipsis); + } + + /** + * Gets the default formatted representation of the specified + * {@code Formattable}. + * + * @param formattable the instance to convert to a string, not null + * @return The resulting string, not null + */ + public static String toString(final Formattable formattable) { + return String.format(SIMPLEST_FORMAT, formattable); + } + + /** + * {@code FormattableUtils} instances should NOT be constructed in + * standard programming. Instead, the methods of the class should be invoked + * statically. + * + *

This constructor is public to permit tools that require a JavaBean + * instance to operate.

+ */ + public FormattableUtils() { + } + +} diff --git a/sources/src/main/java/org/apache/commons/text/RandomStringGenerator.java b/sources/src/main/java/org/apache/commons/text/RandomStringGenerator.java new file mode 100644 index 0000000..ead0148 --- /dev/null +++ b/sources/src/main/java/org/apache/commons/text/RandomStringGenerator.java @@ -0,0 +1,459 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; + +/** + * Generates random Unicode strings containing the specified number of code points. + * Instances are created using a builder class, which allows the + * callers to define the properties of the generator. See the documentation for the + * {@link Builder} class to see available properties. + * + *
+ * // Generates a 20 code point string, using only the letters a-z
+ * RandomStringGenerator generator = new RandomStringGenerator.Builder()
+ *     .withinRange('a', 'z').build();
+ * String randomLetters = generator.generate(20);
+ * 
+ *
+ * // Using Apache Commons RNG for randomness
+ * UniformRandomProvider rng = RandomSource.create(...);
+ * // Generates a 20 code point string, using only the letters a-z
+ * RandomStringGenerator generator = new RandomStringGenerator.Builder()
+ *     .withinRange('a', 'z')
+ *     .usingRandom(rng::nextInt) // uses Java 8 syntax
+ *     .build();
+ * String randomLetters = generator.generate(20);
+ * 
+ *

+ * {@code RandomStringGenerator} instances are thread-safe when using the + * default random number generator (RNG). If a custom RNG is set by calling the method + * {@link Builder#usingRandom(TextRandomProvider) Builder.usingRandom(TextRandomProvider)}, thread-safety + * must be ensured externally. + *

+ * @since 1.1 + */ +public final class RandomStringGenerator { + + /** + * A builder for generating {@code RandomStringGenerator} instances. + * + *

The behavior of a generator is controlled by properties set by this + * builder. Each property has a default value, which can be overridden by + * calling the methods defined in this class, prior to calling {@link #build()}.

+ * + *

All the property setting methods return the {@code Builder} instance to allow for method chaining.

+ * + *

The minimum and maximum code point values are defined using {@link #withinRange(int, int)}. The + * default values are {@code 0} and {@link Character#MAX_CODE_POINT} respectively.

+ * + *

The source of randomness can be set using {@link #usingRandom(TextRandomProvider)}, + * otherwise {@link ThreadLocalRandom} is used.

+ * + *

The type of code points returned can be filtered using {@link #filteredBy(CharacterPredicate...)}, + * which defines a collection of tests that are applied to the randomly generated code points. + * The code points will only be included in the result if they pass at least one of the tests. + * Some commonly used predicates are provided by the {@link CharacterPredicates} enum.

+ * + *

This class is not thread safe.

+ * @since 1.1 + */ + public static class Builder implements org.apache.commons.text.Builder { + + /** + * The default maximum code point allowed: {@link Character#MAX_CODE_POINT} + * ({@value}). + */ + public static final int DEFAULT_MAXIMUM_CODE_POINT = Character.MAX_CODE_POINT; + + /** + * The default string length produced by this builder: {@value}. + */ + public static final int DEFAULT_LENGTH = 0; + + /** + * The default minimum code point allowed: {@value}. + */ + public static final int DEFAULT_MINIMUM_CODE_POINT = 0; + + /** + * The minimum code point allowed. + */ + private int minimumCodePoint = DEFAULT_MINIMUM_CODE_POINT; + + /** + * The maximum code point allowed. + */ + private int maximumCodePoint = DEFAULT_MAXIMUM_CODE_POINT; + + /** + * Filters for code points. + */ + private Set inclusivePredicates; + + /** + * The source of randomness. + */ + private TextRandomProvider random; + + /** + * The source of provided characters. + */ + private List characterList; + + /** + * Builds the {@code RandomStringGenerator} using the properties specified. + * + * @return The configured {@code RandomStringGenerator} + */ + @Override + public RandomStringGenerator build() { + return new RandomStringGenerator(minimumCodePoint, maximumCodePoint, inclusivePredicates, + random, characterList); + } + + /** + * Limits the characters in the generated string to those that match at + * least one of the predicates supplied. + * + *

+ * Passing {@code null} or an empty array to this method will revert to the + * default behavior of allowing any character. Multiple calls to this + * method will replace the previously stored predicates. + *

+ * + * @param predicates + * the predicates, may be {@code null} or empty + * @return {@code this}, to allow method chaining + */ + public Builder filteredBy(final CharacterPredicate... predicates) { + if (ArrayUtils.isEmpty(predicates)) { + inclusivePredicates = null; + return this; + } + + if (inclusivePredicates == null) { + inclusivePredicates = new HashSet<>(); + } else { + inclusivePredicates.clear(); + } + + Collections.addAll(inclusivePredicates, predicates); + + return this; + } + + /** + * Limits the characters in the generated string to those who match at + * supplied list of Character. + * + *

+ * Passing {@code null} or an empty array to this method will revert to the + * default behavior of allowing any character. Multiple calls to this + * method will replace the previously stored Character. + *

+ * + * @param chars set of predefined Characters for random string generation + * the Character can be, may be {@code null} or empty + * @return {@code this}, to allow method chaining + * @since 1.2 + */ + public Builder selectFrom(final char... chars) { + characterList = new ArrayList<>(); + for (final char c : chars) { + characterList.add(c); + } + return this; + } + + /** + * Overrides the default source of randomness. It is highly + * recommended that a random number generator library like + * Apache Commons RNG + * be used to provide the random number generation. + * + *

+ * When using Java 8 or later, {@link TextRandomProvider} is a + * functional interface and need not be explicitly implemented: + *

+ *
+         * {@code
+         *     UniformRandomProvider rng = RandomSource.create(...);
+         *     RandomStringGenerator gen = new RandomStringGenerator.Builder()
+         *         .usingRandom(rng::nextInt)
+         *         // additional builder calls as needed
+         *         .build();
+         * }
+         * 
+ * + *

+ * Passing {@code null} to this method will revert to the default source of + * randomness. + *

+ * + * @param random + * the source of randomness, may be {@code null} + * @return {@code this}, to allow method chaining + */ + public Builder usingRandom(final TextRandomProvider random) { + this.random = random; + return this; + } + + /** + * Sets the array of minimum and maximum char allowed in the + * generated string. + * + * For example: + *
+         * {@code
+         *     char [][] pairs = {{'0','9'}};
+         *     char [][] pairs = {{'a','z'}};
+         *     char [][] pairs = {{'a','z'},{'0','9'}};
+         * }
+         * 
+ * + * @param pairs array of characters array, expected is to pass min, max pairs through this arg. + * @return {@code this}, to allow method chaining. + */ + public Builder withinRange(final char[]... pairs) { + characterList = new ArrayList<>(); + for (final char[] pair : pairs) { + Validate.isTrue(pair.length == 2, + "Each pair must contain minimum and maximum code point"); + final int minimumCodePoint = pair[0]; + final int maximumCodePoint = pair[1]; + Validate.isTrue(minimumCodePoint <= maximumCodePoint, + "Minimum code point %d is larger than maximum code point %d", minimumCodePoint, maximumCodePoint); + + for (int index = minimumCodePoint; index <= maximumCodePoint; index++) { + characterList.add((char) index); + } + } + return this; + + } + + /** + * Sets the minimum and maximum code points allowed in the + * generated string. + * + * @param minimumCodePoint + * the smallest code point allowed (inclusive) + * @param maximumCodePoint + * the largest code point allowed (inclusive) + * @return {@code this}, to allow method chaining + * @throws IllegalArgumentException + * if {@code maximumCodePoint >} + * {@link Character#MAX_CODE_POINT} + * @throws IllegalArgumentException + * if {@code minimumCodePoint < 0} + * @throws IllegalArgumentException + * if {@code minimumCodePoint > maximumCodePoint} + */ + public Builder withinRange(final int minimumCodePoint, final int maximumCodePoint) { + Validate.isTrue(minimumCodePoint <= maximumCodePoint, + "Minimum code point %d is larger than maximum code point %d", minimumCodePoint, maximumCodePoint); + Validate.isTrue(minimumCodePoint >= 0, "Minimum code point %d is negative", minimumCodePoint); + Validate.isTrue(maximumCodePoint <= Character.MAX_CODE_POINT, + "Value %d is larger than Character.MAX_CODE_POINT.", maximumCodePoint); + + this.minimumCodePoint = minimumCodePoint; + this.maximumCodePoint = maximumCodePoint; + return this; + } + } + + /** + * The smallest allowed code point (inclusive). + */ + private final int minimumCodePoint; + + /** + * The largest allowed code point (inclusive). + */ + private final int maximumCodePoint; + + /** + * Filters for code points. + */ + private final Set inclusivePredicates; + + /** + * The source of randomness for this generator. + */ + private final TextRandomProvider random; + + /** + * The source of provided characters. + */ + private final List characterList; + + /** + * Constructs the generator. + * + * @param minimumCodePoint + * smallest allowed code point (inclusive) + * @param maximumCodePoint + * largest allowed code point (inclusive) + * @param inclusivePredicates + * filters for code points + * @param random + * source of randomness + * @param characterList list of predefined set of characters. + */ + private RandomStringGenerator(final int minimumCodePoint, final int maximumCodePoint, + final Set inclusivePredicates, final TextRandomProvider random, + final List characterList) { + this.minimumCodePoint = minimumCodePoint; + this.maximumCodePoint = maximumCodePoint; + this.inclusivePredicates = inclusivePredicates; + this.random = random; + this.characterList = characterList; + } + + /** + * Generates a random string, containing the specified number of code points. + * + *

+ * Code points are randomly selected between the minimum and maximum values defined + * in the generator. + * Surrogate and private use characters are not returned, although the + * resulting string may contain pairs of surrogates that together encode a + * supplementary character. + *

+ *

+ * Note: the number of {@code char} code units generated will exceed + * {@code length} if the string contains supplementary characters. See the + * {@link Character} documentation to understand how Java stores Unicode + * values. + *

+ * + * @param length + * the number of code points to generate + * @return The generated string + * @throws IllegalArgumentException + * if {@code length < 0} + */ + public String generate(final int length) { + if (length == 0) { + return StringUtils.EMPTY; + } + Validate.isTrue(length > 0, "Length %d is smaller than zero.", length); + + final StringBuilder builder = new StringBuilder(length); + long remaining = length; + + do { + final int codePoint; + if (characterList != null && !characterList.isEmpty()) { + codePoint = generateRandomNumber(characterList); + } else { + codePoint = generateRandomNumber(minimumCodePoint, maximumCodePoint); + } + switch (Character.getType(codePoint)) { + case Character.UNASSIGNED: + case Character.PRIVATE_USE: + case Character.SURROGATE: + continue; + default: + } + + if (inclusivePredicates != null) { + boolean matchedFilter = false; + for (final CharacterPredicate predicate : inclusivePredicates) { + if (predicate.test(codePoint)) { + matchedFilter = true; + break; + } + } + if (!matchedFilter) { + continue; + } + } + + builder.appendCodePoint(codePoint); + remaining--; + + } while (remaining != 0); + + return builder.toString(); + } + + /** + * Generates a random string, containing between the minimum (inclusive) and the maximum (inclusive) + * number of code points. + * + * @param minLengthInclusive + * the minimum (inclusive) number of code points to generate + * @param maxLengthInclusive + * the maximum (inclusive) number of code points to generate + * @return The generated string + * @throws IllegalArgumentException + * if {@code minLengthInclusive < 0}, or {@code maxLengthInclusive < minLengthInclusive} + * @see RandomStringGenerator#generate(int) + * @since 1.2 + */ + public String generate(final int minLengthInclusive, final int maxLengthInclusive) { + Validate.isTrue(minLengthInclusive >= 0, "Minimum length %d is smaller than zero.", minLengthInclusive); + Validate.isTrue(minLengthInclusive <= maxLengthInclusive, + "Maximum length %d is smaller than minimum length %d.", maxLengthInclusive, minLengthInclusive); + return generate(generateRandomNumber(minLengthInclusive, maxLengthInclusive)); + } + + /** + * Generates a random number within a range, using a {@link ThreadLocalRandom} instance + * or the user-supplied source of randomness. + * + * @param minInclusive + * the minimum value allowed + * @param maxInclusive + * the maximum value allowed + * @return The random number. + */ + private int generateRandomNumber(final int minInclusive, final int maxInclusive) { + if (random != null) { + return random.nextInt(maxInclusive - minInclusive + 1) + minInclusive; + } + return ThreadLocalRandom.current().nextInt(minInclusive, maxInclusive + 1); + } + + /** + * Generates a random number within a range, using a {@link ThreadLocalRandom} instance + * or the user-supplied source of randomness. + * + * @param characterList predefined char list. + * @return The random number. + */ + private int generateRandomNumber(final List characterList) { + final int listSize = characterList.size(); + if (random != null) { + return String.valueOf(characterList.get(random.nextInt(listSize))).codePointAt(0); + } + return String.valueOf(characterList.get(ThreadLocalRandom.current().nextInt(0, listSize))).codePointAt(0); + } +} diff --git a/sources/src/main/java/org/apache/commons/text/StrBuilder.java b/sources/src/main/java/org/apache/commons/text/StrBuilder.java new file mode 100644 index 0000000..4f021c6 --- /dev/null +++ b/sources/src/main/java/org/apache/commons/text/StrBuilder.java @@ -0,0 +1,3075 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import java.io.IOException; +import java.io.Reader; +import java.io.Serializable; +import java.io.Writer; +import java.nio.CharBuffer; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; + +/** + * Builds a string from constituent parts providing a more flexible and powerful API than {@link StringBuffer} and + * {@link StringBuilder}. + *

+ * The main differences from StringBuffer/StringBuilder are: + *

+ *
    + *
  • Not synchronized
  • + *
  • Not final
  • + *
  • Subclasses have direct access to character array
  • + *
  • Additional methods + *
      + *
    • appendWithSeparators - adds an array of values, with a separator
    • + *
    • appendPadding - adds a length padding characters
    • + *
    • appendFixedLength - adds a fixed width field to the builder
    • + *
    • toCharArray/getChars - simpler ways to get a range of the character array
    • + *
    • delete - delete char or string
    • + *
    • replace - search and replace for a char or string
    • + *
    • leftString/rightString/midString - substring without exceptions
    • + *
    • contains - whether the builder contains a char or string
    • + *
    • size/clear/isEmpty - collections style API methods
    • + *
    + *
  • + *
  • Views + *
      + *
    • asTokenizer - uses the internal buffer as the source of a StrTokenizer
    • + *
    • asReader - uses the internal buffer as the source of a Reader
    • + *
    • asWriter - allows a Writer to write directly to the internal buffer
    • + *
    + *
  • + *
+ *

+ * The aim has been to provide an API that mimics very closely what StringBuffer provides, but with additional methods. + * It should be noted that some edge cases, with invalid indices or null input, have been altered - see individual + * methods. The biggest of these changes is that by default, null will not output the text 'null'. This can be + * controlled by a property, {@link #setNullText(String)}. + *

+ * + * @since 1.0 + * @deprecated Deprecated as of 1.3, use {@link TextStringBuilder} instead. This class will be removed in 2.0. + */ +@Deprecated +public class StrBuilder implements CharSequence, Appendable, Serializable, Builder { + + /** + * Inner class to allow StrBuilder to operate as a reader. + */ + class StrBuilderReader extends Reader { + + /** The current stream position. */ + private int pos; + + /** The last mark position. */ + private int mark; + + /** + * Default constructor. + */ + StrBuilderReader() { + } + + /** {@inheritDoc} */ + @Override + public void close() { + // do nothing + } + + /** {@inheritDoc} */ + @Override + public void mark(final int readAheadLimit) { + mark = pos; + } + + /** {@inheritDoc} */ + @Override + public boolean markSupported() { + return true; + } + + /** {@inheritDoc} */ + @Override + public int read() { + if (!ready()) { + return -1; + } + return StrBuilder.this.charAt(pos++); + } + + /** {@inheritDoc} */ + @Override + public int read(final char[] b, final int off, int len) { + if (off < 0 || len < 0 || off > b.length + || off + len > b.length || off + len < 0) { + throw new IndexOutOfBoundsException(); + } + if (len == 0) { + return 0; + } + if (pos >= StrBuilder.this.size()) { + return -1; + } + if (pos + len > size()) { + len = StrBuilder.this.size() - pos; + } + StrBuilder.this.getChars(pos, pos + len, b, off); + pos += len; + return len; + } + + /** {@inheritDoc} */ + @Override + public boolean ready() { + return pos < StrBuilder.this.size(); + } + + /** {@inheritDoc} */ + @Override + public void reset() { + pos = mark; + } + + /** {@inheritDoc} */ + @Override + public long skip(long n) { + if (pos + n > StrBuilder.this.size()) { + n = StrBuilder.this.size() - pos; + } + if (n < 0) { + return 0; + } + pos = Math.addExact(pos, Math.toIntExact(n)); + return n; + } + } + + /** + * Inner class to allow StrBuilder to operate as a tokenizer. + */ + class StrBuilderTokenizer extends StrTokenizer { + + /** + * Default constructor. + */ + StrBuilderTokenizer() { + } + + /** {@inheritDoc} */ + @Override + public String getContent() { + final String str = super.getContent(); + if (str == null) { + return StrBuilder.this.toString(); + } + return str; + } + + /** {@inheritDoc} */ + @Override + protected List tokenize(final char[] chars, final int offset, final int count) { + if (chars == null) { + return super.tokenize( + StrBuilder.this.buffer, 0, StrBuilder.this.size()); + } + return super.tokenize(chars, offset, count); + } + } + + /** + * Inner class to allow StrBuilder to operate as a writer. + */ + class StrBuilderWriter extends Writer { + + /** + * Default constructor. + */ + StrBuilderWriter() { + } + + /** {@inheritDoc} */ + @Override + public void close() { + // do nothing + } + + /** {@inheritDoc} */ + @Override + public void flush() { + // do nothing + } + + /** {@inheritDoc} */ + @Override + public void write(final char[] cbuf) { + StrBuilder.this.append(cbuf); + } + + /** {@inheritDoc} */ + @Override + public void write(final char[] cbuf, final int off, final int len) { + StrBuilder.this.append(cbuf, off, len); + } + + /** {@inheritDoc} */ + @Override + public void write(final int c) { + StrBuilder.this.append((char) c); + } + + /** {@inheritDoc} */ + @Override + public void write(final String str) { + StrBuilder.this.append(str); + } + + /** {@inheritDoc} */ + @Override + public void write(final String str, final int off, final int len) { + StrBuilder.this.append(str, off, len); + } + } + + /** + * The extra capacity for new builders. + */ + static final int CAPACITY = 32; + + /** + * Required for serialization support. + * + * @see java.io.Serializable + */ + private static final long serialVersionUID = 7628716375283629643L; + + /** Internal data storage. */ + char[] buffer; // package-protected for test code use only + + /** Current size of the buffer. */ + private int size; + + /** The new line. */ + private String newLine; + + /** The null text. */ + private String nullText; + + /** + * Constructs an empty builder initial capacity 32 characters. + */ + public StrBuilder() { + this(CAPACITY); + } + + /** + * Constructs an empty builder the specified initial capacity. + * + * @param initialCapacity the initial capacity, zero or less will be converted to 32 + */ + public StrBuilder(int initialCapacity) { + if (initialCapacity <= 0) { + initialCapacity = CAPACITY; + } + buffer = new char[initialCapacity]; + } + + /** + * Constructs a builder from the string, allocating + * 32 extra characters for growth. + * + * @param str the string to copy, null treated as blank string + */ + public StrBuilder(final String str) { + if (str == null) { + buffer = new char[CAPACITY]; + } else { + buffer = new char[str.length() + CAPACITY]; + append(str); + } + } + + /** + * Appends a boolean value to the string builder. + * + * @param value the value to append + * @return this, to enable chaining + */ + public StrBuilder append(final boolean value) { + if (value) { + ensureCapacity(size + 4); + buffer[size++] = 't'; + buffer[size++] = 'r'; + buffer[size++] = 'u'; + buffer[size++] = 'e'; + } else { + ensureCapacity(size + 5); + buffer[size++] = 'f'; + buffer[size++] = 'a'; + buffer[size++] = 'l'; + buffer[size++] = 's'; + buffer[size++] = 'e'; + } + return this; + } + + /** + * Appends a char value to the string builder. + * + * @param ch the value to append + * @return this, to enable chaining + */ + @Override + public StrBuilder append(final char ch) { + final int len = length(); + ensureCapacity(len + 1); + buffer[size++] = ch; + return this; + } + + /** + * Appends a char array to the string builder. + * Appending null will call {@link #appendNull()}. + * + * @param chars the char array to append + * @return this, to enable chaining + */ + public StrBuilder append(final char[] chars) { + if (chars == null) { + return appendNull(); + } + final int strLen = chars.length; + if (strLen > 0) { + final int len = length(); + ensureCapacity(len + strLen); + System.arraycopy(chars, 0, buffer, len, strLen); + size += strLen; + } + return this; + } + + /** + * Appends a char array to the string builder. + * Appending null will call {@link #appendNull()}. + * + * @param chars the char array to append + * @param startIndex the start index, inclusive, must be valid + * @param length the length to append, must be valid + * @return this, to enable chaining + */ + public StrBuilder append(final char[] chars, final int startIndex, final int length) { + if (chars == null) { + return appendNull(); + } + if (startIndex < 0 || startIndex > chars.length) { + throw new StringIndexOutOfBoundsException("Invalid startIndex: " + length); + } + if (length < 0 || startIndex + length > chars.length) { + throw new StringIndexOutOfBoundsException("Invalid length: " + length); + } + if (length > 0) { + final int len = length(); + ensureCapacity(len + length); + System.arraycopy(chars, startIndex, buffer, len, length); + size += length; + } + return this; + } + + /** + * Appends the contents of a char buffer to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param buf the char buffer to append + * @return this, to enable chaining + */ + public StrBuilder append(final CharBuffer buf) { + if (buf == null) { + return appendNull(); + } + if (buf.hasArray()) { + final int length = buf.remaining(); + final int len = length(); + ensureCapacity(len + length); + System.arraycopy(buf.array(), buf.arrayOffset() + buf.position(), buffer, len, length); + size += length; + } else { + append(buf.toString()); + } + return this; + } + + /** + * Appends the contents of a char buffer to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param buf the char buffer to append + * @param startIndex the start index, inclusive, must be valid + * @param length the length to append, must be valid + * @return this, to enable chaining + */ + public StrBuilder append(final CharBuffer buf, final int startIndex, final int length) { + if (buf == null) { + return appendNull(); + } + if (buf.hasArray()) { + final int totalLength = buf.remaining(); + if (startIndex < 0 || startIndex > totalLength) { + throw new StringIndexOutOfBoundsException("startIndex must be valid"); + } + if (length < 0 || startIndex + length > totalLength) { + throw new StringIndexOutOfBoundsException("length must be valid"); + } + final int len = length(); + ensureCapacity(len + length); + System.arraycopy(buf.array(), buf.arrayOffset() + buf.position() + startIndex, buffer, len, length); + size += length; + } else { + append(buf.toString(), startIndex, length); + } + return this; + } + + /** + * Appends a CharSequence to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param seq the CharSequence to append + * @return this, to enable chaining + */ + @Override + public StrBuilder append(final CharSequence seq) { + if (seq == null) { + return appendNull(); + } + if (seq instanceof StrBuilder) { + return append((StrBuilder) seq); + } + if (seq instanceof StringBuilder) { + return append((StringBuilder) seq); + } + if (seq instanceof StringBuffer) { + return append((StringBuffer) seq); + } + if (seq instanceof CharBuffer) { + return append((CharBuffer) seq); + } + return append(seq.toString()); + } + + /** + * Appends part of a CharSequence to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param seq the CharSequence to append + * @param startIndex the start index, inclusive, must be valid + * @param length the length to append, must be valid + * @return this, to enable chaining + */ + @Override + public StrBuilder append(final CharSequence seq, final int startIndex, final int length) { + if (seq == null) { + return appendNull(); + } + return append(seq.toString(), startIndex, length); + } + + /** + * Appends a double value to the string builder using {@code String.valueOf}. + * + * @param value the value to append + * @return this, to enable chaining + */ + public StrBuilder append(final double value) { + return append(String.valueOf(value)); + } + + /** + * Appends a float value to the string builder using {@code String.valueOf}. + * + * @param value the value to append + * @return this, to enable chaining + */ + public StrBuilder append(final float value) { + return append(String.valueOf(value)); + } + + /** + * Appends an int value to the string builder using {@code String.valueOf}. + * + * @param value the value to append + * @return this, to enable chaining + */ + public StrBuilder append(final int value) { + return append(String.valueOf(value)); + } + + /** + * Appends a long value to the string builder using {@code String.valueOf}. + * + * @param value the value to append + * @return this, to enable chaining + */ + public StrBuilder append(final long value) { + return append(String.valueOf(value)); + } + + /** + * Appends an object to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param obj the object to append + * @return this, to enable chaining + */ + public StrBuilder append(final Object obj) { + if (obj == null) { + return appendNull(); + } + if (obj instanceof CharSequence) { + return append((CharSequence) obj); + } + return append(obj.toString()); + } + + /** + * Appends another string builder to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param str the string builder to append + * @return this, to enable chaining + */ + public StrBuilder append(final StrBuilder str) { + if (str == null) { + return appendNull(); + } + final int strLen = str.length(); + if (strLen > 0) { + final int len = length(); + ensureCapacity(len + strLen); + System.arraycopy(str.buffer, 0, buffer, len, strLen); + size += strLen; + } + return this; + } + + /** + * Appends part of a string builder to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param str the string to append + * @param startIndex the start index, inclusive, must be valid + * @param length the length to append, must be valid + * @return this, to enable chaining + */ + public StrBuilder append(final StrBuilder str, final int startIndex, final int length) { + if (str == null) { + return appendNull(); + } + if (startIndex < 0 || startIndex > str.length()) { + throw new StringIndexOutOfBoundsException("startIndex must be valid"); + } + if (length < 0 || startIndex + length > str.length()) { + throw new StringIndexOutOfBoundsException("length must be valid"); + } + if (length > 0) { + final int len = length(); + ensureCapacity(len + length); + str.getChars(startIndex, startIndex + length, buffer, len); + size += length; + } + return this; + } + + /** + * Appends a string to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param str the string to append + * @return this, to enable chaining + */ + public StrBuilder append(final String str) { + if (str == null) { + return appendNull(); + } + final int strLen = str.length(); + if (strLen > 0) { + final int len = length(); + ensureCapacity(len + strLen); + str.getChars(0, strLen, buffer, len); + size += strLen; + } + return this; + } + + /** + * Appends part of a string to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param str the string to append + * @param startIndex the start index, inclusive, must be valid + * @param length the length to append, must be valid + * @return this, to enable chaining + */ + public StrBuilder append(final String str, final int startIndex, final int length) { + if (str == null) { + return appendNull(); + } + if (startIndex < 0 || startIndex > str.length()) { + throw new StringIndexOutOfBoundsException("startIndex must be valid"); + } + if (length < 0 || startIndex + length > str.length()) { + throw new StringIndexOutOfBoundsException("length must be valid"); + } + if (length > 0) { + final int len = length(); + ensureCapacity(len + length); + str.getChars(startIndex, startIndex + length, buffer, len); + size += length; + } + return this; + } + + /** + * Calls {@link String#format(String, Object...)} and appends the result. + * + * @param format the format string + * @param objs the objects to use in the format string + * @return {@code this} to enable chaining + * @see String#format(String, Object...) + */ + public StrBuilder append(final String format, final Object... objs) { + return append(String.format(format, objs)); + } + + /** + * Appends a string buffer to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param str the string buffer to append + * @return this, to enable chaining + */ + public StrBuilder append(final StringBuffer str) { + if (str == null) { + return appendNull(); + } + final int strLen = str.length(); + if (strLen > 0) { + final int len = length(); + ensureCapacity(len + strLen); + str.getChars(0, strLen, buffer, len); + size += strLen; + } + return this; + } + + /** + * Appends part of a string buffer to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param str the string to append + * @param startIndex the start index, inclusive, must be valid + * @param length the length to append, must be valid + * @return this, to enable chaining + */ + public StrBuilder append(final StringBuffer str, final int startIndex, final int length) { + if (str == null) { + return appendNull(); + } + if (startIndex < 0 || startIndex > str.length()) { + throw new StringIndexOutOfBoundsException("startIndex must be valid"); + } + if (length < 0 || startIndex + length > str.length()) { + throw new StringIndexOutOfBoundsException("length must be valid"); + } + if (length > 0) { + final int len = length(); + ensureCapacity(len + length); + str.getChars(startIndex, startIndex + length, buffer, len); + size += length; + } + return this; + } + + /** + * Appends a StringBuilder to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param str the StringBuilder to append + * @return this, to enable chaining + */ + public StrBuilder append(final StringBuilder str) { + if (str == null) { + return appendNull(); + } + final int strLen = str.length(); + if (strLen > 0) { + final int len = length(); + ensureCapacity(len + strLen); + str.getChars(0, strLen, buffer, len); + size += strLen; + } + return this; + } + + /** + * Appends part of a StringBuilder to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param str the StringBuilder to append + * @param startIndex the start index, inclusive, must be valid + * @param length the length to append, must be valid + * @return this, to enable chaining + */ + public StrBuilder append(final StringBuilder str, final int startIndex, final int length) { + if (str == null) { + return appendNull(); + } + if (startIndex < 0 || startIndex > str.length()) { + throw new StringIndexOutOfBoundsException("startIndex must be valid"); + } + if (length < 0 || startIndex + length > str.length()) { + throw new StringIndexOutOfBoundsException("length must be valid"); + } + if (length > 0) { + final int len = length(); + ensureCapacity(len + length); + str.getChars(startIndex, startIndex + length, buffer, len); + size += length; + } + return this; + } + + /** + * Appends each item in an iterable to the builder without any separators. + * Appending a null iterable will have no effect. + * Each object is appended using {@link #append(Object)}. + * + * @param iterable the iterable to append + * @return this, to enable chaining + */ + public StrBuilder appendAll(final Iterable iterable) { + if (iterable != null) { + iterable.forEach(this::append); + } + return this; + } + + /** + * Appends each item in an iterator to the builder without any separators. + * Appending a null iterator will have no effect. + * Each object is appended using {@link #append(Object)}. + * + * @param it the iterator to append + * @return this, to enable chaining + */ + public StrBuilder appendAll(final Iterator it) { + if (it != null) { + while (it.hasNext()) { + append(it.next()); + } + } + return this; + } + + /** + * Appends each item in an array to the builder without any separators. + * Appending a null array will have no effect. + * Each object is appended using {@link #append(Object)}. + * + * @param the element type + * @param array the array to append + * @return this, to enable chaining + */ + public StrBuilder appendAll(@SuppressWarnings("unchecked") final T... array) { + /* + * @SuppressWarnings used to hide warning about vararg usage. We cannot + * use @SafeVarargs, since this method is not final. Using @SuppressWarnings + * is fine, because it isn't inherited by subclasses, so each subclass must + * vouch for itself whether its use of 'array' is safe. + */ + if (array != null && array.length > 0) { + for (final Object element : array) { + append(element); + } + } + return this; + } + + /** + * Appends an object to the builder padding on the left to a fixed width. + * The {@code String.valueOf} of the {@code int} value is used. + * If the formatted value is larger than the length, the left hand side is lost. + * + * @param value the value to append + * @param width the fixed field width, zero or negative has no effect + * @param padChar the pad character to use + * @return this, to enable chaining + */ + public StrBuilder appendFixedWidthPadLeft(final int value, final int width, final char padChar) { + return appendFixedWidthPadLeft(String.valueOf(value), width, padChar); + } + + /** + * Appends an object to the builder padding on the left to a fixed width. + * The {@code toString} of the object is used. + * If the object is larger than the length, the left hand side is lost. + * If the object is null, the null text value is used. + * + * @param obj the object to append, null uses null text + * @param width the fixed field width, zero or negative has no effect + * @param padChar the pad character to use + * @return this, to enable chaining + */ + public StrBuilder appendFixedWidthPadLeft(final Object obj, final int width, final char padChar) { + if (width > 0) { + ensureCapacity(size + width); + String str = obj == null ? getNullText() : obj.toString(); + if (str == null) { + str = StringUtils.EMPTY; + } + final int strLen = str.length(); + if (strLen >= width) { + str.getChars(strLen - width, strLen, buffer, size); + } else { + final int padLen = width - strLen; + for (int i = 0; i < padLen; i++) { + buffer[size + i] = padChar; + } + str.getChars(0, strLen, buffer, size + padLen); + } + size += width; + } + return this; + } + + /** + * Appends an object to the builder padding on the right to a fixed length. + * The {@code String.valueOf} of the {@code int} value is used. + * If the object is larger than the length, the right hand side is lost. + * + * @param value the value to append + * @param width the fixed field width, zero or negative has no effect + * @param padChar the pad character to use + * @return this, to enable chaining + */ + public StrBuilder appendFixedWidthPadRight(final int value, final int width, final char padChar) { + return appendFixedWidthPadRight(String.valueOf(value), width, padChar); + } + + /** + * Appends an object to the builder padding on the right to a fixed length. + * The {@code toString} of the object is used. + * If the object is larger than the length, the right hand side is lost. + * If the object is null, null text value is used. + * + * @param obj the object to append, null uses null text + * @param width the fixed field width, zero or negative has no effect + * @param padChar the pad character to use + * @return this, to enable chaining + */ + public StrBuilder appendFixedWidthPadRight(final Object obj, final int width, final char padChar) { + if (width > 0) { + ensureCapacity(size + width); + String str = obj == null ? getNullText() : obj.toString(); + if (str == null) { + str = StringUtils.EMPTY; + } + final int strLen = str.length(); + if (strLen >= width) { + str.getChars(0, width, buffer, size); + } else { + final int padLen = width - strLen; + str.getChars(0, strLen, buffer, size); + for (int i = 0; i < padLen; i++) { + buffer[size + strLen + i] = padChar; + } + } + size += width; + } + return this; + } + + /** + * Appends a boolean value followed by a new line to the string builder. + * + * @param value the value to append + * @return this, to enable chaining + */ + public StrBuilder appendln(final boolean value) { + return append(value).appendNewLine(); + } + + /** + * Appends a char value followed by a new line to the string builder. + * + * @param ch the value to append + * @return this, to enable chaining + */ + public StrBuilder appendln(final char ch) { + return append(ch).appendNewLine(); + } + + /** + * Appends a char array followed by a new line to the string builder. + * Appending null will call {@link #appendNull()}. + * + * @param chars the char array to append + * @return this, to enable chaining + */ + public StrBuilder appendln(final char[] chars) { + return append(chars).appendNewLine(); + } + + /** + * Appends a char array followed by a new line to the string builder. + * Appending null will call {@link #appendNull()}. + * + * @param chars the char array to append + * @param startIndex the start index, inclusive, must be valid + * @param length the length to append, must be valid + * @return this, to enable chaining + */ + public StrBuilder appendln(final char[] chars, final int startIndex, final int length) { + return append(chars, startIndex, length).appendNewLine(); + } + + /** + * Appends a double value followed by a new line to the string builder using {@code String.valueOf}. + * + * @param value the value to append + * @return this, to enable chaining + */ + public StrBuilder appendln(final double value) { + return append(value).appendNewLine(); + } + + /** + * Appends a float value followed by a new line to the string builder using {@code String.valueOf}. + * + * @param value the value to append + * @return this, to enable chaining + */ + public StrBuilder appendln(final float value) { + return append(value).appendNewLine(); + } + + /** + * Appends an int value followed by a new line to the string builder using {@code String.valueOf}. + * + * @param value the value to append + * @return this, to enable chaining + */ + public StrBuilder appendln(final int value) { + return append(value).appendNewLine(); + } + + /** + * Appends a long value followed by a new line to the string builder using {@code String.valueOf}. + * + * @param value the value to append + * @return this, to enable chaining + */ + public StrBuilder appendln(final long value) { + return append(value).appendNewLine(); + } + + /** + * Appends an object followed by a new line to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param obj the object to append + * @return this, to enable chaining + */ + public StrBuilder appendln(final Object obj) { + return append(obj).appendNewLine(); + } + + /** + * Appends another string builder followed by a new line to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param str the string builder to append + * @return this, to enable chaining + */ + public StrBuilder appendln(final StrBuilder str) { + return append(str).appendNewLine(); + } + + /** + * Appends part of a string builder followed by a new line to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param str the string to append + * @param startIndex the start index, inclusive, must be valid + * @param length the length to append, must be valid + * @return this, to enable chaining + */ + public StrBuilder appendln(final StrBuilder str, final int startIndex, final int length) { + return append(str, startIndex, length).appendNewLine(); + } + + /** + * Appends a string followed by a new line to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param str the string to append + * @return this, to enable chaining + */ + public StrBuilder appendln(final String str) { + return append(str).appendNewLine(); + } + + /** + * Appends part of a string followed by a new line to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param str the string to append + * @param startIndex the start index, inclusive, must be valid + * @param length the length to append, must be valid + * @return this, to enable chaining + */ + public StrBuilder appendln(final String str, final int startIndex, final int length) { + return append(str, startIndex, length).appendNewLine(); + } + + /** + * Calls {@link String#format(String, Object...)} and appends the result. + * + * @param format the format string + * @param objs the objects to use in the format string + * @return {@code this} to enable chaining + * @see String#format(String, Object...) + */ + public StrBuilder appendln(final String format, final Object... objs) { + return append(format, objs).appendNewLine(); + } + + /** + * Appends a string buffer followed by a new line to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param str the string buffer to append + * @return this, to enable chaining + */ + public StrBuilder appendln(final StringBuffer str) { + return append(str).appendNewLine(); + } + + /** + * Appends part of a string buffer followed by a new line to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param str the string to append + * @param startIndex the start index, inclusive, must be valid + * @param length the length to append, must be valid + * @return this, to enable chaining + */ + public StrBuilder appendln(final StringBuffer str, final int startIndex, final int length) { + return append(str, startIndex, length).appendNewLine(); + } + + /** + * Appends a string builder followed by a new line to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param str the string builder to append + * @return this, to enable chaining + */ + public StrBuilder appendln(final StringBuilder str) { + return append(str).appendNewLine(); + } + + /** + * Appends part of a string builder followed by a new line to this string builder. + * Appending null will call {@link #appendNull()}. + * + * @param str the string builder to append + * @param startIndex the start index, inclusive, must be valid + * @param length the length to append, must be valid + * @return this, to enable chaining + */ + public StrBuilder appendln(final StringBuilder str, final int startIndex, final int length) { + return append(str, startIndex, length).appendNewLine(); + } + + /** + * Appends the new line string to this string builder. + *

+ * The new line string can be altered using {@link #setNewLineText(String)}. + * This might be used to force the output to always use Unix line endings + * even when on Windows. + *

+ * @return this, to enable chaining + */ + public StrBuilder appendNewLine() { + if (newLine == null) { + append(System.lineSeparator()); + return this; + } + return append(newLine); + } + + /** + * Appends the text representing {@code null} to this string builder. + * + * @return this, to enable chaining + */ + public StrBuilder appendNull() { + if (nullText == null) { + return this; + } + return append(nullText); + } + + /** + * Appends the pad character to the builder the specified number of times. + * + * @param length the length to append, negative means no append + * @param padChar the character to append + * @return this, to enable chaining + */ + public StrBuilder appendPadding(final int length, final char padChar) { + if (length >= 0) { + ensureCapacity(size + length); + for (int i = 0; i < length; i++) { + buffer[size++] = padChar; + } + } + return this; + } + + /** + * Appends a separator if the builder is currently non-empty. + * The separator is appended using {@link #append(char)}. + *

+ * This method is useful for adding a separator each time around the + * loop except the first. + *

+ *
+     * for (Iterator it = list.iterator(); it.hasNext();){
+     *   appendSeparator(',');
+     *   append(it.next());
+     * }
+     * 
+ *

+ * Note that for this simple example, you should use + * {@link #appendWithSeparators(Iterable, String)}. + *

+ * + * @param separator the separator to use + * @return this, to enable chaining + */ + public StrBuilder appendSeparator(final char separator) { + if (isNotEmpty()) { + append(separator); + } + return this; + } + + /** + * Appends one of both separators to the builder + * If the builder is currently empty it will append the defaultIfEmpty-separator + * Otherwise it will append the standard-separator + * + * The separator is appended using {@link #append(char)}. + * @param standard the separator if builder is not empty + * @param defaultIfEmpty the separator if builder is empty + * @return this, to enable chaining + */ + public StrBuilder appendSeparator(final char standard, final char defaultIfEmpty) { + if (isNotEmpty()) { + append(standard); + } else { + append(defaultIfEmpty); + } + return this; + } + + /** + * Appends a separator to the builder if the loop index is greater than zero. + * The separator is appended using {@link #append(char)}. + *

+ * This method is useful for adding a separator each time around the + * loop except the first. + *

+ *
+     * for (int i = 0; i < list.size(); i++) {
+     *   appendSeparator(",", i);
+     *   append(list.get(i));
+     * }
+     * 
+ *

+ * Note that for this simple example, you should use + * {@link #appendWithSeparators(Iterable, String)}. + *

+ * + * @param separator the separator to use + * @param loopIndex the loop index + * @return this, to enable chaining + */ + public StrBuilder appendSeparator(final char separator, final int loopIndex) { + if (loopIndex > 0) { + append(separator); + } + return this; + } + + /** + * Appends a separator if the builder is currently non-empty. + * Appending a null separator will have no effect. + * The separator is appended using {@link #append(String)}. + *

+ * This method is useful for adding a separator each time around the + * loop except the first. + *

+ *
+     * for (Iterator it = list.iterator(); it.hasNext();){
+     *   appendSeparator(",");
+     *   append(it.next());
+     * }
+     * 
+ *

+ * Note that for this simple example, you should use + * {@link #appendWithSeparators(Iterable, String)}. + *

+ * + * @param separator the separator to use, null means no separator + * @return this, to enable chaining + */ + public StrBuilder appendSeparator(final String separator) { + return appendSeparator(separator, null); + } + + /** + * Appends a separator to the builder if the loop index is greater than zero. + * Appending a null separator will have no effect. + * The separator is appended using {@link #append(String)}. + *

+ * This method is useful for adding a separator each time around the + * loop except the first. + *

+ *
+     * for (int i = 0; i < list.size(); i++) {
+     *   appendSeparator(",", i);
+     *   append(list.get(i));
+     * }
+     * 
+ *

+ * Note that for this simple example, you should use + * {@link #appendWithSeparators(Iterable, String)}. + *

+ * + * @param separator the separator to use, null means no separator + * @param loopIndex the loop index + * @return this, to enable chaining + */ + public StrBuilder appendSeparator(final String separator, final int loopIndex) { + if (separator != null && loopIndex > 0) { + append(separator); + } + return this; + } + + /** + * Appends one of both separators to the StrBuilder. + * If the builder is currently empty it will append the defaultIfEmpty-separator + * Otherwise it will append the standard-separator + *

+ * Appending a null separator will have no effect. + * The separator is appended using {@link #append(String)}. + *

+ *

+ * This method is for example useful for constructing queries + *

+ *
+     * StrBuilder whereClause = new StrBuilder();
+     * if(searchCommand.getPriority() != null) {
+     *   whereClause.appendSeparator(" and", " where");
+     *   whereClause.append(" priority = ?")
+     * }
+     * if(searchCommand.getComponent() != null) {
+     *   whereClause.appendSeparator(" and", " where");
+     *   whereClause.append(" component = ?")
+     * }
+     * selectClause.append(whereClause)
+     * 
+ * + * @param standard the separator if builder is not empty, null means no separator + * @param defaultIfEmpty the separator if builder is empty, null means no separator + * @return this, to enable chaining + */ + public StrBuilder appendSeparator(final String standard, final String defaultIfEmpty) { + final String str = isEmpty() ? defaultIfEmpty : standard; + if (str != null) { + append(str); + } + return this; + } + + /** + * Appends current contents of this {@code StrBuilder} to the + * provided {@link Appendable}. + *

+ * This method tries to avoid doing any extra copies of contents. + *

+ * + * @param appendable the appendable to append data to + * @throws IOException if an I/O error occurs + * + * @see #readFrom(Readable) + */ + public void appendTo(final Appendable appendable) throws IOException { + if (appendable instanceof Writer) { + ((Writer) appendable).write(buffer, 0, size); + } else if (appendable instanceof StringBuilder) { + ((StringBuilder) appendable).append(buffer, 0, size); + } else if (appendable instanceof StringBuffer) { + ((StringBuffer) appendable).append(buffer, 0, size); + } else if (appendable instanceof CharBuffer) { + ((CharBuffer) appendable).put(buffer, 0, size); + } else { + appendable.append(this); + } + } + + /** + * Appends an iterable placing separators between each value, but + * not before the first or after the last. + * Appending a null iterable will have no effect. + * Each object is appended using {@link #append(Object)}. + * + * @param iterable the iterable to append + * @param separator the separator to use, null means no separator + * @return this, to enable chaining + */ + public StrBuilder appendWithSeparators(final Iterable iterable, final String separator) { + if (iterable != null) { + appendWithSeparators(iterable.iterator(), separator); + } + return this; + } + + /** + * Appends an iterator placing separators between each value, but + * not before the first or after the last. + * Appending a null iterator will have no effect. + * Each object is appended using {@link #append(Object)}. + * + * @param iterator the iterator to append + * @param separator the separator to use, null means no separator + * @return this, to enable chaining + */ + public StrBuilder appendWithSeparators(final Iterator iterator, final String separator) { + if (iterator != null) { + final String sep = Objects.toString(separator, StringUtils.EMPTY); + while (iterator.hasNext()) { + append(iterator.next()); + if (iterator.hasNext()) { + append(sep); + } + } + } + return this; + } + + /** + * Appends an array placing separators between each value, but + * not before the first or after the last. + * Appending a null array will have no effect. + * Each object is appended using {@link #append(Object)}. + * + * @param array the array to append + * @param separator the separator to use, null means no separator + * @return this, to enable chaining + */ + public StrBuilder appendWithSeparators(final Object[] array, final String separator) { + if (array != null && array.length > 0) { + final String sep = Objects.toString(separator, StringUtils.EMPTY); + append(array[0]); + for (int i = 1; i < array.length; i++) { + append(sep); + append(array[i]); + } + } + return this; + } + + /** + * Gets the contents of this builder as a Reader. + *

+ * This method allows the contents of the builder to be read + * using any standard method that expects a Reader. + *

+ *

+ * To use, simply create a {@code StrBuilder}, populate it with + * data, call {@code asReader}, and then read away. + *

+ *

+ * The internal character array is shared between the builder and the reader. + * This allows you to append to the builder after creating the reader, + * and the changes will be picked up. + * Note however, that no synchronization occurs, so you must perform + * all operations with the builder and the reader in one thread. + *

+ *

+ * The returned reader supports marking, and ignores the flush method. + *

+ * + * @return a reader that reads from this builder + */ + public Reader asReader() { + return new StrBuilderReader(); + } + + /** + * Creates a tokenizer that can tokenize the contents of this builder. + *

+ * This method allows the contents of this builder to be tokenized. + * The tokenizer will be setup by default to tokenize on space, tab, + * newline and form feed (as per StringTokenizer). These values can be + * changed on the tokenizer class, before retrieving the tokens. + *

+ *

+ * The returned tokenizer is linked to this builder. You may intermix + * calls to the builder and tokenizer within certain limits, however + * there is no synchronization. Once the tokenizer has been used once, + * it must be {@link StrTokenizer#reset() reset} to pickup the latest + * changes in the builder. For example: + *

+ *
+     * StrBuilder b = new StrBuilder();
+     * b.append("a b ");
+     * StrTokenizer t = b.asTokenizer();
+     * String[] tokens1 = t.getTokenArray();  // returns a,b
+     * b.append("c d ");
+     * String[] tokens2 = t.getTokenArray();  // returns a,b (c and d ignored)
+     * t.reset();              // reset causes builder changes to be picked up
+     * String[] tokens3 = t.getTokenArray();  // returns a,b,c,d
+     * 
+ *

+ * In addition to simply intermixing appends and tokenization, you can also + * call the set methods on the tokenizer to alter how it tokenizes. Just + * remember to call reset when you want to pickup builder changes. + *

+ *

+ * Calling {@link StrTokenizer#reset(String)} or {@link StrTokenizer#reset(char[])} + * with a non-null value will break the link with the builder. + *

+ * + * @return a tokenizer that is linked to this builder + */ + public StrTokenizer asTokenizer() { + return new StrBuilderTokenizer(); + } + + /** + * Gets this builder as a Writer that can be written to. + *

+ * This method allows you to populate the contents of the builder + * using any standard method that takes a Writer. + *

+ *

+ * To use, simply create a {@code StrBuilder}, + * call {@code asWriter}, and populate away. The data is available + * at any time using the methods of the {@code StrBuilder}. + *

+ *

+ * The internal character array is shared between the builder and the writer. + * This allows you to intermix calls that append to the builder and + * write using the writer and the changes will be occur correctly. + * Note however, that no synchronization occurs, so you must perform + * all operations with the builder and the writer in one thread. + *

+ *

+ * The returned writer ignores the close and flush methods. + *

+ * + * @return a writer that populates this builder + */ + public Writer asWriter() { + return new StrBuilderWriter(); + } + + /** + * Builds a string. + * + * @return The builder as a String + * @see #toString() + */ + @Override + public String build() { + return toString(); + } + + /** + * Gets the current size of the internal character array buffer. + * + * @return The capacity + */ + public int capacity() { + return buffer.length; + } + + /** + * Gets the character at the specified index. + * + * @see #setCharAt(int, char) + * @see #deleteCharAt(int) + * @param index the index to retrieve, must be valid + * @return The character at the index + * @throws IndexOutOfBoundsException if the index is invalid + */ + @Override + public char charAt(final int index) { + if (index < 0 || index >= length()) { + throw new StringIndexOutOfBoundsException(index); + } + return buffer[index]; + } + + /** + * Clears the string builder (convenience Collections API style method). + *

+ * This method does not reduce the size of the internal character buffer. + * To do that, call {@code clear()} followed by {@link #minimizeCapacity()}. + *

+ *

+ * This method is the same as {@link #setLength(int)} called with zero + * and is provided to match the API of Collections. + *

+ * + * @return this, to enable chaining + */ + public StrBuilder clear() { + size = 0; + return this; + } + + /** + * Checks if the string builder contains the specified char. + * + * @param ch the character to find + * @return true if the builder contains the character + */ + public boolean contains(final char ch) { + final char[] thisBuf = buffer; + for (int i = 0; i < this.size; i++) { + if (thisBuf[i] == ch) { + return true; + } + } + return false; + } + + /** + * Tests if the string builder contains the specified string. + * + * @param str the string to find + * @return true if the builder contains the string + */ + public boolean contains(final String str) { + return indexOf(str, 0) >= 0; + } + + /** + * Tests if the string builder contains a string matched using the + * specified matcher. + *

+ * Matchers can be used to perform advanced searching behavior. + * For example you could write a matcher to search for the character + * 'a' followed by a number. + *

+ * + * @param matcher the matcher to use, null returns -1 + * @return true if the matcher finds a match in the builder + */ + public boolean contains(final StrMatcher matcher) { + return indexOf(matcher, 0) >= 0; + } + + /** + * Deletes the characters between the two specified indices. + * + * @param startIndex the start index, inclusive, must be valid + * @param endIndex the end index, exclusive, must be valid except + * that if too large it is treated as end of string + * @return this, to enable chaining + * @throws IndexOutOfBoundsException if the index is invalid + */ + public StrBuilder delete(final int startIndex, int endIndex) { + endIndex = validateRange(startIndex, endIndex); + final int len = endIndex - startIndex; + if (len > 0) { + deleteImpl(startIndex, endIndex, len); + } + return this; + } + + /** + * Deletes the character wherever it occurs in the builder. + * + * @param ch the character to delete + * @return this, to enable chaining + */ + public StrBuilder deleteAll(final char ch) { + for (int i = 0; i < size; i++) { + if (buffer[i] == ch) { + final int start = i; + while (++i < size) { + if (buffer[i] != ch) { + break; + } + } + final int len = i - start; + deleteImpl(start, i, len); + i -= len; + } + } + return this; + } + + /** + * Deletes the string wherever it occurs in the builder. + * + * @param str the string to delete, null causes no action + * @return this, to enable chaining + */ + public StrBuilder deleteAll(final String str) { + final int len = str == null ? 0 : str.length(); + if (len > 0) { + int index = indexOf(str, 0); + while (index >= 0) { + deleteImpl(index, index + len, len); + index = indexOf(str, index); + } + } + return this; + } + + /** + * Deletes all parts of the builder that the matcher matches. + *

+ * Matchers can be used to perform advanced deletion behavior. + * For example you could write a matcher to delete all occurrences + * where the character 'a' is followed by a number. + *

+ * + * @param matcher the matcher to use to find the deletion, null causes no action + * @return this, to enable chaining + */ + public StrBuilder deleteAll(final StrMatcher matcher) { + return replace(matcher, null, 0, size, -1); + } + + /** + * Deletes the character at the specified index. + * + * @see #charAt(int) + * @see #setCharAt(int, char) + * @param index the index to delete + * @return this, to enable chaining + * @throws IndexOutOfBoundsException if the index is invalid + */ + public StrBuilder deleteCharAt(final int index) { + if (index < 0 || index >= size) { + throw new StringIndexOutOfBoundsException(index); + } + deleteImpl(index, index + 1, 1); + return this; + } + + /** + * Deletes the character wherever it occurs in the builder. + * + * @param ch the character to delete + * @return this, to enable chaining + */ + public StrBuilder deleteFirst(final char ch) { + for (int i = 0; i < size; i++) { + if (buffer[i] == ch) { + deleteImpl(i, i + 1, 1); + break; + } + } + return this; + } + + /** + * Deletes the string wherever it occurs in the builder. + * + * @param str the string to delete, null causes no action + * @return this, to enable chaining + */ + public StrBuilder deleteFirst(final String str) { + final int len = str == null ? 0 : str.length(); + if (len > 0) { + final int index = indexOf(str, 0); + if (index >= 0) { + deleteImpl(index, index + len, len); + } + } + return this; + } + + /** + * Deletes the first match within the builder using the specified matcher. + *

+ * Matchers can be used to perform advanced deletion behavior. + * For example you could write a matcher to delete + * where the character 'a' is followed by a number. + *

+ * + * @param matcher the matcher to use to find the deletion, null causes no action + * @return this, to enable chaining + */ + public StrBuilder deleteFirst(final StrMatcher matcher) { + return replace(matcher, null, 0, size, 1); + } + + /** + * Internal method to delete a range without validation. + * + * @param startIndex the start index, must be valid + * @param endIndex the end index (exclusive), must be valid + * @param len the length, must be valid + * @throws IndexOutOfBoundsException if any index is invalid + */ + private void deleteImpl(final int startIndex, final int endIndex, final int len) { + System.arraycopy(buffer, endIndex, buffer, startIndex, size - endIndex); + size -= len; + } + + /** + * Tests whether this builder ends with the specified string. + *

+ * Note that this method handles null input quietly, unlike String. + *

+ * + * @param str the string to search for, null returns false + * @return true if the builder ends with the string + */ + public boolean endsWith(final String str) { + if (str == null) { + return false; + } + final int len = str.length(); + if (len == 0) { + return true; + } + if (len > size) { + return false; + } + int pos = size - len; + for (int i = 0; i < len; i++, pos++) { + if (buffer[pos] != str.charAt(i)) { + return false; + } + } + return true; + } + + /** + * Tests the capacity and ensures that it is at least the size specified. + * + * @param capacity the capacity to ensure + * @return this, to enable chaining + */ + public StrBuilder ensureCapacity(final int capacity) { + if (capacity > buffer.length) { + final char[] old = buffer; + buffer = new char[capacity * 2]; + System.arraycopy(old, 0, buffer, 0, size); + } + return this; + } + + /** + * Tests the contents of this builder against another to see if they + * contain the same character content. + * + * @param obj the object to check, null returns false + * @return true if the builders contain the same characters in the same order + */ + @Override + public boolean equals(final Object obj) { + return obj instanceof StrBuilder + && equals((StrBuilder) obj); + } + + /** + * Tests the contents of this builder against another to see if they + * contain the same character content. + * + * @param other the object to check, null returns false + * @return true if the builders contain the same characters in the same order + */ + public boolean equals(final StrBuilder other) { + if (this == other) { + return true; + } + if (other == null) { + return false; + } + if (this.size != other.size) { + return false; + } + final char[] thisBuf = this.buffer; + final char[] otherBuf = other.buffer; + for (int i = size - 1; i >= 0; i--) { + if (thisBuf[i] != otherBuf[i]) { + return false; + } + } + return true; + } + + /** + * Tests the contents of this builder against another to see if they + * contain the same character content ignoring case. + * + * @param other the object to check, null returns false + * @return true if the builders contain the same characters in the same order + */ + public boolean equalsIgnoreCase(final StrBuilder other) { + if (this == other) { + return true; + } + if (this.size != other.size) { + return false; + } + final char[] thisBuf = this.buffer; + final char[] otherBuf = other.buffer; + for (int i = size - 1; i >= 0; i--) { + final char c1 = thisBuf[i]; + final char c2 = otherBuf[i]; + if (c1 != c2 && Character.toUpperCase(c1) != Character.toUpperCase(c2)) { + return false; + } + } + return true; + } + + /** + * Copies the character array into the specified array. + * + * @param destination the destination array, null will cause an array to be created + * @return The input array, unless that was null or too small + */ + public char[] getChars(char[] destination) { + final int len = length(); + if (destination == null || destination.length < len) { + destination = new char[len]; + } + System.arraycopy(buffer, 0, destination, 0, len); + return destination; + } + + /** + * Copies the character array into the specified array. + * + * @param startIndex first index to copy, inclusive, must be valid + * @param endIndex last index, exclusive, must be valid + * @param destination the destination array, must not be null or too small + * @param destinationIndex the index to start copying in destination + * @throws NullPointerException if the array is null + * @throws IndexOutOfBoundsException if any index is invalid + */ + public void getChars(final int startIndex, + final int endIndex, + final char[] destination, + final int destinationIndex) { + if (startIndex < 0) { + throw new StringIndexOutOfBoundsException(startIndex); + } + if (endIndex < 0 || endIndex > length()) { + throw new StringIndexOutOfBoundsException(endIndex); + } + if (startIndex > endIndex) { + throw new StringIndexOutOfBoundsException("end < start"); + } + System.arraycopy(buffer, startIndex, destination, destinationIndex, endIndex - startIndex); + } + + /** + * Gets the text to be appended when a new line is added. + * + * @return The new line text, null means use system default + */ + public String getNewLineText() { + return newLine; + } + + /** + * Gets the text to be appended when null is added. + * + * @return The null text, null means no append + */ + public String getNullText() { + return nullText; + } + + /** + * Gets a suitable hash code for this builder. + * + * @return a hash code + */ + @Override + public int hashCode() { + final char[] buf = buffer; + int hash = 0; + for (int i = size - 1; i >= 0; i--) { + hash = 31 * hash + buf[i]; + } + return hash; + } + + /** + * Searches the string builder to find the first reference to the specified char. + * + * @param ch the character to find + * @return The first index of the character, or -1 if not found + */ + public int indexOf(final char ch) { + return indexOf(ch, 0); + } + + /** + * Searches the string builder to find the first reference to the specified char. + * + * @param ch the character to find + * @param startIndex the index to start at, invalid index rounded to edge + * @return The first index of the character, or -1 if not found + */ + public int indexOf(final char ch, int startIndex) { + startIndex = Math.max(startIndex, 0); + if (startIndex >= size) { + return -1; + } + final char[] thisBuf = buffer; + for (int i = startIndex; i < size; i++) { + if (thisBuf[i] == ch) { + return i; + } + } + return -1; + } + + /** + * Searches the string builder to find the first reference to the specified string. + *

+ * Note that a null input string will return -1, whereas the JDK throws an exception. + *

+ * + * @param str the string to find, null returns -1 + * @return The first index of the string, or -1 if not found + */ + public int indexOf(final String str) { + return indexOf(str, 0); + } + + /** + * Searches the string builder to find the first reference to the specified + * string starting searching from the given index. + *

+ * Note that a null input string will return -1, whereas the JDK throws an exception. + *

+ * + * @param str the string to find, null returns -1 + * @param startIndex the index to start at, invalid index rounded to edge + * @return The first index of the string, or -1 if not found + */ + public int indexOf(final String str, int startIndex) { + startIndex = Math.max(startIndex, 0); + if (str == null || startIndex >= size) { + return -1; + } + final int strLen = str.length(); + if (strLen == 1) { + return indexOf(str.charAt(0), startIndex); + } + if (strLen == 0) { + return startIndex; + } + if (strLen > size) { + return -1; + } + final char[] thisBuf = buffer; + final int len = size - strLen + 1; + outer: + for (int i = startIndex; i < len; i++) { + for (int j = 0; j < strLen; j++) { + if (str.charAt(j) != thisBuf[i + j]) { + continue outer; + } + } + return i; + } + return -1; + } + + /** + * Searches the string builder using the matcher to find the first match. + *

+ * Matchers can be used to perform advanced searching behavior. + * For example you could write a matcher to find the character 'a' + * followed by a number. + *

+ * + * @param matcher the matcher to use, null returns -1 + * @return The first index matched, or -1 if not found + */ + public int indexOf(final StrMatcher matcher) { + return indexOf(matcher, 0); + } + + /** + * Searches the string builder using the matcher to find the first + * match searching from the given index. + *

+ * Matchers can be used to perform advanced searching behavior. + * For example you could write a matcher to find the character 'a' + * followed by a number. + *

+ * + * @param matcher the matcher to use, null returns -1 + * @param startIndex the index to start at, invalid index rounded to edge + * @return The first index matched, or -1 if not found + */ + public int indexOf(final StrMatcher matcher, int startIndex) { + startIndex = Math.max(startIndex, 0); + if (matcher == null || startIndex >= size) { + return -1; + } + final int len = size; + final char[] buf = buffer; + for (int i = startIndex; i < len; i++) { + if (matcher.isMatch(buf, i, startIndex, len) > 0) { + return i; + } + } + return -1; + } + + /** + * Inserts the value into this builder. + * + * @param index the index to add at, must be valid + * @param value the value to insert + * @return this, to enable chaining + * @throws IndexOutOfBoundsException if the index is invalid + */ + public StrBuilder insert(int index, final boolean value) { + validateIndex(index); + if (value) { + ensureCapacity(size + 4); + System.arraycopy(buffer, index, buffer, index + 4, size - index); + buffer[index++] = 't'; + buffer[index++] = 'r'; + buffer[index++] = 'u'; + buffer[index] = 'e'; + size += 4; + } else { + ensureCapacity(size + 5); + System.arraycopy(buffer, index, buffer, index + 5, size - index); + buffer[index++] = 'f'; + buffer[index++] = 'a'; + buffer[index++] = 'l'; + buffer[index++] = 's'; + buffer[index] = 'e'; + size += 5; + } + return this; + } + + /** + * Inserts the value into this builder. + * + * @param index the index to add at, must be valid + * @param value the value to insert + * @return this, to enable chaining + * @throws IndexOutOfBoundsException if the index is invalid + */ + public StrBuilder insert(final int index, final char value) { + validateIndex(index); + ensureCapacity(size + 1); + System.arraycopy(buffer, index, buffer, index + 1, size - index); + buffer[index] = value; + size++; + return this; + } + + /** + * Inserts the character array into this builder. + * Inserting null will use the stored null text value. + * + * @param index the index to add at, must be valid + * @param chars the char array to insert + * @return this, to enable chaining + * @throws IndexOutOfBoundsException if the index is invalid + */ + public StrBuilder insert(final int index, final char[] chars) { + validateIndex(index); + if (chars == null) { + return insert(index, nullText); + } + final int len = chars.length; + if (len > 0) { + ensureCapacity(size + len); + System.arraycopy(buffer, index, buffer, index + len, size - index); + System.arraycopy(chars, 0, buffer, index, len); + size += len; + } + return this; + } + + /** + * Inserts part of the character array into this builder. + * Inserting null will use the stored null text value. + * + * @param index the index to add at, must be valid + * @param chars the char array to insert + * @param offset the offset into the character array to start at, must be valid + * @param length the length of the character array part to copy, must be positive + * @return this, to enable chaining + * @throws IndexOutOfBoundsException if any index is invalid + */ + public StrBuilder insert(final int index, final char[] chars, final int offset, final int length) { + validateIndex(index); + if (chars == null) { + return insert(index, nullText); + } + if (offset < 0 || offset > chars.length) { + throw new StringIndexOutOfBoundsException("Invalid offset: " + offset); + } + if (length < 0 || offset + length > chars.length) { + throw new StringIndexOutOfBoundsException("Invalid length: " + length); + } + if (length > 0) { + ensureCapacity(size + length); + System.arraycopy(buffer, index, buffer, index + length, size - index); + System.arraycopy(chars, offset, buffer, index, length); + size += length; + } + return this; + } + + /** + * Inserts the value into this builder. + * + * @param index the index to add at, must be valid + * @param value the value to insert + * @return this, to enable chaining + * @throws IndexOutOfBoundsException if the index is invalid + */ + public StrBuilder insert(final int index, final double value) { + return insert(index, String.valueOf(value)); + } + + /** + * Inserts the value into this builder. + * + * @param index the index to add at, must be valid + * @param value the value to insert + * @return this, to enable chaining + * @throws IndexOutOfBoundsException if the index is invalid + */ + public StrBuilder insert(final int index, final float value) { + return insert(index, String.valueOf(value)); + } + + /** + * Inserts the value into this builder. + * + * @param index the index to add at, must be valid + * @param value the value to insert + * @return this, to enable chaining + * @throws IndexOutOfBoundsException if the index is invalid + */ + public StrBuilder insert(final int index, final int value) { + return insert(index, String.valueOf(value)); + } + + /** + * Inserts the value into this builder. + * + * @param index the index to add at, must be valid + * @param value the value to insert + * @return this, to enable chaining + * @throws IndexOutOfBoundsException if the index is invalid + */ + public StrBuilder insert(final int index, final long value) { + return insert(index, String.valueOf(value)); + } + + /** + * Inserts the string representation of an object into this builder. + * Inserting null will use the stored null text value. + * + * @param index the index to add at, must be valid + * @param obj the object to insert + * @return this, to enable chaining + * @throws IndexOutOfBoundsException if the index is invalid + */ + public StrBuilder insert(final int index, final Object obj) { + if (obj == null) { + return insert(index, nullText); + } + return insert(index, obj.toString()); + } + + /** + * Inserts the string into this builder. + * Inserting null will use the stored null text value. + * + * @param index the index to add at, must be valid + * @param str the string to insert + * @return this, to enable chaining + * @throws IndexOutOfBoundsException if the index is invalid + */ + public StrBuilder insert(final int index, String str) { + validateIndex(index); + if (str == null) { + str = nullText; + } + if (str != null) { + final int strLen = str.length(); + if (strLen > 0) { + final int newSize = size + strLen; + ensureCapacity(newSize); + System.arraycopy(buffer, index, buffer, index + strLen, size - index); + size = newSize; + str.getChars(0, strLen, buffer, index); + } + } + return this; + } + + /** + * Tests if the string builder is empty (convenience Collections API style method). + *

+ * This method is the same as checking {@link #length()} and is provided to match the + * API of Collections. + *

+ * + * @return {@code true} if the size is {@code 0}. + */ + public boolean isEmpty() { + return size == 0; + } + + /** + * Tests if the string builder is not empty (convenience Collections API style method). + *

+ * This method is the same as checking {@link #length()} and is provided to match the + * API of Collections. + *

+ * + * @return {@code true} if the size is greater than {@code 0}. + * @since 1.10.0 + */ + public boolean isNotEmpty() { + return size > 0; + } + + /** + * Searches the string builder to find the last reference to the specified char. + * + * @param ch the character to find + * @return The last index of the character, or -1 if not found + */ + public int lastIndexOf(final char ch) { + return lastIndexOf(ch, size - 1); + } + + /** + * Searches the string builder to find the last reference to the specified char. + * + * @param ch the character to find + * @param startIndex the index to start at, invalid index rounded to edge + * @return The last index of the character, or -1 if not found + */ + public int lastIndexOf(final char ch, int startIndex) { + startIndex = startIndex >= size ? size - 1 : startIndex; + if (startIndex < 0) { + return -1; + } + for (int i = startIndex; i >= 0; i--) { + if (buffer[i] == ch) { + return i; + } + } + return -1; + } + + /** + * Searches the string builder to find the last reference to the specified string. + *

+ * Note that a null input string will return -1, whereas the JDK throws an exception. + *

+ * + * @param str the string to find, null returns -1 + * @return The last index of the string, or -1 if not found + */ + public int lastIndexOf(final String str) { + return lastIndexOf(str, size - 1); + } + + /** + * Searches the string builder to find the last reference to the specified + * string starting searching from the given index. + *

+ * Note that a null input string will return -1, whereas the JDK throws an exception. + *

+ * + * @param str the string to find, null returns -1 + * @param startIndex the index to start at, invalid index rounded to edge + * @return The last index of the string, or -1 if not found + */ + public int lastIndexOf(final String str, int startIndex) { + startIndex = startIndex >= size ? size - 1 : startIndex; + if (str == null || startIndex < 0) { + return -1; + } + final int strLen = str.length(); + if (strLen > 0 && strLen <= size) { + if (strLen == 1) { + return lastIndexOf(str.charAt(0), startIndex); + } + + outer: + for (int i = startIndex - strLen + 1; i >= 0; i--) { + for (int j = 0; j < strLen; j++) { + if (str.charAt(j) != buffer[i + j]) { + continue outer; + } + } + return i; + } + + } else if (strLen == 0) { + return startIndex; + } + return -1; + } + + /** + * Searches the string builder using the matcher to find the last match. + *

+ * Matchers can be used to perform advanced searching behavior. + * For example you could write a matcher to find the character 'a' + * followed by a number. + *

+ * + * @param matcher the matcher to use, null returns -1 + * @return The last index matched, or -1 if not found + */ + public int lastIndexOf(final StrMatcher matcher) { + return lastIndexOf(matcher, size); + } + + /** + * Searches the string builder using the matcher to find the last + * match searching from the given index. + *

+ * Matchers can be used to perform advanced searching behavior. + * For example you could write a matcher to find the character 'a' + * followed by a number. + *

+ * + * @param matcher the matcher to use, null returns -1 + * @param startIndex the index to start at, invalid index rounded to edge + * @return The last index matched, or -1 if not found + */ + public int lastIndexOf(final StrMatcher matcher, int startIndex) { + startIndex = startIndex >= size ? size - 1 : startIndex; + if (matcher == null || startIndex < 0) { + return -1; + } + final char[] buf = buffer; + final int endIndex = startIndex + 1; + for (int i = startIndex; i >= 0; i--) { + if (matcher.isMatch(buf, i, 0, endIndex) > 0) { + return i; + } + } + return -1; + } + + /** + * Extracts the leftmost characters from the string builder without + * throwing an exception. + *

+ * This method extracts the left {@code length} characters from + * the builder. If this many characters are not available, the whole + * builder is returned. Thus the returned string may be shorter than the + * length requested. + *

+ * + * @param length the number of characters to extract, negative returns empty string + * @return The new string + */ + public String leftString(final int length) { + if (length <= 0) { + return StringUtils.EMPTY; + } + if (length >= size) { + return new String(buffer, 0, size); + } + return new String(buffer, 0, length); + } + + /** + * Gets the length of the string builder. + * + * @return The length + */ + @Override + public int length() { + return size; + } + + /** + * Extracts some characters from the middle of the string builder without + * throwing an exception. + *

+ * This method extracts {@code length} characters from the builder + * at the specified index. + * If the index is negative it is treated as zero. + * If the index is greater than the builder size, it is treated as the builder size. + * If the length is negative, the empty string is returned. + * If insufficient characters are available in the builder, as much as possible is returned. + * Thus the returned string may be shorter than the length requested. + *

+ * + * @param index the index to start at, negative means zero + * @param length the number of characters to extract, negative returns empty string + * @return The new string + */ + public String midString(int index, final int length) { + if (index < 0) { + index = 0; + } + if (length <= 0 || index >= size) { + return StringUtils.EMPTY; + } + if (size <= index + length) { + return new String(buffer, index, size - index); + } + return new String(buffer, index, length); + } + + /** + * Minimizes the capacity to the actual length of the string. + * + * @return this, to enable chaining + */ + public StrBuilder minimizeCapacity() { + if (buffer.length > length()) { + final char[] old = buffer; + buffer = new char[length()]; + System.arraycopy(old, 0, buffer, 0, size); + } + return this; + } + + /** + * If possible, reads chars from the provided {@link Readable} directly into underlying + * character buffer without making extra copies. + * + * @param readable object to read from + * @return The number of characters read + * @throws IOException if an I/O error occurs. + * + * @see #appendTo(Appendable) + */ + public int readFrom(final Readable readable) throws IOException { + final int oldSize = size; + if (readable instanceof Reader) { + final Reader r = (Reader) readable; + ensureCapacity(size + 1); + int read; + while ((read = r.read(buffer, size, buffer.length - size)) != -1) { + size += read; + ensureCapacity(size + 1); + } + } else if (readable instanceof CharBuffer) { + final CharBuffer cb = (CharBuffer) readable; + final int remaining = cb.remaining(); + ensureCapacity(size + remaining); + cb.get(buffer, size, remaining); + size += remaining; + } else { + while (true) { + ensureCapacity(size + 1); + final CharBuffer buf = CharBuffer.wrap(buffer, size, buffer.length - size); + final int read = readable.read(buf); + if (read == -1) { + break; + } + size += read; + } + } + return size - oldSize; + } + + /** + * Replaces a portion of the string builder with another string. + * The length of the inserted string does not have to match the removed length. + * + * @param startIndex the start index, inclusive, must be valid + * @param endIndex the end index, exclusive, must be valid except + * that if too large it is treated as end of string + * @param replaceStr the string to replace with, null means delete range + * @return this, to enable chaining + * @throws IndexOutOfBoundsException if the index is invalid + */ + public StrBuilder replace(final int startIndex, int endIndex, final String replaceStr) { + endIndex = validateRange(startIndex, endIndex); + final int insertLen = replaceStr == null ? 0 : replaceStr.length(); + replaceImpl(startIndex, endIndex, endIndex - startIndex, replaceStr, insertLen); + return this; + } + + /** + * Advanced search and replaces within the builder using a matcher. + *

+ * Matchers can be used to perform advanced behavior. + * For example you could write a matcher to delete all occurrences + * where the character 'a' is followed by a number. + *

+ * + * @param matcher the matcher to use to find the deletion, null causes no action + * @param replaceStr the string to replace the match with, null is a delete + * @param startIndex the start index, inclusive, must be valid + * @param endIndex the end index, exclusive, must be valid except + * that if too large it is treated as end of string + * @param replaceCount the number of times to replace, -1 for replace all + * @return this, to enable chaining + * @throws IndexOutOfBoundsException if start index is invalid + */ + public StrBuilder replace( + final StrMatcher matcher, final String replaceStr, + final int startIndex, int endIndex, final int replaceCount) { + endIndex = validateRange(startIndex, endIndex); + return replaceImpl(matcher, replaceStr, startIndex, endIndex, replaceCount); + } + + /** + * Replaces the search character with the replace character + * throughout the builder. + * + * @param search the search character + * @param replace the replace character + * @return this, to enable chaining + */ + public StrBuilder replaceAll(final char search, final char replace) { + if (search != replace) { + for (int i = 0; i < size; i++) { + if (buffer[i] == search) { + buffer[i] = replace; + } + } + } + return this; + } + + /** + * Replaces the search string with the replace string throughout the builder. + * + * @param searchStr the search string, null causes no action to occur + * @param replaceStr the replace string, null is equivalent to an empty string + * @return this, to enable chaining + */ + public StrBuilder replaceAll(final String searchStr, final String replaceStr) { + final int searchLen = searchStr == null ? 0 : searchStr.length(); + if (searchLen > 0) { + final int replaceLen = replaceStr == null ? 0 : replaceStr.length(); + int index = indexOf(searchStr, 0); + while (index >= 0) { + replaceImpl(index, index + searchLen, searchLen, replaceStr, replaceLen); + index = indexOf(searchStr, index + replaceLen); + } + } + return this; + } + + /** + * Replaces all matches within the builder with the replace string. + *

+ * Matchers can be used to perform advanced replace behavior. + * For example you could write a matcher to replace all occurrences + * where the character 'a' is followed by a number. + *

+ * + * @param matcher the matcher to use to find the deletion, null causes no action + * @param replaceStr the replace string, null is equivalent to an empty string + * @return this, to enable chaining + */ + public StrBuilder replaceAll(final StrMatcher matcher, final String replaceStr) { + return replace(matcher, replaceStr, 0, size, -1); + } + + /** + * Replaces the first instance of the search character with the + * replace character in the builder. + * + * @param search the search character + * @param replace the replace character + * @return this, to enable chaining + */ + public StrBuilder replaceFirst(final char search, final char replace) { + if (search != replace) { + for (int i = 0; i < size; i++) { + if (buffer[i] == search) { + buffer[i] = replace; + break; + } + } + } + return this; + } + + /** + * Replaces the first instance of the search string with the replace string. + * + * @param searchStr the search string, null causes no action to occur + * @param replaceStr the replace string, null is equivalent to an empty string + * @return this, to enable chaining + */ + public StrBuilder replaceFirst(final String searchStr, final String replaceStr) { + final int searchLen = searchStr == null ? 0 : searchStr.length(); + if (searchLen > 0) { + final int index = indexOf(searchStr, 0); + if (index >= 0) { + final int replaceLen = replaceStr == null ? 0 : replaceStr.length(); + replaceImpl(index, index + searchLen, searchLen, replaceStr, replaceLen); + } + } + return this; + } + + /** + * Replaces the first match within the builder with the replace string. + *

+ * Matchers can be used to perform advanced replace behavior. + * For example you could write a matcher to replace + * where the character 'a' is followed by a number. + *

+ * + * @param matcher the matcher to use to find the deletion, null causes no action + * @param replaceStr the replace string, null is equivalent to an empty string + * @return this, to enable chaining + */ + public StrBuilder replaceFirst(final StrMatcher matcher, final String replaceStr) { + return replace(matcher, replaceStr, 0, size, 1); + } + + /** + * Internal method to delete a range without validation. + * + * @param startIndex the start index, must be valid + * @param endIndex the end index (exclusive), must be valid + * @param removeLen the length to remove (endIndex - startIndex), must be valid + * @param insertStr the string to replace with, null means delete range + * @param insertLen the length of the insert string, must be valid + * @throws IndexOutOfBoundsException if any index is invalid + */ + private void replaceImpl(final int startIndex, + final int endIndex, + final int removeLen, + final String insertStr, + final int insertLen) { + final int newSize = size - removeLen + insertLen; + if (insertLen != removeLen) { + ensureCapacity(newSize); + System.arraycopy(buffer, endIndex, buffer, startIndex + insertLen, size - endIndex); + size = newSize; + } + if (insertLen > 0) { + insertStr.getChars(0, insertLen, buffer, startIndex); + } + } + + /** + * Replaces within the builder using a matcher. + *

+ * Matchers can be used to perform advanced behavior. + * For example you could write a matcher to delete all occurrences + * where the character 'a' is followed by a number. + *

+ * + * @param matcher the matcher to use to find the deletion, null causes no action + * @param replaceStr the string to replace the match with, null is a delete + * @param from the start index, must be valid + * @param to the end index (exclusive), must be valid + * @param replaceCount the number of times to replace, -1 for replace all + * @return this, to enable chaining + * @throws IndexOutOfBoundsException if any index is invalid + */ + private StrBuilder replaceImpl( + final StrMatcher matcher, final String replaceStr, + final int from, int to, int replaceCount) { + if (matcher == null || size == 0) { + return this; + } + final int replaceLen = replaceStr == null ? 0 : replaceStr.length(); + for (int i = from; i < to && replaceCount != 0; i++) { + final char[] buf = buffer; + final int removeLen = matcher.isMatch(buf, i, from, to); + if (removeLen > 0) { + replaceImpl(i, i + removeLen, removeLen, replaceStr, replaceLen); + to = to - removeLen + replaceLen; + i = i + replaceLen - 1; + if (replaceCount > 0) { + replaceCount--; + } + } + } + return this; + } + + /** + * Reverses the string builder placing each character in the opposite index. + * + * @return this, to enable chaining + */ + public StrBuilder reverse() { + if (size == 0) { + return this; + } + + final int half = size / 2; + final char[] buf = buffer; + for (int leftIdx = 0, rightIdx = size - 1; leftIdx < half; leftIdx++, rightIdx--) { + final char swap = buf[leftIdx]; + buf[leftIdx] = buf[rightIdx]; + buf[rightIdx] = swap; + } + return this; + } + + /** + * Extracts the rightmost characters from the string builder without + * throwing an exception. + *

+ * This method extracts the right {@code length} characters from + * the builder. If this many characters are not available, the whole + * builder is returned. Thus the returned string may be shorter than the + * length requested. + *

+ * + * @param length the number of characters to extract, negative returns empty string + * @return The new string + */ + public String rightString(final int length) { + if (length <= 0) { + return StringUtils.EMPTY; + } + if (length >= size) { + return new String(buffer, 0, size); + } + return new String(buffer, size - length, length); + } + + /** + * Sets the character at the specified index. + * + * @see #charAt(int) + * @see #deleteCharAt(int) + * @param index the index to set + * @param ch the new character + * @return this, to enable chaining + * @throws IndexOutOfBoundsException if the index is invalid + */ + public StrBuilder setCharAt(final int index, final char ch) { + if (index < 0 || index >= length()) { + throw new StringIndexOutOfBoundsException(index); + } + buffer[index] = ch; + return this; + } + + /** + * Updates the length of the builder by either dropping the last characters + * or adding filler of Unicode zero. + * + * @param length the length to set to, must be zero or positive + * @return this, to enable chaining + * @throws IndexOutOfBoundsException if the length is negative + */ + public StrBuilder setLength(final int length) { + if (length < 0) { + throw new StringIndexOutOfBoundsException(length); + } + if (length < size) { + size = length; + } else if (length > size) { + ensureCapacity(length); + final int oldEnd = size; + size = length; + for (int i = oldEnd; i < length; i++) { + buffer[i] = '\0'; + } + } + return this; + } + + /** + * Sets the text to be appended when a new line is added. + * + * @param newLine the new line text, null means use system default + * @return this, to enable chaining + */ + public StrBuilder setNewLineText(final String newLine) { + this.newLine = newLine; + return this; + } + + /** + * Sets the text to be appended when null is added. + * + * @param nullText the null text, null means no append + * @return this, to enable chaining + */ + public StrBuilder setNullText(String nullText) { + if (nullText != null && nullText.isEmpty()) { + nullText = null; + } + this.nullText = nullText; + return this; + } + + /** + * Gets the length of the string builder. + *

+ * This method is the same as {@link #length()} and is provided to match the + * API of Collections. + *

+ * + * @return The length + */ + public int size() { + return size; + } + + /** + * Checks whether this builder starts with the specified string. + *

+ * Note that this method handles null input quietly, unlike String. + *

+ * + * @param str the string to search for, null returns false + * @return true if the builder starts with the string + */ + public boolean startsWith(final String str) { + if (str == null) { + return false; + } + final int len = str.length(); + if (len == 0) { + return true; + } + if (len > size) { + return false; + } + for (int i = 0; i < len; i++) { + if (buffer[i] != str.charAt(i)) { + return false; + } + } + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public CharSequence subSequence(final int startIndex, final int endIndex) { + if (startIndex < 0) { + throw new StringIndexOutOfBoundsException(startIndex); + } + if (endIndex > size) { + throw new StringIndexOutOfBoundsException(endIndex); + } + if (startIndex > endIndex) { + throw new StringIndexOutOfBoundsException(endIndex - startIndex); + } + return substring(startIndex, endIndex); + } + + /** + * Extracts a portion of this string builder as a string. + * + * @param start the start index, inclusive, must be valid + * @return The new string + * @throws IndexOutOfBoundsException if the index is invalid + */ + public String substring(final int start) { + return substring(start, size); + } + + /** + * Extracts a portion of this string builder as a string. + *

+ * Note: This method treats an endIndex greater than the length of the + * builder as equal to the length of the builder, and continues + * without error, unlike StringBuffer or String. + * + * @param startIndex the start index, inclusive, must be valid + * @param endIndex the end index, exclusive, must be valid except + * that if too large it is treated as end of string + * @return The new string + * @throws IndexOutOfBoundsException if the index is invalid + */ + public String substring(final int startIndex, int endIndex) { + endIndex = validateRange(startIndex, endIndex); + return new String(buffer, startIndex, endIndex - startIndex); + } + + /** + * Copies the builder's character array into a new character array. + * + * @return a new array that represents the contents of the builder + */ + public char[] toCharArray() { + return size == 0 ? ArrayUtils.EMPTY_CHAR_ARRAY : Arrays.copyOf(buffer, size); + } + + /** + * Copies part of the builder's character array into a new character array. + * + * @param startIndex the start index, inclusive, must be valid + * @param endIndex the end index, exclusive, must be valid except that + * if too large it is treated as end of string + * @return a new array that holds part of the contents of the builder + * @throws IndexOutOfBoundsException if startIndex is invalid, + * or if endIndex is invalid (but endIndex greater than size is valid) + */ + public char[] toCharArray(final int startIndex, int endIndex) { + endIndex = validateRange(startIndex, endIndex); + final int len = endIndex - startIndex; + if (len == 0) { + return ArrayUtils.EMPTY_CHAR_ARRAY; + } + final char[] chars = new char[len]; + System.arraycopy(buffer, startIndex, chars, 0, len); + return chars; + } + + /** + * Gets a String version of the string builder, creating a new instance + * each time the method is called. + *

+ * Note that unlike StringBuffer, the string version returned is + * independent of the string builder. + *

+ * + * @return The builder as a String + */ + @Override + public String toString() { + return new String(buffer, 0, size); + } + + /** + * Gets a StringBuffer version of the string builder, creating a + * new instance each time the method is called. + * + * @return The builder as a StringBuffer + */ + public StringBuffer toStringBuffer() { + return new StringBuffer(size).append(buffer, 0, size); + } + + /** + * Gets a StringBuilder version of the string builder, creating a + * new instance each time the method is called. + * + * @return The builder as a StringBuilder + */ + public StringBuilder toStringBuilder() { + return new StringBuilder(size).append(buffer, 0, size); + } + + /** + * Trims the builder by removing characters less than or equal to a space + * from the beginning and end. + * + * @return this, to enable chaining + */ + public StrBuilder trim() { + if (size == 0) { + return this; + } + int len = size; + final char[] buf = buffer; + int pos = 0; + while (pos < len && buf[pos] <= ' ') { + pos++; + } + while (pos < len && buf[len - 1] <= ' ') { + len--; + } + if (len < size) { + delete(len, size); + } + if (pos > 0) { + delete(0, pos); + } + return this; + } + + /** + * Validates parameters defining a single index in the builder. + * + * @param index the index, must be valid + * @throws IndexOutOfBoundsException if the index is invalid + */ + protected void validateIndex(final int index) { + if (index < 0 || index > size) { + throw new StringIndexOutOfBoundsException(index); + } + } + + /** + * Validates parameters defining a range of the builder. + * + * @param startIndex the start index, inclusive, must be valid + * @param endIndex the end index, exclusive, must be valid except + * that if too large it is treated as end of string + * @return The new string + * @throws IndexOutOfBoundsException if the index is invalid + */ + protected int validateRange(final int startIndex, int endIndex) { + if (startIndex < 0) { + throw new StringIndexOutOfBoundsException(startIndex); + } + if (endIndex > size) { + endIndex = size; + } + if (startIndex > endIndex) { + throw new StringIndexOutOfBoundsException("end < start"); + } + return endIndex; + } + +} diff --git a/sources/src/main/java/org/apache/commons/text/StrLookup.java b/sources/src/main/java/org/apache/commons/text/StrLookup.java new file mode 100644 index 0000000..d16a7ed --- /dev/null +++ b/sources/src/main/java/org/apache/commons/text/StrLookup.java @@ -0,0 +1,209 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.ResourceBundle; + +import org.apache.commons.text.lookup.StringLookup; +import org.apache.commons.text.lookup.StringLookupFactory; + +/** + * Lookup a String key to a String value. + *

+ * This class represents the simplest form of a string to string map. It has a benefit over a map in that it can create + * the result on demand based on the key. + *

+ *

+ * This class comes complete with various factory methods. If these do not suffice, you can subclass and implement your + * own matcher. + *

+ *

+ * For example, it would be possible to implement a lookup that used the key as a primary key, and looked up the value + * on demand from the database + *

+ * + * @param the type of the values supported by the lookup + * @since 1.0 + * @deprecated Deprecated as of 1.3, use {@link StringLookupFactory} instead. This class will be removed in 2.0. + */ +@Deprecated +public abstract class StrLookup implements StringLookup { + + /** + * Lookup implementation that uses a Map. + * + * @param the type of the values supported by the lookup + */ + static class MapStrLookup extends StrLookup { + + /** Map keys are variable names and value. */ + private final Map map; + + /** + * Creates a new instance backed by a Map. + * + * @param map the map of keys to values, may be null + */ + MapStrLookup(final Map map) { + this.map = map != null ? map : Collections.emptyMap(); + } + + /** + * Looks up a String key to a String value using the map. + *

+ * If the map is null, then null is returned. The map result object is converted to a string using toString(). + *

+ * + * @param key the key to be looked up, may be null + * @return The matching value, null if no match + */ + @Override + public String lookup(final String key) { + return Objects.toString(map.get(key), null); + } + + @Override + public String toString() { + return super.toString() + " [map=" + map + "]"; + } + } + + /** + * Lookup implementation based on a ResourceBundle. + */ + private static final class ResourceBundleLookup extends StrLookup { + + /** ResourceBundle keys are variable names and value. */ + private final ResourceBundle resourceBundle; + + /** + * Creates a new instance backed by a ResourceBundle. + * + * @param resourceBundle the ResourceBundle of keys to values, may be null + */ + private ResourceBundleLookup(final ResourceBundle resourceBundle) { + this.resourceBundle = resourceBundle; + } + + @Override + public String lookup(final String key) { + if (resourceBundle == null || key == null || !resourceBundle.containsKey(key)) { + return null; + } + return resourceBundle.getString(key); + } + + @Override + public String toString() { + return super.toString() + " [resourceBundle=" + resourceBundle + "]"; + } + + } + + /** + * Lookup implementation based on system properties. + */ + private static final class SystemPropertiesStrLookup extends StrLookup { + /** + * {@inheritDoc} This implementation directly accesses system properties. + */ + @Override + public String lookup(final String key) { + if (!key.isEmpty()) { + try { + return System.getProperty(key); + } catch (final SecurityException ignored) { + // Noop: All lookup(String) will return null. + } + } + return null; + } + } + + /** + * Lookup that always returns null. + */ + private static final StrLookup NONE_LOOKUP = new MapStrLookup<>(null); + + /** + * Lookup based on system properties. + */ + private static final StrLookup SYSTEM_PROPERTIES_LOOKUP = new SystemPropertiesStrLookup(); + + /** + * Returns a lookup which looks up values using a map. + *

+ * If the map is null, then null will be returned from every lookup. The map result object is converted to a string + * using toString(). + *

+ * + * @param the type of the values supported by the lookup + * @param map the map of keys to values, may be null + * @return a lookup using the map, not null + */ + public static StrLookup mapLookup(final Map map) { + return new MapStrLookup<>(map); + } + + /** + * Returns a lookup which always returns null. + * + * @return a lookup that always returns null, not null + */ + public static StrLookup noneLookup() { + return NONE_LOOKUP; + } + + /** + * Returns a lookup which looks up values using a ResourceBundle. + *

+ * If the ResourceBundle is null, then null will be returned from every lookup. The map result object is converted + * to a string using toString(). + *

+ * + * @param resourceBundle the map of keys to values, may be null + * @return a lookup using the map, not null + * @see StringLookupFactory#resourceBundleStringLookup(String) + */ + public static StrLookup resourceBundleLookup(final ResourceBundle resourceBundle) { + return new ResourceBundleLookup(resourceBundle); + } + + /** + * Returns a new lookup which uses a copy of the current {@link System#getProperties() System properties}. + *

+ * If a security manager blocked access to system properties, then null will be returned from every lookup. + *

+ *

+ * If a null key is used, this lookup will throw a NullPointerException. + *

+ * + * @return a lookup using system properties, not null + */ + public static StrLookup systemPropertiesLookup() { + return SYSTEM_PROPERTIES_LOOKUP; + } + + /** + * Constructor. + */ + protected StrLookup() { + } +} diff --git a/sources/src/main/java/org/apache/commons/text/StrMatcher.java b/sources/src/main/java/org/apache/commons/text/StrMatcher.java new file mode 100644 index 0000000..062f670 --- /dev/null +++ b/sources/src/main/java/org/apache/commons/text/StrMatcher.java @@ -0,0 +1,445 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import java.util.Arrays; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.text.matcher.StringMatcherFactory; + +/** + * A matcher class that can be queried to determine if a character array + * portion matches. + *

+ * This class comes complete with various factory methods. + * If these do not suffice, you can subclass and implement your own matcher. + *

+ * + * @since 1.0 + * @deprecated Deprecated as of 1.3, use {@link StringMatcherFactory} instead. This class will be removed in 2.0. + */ +@Deprecated +public abstract class StrMatcher { + + /** + * Class used to define a character for matching purposes. + */ + static final class CharMatcher extends StrMatcher { + /** The character to match. */ + private final char ch; + + /** + * Constructor that creates a matcher that matches a single character. + * + * @param ch the character to match + */ + CharMatcher(final char ch) { + this.ch = ch; + } + + /** + * Returns {@code 1} if there is a match, or {@code 0} if there is no match. + * + * @param buffer the text content to match against, do not change + * @param pos the starting position for the match, valid for buffer + * @param bufferStart the first active index in the buffer, valid for buffer + * @param bufferEnd the end index of the active buffer, valid for buffer + * @return The number of matching characters, or zero if there is no match + */ + @Override + public int isMatch(final char[] buffer, final int pos, final int bufferStart, final int bufferEnd) { + return ch == buffer[pos] ? 1 : 0; + } + } + + /** + * Class used to define a set of characters for matching purposes. + */ + static final class CharSetMatcher extends StrMatcher { + /** The set of characters to match. */ + private final char[] chars; + + /** + * Constructor that creates a matcher from a character array. + * + * @param chars the characters to match, must not be null + */ + CharSetMatcher(final char[] chars) { + this.chars = chars.clone(); + Arrays.sort(this.chars); + } + + /** + * Returns {@code 1} if there is a match, or {@code 0} if there is no match. + * + * @param buffer the text content to match against, do not change + * @param pos the starting position for the match, valid for buffer + * @param bufferStart the first active index in the buffer, valid for buffer + * @param bufferEnd the end index of the active buffer, valid for buffer + * @return The number of matching characters, or zero if there is no match + */ + @Override + public int isMatch(final char[] buffer, final int pos, final int bufferStart, final int bufferEnd) { + return Arrays.binarySearch(chars, buffer[pos]) >= 0 ? 1 : 0; + } + } + + /** + * Class used to match no characters. + */ + static final class NoMatcher extends StrMatcher { + + /** + * Constructs a new instance of {@code NoMatcher}. + */ + NoMatcher() { + } + + /** + * Always returns {@code 0}. + * + * @param buffer the text content to match against, do not change + * @param pos the starting position for the match, valid for buffer + * @param bufferStart the first active index in the buffer, valid for buffer + * @param bufferEnd the end index of the active buffer, valid for buffer + * @return The number of matching characters, or zero if there is no match + */ + @Override + public int isMatch(final char[] buffer, final int pos, final int bufferStart, final int bufferEnd) { + return 0; + } + } + + /** + * Class used to define a set of characters for matching purposes. + */ + static final class StringMatcher extends StrMatcher { + /** The string to match, as a character array. */ + private final char[] chars; + + /** + * Constructor that creates a matcher from a String. + * + * @param str the string to match, must not be null + */ + StringMatcher(final String str) { + chars = str.toCharArray(); + } + + /** + * Returns the number of matching characters, or zero if there is no match. + * + * @param buffer the text content to match against, do not change + * @param pos the starting position for the match, valid for buffer + * @param bufferStart the first active index in the buffer, valid for buffer + * @param bufferEnd the end index of the active buffer, valid for buffer + * @return The number of matching characters, or zero if there is no match + */ + @Override + public int isMatch(final char[] buffer, int pos, final int bufferStart, final int bufferEnd) { + final int len = chars.length; + if (pos + len > bufferEnd) { + return 0; + } + for (int i = 0; i < chars.length; i++, pos++) { + if (chars[i] != buffer[pos]) { + return 0; + } + } + return len; + } + + @Override + public String toString() { + return super.toString() + ' ' + Arrays.toString(chars); + } + + } + + /** + * Class used to match whitespace as per trim(). + */ + static final class TrimMatcher extends StrMatcher { + + /** + * Constructs a new instance of {@code TrimMatcher}. + */ + TrimMatcher() { + } + + /** + * Returns whether or not the given character matches. + * + * @param buffer the text content to match against, do not change + * @param pos the starting position for the match, valid for buffer + * @param bufferStart the first active index in the buffer, valid for buffer + * @param bufferEnd the end index of the active buffer, valid for buffer + * @return The number of matching characters, or zero if there is no match + */ + @Override + public int isMatch(final char[] buffer, final int pos, final int bufferStart, final int bufferEnd) { + return buffer[pos] <= 32 ? 1 : 0; + } + } + + /** + * Matches the comma character. + */ + private static final StrMatcher COMMA_MATCHER = new CharMatcher(','); + + /** + * Matches the tab character. + */ + private static final StrMatcher TAB_MATCHER = new CharMatcher('\t'); + + /** + * Matches the space character. + */ + private static final StrMatcher SPACE_MATCHER = new CharMatcher(' '); + + /** + * Matches the same characters as StringTokenizer, + * namely space, tab, newline, form feed. + */ + private static final StrMatcher SPLIT_MATCHER = new CharSetMatcher(" \t\n\r\f".toCharArray()); + + /** + * Matches the String trim() whitespace characters. + */ + private static final StrMatcher TRIM_MATCHER = new TrimMatcher(); + + /** + * Matches the double quote character. + */ + private static final StrMatcher SINGLE_QUOTE_MATCHER = new CharMatcher('\''); + + /** + * Matches the double quote character. + */ + private static final StrMatcher DOUBLE_QUOTE_MATCHER = new CharMatcher('"'); + + /** + * Matches the single or double quote character. + */ + private static final StrMatcher QUOTE_MATCHER = new CharSetMatcher("'\"".toCharArray()); + + /** + * Matches no characters. + */ + private static final StrMatcher NONE_MATCHER = new NoMatcher(); + + /** + * Creates a matcher from a character. + * + * @param ch the character to match, must not be null + * @return a new Matcher for the given char + */ + public static StrMatcher charMatcher(final char ch) { + return new CharMatcher(ch); + } + + /** + * Creates a matcher from a set of characters. + * + * @param chars the characters to match, null or empty matches nothing + * @return a new matcher for the given char[] + */ + public static StrMatcher charSetMatcher(final char... chars) { + if (ArrayUtils.isEmpty(chars)) { + return NONE_MATCHER; + } + if (chars.length == 1) { + return new CharMatcher(chars[0]); + } + return new CharSetMatcher(chars); + } + + /** + * Creates a matcher from a string representing a set of characters. + * + * @param chars the characters to match, null or empty matches nothing + * @return a new Matcher for the given characters + */ + public static StrMatcher charSetMatcher(final String chars) { + if (chars == null || chars.isEmpty()) { + return NONE_MATCHER; + } + if (chars.length() == 1) { + return new CharMatcher(chars.charAt(0)); + } + return new CharSetMatcher(chars.toCharArray()); + } + + /** + * Returns a matcher which matches the comma character. + * + * @return a matcher for a comma + */ + public static StrMatcher commaMatcher() { + return COMMA_MATCHER; + } + + /** + * Returns a matcher which matches the double quote character. + * + * @return a matcher for a double quote + */ + public static StrMatcher doubleQuoteMatcher() { + return DOUBLE_QUOTE_MATCHER; + } + + /** + * Matches no characters. + * + * @return a matcher that matches nothing + */ + public static StrMatcher noneMatcher() { + return NONE_MATCHER; + } + + /** + * Returns a matcher which matches the single or double quote character. + * + * @return a matcher for a single or double quote + */ + public static StrMatcher quoteMatcher() { + return QUOTE_MATCHER; + } + + /** + * Returns a matcher which matches the single quote character. + * + * @return a matcher for a single quote + */ + public static StrMatcher singleQuoteMatcher() { + return SINGLE_QUOTE_MATCHER; + } + + /** + * Returns a matcher which matches the space character. + * + * @return a matcher for a space + */ + public static StrMatcher spaceMatcher() { + return SPACE_MATCHER; + } + + /** + * Matches the same characters as StringTokenizer, + * namely space, tab, newline and form feed. + * + * @return The split matcher + */ + public static StrMatcher splitMatcher() { + return SPLIT_MATCHER; + } + + /** + * Creates a matcher from a string. + * + * @param str the string to match, null or empty matches nothing + * @return a new Matcher for the given String + */ + public static StrMatcher stringMatcher(final String str) { + if (str == null || str.isEmpty()) { + return NONE_MATCHER; + } + return new StringMatcher(str); + } + + /** + * Returns a matcher which matches the tab character. + * + * @return a matcher for a tab + */ + public static StrMatcher tabMatcher() { + return TAB_MATCHER; + } + + /** + * Matches the String trim() whitespace characters. + * + * @return The trim matcher + */ + public static StrMatcher trimMatcher() { + return TRIM_MATCHER; + } + + /** + * Constructor. + */ + protected StrMatcher() { + } + + /** + * Returns the number of matching characters, or zero if there is no match. + *

+ * This method is called to check for a match. + * The parameter {@code pos} represents the current position to be + * checked in the string {@code buffer} (a character array which must + * not be changed). + * The API guarantees that {@code pos} is a valid index for {@code buffer}. + *

+ *

+ * The matching code may check one character or many. + * It may check characters preceding {@code pos} as well as those after. + *

+ *

+ * It must return zero for no match, or a positive number if a match was found. + * The number indicates the number of characters that matched. + *

+ * + * @param buffer the text content to match against, do not change + * @param pos the starting position for the match, valid for buffer + * @return The number of matching characters, or zero if there is no match + */ + public int isMatch(final char[] buffer, final int pos) { + return isMatch(buffer, pos, 0, buffer.length); + } + + /** + * Returns the number of matching characters, or zero if there is no match. + *

+ * This method is called to check for a match. + * The parameter {@code pos} represents the current position to be + * checked in the string {@code buffer} (a character array which must + * not be changed). + * The API guarantees that {@code pos} is a valid index for {@code buffer}. + *

+ *

+ * The character array may be larger than the active area to be matched. + * Only values in the buffer between the specified indices may be accessed. + *

+ *

+ * The matching code may check one character or many. + * It may check characters preceding {@code pos} as well as those + * after, so long as no checks exceed the bounds specified. + *

+ *

+ * It must return zero for no match, or a positive number if a match was found. + * The number indicates the number of characters that matched. + *

+ * + * @param buffer the text content to match against, do not change + * @param pos the starting position for the match, valid for buffer + * @param bufferStart the first active index in the buffer, valid for buffer + * @param bufferEnd the end index (exclusive) of the active buffer, valid for buffer + * @return The number of matching characters, or zero if there is no match + */ + public abstract int isMatch(char[] buffer, int pos, int bufferStart, int bufferEnd); + +} diff --git a/sources/src/main/java/org/apache/commons/text/StrSubstitutor.java b/sources/src/main/java/org/apache/commons/text/StrSubstitutor.java new file mode 100644 index 0000000..8a71383 --- /dev/null +++ b/sources/src/main/java/org/apache/commons/text/StrSubstitutor.java @@ -0,0 +1,1279 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.apache.commons.lang3.Validate; + +/** + * Substitutes variables within a string by values. + *

+ * This class takes a piece of text and substitutes all the variables within it. + * The default definition of a variable is {@code ${variableName}}. + * The prefix and suffix can be changed via constructors and set methods. + *

+ * Variable values are typically resolved from a map, but could also be resolved + * from system properties, or by supplying a custom variable resolver. + *

+ * The simplest example is to use this class to replace Java System properties. For example: + *

+ * StrSubstitutor.replaceSystemProperties(
+ *      "You are running with java.version = ${java.version} and os.name = ${os.name}.");
+ * 
+ *

+ * Typical usage of this class follows the following pattern: First an instance is created + * and initialized with the map that contains the values for the available variables. + * If a prefix and/or suffix for variables should be used other than the default ones, + * the appropriate settings can be performed. After that the {@code replace()} + * method can be called passing in the source text for interpolation. In the returned + * text all variable references (as long as their values are known) will be resolved. + * The following example demonstrates this: + *

+ * Map<String, String> valuesMap = new HashMap<>();
+ * valuesMap.put("animal", "quick brown fox");
+ * valuesMap.put("target", "lazy dog");
+ * String templateString = "The ${animal} jumped over the ${target}.";
+ * StrSubstitutor sub = new StrSubstitutor(valuesMap);
+ * String resolvedString = sub.replace(templateString);
+ * 
+ * yielding: + *
+ *      The quick brown fox jumped over the lazy dog.
+ * 
+ *

+ * Also, this class allows to set a default value for unresolved variables. + * The default value for a variable can be appended to the variable name after the variable + * default value delimiter. The default value of the variable default value delimiter is ':-', + * as in bash and other *nix shells, as those are arguably where the default ${} delimiter set originated. + * The variable default value delimiter can be manually set by calling {@link #setValueDelimiterMatcher(StrMatcher)}, + * {@link #setValueDelimiter(char)} or {@link #setValueDelimiter(String)}. + * The following shows an example with variable default value settings: + *

+ * Map<String, String> valuesMap = new HashMap<>();
+ * valuesMap.put("animal", "quick brown fox");
+ * valuesMap.put("target", "lazy dog");
+ * String templateString = "The ${animal} jumped over the ${target}. ${undefined.number:-1234567890}.";
+ * StrSubstitutor sub = new StrSubstitutor(valuesMap);
+ * String resolvedString = sub.replace(templateString);
+ * 
+ * yielding: + *
+ *      The quick brown fox jumped over the lazy dog. 1234567890.
+ * 
+ *

+ * In addition to this usage pattern there are some static convenience methods that + * cover the most common use cases. These methods can be used without the need of + * manually creating an instance. However if multiple replace operations are to be + * performed, creating and reusing an instance of this class will be more efficient. + *

+ * Variable replacement works in a recursive way. Thus, if a variable value contains + * a variable then that variable will also be replaced. Cyclic replacements are + * detected and will cause an exception to be thrown. + *

+ * Sometimes the interpolation's result must contain a variable prefix. As an example + * take the following source text: + *

+ *   The variable ${${name}} must be used.
+ * 
+ * Here only the variable's name referred to in the text should be replaced resulting + * in the text (assuming that the value of the {@code name} variable is {@code x}): + *
+ *   The variable ${x} must be used.
+ * 
+ * To achieve this effect there are two possibilities: Either set a different prefix + * and suffix for variables which do not conflict with the result text you want to + * produce. The other possibility is to use the escape character, by default '$'. + * If this character is placed before a variable reference, this reference is ignored + * and won't be replaced. For example: + *
+ *   The variable $${${name}} must be used.
+ * 
+ *

+ * In some complex scenarios you might even want to perform substitution in the + * names of variables, for instance + *

+ * ${jre-${java.specification.version}}
+ * 
+ * {@code StrSubstitutor} supports this recursive substitution in variable + * names, but it has to be enabled explicitly by setting the + * {@link #setEnableSubstitutionInVariables(boolean) enableSubstitutionInVariables} + * property to true. + *

This class is not thread safe.

+ * + * @since 1.0 + * @deprecated Deprecated as of 1.3, use {@link StringSubstitutor} instead. This class will be removed in 2.0. + */ +@Deprecated +public class StrSubstitutor { + + /** + * Constant for the default escape character. + */ + public static final char DEFAULT_ESCAPE = '$'; + + /** + * Constant for the default variable prefix. + */ + public static final StrMatcher DEFAULT_PREFIX = StrMatcher.stringMatcher("${"); + + /** + * Constant for the default variable suffix. + */ + public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}"); + + /** + * Constant for the default value delimiter of a variable. + */ + public static final StrMatcher DEFAULT_VALUE_DELIMITER = StrMatcher.stringMatcher(":-"); + + /** + * Replaces all the occurrences of variables in the given source object with + * their matching values from the map. + * + * @param the type of the values in the map + * @param source the source text containing the variables to substitute, null returns null + * @param valueMap the map with the values, may be null + * @return The result of the replace operation + */ + public static String replace(final Object source, final Map valueMap) { + return new StrSubstitutor(valueMap).replace(source); + } + + /** + * Replaces all the occurrences of variables in the given source object with + * their matching values from the map. This method allows to specify a + * custom variable prefix and suffix + * + * @param the type of the values in the map + * @param source the source text containing the variables to substitute, null returns null + * @param valueMap the map with the values, may be null + * @param prefix the prefix of variables, not null + * @param suffix the suffix of variables, not null + * @return The result of the replace operation + * @throws IllegalArgumentException if the prefix or suffix is null + */ + public static String replace(final Object source, + final Map valueMap, + final String prefix, + final String suffix) { + return new StrSubstitutor(valueMap, prefix, suffix).replace(source); + } + + /** + * Replaces all the occurrences of variables in the given source object with their matching + * values from the properties. + * + * @param source the source text containing the variables to substitute, null returns null + * @param valueProperties the properties with values, may be null + * @return The result of the replace operation + */ + public static String replace(final Object source, final Properties valueProperties) { + if (valueProperties == null) { + return source.toString(); + } + final Map valueMap = new HashMap<>(); + final Enumeration propNames = valueProperties.propertyNames(); + while (propNames.hasMoreElements()) { + final String propName = String.valueOf(propNames.nextElement()); + final String propValue = valueProperties.getProperty(propName); + valueMap.put(propName, propValue); + } + return StrSubstitutor.replace(source, valueMap); + } + + /** + * Replaces all the occurrences of variables in the given source object with + * their matching values from the system properties. + * + * @param source the source text containing the variables to substitute, null returns null + * @return The result of the replace operation + */ + public static String replaceSystemProperties(final Object source) { + return new StrSubstitutor(StrLookup.systemPropertiesLookup()).replace(source); + } + + /** + * Stores the escape character. + */ + private char escapeChar; + + /** + * Stores the variable prefix. + */ + private StrMatcher prefixMatcher; + + /** + * Stores the variable suffix. + */ + private StrMatcher suffixMatcher; + + /** + * Stores the default variable value delimiter. + */ + private StrMatcher valueDelimiterMatcher; + + /** + * Variable resolution is delegated to an implementor of VariableResolver. + */ + private StrLookup variableResolver; + + /** + * The flag whether substitution in variable names is enabled. + */ + private boolean enableSubstitutionInVariables; + + /** + * Whether escapes should be preserved. Default is false; + */ + private boolean preserveEscapes; + + /** + * The flag whether substitution in variable values is disabled. + */ + private boolean disableSubstitutionInValues; + + /** + * Constructs a new instance with defaults for variable prefix and suffix + * and the escaping character. + */ + public StrSubstitutor() { + this((StrLookup) null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE); + } + + /** + * Constructs a new instance and initializes it. Uses defaults for variable + * prefix and suffix and the escaping character. + * + * @param the type of the values in the map + * @param valueMap the map with the variables' values, may be null + */ + public StrSubstitutor(final Map valueMap) { + this(StrLookup.mapLookup(valueMap), DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE); + } + + /** + * Constructs a new instance and initializes it. Uses a default escaping character. + * + * @param the type of the values in the map + * @param valueMap the map with the variables' values, may be null + * @param prefix the prefix for variables, not null + * @param suffix the suffix for variables, not null + * @throws IllegalArgumentException if the prefix or suffix is null + */ + public StrSubstitutor(final Map valueMap, final String prefix, final String suffix) { + this(StrLookup.mapLookup(valueMap), prefix, suffix, DEFAULT_ESCAPE); + } + + /** + * Constructs a new instance and initializes it. + * + * @param the type of the values in the map + * @param valueMap the map with the variables' values, may be null + * @param prefix the prefix for variables, not null + * @param suffix the suffix for variables, not null + * @param escape the escape character + * @throws IllegalArgumentException if the prefix or suffix is null + */ + public StrSubstitutor(final Map valueMap, final String prefix, final String suffix, + final char escape) { + this(StrLookup.mapLookup(valueMap), prefix, suffix, escape); + } + + /** + * Constructs a new instance and initializes it. + * + * @param the type of the values in the map + * @param valueMap the map with the variables' values, may be null + * @param prefix the prefix for variables, not null + * @param suffix the suffix for variables, not null + * @param escape the escape character + * @param valueDelimiter the variable default value delimiter, may be null + * @throws IllegalArgumentException if the prefix or suffix is null + */ + public StrSubstitutor(final Map valueMap, final String prefix, final String suffix, + final char escape, final String valueDelimiter) { + this(StrLookup.mapLookup(valueMap), prefix, suffix, escape, valueDelimiter); + } + + /** + * Constructs a new instance and initializes it. + * + * @param variableResolver the variable resolver, may be null + */ + public StrSubstitutor(final StrLookup variableResolver) { + this(variableResolver, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE); + } + + /** + * Constructs a new instance and initializes it. + * + * @param variableResolver the variable resolver, may be null + * @param prefix the prefix for variables, not null + * @param suffix the suffix for variables, not null + * @param escape the escape character + * @throws IllegalArgumentException if the prefix or suffix is null + */ + public StrSubstitutor(final StrLookup variableResolver, final String prefix, final String suffix, + final char escape) { + this.setVariableResolver(variableResolver); + this.setVariablePrefix(prefix); + this.setVariableSuffix(suffix); + this.setEscapeChar(escape); + this.setValueDelimiterMatcher(DEFAULT_VALUE_DELIMITER); + } + + /** + * Constructs a new instance and initializes it. + * + * @param variableResolver the variable resolver, may be null + * @param prefix the prefix for variables, not null + * @param suffix the suffix for variables, not null + * @param escape the escape character + * @param valueDelimiter the variable default value delimiter string, may be null + * @throws IllegalArgumentException if the prefix or suffix is null + */ + public StrSubstitutor(final StrLookup variableResolver, final String prefix, final String suffix, + final char escape, final String valueDelimiter) { + this.setVariableResolver(variableResolver); + this.setVariablePrefix(prefix); + this.setVariableSuffix(suffix); + this.setEscapeChar(escape); + this.setValueDelimiter(valueDelimiter); + } + + /** + * Constructs a new instance and initializes it. + * + * @param variableResolver the variable resolver, may be null + * @param prefixMatcher the prefix for variables, not null + * @param suffixMatcher the suffix for variables, not null + * @param escape the escape character + * @throws IllegalArgumentException if the prefix or suffix is null + */ + public StrSubstitutor( + final StrLookup variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher, + final char escape) { + this(variableResolver, prefixMatcher, suffixMatcher, escape, DEFAULT_VALUE_DELIMITER); + } + + /** + * Constructs a new instance and initializes it. + * + * @param variableResolver the variable resolver, may be null + * @param prefixMatcher the prefix for variables, not null + * @param suffixMatcher the suffix for variables, not null + * @param escape the escape character + * @param valueDelimiterMatcher the variable default value delimiter matcher, may be null + * @throws IllegalArgumentException if the prefix or suffix is null + */ + public StrSubstitutor( + final StrLookup variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher, + final char escape, final StrMatcher valueDelimiterMatcher) { + this.setVariableResolver(variableResolver); + this.setVariablePrefixMatcher(prefixMatcher); + this.setVariableSuffixMatcher(suffixMatcher); + this.setEscapeChar(escape); + this.setValueDelimiterMatcher(valueDelimiterMatcher); + } + + /** + * Checks if the specified variable is already in the stack (list) of variables. + * + * @param varName the variable name to check + * @param priorVariables the list of prior variables + */ + private void checkCyclicSubstitution(final String varName, final List priorVariables) { + if (!priorVariables.contains(varName)) { + return; + } + final StrBuilder buf = new StrBuilder(256); + buf.append("Infinite loop in property interpolation of "); + buf.append(priorVariables.remove(0)); + buf.append(": "); + buf.appendWithSeparators(priorVariables, "->"); + throw new IllegalStateException(buf.toString()); + } + + /** + * Returns the escape character. + * + * @return The character used for escaping variable references + */ + public char getEscapeChar() { + return this.escapeChar; + } + + /** + * Gets the variable default value delimiter matcher currently in use. + *

+ * The variable default value delimiter is the character or characters that delimit the + * variable name and the variable default value. This delimiter is expressed in terms of a matcher + * allowing advanced variable default value delimiter matches. + *

+ *

+ * If it returns null, then the variable default value resolution is disabled. + *

+ * + * @return The variable default value delimiter matcher in use, may be null + */ + public StrMatcher getValueDelimiterMatcher() { + return valueDelimiterMatcher; + } + + /** + * Gets the variable prefix matcher currently in use. + *

+ * The variable prefix is the character or characters that identify the + * start of a variable. This prefix is expressed in terms of a matcher + * allowing advanced prefix matches. + *

+ * + * @return The prefix matcher in use + */ + public StrMatcher getVariablePrefixMatcher() { + return prefixMatcher; + } + + /** + * Gets the VariableResolver that is used to lookup variables. + * + * @return The VariableResolver + */ + public StrLookup getVariableResolver() { + return this.variableResolver; + } + + /** + * Gets the variable suffix matcher currently in use. + *

+ * The variable suffix is the character or characters that identify the + * end of a variable. This suffix is expressed in terms of a matcher + * allowing advanced suffix matches. + *

+ * + * @return The suffix matcher in use + */ + public StrMatcher getVariableSuffixMatcher() { + return suffixMatcher; + } + + /** + * Returns a flag whether substitution is disabled in variable values.If set to + * true, the values of variables can contain other variables will not be + * processed and substituted original variable is evaluated, e.g. + *
+     * Map<String, String> valuesMap = new HashMap<>();
+     * valuesMap.put("name", "Douglas ${surname}");
+     * valuesMap.put("surname", "Crockford");
+     * String templateString = "Hi ${name}";
+     * StrSubstitutor sub = new StrSubstitutor(valuesMap);
+     * String resolvedString = sub.replace(templateString);
+     * 
+ * yielding: + *
+     *      Hi Douglas ${surname}
+     * 
+ * + * @return The substitution in variable values flag + * + * @since 1.2 + */ + public boolean isDisableSubstitutionInValues() { + return disableSubstitutionInValues; + } + + /** + * Returns a flag whether substitution is done in variable names. + * + * @return The substitution in variable names flag + */ + public boolean isEnableSubstitutionInVariables() { + return enableSubstitutionInVariables; + } + + /** + * Returns the flag controlling whether escapes are preserved during + * substitution. + * + * @return The preserve escape flag + */ + public boolean isPreserveEscapes() { + return preserveEscapes; + } + + /** + * Replaces all the occurrences of variables with their matching values + * from the resolver using the given source array as a template. + * The array is not altered by this method. + * + * @param source the character array to replace in, not altered, null returns null + * @return The result of the replace operation + */ + public String replace(final char[] source) { + if (source == null) { + return null; + } + final StrBuilder buf = new StrBuilder(source.length).append(source); + substitute(buf, 0, source.length); + return buf.toString(); + } + + /** + * Replaces all the occurrences of variables with their matching values + * from the resolver using the given source array as a template. + * The array is not altered by this method. + *

+ * Only the specified portion of the array will be processed. + * The rest of the array is not processed, and is not returned. + *

+ * + * @param source the character array to replace in, not altered, null returns null + * @param offset the start offset within the array, must be valid + * @param length the length within the array to be processed, must be valid + * @return The result of the replace operation + */ + public String replace(final char[] source, final int offset, final int length) { + if (source == null) { + return null; + } + final StrBuilder buf = new StrBuilder(length).append(source, offset, length); + substitute(buf, 0, length); + return buf.toString(); + } + + /** + * Replaces all the occurrences of variables with their matching values + * from the resolver using the given source as a template. + * The source is not altered by this method. + * + * @param source the buffer to use as a template, not changed, null returns null + * @return The result of the replace operation + */ + public String replace(final CharSequence source) { + if (source == null) { + return null; + } + return replace(source, 0, source.length()); + } + + /** + * Replaces all the occurrences of variables with their matching values + * from the resolver using the given source as a template. + * The source is not altered by this method. + *

+ * Only the specified portion of the buffer will be processed. + * The rest of the buffer is not processed, and is not returned. + *

+ * + * @param source the buffer to use as a template, not changed, null returns null + * @param offset the start offset within the array, must be valid + * @param length the length within the array to be processed, must be valid + * @return The result of the replace operation + */ + public String replace(final CharSequence source, final int offset, final int length) { + if (source == null) { + return null; + } + final StrBuilder buf = new StrBuilder(length).append(source, offset, length); + substitute(buf, 0, length); + return buf.toString(); + } + + /** + * Replaces all the occurrences of variables in the given source object with + * their matching values from the resolver. The input source object is + * converted to a string using {@code toString} and is not altered. + * + * @param source the source to replace in, null returns null + * @return The result of the replace operation + */ + public String replace(final Object source) { + if (source == null) { + return null; + } + final StrBuilder buf = new StrBuilder().append(source); + substitute(buf, 0, buf.length()); + return buf.toString(); + } + + /** + * Replaces all the occurrences of variables with their matching values + * from the resolver using the given source builder as a template. + * The builder is not altered by this method. + * + * @param source the builder to use as a template, not changed, null returns null + * @return The result of the replace operation + */ + public String replace(final StrBuilder source) { + if (source == null) { + return null; + } + final StrBuilder buf = new StrBuilder(source.length()).append(source); + substitute(buf, 0, buf.length()); + return buf.toString(); + } + + /** + * Replaces all the occurrences of variables with their matching values + * from the resolver using the given source builder as a template. + * The builder is not altered by this method. + *

+ * Only the specified portion of the builder will be processed. + * The rest of the builder is not processed, and is not returned. + *

+ * + * @param source the builder to use as a template, not changed, null returns null + * @param offset the start offset within the array, must be valid + * @param length the length within the array to be processed, must be valid + * @return The result of the replace operation + */ + public String replace(final StrBuilder source, final int offset, final int length) { + if (source == null) { + return null; + } + final StrBuilder buf = new StrBuilder(length).append(source, offset, length); + substitute(buf, 0, length); + return buf.toString(); + } + + /** + * Replaces all the occurrences of variables with their matching values + * from the resolver using the given source string as a template. + * + * @param source the string to replace in, null returns null + * @return The result of the replace operation + */ + public String replace(final String source) { + if (source == null) { + return null; + } + final StrBuilder buf = new StrBuilder(source); + if (!substitute(buf, 0, source.length())) { + return source; + } + return buf.toString(); + } + + /** + * Replaces all the occurrences of variables with their matching values + * from the resolver using the given source string as a template. + *

+ * Only the specified portion of the string will be processed. + * The rest of the string is not processed, and is not returned. + *

+ * + * @param source the string to replace in, null returns null + * @param offset the start offset within the array, must be valid + * @param length the length within the array to be processed, must be valid + * @return The result of the replace operation + */ + public String replace(final String source, final int offset, final int length) { + if (source == null) { + return null; + } + final StrBuilder buf = new StrBuilder(length).append(source, offset, length); + if (!substitute(buf, 0, length)) { + return source.substring(offset, offset + length); + } + return buf.toString(); + } + + /** + * Replaces all the occurrences of variables with their matching values + * from the resolver using the given source buffer as a template. + * The buffer is not altered by this method. + * + * @param source the buffer to use as a template, not changed, null returns null + * @return The result of the replace operation + */ + public String replace(final StringBuffer source) { + if (source == null) { + return null; + } + final StrBuilder buf = new StrBuilder(source.length()).append(source); + substitute(buf, 0, buf.length()); + return buf.toString(); + } + + /** + * Replaces all the occurrences of variables with their matching values + * from the resolver using the given source buffer as a template. + * The buffer is not altered by this method. + *

+ * Only the specified portion of the buffer will be processed. + * The rest of the buffer is not processed, and is not returned. + *

+ * + * @param source the buffer to use as a template, not changed, null returns null + * @param offset the start offset within the array, must be valid + * @param length the length within the array to be processed, must be valid + * @return The result of the replace operation + */ + public String replace(final StringBuffer source, final int offset, final int length) { + if (source == null) { + return null; + } + final StrBuilder buf = new StrBuilder(length).append(source, offset, length); + substitute(buf, 0, length); + return buf.toString(); + } + + /** + * Replaces all the occurrences of variables within the given source + * builder with their matching values from the resolver. + * + * @param source the builder to replace in, updated, null returns zero + * @return true if altered + */ + public boolean replaceIn(final StrBuilder source) { + if (source == null) { + return false; + } + return substitute(source, 0, source.length()); + } + + /** + * Replaces all the occurrences of variables within the given source + * builder with their matching values from the resolver. + *

+ * Only the specified portion of the builder will be processed. + * The rest of the builder is not processed, but it is not deleted. + *

+ * + * @param source the builder to replace in, null returns zero + * @param offset the start offset within the array, must be valid + * @param length the length within the builder to be processed, must be valid + * @return true if altered + */ + public boolean replaceIn(final StrBuilder source, final int offset, final int length) { + if (source == null) { + return false; + } + return substitute(source, offset, length); + } + + /** + * Replaces all the occurrences of variables within the given source buffer + * with their matching values from the resolver. + * The buffer is updated with the result. + * + * @param source the buffer to replace in, updated, null returns zero + * @return true if altered + */ + public boolean replaceIn(final StringBuffer source) { + if (source == null) { + return false; + } + return replaceIn(source, 0, source.length()); + } + + /** + * Replaces all the occurrences of variables within the given source buffer + * with their matching values from the resolver. + * The buffer is updated with the result. + *

+ * Only the specified portion of the buffer will be processed. + * The rest of the buffer is not processed, but it is not deleted. + *

+ * + * @param source the buffer to replace in, updated, null returns zero + * @param offset the start offset within the array, must be valid + * @param length the length within the buffer to be processed, must be valid + * @return true if altered + */ + public boolean replaceIn(final StringBuffer source, final int offset, final int length) { + if (source == null) { + return false; + } + final StrBuilder buf = new StrBuilder(length).append(source, offset, length); + if (!substitute(buf, 0, length)) { + return false; + } + source.replace(offset, offset + length, buf.toString()); + return true; + } + + /** + * Replaces all the occurrences of variables within the given source buffer + * with their matching values from the resolver. + * The buffer is updated with the result. + * + * @param source the buffer to replace in, updated, null returns zero + * @return true if altered + */ + public boolean replaceIn(final StringBuilder source) { + if (source == null) { + return false; + } + return replaceIn(source, 0, source.length()); + } + + /** + * Replaces all the occurrences of variables within the given source builder + * with their matching values from the resolver. + * The builder is updated with the result. + *

+ * Only the specified portion of the buffer will be processed. + * The rest of the buffer is not processed, but it is not deleted. + *

+ * + * @param source the buffer to replace in, updated, null returns zero + * @param offset the start offset within the array, must be valid + * @param length the length within the buffer to be processed, must be valid + * @return true if altered + */ + public boolean replaceIn(final StringBuilder source, final int offset, final int length) { + if (source == null) { + return false; + } + final StrBuilder buf = new StrBuilder(length).append(source, offset, length); + if (!substitute(buf, 0, length)) { + return false; + } + source.replace(offset, offset + length, buf.toString()); + return true; + } + + /** + * Internal method that resolves the value of a variable. + *

+ * Most users of this class do not need to call this method. This method is + * called automatically by the substitution process. + *

+ *

+ * Writers of subclasses can override this method if they need to alter + * how each substitution occurs. The method is passed the variable's name + * and must return the corresponding value. This implementation uses the + * {@link #getVariableResolver()} with the variable's name as the key. + *

+ * + * @param variableName the name of the variable, not null + * @param buf the buffer where the substitution is occurring, not null + * @param startPos the start position of the variable including the prefix, valid + * @param endPos the end position of the variable including the suffix, valid + * @return The variable's value or null if the variable is unknown + */ + protected String resolveVariable(final String variableName, + final StrBuilder buf, + final int startPos, + final int endPos) { + final StrLookup resolver = getVariableResolver(); + if (resolver == null) { + return null; + } + return resolver.lookup(variableName); + } + + /** + * Sets a flag whether substitution is done in variable values (recursive). + * + * @param disableSubstitutionInValues true if substitution in variable value are disabled + * + * @since 1.2 + */ + public void setDisableSubstitutionInValues(final boolean disableSubstitutionInValues) { + this.disableSubstitutionInValues = disableSubstitutionInValues; + } + + /** + * Sets a flag whether substitution is done in variable names. If set to + * true, the names of variables can contain other variables which are + * processed first before the original variable is evaluated, e.g. + * {@code ${jre-${java.version}}}. The default value is false. + * + * @param enableSubstitutionInVariables the new value of the flag + */ + public void setEnableSubstitutionInVariables( + final boolean enableSubstitutionInVariables) { + this.enableSubstitutionInVariables = enableSubstitutionInVariables; + } + + /** + * Sets the escape character. + * If this character is placed before a variable reference in the source + * text, this variable will be ignored. + * + * @param escapeCharacter the escape character (0 for disabling escaping) + */ + public void setEscapeChar(final char escapeCharacter) { + this.escapeChar = escapeCharacter; + } + + /** + * Sets a flag controlling whether escapes are preserved during + * substitution. If set to true, the escape character is retained + * during substitution (e.g. {@code $${this-is-escaped}} remains + * {@code $${this-is-escaped}}). If set to false, the escape + * character is removed during substitution (e.g. + * {@code $${this-is-escaped}} becomes + * {@code ${this-is-escaped}}). The default value is false + * + * @param preserveEscapes true if escapes are to be preserved + */ + public void setPreserveEscapes(final boolean preserveEscapes) { + this.preserveEscapes = preserveEscapes; + } + + /** + * Sets the variable default value delimiter to use. + *

+ * The variable default value delimiter is the character or characters that delimit the + * variable name and the variable default value. This method allows a single character + * variable default value delimiter to be easily set. + *

+ * + * @param valueDelimiter the variable default value delimiter character to use + * @return this, to enable chaining + */ + public StrSubstitutor setValueDelimiter(final char valueDelimiter) { + return setValueDelimiterMatcher(StrMatcher.charMatcher(valueDelimiter)); + } + + /** + * Sets the variable default value delimiter to use. + *

+ * The variable default value delimiter is the character or characters that delimit the + * variable name and the variable default value. This method allows a string + * variable default value delimiter to be easily set. + *

+ *

+ * If the {@code valueDelimiter} is null or empty string, then the variable default + * value resolution becomes disabled. + *

+ * + * @param valueDelimiter the variable default value delimiter string to use, may be null or empty + * @return this, to enable chaining + */ + public StrSubstitutor setValueDelimiter(final String valueDelimiter) { + if (valueDelimiter == null || valueDelimiter.isEmpty()) { + setValueDelimiterMatcher(null); + return this; + } + return setValueDelimiterMatcher(StrMatcher.stringMatcher(valueDelimiter)); + } + + /** + * Sets the variable default value delimiter matcher to use. + *

+ * The variable default value delimiter is the character or characters that delimit the + * variable name and the variable default value. This delimiter is expressed in terms of a matcher + * allowing advanced variable default value delimiter matches. + *

+ *

+ * If the {@code valueDelimiterMatcher} is null, then the variable default value resolution + * becomes disabled. + *

+ * + * @param valueDelimiterMatcher variable default value delimiter matcher to use, may be null + * @return this, to enable chaining + */ + public StrSubstitutor setValueDelimiterMatcher(final StrMatcher valueDelimiterMatcher) { + this.valueDelimiterMatcher = valueDelimiterMatcher; + return this; + } + + /** + * Sets the variable prefix to use. + *

+ * The variable prefix is the character or characters that identify the + * start of a variable. This method allows a single character prefix to + * be easily set. + *

+ * + * @param prefix the prefix character to use + * @return this, to enable chaining + */ + public StrSubstitutor setVariablePrefix(final char prefix) { + return setVariablePrefixMatcher(StrMatcher.charMatcher(prefix)); + } + + /** + * Sets the variable prefix to use. + *

+ * The variable prefix is the character or characters that identify the + * start of a variable. This method allows a string prefix to be easily set. + *

+ * + * @param prefix the prefix for variables, not null + * @return this, to enable chaining + * @throws IllegalArgumentException if the prefix is null + */ + public StrSubstitutor setVariablePrefix(final String prefix) { + Validate.isTrue(prefix != null, "Variable prefix must not be null!"); + return setVariablePrefixMatcher(StrMatcher.stringMatcher(prefix)); + } + + /** + * Sets the variable prefix matcher currently in use. + *

+ * The variable prefix is the character or characters that identify the + * start of a variable. This prefix is expressed in terms of a matcher + * allowing advanced prefix matches. + *

+ * + * @param prefixMatcher the prefix matcher to use, null ignored + * @return this, to enable chaining + * @throws IllegalArgumentException if the prefix matcher is null + */ + public StrSubstitutor setVariablePrefixMatcher(final StrMatcher prefixMatcher) { + Validate.isTrue(prefixMatcher != null, "Variable prefix matcher must not be null!"); + this.prefixMatcher = prefixMatcher; + return this; + } + + /** + * Sets the VariableResolver that is used to lookup variables. + * + * @param variableResolver the VariableResolver + */ + public void setVariableResolver(final StrLookup variableResolver) { + this.variableResolver = variableResolver; + } + + /** + * Sets the variable suffix to use. + *

+ * The variable suffix is the character or characters that identify the + * end of a variable. This method allows a single character suffix to + * be easily set. + *

+ * + * @param suffix the suffix character to use + * @return this, to enable chaining + */ + public StrSubstitutor setVariableSuffix(final char suffix) { + return setVariableSuffixMatcher(StrMatcher.charMatcher(suffix)); + } + + /** + * Sets the variable suffix to use. + *

+ * The variable suffix is the character or characters that identify the + * end of a variable. This method allows a string suffix to be easily set. + *

+ * + * @param suffix the suffix for variables, not null + * @return this, to enable chaining + * @throws IllegalArgumentException if the suffix is null + */ + public StrSubstitutor setVariableSuffix(final String suffix) { + Validate.isTrue(suffix != null, "Variable suffix must not be null!"); + return setVariableSuffixMatcher(StrMatcher.stringMatcher(suffix)); + } + + /** + * Sets the variable suffix matcher currently in use. + *

+ * The variable suffix is the character or characters that identify the + * end of a variable. This suffix is expressed in terms of a matcher + * allowing advanced suffix matches. + *

+ * + * @param suffixMatcher the suffix matcher to use, null ignored + * @return this, to enable chaining + * @throws IllegalArgumentException if the suffix matcher is null + */ + public StrSubstitutor setVariableSuffixMatcher(final StrMatcher suffixMatcher) { + Validate.isTrue(suffixMatcher != null, "Variable suffix matcher must not be null!"); + this.suffixMatcher = suffixMatcher; + return this; + } + + /** + * Internal method that substitutes the variables. + *

+ * Most users of this class do not need to call this method. This method will + * be called automatically by another (public) method. + *

+ *

+ * Writers of subclasses can override this method if they need access to + * the substitution process at the start or end. + *

+ * + * @param buf the string builder to substitute into, not null + * @param offset the start offset within the builder, must be valid + * @param length the length within the builder to be processed, must be valid + * @return true if altered + */ + protected boolean substitute(final StrBuilder buf, final int offset, final int length) { + return substitute(buf, offset, length, null) > 0; + } + + /** + * Recursive handler for multiple levels of interpolation. This is the main + * interpolation method, which resolves the values of all variable references + * contained in the passed in text. + * + * @param buf the string builder to substitute into, not null + * @param offset the start offset within the builder, must be valid + * @param length the length within the builder to be processed, must be valid + * @param priorVariables the stack keeping track of the replaced variables, may be null + * @return The length change that occurs, unless priorVariables is null when the int + * represents a boolean flag as to whether any change occurred. + */ + private int substitute(final StrBuilder buf, final int offset, final int length, List priorVariables) { + final StrMatcher pfxMatcher = getVariablePrefixMatcher(); + final StrMatcher suffMatcher = getVariableSuffixMatcher(); + final char escape = getEscapeChar(); + final StrMatcher valueDelimMatcher = getValueDelimiterMatcher(); + final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables(); + final boolean substitutionInValuesDisabled = isDisableSubstitutionInValues(); + + final boolean top = priorVariables == null; + boolean altered = false; + int lengthChange = 0; + char[] chars = buf.buffer; + int bufEnd = offset + length; + int pos = offset; + while (pos < bufEnd) { + final int startMatchLen = pfxMatcher.isMatch(chars, pos, offset, + bufEnd); + if (startMatchLen == 0) { + pos++; + } else { + // found variable start marker + if (pos > offset && chars[pos - 1] == escape) { + // escaped + if (preserveEscapes) { + pos++; + continue; + } + buf.deleteCharAt(pos - 1); + chars = buf.buffer; // in case buffer was altered + lengthChange--; + altered = true; + bufEnd--; + } else { + // find suffix + final int startPos = pos; + pos += startMatchLen; + int endMatchLen = 0; + int nestedVarCount = 0; + while (pos < bufEnd) { + if (substitutionInVariablesEnabled + && pfxMatcher.isMatch(chars, + pos, offset, bufEnd) != 0) { + // found a nested variable start + endMatchLen = pfxMatcher.isMatch(chars, + pos, offset, bufEnd); + nestedVarCount++; + pos += endMatchLen; + continue; + } + + endMatchLen = suffMatcher.isMatch(chars, pos, offset, + bufEnd); + if (endMatchLen == 0) { + pos++; + } else { + // found variable end marker + if (nestedVarCount == 0) { + String varNameExpr = new String(chars, startPos + + startMatchLen, pos - startPos + - startMatchLen); + if (substitutionInVariablesEnabled) { + final StrBuilder bufName = new StrBuilder(varNameExpr); + substitute(bufName, 0, bufName.length()); + varNameExpr = bufName.toString(); + } + pos += endMatchLen; + final int endPos = pos; + + String varName = varNameExpr; + String varDefaultValue = null; + + if (valueDelimMatcher != null) { + final char[] varNameExprChars = varNameExpr.toCharArray(); + int valueDelimiterMatchLen = 0; + for (int i = 0; i < varNameExprChars.length; i++) { + // if there's any nested variable when nested variable substitution disabled, + // then stop resolving name and default value. + if (!substitutionInVariablesEnabled + && pfxMatcher.isMatch(varNameExprChars, + i, + i, + varNameExprChars.length) != 0) { + break; + } + if (valueDelimMatcher.isMatch(varNameExprChars, i) != 0) { + valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i); + varName = varNameExpr.substring(0, i); + varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen); + break; + } + } + } + + // on the first call initialize priorVariables + if (priorVariables == null) { + priorVariables = new ArrayList<>(); + priorVariables.add(new String(chars, + offset, length)); + } + + // handle cyclic substitution + checkCyclicSubstitution(varName, priorVariables); + priorVariables.add(varName); + + // resolve the variable + String varValue = resolveVariable(varName, buf, + startPos, endPos); + if (varValue == null) { + varValue = varDefaultValue; + } + if (varValue != null) { + final int varLen = varValue.length(); + buf.replace(startPos, endPos, varValue); + altered = true; + int change = 0; + if (!substitutionInValuesDisabled) { // recursive replace + change = substitute(buf, startPos, + varLen, priorVariables); + } + change = change + + varLen - (endPos - startPos); + pos += change; + bufEnd += change; + lengthChange += change; + chars = buf.buffer; // in case buffer was + // altered + } + + // remove variable from the cyclic stack + priorVariables + .remove(priorVariables.size() - 1); + break; + } + nestedVarCount--; + pos += endMatchLen; + } + } + } + } + } + if (top) { + return altered ? 1 : 0; + } + return lengthChange; + } +} diff --git a/sources/src/main/java/org/apache/commons/text/StrTokenizer.java b/sources/src/main/java/org/apache/commons/text/StrTokenizer.java new file mode 100644 index 0000000..8940f47 --- /dev/null +++ b/sources/src/main/java/org/apache/commons/text/StrTokenizer.java @@ -0,0 +1,1122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; + +/** + * Tokenizes a string based on delimiters (separators) + * and supporting quoting and ignored character concepts. + *

+ * This class can split a String into many smaller strings. It aims + * to do a similar job to {@link java.util.StringTokenizer StringTokenizer}, + * however it offers much more control and flexibility including implementing + * the {@code ListIterator} interface. By default, it is set up + * like {@code StringTokenizer}. + *

+ * The input String is split into a number of tokens. + * Each token is separated from the next String by a delimiter. + * One or more delimiter characters must be specified. + *

+ * Each token may be surrounded by quotes. + * The quote matcher specifies the quote character(s). + * A quote may be escaped within a quoted section by duplicating itself. + *

+ * Between each token and the delimiter are potentially characters that need trimming. + * The trimmer matcher specifies these characters. + * One usage might be to trim whitespace characters. + *

+ * At any point outside the quotes there might potentially be invalid characters. + * The ignored matcher specifies these characters to be removed. + * One usage might be to remove new line characters. + *

+ * Empty tokens may be removed or returned as null. + *

+ * "a,b,c"         - Three tokens "a","b","c"   (comma delimiter)
+ * " a, b , c "    - Three tokens "a","b","c"   (default CSV processing trims whitespace)
+ * "a, ", b ,", c" - Three tokens "a, " , " b ", ", c" (quoted text untouched)
+ * 
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
StrTokenizer properties and options
PropertyTypeDefault
delimCharSetMatcher{ \t\n\r\f}
quoteNoneMatcher{}
ignoreNoneMatcher{}
emptyTokenAsNullbooleanfalse
ignoreEmptyTokensbooleantrue
+ * + * @since 1.0 + * @deprecated Deprecated as of 1.3, use {@link StringTokenizer} instead. This class will be removed in 2.0. + */ +@Deprecated +public class StrTokenizer implements ListIterator, Cloneable { + + /** Comma separated values tokenizer internal variable. */ + private static final StrTokenizer CSV_TOKENIZER_PROTOTYPE; + + /** Tab separated values tokenizer internal variable. */ + private static final StrTokenizer TSV_TOKENIZER_PROTOTYPE; + + static { + CSV_TOKENIZER_PROTOTYPE = new StrTokenizer(); + CSV_TOKENIZER_PROTOTYPE.setDelimiterMatcher(StrMatcher.commaMatcher()); + CSV_TOKENIZER_PROTOTYPE.setQuoteMatcher(StrMatcher.doubleQuoteMatcher()); + CSV_TOKENIZER_PROTOTYPE.setIgnoredMatcher(StrMatcher.noneMatcher()); + CSV_TOKENIZER_PROTOTYPE.setTrimmerMatcher(StrMatcher.trimMatcher()); + CSV_TOKENIZER_PROTOTYPE.setEmptyTokenAsNull(false); + CSV_TOKENIZER_PROTOTYPE.setIgnoreEmptyTokens(false); + + TSV_TOKENIZER_PROTOTYPE = new StrTokenizer(); + TSV_TOKENIZER_PROTOTYPE.setDelimiterMatcher(StrMatcher.tabMatcher()); + TSV_TOKENIZER_PROTOTYPE.setQuoteMatcher(StrMatcher.doubleQuoteMatcher()); + TSV_TOKENIZER_PROTOTYPE.setIgnoredMatcher(StrMatcher.noneMatcher()); + TSV_TOKENIZER_PROTOTYPE.setTrimmerMatcher(StrMatcher.trimMatcher()); + TSV_TOKENIZER_PROTOTYPE.setEmptyTokenAsNull(false); + TSV_TOKENIZER_PROTOTYPE.setIgnoreEmptyTokens(false); + } + + /** + * Returns a clone of {@code CSV_TOKENIZER_PROTOTYPE}. + * + * @return a clone of {@code CSV_TOKENIZER_PROTOTYPE}. + */ + private static StrTokenizer getCSVClone() { + return (StrTokenizer) CSV_TOKENIZER_PROTOTYPE.clone(); + } + + /** + * Gets a new tokenizer instance which parses Comma Separated Value strings + * initializing it with the given input. The default for CSV processing + * will be trim whitespace from both ends (which can be overridden with + * the setTrimmer method). + *

+ * You must call a "reset" method to set the string which you want to parse. + *

+ * @return a new tokenizer instance which parses Comma Separated Value strings + */ + public static StrTokenizer getCSVInstance() { + return getCSVClone(); + } + + /** + * Gets a new tokenizer instance which parses Comma Separated Value strings + * initializing it with the given input. The default for CSV processing + * will be trim whitespace from both ends (which can be overridden with + * the setTrimmer method). + * + * @param input the text to parse + * @return a new tokenizer instance which parses Comma Separated Value strings + */ + public static StrTokenizer getCSVInstance(final char[] input) { + final StrTokenizer tok = getCSVClone(); + tok.reset(input); + return tok; + } + + /** + * Gets a new tokenizer instance which parses Comma Separated Value strings + * initializing it with the given input. The default for CSV processing + * will be trim whitespace from both ends (which can be overridden with + * the setTrimmer method). + * + * @param input the text to parse + * @return a new tokenizer instance which parses Comma Separated Value strings + */ + public static StrTokenizer getCSVInstance(final String input) { + final StrTokenizer tok = getCSVClone(); + tok.reset(input); + return tok; + } + /** + * Returns a clone of {@code TSV_TOKENIZER_PROTOTYPE}. + * + * @return a clone of {@code TSV_TOKENIZER_PROTOTYPE}. + */ + private static StrTokenizer getTSVClone() { + return (StrTokenizer) TSV_TOKENIZER_PROTOTYPE.clone(); + } + + /** + * Gets a new tokenizer instance which parses Tab Separated Value strings. + * The default for CSV processing will be trim whitespace from both ends + * (which can be overridden with the setTrimmer method). + *

+ * You must call a "reset" method to set the string which you want to parse. + *

+ * @return a new tokenizer instance which parses Tab Separated Value strings. + */ + public static StrTokenizer getTSVInstance() { + return getTSVClone(); + } + + /** + * Gets a new tokenizer instance which parses Tab Separated Value strings. + * The default for CSV processing will be trim whitespace from both ends + * (which can be overridden with the setTrimmer method). + * @param input the string to parse + * @return a new tokenizer instance which parses Tab Separated Value strings. + */ + public static StrTokenizer getTSVInstance(final char[] input) { + final StrTokenizer tok = getTSVClone(); + tok.reset(input); + return tok; + } + + /** + * Gets a new tokenizer instance which parses Tab Separated Value strings. + * The default for CSV processing will be trim whitespace from both ends + * (which can be overridden with the setTrimmer method). + * @param input the string to parse + * @return a new tokenizer instance which parses Tab Separated Value strings. + */ + public static StrTokenizer getTSVInstance(final String input) { + final StrTokenizer tok = getTSVClone(); + tok.reset(input); + return tok; + } + + /** The text to work on. */ + private char[] chars; + + /** The parsed tokens. */ + private String[] tokens; + + /** The current iteration position. */ + private int tokenPos; + + /** The delimiter matcher. */ + private StrMatcher delimMatcher = StrMatcher.splitMatcher(); + + /** The quote matcher. */ + private StrMatcher quoteMatcher = StrMatcher.noneMatcher(); + + /** The ignored matcher. */ + private StrMatcher ignoredMatcher = StrMatcher.noneMatcher(); + + /** The trimmer matcher. */ + private StrMatcher trimmerMatcher = StrMatcher.noneMatcher(); + + /** Whether to return empty tokens as null. */ + private boolean emptyAsNull; + + /** Whether to ignore empty tokens. */ + private boolean ignoreEmptyTokens = true; + + /** + * Constructs a tokenizer splitting on space, tab, newline and form feed + * as per StringTokenizer, but with no text to tokenize. + *

+ * This constructor is normally used with {@link #reset(String)}. + *

+ */ + public StrTokenizer() { + this.chars = null; + } + + /** + * Constructs a tokenizer splitting on space, tab, newline and form feed + * as per StringTokenizer. + * + * @param input the string which is to be parsed, not cloned + */ + public StrTokenizer(final char[] input) { + if (input == null) { + this.chars = null; + } else { + this.chars = input.clone(); + } + } + + /** + * Constructs a tokenizer splitting on the specified character. + * + * @param input the string which is to be parsed, not cloned + * @param delim the field delimiter character + */ + public StrTokenizer(final char[] input, final char delim) { + this(input); + setDelimiterChar(delim); + } + + /** + * Constructs a tokenizer splitting on the specified delimiter character + * and handling quotes using the specified quote character. + * + * @param input the string which is to be parsed, not cloned + * @param delim the field delimiter character + * @param quote the field quoted string character + */ + public StrTokenizer(final char[] input, final char delim, final char quote) { + this(input, delim); + setQuoteChar(quote); + } + + /** + * Constructs a tokenizer splitting on the specified string. + * + * @param input the string which is to be parsed, not cloned + * @param delim the field delimiter string + */ + public StrTokenizer(final char[] input, final String delim) { + this(input); + setDelimiterString(delim); + } + + /** + * Constructs a tokenizer splitting using the specified delimiter matcher. + * + * @param input the string which is to be parsed, not cloned + * @param delim the field delimiter matcher + */ + public StrTokenizer(final char[] input, final StrMatcher delim) { + this(input); + setDelimiterMatcher(delim); + } + + /** + * Constructs a tokenizer splitting using the specified delimiter matcher + * and handling quotes using the specified quote matcher. + * + * @param input the string which is to be parsed, not cloned + * @param delim the field delimiter character + * @param quote the field quoted string character + */ + public StrTokenizer(final char[] input, final StrMatcher delim, final StrMatcher quote) { + this(input, delim); + setQuoteMatcher(quote); + } + + /** + * Constructs a tokenizer splitting on space, tab, newline and form feed + * as per StringTokenizer. + * + * @param input the string which is to be parsed + */ + public StrTokenizer(final String input) { + if (input != null) { + chars = input.toCharArray(); + } else { + chars = null; + } + } + + /** + * Constructs a tokenizer splitting on the specified delimiter character. + * + * @param input the string which is to be parsed + * @param delim the field delimiter character + */ + public StrTokenizer(final String input, final char delim) { + this(input); + setDelimiterChar(delim); + } + + /** + * Constructs a tokenizer splitting on the specified delimiter character + * and handling quotes using the specified quote character. + * + * @param input the string which is to be parsed + * @param delim the field delimiter character + * @param quote the field quoted string character + */ + public StrTokenizer(final String input, final char delim, final char quote) { + this(input, delim); + setQuoteChar(quote); + } + + /** + * Constructs a tokenizer splitting on the specified delimiter string. + * + * @param input the string which is to be parsed + * @param delim the field delimiter string + */ + public StrTokenizer(final String input, final String delim) { + this(input); + setDelimiterString(delim); + } + + /** + * Constructs a tokenizer splitting using the specified delimiter matcher. + * + * @param input the string which is to be parsed + * @param delim the field delimiter matcher + */ + public StrTokenizer(final String input, final StrMatcher delim) { + this(input); + setDelimiterMatcher(delim); + } + + /** + * Constructs a tokenizer splitting using the specified delimiter matcher + * and handling quotes using the specified quote matcher. + * + * @param input the string which is to be parsed + * @param delim the field delimiter matcher + * @param quote the field quoted string matcher + */ + public StrTokenizer(final String input, final StrMatcher delim, final StrMatcher quote) { + this(input, delim); + setQuoteMatcher(quote); + } + + /** + * Unsupported ListIterator operation. + * @param obj this parameter ignored. + * @throws UnsupportedOperationException always + */ + @Override + public void add(final String obj) { + throw new UnsupportedOperationException("add() is unsupported"); + } + + /** + * Adds a token to a list, paying attention to the parameters we've set. + * + * @param list the list to add to + * @param tok the token to add + */ + private void addToken(final List list, String tok) { + if (tok == null || tok.isEmpty()) { + if (isIgnoreEmptyTokens()) { + return; + } + if (isEmptyTokenAsNull()) { + tok = null; + } + } + list.add(tok); + } + + /** + * Checks if tokenization has been done, and if not then do it. + */ + private void checkTokenized() { + if (tokens == null) { + if (chars == null) { + // still call tokenize as subclass may do some work + final List split = tokenize(null, 0, 0); + tokens = split.toArray(ArrayUtils.EMPTY_STRING_ARRAY); + } else { + final List split = tokenize(chars, 0, chars.length); + tokens = split.toArray(ArrayUtils.EMPTY_STRING_ARRAY); + } + } + } + + /** + * Creates a new instance of this Tokenizer. The new instance is reset so + * that it will be at the start of the token list. + * If a {@link CloneNotSupportedException} is caught, return {@code null}. + * + * @return a new instance of this Tokenizer which has been reset. + */ + @Override + public Object clone() { + try { + return cloneReset(); + } catch (final CloneNotSupportedException ex) { + return null; + } + } + + /** + * Creates a new instance of this Tokenizer. The new instance is reset so that + * it will be at the start of the token list. + * + * @return a new instance of this Tokenizer which has been reset. + * @throws CloneNotSupportedException if there is a problem cloning + */ + Object cloneReset() throws CloneNotSupportedException { + // this method exists to enable 100% test coverage + final StrTokenizer cloned = (StrTokenizer) super.clone(); + if (cloned.chars != null) { + cloned.chars = cloned.chars.clone(); + } + cloned.reset(); + return cloned; + } + + /** + * Gets the String content that the tokenizer is parsing. + * + * @return The string content being parsed + */ + public String getContent() { + if (chars == null) { + return null; + } + return new String(chars); + } + + /** + * Gets the field delimiter matcher. + * + * @return The delimiter matcher in use + */ + public StrMatcher getDelimiterMatcher() { + return this.delimMatcher; + } + + /** + * Gets the ignored character matcher. + *

+ * These characters are ignored when parsing the String, unless they are + * within a quoted region. + * The default value is not to ignore anything. + *

+ * + * @return The ignored matcher in use + */ + public StrMatcher getIgnoredMatcher() { + return ignoredMatcher; + } + + /** + * Gets the quote matcher currently in use. + *

+ * The quote character is used to wrap data between the tokens. + * This enables delimiters to be entered as data. + * The default value is '"' (double quote). + *

+ * + * @return The quote matcher in use + */ + public StrMatcher getQuoteMatcher() { + return quoteMatcher; + } + + /** + * Gets a copy of the full token list as an independent modifiable array. + * + * @return The tokens as a String array + */ + public String[] getTokenArray() { + checkTokenized(); + return tokens.clone(); + } + + /** + * Gets a copy of the full token list as an independent modifiable list. + * + * @return The tokens as a String array + */ + public List getTokenList() { + checkTokenized(); + final List list = new ArrayList<>(tokens.length); + Collections.addAll(list, tokens); + + return list; + } + + /** + * Gets the trimmer character matcher. + *

+ * These characters are trimmed off on each side of the delimiter + * until the token or quote is found. + * The default value is not to trim anything. + *

+ * + * @return The trimmer matcher in use + */ + public StrMatcher getTrimmerMatcher() { + return trimmerMatcher; + } + + /** + * Checks whether there are any more tokens. + * + * @return true if there are more tokens + */ + @Override + public boolean hasNext() { + checkTokenized(); + return tokenPos < tokens.length; + } + + /** + * Checks whether there are any previous tokens that can be iterated to. + * + * @return true if there are previous tokens + */ + @Override + public boolean hasPrevious() { + checkTokenized(); + return tokenPos > 0; + } + + /** + * Gets whether the tokenizer currently returns empty tokens as null. + * The default for this property is false. + * + * @return true if empty tokens are returned as null + */ + public boolean isEmptyTokenAsNull() { + return this.emptyAsNull; + } + + /** + * Gets whether the tokenizer currently ignores empty tokens. + * The default for this property is true. + * + * @return true if empty tokens are not returned + */ + public boolean isIgnoreEmptyTokens() { + return ignoreEmptyTokens; + } + + /** + * Checks if the characters at the index specified match the quote + * already matched in readNextToken(). + * + * @param srcChars the character array being tokenized + * @param pos the position to check for a quote + * @param len the length of the character array being tokenized + * @param quoteStart the start position of the matched quote, 0 if no quoting + * @param quoteLen the length of the matched quote, 0 if no quoting + * @return true if a quote is matched + */ + private boolean isQuote(final char[] srcChars, + final int pos, + final int len, + final int quoteStart, + final int quoteLen) { + for (int i = 0; i < quoteLen; i++) { + if (pos + i >= len || srcChars[pos + i] != srcChars[quoteStart + i]) { + return false; + } + } + return true; + } + + /** + * Gets the next token. + * + * @return The next String token + * @throws NoSuchElementException if there are no more elements + */ + @Override + public String next() { + if (hasNext()) { + return tokens[tokenPos++]; + } + throw new NoSuchElementException(); + } + + /** + * Gets the index of the next token to return. + * + * @return The next token index + */ + @Override + public int nextIndex() { + return tokenPos; + } + + /** + * Gets the next token from the String. + * Equivalent to {@link #next()} except it returns null rather than + * throwing {@link NoSuchElementException} when no tokens remain. + * + * @return The next sequential token, or null when no more tokens are found + */ + public String nextToken() { + if (hasNext()) { + return tokens[tokenPos++]; + } + return null; + } + + /** + * Gets the token previous to the last returned token. + * + * @return The previous token + */ + @Override + public String previous() { + if (hasPrevious()) { + return tokens[--tokenPos]; + } + throw new NoSuchElementException(); + } + + /** + * Gets the index of the previous token. + * + * @return The previous token index + */ + @Override + public int previousIndex() { + return tokenPos - 1; + } + + /** + * Gets the previous token from the String. + * + * @return The previous sequential token, or null when no more tokens are found + */ + public String previousToken() { + if (hasPrevious()) { + return tokens[--tokenPos]; + } + return null; + } + + /** + * Reads character by character through the String to get the next token. + * + * @param srcChars the character array being tokenized + * @param start the first character of field + * @param len the length of the character array being tokenized + * @param workArea a temporary work area + * @param tokenList the list of parsed tokens + * @return The starting position of the next field (the character + * immediately after the delimiter), or -1 if end of string found + */ + private int readNextToken(final char[] srcChars, + int start, + final int len, + final StrBuilder workArea, + final List tokenList) { + // skip all leading whitespace, unless it is the + // field delimiter or the quote character + while (start < len) { + final int removeLen = Math.max( + getIgnoredMatcher().isMatch(srcChars, start, start, len), + getTrimmerMatcher().isMatch(srcChars, start, start, len)); + if (removeLen == 0 + || getDelimiterMatcher().isMatch(srcChars, start, start, len) > 0 + || getQuoteMatcher().isMatch(srcChars, start, start, len) > 0) { + break; + } + start += removeLen; + } + + // handle reaching end + if (start >= len) { + addToken(tokenList, StringUtils.EMPTY); + return -1; + } + + // handle empty token + final int delimLen = getDelimiterMatcher().isMatch(srcChars, start, start, len); + if (delimLen > 0) { + addToken(tokenList, StringUtils.EMPTY); + return start + delimLen; + } + + // handle found token + final int quoteLen = getQuoteMatcher().isMatch(srcChars, start, start, len); + if (quoteLen > 0) { + return readWithQuotes(srcChars, start + quoteLen, len, workArea, tokenList, start, quoteLen); + } + return readWithQuotes(srcChars, start, len, workArea, tokenList, 0, 0); + } + + /** + * Reads a possibly quoted string token. + * + * @param srcChars the character array being tokenized + * @param start the first character of field + * @param len the length of the character array being tokenized + * @param workArea a temporary work area + * @param tokenList the list of parsed tokens + * @param quoteStart the start position of the matched quote, 0 if no quoting + * @param quoteLen the length of the matched quote, 0 if no quoting + * @return The starting position of the next field (the character + * immediately after the delimiter, or if end of string found, + * then the length of string + */ + private int readWithQuotes(final char[] srcChars, final int start, final int len, final StrBuilder workArea, + final List tokenList, final int quoteStart, final int quoteLen) { + // Loop until we've found the end of the quoted + // string or the end of the input + workArea.clear(); + int pos = start; + boolean quoting = quoteLen > 0; + int trimStart = 0; + + while (pos < len) { + // quoting mode can occur several times throughout a string + // we must switch between quoting and non-quoting until we + // encounter a non-quoted delimiter, or end of string + if (quoting) { + // In quoting mode + + // If we've found a quote character, see if it's + // followed by a second quote. If so, then we need + // to actually put the quote character into the token + // rather than end the token. + if (isQuote(srcChars, pos, len, quoteStart, quoteLen)) { + if (isQuote(srcChars, pos + quoteLen, len, quoteStart, quoteLen)) { + // matched pair of quotes, thus an escaped quote + workArea.append(srcChars, pos, quoteLen); + pos += quoteLen * 2; + trimStart = workArea.size(); + continue; + } + + // end of quoting + quoting = false; + pos += quoteLen; + continue; + } + + } else { + // Not in quoting mode + + // check for delimiter, and thus end of token + final int delimLen = getDelimiterMatcher().isMatch(srcChars, pos, start, len); + if (delimLen > 0) { + // return condition when end of token found + addToken(tokenList, workArea.substring(0, trimStart)); + return pos + delimLen; + } + + // check for quote, and thus back into quoting mode + if (quoteLen > 0 && isQuote(srcChars, pos, len, quoteStart, quoteLen)) { + quoting = true; + pos += quoteLen; + continue; + } + + // check for ignored (outside quotes), and ignore + final int ignoredLen = getIgnoredMatcher().isMatch(srcChars, pos, start, len); + if (ignoredLen > 0) { + pos += ignoredLen; + continue; + } + + // check for trimmed character + // don't yet know if its at the end, so copy to workArea + // use trimStart to keep track of trim at the end + final int trimmedLen = getTrimmerMatcher().isMatch(srcChars, pos, start, len); + if (trimmedLen > 0) { + workArea.append(srcChars, pos, trimmedLen); + pos += trimmedLen; + continue; + } + + } + // copy regular character from inside quotes + workArea.append(srcChars[pos++]); + trimStart = workArea.size(); + } + + // return condition when end of string found + addToken(tokenList, workArea.substring(0, trimStart)); + return -1; + } + + /** + * Unsupported ListIterator operation. + * + * @throws UnsupportedOperationException always + */ + @Override + public void remove() { + throw new UnsupportedOperationException("remove() is unsupported"); + } + + /** + * Resets this tokenizer, forgetting all parsing and iteration already completed. + *

+ * This method allows the same tokenizer to be reused for the same String. + * + * @return this, to enable chaining + */ + public StrTokenizer reset() { + tokenPos = 0; + tokens = null; + return this; + } + + /** + * Reset this tokenizer, giving it a new input string to parse. + * In this manner you can re-use a tokenizer with the same settings + * on multiple input lines. + * + * @param input the new character array to tokenize, not cloned, null sets no text to parse + * @return this, to enable chaining + */ + public StrTokenizer reset(final char[] input) { + reset(); + if (input != null) { + this.chars = input.clone(); + } else { + this.chars = null; + } + return this; + } + + /** + * Reset this tokenizer, giving it a new input string to parse. + * In this manner you can re-use a tokenizer with the same settings + * on multiple input lines. + * + * @param input the new string to tokenize, null sets no text to parse + * @return this, to enable chaining + */ + public StrTokenizer reset(final String input) { + reset(); + if (input != null) { + this.chars = input.toCharArray(); + } else { + this.chars = null; + } + return this; + } + + /** + * Unsupported ListIterator operation. + * @param obj this parameter ignored. + * @throws UnsupportedOperationException always + */ + @Override + public void set(final String obj) { + throw new UnsupportedOperationException("set() is unsupported"); + } + + /** + * Sets the field delimiter character. + * + * @param delim the delimiter character to use + * @return this, to enable chaining + */ + public StrTokenizer setDelimiterChar(final char delim) { + return setDelimiterMatcher(StrMatcher.charMatcher(delim)); + } + + /** + * Sets the field delimiter matcher. + *

+ * The delimiter is used to separate one token from another. + *

+ * + * @param delim the delimiter matcher to use + * @return this, to enable chaining + */ + public StrTokenizer setDelimiterMatcher(final StrMatcher delim) { + if (delim == null) { + this.delimMatcher = StrMatcher.noneMatcher(); + } else { + this.delimMatcher = delim; + } + return this; + } + + /** + * Sets the field delimiter string. + * + * @param delim the delimiter string to use + * @return this, to enable chaining + */ + public StrTokenizer setDelimiterString(final String delim) { + return setDelimiterMatcher(StrMatcher.stringMatcher(delim)); + } + + /** + * Sets whether the tokenizer should return empty tokens as null. + * The default for this property is false. + * + * @param emptyAsNull whether empty tokens are returned as null + * @return this, to enable chaining + */ + public StrTokenizer setEmptyTokenAsNull(final boolean emptyAsNull) { + this.emptyAsNull = emptyAsNull; + return this; + } + + /** + * Set the character to ignore. + *

+ * This character is ignored when parsing the String, unless it is + * within a quoted region. + *

+ * + * @param ignored the ignored character to use + * @return this, to enable chaining + */ + public StrTokenizer setIgnoredChar(final char ignored) { + return setIgnoredMatcher(StrMatcher.charMatcher(ignored)); + } + + /** + * Set the matcher for characters to ignore. + *

+ * These characters are ignored when parsing the String, unless they are + * within a quoted region. + *

+ * + * @param ignored the ignored matcher to use, null ignored + * @return this, to enable chaining + */ + public StrTokenizer setIgnoredMatcher(final StrMatcher ignored) { + if (ignored != null) { + this.ignoredMatcher = ignored; + } + return this; + } + + /** + * Sets whether the tokenizer should ignore and not return empty tokens. + * The default for this property is true. + * + * @param ignoreEmptyTokens whether empty tokens are not returned + * @return this, to enable chaining + */ + public StrTokenizer setIgnoreEmptyTokens(final boolean ignoreEmptyTokens) { + this.ignoreEmptyTokens = ignoreEmptyTokens; + return this; + } + + /** + * Sets the quote character to use. + *

+ * The quote character is used to wrap data between the tokens. + * This enables delimiters to be entered as data. + *

+ * + * @param quote the quote character to use + * @return this, to enable chaining + */ + public StrTokenizer setQuoteChar(final char quote) { + return setQuoteMatcher(StrMatcher.charMatcher(quote)); + } + + /** + * Set the quote matcher to use. + *

+ * The quote character is used to wrap data between the tokens. + * This enables delimiters to be entered as data. + *

+ * + * @param quote the quote matcher to use, null ignored + * @return this, to enable chaining + */ + public StrTokenizer setQuoteMatcher(final StrMatcher quote) { + if (quote != null) { + this.quoteMatcher = quote; + } + return this; + } + + /** + * Sets the matcher for characters to trim. + *

+ * These characters are trimmed off on each side of the delimiter + * until the token or quote is found. + *

+ * + * @param trimmer the trimmer matcher to use, null ignored + * @return this, to enable chaining + */ + public StrTokenizer setTrimmerMatcher(final StrMatcher trimmer) { + if (trimmer != null) { + this.trimmerMatcher = trimmer; + } + return this; + } + + /** + * Gets the number of tokens found in the String. + * + * @return The number of matched tokens + */ + public int size() { + checkTokenized(); + return tokens.length; + } + + /** + * Internal method to performs the tokenization. + *

+ * Most users of this class do not need to call this method. This method + * will be called automatically by other (public) methods when required. + *

+ *

+ * This method exists to allow subclasses to add code before or after the + * tokenization. For example, a subclass could alter the character array, + * offset or count to be parsed, or call the tokenizer multiple times on + * multiple strings. It is also be possible to filter the results. + *

+ *

+ * {@code StrTokenizer} will always pass a zero offset and a count + * equal to the length of the array to this method, however a subclass + * may pass other values, or even an entirely different array. + *

+ * + * @param srcChars the character array being tokenized, may be null + * @param offset the start position within the character array, must be valid + * @param count the number of characters to tokenize, must be valid + * @return The modifiable list of String tokens, unmodifiable if null array or zero count + */ + protected List tokenize(final char[] srcChars, final int offset, final int count) { + if (srcChars == null || count == 0) { + return Collections.emptyList(); + } + final StrBuilder buf = new StrBuilder(); + final List tokenList = new ArrayList<>(); + int pos = offset; + + // loop around the entire buffer + while (pos >= 0 && pos < count) { + // find next token + pos = readNextToken(srcChars, pos, count, buf, tokenList); + + // handle case where end of string is a delimiter + if (pos >= count) { + addToken(tokenList, StringUtils.EMPTY); + } + } + return tokenList; + } + + /** + * Gets the String content that the tokenizer is parsing. + * + * @return The string content being parsed + */ + @Override + public String toString() { + if (tokens == null) { + return "StrTokenizer[not tokenized yet]"; + } + return "StrTokenizer" + getTokenList(); + } + +} diff --git a/sources/src/main/java/org/apache/commons/text/StringEscapeUtils.java b/sources/src/main/java/org/apache/commons/text/StringEscapeUtils.java new file mode 100644 index 0000000..5888817 --- /dev/null +++ b/sources/src/main/java/org/apache/commons/text/StringEscapeUtils.java @@ -0,0 +1,865 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import java.io.IOException; +import java.io.Writer; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.translate.AggregateTranslator; +import org.apache.commons.text.translate.CharSequenceTranslator; +import org.apache.commons.text.translate.CsvTranslators; +import org.apache.commons.text.translate.EntityArrays; +import org.apache.commons.text.translate.JavaUnicodeEscaper; +import org.apache.commons.text.translate.LookupTranslator; +import org.apache.commons.text.translate.NumericEntityEscaper; +import org.apache.commons.text.translate.NumericEntityUnescaper; +import org.apache.commons.text.translate.OctalUnescaper; +import org.apache.commons.text.translate.UnicodeUnescaper; +import org.apache.commons.text.translate.UnicodeUnpairedSurrogateRemover; + +/** + *

+ * Escapes and unescapes {@code String}s for Java, Java Script, HTML and XML. + *

+ * + *

+ * #ThreadSafe# + *

+ * + *

+ * This code has been adapted from Apache Commons Lang 3.5. + *

+ * + * @since 1.0 + */ +public class StringEscapeUtils { + + /* ESCAPE TRANSLATORS */ + + /** + * Convenience wrapper for {@link java.lang.StringBuilder} providing escape methods. + * + *

Example:

+ *
+     * new Builder(ESCAPE_HTML4)
+     *      .append("<p>")
+     *      .escape("This is paragraph 1 and special chars like & get escaped.")
+     *      .append("</p><p>")
+     *      .escape("This is paragraph 2 & more...")
+     *      .append("</p>")
+     *      .toString()
+     * 
+ * + */ + public static final class Builder { + + /** + * StringBuilder to be used in the Builder class. + */ + private final StringBuilder sb; + + /** + * CharSequenceTranslator to be used in the Builder class. + */ + private final CharSequenceTranslator translator; + + /** + * Builder constructor. + * + * @param translator a CharSequenceTranslator. + */ + private Builder(final CharSequenceTranslator translator) { + this.sb = new StringBuilder(); + this.translator = translator; + } + + /** + * Literal append, no escaping being done. + * + * @param input the String to append + * @return {@code this}, to enable chaining + */ + public Builder append(final String input) { + sb.append(input); + return this; + } + + /** + * Escape {@code input} according to the given {@link CharSequenceTranslator}. + * + * @param input the String to escape + * @return {@code this}, to enable chaining + */ + public Builder escape(final String input) { + sb.append(translator.translate(input)); + return this; + } + + /** + * Return the escaped string. + * + * @return The escaped string + */ + @Override + public String toString() { + return sb.toString(); + } + } + /** + * Translator object for unescaping backslash escaped entries. + */ + static class XsiUnescaper extends CharSequenceTranslator { + + /** + * Escaped backslash constant. + */ + private static final char BACKSLASH = '\\'; + + @Override + public int translate(final CharSequence input, final int index, final Writer writer) throws IOException { + + if (index != 0) { + throw new IllegalStateException("XsiUnescaper should never reach the [1] index"); + } + + final String s = input.toString(); + + int segmentStart = 0; + int searchOffset = 0; + while (true) { + final int pos = s.indexOf(BACKSLASH, searchOffset); + if (pos == -1) { + if (segmentStart < s.length()) { + writer.write(s.substring(segmentStart)); + } + break; + } + if (pos > segmentStart) { + writer.write(s.substring(segmentStart, pos)); + } + segmentStart = pos + 1; + searchOffset = pos + 2; + } + + return Character.codePointCount(input, 0, input.length()); + } + } + + /** + * Translator object for escaping Java. + * + * While {@link #escapeJava(String)} is the expected method of use, this + * object allows the Java escaping functionality to be used + * as the foundation for a custom translator. + */ + public static final CharSequenceTranslator ESCAPE_JAVA; + static { + final Map escapeJavaMap = new HashMap<>(); + escapeJavaMap.put("\"", "\\\""); + escapeJavaMap.put("\\", "\\\\"); + ESCAPE_JAVA = new AggregateTranslator( + new LookupTranslator(Collections.unmodifiableMap(escapeJavaMap)), + new LookupTranslator(EntityArrays.JAVA_CTRL_CHARS_ESCAPE), + JavaUnicodeEscaper.outsideOf(32, 0x7f) + ); + } + + /** + * Translator object for escaping EcmaScript/JavaScript. + * + * While {@link #escapeEcmaScript(String)} is the expected method of use, this + * object allows the EcmaScript escaping functionality to be used + * as the foundation for a custom translator. + */ + public static final CharSequenceTranslator ESCAPE_ECMASCRIPT; + static { + final Map escapeEcmaScriptMap = new HashMap<>(); + escapeEcmaScriptMap.put("'", "\\'"); + escapeEcmaScriptMap.put("\"", "\\\""); + escapeEcmaScriptMap.put("\\", "\\\\"); + escapeEcmaScriptMap.put("/", "\\/"); + ESCAPE_ECMASCRIPT = new AggregateTranslator( + new LookupTranslator(Collections.unmodifiableMap(escapeEcmaScriptMap)), + new LookupTranslator(EntityArrays.JAVA_CTRL_CHARS_ESCAPE), + JavaUnicodeEscaper.outsideOf(32, 0x7f) + ); + } + + /** + * Translator object for escaping Json. + * + * While {@link #escapeJson(String)} is the expected method of use, this + * object allows the Json escaping functionality to be used + * as the foundation for a custom translator. + */ + public static final CharSequenceTranslator ESCAPE_JSON; + static { + final Map escapeJsonMap = new HashMap<>(); + escapeJsonMap.put("\"", "\\\""); + escapeJsonMap.put("\\", "\\\\"); + escapeJsonMap.put("/", "\\/"); + ESCAPE_JSON = new AggregateTranslator( + new LookupTranslator(Collections.unmodifiableMap(escapeJsonMap)), + new LookupTranslator(EntityArrays.JAVA_CTRL_CHARS_ESCAPE), + JavaUnicodeEscaper.outsideOf(32, 0x7e) + ); + } + + /** + * Translator object for escaping XML 1.0. + * + * While {@link #escapeXml10(String)} is the expected method of use, this + * object allows the XML escaping functionality to be used + * as the foundation for a custom translator. + */ + public static final CharSequenceTranslator ESCAPE_XML10; + static { + final Map escapeXml10Map = new HashMap<>(); + escapeXml10Map.put("\u0000", StringUtils.EMPTY); + escapeXml10Map.put("\u0001", StringUtils.EMPTY); + escapeXml10Map.put("\u0002", StringUtils.EMPTY); + escapeXml10Map.put("\u0003", StringUtils.EMPTY); + escapeXml10Map.put("\u0004", StringUtils.EMPTY); + escapeXml10Map.put("\u0005", StringUtils.EMPTY); + escapeXml10Map.put("\u0006", StringUtils.EMPTY); + escapeXml10Map.put("\u0007", StringUtils.EMPTY); + escapeXml10Map.put("\u0008", StringUtils.EMPTY); + escapeXml10Map.put("\u000b", StringUtils.EMPTY); + escapeXml10Map.put("\u000c", StringUtils.EMPTY); + escapeXml10Map.put("\u000e", StringUtils.EMPTY); + escapeXml10Map.put("\u000f", StringUtils.EMPTY); + escapeXml10Map.put("\u0010", StringUtils.EMPTY); + escapeXml10Map.put("\u0011", StringUtils.EMPTY); + escapeXml10Map.put("\u0012", StringUtils.EMPTY); + escapeXml10Map.put("\u0013", StringUtils.EMPTY); + escapeXml10Map.put("\u0014", StringUtils.EMPTY); + escapeXml10Map.put("\u0015", StringUtils.EMPTY); + escapeXml10Map.put("\u0016", StringUtils.EMPTY); + escapeXml10Map.put("\u0017", StringUtils.EMPTY); + escapeXml10Map.put("\u0018", StringUtils.EMPTY); + escapeXml10Map.put("\u0019", StringUtils.EMPTY); + escapeXml10Map.put("\u001a", StringUtils.EMPTY); + escapeXml10Map.put("\u001b", StringUtils.EMPTY); + escapeXml10Map.put("\u001c", StringUtils.EMPTY); + escapeXml10Map.put("\u001d", StringUtils.EMPTY); + escapeXml10Map.put("\u001e", StringUtils.EMPTY); + escapeXml10Map.put("\u001f", StringUtils.EMPTY); + escapeXml10Map.put("\ufffe", StringUtils.EMPTY); + escapeXml10Map.put("\uffff", StringUtils.EMPTY); + ESCAPE_XML10 = new AggregateTranslator( + new LookupTranslator(EntityArrays.BASIC_ESCAPE), + new LookupTranslator(EntityArrays.APOS_ESCAPE), + new LookupTranslator(Collections.unmodifiableMap(escapeXml10Map)), + NumericEntityEscaper.between(0x7f, 0x84), + NumericEntityEscaper.between(0x86, 0x9f), + new UnicodeUnpairedSurrogateRemover() + ); + } + + /** + * Translator object for escaping XML 1.1. + * + * While {@link #escapeXml11(String)} is the expected method of use, this + * object allows the XML escaping functionality to be used + * as the foundation for a custom translator. + */ + public static final CharSequenceTranslator ESCAPE_XML11; + + static { + final Map escapeXml11Map = new HashMap<>(); + escapeXml11Map.put("\u0000", StringUtils.EMPTY); + escapeXml11Map.put("\u000b", " "); + escapeXml11Map.put("\u000c", " "); + escapeXml11Map.put("\ufffe", StringUtils.EMPTY); + escapeXml11Map.put("\uffff", StringUtils.EMPTY); + ESCAPE_XML11 = new AggregateTranslator( + new LookupTranslator(EntityArrays.BASIC_ESCAPE), + new LookupTranslator(EntityArrays.APOS_ESCAPE), + new LookupTranslator(Collections.unmodifiableMap(escapeXml11Map)), + NumericEntityEscaper.between(0x1, 0x8), + NumericEntityEscaper.between(0xe, 0x1f), + NumericEntityEscaper.between(0x7f, 0x84), + NumericEntityEscaper.between(0x86, 0x9f), + new UnicodeUnpairedSurrogateRemover() + ); + } + + /** + * Translator object for escaping HTML version 3.0. + * + * While {@link #escapeHtml3(String)} is the expected method of use, this + * object allows the HTML escaping functionality to be used + * as the foundation for a custom translator. + */ + public static final CharSequenceTranslator ESCAPE_HTML3 = + new AggregateTranslator( + new LookupTranslator(EntityArrays.BASIC_ESCAPE), + new LookupTranslator(EntityArrays.ISO8859_1_ESCAPE) + ); + + /** + * Translator object for escaping HTML version 4.0. + * + * While {@link #escapeHtml4(String)} is the expected method of use, this + * object allows the HTML escaping functionality to be used + * as the foundation for a custom translator. + */ + public static final CharSequenceTranslator ESCAPE_HTML4 = + new AggregateTranslator( + new LookupTranslator(EntityArrays.BASIC_ESCAPE), + new LookupTranslator(EntityArrays.ISO8859_1_ESCAPE), + new LookupTranslator(EntityArrays.HTML40_EXTENDED_ESCAPE) + ); + /** + * Translator object for escaping individual Comma Separated Values. + * + * While {@link #escapeCsv(String)} is the expected method of use, this + * object allows the CSV escaping functionality to be used + * as the foundation for a custom translator. + */ + public static final CharSequenceTranslator ESCAPE_CSV = new CsvTranslators.CsvEscaper(); + + /* UNESCAPE TRANSLATORS */ + + /** + * Translator object for escaping Shell command language. + * + * @see Shell Command Language + */ + public static final CharSequenceTranslator ESCAPE_XSI; + static { + final Map escapeXsiMap = new HashMap<>(); + escapeXsiMap.put("|", "\\|"); + escapeXsiMap.put("&", "\\&"); + escapeXsiMap.put(";", "\\;"); + escapeXsiMap.put("<", "\\<"); + escapeXsiMap.put(">", "\\>"); + escapeXsiMap.put("(", "\\("); + escapeXsiMap.put(")", "\\)"); + escapeXsiMap.put("$", "\\$"); + escapeXsiMap.put("`", "\\`"); + escapeXsiMap.put("\\", "\\\\"); + escapeXsiMap.put("\"", "\\\""); + escapeXsiMap.put("'", "\\'"); + escapeXsiMap.put(" ", "\\ "); + escapeXsiMap.put("\t", "\\\t"); + escapeXsiMap.put("\r\n", StringUtils.EMPTY); + escapeXsiMap.put("\n", StringUtils.EMPTY); + escapeXsiMap.put("*", "\\*"); + escapeXsiMap.put("?", "\\?"); + escapeXsiMap.put("[", "\\["); + escapeXsiMap.put("#", "\\#"); + escapeXsiMap.put("~", "\\~"); + escapeXsiMap.put("=", "\\="); + escapeXsiMap.put("%", "\\%"); + ESCAPE_XSI = new LookupTranslator( + Collections.unmodifiableMap(escapeXsiMap) + ); + } + + /** + * Translator object for unescaping escaped Java. + * + * While {@link #unescapeJava(String)} is the expected method of use, this + * object allows the Java unescaping functionality to be used + * as the foundation for a custom translator. + */ + public static final CharSequenceTranslator UNESCAPE_JAVA; + + static { + final Map unescapeJavaMap = new HashMap<>(); + unescapeJavaMap.put("\\\\", "\\"); + unescapeJavaMap.put("\\\"", "\""); + unescapeJavaMap.put("\\'", "'"); + unescapeJavaMap.put("\\", StringUtils.EMPTY); + UNESCAPE_JAVA = new AggregateTranslator( + new OctalUnescaper(), // .between('\1', '\377'), + new UnicodeUnescaper(), + new LookupTranslator(EntityArrays.JAVA_CTRL_CHARS_UNESCAPE), + new LookupTranslator(Collections.unmodifiableMap(unescapeJavaMap)) + ); + } + + /** + * Translator object for unescaping escaped EcmaScript. + * + * While {@link #unescapeEcmaScript(String)} is the expected method of use, this + * object allows the EcmaScript unescaping functionality to be used + * as the foundation for a custom translator. + */ + public static final CharSequenceTranslator UNESCAPE_ECMASCRIPT = UNESCAPE_JAVA; + + /** + * Translator object for unescaping escaped Json. + * + * While {@link #unescapeJson(String)} is the expected method of use, this + * object allows the Json unescaping functionality to be used + * as the foundation for a custom translator. + */ + public static final CharSequenceTranslator UNESCAPE_JSON = UNESCAPE_JAVA; + + /** + * Translator object for unescaping escaped HTML 3.0. + * + * While {@link #unescapeHtml3(String)} is the expected method of use, this + * object allows the HTML unescaping functionality to be used + * as the foundation for a custom translator. + */ + public static final CharSequenceTranslator UNESCAPE_HTML3 = + new AggregateTranslator( + new LookupTranslator(EntityArrays.BASIC_UNESCAPE), + new LookupTranslator(EntityArrays.ISO8859_1_UNESCAPE), + new NumericEntityUnescaper() + ); + + /** + * Translator object for unescaping escaped HTML 4.0. + * + * While {@link #unescapeHtml4(String)} is the expected method of use, this + * object allows the HTML unescaping functionality to be used + * as the foundation for a custom translator. + */ + public static final CharSequenceTranslator UNESCAPE_HTML4 = + new AggregateTranslator( + new LookupTranslator(EntityArrays.BASIC_UNESCAPE), + new LookupTranslator(EntityArrays.ISO8859_1_UNESCAPE), + new LookupTranslator(EntityArrays.HTML40_EXTENDED_UNESCAPE), + new NumericEntityUnescaper() + ); + + /** + * Translator object for unescaping escaped XML. + * + * While {@link #unescapeXml(String)} is the expected method of use, this + * object allows the XML unescaping functionality to be used + * as the foundation for a custom translator. + */ + public static final CharSequenceTranslator UNESCAPE_XML = + new AggregateTranslator( + new LookupTranslator(EntityArrays.BASIC_UNESCAPE), + new LookupTranslator(EntityArrays.APOS_UNESCAPE), + new NumericEntityUnescaper() + ); + + /** + * Translator object for unescaping escaped Comma Separated Value entries. + * + * While {@link #unescapeCsv(String)} is the expected method of use, this + * object allows the CSV unescaping functionality to be used + * as the foundation for a custom translator. + */ + public static final CharSequenceTranslator UNESCAPE_CSV = new CsvTranslators.CsvUnescaper(); + + /* Helper functions */ + + /** + * Translator object for unescaping escaped XSI Value entries. + * + * While {@link #unescapeXSI(String)} is the expected method of use, this + * object allows the XSI unescaping functionality to be used + * as the foundation for a custom translator. + */ + public static final CharSequenceTranslator UNESCAPE_XSI = new XsiUnescaper(); + + /** + * Get a {@link Builder}. + * @param translator the text translator + * @return {@link Builder} + */ + public static StringEscapeUtils.Builder builder(final CharSequenceTranslator translator) { + return new Builder(translator); + } + + /** + * Returns a {@code String} value for a CSV column enclosed in double quotes, + * if required. + * + *

If the value contains a comma, newline or double quote, then the + * String value is returned enclosed in double quotes.

+ * + *

Any double quote characters in the value are escaped with another double quote.

+ * + *

If the value does not contain a comma, newline or double quote, then the + * String value is returned unchanged.

+ * + * see Wikipedia and + * RFC 4180. + * + * @param input the input CSV column String, may be null + * @return The input String, enclosed in double quotes if the value contains a comma, + * newline or double quote, {@code null} if null string input + */ + public static final String escapeCsv(final String input) { + return ESCAPE_CSV.translate(input); + } + + /** + * Escapes the characters in a {@code String} using EcmaScript String rules. + * + *

Escapes any values it finds into their EcmaScript String form. + * Deals correctly with quotes and control-chars (tab, backslash, cr, ff, etc.)

+ * + *

So a tab becomes the characters {@code '\\'} and + * {@code 't'}.

+ * + *

The only difference between Java strings and EcmaScript strings + * is that in EcmaScript, a single quote and forward-slash (/) are escaped.

+ * + *

Note that EcmaScript is best known by the JavaScript and ActionScript dialects.

+ * + *

Example:

+ *
+     * input string: He didn't say, "Stop!"
+     * output string: He didn\'t say, \"Stop!\"
+     * 
+ * + * Security Note. We only provide backslash escaping in this method. For example, {@code '\"'} has the output + * {@code '\\\"'} which could result in potential issues in the case where the string being escaped is being used + * in an HTML tag like {@code + [if-any http] + [for http][end] + [end] + [if-any ftp] + [for ftp][end] + [end] + [if-any backup] + [for backup][end] + [end] + + +

+ + +

+ It is essential that you + verify the integrity + of downloaded files, preferably using the PGP signature (*.asc files); + failing that using the SHA512 hash (*.sha512 checksum files). +

+

+ The KEYS + file contains the public PGP keys used by Apache Commons developers + to sign releases. +

+ + +
+ + + + + + + + + + + + +
commons-text-1.10.0-bin.tar.gzsha512pgp
commons-text-1.10.0-bin.zipsha512pgp
+
+ + + + + + + + + + + + +
commons-text-1.10.0-src.tar.gzsha512pgp
commons-text-1.10.0-src.zipsha512pgp
+
+
+
+

+ Older releases can be obtained from the archives. +

+ +
+ + diff --git a/sources/src/site/xdoc/index.xml b/sources/src/site/xdoc/index.xml new file mode 100644 index 0000000..649e07b --- /dev/null +++ b/sources/src/site/xdoc/index.xml @@ -0,0 +1,84 @@ + + + + + Home + Commons Documentation Team + + + +
+ +

+Apache Commons Text is a library focused on algorithms working on strings. +

+ +
+ +
+

+We provide documentation in the form of a User Guide, +Javadoc, and Project Reports. +

+

+The Git repository can be +browsed, or you can browse/contribute via GitHub. +

+
+ +
+

+ Download the latest release from dependency-info.html

+

+ Alternatively, you can use a build tool dependency. +

+ +

+ For information on previous releases see the Release History, and to download previous releases see the Commons Text Archive. +

+ +
+ +
+

+The commons developer mailing list is the main channel of communication for contributors. Please remember that the lists are shared between all commons components, so prefix your email by [text].

+

You can also visit the #apache-commons IRC channel on irc.freenode.net or peruse JIRA. Specific links of interest for JIRA are:

+ +

Alternatively you can go through the Needs Work tags in the TagList report.

+

If you'd like to offer up pull requests via GitHub rather than applying patches to JIRA, we have a GitHub mirror.

+
+ +
+

+The commons mailing lists act as the main support forum. +The user list is suitable for most library usage queries. +The dev list is intended for the development discussion. +Please remember that the lists are shared between all commons components, +so prefix your email by [text]. +

+

+Bug reports and enhancements are also welcomed via the JIRA issue tracker. +Please read the instructions carefully. +

+
+ + +
diff --git a/sources/src/site/xdoc/issue-tracking.xml b/sources/src/site/xdoc/issue-tracking.xml new file mode 100644 index 0000000..49f6dff --- /dev/null +++ b/sources/src/site/xdoc/issue-tracking.xml @@ -0,0 +1,102 @@ + + + + + + Apache Commons Text Issue tracking + Apache Commons Documentation Team + + + +
+

+ Apache Commons Text uses ASF JIRA for tracking issues. + See the Apache Commons Text JIRA project page. +

+ +

+ To use JIRA you may need to create an account + (if you have previously created/updated Commons issues using Bugzilla an account will have been automatically + created and you can use the Forgot Password + page to get a new password). +

+ +

+ If you would like to report a bug, or raise an enhancement request with + Apache Commons Text please do the following: +

    +
  1. Search existing open bugs. + If you find your issue listed then please add a comment with your details.
  2. +
  3. Search the mailing list archive(s). + You may find your issue or idea has already been discussed.
  4. +
  5. Decide if your issue is a bug or an enhancement.
  6. +
  7. Submit either a bug report + or enhancement request.
  8. +
+

+ +

+ Please also remember these points: +

    +
  • the more information you provide, the better we can help you
  • +
  • test cases are vital, particularly for any proposed enhancements
  • +
  • the developers of Apache Commons Text are all unpaid volunteers
  • +
+

+ +

+ For more information on creating patches see the + Apache Contributors Guide. +

+ +

+ You may also find these links useful: +

+

+
+ +
diff --git a/sources/src/site/xdoc/mail-lists.xml b/sources/src/site/xdoc/mail-lists.xml new file mode 100644 index 0000000..3a6dd20 --- /dev/null +++ b/sources/src/site/xdoc/mail-lists.xml @@ -0,0 +1,215 @@ + + + + + + Apache Commons Text Mailing Lists + Apache Commons Documentation Team + + + +
+

+ Apache Commons Text shares mailing lists with all the other + Commons Components. + To make it easier for people to only read messages related to components they are interested in, + the convention in Commons is to prefix the subject line of messages with the component's name, + for example: +

    +
  • [text] Problem with the ...
  • +
+

+

+ Questions related to the usage of Apache Commons Text should be posted to the + User List. +
+ The Developer List + is for questions and discussion related to the development of Apache Commons Text. +
+ Please do not cross-post; developers are also subscribed to the user list. +
+ You must be subscribed to post to the mailing lists. Follow the Subscribe links below + to subscribe. +

+

+ Note: please don't send patches or attachments to any of the mailing lists. + Patches are best handled via the Issue Tracking system. + Otherwise, please upload the file to a public server and include the URL in the mail. +

+
+ +
+

+ Please prefix the subject line of any messages for Apache Commons Text + with [text] - thanks! +
+
+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameSubscribeUnsubscribePostArchiveOther Archives
+ Commons User List +

+ Questions on using Apache Commons Text. +

+
SubscribeUnsubscribePostmail-archives.apache.org
+ lists.apache.org +
markmail.org
+ www.mail-archive.com
+ news.gmane.org +
+ Commons Developer List +

+ Discussion of development of Apache Commons Text. +

+
SubscribeUnsubscribePostmail-archives.apache.org
+ lists.apache.org +
markmail.org
+ www.mail-archive.com
+ news.gmane.org +
+ Commons Issues List +

+ Only for e-mails automatically generated by the issue tracking system. +

+
SubscribeUnsubscriberead onlymail-archives.apache.org
+ lists.apache.org +
markmail.org
+ www.mail-archive.com +
+ Commons Commits List +

+ Only for e-mails automatically generated by the source control system. +

+
SubscribeUnsubscriberead onlymail-archives.apache.org
+ lists.apache.org +
markmail.org
+ www.mail-archive.com +
+ +
+
+

+ Other mailing lists which you may find useful include: +

+ + + + + + + + + + + + + + + + + + +
NameSubscribeUnsubscribePostArchiveOther Archives
+ Apache Announce List +

+ General announcements of Apache project releases. +

+
SubscribeUnsubscriberead onlymail-archives.apache.org
+ lists.apache.org +
markmail.org
+ old.nabble.com
+ www.mail-archive.com
+ news.gmane.org +
+ +
+ +
diff --git a/sources/src/site/xdoc/proposal.xml b/sources/src/site/xdoc/proposal.xml new file mode 100644 index 0000000..1af93b5 --- /dev/null +++ b/sources/src/site/xdoc/proposal.xml @@ -0,0 +1,89 @@ + + + +Proposal for Apache Commons Text Package + + + +
+ + + +

Providing algorithms for processing texts like editing distance or +similarity is out of scope of the standard Java libraries. The +Commons Text Package provides these extra methods.

+ + +
+ + +

This proposal is to create a package of Java utility classes implementing +well known string algorithms and metrics.

+ + +
+ + +

Commons Text relies only on standard JDK 7 (or later) APIs for +production deployment. It utilizes the JUnit unit testing framework and +the hamcrest matcher library for developing and executing unit tests, but +this is of interest only to developers of the component. Commons Text may be +a dependency for several existing components in the open source world that +implement higher order text processing.

+ +

No external configuration files are utilized.

+ + +
+ + +

The initial classes came from the Commons Lang and Commons Codec subprojects.

+ +

The proposed package name for the new component is +org.apache.commons.text.

+ + +
+ + +
    +
  • Git Repository - New repository commons-text.
  • +
  • Mailing List - Discussions will take place on the general + dev@commons.apache.org mailing list. To help + list subscribers identify messages of interest, it is suggested that + the message subject of messages about this component be prefixed with + [text].
  • +
  • Jira - New component "Common Text" under the "Commons Sandbox" product.
  • +
  • Confluence FAQ - New category "commons-text" (when available).
  • +
+ + +
+ + +

The initial committers on the Commons Text component shall be as follows: +

    +
  • Benedikt Ritter (britter)
  • +
  • Bruno P. Kinoshita (kinow)
  • +
+

+ +
+
+ +
diff --git a/sources/src/site/xdoc/security.xml b/sources/src/site/xdoc/security.xml new file mode 100644 index 0000000..e7ba25e --- /dev/null +++ b/sources/src/site/xdoc/security.xml @@ -0,0 +1,118 @@ + + + + + Apache Commons Text Security Reports + Commons Team + + +
+

+ For information about reporting or asking questions about + security, please see the + security page + of the Apache Commons project. +

+

+ This page lists all security vulnerabilities fixed in released versions of this component. +

+ +

+ Please note that binary patches are never provided. If you need to apply a source code patch, use the + building instructions for the component version that you are using. +

+ +

+ If you need help on building this component or other help on following the instructions to + mitigate the + known vulnerabilities listed here, please send your questions to the public + user mailing list + . +

+ +

+ If you have encountered an unlisted security vulnerability or other unexpected behavior that has security + impact, or if the descriptions here are incomplete, please report them privately to the Apache Security + Team. Thank you. +

+ + +

+ On 2022-10-13, the Apache Commons Text team disclosed + CVE-2022-42889 + . Key takeaways: +

    +
  • + If you rely on software that uses a version of commons-text prior to 1.10.0, you are likely + still not vulnerable: only if this software uses the + StringSubstitutor + API without properly sanitizing any untrusted input. +
  • +
  • + If your own software uses commons-text, double-check whether it uses the + StringSubstitutor + API without properly sanitizing any untrusted input. If so, an update to 1.10.0 could be a + quick workaround, but the recommended solution is to also properly validate and sanitize any + untrusted input. +
  • +
+

+

+ Apache Commons Text is a low-level library for performing various text operations, such as escaping, calculating string + differences, and substituting placeholders in the text with values looked up through interpolators. + When using the string substitution feature, some of the available interpolators can trigger network + access or code execution. This is intended, but it also means an application that includes user + input in the string passed to the substitution without properly sanitizing it would allow an + attacker to trigger those interpolators. +

+

For that reason the Apache Commons Text team have decided to update the configuration to be more + "secure by default", so that the impact of a failure to validate inputs is mitigated and will not + give an attacker access to these interpolators. However, it is still recommended that users treat + untrusted input with care. +

+

+ We're not currently aware of any applications that pass untrusted input to the substitutor and thus would have been + impacted by this problem prior to Apache Commons Text 1.10.0. +

+

+ This issue is different from + Log4Shell (CVE-2021-44228) + because in Log4Shell, string interpolation was possible from the log message body, which commonly + contains untrusted input. In the Apache Common Text issue, the relevant method is explicitly + intended and clearly documented to perform string interpolation, so it is much less likely that + applications would inadvertently pass in untrusted input without proper validation. +

+

+ Credit: this issue was reported independently by Ruilin and by + @pwntester (Alvaro Muñoz) + of the + GitHub Security Lab team + . Thank you! +

+

+ References: +

+

+
+
+ +
diff --git a/sources/src/site/xdoc/userguide.xml b/sources/src/site/xdoc/userguide.xml new file mode 100644 index 0000000..db71471 --- /dev/null +++ b/sources/src/site/xdoc/userguide.xml @@ -0,0 +1,290 @@ + + + + + + + Commons Text - User guide + Commons Documentation Team + + + + +
+
+

The Commons Text Package +

+

Users Guide

+
+ [Description] + [text] + [text.diff] + [text.lookup] + [text.similarity] + [text.translate] +
+
+
+
+ +
+

The Commons Text library provides additions to the standard JDK's + text handling. Our goal is to provide a consistent set of tools for + processing text generally from computing distances between Strings + to being able to efficiently do String escaping of various types. +

+
+ +
+ +

Originally the text package was added in Commons Lang 2.2. However, its + new home is here. It provides, amongst other + classes, a replacement for StringBuffer named + StrBuilder, a class for substituting variables within a String + named StrSubstitutor and a replacement for StringTokenizer + named StrTokenizer. While somewhat ungainly, the + Str + prefix has been used to ensure we don't clash with any current + or future standard Java classes. +

+ +

Beyond the text utilities ported over from Commons Lang, we have also included various + string similarity and distance functions. Lastly, there are also utilities for + addressing differences between bodies of text for the sake of viewing these + differences. +

+ + +

From Lang 3.5, we have moved into Text StringEscapeUtils and StrTokenizer. + It provides ways in which to generate pieces of text, such as might + be used for default passwords. StringEscapeUtils contains methods to + escape and unescape Java, JavaScript, HTML and XML. It is worth noting that + the package org.apache.commons.text.translate holds the + functionality underpinning the StringEscapeUtils with mappings and translations + between such mappings for the sake of doing String escaping. StrTokenizer is + an improved alternative to java.util.StringTokenizer. +

+
+ + +

+ The simplest example is to use this class to replace Java System properties. For example: +

              
+        StringSubstitutor.replaceSystemProperties(
+          "You are running with java.version = ${java.version} and os.name = ${os.name}.");
+        
+

+

+ For details see StringSubstitutor. +

+

+ Use a StringSubstitutorReader + to avoid reading a whole file into memory as a String to perform string substitution, for example, when a Servlet filters a file to a client. +

+

+ To build a default full-featured substitutor, use: +

+ +

+ The available substitutions are defined in + org.apache.commons.text.lookup.StringLookupFactory. +

+
+ + +

The org.apache.commons.text.similarity packages contains various different mechanisms of + calculating "similarity scores" as well as "edit distances between Strings. Note, + the difference between a "similarity score" and a "distance function" is that + a distance functions meets the following qualifications: +

    +
  • d(x,y) >= 0, non-negativity or separation axiom +
  • +
  • d(x,y) == 0, if and only if, + x == y +
  • +
  • d(x,y) == d(y,x), symmetry, and +
  • +
  • d(x,z) <= d(x,y) + d(y,z), the triangle inequality +
  • +
+ whereas a "similarity score" need not satisfy all such properties. Though, it + is fairly easy to "normalize" a similarity score to manufacture an "edit distance." +

+

+ The list of "edit distances" that we currently support follow: +

    +
  • Cosine Distance,
  • +
  • Hamming Distance,
  • +
  • Jaccard Distance,
  • +
  • Jaro Winkler Distance,
  • +
  • Levenshtein Distance,
  • +
  • Longest Commons Subsequence Distance,
  • +
+ and the list of "similarity scores" that we support follows: +
    +
  • Cosine Similarity,
  • +
  • Fuzzy Score Similarity,
  • +
  • Jaccard Similarity,
  • +
  • Jaro-Winkler Similarity, and
  • +
  • Longest Common Subsequence Similarity.
  • +
+

+
+ + +

The org.apache.commons.text.diff package contains code for + doing diff between strings. The initial implementation of the Myers algorithm was adapted from the + commons-collections sequence package. +

+
+ + +
+ +
+ +

Provides algorithms for diff between strings.

+

The initial implementation of the Myers algorithm was adapted from the + commons-collections sequence package. +

+
+ +
+

Provides algorithms for looking up strings used by a + StringSubstitutor. + Standard lookups are defined in + StringLookupFactory + and the associated + DefaultStringLookup + enum. +

+

+ The example below demonstrates use of the default lookups for StringSubstitutor in order to + construct a complex string. +

+

NOTE: The list of lookups available by default changed in version 1.10.0. See the documentation for + StringLookupFactory + for details and instructions on how to reproduce the previous behavior. +

+ +final StringSubstitutor interpolator = StringSubstitutor.createInterpolator(); +final String text = interpolator.replace( + "Base64 Decoder: ${base64Decoder:SGVsbG9Xb3JsZCE=}\n" + + "Base64 Encoder: ${base64Encoder:HelloWorld!}\n" + + "Java Constant: ${const:java.awt.event.KeyEvent.VK_ESCAPE}\n" + + "Date: ${date:yyyy-MM-dd}\n" + + "Environment Variable: ${env:USERNAME}\n" + + "File Content: ${file:UTF-8:src/test/resources/document.properties}\n" + + "Java: ${java:version}\n" + + "Localhost: ${localhost:canonical-name}\n" + + "Properties File: ${properties:src/test/resources/document.properties::mykey}\n" + + "Resource Bundle: ${resourceBundle:org.apache.commons.text.example.testResourceBundleLookup:mykey}\n" + + "System Property: ${sys:user.dir}\n" + + "URL Decoder: ${urlDecoder:Hello%20World%21}\n" + + "URL Encoder: ${urlEncoder:Hello World!}\n" + + "XML XPath: ${xml:src/test/resources/document.xml:/root/path/to/node}\n" +); + +
+ +
+ +

Provides algorithms for string similarity.

+ +

The algorithms that implement the EditDistance interface follow the + same + simple principle: the more similar (closer) strings are, the lower is the + distance. + For example, the words house and hose are closer than house and + trousers. +

+ +

The following algorithms are available at the moment:

+ +
    +
  • + CosineDistance +
  • +
  • + CosineSimilarity +
  • +
  • + FuzzyScore +
  • +
  • + HammingDistance +
  • +
  • + JaroWinklerDistance +
  • +
  • + JaroWinklerSimilarity +
  • +
  • + LevenshteinDistance +
  • +
  • + LongestCommonSubsequenceDistance +
  • +
+ +

The CosineDistance utilises a + RegexTokenizer + regular expression tokenizer (\w+). And the + LevenshteinDistance's + behavior can be changed to take into consideration a maximum + throughput. +

+
+ +
+ +

An API for creating text translation routines from a set of smaller + building blocks. Initially created to make it possible for the user to + customize the rules in the StringEscapeUtils class. +

+

These classes are immutable, and therefore thread-safe.

+
+ + +
diff --git a/sources/src/test/java/org/apache/commons/text/AlphabetConverterTest.java b/sources/src/test/java/org/apache/commons/text/AlphabetConverterTest.java new file mode 100644 index 0000000..fcab45e --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/AlphabetConverterTest.java @@ -0,0 +1,300 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.ArrayUtils; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link AlphabetConverter}. + */ +public class AlphabetConverterTest { + + private static final Character[] LOWER_CASE_ENGLISH = {' ', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', + 't', 'u', 'v', 'w', 'x', 'y', 'z'}; + + private static final Character[] ENGLISH_AND_NUMBERS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', + 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' '}; + + private static final Character[] LOWER_CASE_ENGLISH_AND_NUMBERS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ' '}; + + private static final Character[] NUMBERS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; + + private static final Character[] BINARY = {'0', '1'}; + + private static final Character[] HEBREW = {'_', ' ', '\u05e7', '\u05e8', '\u05d0', '\u05d8', '\u05d5', '\u05df', '\u05dd', '\u05e4', '\u05e9', '\u05d3', + '\u05d2', '\u05db', '\u05e2', '\u05d9', '\u05d7', '\u05dc', '\u05da', '\u05e3', '\u05d6', '\u05e1', '\u05d1', '\u05d4', '\u05e0', '\u05de', '\u05e6', + '\u05ea', '\u05e5'}; + + private static final Integer[] UNICODE = {32, 35395, 35397, 36302, 36291, 35203, 35201, 35215, 35219, 35268, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, + 107, 108, 109, 110, 1001, 1002, 1003, 1004, 1005}; + + private static final Integer[] LOWER_CASE_ENGLISH_CODEPOINTS = {32, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, + 115, 116, 117, 118, 119, 120, 121, 122}; + + private static final Integer[] DO_NOT_ENCODE_CODEPOINTS = {32, 97, 98, 99}; // space, a, b, c + + @Test + public void binaryTest() throws UnsupportedEncodingException { + test(BINARY, NUMBERS, ArrayUtils.EMPTY_CHARACTER_OBJECT_ARRAY, "0", "1", "10", "11"); + test(NUMBERS, BINARY, ArrayUtils.EMPTY_CHARACTER_OBJECT_ARRAY, "12345", "0"); + test(LOWER_CASE_ENGLISH, BINARY, ArrayUtils.EMPTY_CHARACTER_OBJECT_ARRAY, "abc", "a"); + } + + private AlphabetConverter createJavadocExample() { + final Character[] original = {'a', 'b', 'c', 'd'}; + final Character[] encoding = {'0', '1', 'd'}; + final Character[] doNotEncode = {'d'}; + + return AlphabetConverter.createConverterFromChars(original, encoding, doNotEncode); + } + + @Test + public void doNotEncodeTest() throws UnsupportedEncodingException { + test(ENGLISH_AND_NUMBERS, LOWER_CASE_ENGLISH_AND_NUMBERS, LOWER_CASE_ENGLISH, "1", "456", "abc", "ABC", "this will not be converted but THIS WILL"); + test(ENGLISH_AND_NUMBERS, LOWER_CASE_ENGLISH_AND_NUMBERS, NUMBERS, "1", "456", "abc", "ABC", "this will be converted but 12345 and this will be"); + } + + @Test + public void encodeFailureTest() { + assertThatThrownBy(() -> test(BINARY, NUMBERS, ArrayUtils.EMPTY_CHARACTER_OBJECT_ARRAY, "3")).isInstanceOf(UnsupportedEncodingException.class) + .hasMessage("Couldn't find encoding for '3' in 3"); + } + + @Test + public void hebrewTest() throws UnsupportedEncodingException { + test(HEBREW, BINARY, ArrayUtils.EMPTY_CHARACTER_OBJECT_ARRAY, "\u05d0", "\u05e2", + "\u05d0\u05dc\u05e3_\u05d0\u05d5\u05d4\u05d1\u05dc_\u05d1\u05d9\u05ea_\u05d6\u05d4_\u05d1\u05d9\u05ea_" + + "\u05d2\u05d9\u05de\u05dc_\u05d6\u05d4_\u05db\u05de\u05dc_\u05d2\u05d3\u05d5\u05dc"); + test(HEBREW, NUMBERS, ArrayUtils.EMPTY_CHARACTER_OBJECT_ARRAY, "\u05d0", "\u05e2", + "\u05d0\u05dc\u05e3_\u05d0\u05d5\u05d4\u05d1\u05dc_\u05d1\u05d9\u05ea_\u05d6\u05d4_\u05d1\u05d9\u05ea_" + + "\u05d2\u05d9\u05de\u05dc_\u05d6\u05d4_\u05db\u05de\u05dc_\u05d2\u05d3\u05d5\u05dc"); + test(NUMBERS, HEBREW, ArrayUtils.EMPTY_CHARACTER_OBJECT_ARRAY, "123456789", "1", "5"); + test(LOWER_CASE_ENGLISH, HEBREW, ArrayUtils.EMPTY_CHARACTER_OBJECT_ARRAY, "this is a test"); + } + + /* + * Test example in javadocs for consistency + */ + @Test + public void javadocExampleTest() throws UnsupportedEncodingException { + final AlphabetConverter ac = createJavadocExample(); + + assertThat(ac.encode("a")).isEqualTo("00"); + assertThat(ac.encode("b")).isEqualTo("01"); + assertThat(ac.encode("c")).isEqualTo("0d"); + assertThat(ac.encode("d")).isEqualTo("d"); + assertThat(ac.encode("abcd")).isEqualTo("00010dd"); + } + + @Test + public void missingDoNotEncodeLettersFromEncodingTest() { + assertThatThrownBy(() -> AlphabetConverter.createConverterFromChars(ENGLISH_AND_NUMBERS, LOWER_CASE_ENGLISH, NUMBERS)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("Can not use 'do not encode' list because encoding alphabet does not contain '0'"); + } + + @Test + public void missingDoNotEncodeLettersFromOriginalTest() { + assertThatThrownBy(() -> AlphabetConverter.createConverterFromChars(LOWER_CASE_ENGLISH, ENGLISH_AND_NUMBERS, NUMBERS)) + .isInstanceOf(IllegalArgumentException.class).hasMessage("Can not use 'do not encode' list because original alphabet does not contain '0'"); + } + + @Test + public void noEncodingLettersTest() { + assertThatThrownBy(() -> AlphabetConverter.createConverterFromChars(ENGLISH_AND_NUMBERS, NUMBERS, NUMBERS)).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Must have at least two encoding characters (excluding those in the 'do not encode' list), but has 0"); + } + + @Test + public void onlyOneEncodingLettersTest() { + assertThatThrownBy(() -> { + final Character[] numbersPlusUnderscore = Arrays.copyOf(NUMBERS, NUMBERS.length + 1); + numbersPlusUnderscore[numbersPlusUnderscore.length - 1] = '_'; + + AlphabetConverter.createConverterFromChars(ENGLISH_AND_NUMBERS, numbersPlusUnderscore, NUMBERS); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Must have at least two encoding characters (excluding those in the 'do not encode' list), but has 1"); + } + + private void test(final Character[] originalChars, final Character[] encodingChars, final Character[] doNotEncodeChars, final String... strings) + throws UnsupportedEncodingException { + + final AlphabetConverter ac = AlphabetConverter.createConverterFromChars(originalChars, encodingChars, doNotEncodeChars); + + final AlphabetConverter reconstructedAlphabetConverter = AlphabetConverter.createConverterFromMap(ac.getOriginalToEncoded()); + + assertThat(reconstructedAlphabetConverter).isEqualTo(ac); + assertThat(reconstructedAlphabetConverter.hashCode()).isEqualTo(ac.hashCode()); + assertThat(reconstructedAlphabetConverter.toString()).isEqualTo(ac.toString()); + assertThat(ac.encode(null)).isNull(); // test null conversions + assertThat(ac.encode("")).isEqualTo(""); // test empty conversion + + // test all the trial strings + for (final String s : strings) { + final String encoded = ac.encode(s); + + // test that only encoding chars are used + final List originalEncodingChars = Arrays.asList(encodingChars); + for (int i = 0; i < encoded.length(); i++) { + assertThat(originalEncodingChars.contains(encoded.charAt(i))).isTrue(); + } + + final String decoded = ac.decode(encoded); + + // test that only the original alphabet is used after decoding + final List originalCharsList = Arrays.asList(originalChars); + for (int i = 0; i < decoded.length(); i++) { + assertThat(originalCharsList.contains(decoded.charAt(i))).isTrue(); + } + + assertThat(decoded).as("Encoded '" + s + "' into '" + encoded + "', but decoded into '" + decoded + "'").isEqualTo(s); + } + } + + @Test + public void testCreateConverterFromCharsAndEquals() { + final Character[] characterArray = new Character[2]; + final char charOne = '+'; + final char character = '+'; + characterArray[0] = character; + characterArray[1] = characterArray[0]; + final AlphabetConverter alphabetConverter = AlphabetConverter.createConverterFromChars(characterArray, characterArray, characterArray); + + assertThat(alphabetConverter.equals(charOne)).isFalse(); + } + + @Test + public void testCreateConverterFromCharsOne() { + final Character[] characterArray = new Character[2]; + characterArray[0] = '5'; + characterArray[1] = characterArray[0]; + final AlphabetConverter alphabetConverter = AlphabetConverter.createConverterFromChars(characterArray, characterArray, characterArray); + + assertThat(alphabetConverter.getEncodedCharLength()).isEqualTo(1); + } + + @Test + public void testCreateConverterFromCharsWithNullAndNull() { + assertThatIllegalArgumentException().isThrownBy(() -> { + final Character[] characterArray = new Character[2]; + characterArray[0] = '$'; + characterArray[1] = characterArray[0]; + + AlphabetConverter.createConverterFromChars(characterArray, null, null); + }); + } + + @Test + public void testCreateConverterFromMapAndEquals() { + final Map hashMap = new HashMap<>(); + final AlphabetConverter alphabetConverter = AlphabetConverter.createConverterFromMap(hashMap); + hashMap.put(0, "CtDs"); + final AlphabetConverter alphabetConverterTwo = AlphabetConverter.createConverterFromMap(hashMap); + + assertThat(alphabetConverter.equals(alphabetConverterTwo)).isFalse(); + assertThat(alphabetConverter.getEncodedCharLength()).isEqualTo(1); + } + + @Test + public void testDecodeReturningNull() throws UnsupportedEncodingException { + final Map map = new HashMap<>(); + final AlphabetConverter alphabetConverter = AlphabetConverter.createConverterFromMap(map); + alphabetConverter.decode(null); + + assertThat(alphabetConverter.getEncodedCharLength()).isEqualTo(1); + } + + @Test + public void testEquals() { + final Character[] characterArray = new Character[2]; + final char character = 'R'; + characterArray[0] = character; + characterArray[1] = character; + final AlphabetConverter alphabetConverter = AlphabetConverter.createConverterFromChars(characterArray, characterArray, characterArray); + final Map map = new HashMap<>(); + final AlphabetConverter alphabetConverterTwo = AlphabetConverter.createConverterFromMap(map); + + assertThat(alphabetConverterTwo.getEncodedCharLength()).isEqualTo(1); + assertThat(alphabetConverter.equals(alphabetConverterTwo)).isFalse(); + } + + @Test + public void testEqualsWithNull() { + final Character[] characterArray = ArrayUtils.EMPTY_CHARACTER_OBJECT_ARRAY; + final AlphabetConverter alphabetConverter = AlphabetConverter.createConverterFromChars(characterArray, null, null); + + assertThat(alphabetConverter.equals(null)).isFalse(); + } + + @Test + public void testEqualsWithSameObject() { + final Character[] characterArray = new Character[2]; + final char character = 'R'; + characterArray[0] = character; + characterArray[1] = character; + final AlphabetConverter alphabetConverter = AlphabetConverter.createConverterFromChars(characterArray, characterArray, characterArray); + + assertThat(alphabetConverter.equals(alphabetConverter)).isTrue(); + } + + @Test + public void unexpectedEndWhileDecodingTest() { + final String toDecode = "00d01d0"; + assertThatThrownBy(() -> { + final AlphabetConverter ac = createJavadocExample(); + ac.decode(toDecode); + }).isInstanceOf(UnsupportedEncodingException.class).hasMessage("Unexpected end of string while decoding " + toDecode); + } + + @Test + public void unexpectedStringWhileDecodingTest() { + final String toDecode = "00XX"; + assertThatThrownBy(() -> { + final AlphabetConverter ac = createJavadocExample(); + ac.decode(toDecode); + }).isInstanceOf(UnsupportedEncodingException.class).hasMessage("Unexpected string without decoding (XX) in " + toDecode); + } + + /** + * Test constructor from code points + */ + @Test + public void unicodeTest() throws UnsupportedEncodingException { + final AlphabetConverter ac = AlphabetConverter.createConverter(UNICODE, LOWER_CASE_ENGLISH_CODEPOINTS, DO_NOT_ENCODE_CODEPOINTS); + + assertThat(ac.getEncodedCharLength()).isEqualTo(2); + + final String original = "\u8a43\u8a45 \u8dce ab \u8dc3 c \u8983"; + final String encoded = ac.encode(original); + final String decoded = ac.decode(encoded); + + assertThat(decoded).as("Encoded '" + original + "' into '" + encoded + "', but decoded into '" + decoded + "'").isEqualTo(original); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/CaseUtilsTest.java b/sources/src/test/java/org/apache/commons/text/CaseUtilsTest.java new file mode 100644 index 0000000..f7195e7 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/CaseUtilsTest.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link CaseUtils} class. + */ +public class CaseUtilsTest { + + @Test + public void testConstructor() { + assertThat(new CaseUtils()).isNotNull(); + final Constructor[] cons = CaseUtils.class.getDeclaredConstructors(); + assertThat(cons.length).isEqualTo(1); + assertThat(Modifier.isPublic(cons[0].getModifiers())).isTrue(); + assertThat(Modifier.isPublic(CaseUtils.class.getModifiers())).isTrue(); + assertThat(Modifier.isFinal(CaseUtils.class.getModifiers())).isFalse(); + } + + @Test + public void testToCamelCase() { + assertThat(CaseUtils.toCamelCase(null, false, null)).isNull(); + assertThat(CaseUtils.toCamelCase("", true, null)).isEqualTo(""); + assertThat(CaseUtils.toCamelCase(" ", false, null)).isEqualTo(""); + assertThat(CaseUtils.toCamelCase("a b c @def", false, null)).isEqualTo("aBC@def"); + assertThat(CaseUtils.toCamelCase("a b c @def", true)).isEqualTo("ABC@def"); + assertThat(CaseUtils.toCamelCase("a b c @def", true, '-')).isEqualTo("ABC@def"); + assertThat(CaseUtils.toCamelCase("a b c @def", true, '-')).isEqualTo("ABC@def"); + + final char[] chars = {'-', '+', ' ', '@'}; + assertThat(CaseUtils.toCamelCase("-+@ ", true, chars)).isEqualTo(""); + assertThat(CaseUtils.toCamelCase(" to-CAMEL-cASE", false, chars)).isEqualTo("toCamelCase"); + assertThat(CaseUtils.toCamelCase("@@@@ to+CAMEL@cASE ", true, chars)).isEqualTo("ToCamelCase"); + assertThat(CaseUtils.toCamelCase("To+CA+ME L@cASE", true, chars)).isEqualTo("ToCaMeLCase"); + + assertThat(CaseUtils.toCamelCase("To.Camel.Case", false, '.')).isEqualTo("toCamelCase"); + assertThat(CaseUtils.toCamelCase("To.Camel-Case", false, '-', '.')).isEqualTo("toCamelCase"); + assertThat(CaseUtils.toCamelCase(" to @ Camel case", false, '-', '@')).isEqualTo("toCamelCase"); + assertThat(CaseUtils.toCamelCase(" @to @ Camel case", true, '-', '@')).isEqualTo("ToCamelCase"); + + assertThat(CaseUtils.toCamelCase("TO CAMEL CASE", true, null)).isEqualTo("ToCamelCase"); + assertThat(CaseUtils.toCamelCase("TO CAMEL CASE", false, null)).isEqualTo("toCamelCase"); + assertThat(CaseUtils.toCamelCase("TO CAMEL CASE", false, null)).isEqualTo("toCamelCase"); + assertThat(CaseUtils.toCamelCase("tocamelcase", false, null)).isEqualTo("tocamelcase"); + assertThat(CaseUtils.toCamelCase("tocamelcase", true, null)).isEqualTo("Tocamelcase"); + assertThat(CaseUtils.toCamelCase("Tocamelcase", false, null)).isEqualTo("tocamelcase"); + + assertThat(CaseUtils.toCamelCase("tocamelcase", true)).isEqualTo("Tocamelcase"); + assertThat(CaseUtils.toCamelCase("tocamelcase", false)).isEqualTo("tocamelcase"); + + assertThat(CaseUtils.toCamelCase("\uD800\uDF00 \uD800\uDF02", true)).isEqualTo("\uD800\uDF00\uD800\uDF02"); + assertThat(CaseUtils.toCamelCase("\uD800\uDF00\uD800\uDF01\uD800\uDF14\uD800\uDF02\uD800\uDF03", true, '\uD800', + '\uDF14')).isEqualTo("\uD800\uDF00\uD800\uDF01\uD800\uDF02\uD800\uDF03"); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/CharacterPredicatesTest.java b/sources/src/test/java/org/apache/commons/text/CharacterPredicatesTest.java new file mode 100644 index 0000000..4a53c6b --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/CharacterPredicatesTest.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link CharacterPredicates}. + */ +public class CharacterPredicatesTest { + + @Test + public void testArabicNumerals() { + assertThat(CharacterPredicates.ARABIC_NUMERALS.test('0')).isTrue(); + assertThat(CharacterPredicates.ARABIC_NUMERALS.test('1')).isTrue(); + assertThat(CharacterPredicates.ARABIC_NUMERALS.test('9')).isTrue(); + + assertThat(CharacterPredicates.ARABIC_NUMERALS.test('/')).isFalse(); + assertThat(CharacterPredicates.ARABIC_NUMERALS.test(':')).isFalse(); + assertThat(CharacterPredicates.ARABIC_NUMERALS.test('a')).isFalse(); + } + + @Test + public void testAsciiAlphaNumerals() { + assertThat(CharacterPredicates.ASCII_ALPHA_NUMERALS.test('a')).isTrue(); + assertThat(CharacterPredicates.ASCII_ALPHA_NUMERALS.test('z')).isTrue(); + assertThat(CharacterPredicates.ASCII_ALPHA_NUMERALS.test('A')).isTrue(); + assertThat(CharacterPredicates.ASCII_ALPHA_NUMERALS.test('Z')).isTrue(); + assertThat(CharacterPredicates.ASCII_ALPHA_NUMERALS.test('0')).isTrue(); + assertThat(CharacterPredicates.ASCII_ALPHA_NUMERALS.test('9')).isTrue(); + + assertThat(CharacterPredicates.ASCII_ALPHA_NUMERALS.test('`')).isFalse(); + assertThat(CharacterPredicates.ASCII_ALPHA_NUMERALS.test('{')).isFalse(); + assertThat(CharacterPredicates.ASCII_ALPHA_NUMERALS.test('@')).isFalse(); + assertThat(CharacterPredicates.ASCII_ALPHA_NUMERALS.test('[')).isFalse(); + assertThat(CharacterPredicates.ASCII_ALPHA_NUMERALS.test('/')).isFalse(); + assertThat(CharacterPredicates.ASCII_ALPHA_NUMERALS.test(':')).isFalse(); + } + + @Test + public void testAsciiLetters() { + assertThat(CharacterPredicates.ASCII_LETTERS.test('a')).isTrue(); + assertThat(CharacterPredicates.ASCII_LETTERS.test('z')).isTrue(); + assertThat(CharacterPredicates.ASCII_LETTERS.test('A')).isTrue(); + assertThat(CharacterPredicates.ASCII_LETTERS.test('Z')).isTrue(); + + assertThat(CharacterPredicates.ASCII_LETTERS.test('9')).isFalse(); + assertThat(CharacterPredicates.ASCII_LETTERS.test('`')).isFalse(); + assertThat(CharacterPredicates.ASCII_LETTERS.test('{')).isFalse(); + assertThat(CharacterPredicates.ASCII_LETTERS.test('@')).isFalse(); + assertThat(CharacterPredicates.ASCII_LETTERS.test('[')).isFalse(); + } + + @Test + public void testAsciiLowercaseLetters() { + assertThat(CharacterPredicates.ASCII_LOWERCASE_LETTERS.test('a')).isTrue(); + assertThat(CharacterPredicates.ASCII_LOWERCASE_LETTERS.test('z')).isTrue(); + + assertThat(CharacterPredicates.ASCII_LOWERCASE_LETTERS.test('9')).isFalse(); + assertThat(CharacterPredicates.ASCII_LOWERCASE_LETTERS.test('A')).isFalse(); + assertThat(CharacterPredicates.ASCII_LOWERCASE_LETTERS.test('Z')).isFalse(); + assertThat(CharacterPredicates.ASCII_LOWERCASE_LETTERS.test('`')).isFalse(); + assertThat(CharacterPredicates.ASCII_LOWERCASE_LETTERS.test('{')).isFalse(); + } + + @Test + public void testAsciiUppercaseLetters() { + assertThat(CharacterPredicates.ASCII_UPPERCASE_LETTERS.test('A')).isTrue(); + assertThat(CharacterPredicates.ASCII_UPPERCASE_LETTERS.test('Z')).isTrue(); + + assertThat(CharacterPredicates.ASCII_UPPERCASE_LETTERS.test('9')).isFalse(); + assertThat(CharacterPredicates.ASCII_UPPERCASE_LETTERS.test('@')).isFalse(); + assertThat(CharacterPredicates.ASCII_UPPERCASE_LETTERS.test('[')).isFalse(); + assertThat(CharacterPredicates.ASCII_UPPERCASE_LETTERS.test('a')).isFalse(); + assertThat(CharacterPredicates.ASCII_UPPERCASE_LETTERS.test('z')).isFalse(); + } + + @Test + public void testDigits() { + assertThat(CharacterPredicates.DIGITS.test('0')).isTrue(); + assertThat(CharacterPredicates.DIGITS.test('9')).isTrue(); + + assertThat(CharacterPredicates.DIGITS.test('-')).isFalse(); + assertThat(CharacterPredicates.DIGITS.test('.')).isFalse(); + assertThat(CharacterPredicates.DIGITS.test('L')).isFalse(); + } + + @Test + public void testLetters() { + assertThat(CharacterPredicates.LETTERS.test('a')).isTrue(); + assertThat(CharacterPredicates.LETTERS.test('Z')).isTrue(); + + assertThat(CharacterPredicates.LETTERS.test('1')).isFalse(); + assertThat(CharacterPredicates.LETTERS.test('?')).isFalse(); + assertThat(CharacterPredicates.LETTERS.test('@')).isFalse(); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/CompositeFormatTest.java b/sources/src/test/java/org/apache/commons/text/CompositeFormatTest.java new file mode 100644 index 0000000..ded30a0 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/CompositeFormatTest.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.text.FieldPosition; +import java.text.Format; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link CompositeFormat}. + */ +public class CompositeFormatTest { + + /** + * Ensures that the parse/format separation is correctly maintained. + */ + @Test + public void testCompositeFormat() { + + final Format parser = new Format() { + private static final long serialVersionUID = 1L; + + @Override + public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public Object parseObject(final String source, final ParsePosition pos) { + return null; // do nothing + } + }; + + final Format formatter = new Format() { + private static final long serialVersionUID = 1L; + + @Override + public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) { + return null; // do nothing + } + + @Override + public Object parseObject(final String source, final ParsePosition pos) { + throw new UnsupportedOperationException("Not implemented"); + } + }; + + final CompositeFormat composite = new CompositeFormat(parser, formatter); + + composite.parseObject("", null); + composite.format(new Object(), new StringBuffer(), null); + assertEquals(parser, composite.getParser(), "Parser get method incorrectly implemented"); + assertEquals(formatter, composite.getFormatter(), "Formatter get method incorrectly implemented"); + } + + @Test + public void testUsage() throws Exception { + final Format f1 = new SimpleDateFormat("MMddyyyy", Locale.ENGLISH); + final Format f2 = new SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH); + final CompositeFormat c = new CompositeFormat(f1, f2); + final String testString = "January 3, 2005"; + assertEquals(testString, c.format(c.parseObject("01032005"))); + assertEquals(testString, c.reformat("01032005")); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/ExtendedMessageFormatTest.java b/sources/src/test/java/org/apache/commons/text/ExtendedMessageFormatTest.java new file mode 100644 index 0000000..314dc73 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/ExtendedMessageFormatTest.java @@ -0,0 +1,564 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import java.text.DateFormat; +import java.text.FieldPosition; +import java.text.Format; +import java.text.MessageFormat; +import java.text.NumberFormat; +import java.text.ParsePosition; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link ExtendedMessageFormat}. + */ +public class ExtendedMessageFormatTest { + + /** + * {@link Format} implementation which converts to lower case. + */ + private static class LowerCaseFormat extends Format { + private static final long serialVersionUID = 1L; + + @Override + public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) { + return toAppendTo.append(((String) obj).toLowerCase(Locale.ROOT)); + } + + @Override + public Object parseObject(final String source, final ParsePosition pos) { + throw new UnsupportedOperationException(); + } + } + + /** + * {@link FormatFactory} implementation for lower case format. + */ + private static class LowerCaseFormatFactory implements FormatFactory { + private static final Format LOWER_INSTANCE = new LowerCaseFormat(); + @Override + public Format getFormat(final String name, final String arguments, final Locale locale) { + return LOWER_INSTANCE; + } + } + + /** + * Alternative ExtendedMessageFormat impl. + */ + private static class OtherExtendedMessageFormat extends ExtendedMessageFormat { + private static final long serialVersionUID = 1L; + + OtherExtendedMessageFormat(final String pattern, final Locale locale, + final Map registry) { + super(pattern, locale, registry); + } + } + + /** + * {@link FormatFactory} implementation to override date format "short" to "default". + */ + private static class OverrideShortDateFormatFactory implements FormatFactory { + @Override + public Format getFormat(final String name, final String arguments, final Locale locale) { + return !"short".equals(arguments) ? null + : locale == null ? DateFormat + .getDateInstance(DateFormat.DEFAULT) : DateFormat + .getDateInstance(DateFormat.DEFAULT, locale); + } + } + + /** + * {@link Format} implementation which converts to upper case. + */ + private static class UpperCaseFormat extends Format { + private static final long serialVersionUID = 1L; + + @Override + public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) { + return toAppendTo.append(((String) obj).toUpperCase(Locale.ROOT)); + } + + @Override + public Object parseObject(final String source, final ParsePosition pos) { + throw new UnsupportedOperationException(); + } + } + + /** + * {@link FormatFactory} implementation for upper case format. + */ + private static class UpperCaseFormatFactory implements FormatFactory { + private static final Format UPPER_INSTANCE = new UpperCaseFormat(); + @Override + public Format getFormat(final String name, final String arguments, final Locale locale) { + return UPPER_INSTANCE; + } + } + + private final Map registry = new HashMap<>(); + + /** + * Create an ExtendedMessageFormat for the specified pattern and locale and check the + * formatted output matches the expected result for the parameters. + * @param pattern string + * @param registryUnused map (currently unused) + * @param args Object[] + * @param locale Locale + */ + private void checkBuiltInFormat(final String pattern, final Map registryUnused, final Object[] args, + final Locale locale) { + final StringBuilder buffer = new StringBuilder(); + buffer.append("Pattern=["); + buffer.append(pattern); + buffer.append("], locale=["); + buffer.append(locale); + buffer.append("]"); + final MessageFormat mf = createMessageFormat(pattern, locale); + // System.out.println(buffer + ", result=[" + mf.format(args) +"]"); + ExtendedMessageFormat emf = null; + if (locale == null) { + emf = new ExtendedMessageFormat(pattern); + } else { + emf = new ExtendedMessageFormat(pattern, locale); + } + assertEquals(mf.format(args), emf.format(args), "format " + buffer.toString()); + assertEquals(mf.toPattern(), emf.toPattern(), "toPattern " + buffer.toString()); + } + +// /** +// * Test extended formats with choice format. +// * +// * NOTE: FAILING - currently sub-formats not supported +// */ +// public void testExtendedWithChoiceFormat() { +// String pattern = "Choice: {0,choice,1.0#{1,lower}|2.0#{1,upper}}"; +// ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry); +// assertPatterns(null, pattern, emf.toPattern()); +// try { +// assertEquals("one", emf.format(new Object[] {Integer.valueOf(1), "ONE"})); +// assertEquals("TWO", emf.format(new Object[] {Integer.valueOf(2), "two"})); +// } catch (IllegalArgumentException e) { +// // currently sub-formats not supported +// } +// } + +// /** +// * Test mixed extended and built-in formats with choice format. +// * +// * NOTE: FAILING - currently sub-formats not supported +// */ +// public void testExtendedAndBuiltInWithChoiceFormat() { +// String pattern = "Choice: {0,choice,1.0#{0} {1,lower} {2,number}|2.0#{0} {1,upper} {2,number,currency}}"; +// Object[] lowArgs = new Object[] {Integer.valueOf(1), "Low", Double.valueOf("1234.56")}; +// Object[] highArgs = new Object[] {Integer.valueOf(2), "High", Double.valueOf("9876.54")}; +// Locale[] availableLocales = ChoiceFormat.getAvailableLocales(); +// Locale[] testLocales = new Locale[availableLocales.length + 1]; +// testLocales[0] = null; +// System.arraycopy(availableLocales, 0, testLocales, 1, availableLocales.length); +// for (int i = 0; i < testLocales.length; i++) { +// NumberFormat nf = null; +// NumberFormat cf = null; +// ExtendedMessageFormat emf = null; +// if (testLocales[i] == null) { +// nf = NumberFormat.getNumberInstance(); +// cf = NumberFormat.getCurrencyInstance(); +// emf = new ExtendedMessageFormat(pattern, registry); +// } else { +// nf = NumberFormat.getNumberInstance(testLocales[i]); +// cf = NumberFormat.getCurrencyInstance(testLocales[i]); +// emf = new ExtendedMessageFormat(pattern, testLocales[i], registry); +// } +// assertPatterns(null, pattern, emf.toPattern()); +// try { +// String lowExpected = lowArgs[0] + " low " + nf.format(lowArgs[2]); +// String highExpected = highArgs[0] + " HIGH " + cf.format(highArgs[2]); +// assertEquals(lowExpected, emf.format(lowArgs)); +// assertEquals(highExpected, emf.format(highArgs)); +// } catch (IllegalArgumentException e) { +// // currently sub-formats not supported +// } +// } +// } + + /** + * Test a built in format for the specified Locales, plus {@code null} Locale. + * @param pattern MessageFormat pattern + * @param fmtRegistry FormatFactory registry to use + * @param args MessageFormat arguments + * @param locales to test + */ + private void checkBuiltInFormat(final String pattern, final Map fmtRegistry, final Object[] args, + final Locale[] locales) { + checkBuiltInFormat(pattern, fmtRegistry, args, (Locale) null); + for (final Locale locale : locales) { + checkBuiltInFormat(pattern, fmtRegistry, args, locale); + } + } + + /** + * Test a built in format for the specified Locales, plus {@code null} Locale. + * @param pattern MessageFormat pattern + * @param args MessageFormat arguments + * @param locales to test + */ + private void checkBuiltInFormat(final String pattern, final Object[] args, final Locale[] locales) { + checkBuiltInFormat(pattern, null, args, locales); + } + + /** + * Replace MessageFormat(String, Locale) constructor (not available until JDK 1.4). + * @param pattern string + * @param locale Locale + * @return MessageFormat + */ + private MessageFormat createMessageFormat(final String pattern, final Locale locale) { + final MessageFormat result = new MessageFormat(pattern); + if (locale != null) { + result.setLocale(locale); + result.applyPattern(pattern); + } + return result; + } + + @BeforeEach + public void setUp() { + registry.put("lower", new LowerCaseFormatFactory()); + registry.put("upper", new UpperCaseFormatFactory()); + } + + /** + * Test the built in choice format. + */ + @Test + public void testBuiltInChoiceFormat() { + final Object[] values = new Number[] {1, Double.valueOf("2.2"), Double.valueOf("1234.5")}; + String choicePattern; + final Locale[] availableLocales = NumberFormat.getAvailableLocales(); + + choicePattern = "{0,choice,1#One|2#Two|3#Many {0,number}}"; + for (final Object value : values) { + checkBuiltInFormat(value + ": " + choicePattern, new Object[] {value}, availableLocales); + } + + choicePattern = "{0,choice,1#''One''|2#\"Two\"|3#''{Many}'' {0,number}}"; + for (final Object value : values) { + checkBuiltInFormat(value + ": " + choicePattern, new Object[] {value}, availableLocales); + } + } + + /** + * Test the built in date/time formats + */ + @Test + public void testBuiltInDateTimeFormat() { + final Calendar cal = Calendar.getInstance(); + cal.set(2007, Calendar.JANUARY, 23, 18, 33, 5); + final Object[] args = {cal.getTime()}; + final Locale[] availableLocales = DateFormat.getAvailableLocales(); + + checkBuiltInFormat("1: {0,date,short}", args, availableLocales); + checkBuiltInFormat("2: {0,date,medium}", args, availableLocales); + checkBuiltInFormat("3: {0,date,long}", args, availableLocales); + checkBuiltInFormat("4: {0,date,full}", args, availableLocales); + checkBuiltInFormat("5: {0,date,d MMM yy}", args, availableLocales); + checkBuiltInFormat("6: {0,time,short}", args, availableLocales); + checkBuiltInFormat("7: {0,time,medium}", args, availableLocales); + checkBuiltInFormat("8: {0,time,long}", args, availableLocales); + checkBuiltInFormat("9: {0,time,full}", args, availableLocales); + checkBuiltInFormat("10: {0,time,HH:mm}", args, availableLocales); + checkBuiltInFormat("11: {0,date}", args, availableLocales); + checkBuiltInFormat("12: {0,time}", args, availableLocales); + } + + /** + * Test the built in number formats. + */ + @Test + public void testBuiltInNumberFormat() { + final Object[] args = {Double.valueOf("6543.21")}; + final Locale[] availableLocales = NumberFormat.getAvailableLocales(); + checkBuiltInFormat("1: {0,number}", args, availableLocales); + checkBuiltInFormat("2: {0,number,integer}", args, availableLocales); + checkBuiltInFormat("3: {0,number,currency}", args, availableLocales); + checkBuiltInFormat("4: {0,number,percent}", args, availableLocales); + checkBuiltInFormat("5: {0,number,00000.000}", args, availableLocales); + } + + /** + * Test Bug TEXT-106 - Exception while using ExtendedMessageFormat and choice format element with quote just + * before brace end + */ + @Test + public void testChoiceQuoteJustBeforeBraceEnd_TEXT_106() { + final String pattern2 = "Choice format element with quote just before brace end ''{0,choice,0#0|0<'1'}''"; + final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern2, registry); + assertEquals("Choice format element with quote just before brace end '0'", emf.format(new Object[] {0})); + assertEquals("Choice format element with quote just before brace end '1'", emf.format(new Object[] {1})); + } + + @Test + public void testCreatesExtendedMessageFormatTakingString() { + final ExtendedMessageFormat extendedMessageFormat = + new ExtendedMessageFormat("Unterminated format element at position "); + final Map map = new HashMap<>(); + final ExtendedMessageFormat extendedMessageFormatTwo = + new ExtendedMessageFormat("Unterminated format element at position ", map); + + assertEquals("Unterminated format element at position ", extendedMessageFormatTwo.toPattern()); + assertNotEquals(extendedMessageFormat, extendedMessageFormatTwo); + } + + /** + * Test Bug LANG-917 - IndexOutOfBoundsException and/or infinite loop when using a choice pattern + */ + @Test + public void testEmbeddedPatternInChoice() { + final String pattern = "Hi {0,lower}, got {1,choice,0#none|1#one|1<{1,number}}, {2,upper}!"; + final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry); + assertEquals("Hi there, got 3, GREAT!", emf.format(new Object[] {"there", 3, "great"})); + } + + /** + * Test equals() and hashcode. + */ + @Test + public void testEqualsHashcode() { + final Map fmtRegistry = + Collections.singletonMap("testfmt", new LowerCaseFormatFactory()); + final Map otherRegistry = + Collections.singletonMap("testfmt", new UpperCaseFormatFactory()); + + final String pattern = "Pattern: {0,testfmt}"; + final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, Locale.US, fmtRegistry); + + ExtendedMessageFormat other; + + // Same object + assertEquals(emf, emf, "same, equals()"); + assertEquals(emf.hashCode(), emf.hashCode(), "same, hashcode()"); + + assertNotEquals(null, emf, "null, equals"); + + // Equal Object + other = new ExtendedMessageFormat(pattern, Locale.US, fmtRegistry); + assertEquals(emf, other, "equal, equals()"); + assertEquals(emf.hashCode(), other.hashCode(), "equal, hashcode()"); + + // Different Class + other = new OtherExtendedMessageFormat(pattern, Locale.US, fmtRegistry); + assertNotEquals(emf, other, "class, equals()"); + assertEquals(emf.hashCode(), other.hashCode(), "class, hashcode()"); // same hashcode + + // Different pattern + other = new ExtendedMessageFormat("X" + pattern, Locale.US, fmtRegistry); + assertNotEquals(emf, other, "pattern, equals()"); + assertNotEquals(emf.hashCode(), other.hashCode(), "pattern, hashcode()"); + + // Different registry + other = new ExtendedMessageFormat(pattern, Locale.US, otherRegistry); + assertNotEquals(emf, other, "registry, equals()"); + assertNotEquals(emf.hashCode(), other.hashCode(), "registry, hashcode()"); + + // Different Locale + other = new ExtendedMessageFormat(pattern, Locale.FRANCE, fmtRegistry); + assertNotEquals(emf, other, "locale, equals()"); + assertEquals(emf.hashCode(), other.hashCode(), "locale, hashcode()"); // same hashcode + } + + /** + * Test Bug LANG-948 - Exception while using ExtendedMessageFormat and escaping braces + */ + @Test + public void testEscapedBraces_LANG_948() { + // message without placeholder because braces are escaped by quotes + final String pattern = "Message without placeholders '{}'"; + final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry); + assertEquals("Message without placeholders {}", emf.format(new Object[] {"DUMMY"})); + + // message with placeholder because quotes are escaped by quotes + final String pattern2 = "Message with placeholder ''{0}''"; + final ExtendedMessageFormat emf2 = new ExtendedMessageFormat(pattern2, registry); + assertEquals("Message with placeholder 'DUMMY'", emf2.format(new Object[] {"DUMMY"})); + } + + /** + * Test Bug LANG-477 - out of memory error with escaped quote + */ + @Test + public void testEscapedQuote_LANG_477() { + final String pattern = "it''s a {0,lower} 'test'!"; + final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry); + assertEquals("it's a dummy test!", emf.format(new Object[] {"DUMMY"})); + } + + /** + * Test extended and built in formats. + */ + @Test + public void testExtendedAndBuiltInFormats() { + final Calendar cal = Calendar.getInstance(); + cal.set(2007, Calendar.JANUARY, 23, 18, 33, 5); + final Object[] args = {"John Doe", cal.getTime(), Double.valueOf("12345.67")}; + final String builtinsPattern = "DOB: {1,date,short} Salary: {2,number,currency}"; + final String extendedPattern = "Name: {0,upper} "; + final String pattern = extendedPattern + builtinsPattern; + + final HashSet testLocales = new HashSet<>(Arrays.asList(DateFormat.getAvailableLocales())); + testLocales.retainAll(Arrays.asList(NumberFormat.getAvailableLocales())); + testLocales.add(null); + + for (final Locale locale : testLocales) { + final MessageFormat builtins = createMessageFormat(builtinsPattern, locale); + final String expectedPattern = extendedPattern + builtins.toPattern(); + DateFormat df = null; + NumberFormat nf = null; + ExtendedMessageFormat emf = null; + if (locale == null) { + df = DateFormat.getDateInstance(DateFormat.SHORT); + nf = NumberFormat.getCurrencyInstance(); + emf = new ExtendedMessageFormat(pattern, registry); + } else { + df = DateFormat.getDateInstance(DateFormat.SHORT, locale); + nf = NumberFormat.getCurrencyInstance(locale); + emf = new ExtendedMessageFormat(pattern, locale, registry); + } + final StringBuilder expected = new StringBuilder(); + expected.append("Name: "); + expected.append(args[0].toString().toUpperCase(Locale.ROOT)); + expected.append(" DOB: "); + expected.append(df.format(args[1])); + expected.append(" Salary: "); + expected.append(nf.format(args[2])); + assertEquals(expectedPattern, emf.toPattern(), "pattern comparison for locale " + locale); + assertEquals(expected.toString(), emf.format(args), String.valueOf(locale)); + } + } + + /** + * Test extended formats. + */ + @Test + public void testExtendedFormats() { + final String pattern = "Lower: {0,lower} Upper: {1,upper}"; + final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry); + assertEquals(pattern, emf.toPattern(), "TOPATTERN"); + assertEquals("Lower: foo Upper: BAR", emf.format(new Object[] {"foo", "bar"})); + assertEquals("Lower: foo Upper: BAR", emf.format(new Object[] {"Foo", "Bar"})); + assertEquals("Lower: foo Upper: BAR", emf.format(new Object[] {"FOO", "BAR"})); + assertEquals("Lower: foo Upper: BAR", emf.format(new Object[] {"FOO", "bar"})); + assertEquals("Lower: foo Upper: BAR", emf.format(new Object[] {"foo", "BAR"})); + } + + @Test + public void testFailsToCreateExtendedMessageFormatTakingTwoArgumentsThrowsIllegalArgumentExceptionFive() { + assertThatIllegalArgumentException().isThrownBy(() -> new ExtendedMessageFormat("j/[_D9{0,\"&'+0o", new HashMap())); + } + + @Test + public void testFailsToCreateExtendedMessageFormatTakingTwoArgumentsThrowsIllegalArgumentExceptionFour() { + assertThatIllegalArgumentException().isThrownBy(() -> new ExtendedMessageFormat("RD,nXhM{}{", new HashMap())); + } + + @Test + public void testFailsToCreateExtendedMessageFormatTakingTwoArgumentsThrowsIllegalArgumentExceptionOne() { + assertThatIllegalArgumentException().isThrownBy(() -> new ExtendedMessageFormat("agdXdkR;T1{9 ^,LzXf?", new HashMap())); + } + + @Test + public void testFailsToCreateExtendedMessageFormatTakingTwoArgumentsThrowsIllegalArgumentExceptionThree() { + assertThatIllegalArgumentException().isThrownBy(() -> new ExtendedMessageFormat("9jLh_D9{ ", new HashMap())); + } + + @Test + public void testFailsToCreateExtendedMessageFormatTakingTwoArgumentsThrowsIllegalArgumentExceptionTwo() { + assertThatIllegalArgumentException().isThrownBy(() -> new ExtendedMessageFormat("a5XdkR;T1{9 ,LzXf?", new HashMap())); + } + + @Test + public void testOverriddenBuiltinFormat() { + final Calendar cal = Calendar.getInstance(); + cal.set(2007, Calendar.JANUARY, 23); + final Object[] args = {cal.getTime()}; + final Locale[] availableLocales = DateFormat.getAvailableLocales(); + final Map dateRegistry = + Collections.singletonMap("date", new OverrideShortDateFormatFactory()); + + //check the non-overridden builtins: + checkBuiltInFormat("1: {0,date}", dateRegistry, args, availableLocales); + checkBuiltInFormat("2: {0,date,medium}", dateRegistry, args, availableLocales); + checkBuiltInFormat("3: {0,date,long}", dateRegistry, args, availableLocales); + checkBuiltInFormat("4: {0,date,full}", dateRegistry, args, availableLocales); + checkBuiltInFormat("5: {0,date,d MMM yy}", dateRegistry, args, availableLocales); + + //check the overridden format: + for (int i = -1; i < availableLocales.length; i++) { + final Locale locale = i < 0 ? null : availableLocales[i]; + final MessageFormat dateDefault = createMessageFormat("{0,date}", locale); + final String pattern = "{0,date,short}"; + final ExtendedMessageFormat dateShort = new ExtendedMessageFormat(pattern, locale, dateRegistry); + assertEquals(dateDefault.format(args), dateShort.format(args), "overridden date,short format"); + assertEquals(pattern, dateShort.toPattern(), "overridden date,short pattern"); + } + } + + + @Test + public void testSetFormatByArgumentIndexIsUnsupported() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + final ExtendedMessageFormat emf = new ExtendedMessageFormat(""); + emf.setFormatByArgumentIndex(0, new LowerCaseFormat()); + }); + } + @Test + public void testSetFormatIsUnsupported() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + final ExtendedMessageFormat emf = new ExtendedMessageFormat(""); + emf.setFormat(0, new LowerCaseFormat()); + }); + } + @Test + public void testSetFormatsByArgumentIndex() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + final ExtendedMessageFormat emf = new ExtendedMessageFormat(""); + emf.setFormatsByArgumentIndex(new Format[] {new LowerCaseFormat(), new UpperCaseFormat()}); + }); + } + + @Test + public void testSetFormatsIsUnsupported() { + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> { + final ExtendedMessageFormat emf = new ExtendedMessageFormat(""); + emf.setFormats(new Format[] {new LowerCaseFormat(), new UpperCaseFormat()}); + }); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/FormattableUtilsTest.java b/sources/src/test/java/org/apache/commons/text/FormattableUtilsTest.java new file mode 100644 index 0000000..5111616 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/FormattableUtilsTest.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import static java.util.FormattableFlags.LEFT_JUSTIFY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import java.util.Formattable; +import java.util.Formatter; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests {@link FormattableUtils}. + */ +public class FormattableUtilsTest { + + static class SimplestFormattable implements Formattable { + private final String text; + + SimplestFormattable(final String text) { + this.text = text; + } + + @Override + public void formatTo(final Formatter formatter, final int flags, final int width, final int precision) { + formatter.format(text); + } + } + + private Formatter createFormatter() { + return new Formatter(); + } + + @Test + public void testAlternatePadCharacter() { + final char pad = '_'; + assertThat(FormattableUtils.append("foo", createFormatter(), 0, -1, -1, pad).toString()).isEqualTo("foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, -1, 2, pad).toString()).isEqualTo("fo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 4, -1, pad).toString()).isEqualTo("_foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 6, -1, pad).toString()).isEqualTo("___foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 3, 2, pad).toString()).isEqualTo("_fo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 5, 2, pad).toString()).isEqualTo("___fo"); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 4, -1, pad).toString()) + .isEqualTo("foo_"); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 6, -1, pad).toString()) + .isEqualTo("foo___"); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 3, 2, pad).toString()) + .isEqualTo("fo_"); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 5, 2, pad).toString()) + .isEqualTo("fo___"); + } + + @Test + public void testAlternatePadCharAndEllipsis() { + assertThat(FormattableUtils.append("foo", createFormatter(), 0, -1, -1, '_', "*").toString()).isEqualTo("foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, -1, 2, '_', "*").toString()).isEqualTo("f*"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 4, -1, '_', "*").toString()).isEqualTo("_foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 6, -1, '_', "*").toString()) + .isEqualTo("___foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 3, 2, '_', "*").toString()).isEqualTo("_f*"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 5, 2, '_', "*").toString()).isEqualTo("___f*"); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 4, -1, '_', "*").toString()) + .isEqualTo("foo_"); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 6, -1, '_', "*").toString()) + .isEqualTo("foo___"); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 3, 2, '_', "*").toString()) + .isEqualTo("f*_"); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 5, 2, '_', "*").toString()) + .isEqualTo("f*___"); + + assertThat(FormattableUtils.append("foo", createFormatter(), 0, -1, -1, '_', "+*").toString()).isEqualTo("foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, -1, 2, '_', "+*").toString()).isEqualTo("+*"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 4, -1, '_', "+*").toString()).isEqualTo("_foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 6, -1, '_', "+*").toString()) + .isEqualTo("___foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 3, 2, '_', "+*").toString()).isEqualTo("_+*"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 5, 2, '_', "+*").toString()).isEqualTo("___+*"); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 4, -1, '_', "+*").toString()) + .isEqualTo("foo_"); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 6, -1, '_', "+*").toString()) + .isEqualTo("foo___"); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 3, 2, '_', "+*").toString()) + .isEqualTo("+*_"); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 5, 2, '_', "+*").toString()) + .isEqualTo("+*___"); + } + + @Test + public void testAppendWithNullFormatterAndIntsThrowsNullPointerException() { + assertThatNullPointerException().isThrownBy(() -> FormattableUtils.append("", null, 0, 0, 0, '}')); + } + + @Test + public void testDefaultAppend() { + assertThat(FormattableUtils.append("foo", createFormatter(), 0, -1, -1).toString()).isEqualTo("foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, -1, 2).toString()).isEqualTo("fo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 4, -1).toString()).isEqualTo(" foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 6, -1).toString()).isEqualTo(" foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 3, 2).toString()).isEqualTo(" fo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 5, 2).toString()).isEqualTo(" fo"); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 4, -1).toString()).isEqualTo("foo "); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 6, -1).toString()) + .isEqualTo("foo "); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 3, 2).toString()).isEqualTo("fo "); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 5, 2).toString()).isEqualTo("fo "); + } + + @Test + public void testEllipsis() { + assertThat(FormattableUtils.append("foo", createFormatter(), 0, -1, -1, "*").toString()).isEqualTo("foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, -1, 2, "*").toString()).isEqualTo("f*"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 4, -1, "*").toString()).isEqualTo(" foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 6, -1, "*").toString()).isEqualTo(" foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 3, 2, "*").toString()).isEqualTo(" f*"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 5, 2, "*").toString()).isEqualTo(" f*"); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 4, -1, "*").toString()) + .isEqualTo("foo "); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 6, -1, "*").toString()) + .isEqualTo("foo "); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 3, 2, "*").toString()) + .isEqualTo("f* "); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 5, 2, "*").toString()) + .isEqualTo("f* "); + + assertThat(FormattableUtils.append("foo", createFormatter(), 0, -1, -1, "+*").toString()).isEqualTo("foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, -1, 2, "+*").toString()).isEqualTo("+*"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 4, -1, "+*").toString()).isEqualTo(" foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 6, -1, "+*").toString()).isEqualTo(" foo"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 3, 2, "+*").toString()).isEqualTo(" +*"); + assertThat(FormattableUtils.append("foo", createFormatter(), 0, 5, 2, "+*").toString()).isEqualTo(" +*"); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 4, -1, "+*").toString()) + .isEqualTo("foo "); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 6, -1, "+*").toString()) + .isEqualTo("foo "); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 3, 2, "+*").toString()) + .isEqualTo("+* "); + assertThat(FormattableUtils.append("foo", createFormatter(), LEFT_JUSTIFY, 5, 2, "+*").toString()) + .isEqualTo("+* "); + } + + @Test + public void testIllegalEllipsis() { + assertThatIllegalArgumentException().isThrownBy(() -> FormattableUtils.append("foo", createFormatter(), 0, -1, 1, "xx")); + } + + @Test + public void testIllegalEllipsisWith7Args() { + final String ellipsis = "xxxx"; + final int precisionLessThanEllipsisLength = ellipsis.length() - 1; + assertThatIllegalArgumentException().isThrownBy(() -> FormattableUtils.append("foo", createFormatter(), 0, 0, + precisionLessThanEllipsisLength, '}', ellipsis)); + } + + @Test + public void testPublicConstructorExists() { + new FormattableUtils(); + } + + @Test + public void testSimplestFormat() { + final Formattable formattable = new SimplestFormattable("foo"); + + assertThat(FormattableUtils.toString(formattable)).isEqualTo("foo"); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/RandomStringGeneratorTest.java b/sources/src/test/java/org/apache/commons/text/RandomStringGeneratorTest.java new file mode 100644 index 0000000..d3c8b2b --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/RandomStringGeneratorTest.java @@ -0,0 +1,279 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.assertj.core.api.Assertions.fail; + +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link RandomStringGenerator} + */ +public class RandomStringGeneratorTest { + + private static final CharacterPredicate A_FILTER = codePoint -> codePoint == 'a'; + + private static final CharacterPredicate B_FILTER = codePoint -> codePoint == 'b'; + + private static int codePointLength(final String s) { + return s.codePointCount(0, s.length()); + } + + @Test + public void testBadMaximumCodePoint() { + assertThatIllegalArgumentException().isThrownBy(() -> new RandomStringGenerator.Builder().withinRange(0, Character.MAX_CODE_POINT + 1)); + } + + @Test + public void testBadMinAndMax() { + assertThatIllegalArgumentException().isThrownBy(() -> new RandomStringGenerator.Builder().withinRange(2, 1)); + } + + @Test + public void testBadMinimumCodePoint() { + assertThatIllegalArgumentException().isThrownBy(() -> new RandomStringGenerator.Builder().withinRange(-1, 1)); + } + + @Test + public void testChangeOfFilter() { + final RandomStringGenerator.Builder builder = new RandomStringGenerator.Builder().withinRange('a', 'z') + .filteredBy(A_FILTER); + final String str = builder.filteredBy(B_FILTER).build().generate(100); + + for (final char c : str.toCharArray()) { + assertThat(c == 'b').isTrue(); + } + } + + @Test + public void testGenerateMinMaxLength() { + final int minLength = 0; + final int maxLength = 3; + final RandomStringGenerator generator = new RandomStringGenerator.Builder().build(); + final String str = generator.generate(minLength, maxLength); + assertThat(codePointLength(str)).isBetween(0, 3); + } + + @Test + public void testGenerateMinMaxLengthInvalidLength() { + assertThatIllegalArgumentException().isThrownBy(() -> { + final RandomStringGenerator generator = new RandomStringGenerator.Builder().build(); + generator.generate(-1, 0); + }); + } + + @Test + public void testGenerateMinMaxLengthMinGreaterThanMax() { + assertThatIllegalArgumentException().isThrownBy(() -> { + final RandomStringGenerator generator = new RandomStringGenerator.Builder().build(); + generator.generate(1, 0); + }); + } + + @Test + public void testGenerateTakingIntThrowsNullPointerException() { + assertThatNullPointerException().isThrownBy(() -> { + final RandomStringGenerator.Builder randomStringGeneratorBuilder = new RandomStringGenerator.Builder(); + final CharacterPredicate[] characterPredicateArray = new CharacterPredicate[2]; + randomStringGeneratorBuilder.filteredBy(characterPredicateArray); + final RandomStringGenerator randomStringGenerator = randomStringGeneratorBuilder.build(); + + randomStringGenerator.generate(18); + }); + } + + @Test + public void testInvalidLength() { + assertThatIllegalArgumentException().isThrownBy(() -> { + final RandomStringGenerator generator = new RandomStringGenerator.Builder().build(); + generator.generate(-1); + }); + } + + @Test + public void testMultipleFilters() { + final String str = new RandomStringGenerator.Builder().withinRange('a', 'd') + .filteredBy(A_FILTER, B_FILTER).build().generate(5000); + + boolean aFound = false; + boolean bFound = false; + + for (final char c : str.toCharArray()) { + if (c == 'a') { + aFound = true; + } else if (c == 'b') { + bFound = true; + } else { + fail("Invalid character"); + } + } + + assertThat(aFound && bFound).isTrue(); + } + + @Test + public void testNoLoneSurrogates() { + final int length = 5000; + final String str = new RandomStringGenerator.Builder().build().generate(length); + + char lastChar = str.charAt(0); + for (int i = 1; i < str.length(); i++) { + final char c = str.charAt(i); + + if (Character.isLowSurrogate(c)) { + assertThat(Character.isHighSurrogate(lastChar)).isTrue(); + } + + if (Character.isHighSurrogate(lastChar)) { + assertThat(Character.isLowSurrogate(c)).isTrue(); + } + + if (Character.isHighSurrogate(c)) { + // test this isn't the last character in the string + assertThat(i + 1 < str.length()).isTrue(); + } + + lastChar = c; + } + } + + @Test + public void testNoPrivateCharacters() { + final int startOfPrivateBMPChars = 0xE000; + + // Request a string in an area of the Basic Multilingual Plane that is + // largely occupied by private characters + final String str = new RandomStringGenerator.Builder().withinRange(startOfPrivateBMPChars, + Character.MIN_SUPPLEMENTARY_CODE_POINT - 1).build().generate(5000); + + int i = 0; + do { + final int codePoint = str.codePointAt(i); + assertThat(Character.getType(codePoint) == Character.PRIVATE_USE).isFalse(); + i += Character.charCount(codePoint); + } while (i < str.length()); + } + + @Test + public void testRemoveFilters() { + final RandomStringGenerator.Builder builder = new RandomStringGenerator.Builder().withinRange('a', 'z') + .filteredBy(A_FILTER); + + builder.filteredBy(); + + final String str = builder.build().generate(100); + for (final char c : str.toCharArray()) { + if (c != 'a') { + // filter was successfully removed + return; + } + } + + fail("Filter appears to have remained in place"); + } + + @Test + public void testSelectFromCharArray() { + final String str = "abc"; + final char[] charArray = str.toCharArray(); + final RandomStringGenerator generator = new RandomStringGenerator.Builder().selectFrom(charArray).build(); + + final String randomText = generator.generate(5); + + for (final char c : randomText.toCharArray()) { + assertThat(str.indexOf(c) != -1).isTrue(); + } + } + + @Test + public void testSelectFromCharVarargs() { + final String str = "abc"; + final RandomStringGenerator generator = new RandomStringGenerator.Builder().selectFrom('a', 'b', 'c').build(); + final String randomText = generator.generate(5); + for (final char c : randomText.toCharArray()) { + assertThat(str.indexOf(c) != -1).isTrue(); + } + } + + @Test + public void testSetLength() { + final int length = 99; + final RandomStringGenerator generator = new RandomStringGenerator.Builder().build(); + final String str = generator.generate(length); + assertThat(codePointLength(str)).isEqualTo(length); + } + + @Test + public void testUsingRandom() { + final char testChar = 'a'; + final TextRandomProvider testRandom = n -> testChar; + + final String str = new RandomStringGenerator.Builder().usingRandom(testRandom).build().generate(10); + for (final char c : str.toCharArray()) { + assertThat(c).isEqualTo(testChar); + } + } + + @Test + public void testWithinMultipleRanges() { + final int length = 5000; + final char[][] pairs = {{'a', 'z'}, {'0', '9'}}; + final RandomStringGenerator generator = new RandomStringGenerator.Builder() + .withinRange(pairs).build(); + final String str = generator.generate(length); + + int minimumCodePoint = 0, maximumCodePoint = 0; + + for (final char[] pair : pairs) { + minimumCodePoint = Math.min(minimumCodePoint, pair[0]); + maximumCodePoint = Math.max(maximumCodePoint, pair[1]); + } + + int i = 0; + do { + final int codePoint = str.codePointAt(i); + assertThat(codePoint >= minimumCodePoint && codePoint <= maximumCodePoint).isTrue(); + i += Character.charCount(codePoint); + } while (i < str.length()); + } + + @Test + public void testWithinRange() { + final int length = 5000; + final int minimumCodePoint = 'a'; + final int maximumCodePoint = 'z'; + final RandomStringGenerator generator = new RandomStringGenerator.Builder() + .withinRange(minimumCodePoint, maximumCodePoint).build(); + final String str = generator.generate(length); + + int i = 0; + do { + final int codePoint = str.codePointAt(i); + assertThat(codePoint >= minimumCodePoint && codePoint <= maximumCodePoint).isTrue(); + i += Character.charCount(codePoint); + } while (i < str.length()); + } + + @Test + public void testZeroLength() { + final RandomStringGenerator generator = new RandomStringGenerator.Builder().build(); + assertThat(generator.generate(0)).isEqualTo(""); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/StrBuilderAppendInsertTest.java b/sources/src/test/java/org/apache/commons/text/StrBuilderAppendInsertTest.java new file mode 100644 index 0000000..5652f57 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/StrBuilderAppendInsertTest.java @@ -0,0 +1,1429 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.text.DecimalFormatSymbols; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; + +import org.apache.commons.lang3.ArrayUtils; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link StrBuilder}. + * + * @deprecated This class will be removed in 2.0. + */ +@Deprecated +public class StrBuilderAppendInsertTest { + + /** The system line separator. */ + private static final String SEP = System.lineSeparator(); + + /** Test subclass of Object, with a toString method. */ + private static final Object FOO = new Object() { + @Override + public String toString() { + return "foo"; + } + }; + + @Test + public void testAppend_Boolean() { + final StrBuilder sb = new StrBuilder(); + sb.append(true); + assertThat(sb.toString()).isEqualTo("true"); + + sb.append(false); + assertThat(sb.toString()).isEqualTo("truefalse"); + + sb.append('!'); + assertThat(sb.toString()).isEqualTo("truefalse!"); + } + + @Test + public void testAppend_CharArray() { + StrBuilder sb = new StrBuilder(); + sb.setNullText("NULL").append((char[]) null); + assertThat(sb.toString()).isEqualTo("NULL"); + + sb = new StrBuilder(); + sb.append(ArrayUtils.EMPTY_CHAR_ARRAY); + assertThat(sb.toString()).isEqualTo(""); + + sb.append(new char[]{'f', 'o', 'o'}); + assertThat(sb.toString()).isEqualTo("foo"); + } + + @Test + public void testAppend_CharArray_int_int() { + StrBuilder sb = new StrBuilder(); + sb.setNullText("NULL").append((char[]) null, 0, 1); + assertThat(sb.toString()).isEqualTo("NULL"); + + sb = new StrBuilder(); + sb.append(new char[]{'f', 'o', 'o'}, 0, 3); + assertThat(sb.toString()).isEqualTo("foo"); + + try { + sb.append(new char[]{'b', 'a', 'r'}, -1, 1); + fail("append(char[], -1,) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new char[]{'b', 'a', 'r'}, 3, 1); + fail("append(char[], 3,) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new char[]{'b', 'a', 'r'}, 1, -1); + fail("append(char[],, -1) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new char[]{'b', 'a', 'r'}, 1, 3); + fail("append(char[], 1, 3) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new char[]{'b', 'a', 'r'}, -1, 3); + fail("append(char[], -1, 3) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new char[]{'b', 'a', 'r'}, 4, 0); + fail("append(char[], 4, 0) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + sb.append(new char[]{'b', 'a', 'r'}, 3, 0); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new char[]{'a', 'b', 'c', 'b', 'a', 'r', 'd', 'e', 'f'}, 3, 3); + assertThat(sb.toString()).isEqualTo("foobar"); + } + + @Test + public void testAppend_FormattedString() { + StrBuilder sb; + + sb = new StrBuilder(); + sb.append("Hi", (Object[]) null); + assertThat(sb.toString()).isEqualTo("Hi"); + + sb = new StrBuilder(); + sb.append("Hi", "Alice"); + assertThat(sb.toString()).isEqualTo("Hi"); + + sb = new StrBuilder(); + sb.append("Hi %s", "Alice"); + assertThat(sb.toString()).isEqualTo("Hi Alice"); + + sb = new StrBuilder(); + sb.append("Hi %s %,d", "Alice", 5000); + // group separator depends on system locale + final char groupingSeparator = DecimalFormatSymbols.getInstance().getGroupingSeparator(); + final String expected = "Hi Alice 5" + groupingSeparator + "000"; + assertThat(sb.toString()).isEqualTo(expected); + } + + @Test + public void testAppend_Object() { + final StrBuilder sb = new StrBuilder(); + sb.appendNull(); + assertThat(sb.toString()).isEqualTo(""); + + sb.append((Object) null); + assertThat(sb.toString()).isEqualTo(""); + + sb.append(FOO); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append((StringBuffer) null); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new StringBuffer("baz")); + assertThat(sb.toString()).isEqualTo("foobaz"); + + sb.append(new StrBuilder("yes")); + assertThat(sb.toString()).isEqualTo("foobazyes"); + + sb.append((CharSequence) "Seq"); + assertThat(sb.toString()).isEqualTo("foobazyesSeq"); + + sb.append(new StringBuilder("bld")); // Check it supports StringBuilder + assertThat(sb.toString()).isEqualTo("foobazyesSeqbld"); + } + + @Test + public void testAppend_PrimitiveNumber() { + final StrBuilder sb = new StrBuilder(); + sb.append(0); + assertThat(sb.toString()).isEqualTo("0"); + + sb.append(1L); + assertThat(sb.toString()).isEqualTo("01"); + + sb.append(2.3f); + assertThat(sb.toString()).isEqualTo("012.3"); + + sb.append(4.5d); + assertThat(sb.toString()).isEqualTo("012.34.5"); + } + + @Test + public void testAppend_StrBuilder() { + StrBuilder sb = new StrBuilder(); + sb.setNullText("NULL").append((StrBuilder) null); + assertThat(sb.toString()).isEqualTo("NULL"); + + sb = new StrBuilder(); + sb.append(new StrBuilder("foo")); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new StrBuilder("")); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new StrBuilder("bar")); + assertThat(sb.toString()).isEqualTo("foobar"); + } + + @Test + public void testAppend_StrBuilder_int_int() { + StrBuilder sb = new StrBuilder(); + sb.setNullText("NULL").append((StrBuilder) null, 0, 1); + assertThat(sb.toString()).isEqualTo("NULL"); + + sb = new StrBuilder(); + sb.append(new StrBuilder("foo"), 0, 3); + assertThat(sb.toString()).isEqualTo("foo"); + + try { + sb.append(new StrBuilder("bar"), -1, 1); + fail("append(char[], -1,) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new StrBuilder("bar"), 3, 1); + fail("append(char[], 3,) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new StrBuilder("bar"), 1, -1); + fail("append(char[],, -1) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new StrBuilder("bar"), 1, 3); + fail("append(char[], 1, 3) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new StrBuilder("bar"), -1, 3); + fail("append(char[], -1, 3) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new StrBuilder("bar"), 4, 0); + fail("append(char[], 4, 0) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + sb.append(new StrBuilder("bar"), 3, 0); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new StrBuilder("abcbardef"), 3, 3); + assertThat(sb.toString()).isEqualTo("foobar"); + } + + @Test + public void testAppend_String() { + StrBuilder sb = new StrBuilder(); + sb.setNullText("NULL").append((String) null); + assertThat(sb.toString()).isEqualTo("NULL"); + + sb = new StrBuilder(); + sb.append("foo"); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(""); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append("bar"); + assertThat(sb.toString()).isEqualTo("foobar"); + } + + @Test + public void testAppend_String_int_int() { + StrBuilder sb = new StrBuilder(); + sb.setNullText("NULL").append((String) null, 0, 1); + assertThat(sb.toString()).isEqualTo("NULL"); + + sb = new StrBuilder(); + sb.append("foo", 0, 3); + assertThat(sb.toString()).isEqualTo("foo"); + + try { + sb.append("bar", -1, 1); + fail("append(char[], -1,) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append("bar", 3, 1); + fail("append(char[], 3,) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append("bar", 1, -1); + fail("append(char[],, -1) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append("bar", 1, 3); + fail("append(char[], 1, 3) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append("bar", -1, 3); + fail("append(char[], -1, 3) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append("bar", 4, 0); + fail("append(char[], 4, 0) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + sb.append("bar", 3, 0); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append("abcbardef", 3, 3); + assertThat(sb.toString()).isEqualTo("foobar"); + + sb.append((CharSequence) "abcbardef", 4, 3); + assertThat(sb.toString()).isEqualTo("foobarard"); + } + + @Test + public void testAppend_StringBuffer() { + StrBuilder sb = new StrBuilder(); + sb.setNullText("NULL").append((StringBuffer) null); + assertThat(sb.toString()).isEqualTo("NULL"); + + sb = new StrBuilder(); + sb.append(new StringBuffer("foo")); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new StringBuffer("")); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new StringBuffer("bar")); + assertThat(sb.toString()).isEqualTo("foobar"); + } + + @Test + public void testAppend_StringBuffer_int_int() { + StrBuilder sb = new StrBuilder(); + sb.setNullText("NULL").append((StringBuffer) null, 0, 1); + assertThat(sb.toString()).isEqualTo("NULL"); + + sb = new StrBuilder(); + sb.append(new StringBuffer("foo"), 0, 3); + assertThat(sb.toString()).isEqualTo("foo"); + + try { + sb.append(new StringBuffer("bar"), -1, 1); + fail("append(char[], -1,) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new StringBuffer("bar"), 3, 1); + fail("append(char[], 3,) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new StringBuffer("bar"), 1, -1); + fail("append(char[],, -1) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new StringBuffer("bar"), 1, 3); + fail("append(char[], 1, 3) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new StringBuffer("bar"), -1, 3); + fail("append(char[], -1, 3) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new StringBuffer("bar"), 4, 0); + fail("append(char[], 4, 0) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + sb.append(new StringBuffer("bar"), 3, 0); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new StringBuffer("abcbardef"), 3, 3); + assertThat(sb.toString()).isEqualTo("foobar"); + } + + @Test + public void testAppend_StringBuilder() { + StrBuilder sb = new StrBuilder(); + sb.setNullText("NULL").append((String) null); + assertThat(sb.toString()).isEqualTo("NULL"); + + sb = new StrBuilder(); + sb.append(new StringBuilder("foo")); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new StringBuilder("")); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new StringBuilder("bar")); + assertThat(sb.toString()).isEqualTo("foobar"); + } + + @Test + public void testAppend_StringBuilder_int_int() { + StrBuilder sb = new StrBuilder(); + sb.setNullText("NULL").append((String) null, 0, 1); + assertThat(sb.toString()).isEqualTo("NULL"); + + sb = new StrBuilder(); + sb.append(new StringBuilder("foo"), 0, 3); + assertThat(sb.toString()).isEqualTo("foo"); + + try { + sb.append(new StringBuilder("bar"), -1, 1); + fail("append(StringBuilder, -1,) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new StringBuilder("bar"), 3, 1); + fail("append(StringBuilder, 3,) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new StringBuilder("bar"), 1, -1); + fail("append(StringBuilder,, -1) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new StringBuilder("bar"), 1, 3); + fail("append(StringBuilder, 1, 3) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new StringBuilder("bar"), -1, 3); + fail("append(StringBuilder, -1, 3) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.append(new StringBuilder("bar"), 4, 0); + fail("append(StringBuilder, 4, 0) expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + sb.append(new StringBuilder("bar"), 3, 0); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new StringBuilder("abcbardef"), 3, 3); + assertThat(sb.toString()).isEqualTo("foobar"); + + sb.append(new StringBuilder("abcbardef"), 4, 3); + assertThat(sb.toString()).isEqualTo("foobarard"); + } + + @Test + public void testAppendAll_Array() { + final StrBuilder sb = new StrBuilder(); + sb.appendAll((Object[]) null); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendAll(); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendAll("foo", "bar", "baz"); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + + sb.clear(); + sb.appendAll("foo", "bar", "baz"); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + } + + @Test + public void testAppendAll_Collection() { + final StrBuilder sb = new StrBuilder(); + sb.appendAll((Collection) null); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendAll(Collections.EMPTY_LIST); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendAll(Arrays.asList("foo", "bar", "baz")); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + } + + @Test + public void testAppendAll_Iterator() { + final StrBuilder sb = new StrBuilder(); + sb.appendAll((Iterator) null); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendAll(Collections.EMPTY_LIST.iterator()); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendAll(Arrays.asList("foo", "bar", "baz").iterator()); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + } + + @Test + public void testAppendFixedWidthPadLeft() { + final StrBuilder sb = new StrBuilder(); + sb.appendFixedWidthPadLeft("foo", -1, '-'); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendFixedWidthPadLeft("foo", 0, '-'); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendFixedWidthPadLeft("foo", 1, '-'); + assertThat(sb.toString()).isEqualTo("o"); + + sb.clear(); + sb.appendFixedWidthPadLeft("foo", 2, '-'); + assertThat(sb.toString()).isEqualTo("oo"); + + sb.clear(); + sb.appendFixedWidthPadLeft("foo", 3, '-'); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.clear(); + sb.appendFixedWidthPadLeft("foo", 4, '-'); + assertThat(sb.toString()).isEqualTo("-foo"); + + sb.clear(); + sb.appendFixedWidthPadLeft("foo", 10, '-'); + assertThat(sb.length()).isEqualTo(10); + // 1234567890 + assertThat(sb.toString()).isEqualTo("-------foo"); + + sb.clear(); + sb.setNullText("null"); + sb.appendFixedWidthPadLeft(null, 5, '-'); + assertThat(sb.toString()).isEqualTo("-null"); + } + + @Test + public void testAppendFixedWidthPadLeft_int() { + final StrBuilder sb = new StrBuilder(); + sb.appendFixedWidthPadLeft(123, -1, '-'); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendFixedWidthPadLeft(123, 0, '-'); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendFixedWidthPadLeft(123, 1, '-'); + assertThat(sb.toString()).isEqualTo("3"); + + sb.clear(); + sb.appendFixedWidthPadLeft(123, 2, '-'); + assertThat(sb.toString()).isEqualTo("23"); + + sb.clear(); + sb.appendFixedWidthPadLeft(123, 3, '-'); + assertThat(sb.toString()).isEqualTo("123"); + + sb.clear(); + sb.appendFixedWidthPadLeft(123, 4, '-'); + assertThat(sb.toString()).isEqualTo("-123"); + + sb.clear(); + sb.appendFixedWidthPadLeft(123, 10, '-'); + assertThat(sb.length()).isEqualTo(10); + // 1234567890 + assertThat(sb.toString()).isEqualTo("-------123"); + } + + @Test + public void testAppendFixedWidthPadRight() { + final StrBuilder sb = new StrBuilder(); + sb.appendFixedWidthPadRight("foo", -1, '-'); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendFixedWidthPadRight("foo", 0, '-'); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendFixedWidthPadRight("foo", 1, '-'); + assertThat(sb.toString()).isEqualTo("f"); + + sb.clear(); + sb.appendFixedWidthPadRight("foo", 2, '-'); + assertThat(sb.toString()).isEqualTo("fo"); + + sb.clear(); + sb.appendFixedWidthPadRight("foo", 3, '-'); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.clear(); + sb.appendFixedWidthPadRight("foo", 4, '-'); + assertThat(sb.toString()).isEqualTo("foo-"); + + sb.clear(); + sb.appendFixedWidthPadRight("foo", 10, '-'); + assertThat(sb.length()).isEqualTo(10); + // 1234567890 + assertThat(sb.toString()).isEqualTo("foo-------"); + + sb.clear(); + sb.setNullText("null"); + sb.appendFixedWidthPadRight(null, 5, '-'); + assertThat(sb.toString()).isEqualTo("null-"); + } + + @Test + public void testAppendFixedWidthPadRight_int() { + final StrBuilder sb = new StrBuilder(); + sb.appendFixedWidthPadRight(123, -1, '-'); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendFixedWidthPadRight(123, 0, '-'); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendFixedWidthPadRight(123, 1, '-'); + assertThat(sb.toString()).isEqualTo("1"); + + sb.clear(); + sb.appendFixedWidthPadRight(123, 2, '-'); + assertThat(sb.toString()).isEqualTo("12"); + + sb.clear(); + sb.appendFixedWidthPadRight(123, 3, '-'); + assertThat(sb.toString()).isEqualTo("123"); + + sb.clear(); + sb.appendFixedWidthPadRight(123, 4, '-'); + assertThat(sb.toString()).isEqualTo("123-"); + + sb.clear(); + sb.appendFixedWidthPadRight(123, 10, '-'); + assertThat(sb.length()).isEqualTo(10); + // 1234567890 + assertThat(sb.toString()).isEqualTo("123-------"); + } + + @Test + public void testAppendln_Boolean() { + final StrBuilder sb = new StrBuilder(); + sb.appendln(true); + assertThat(sb.toString()).isEqualTo("true" + SEP); + + sb.clear(); + sb.appendln(false); + assertThat(sb.toString()).isEqualTo("false" + SEP); + } + + @Test + public void testAppendln_CharArray() { + final int[] count = new int[2]; + final StrBuilder sb = new StrBuilder() { + private static final long serialVersionUID = 1L; + + @Override + public StrBuilder append(final char[] str) { + count[0]++; + return super.append(str); + } + @Override + public StrBuilder appendNewLine() { + count[1]++; + return super.appendNewLine(); + } + }; + sb.appendln("foo".toCharArray()); + assertThat(sb.toString()).isEqualTo("foo" + SEP); + assertThat(count[0]).isEqualTo(1); + assertThat(count[1]).isEqualTo(1); + } + + @Test + public void testAppendln_CharArray_int_int() { + final int[] count = new int[2]; + final StrBuilder sb = new StrBuilder() { + private static final long serialVersionUID = 1L; + + @Override + public StrBuilder append(final char[] str, final int startIndex, final int length) { + count[0]++; + return super.append(str, startIndex, length); + } + @Override + public StrBuilder appendNewLine() { + count[1]++; + return super.appendNewLine(); + } + }; + sb.appendln("foo".toCharArray(), 0, 3); + assertThat(sb.toString()).isEqualTo("foo" + SEP); + assertThat(count[0]).isEqualTo(1); + assertThat(count[1]).isEqualTo(1); + } + + @Test + public void testAppendln_FormattedString() { + final int[] count = new int[2]; + final StrBuilder sb = new StrBuilder() { + private static final long serialVersionUID = 1L; + + @Override + public StrBuilder append(final String str) { + count[0]++; + return super.append(str); + } + @Override + public StrBuilder appendNewLine() { + count[1]++; + return super.appendNewLine(); + } + }; + sb.appendln("Hello %s", "Alice"); + assertThat(sb.toString()).isEqualTo("Hello Alice" + SEP); + assertThat(count[0]).isEqualTo(2); // appendNewLine() calls append(String) + assertThat(count[1]).isEqualTo(1); + } + + @Test + public void testAppendln_Object() { + final StrBuilder sb = new StrBuilder(); + sb.appendln((Object) null); + assertThat(sb.toString()).isEqualTo("" + SEP); + + sb.appendln(FOO); + assertThat(sb.toString()).isEqualTo(SEP + "foo" + SEP); + + sb.appendln(Integer.valueOf(6)); + assertThat(sb.toString()).isEqualTo(SEP + "foo" + SEP + "6" + SEP); + } + + @Test + public void testAppendln_PrimitiveNumber() { + final StrBuilder sb = new StrBuilder(); + sb.appendln(0); + assertThat(sb.toString()).isEqualTo("0" + SEP); + + sb.clear(); + sb.appendln(1L); + assertThat(sb.toString()).isEqualTo("1" + SEP); + + sb.clear(); + sb.appendln(2.3f); + assertThat(sb.toString()).isEqualTo("2.3" + SEP); + + sb.clear(); + sb.appendln(4.5d); + assertThat(sb.toString()).isEqualTo("4.5" + SEP); + } + + @Test + public void testAppendln_StrBuilder() { + final int[] count = new int[2]; + final StrBuilder sb = new StrBuilder() { + private static final long serialVersionUID = 1L; + + @Override + public StrBuilder append(final StrBuilder str) { + count[0]++; + return super.append(str); + } + @Override + public StrBuilder appendNewLine() { + count[1]++; + return super.appendNewLine(); + } + }; + sb.appendln(new StrBuilder("foo")); + assertThat(sb.toString()).isEqualTo("foo" + SEP); + assertThat(count[0]).isEqualTo(1); + assertThat(count[1]).isEqualTo(1); + } + + @Test + public void testAppendln_StrBuilder_int_int() { + final int[] count = new int[2]; + final StrBuilder sb = new StrBuilder() { + private static final long serialVersionUID = 1L; + + @Override + public StrBuilder append(final StrBuilder str, final int startIndex, final int length) { + count[0]++; + return super.append(str, startIndex, length); + } + @Override + public StrBuilder appendNewLine() { + count[1]++; + return super.appendNewLine(); + } + }; + sb.appendln(new StrBuilder("foo"), 0, 3); + assertThat(sb.toString()).isEqualTo("foo" + SEP); + assertThat(count[0]).isEqualTo(1); + assertThat(count[1]).isEqualTo(1); + } + + @Test + public void testAppendln_String() { + final int[] count = new int[2]; + final StrBuilder sb = new StrBuilder() { + private static final long serialVersionUID = 1L; + + @Override + public StrBuilder append(final String str) { + count[0]++; + return super.append(str); + } + @Override + public StrBuilder appendNewLine() { + count[1]++; + return super.appendNewLine(); + } + }; + sb.appendln("foo"); + assertThat(sb.toString()).isEqualTo("foo" + SEP); + assertThat(count[0]).isEqualTo(2); // appendNewLine() calls append(String) + assertThat(count[1]).isEqualTo(1); + } + + @Test + public void testAppendln_String_int_int() { + final int[] count = new int[2]; + final StrBuilder sb = new StrBuilder() { + private static final long serialVersionUID = 1L; + + @Override + public StrBuilder append(final String str, final int startIndex, final int length) { + count[0]++; + return super.append(str, startIndex, length); + } + @Override + public StrBuilder appendNewLine() { + count[1]++; + return super.appendNewLine(); + } + }; + sb.appendln("foo", 0, 3); + assertThat(sb.toString()).isEqualTo("foo" + SEP); + assertThat(count[0]).isEqualTo(1); + assertThat(count[1]).isEqualTo(1); + } + + @Test + public void testAppendln_StringBuffer() { + final int[] count = new int[2]; + final StrBuilder sb = new StrBuilder() { + private static final long serialVersionUID = 1L; + + @Override + public StrBuilder append(final StringBuffer str) { + count[0]++; + return super.append(str); + } + @Override + public StrBuilder appendNewLine() { + count[1]++; + return super.appendNewLine(); + } + }; + sb.appendln(new StringBuffer("foo")); + assertThat(sb.toString()).isEqualTo("foo" + SEP); + assertThat(count[0]).isEqualTo(1); + assertThat(count[1]).isEqualTo(1); + } + + @Test + public void testAppendln_StringBuffer_int_int() { + final int[] count = new int[2]; + final StrBuilder sb = new StrBuilder() { + private static final long serialVersionUID = 1L; + + @Override + public StrBuilder append(final StringBuffer str, final int startIndex, final int length) { + count[0]++; + return super.append(str, startIndex, length); + } + @Override + public StrBuilder appendNewLine() { + count[1]++; + return super.appendNewLine(); + } + }; + sb.appendln(new StringBuffer("foo"), 0, 3); + assertThat(sb.toString()).isEqualTo("foo" + SEP); + assertThat(count[0]).isEqualTo(1); + assertThat(count[1]).isEqualTo(1); + } + + @Test + public void testAppendln_StringBuilder() { + final int[] count = new int[2]; + final StrBuilder sb = new StrBuilder() { + private static final long serialVersionUID = 1L; + + @Override + public StrBuilder append(final StringBuilder str) { + count[0]++; + return super.append(str); + } + @Override + public StrBuilder appendNewLine() { + count[1]++; + return super.appendNewLine(); + } + }; + sb.appendln(new StringBuilder("foo")); + assertThat(sb.toString()).isEqualTo("foo" + SEP); + assertThat(count[0]).isEqualTo(1); + assertThat(count[1]).isEqualTo(1); + } + + @Test + public void testAppendln_StringBuilder_int_int() { + final int[] count = new int[2]; + final StrBuilder sb = new StrBuilder() { + private static final long serialVersionUID = 1L; + + @Override + public StrBuilder append(final StringBuilder str, final int startIndex, final int length) { + count[0]++; + return super.append(str, startIndex, length); + } + @Override + public StrBuilder appendNewLine() { + count[1]++; + return super.appendNewLine(); + } + }; + sb.appendln(new StringBuilder("foo"), 0, 3); + assertThat(sb.toString()).isEqualTo("foo" + SEP); + assertThat(count[0]).isEqualTo(1); + assertThat(count[1]).isEqualTo(1); + } + + @Test + public void testAppendNewLine() { + StrBuilder sb = new StrBuilder("---"); + sb.appendNewLine().append("+++"); + assertThat(sb.toString()).isEqualTo("---" + SEP + "+++"); + + sb = new StrBuilder("---"); + sb.setNewLineText("#").appendNewLine().setNewLineText(null).appendNewLine(); + assertThat(sb.toString()).isEqualTo("---#" + SEP); + } + + @Test + public void testAppendPadding() { + final StrBuilder sb = new StrBuilder(); + sb.append("foo"); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.appendPadding(-1, '-'); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.appendPadding(0, '-'); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.appendPadding(1, '-'); + assertThat(sb.toString()).isEqualTo("foo-"); + + sb.appendPadding(16, '-'); + assertThat(sb.length()).isEqualTo(20); + // 12345678901234567890 + assertThat(sb.toString()).isEqualTo("foo-----------------"); + } + + @Test + public void testAppendSeparator_char() { + final StrBuilder sb = new StrBuilder(); + sb.appendSeparator(','); // no effect + assertThat(sb.toString()).isEqualTo(""); + sb.append("foo"); + assertThat(sb.toString()).isEqualTo("foo"); + sb.appendSeparator(','); + assertThat(sb.toString()).isEqualTo("foo,"); + } + + @Test + public void testAppendSeparator_char_char() { + final StrBuilder sb = new StrBuilder(); + final char startSeparator = ':'; + final char standardSeparator = ','; + final String foo = "foo"; + sb.appendSeparator(standardSeparator, startSeparator); // no effect + assertThat(sb.toString()).isEqualTo(String.valueOf(startSeparator)); + sb.append(foo); + assertThat(sb.toString()).isEqualTo(String.valueOf(startSeparator) + foo); + sb.appendSeparator(standardSeparator, startSeparator); + assertThat(sb.toString()).isEqualTo(String.valueOf(startSeparator) + foo + standardSeparator); + } + + @Test + public void testAppendSeparator_char_int() { + final StrBuilder sb = new StrBuilder(); + sb.appendSeparator(',', 0); // no effect + assertThat(sb.toString()).isEqualTo(""); + sb.append("foo"); + assertThat(sb.toString()).isEqualTo("foo"); + sb.appendSeparator(',', 1); + assertThat(sb.toString()).isEqualTo("foo,"); + + sb.appendSeparator(',', -1); // no effect + assertThat(sb.toString()).isEqualTo("foo,"); + } + + @Test + public void testAppendSeparator_String() { + final StrBuilder sb = new StrBuilder(); + sb.appendSeparator(","); // no effect + assertThat(sb.toString()).isEqualTo(""); + sb.append("foo"); + assertThat(sb.toString()).isEqualTo("foo"); + sb.appendSeparator(","); + assertThat(sb.toString()).isEqualTo("foo,"); + } + + @Test + public void testAppendSeparator_String_int() { + final StrBuilder sb = new StrBuilder(); + sb.appendSeparator(",", 0); // no effect + assertThat(sb.toString()).isEqualTo(""); + sb.append("foo"); + assertThat(sb.toString()).isEqualTo("foo"); + sb.appendSeparator(",", 1); + assertThat(sb.toString()).isEqualTo("foo,"); + + sb.appendSeparator(",", -1); // no effect + assertThat(sb.toString()).isEqualTo("foo,"); + } + + @Test + public void testAppendSeparator_String_String() { + final StrBuilder sb = new StrBuilder(); + final String startSeparator = "order by "; + final String standardSeparator = ","; + final String foo = "foo"; + sb.appendSeparator(null, null); + assertThat(sb.toString()).isEqualTo(""); + sb.appendSeparator(standardSeparator, null); + assertThat(sb.toString()).isEqualTo(""); + sb.appendSeparator(standardSeparator, startSeparator); + assertThat(sb.toString()).isEqualTo(startSeparator); + sb.appendSeparator(null, null); + assertThat(sb.toString()).isEqualTo(startSeparator); + sb.appendSeparator(null, startSeparator); + assertThat(sb.toString()).isEqualTo(startSeparator); + sb.append(foo); + assertThat(sb.toString()).isEqualTo(startSeparator + foo); + sb.appendSeparator(standardSeparator, startSeparator); + assertThat(sb.toString()).isEqualTo(startSeparator + foo + standardSeparator); + } + + @Test + public void testAppendWithNullText() { + final StrBuilder sb = new StrBuilder(); + sb.setNullText("NULL"); + assertThat(sb.toString()).isEqualTo(""); + + sb.appendNull(); + assertThat(sb.toString()).isEqualTo("NULL"); + + sb.append((Object) null); + assertThat(sb.toString()).isEqualTo("NULLNULL"); + + sb.append(FOO); + assertThat(sb.toString()).isEqualTo("NULLNULLfoo"); + + sb.append((String) null); + assertThat(sb.toString()).isEqualTo("NULLNULLfooNULL"); + + sb.append(""); + assertThat(sb.toString()).isEqualTo("NULLNULLfooNULL"); + + sb.append("bar"); + assertThat(sb.toString()).isEqualTo("NULLNULLfooNULLbar"); + + sb.append((StringBuffer) null); + assertThat(sb.toString()).isEqualTo("NULLNULLfooNULLbarNULL"); + + sb.append(new StringBuffer("baz")); + assertThat(sb.toString()).isEqualTo("NULLNULLfooNULLbarNULLbaz"); + } + + @Test + public void testAppendWithSeparators_Array() { + final StrBuilder sb = new StrBuilder(); + sb.appendWithSeparators((Object[]) null, ","); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendWithSeparators(ArrayUtils.EMPTY_OBJECT_ARRAY, ","); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendWithSeparators(new Object[]{"foo", "bar", "baz"}, ","); + assertThat(sb.toString()).isEqualTo("foo,bar,baz"); + + sb.clear(); + sb.appendWithSeparators(new Object[]{"foo", "bar", "baz"}, null); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + + sb.clear(); + sb.appendWithSeparators(new Object[]{"foo", null, "baz"}, ","); + assertThat(sb.toString()).isEqualTo("foo,,baz"); + } + + @Test + public void testAppendWithSeparators_Collection() { + final StrBuilder sb = new StrBuilder(); + sb.appendWithSeparators((Collection) null, ","); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendWithSeparators(Collections.EMPTY_LIST, ","); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendWithSeparators(Arrays.asList("foo", "bar", "baz"), ","); + assertThat(sb.toString()).isEqualTo("foo,bar,baz"); + + sb.clear(); + sb.appendWithSeparators(Arrays.asList("foo", "bar", "baz"), null); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + + sb.clear(); + sb.appendWithSeparators(Arrays.asList("foo", null, "baz"), ","); + assertThat(sb.toString()).isEqualTo("foo,,baz"); + } + @Test + public void testAppendWithSeparators_Iterator() { + final StrBuilder sb = new StrBuilder(); + sb.appendWithSeparators((Iterator) null, ","); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendWithSeparators(Collections.EMPTY_LIST.iterator(), ","); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendWithSeparators(Arrays.asList("foo", "bar", "baz").iterator(), ","); + assertThat(sb.toString()).isEqualTo("foo,bar,baz"); + + sb.clear(); + sb.appendWithSeparators(Arrays.asList("foo", "bar", "baz").iterator(), null); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + + sb.clear(); + sb.appendWithSeparators(Arrays.asList("foo", null, "baz").iterator(), ","); + assertThat(sb.toString()).isEqualTo("foo,,baz"); + } + + @Test + public void testAppendWithSeparatorsWithNullText() { + final StrBuilder sb = new StrBuilder(); + sb.setNullText("null"); + sb.appendWithSeparators(new Object[]{"foo", null, "baz"}, ","); + assertThat(sb.toString()).isEqualTo("foo,null,baz"); + + sb.clear(); + sb.appendWithSeparators(Arrays.asList("foo", null, "baz"), ","); + assertThat(sb.toString()).isEqualTo("foo,null,baz"); + } + + @Test + public void testInsert() { + + final StrBuilder sb = new StrBuilder(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, FOO)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, FOO)); + + sb.insert(0, (Object) null); + assertThat(sb.toString()).isEqualTo("barbaz"); + + sb.insert(0, FOO); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, "foo")); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, "foo")); + + sb.insert(0, (String) null); + assertThat(sb.toString()).isEqualTo("barbaz"); + + sb.insert(0, "foo"); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, new char[]{'f', 'o', 'o'})); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, new char[]{'f', 'o', 'o'})); + + sb.insert(0, (char[]) null); + assertThat(sb.toString()).isEqualTo("barbaz"); + + sb.insert(0, ArrayUtils.EMPTY_CHAR_ARRAY); + assertThat(sb.toString()).isEqualTo("barbaz"); + + sb.insert(0, new char[]{'f', 'o', 'o'}); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, + () -> sb.insert(-1, new char[]{'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 3, 3)); + + assertThrows(IndexOutOfBoundsException.class, + () -> sb.insert(7, new char[]{'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 3, 3)); + + sb.insert(0, (char[]) null, 0, 0); + assertThat(sb.toString()).isEqualTo("barbaz"); + + sb.insert(0, ArrayUtils.EMPTY_CHAR_ARRAY, 0, 0); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, + () -> sb.insert(0, new char[]{'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, -1, 3)); + + assertThrows(IndexOutOfBoundsException.class, + () -> sb.insert(0, new char[]{'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 10, 3)); + + assertThrows(IndexOutOfBoundsException.class, + () -> sb.insert(0, new char[]{'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 0, -1)); + + assertThrows(IndexOutOfBoundsException.class, + () -> sb.insert(0, new char[]{'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 0, 10)); + + sb.insert(0, new char[]{'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 0, 0); + assertThat(sb.toString()).isEqualTo("barbaz"); + + sb.insert(0, new char[]{'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 3, 3); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, true)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, true)); + + sb.insert(0, true); + assertThat(sb.toString()).isEqualTo("truebarbaz"); + + sb.insert(0, false); + assertThat(sb.toString()).isEqualTo("falsetruebarbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, '!')); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, '!')); + + sb.insert(0, '!'); + assertThat(sb.toString()).isEqualTo("!barbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, 0)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, 0)); + + sb.insert(0, '0'); + assertThat(sb.toString()).isEqualTo("0barbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, 1L)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, 1L)); + + sb.insert(0, 1L); + assertThat(sb.toString()).isEqualTo("1barbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, 2.3F)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, 2.3F)); + + sb.insert(0, 2.3F); + assertThat(sb.toString()).isEqualTo("2.3barbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, 4.5D)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, 4.5D)); + + sb.insert(0, 4.5D); + assertThat(sb.toString()).isEqualTo("4.5barbaz"); + } + + @Test + public void testInsertWithNullText() { + final StrBuilder sb = new StrBuilder(); + sb.setNullText("null"); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, FOO)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, FOO)); + + sb.insert(0, (Object) null); + assertThat(sb.toString()).isEqualTo("nullbarbaz"); + + sb.insert(0, FOO); + assertThat(sb.toString()).isEqualTo("foonullbarbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, "foo")); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, "foo")); + + sb.insert(0, (String) null); + assertThat(sb.toString()).isEqualTo("nullbarbaz"); + + sb.insert(0, "foo"); + assertThat(sb.toString()).isEqualTo("foonullbarbaz"); + + sb.insert(0, (char[]) null); + assertThat(sb.toString()).isEqualTo("nullfoonullbarbaz"); + + sb.insert(0, (char[]) null, 0, 0); + assertThat(sb.toString()).isEqualTo("nullnullfoonullbarbaz"); + } + + /** See: https://issues.apache.org/jira/browse/LANG-299 */ + @Test + public void testLang299() { + final StrBuilder sb = new StrBuilder(1); + sb.appendFixedWidthPadRight("foo", 1, '-'); + assertThat(sb.toString()).isEqualTo("f"); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/StrBuilderTest.java b/sources/src/test/java/org/apache/commons/text/StrBuilderTest.java new file mode 100644 index 0000000..738fa93 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/StrBuilderTest.java @@ -0,0 +1,2024 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link StrBuilder}. + * + * @deprecated This class will be removed in 2.0. + */ +@Deprecated +public class StrBuilderTest { + + private static class MockReadable implements Readable { + + private final CharBuffer src; + + MockReadable(final String src) { + this.src = CharBuffer.wrap(src); + } + + @Override + public int read(final CharBuffer cb) throws IOException { + return src.read(cb); + } + } + + static final StrMatcher A_NUMBER_MATCHER = new StrMatcher() { + + @Override + public int isMatch(final char[] buffer, int pos, final int bufferStart, final int bufferEnd) { + if (buffer[pos] == 'A') { + pos++; + if (pos < bufferEnd && buffer[pos] >= '0' && buffer[pos] <= '9') { + return 2; + } + } + return 0; + } + }; + + @Test + public void test_LANG_1131_EqualsWithNullStrBuilder() throws Exception { + final StrBuilder sb = new StrBuilder(); + final StrBuilder other = null; + assertFalse(sb.equals(other)); + } + + @Test + public void testAppendCharBuffer() { + final StrBuilder sb1 = new StrBuilder(); + final CharBuffer buf = CharBuffer.allocate(10); + buf.append("0123456789"); + buf.flip(); + sb1.append(buf); + assertEquals("0123456789", sb1.toString()); + + final StrBuilder sb2 = new StrBuilder(); + sb2.append(buf, 1, 8); + assertEquals("12345678", sb2.toString()); + } + + @Test + public void testAppendCharBufferException() throws Exception { + final StrBuilder sb = new StrBuilder("1234567890"); + final String text = "Test"; + final CharBuffer buffer = CharBuffer.allocate(sb.size() + text.length()); + buffer.put(text); + buffer.flip(); + try { + sb.append(buffer, -1, 12); + } catch (final StringIndexOutOfBoundsException e) { + assertEquals("startIndex must be valid", e.getMessage()); + } + + try { + sb.append(buffer, 0, -1); + } catch (final StringIndexOutOfBoundsException e) { + assertEquals("length must be valid", e.getMessage()); + } + + sb.append(buffer); + assertEquals("1234567890Test", sb.toString()); + } + + @Test + public void testAppendCharBufferNull() throws Exception { + final StrBuilder sb = new StrBuilder("1234567890"); + final CharBuffer buffer = null; + sb.append(buffer); + assertEquals("1234567890", sb.toString()); + + final StrBuilder sb1 = new StrBuilder("1234567890"); + final CharBuffer buffer1 = null; + sb.append(buffer1, 0, 0); + assertEquals("1234567890", sb1.toString()); + } + + @Test + public void testAppendCharSequence() { + final CharSequence obj0 = null; + final CharSequence obj1 = new StrBuilder("test1"); + final CharSequence obj2 = new StringBuilder("test2"); + final CharSequence obj3 = new StringBuffer("test3"); + final CharBuffer obj4 = CharBuffer.wrap("test4".toCharArray()); + + final StrBuilder sb0 = new StrBuilder(); + assertEquals("", sb0.append(obj0).toString()); + + final StrBuilder sb1 = new StrBuilder(); + assertEquals("test1", sb1.append(obj1).toString()); + + final StrBuilder sb2 = new StrBuilder(); + assertEquals("test2", sb2.append(obj2).toString()); + + final StrBuilder sb3 = new StrBuilder(); + assertEquals("test3", sb3.append(obj3).toString()); + + final StrBuilder sb4 = new StrBuilder(); + assertEquals("test4", sb4.append(obj4).toString()); + + final StrBuilder sb5 = new StrBuilder(); + assertEquals("", sb5.append(obj0, 0, 0).toString()); + } + + @Test + public void testAppendln() { + final StrBuilder sb1 = new StrBuilder(); + final char ch = 'c'; + assertEquals("c" + System.lineSeparator(), sb1.appendln(ch).toString()); + } + + @Test + public void testAppendStringBuilderNull() { + final StrBuilder sb1 = new StrBuilder(); + final StringBuilder b = null; + assertEquals("", sb1.append(b).toString()); + + final StrBuilder sb2 = new StrBuilder(); + assertEquals("", sb2.append(b, 0, 0).toString()); + } + + @Test + public void testAppendTakingTwoIntsWithIndexOutOfBoundsThrowsStringIndexOutOfBoundsExceptionTwo() { + assertThatExceptionOfType(StringIndexOutOfBoundsException.class).isThrownBy(() -> { + final Charset charset = Charset.defaultCharset(); + final ByteBuffer byteBuffer = charset.encode("asdf"); + final CharBuffer charBuffer = charset.decode(byteBuffer); + + new StrBuilder().append(charBuffer, 933, 654); + }); + } + + @Test + public void testAppendTakingTwoIntsWithZeroThrowsStringIndexOutOfBoundsException() { + assertThatExceptionOfType(StringIndexOutOfBoundsException.class).isThrownBy(() -> { + final Charset charset = Charset.defaultCharset(); + final ByteBuffer byteBuffer = charset.encode("end < start"); + final CharBuffer charBuffer = charset.decode(byteBuffer); + + new StrBuilder(630).append(charBuffer, 0, 630); + }); + } + + @Test + public void testAppendToCharBuffer() throws Exception { + final StrBuilder sb = new StrBuilder("1234567890"); + final String text = "Test "; + final CharBuffer buffer = CharBuffer.allocate(sb.size() + text.length()); + buffer.put(text); + + sb.appendTo(buffer); + + buffer.flip(); + assertEquals("Test 1234567890", buffer.toString()); + } + + @Test + public void testAppendToStringBuffer() throws Exception { + final StrBuilder sb = new StrBuilder("1234567890"); + final StringBuilder buffer = new StringBuilder("Test "); + + sb.appendTo(buffer); + + assertEquals("Test 1234567890", buffer.toString()); + } + + @Test + public void testAppendToStringBuilder() throws Exception { + final StrBuilder sb = new StrBuilder("1234567890"); + final StringBuilder builder = new StringBuilder("Test "); + + sb.appendTo(builder); + + assertEquals("Test 1234567890", builder.toString()); + } + + @Test + public void testAppendToWriter() throws Exception { + final StrBuilder sb = new StrBuilder("1234567890"); + final StringWriter writer = new StringWriter(); + writer.append("Test "); + + sb.appendTo(writer); + + assertEquals("Test 1234567890", writer.toString()); + } + + @Test + public void testAsBuilder() { + final StrBuilder sb = new StrBuilder().appendAll("Lorem", " ", "ipsum", " ", "dolor"); + assertEquals(sb.toString(), sb.build()); + } + + @Test + public void testAsReader() throws Exception { + final StrBuilder sb = new StrBuilder("some text"); + try (Reader reader = sb.asReader()) { + assertTrue(reader.ready()); + final char[] buf = new char[40]; + assertEquals(9, reader.read(buf)); + assertEquals("some text", new String(buf, 0, 9)); + + assertEquals(-1, reader.read()); + assertFalse(reader.ready()); + assertEquals(0, reader.skip(2)); + assertEquals(0, reader.skip(-1)); + + assertTrue(reader.markSupported()); + } + try (Reader reader = sb.asReader()) { + assertEquals('s', reader.read()); + reader.mark(-1); + final char[] array = new char[3]; + assertEquals(3, reader.read(array, 0, 3)); + assertEquals('o', array[0]); + assertEquals('m', array[1]); + assertEquals('e', array[2]); + reader.reset(); + assertEquals(1, reader.read(array, 1, 1)); + assertEquals('o', array[0]); + assertEquals('o', array[1]); + assertEquals('e', array[2]); + assertEquals(2, reader.skip(2)); + assertEquals(' ', reader.read()); + + assertTrue(reader.ready()); + reader.close(); + assertTrue(reader.ready()); + } + try (Reader reader = sb.asReader()) { + char[] array = new char[3]; + try { + reader.read(array, -1, 0); + fail("Exception expected!"); + } catch (final IndexOutOfBoundsException ex) { + // expected + } + try { + reader.read(array, 0, -1); + fail("Exception expected!"); + } catch (final IndexOutOfBoundsException ex) { + // expected + } + try { + reader.read(array, 100, 1); + fail("Exception expected!"); + } catch (final IndexOutOfBoundsException ex) { + // expected + } + try { + reader.read(array, 0, 100); + fail("Exception expected!"); + } catch (final IndexOutOfBoundsException ex) { + // expected + } + try { + reader.read(array, Integer.MAX_VALUE, Integer.MAX_VALUE); + fail("Exception expected!"); + } catch (final IndexOutOfBoundsException ex) { + // expected + } + + assertEquals(0, reader.read(array, 0, 0)); + assertEquals(0, array[0]); + assertEquals(0, array[1]); + assertEquals(0, array[2]); + + reader.skip(9); + assertEquals(-1, reader.read(array, 0, 1)); + + reader.reset(); + array = new char[30]; + assertEquals(9, reader.read(array, 0, 30)); + } + } + + @Test + public void testAsTokenizer() throws Exception { + // from Javadoc + final StrBuilder b = new StrBuilder(); + b.append("a b "); + final StrTokenizer t = b.asTokenizer(); + + final String[] tokens1 = t.getTokenArray(); + assertEquals(2, tokens1.length); + assertEquals("a", tokens1[0]); + assertEquals("b", tokens1[1]); + assertEquals(2, t.size()); + + b.append("c d "); + final String[] tokens2 = t.getTokenArray(); + assertEquals(2, tokens2.length); + assertEquals("a", tokens2[0]); + assertEquals("b", tokens2[1]); + assertEquals(2, t.size()); + assertEquals("a", t.next()); + assertEquals("b", t.next()); + + t.reset(); + final String[] tokens3 = t.getTokenArray(); + assertEquals(4, tokens3.length); + assertEquals("a", tokens3[0]); + assertEquals("b", tokens3[1]); + assertEquals("c", tokens3[2]); + assertEquals("d", tokens3[3]); + assertEquals(4, t.size()); + assertEquals("a", t.next()); + assertEquals("b", t.next()); + assertEquals("c", t.next()); + assertEquals("d", t.next()); + + assertEquals("a b c d ", t.getContent()); + } + + @Test + public void testAsWriter() throws Exception { + final StrBuilder sb = new StrBuilder("base"); + try (Writer writer = sb.asWriter()) { + + writer.write('l'); + assertEquals("basel", sb.toString()); + + writer.write(new char[] {'i', 'n'}); + assertEquals("baselin", sb.toString()); + + writer.write(new char[] {'n', 'e', 'r'}, 1, 2); + assertEquals("baseliner", sb.toString()); + + writer.write(" rout"); + assertEquals("baseliner rout", sb.toString()); + + writer.write("ping that server", 1, 3); + assertEquals("baseliner routing", sb.toString()); + + writer.flush(); // no effect + assertEquals("baseliner routing", sb.toString()); + + writer.close(); // no effect + assertEquals("baseliner routing", sb.toString()); + + writer.write(" hi"); // works after close + assertEquals("baseliner routing hi", sb.toString()); + + sb.setLength(4); // mix and match + writer.write('d'); + assertEquals("based", sb.toString()); + } + } + + @Test + public void testCapacity() { + final StrBuilder sb = new StrBuilder(); + assertEquals(sb.buffer.length, sb.capacity()); + + sb.append("HelloWorldHelloWorldHelloWorldHelloWorld"); + assertEquals(sb.buffer.length, sb.capacity()); + } + + @Test + public void testCapacityAndLength() { + final StrBuilder sb = new StrBuilder(); + assertEquals(32, sb.capacity()); + assertEquals(0, sb.length()); + assertEquals(0, sb.size()); + assertTrue(sb.isEmpty()); + + sb.minimizeCapacity(); + assertEquals(0, sb.capacity()); + assertEquals(0, sb.length()); + assertEquals(0, sb.size()); + assertTrue(sb.isEmpty()); + + sb.ensureCapacity(32); + assertTrue(sb.capacity() >= 32); + assertEquals(0, sb.length()); + assertEquals(0, sb.size()); + assertTrue(sb.isEmpty()); + + sb.append("foo"); + assertTrue(sb.capacity() >= 32); + assertEquals(3, sb.length()); + assertEquals(3, sb.size()); + assertFalse(sb.isEmpty()); + + sb.clear(); + assertTrue(sb.capacity() >= 32); + assertEquals(0, sb.length()); + assertEquals(0, sb.size()); + assertTrue(sb.isEmpty()); + + sb.append("123456789012345678901234567890123"); + assertTrue(sb.capacity() > 32); + assertEquals(33, sb.length()); + assertEquals(33, sb.size()); + assertFalse(sb.isEmpty()); + + sb.ensureCapacity(16); + assertTrue(sb.capacity() > 16); + assertEquals(33, sb.length()); + assertEquals(33, sb.size()); + assertFalse(sb.isEmpty()); + + sb.minimizeCapacity(); + assertEquals(33, sb.capacity()); + assertEquals(33, sb.length()); + assertEquals(33, sb.size()); + assertFalse(sb.isEmpty()); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.setLength(-1)); + + sb.setLength(33); + assertEquals(33, sb.capacity()); + assertEquals(33, sb.length()); + assertEquals(33, sb.size()); + assertFalse(sb.isEmpty()); + + sb.setLength(16); + assertTrue(sb.capacity() >= 16); + assertEquals(16, sb.length()); + assertEquals(16, sb.size()); + assertEquals("1234567890123456", sb.toString()); + assertFalse(sb.isEmpty()); + + sb.setLength(32); + assertTrue(sb.capacity() >= 32); + assertEquals(32, sb.length()); + assertEquals(32, sb.size()); + assertEquals("1234567890123456\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", sb.toString()); + assertFalse(sb.isEmpty()); + + sb.setLength(0); + assertTrue(sb.capacity() >= 32); + assertEquals(0, sb.length()); + assertEquals(0, sb.size()); + assertTrue(sb.isEmpty()); + } + + @Test + public void testChaining() { + final StrBuilder sb = new StrBuilder(); + assertSame(sb, sb.setNewLineText(null)); + assertSame(sb, sb.setNullText(null)); + assertSame(sb, sb.setLength(1)); + assertSame(sb, sb.setCharAt(0, 'a')); + assertSame(sb, sb.ensureCapacity(0)); + assertSame(sb, sb.minimizeCapacity()); + assertSame(sb, sb.clear()); + assertSame(sb, sb.reverse()); + assertSame(sb, sb.trim()); + } + + @Test + public void testCharAt() { + final StrBuilder sb = new StrBuilder(); + assertThrows(IndexOutOfBoundsException.class, () -> sb.charAt(0)); + assertThrows(IndexOutOfBoundsException.class, () -> sb.charAt(-1)); + sb.append("foo"); + assertEquals('f', sb.charAt(0)); + assertEquals('o', sb.charAt(1)); + assertEquals('o', sb.charAt(2)); + assertThrows(IndexOutOfBoundsException.class, () -> sb.charAt(-1)); + assertThrows(IndexOutOfBoundsException.class, () -> sb.charAt(3)); + } + + @Test + public void testClear() { + final StrBuilder sb = new StrBuilder(); + sb.append("Hello"); + sb.clear(); + assertEquals(0, sb.length()); + assertTrue(sb.buffer.length >= 5); + } + + @Test + public void testConstructors() { + final StrBuilder sb0 = new StrBuilder(); + assertEquals(32, sb0.capacity()); + assertEquals(0, sb0.length()); + assertEquals(0, sb0.size()); + + final StrBuilder sb1 = new StrBuilder(32); + assertEquals(32, sb1.capacity()); + assertEquals(0, sb1.length()); + assertEquals(0, sb1.size()); + + final StrBuilder sb2 = new StrBuilder(0); + assertEquals(32, sb2.capacity()); + assertEquals(0, sb2.length()); + assertEquals(0, sb2.size()); + + final StrBuilder sb3 = new StrBuilder(-1); + assertEquals(32, sb3.capacity()); + assertEquals(0, sb3.length()); + assertEquals(0, sb3.size()); + + final StrBuilder sb4 = new StrBuilder(1); + assertEquals(1, sb4.capacity()); + assertEquals(0, sb4.length()); + assertEquals(0, sb4.size()); + + final StrBuilder sb5 = new StrBuilder((String) null); + assertEquals(32, sb5.capacity()); + assertEquals(0, sb5.length()); + assertEquals(0, sb5.size()); + + final StrBuilder sb6 = new StrBuilder(""); + assertEquals(32, sb6.capacity()); + assertEquals(0, sb6.length()); + assertEquals(0, sb6.size()); + + final StrBuilder sb7 = new StrBuilder("foo"); + assertEquals(35, sb7.capacity()); + assertEquals(3, sb7.length()); + assertEquals(3, sb7.size()); + } + + @Test + public void testContains_char() { + final StrBuilder sb = new StrBuilder("abcdefghijklmnopqrstuvwxyz"); + assertTrue(sb.contains('a')); + assertTrue(sb.contains('o')); + assertTrue(sb.contains('z')); + assertFalse(sb.contains('1')); + } + + @Test + public void testContains_String() { + final StrBuilder sb = new StrBuilder("abcdefghijklmnopqrstuvwxyz"); + assertTrue(sb.contains("a")); + assertTrue(sb.contains("pq")); + assertTrue(sb.contains("z")); + assertFalse(sb.contains("zyx")); + assertFalse(sb.contains((String) null)); + } + + @Test + public void testContains_StrMatcher() { + StrBuilder sb = new StrBuilder("abcdefghijklmnopqrstuvwxyz"); + assertTrue(sb.contains(StrMatcher.charMatcher('a'))); + assertTrue(sb.contains(StrMatcher.stringMatcher("pq"))); + assertTrue(sb.contains(StrMatcher.charMatcher('z'))); + assertFalse(sb.contains(StrMatcher.stringMatcher("zy"))); + assertFalse(sb.contains((StrMatcher) null)); + + sb = new StrBuilder(); + assertFalse(sb.contains(A_NUMBER_MATCHER)); + sb.append("B A1 C"); + assertTrue(sb.contains(A_NUMBER_MATCHER)); + } + + @Test + public void testDeleteAll_char() { + StrBuilder sb = new StrBuilder("abcbccba"); + sb.deleteAll('X'); + assertEquals("abcbccba", sb.toString()); + sb.deleteAll('a'); + assertEquals("bcbccb", sb.toString()); + sb.deleteAll('c'); + assertEquals("bbb", sb.toString()); + sb.deleteAll('b'); + assertEquals("", sb.toString()); + + sb = new StrBuilder(""); + sb.deleteAll('b'); + assertEquals("", sb.toString()); + } + + @Test + public void testDeleteAll_String() { + StrBuilder sb = new StrBuilder("abcbccba"); + sb.deleteAll((String) null); + assertEquals("abcbccba", sb.toString()); + sb.deleteAll(""); + assertEquals("abcbccba", sb.toString()); + + sb.deleteAll("X"); + assertEquals("abcbccba", sb.toString()); + sb.deleteAll("a"); + assertEquals("bcbccb", sb.toString()); + sb.deleteAll("c"); + assertEquals("bbb", sb.toString()); + sb.deleteAll("b"); + assertEquals("", sb.toString()); + + sb = new StrBuilder("abcbccba"); + sb.deleteAll("bc"); + assertEquals("acba", sb.toString()); + + sb = new StrBuilder(""); + sb.deleteAll("bc"); + assertEquals("", sb.toString()); + } + + @Test + public void testDeleteAll_StrMatcher() { + StrBuilder sb = new StrBuilder("A0xA1A2yA3"); + sb.deleteAll((StrMatcher) null); + assertEquals("A0xA1A2yA3", sb.toString()); + sb.deleteAll(A_NUMBER_MATCHER); + assertEquals("xy", sb.toString()); + + sb = new StrBuilder("Ax1"); + sb.deleteAll(A_NUMBER_MATCHER); + assertEquals("Ax1", sb.toString()); + + sb = new StrBuilder(""); + sb.deleteAll(A_NUMBER_MATCHER); + assertEquals("", sb.toString()); + } + + @Test + public void testDeleteCharAt() { + final StrBuilder sb = new StrBuilder("abc"); + sb.deleteCharAt(0); + assertEquals("bc", sb.toString()); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.deleteCharAt(1000)); + } + + @Test + public void testDeleteCharAtWithNegative() { + assertThatExceptionOfType(StringIndexOutOfBoundsException.class).isThrownBy(() -> new StrBuilder().deleteCharAt(-1258)); + } + + @Test + public void testDeleteFirst_char() { + StrBuilder sb = new StrBuilder("abcba"); + sb.deleteFirst('X'); + assertEquals("abcba", sb.toString()); + sb.deleteFirst('a'); + assertEquals("bcba", sb.toString()); + sb.deleteFirst('c'); + assertEquals("bba", sb.toString()); + sb.deleteFirst('b'); + assertEquals("ba", sb.toString()); + + sb = new StrBuilder(""); + sb.deleteFirst('b'); + assertEquals("", sb.toString()); + } + + @Test + public void testDeleteFirst_String() { + StrBuilder sb = new StrBuilder("abcbccba"); + sb.deleteFirst((String) null); + assertEquals("abcbccba", sb.toString()); + sb.deleteFirst(""); + assertEquals("abcbccba", sb.toString()); + + sb.deleteFirst("X"); + assertEquals("abcbccba", sb.toString()); + sb.deleteFirst("a"); + assertEquals("bcbccba", sb.toString()); + sb.deleteFirst("c"); + assertEquals("bbccba", sb.toString()); + sb.deleteFirst("b"); + assertEquals("bccba", sb.toString()); + + sb = new StrBuilder("abcbccba"); + sb.deleteFirst("bc"); + assertEquals("abccba", sb.toString()); + + sb = new StrBuilder(""); + sb.deleteFirst("bc"); + assertEquals("", sb.toString()); + } + + @Test + public void testDeleteFirst_StrMatcher() { + StrBuilder sb = new StrBuilder("A0xA1A2yA3"); + sb.deleteFirst((StrMatcher) null); + assertEquals("A0xA1A2yA3", sb.toString()); + sb.deleteFirst(A_NUMBER_MATCHER); + assertEquals("xA1A2yA3", sb.toString()); + + sb = new StrBuilder("Ax1"); + sb.deleteFirst(A_NUMBER_MATCHER); + assertEquals("Ax1", sb.toString()); + + sb = new StrBuilder(""); + sb.deleteFirst(A_NUMBER_MATCHER); + assertEquals("", sb.toString()); + } + + @Test + public void testDeleteIntInt() { + final StrBuilder sb = new StrBuilder("abc"); + sb.delete(0, 1); + assertEquals("bc", sb.toString()); + sb.delete(1, 2); + assertEquals("b", sb.toString()); + sb.delete(0, 1); + assertEquals("", sb.toString()); + sb.delete(0, 1000); + assertEquals("", sb.toString()); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.delete(1, 2)); + assertThrows(IndexOutOfBoundsException.class, () -> sb.delete(-1, 1)); + + assertThrows(IndexOutOfBoundsException.class, () -> new StrBuilder("anything").delete(2, 1)); + } + + @Test + public void testEndsWith() { + final StrBuilder sb = new StrBuilder(); + assertFalse(sb.endsWith("a")); + assertFalse(sb.endsWith("c")); + assertTrue(sb.endsWith("")); + assertFalse(sb.endsWith(null)); + sb.append("abc"); + assertTrue(sb.endsWith("c")); + assertTrue(sb.endsWith("bc")); + assertTrue(sb.endsWith("abc")); + assertFalse(sb.endsWith("cba")); + assertFalse(sb.endsWith("abcd")); + assertFalse(sb.endsWith(" abc")); + assertFalse(sb.endsWith("abc ")); + } + + @Test + public void testEnsureCapacity() { + final StrBuilder sb = new StrBuilder(); + sb.ensureCapacity(2); + assertTrue(sb.capacity() >= 2); + + sb.ensureCapacity(-1); + assertTrue(sb.capacity() >= 0); + + sb.append("HelloWorld"); + sb.ensureCapacity(40); + assertTrue(sb.capacity() >= 40); + } + + @Test + public void testEquals() { + final StrBuilder sb1 = new StrBuilder(); + final StrBuilder sb2 = new StrBuilder(); + assertTrue(sb1.equals(sb2)); + assertTrue(sb1.equals(sb1)); + assertTrue(sb2.equals(sb2)); + assertEquals(sb1, (Object) sb2); + + sb1.append("abc"); + assertFalse(sb1.equals(sb2)); + assertNotEquals(sb1, (Object) sb2); + + sb2.append("ABC"); + assertFalse(sb1.equals(sb2)); + assertNotEquals(sb1, (Object) sb2); + + sb2.clear().append("abc"); + assertTrue(sb1.equals(sb2)); + assertEquals(sb1, (Object) sb2); + + assertNotEquals(sb1, Integer.valueOf(1)); + assertNotEquals("abc", sb1); + } + + @Test + public void testEqualsIgnoreCase() { + final StrBuilder sb1 = new StrBuilder(); + final StrBuilder sb2 = new StrBuilder(); + assertTrue(sb1.equalsIgnoreCase(sb1)); + assertTrue(sb1.equalsIgnoreCase(sb2)); + assertTrue(sb2.equalsIgnoreCase(sb2)); + + sb1.append("abc"); + assertFalse(sb1.equalsIgnoreCase(sb2)); + + sb2.append("ABC"); + assertTrue(sb1.equalsIgnoreCase(sb2)); + + sb2.clear().append("abc"); + assertTrue(sb1.equalsIgnoreCase(sb2)); + assertTrue(sb1.equalsIgnoreCase(sb1)); + assertTrue(sb2.equalsIgnoreCase(sb2)); + + sb2.clear().append("aBc"); + assertTrue(sb1.equalsIgnoreCase(sb2)); + } + + @Test + public void testGetChars() { + final StrBuilder sb = new StrBuilder(); + + char[] input = new char[10]; + char[] a = sb.getChars(input); + assertSame(input, a); + assertArrayEquals(new char[10], a); + + sb.append("junit"); + a = sb.getChars(input); + assertSame(input, a); + assertArrayEquals(new char[] {'j', 'u', 'n', 'i', 't', 0, 0, 0, 0, 0 }, a); + + a = sb.getChars(null); + assertNotSame(input, a); + assertEquals(5, a.length); + assertArrayEquals("junit".toCharArray(), a); + + input = new char[5]; + a = sb.getChars(input); + assertSame(input, a); + + input = new char[4]; + a = sb.getChars(input); + assertNotSame(input, a); + } + + @Test + public void testGetCharsIntIntCharArrayInt() { + final StrBuilder sb = new StrBuilder(); + + sb.append("junit"); + char[] a = new char[5]; + sb.getChars(0, 5, a, 0); + assertArrayEquals(new char[] {'j', 'u', 'n', 'i', 't' }, a); + + a = new char[5]; + sb.getChars(0, 2, a, 3); + assertArrayEquals(new char[] {0, 0, 0, 'j', 'u' }, a); + + try { + sb.getChars(-1, 0, a, 0); + fail("no exception"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.getChars(0, -1, a, 0); + fail("no exception"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.getChars(0, 20, a, 0); + fail("no exception"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + try { + sb.getChars(4, 2, a, 0); + fail("no exception"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + } + + @Test + public void testGetSetNewLineText() { + final StrBuilder sb = new StrBuilder(); + assertNull(sb.getNewLineText()); + + sb.setNewLineText("#"); + assertEquals("#", sb.getNewLineText()); + + sb.setNewLineText(""); + assertEquals("", sb.getNewLineText()); + + sb.setNewLineText((String) null); + assertNull(sb.getNewLineText()); + } + + @Test + public void testGetSetNullText() { + final StrBuilder sb = new StrBuilder(); + assertNull(sb.getNullText()); + + sb.setNullText("null"); + assertEquals("null", sb.getNullText()); + + sb.setNullText(""); + assertNull(sb.getNullText()); + + sb.setNullText("NULL"); + assertEquals("NULL", sb.getNullText()); + + sb.setNullText((String) null); + assertNull(sb.getNullText()); + } + + @Test + public void testHashCode() { + final StrBuilder sb = new StrBuilder(); + final int hc1a = sb.hashCode(); + final int hc1b = sb.hashCode(); + assertEquals(0, hc1a); + assertEquals(hc1a, hc1b); + + sb.append("abc"); + final int hc2a = sb.hashCode(); + final int hc2b = sb.hashCode(); + assertTrue(hc2a != 0); + assertEquals(hc2a, hc2b); + } + + @Test + public void testIndexOf_char() { + final StrBuilder sb = new StrBuilder("abab"); + assertEquals(0, sb.indexOf('a')); + + // should work like String#indexOf + assertEquals("abab".indexOf('a'), sb.indexOf('a')); + + assertEquals(1, sb.indexOf('b')); + assertEquals("abab".indexOf('b'), sb.indexOf('b')); + + assertEquals(-1, sb.indexOf('z')); + } + + @Test + public void testIndexOf_char_int() { + StrBuilder sb = new StrBuilder("abab"); + assertEquals(0, sb.indexOf('a', -1)); + assertEquals(0, sb.indexOf('a', 0)); + assertEquals(2, sb.indexOf('a', 1)); + assertEquals(-1, sb.indexOf('a', 4)); + assertEquals(-1, sb.indexOf('a', 5)); + + // should work like String#indexOf + assertEquals("abab".indexOf('a', 1), sb.indexOf('a', 1)); + + assertEquals(3, sb.indexOf('b', 2)); + assertEquals("abab".indexOf('b', 2), sb.indexOf('b', 2)); + + assertEquals(-1, sb.indexOf('z', 2)); + + sb = new StrBuilder("xyzabc"); + assertEquals(2, sb.indexOf('z', 0)); + assertEquals(-1, sb.indexOf('z', 3)); + } + + @Test + public void testIndexOf_String() { + final StrBuilder sb = new StrBuilder("abab"); + + assertEquals(0, sb.indexOf("a")); + // should work like String#indexOf + assertEquals("abab".indexOf("a"), sb.indexOf("a")); + + assertEquals(0, sb.indexOf("ab")); + // should work like String#indexOf + assertEquals("abab".indexOf("ab"), sb.indexOf("ab")); + + assertEquals(1, sb.indexOf("b")); + assertEquals("abab".indexOf("b"), sb.indexOf("b")); + + assertEquals(1, sb.indexOf("ba")); + assertEquals("abab".indexOf("ba"), sb.indexOf("ba")); + + assertEquals(-1, sb.indexOf("z")); + + assertEquals(-1, sb.indexOf((String) null)); + } + + @Test + public void testIndexOf_String_int() { + StrBuilder sb = new StrBuilder("abab"); + assertEquals(0, sb.indexOf("a", -1)); + assertEquals(0, sb.indexOf("a", 0)); + assertEquals(2, sb.indexOf("a", 1)); + assertEquals(2, sb.indexOf("a", 2)); + assertEquals(-1, sb.indexOf("a", 3)); + assertEquals(-1, sb.indexOf("a", 4)); + assertEquals(-1, sb.indexOf("a", 5)); + + assertEquals(-1, sb.indexOf("abcdef", 0)); + assertEquals(0, sb.indexOf("", 0)); + assertEquals(1, sb.indexOf("", 1)); + + // should work like String#indexOf + assertEquals("abab".indexOf("a", 1), sb.indexOf("a", 1)); + + assertEquals(2, sb.indexOf("ab", 1)); + // should work like String#indexOf + assertEquals("abab".indexOf("ab", 1), sb.indexOf("ab", 1)); + + assertEquals(3, sb.indexOf("b", 2)); + assertEquals("abab".indexOf("b", 2), sb.indexOf("b", 2)); + + assertEquals(1, sb.indexOf("ba", 1)); + assertEquals("abab".indexOf("ba", 2), sb.indexOf("ba", 2)); + + assertEquals(-1, sb.indexOf("z", 2)); + + sb = new StrBuilder("xyzabc"); + assertEquals(2, sb.indexOf("za", 0)); + assertEquals(-1, sb.indexOf("za", 3)); + + assertEquals(-1, sb.indexOf((String) null, 2)); + } + + @Test + public void testIndexOf_StrMatcher() { + final StrBuilder sb = new StrBuilder(); + assertEquals(-1, sb.indexOf((StrMatcher) null)); + assertEquals(-1, sb.indexOf(StrMatcher.charMatcher('a'))); + + sb.append("ab bd"); + assertEquals(0, sb.indexOf(StrMatcher.charMatcher('a'))); + assertEquals(1, sb.indexOf(StrMatcher.charMatcher('b'))); + assertEquals(2, sb.indexOf(StrMatcher.spaceMatcher())); + assertEquals(4, sb.indexOf(StrMatcher.charMatcher('d'))); + assertEquals(-1, sb.indexOf(StrMatcher.noneMatcher())); + assertEquals(-1, sb.indexOf((StrMatcher) null)); + + sb.append(" A1 junction"); + assertEquals(6, sb.indexOf(A_NUMBER_MATCHER)); + } + + @Test + public void testIndexOf_StrMatcher_int() { + final StrBuilder sb = new StrBuilder(); + assertEquals(-1, sb.indexOf((StrMatcher) null, 2)); + assertEquals(-1, sb.indexOf(StrMatcher.charMatcher('a'), 2)); + assertEquals(-1, sb.indexOf(StrMatcher.charMatcher('a'), 0)); + + sb.append("ab bd"); + assertEquals(0, sb.indexOf(StrMatcher.charMatcher('a'), -2)); + assertEquals(0, sb.indexOf(StrMatcher.charMatcher('a'), 0)); + assertEquals(-1, sb.indexOf(StrMatcher.charMatcher('a'), 2)); + assertEquals(-1, sb.indexOf(StrMatcher.charMatcher('a'), 20)); + + assertEquals(1, sb.indexOf(StrMatcher.charMatcher('b'), -1)); + assertEquals(1, sb.indexOf(StrMatcher.charMatcher('b'), 0)); + assertEquals(1, sb.indexOf(StrMatcher.charMatcher('b'), 1)); + assertEquals(3, sb.indexOf(StrMatcher.charMatcher('b'), 2)); + assertEquals(3, sb.indexOf(StrMatcher.charMatcher('b'), 3)); + assertEquals(-1, sb.indexOf(StrMatcher.charMatcher('b'), 4)); + assertEquals(-1, sb.indexOf(StrMatcher.charMatcher('b'), 5)); + assertEquals(-1, sb.indexOf(StrMatcher.charMatcher('b'), 6)); + + assertEquals(2, sb.indexOf(StrMatcher.spaceMatcher(), -2)); + assertEquals(2, sb.indexOf(StrMatcher.spaceMatcher(), 0)); + assertEquals(2, sb.indexOf(StrMatcher.spaceMatcher(), 2)); + assertEquals(-1, sb.indexOf(StrMatcher.spaceMatcher(), 4)); + assertEquals(-1, sb.indexOf(StrMatcher.spaceMatcher(), 20)); + + assertEquals(-1, sb.indexOf(StrMatcher.noneMatcher(), 0)); + assertEquals(-1, sb.indexOf((StrMatcher) null, 0)); + + sb.append(" A1 junction with A2"); + assertEquals(6, sb.indexOf(A_NUMBER_MATCHER, 5)); + assertEquals(6, sb.indexOf(A_NUMBER_MATCHER, 6)); + assertEquals(23, sb.indexOf(A_NUMBER_MATCHER, 7)); + assertEquals(23, sb.indexOf(A_NUMBER_MATCHER, 22)); + assertEquals(23, sb.indexOf(A_NUMBER_MATCHER, 23)); + assertEquals(-1, sb.indexOf(A_NUMBER_MATCHER, 24)); + } + + @Test + public void testIndexOfLang294() { + final StrBuilder sb = new StrBuilder("onetwothree"); + sb.deleteFirst("three"); + assertEquals(-1, sb.indexOf("three")); + } + + @Test + public void testIsEmpty() { + final StrBuilder sb = new StrBuilder(); + assertTrue(sb.isEmpty()); + + sb.append("Hello"); + assertFalse(sb.isEmpty()); + + sb.clear(); + assertTrue(sb.isEmpty()); + } + + @Test + public void testLang294() { + final StrBuilder sb = new StrBuilder("\n%BLAH%\nDo more stuff\neven more stuff\n%BLAH%\n"); + sb.deleteAll("\n%BLAH%"); + assertEquals("\nDo more stuff\neven more stuff\n", sb.toString()); + } + + @Test + public void testLang295() { + final StrBuilder sb = new StrBuilder("onetwothree"); + sb.deleteFirst("three"); + assertFalse(sb.contains('h'), "The contains(char) method is looking beyond the end of the string"); + assertEquals(-1, sb.indexOf('h'), "The indexOf(char) method is looking beyond the end of the string"); + } + + @Test + public void testLang412Left() { + final StrBuilder sb = new StrBuilder(); + sb.appendFixedWidthPadLeft(null, 10, '*'); + assertEquals("**********", sb.toString(), "Failed to invoke appendFixedWidthPadLeft correctly"); + } + + @Test + public void testLang412Right() { + final StrBuilder sb = new StrBuilder(); + sb.appendFixedWidthPadRight(null, 10, '*'); + assertEquals("**********", sb.toString(), "Failed to invoke appendFixedWidthPadRight correctly"); + } + + @Test + public void testLastIndexOf_char() { + final StrBuilder sb = new StrBuilder("abab"); + + assertEquals(2, sb.lastIndexOf('a')); + // should work like String#lastIndexOf + assertEquals("abab".lastIndexOf('a'), sb.lastIndexOf('a')); + + assertEquals(3, sb.lastIndexOf('b')); + assertEquals("abab".lastIndexOf('b'), sb.lastIndexOf('b')); + + assertEquals(-1, sb.lastIndexOf('z')); + } + + @Test + public void testLastIndexOf_char_int() { + StrBuilder sb = new StrBuilder("abab"); + assertEquals(-1, sb.lastIndexOf('a', -1)); + assertEquals(0, sb.lastIndexOf('a', 0)); + assertEquals(0, sb.lastIndexOf('a', 1)); + + // should work like String#lastIndexOf + assertEquals("abab".lastIndexOf('a', 1), sb.lastIndexOf('a', 1)); + + assertEquals(1, sb.lastIndexOf('b', 2)); + assertEquals("abab".lastIndexOf('b', 2), sb.lastIndexOf('b', 2)); + + assertEquals(-1, sb.lastIndexOf('z', 2)); + + sb = new StrBuilder("xyzabc"); + assertEquals(2, sb.lastIndexOf('z', sb.length())); + assertEquals(-1, sb.lastIndexOf('z', 1)); + } + + @Test + public void testLastIndexOf_String() { + final StrBuilder sb = new StrBuilder("abab"); + + assertEquals(2, sb.lastIndexOf("a")); + // should work like String#lastIndexOf + assertEquals("abab".lastIndexOf("a"), sb.lastIndexOf("a")); + + assertEquals(2, sb.lastIndexOf("ab")); + // should work like String#lastIndexOf + assertEquals("abab".lastIndexOf("ab"), sb.lastIndexOf("ab")); + + assertEquals(3, sb.lastIndexOf("b")); + assertEquals("abab".lastIndexOf("b"), sb.lastIndexOf("b")); + + assertEquals(1, sb.lastIndexOf("ba")); + assertEquals("abab".lastIndexOf("ba"), sb.lastIndexOf("ba")); + + assertEquals(-1, sb.lastIndexOf("z")); + + assertEquals(-1, sb.lastIndexOf((String) null)); + } + + @Test + public void testLastIndexOf_String_int() { + StrBuilder sb = new StrBuilder("abab"); + assertEquals(-1, sb.lastIndexOf("a", -1)); + assertEquals(0, sb.lastIndexOf("a", 0)); + assertEquals(0, sb.lastIndexOf("a", 1)); + assertEquals(2, sb.lastIndexOf("a", 2)); + assertEquals(2, sb.lastIndexOf("a", 3)); + assertEquals(2, sb.lastIndexOf("a", 4)); + assertEquals(2, sb.lastIndexOf("a", 5)); + + assertEquals(-1, sb.lastIndexOf("abcdef", 3)); + assertEquals("abab".lastIndexOf("", 3), sb.lastIndexOf("", 3)); + assertEquals("abab".lastIndexOf("", 1), sb.lastIndexOf("", 1)); + + // should work like String#lastIndexOf + assertEquals("abab".lastIndexOf("a", 1), sb.lastIndexOf("a", 1)); + + assertEquals(0, sb.lastIndexOf("ab", 1)); + // should work like String#lastIndexOf + assertEquals("abab".lastIndexOf("ab", 1), sb.lastIndexOf("ab", 1)); + + assertEquals(1, sb.lastIndexOf("b", 2)); + assertEquals("abab".lastIndexOf("b", 2), sb.lastIndexOf("b", 2)); + + assertEquals(1, sb.lastIndexOf("ba", 2)); + assertEquals("abab".lastIndexOf("ba", 2), sb.lastIndexOf("ba", 2)); + + assertEquals(-1, sb.lastIndexOf("z", 2)); + + sb = new StrBuilder("xyzabc"); + assertEquals(2, sb.lastIndexOf("za", sb.length())); + assertEquals(-1, sb.lastIndexOf("za", 1)); + + assertEquals(-1, sb.lastIndexOf((String) null, 2)); + } + + @Test + public void testLastIndexOf_StrMatcher() { + final StrBuilder sb = new StrBuilder(); + assertEquals(-1, sb.lastIndexOf((StrMatcher) null)); + assertEquals(-1, sb.lastIndexOf(StrMatcher.charMatcher('a'))); + + sb.append("ab bd"); + assertEquals(0, sb.lastIndexOf(StrMatcher.charMatcher('a'))); + assertEquals(3, sb.lastIndexOf(StrMatcher.charMatcher('b'))); + assertEquals(2, sb.lastIndexOf(StrMatcher.spaceMatcher())); + assertEquals(4, sb.lastIndexOf(StrMatcher.charMatcher('d'))); + assertEquals(-1, sb.lastIndexOf(StrMatcher.noneMatcher())); + assertEquals(-1, sb.lastIndexOf((StrMatcher) null)); + + sb.append(" A1 junction"); + assertEquals(6, sb.lastIndexOf(A_NUMBER_MATCHER)); + } + + @Test + public void testLastIndexOf_StrMatcher_int() { + final StrBuilder sb = new StrBuilder(); + assertEquals(-1, sb.lastIndexOf((StrMatcher) null, 2)); + assertEquals(-1, sb.lastIndexOf(StrMatcher.charMatcher('a'), 2)); + assertEquals(-1, sb.lastIndexOf(StrMatcher.charMatcher('a'), 0)); + assertEquals(-1, sb.lastIndexOf(StrMatcher.charMatcher('a'), -1)); + + sb.append("ab bd"); + assertEquals(-1, sb.lastIndexOf(StrMatcher.charMatcher('a'), -2)); + assertEquals(0, sb.lastIndexOf(StrMatcher.charMatcher('a'), 0)); + assertEquals(0, sb.lastIndexOf(StrMatcher.charMatcher('a'), 2)); + assertEquals(0, sb.lastIndexOf(StrMatcher.charMatcher('a'), 20)); + + assertEquals(-1, sb.lastIndexOf(StrMatcher.charMatcher('b'), -1)); + assertEquals(-1, sb.lastIndexOf(StrMatcher.charMatcher('b'), 0)); + assertEquals(1, sb.lastIndexOf(StrMatcher.charMatcher('b'), 1)); + assertEquals(1, sb.lastIndexOf(StrMatcher.charMatcher('b'), 2)); + assertEquals(3, sb.lastIndexOf(StrMatcher.charMatcher('b'), 3)); + assertEquals(3, sb.lastIndexOf(StrMatcher.charMatcher('b'), 4)); + assertEquals(3, sb.lastIndexOf(StrMatcher.charMatcher('b'), 5)); + assertEquals(3, sb.lastIndexOf(StrMatcher.charMatcher('b'), 6)); + + assertEquals(-1, sb.lastIndexOf(StrMatcher.spaceMatcher(), -2)); + assertEquals(-1, sb.lastIndexOf(StrMatcher.spaceMatcher(), 0)); + assertEquals(2, sb.lastIndexOf(StrMatcher.spaceMatcher(), 2)); + assertEquals(2, sb.lastIndexOf(StrMatcher.spaceMatcher(), 4)); + assertEquals(2, sb.lastIndexOf(StrMatcher.spaceMatcher(), 20)); + + assertEquals(-1, sb.lastIndexOf(StrMatcher.noneMatcher(), 0)); + assertEquals(-1, sb.lastIndexOf((StrMatcher) null, 0)); + + sb.append(" A1 junction with A2"); + assertEquals(-1, sb.lastIndexOf(A_NUMBER_MATCHER, 5)); + assertEquals(-1, sb.lastIndexOf(A_NUMBER_MATCHER, 6)); // A matches, 1 + // is outside + // bounds + assertEquals(6, sb.lastIndexOf(A_NUMBER_MATCHER, 7)); + assertEquals(6, sb.lastIndexOf(A_NUMBER_MATCHER, 22)); + assertEquals(6, sb.lastIndexOf(A_NUMBER_MATCHER, 23)); // A matches, 2 + // is outside + // bounds + assertEquals(23, sb.lastIndexOf(A_NUMBER_MATCHER, 24)); + } + + @Test + public void testLeftString() { + final StrBuilder sb = new StrBuilder("left right"); + assertEquals("left", sb.leftString(4)); + assertEquals("", sb.leftString(0)); + assertEquals("", sb.leftString(-5)); + assertEquals("left right", sb.leftString(15)); + } + + @Test + public void testLength() { + final StrBuilder sb = new StrBuilder(); + assertEquals(0, sb.length()); + + sb.append("Hello"); + assertEquals(5, sb.length()); + } + + @Test + public void testMidString() { + final StrBuilder sb = new StrBuilder("hello goodbye hello"); + assertEquals("goodbye", sb.midString(6, 7)); + assertEquals("hello", sb.midString(0, 5)); + assertEquals("hello", sb.midString(-5, 5)); + assertEquals("", sb.midString(0, -1)); + assertEquals("", sb.midString(20, 2)); + assertEquals("hello", sb.midString(14, 22)); + } + + @Test + public void testMinimizeCapacity() { + final StrBuilder sb = new StrBuilder(); + sb.minimizeCapacity(); + assertEquals(0, sb.capacity()); + + sb.append("HelloWorld"); + sb.minimizeCapacity(); + assertEquals(10, sb.capacity()); + } + + @Test + public void testReadFromCharBuffer() throws Exception { + String s = ""; + for (int i = 0; i < 100; ++i) { + final StrBuilder sb = new StrBuilder(); + final int len = sb.readFrom(CharBuffer.wrap(s)); + + assertEquals(s.length(), len); + assertEquals(s, sb.toString()); + + s += Integer.toString(i); + } + } + + @Test + public void testReadFromCharBufferAppendsToEnd() throws Exception { + final StrBuilder sb = new StrBuilder("Test"); + sb.readFrom(CharBuffer.wrap(" 123")); + assertEquals("Test 123", sb.toString()); + } + + @Test + public void testReadFromReadable() throws Exception { + String s = ""; + for (int i = 0; i < 100; ++i) { + final StrBuilder sb = new StrBuilder(); + final int len = sb.readFrom(new MockReadable(s)); + + assertEquals(s.length(), len); + assertEquals(s, sb.toString()); + + s += Integer.toString(i); + } + } + + @Test + public void testReadFromReadableAppendsToEnd() throws Exception { + final StrBuilder sb = new StrBuilder("Test"); + sb.readFrom(new MockReadable(" 123")); + assertEquals("Test 123", sb.toString()); + } + + @Test + public void testReadFromReader() throws Exception { + String s = ""; + for (int i = 0; i < 100; ++i) { + final StrBuilder sb = new StrBuilder(); + final int len = sb.readFrom(new StringReader(s)); + + assertEquals(s.length(), len); + assertEquals(s, sb.toString()); + + s += Integer.toString(i); + } + } + + @Test + public void testReadFromReaderAppendsToEnd() throws Exception { + final StrBuilder sb = new StrBuilder("Test"); + sb.readFrom(new StringReader(" 123")); + assertEquals("Test 123", sb.toString()); + } + + @Test + public void testReplace_int_int_String() { + StrBuilder sb = new StrBuilder("abc"); + sb.replace(0, 1, "d"); + assertEquals("dbc", sb.toString()); + sb.replace(0, 1, "aaa"); + assertEquals("aaabc", sb.toString()); + sb.replace(0, 3, ""); + assertEquals("bc", sb.toString()); + sb.replace(1, 2, (String) null); + assertEquals("b", sb.toString()); + sb.replace(1, 1000, "text"); + assertEquals("btext", sb.toString()); + sb.replace(0, 1000, "text"); + assertEquals("text", sb.toString()); + + sb = new StrBuilder("atext"); + sb.replace(1, 1, "ny"); + assertEquals("anytext", sb.toString()); + try { + sb.replace(2, 1, "anything"); + fail("Expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + + sb = new StrBuilder(); + try { + sb.replace(1, 2, "anything"); + fail("Expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + try { + sb.replace(-1, 1, "anything"); + fail("Expected IndexOutOfBoundsException"); + } catch (final IndexOutOfBoundsException e) { + // expected + } + } + + @Test + public void testReplace_StrMatcher_String_int_int_int_VaryCount() { + StrBuilder sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 10, -1); + assertEquals("-x--y-", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 10, 0); + assertEquals("aaxaaaayaa", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 10, 1); + assertEquals("-xaaaayaa", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 10, 2); + assertEquals("-x-aayaa", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 10, 3); + assertEquals("-x--yaa", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 10, 4); + assertEquals("-x--y-", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 10, 5); + assertEquals("-x--y-", sb.toString()); + } + + @Test + public void testReplace_StrMatcher_String_int_int_int_VaryEndIndex() { + StrBuilder sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 0, -1); + assertEquals("aaxaaaayaa", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 2, -1); + assertEquals("-xaaaayaa", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 3, -1); + assertEquals("-xaaaayaa", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 4, -1); + assertEquals("-xaaaayaa", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 5, -1); + assertEquals("-x-aayaa", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 6, -1); + assertEquals("-x-aayaa", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 7, -1); + assertEquals("-x--yaa", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 8, -1); + assertEquals("-x--yaa", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 9, -1); + assertEquals("-x--yaa", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 10, -1); + assertEquals("-x--y-", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 1000, -1); + assertEquals("-x--y-", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + try { + sb.replace(StrMatcher.stringMatcher("aa"), "-", 2, 1, -1); + fail("Exception expected!"); + } catch (final IndexOutOfBoundsException ex) { + // expected + } + assertEquals("aaxaaaayaa", sb.toString()); + } + + @Test + public void testReplace_StrMatcher_String_int_int_int_VaryMatcher() { + StrBuilder sb = new StrBuilder("abcbccba"); + sb.replace((StrMatcher) null, "x", 0, sb.length(), -1); + assertEquals("abcbccba", sb.toString()); + + sb.replace(StrMatcher.charMatcher('a'), "x", 0, sb.length(), -1); + assertEquals("xbcbccbx", sb.toString()); + + sb.replace(StrMatcher.stringMatcher("cb"), "x", 0, sb.length(), -1); + assertEquals("xbxcxx", sb.toString()); + + sb = new StrBuilder("A1-A2A3-A4"); + sb.replace(A_NUMBER_MATCHER, "***", 0, sb.length(), -1); + assertEquals("***-******-***", sb.toString()); + + sb = new StrBuilder(); + sb.replace(A_NUMBER_MATCHER, "***", 0, sb.length(), -1); + assertEquals("", sb.toString()); + } + + @Test + public void testReplace_StrMatcher_String_int_int_int_VaryReplace() { + StrBuilder sb = new StrBuilder("abcbccba"); + sb.replace(StrMatcher.stringMatcher("cb"), "cb", 0, sb.length(), -1); + assertEquals("abcbccba", sb.toString()); + + sb = new StrBuilder("abcbccba"); + sb.replace(StrMatcher.stringMatcher("cb"), "-", 0, sb.length(), -1); + assertEquals("ab-c-a", sb.toString()); + + sb = new StrBuilder("abcbccba"); + sb.replace(StrMatcher.stringMatcher("cb"), "+++", 0, sb.length(), -1); + assertEquals("ab+++c+++a", sb.toString()); + + sb = new StrBuilder("abcbccba"); + sb.replace(StrMatcher.stringMatcher("cb"), "", 0, sb.length(), -1); + assertEquals("abca", sb.toString()); + + sb = new StrBuilder("abcbccba"); + sb.replace(StrMatcher.stringMatcher("cb"), null, 0, sb.length(), -1); + assertEquals("abca", sb.toString()); + } + + @Test + public void testReplace_StrMatcher_String_int_int_int_VaryStartIndex() { + StrBuilder sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, sb.length(), -1); + assertEquals("-x--y-", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 1, sb.length(), -1); + assertEquals("aax--y-", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 2, sb.length(), -1); + assertEquals("aax--y-", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 3, sb.length(), -1); + assertEquals("aax--y-", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 4, sb.length(), -1); + assertEquals("aaxa-ay-", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 5, sb.length(), -1); + assertEquals("aaxaa-y-", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 6, sb.length(), -1); + assertEquals("aaxaaaay-", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 7, sb.length(), -1); + assertEquals("aaxaaaay-", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 8, sb.length(), -1); + assertEquals("aaxaaaay-", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 9, sb.length(), -1); + assertEquals("aaxaaaayaa", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + sb.replace(StrMatcher.stringMatcher("aa"), "-", 10, sb.length(), -1); + assertEquals("aaxaaaayaa", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + try { + sb.replace(StrMatcher.stringMatcher("aa"), "-", 11, sb.length(), -1); + fail("Exception expected!"); + } catch (final IndexOutOfBoundsException ex) { + // expected + } + assertEquals("aaxaaaayaa", sb.toString()); + + sb = new StrBuilder("aaxaaaayaa"); + try { + sb.replace(StrMatcher.stringMatcher("aa"), "-", -1, sb.length(), -1); + fail("Exception expected!"); + } catch (final IndexOutOfBoundsException ex) { + // expected + } + assertEquals("aaxaaaayaa", sb.toString()); + } + + @Test + public void testReplaceAll_char_char() { + final StrBuilder sb = new StrBuilder("abcbccba"); + sb.replaceAll('x', 'y'); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll('a', 'd'); + assertEquals("dbcbccbd", sb.toString()); + sb.replaceAll('b', 'e'); + assertEquals("dececced", sb.toString()); + sb.replaceAll('c', 'f'); + assertEquals("defeffed", sb.toString()); + sb.replaceAll('d', 'd'); + assertEquals("defeffed", sb.toString()); + } + + @Test + public void testReplaceAll_String_String() { + StrBuilder sb = new StrBuilder("abcbccba"); + sb.replaceAll((String) null, null); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll((String) null, "anything"); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll("", null); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll("", "anything"); + assertEquals("abcbccba", sb.toString()); + + sb.replaceAll("x", "y"); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll("a", "d"); + assertEquals("dbcbccbd", sb.toString()); + sb.replaceAll("d", null); + assertEquals("bcbccb", sb.toString()); + sb.replaceAll("cb", "-"); + assertEquals("b-c-", sb.toString()); + + sb = new StrBuilder("abcba"); + sb.replaceAll("b", "xbx"); + assertEquals("axbxcxbxa", sb.toString()); + + sb = new StrBuilder("bb"); + sb.replaceAll("b", "xbx"); + assertEquals("xbxxbx", sb.toString()); + } + + @Test + public void testReplaceAll_StrMatcher_String() { + StrBuilder sb = new StrBuilder("abcbccba"); + sb.replaceAll((StrMatcher) null, null); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll((StrMatcher) null, "anything"); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll(StrMatcher.noneMatcher(), null); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll(StrMatcher.noneMatcher(), "anything"); + assertEquals("abcbccba", sb.toString()); + + sb.replaceAll(StrMatcher.charMatcher('x'), "y"); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll(StrMatcher.charMatcher('a'), "d"); + assertEquals("dbcbccbd", sb.toString()); + sb.replaceAll(StrMatcher.charMatcher('d'), null); + assertEquals("bcbccb", sb.toString()); + sb.replaceAll(StrMatcher.stringMatcher("cb"), "-"); + assertEquals("b-c-", sb.toString()); + + sb = new StrBuilder("abcba"); + sb.replaceAll(StrMatcher.charMatcher('b'), "xbx"); + assertEquals("axbxcxbxa", sb.toString()); + + sb = new StrBuilder("bb"); + sb.replaceAll(StrMatcher.charMatcher('b'), "xbx"); + assertEquals("xbxxbx", sb.toString()); + + sb = new StrBuilder("A1-A2A3-A4"); + sb.replaceAll(A_NUMBER_MATCHER, "***"); + assertEquals("***-******-***", sb.toString()); + + sb = new StrBuilder("Dear X, hello X."); + sb.replaceAll(StrMatcher.stringMatcher("X"), "012345678901234567"); + assertEquals("Dear 012345678901234567, hello 012345678901234567.", sb.toString()); + } + + @Test + public void testReplaceFirst_char_char() { + final StrBuilder sb = new StrBuilder("abcbccba"); + sb.replaceFirst('x', 'y'); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst('a', 'd'); + assertEquals("dbcbccba", sb.toString()); + sb.replaceFirst('b', 'e'); + assertEquals("decbccba", sb.toString()); + sb.replaceFirst('c', 'f'); + assertEquals("defbccba", sb.toString()); + sb.replaceFirst('d', 'd'); + assertEquals("defbccba", sb.toString()); + } + + @Test + public void testReplaceFirst_String_String() { + StrBuilder sb = new StrBuilder("abcbccba"); + sb.replaceFirst((String) null, null); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst((String) null, "anything"); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst("", null); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst("", "anything"); + assertEquals("abcbccba", sb.toString()); + + sb.replaceFirst("x", "y"); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst("a", "d"); + assertEquals("dbcbccba", sb.toString()); + sb.replaceFirst("d", null); + assertEquals("bcbccba", sb.toString()); + sb.replaceFirst("cb", "-"); + assertEquals("b-ccba", sb.toString()); + + sb = new StrBuilder("abcba"); + sb.replaceFirst("b", "xbx"); + assertEquals("axbxcba", sb.toString()); + + sb = new StrBuilder("bb"); + sb.replaceFirst("b", "xbx"); + assertEquals("xbxb", sb.toString()); + } + + @Test + public void testReplaceFirst_StrMatcher_String() { + StrBuilder sb = new StrBuilder("abcbccba"); + sb.replaceFirst((StrMatcher) null, null); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst((StrMatcher) null, "anything"); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst(StrMatcher.noneMatcher(), null); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst(StrMatcher.noneMatcher(), "anything"); + assertEquals("abcbccba", sb.toString()); + + sb.replaceFirst(StrMatcher.charMatcher('x'), "y"); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst(StrMatcher.charMatcher('a'), "d"); + assertEquals("dbcbccba", sb.toString()); + sb.replaceFirst(StrMatcher.charMatcher('d'), null); + assertEquals("bcbccba", sb.toString()); + sb.replaceFirst(StrMatcher.stringMatcher("cb"), "-"); + assertEquals("b-ccba", sb.toString()); + + sb = new StrBuilder("abcba"); + sb.replaceFirst(StrMatcher.charMatcher('b'), "xbx"); + assertEquals("axbxcba", sb.toString()); + + sb = new StrBuilder("bb"); + sb.replaceFirst(StrMatcher.charMatcher('b'), "xbx"); + assertEquals("xbxb", sb.toString()); + + sb = new StrBuilder("A1-A2A3-A4"); + sb.replaceFirst(A_NUMBER_MATCHER, "***"); + assertEquals("***-A2A3-A4", sb.toString()); + } + + @Test + public void testReverse() { + final StrBuilder sb = new StrBuilder(); + assertEquals("", sb.reverse().toString()); + + sb.clear().append(true); + assertEquals("eurt", sb.reverse().toString()); + assertEquals("true", sb.reverse().toString()); + } + + @Test + public void testRightString() { + final StrBuilder sb = new StrBuilder("left right"); + assertEquals("right", sb.rightString(5)); + assertEquals("", sb.rightString(0)); + assertEquals("", sb.rightString(-5)); + assertEquals("left right", sb.rightString(15)); + } + + @Test + public void testSetCharAt() { + final StrBuilder sb = new StrBuilder(); + assertThrows(IndexOutOfBoundsException.class, () -> sb.setCharAt(0, 'f')); + assertThrows(IndexOutOfBoundsException.class, () -> sb.setCharAt(-1, 'f')); + sb.append("foo"); + sb.setCharAt(0, 'b'); + sb.setCharAt(1, 'a'); + sb.setCharAt(2, 'r'); + assertThrows(IndexOutOfBoundsException.class, () -> sb.setCharAt(3, '!')); + assertEquals("bar", sb.toString()); + } + + @Test + public void testSetLength() { + final StrBuilder sb = new StrBuilder(); + sb.append("Hello"); + sb.setLength(2); // shorten + assertEquals("He", sb.toString()); + sb.setLength(2); // no change + assertEquals("He", sb.toString()); + sb.setLength(3); // lengthen + assertEquals("He\0", sb.toString()); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.setLength(-1)); + } + + @Test + public void testSize() { + final StrBuilder sb = new StrBuilder(); + assertEquals(0, sb.size()); + + sb.append("Hello"); + assertEquals(5, sb.size()); + } + + @Test + public void testStartsWith() { + final StrBuilder sb = new StrBuilder(); + assertFalse(sb.startsWith("a")); + assertFalse(sb.startsWith(null)); + assertTrue(sb.startsWith("")); + sb.append("abc"); + assertTrue(sb.startsWith("a")); + assertTrue(sb.startsWith("ab")); + assertTrue(sb.startsWith("abc")); + assertFalse(sb.startsWith("cba")); + } + + @Test + public void testSubSequenceIntInt() { + final StrBuilder sb = new StrBuilder("hello goodbye"); + // Start index is negative + assertThrows(IndexOutOfBoundsException.class, () -> sb.subSequence(-1, 5)); + + // End index is negative + assertThrows(IndexOutOfBoundsException.class, () -> sb.subSequence(2, -1)); + + // End index greater than length() + assertThrows(IndexOutOfBoundsException.class, () -> sb.subSequence(2, sb.length() + 1)); + + // Start index greater then end index + assertThrows(IndexOutOfBoundsException.class, () -> sb.subSequence(3, 2)); + + // Normal cases + assertEquals("hello", sb.subSequence(0, 5)); + assertEquals("hello goodbye".subSequence(0, 6), sb.subSequence(0, 6)); + assertEquals("goodbye", sb.subSequence(6, 13)); + assertEquals("hello goodbye".subSequence(6, 13), sb.subSequence(6, 13)); + } + + @Test + public void testSubstringInt() { + final StrBuilder sb = new StrBuilder("hello goodbye"); + assertEquals("goodbye", sb.substring(6)); + assertEquals("hello goodbye".substring(6), sb.substring(6)); + assertEquals("hello goodbye", sb.substring(0)); + assertEquals("hello goodbye".substring(0), sb.substring(0)); + assertThrows(IndexOutOfBoundsException.class, () -> sb.substring(-1)); + assertThrows(IndexOutOfBoundsException.class, () -> sb.substring(15)); + + } + + @Test + public void testSubstringIntInt() { + final StrBuilder sb = new StrBuilder("hello goodbye"); + assertEquals("hello", sb.substring(0, 5)); + assertEquals("hello goodbye".substring(0, 6), sb.substring(0, 6)); + + assertEquals("goodbye", sb.substring(6, 13)); + assertEquals("hello goodbye".substring(6, 13), sb.substring(6, 13)); + + assertEquals("goodbye", sb.substring(6, 20)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.substring(-1, 5)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.substring(15, 20)); + } + + @Test + public void testToCharArray() { + final StrBuilder sb = new StrBuilder(); + assertEquals(0, sb.toCharArray().length); + + char[] a = sb.toCharArray(); + assertNotNull(a, "toCharArray() result is null"); + assertEquals(0, a.length, "toCharArray() result is too large"); + + sb.append("junit"); + a = sb.toCharArray(); + assertEquals(5, a.length, "toCharArray() result incorrect length"); + assertArrayEquals("junit".toCharArray(), a, "toCharArray() result does not match"); + } + + @Test + public void testToCharArrayIntInt() { + final StrBuilder sb = new StrBuilder(); + assertEquals(0, sb.toCharArray(0, 0).length); + + sb.append("junit"); + char[] a = sb.toCharArray(0, 20); // too large test + assertEquals(5, a.length, "toCharArray(int,int) result incorrect length"); + assertArrayEquals("junit".toCharArray(), a, "toCharArray(int,int) result does not match"); + + a = sb.toCharArray(0, 4); + assertEquals(4, a.length, "toCharArray(int,int) result incorrect length"); + assertArrayEquals("juni".toCharArray(), a, "toCharArray(int,int) result does not match"); + + a = sb.toCharArray(0, 4); + assertEquals(4, a.length, "toCharArray(int,int) result incorrect length"); + assertArrayEquals("juni".toCharArray(), a, "toCharArray(int,int) result does not match"); + + a = sb.toCharArray(0, 1); + assertNotNull(a, "toCharArray(int,int) result is null"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.toCharArray(-1, 5)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.toCharArray(6, 5)); + } + + @Test + public void testToString() { + final StrBuilder sb = new StrBuilder("abc"); + assertEquals("abc", sb.toString()); + } + + @Test + public void testToStringBuffer() { + final StrBuilder sb = new StrBuilder(); + assertEquals(new StringBuffer().toString(), sb.toStringBuffer().toString()); + + sb.append("junit"); + assertEquals(new StringBuffer("junit").toString(), sb.toStringBuffer().toString()); + } + + @Test + public void testToStringBuilder() { + final StrBuilder sb = new StrBuilder(); + assertEquals(new StringBuilder().toString(), sb.toStringBuilder().toString()); + + sb.append("junit"); + assertEquals(new StringBuilder("junit").toString(), sb.toStringBuilder().toString()); + } + + @Test + public void testTrim() { + final StrBuilder sb = new StrBuilder(); + assertEquals("", sb.reverse().toString()); + + sb.clear().append(" \u0000 "); + assertEquals("", sb.trim().toString()); + + sb.clear().append(" \u0000 a b c"); + assertEquals("a b c", sb.trim().toString()); + + sb.clear().append("a b c \u0000 "); + assertEquals("a b c", sb.trim().toString()); + + sb.clear().append(" \u0000 a b c \u0000 "); + assertEquals("a b c", sb.trim().toString()); + + sb.clear().append("a b c"); + assertEquals("a b c", sb.trim().toString()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/StrLookupTest.java b/sources/src/test/java/org/apache/commons/text/StrLookupTest.java new file mode 100644 index 0000000..121ee51 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/StrLookupTest.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.ResourceBundle; + +import org.junit.jupiter.api.Test; + +/** + * Test class for {@link StrLookup}. + * + * @deprecated This class will be removed in 2.0. + */ +@Deprecated +public class StrLookupTest { + + @Test + public void testMapLookup() { + final Map map = new HashMap<>(); + map.put("key", "value"); + map.put("number", 2); + assertEquals("value", StrLookup.mapLookup(map).lookup("key")); + assertEquals("2", StrLookup.mapLookup(map).lookup("number")); + assertNull(StrLookup.mapLookup(map).lookup(null)); + assertNull(StrLookup.mapLookup(map).lookup("")); + assertNull(StrLookup.mapLookup(map).lookup("other")); + } + + @Test + public void testMapLookup_nullMap() { + final Map map = null; + assertNull(StrLookup.mapLookup(map).lookup(null)); + assertNull(StrLookup.mapLookup(map).lookup("")); + assertNull(StrLookup.mapLookup(map).lookup("any")); + } + + @Test + public void testNoneLookup() { + assertNull(StrLookup.noneLookup().lookup(null)); + assertNull(StrLookup.noneLookup().lookup("")); + assertNull(StrLookup.noneLookup().lookup("any")); + } + + @Test + public void testResourceBundleLookup() { + final ResourceBundle map = ResourceBundle.getBundle("org.apache.commons.text.example.testResourceBundleLookup"); + assertEquals("value", StrLookup.resourceBundleLookup(map).lookup("key")); + assertEquals("2", StrLookup.resourceBundleLookup(map).lookup("number")); + assertNull(StrLookup.resourceBundleLookup(map).lookup(null)); + assertNull(StrLookup.resourceBundleLookup(map).lookup("")); + assertNull(StrLookup.resourceBundleLookup(map).lookup("other")); + } + + @Test + public void testResourceBundleLookup_nullMap() { + final ResourceBundle resourceBundle = null; + assertNull(StrLookup.resourceBundleLookup(resourceBundle).lookup(null)); + assertNull(StrLookup.resourceBundleLookup(resourceBundle).lookup("")); + assertNull(StrLookup.resourceBundleLookup(resourceBundle).lookup("any")); + } + + @Test + public void testSystemPropertiesLookup() { + assertEquals(System.getProperty("os.name"), StrLookup.systemPropertiesLookup().lookup("os.name")); + assertNull(StrLookup.systemPropertiesLookup().lookup("")); + assertNull(StrLookup.systemPropertiesLookup().lookup("other")); + assertThrows(NullPointerException.class, () -> StrLookup.systemPropertiesLookup().lookup(null)); + } + + /** + * Tests that a lookup object for system properties can deal with a full + * replacement of the system properties object. This test is related to + * LANG-1055. + */ + @Test + public void testSystemPropertiesLookupReplacedProperties() { + final Properties oldProperties = System.getProperties(); + final String osName = "os.name"; + final String newOsName = oldProperties.getProperty(osName) + "_changed"; + + final StrLookup sysLookup = StrLookup.systemPropertiesLookup(); + final Properties newProps = new Properties(); + newProps.setProperty(osName, newOsName); + System.setProperties(newProps); + try { + assertEquals(newOsName, sysLookup.lookup(osName), "Changed properties not detected"); + } finally { + System.setProperties(oldProperties); + } + } + + /** + * Tests that a lookup object for system properties sees changes on system + * properties. This test is related to LANG-1141. + */ + @Test + public void testSystemPropertiesLookupUpdatedProperty() { + final String osName = "os.name"; + final String oldOs = System.getProperty(osName); + final String newOsName = oldOs + "_changed"; + + final StrLookup sysLookup = StrLookup.systemPropertiesLookup(); + System.setProperty(osName, newOsName); + try { + assertEquals(newOsName, sysLookup.lookup(osName), "Changed properties not detected"); + } finally { + System.setProperty(osName, oldOs); + } + } + + +} diff --git a/sources/src/test/java/org/apache/commons/text/StrMatcherTest.java b/sources/src/test/java/org/apache/commons/text/StrMatcherTest.java new file mode 100644 index 0000000..0b9ccf4 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/StrMatcherTest.java @@ -0,0 +1,201 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link StrMatcher}. + * + * @deprecated This class will be removed in 2.0. + */ +@Deprecated +public class StrMatcherTest { + + private static final char[] BUFFER1 = "0,1\t2 3\n\r\f\u0000'\"".toCharArray(); + + private static final char[] BUFFER2 = "abcdef".toCharArray(); + + + @Test + public void testCharMatcher_char() { + final StrMatcher matcher = StrMatcher.charMatcher('c'); + assertThat(matcher.isMatch(BUFFER2, 0)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER2, 1)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER2, 2)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER2, 3)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER2, 4)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER2, 5)).isEqualTo(0); + } + + @Test + public void testCharSetMatcher_charArray() { + final StrMatcher matcher = StrMatcher.charSetMatcher("ace".toCharArray()); + assertThat(matcher.isMatch(BUFFER2, 0)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER2, 1)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER2, 2)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER2, 3)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER2, 4)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER2, 5)).isEqualTo(0); + assertThat(StrMatcher.charSetMatcher()).isSameAs(StrMatcher.noneMatcher()); + assertThat(StrMatcher.charSetMatcher((char[]) null)).isSameAs(StrMatcher.noneMatcher()); + assertThat(StrMatcher.charSetMatcher("a".toCharArray()) instanceof StrMatcher.CharMatcher).isTrue(); + } + + @Test + public void testCharSetMatcher_String() { + final StrMatcher matcher = StrMatcher.charSetMatcher("ace"); + assertThat(matcher.isMatch(BUFFER2, 0)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER2, 1)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER2, 2)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER2, 3)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER2, 4)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER2, 5)).isEqualTo(0); + assertThat(StrMatcher.charSetMatcher("")).isSameAs(StrMatcher.noneMatcher()); + assertThat(StrMatcher.charSetMatcher((String) null)).isSameAs(StrMatcher.noneMatcher()); + assertThat(StrMatcher.charSetMatcher("a") instanceof StrMatcher.CharMatcher).isTrue(); + } + + @Test + public void testCommaMatcher() { + final StrMatcher matcher = StrMatcher.commaMatcher(); + assertThat(StrMatcher.commaMatcher()).isSameAs(matcher); + assertThat(matcher.isMatch(BUFFER1, 0)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 1)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER1, 2)).isEqualTo(0); + } + + @Test + public void testDoubleQuoteMatcher() { + final StrMatcher matcher = StrMatcher.doubleQuoteMatcher(); + assertThat(StrMatcher.doubleQuoteMatcher()).isSameAs(matcher); + assertThat(matcher.isMatch(BUFFER1, 11)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 12)).isEqualTo(1); + } + + @Test + public void testMatcherIndices() { + // remember that the API contract is tight for the isMatch() method + // all the onus is on the caller, so invalid inputs are not + // the concern of StrMatcher, and are not bugs + final StrMatcher matcher = StrMatcher.stringMatcher("bc"); + assertThat(matcher.isMatch(BUFFER2, 1, 1, BUFFER2.length)).isEqualTo(2); + assertThat(matcher.isMatch(BUFFER2, 1, 0, 3)).isEqualTo(2); + assertThat(matcher.isMatch(BUFFER2, 1, 0, 2)).isEqualTo(0); + } + + @Test + public void testNoneMatcher() { + final StrMatcher matcher = StrMatcher.noneMatcher(); + assertThat(StrMatcher.noneMatcher()).isSameAs(matcher); + assertThat(matcher.isMatch(BUFFER1, 0)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 1)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 2)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 3)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 4)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 5)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 6)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 7)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 8)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 9)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 10)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 11)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 12)).isEqualTo(0); + } + + @Test + public void testQuoteMatcher() { + final StrMatcher matcher = StrMatcher.quoteMatcher(); + assertThat(StrMatcher.quoteMatcher()).isSameAs(matcher); + assertThat(matcher.isMatch(BUFFER1, 10)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 11)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER1, 12)).isEqualTo(1); + } + + @Test + public void testSingleQuoteMatcher() { + final StrMatcher matcher = StrMatcher.singleQuoteMatcher(); + assertThat(StrMatcher.singleQuoteMatcher()).isSameAs(matcher); + assertThat(matcher.isMatch(BUFFER1, 10)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 11)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER1, 12)).isEqualTo(0); + } + + @Test + public void testSpaceMatcher() { + final StrMatcher matcher = StrMatcher.spaceMatcher(); + assertThat(StrMatcher.spaceMatcher()).isSameAs(matcher); + assertThat(matcher.isMatch(BUFFER1, 4)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 5)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER1, 6)).isEqualTo(0); + } + + @Test + public void testSplitMatcher() { + final StrMatcher matcher = StrMatcher.splitMatcher(); + assertThat(StrMatcher.splitMatcher()).isSameAs(matcher); + assertThat(matcher.isMatch(BUFFER1, 2)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 3)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER1, 4)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 5)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER1, 6)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 7)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER1, 8)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER1, 9)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER1, 10)).isEqualTo(0); + } + + @Test + public void testStringMatcher_String() { + final StrMatcher matcher = StrMatcher.stringMatcher("bc"); + assertThat(matcher.isMatch(BUFFER2, 0)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER2, 1)).isEqualTo(2); + assertThat(matcher.isMatch(BUFFER2, 2)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER2, 3)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER2, 4)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER2, 5)).isEqualTo(0); + assertThat(StrMatcher.stringMatcher("")).isSameAs(StrMatcher.noneMatcher()); + assertThat(StrMatcher.stringMatcher((String) null)).isSameAs(StrMatcher.noneMatcher()); + } + + @Test + public void testTabMatcher() { + final StrMatcher matcher = StrMatcher.tabMatcher(); + assertThat(StrMatcher.tabMatcher()).isSameAs(matcher); + assertThat(matcher.isMatch(BUFFER1, 2)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 3)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER1, 4)).isEqualTo(0); + } + + @Test + public void testTrimMatcher() { + final StrMatcher matcher = StrMatcher.trimMatcher(); + assertThat(StrMatcher.trimMatcher()).isSameAs(matcher); + assertThat(matcher.isMatch(BUFFER1, 2)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 3)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER1, 4)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 5)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER1, 6)).isEqualTo(0); + assertThat(matcher.isMatch(BUFFER1, 7)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER1, 8)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER1, 9)).isEqualTo(1); + assertThat(matcher.isMatch(BUFFER1, 10)).isEqualTo(1); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/StrSubstitutorTest.java b/sources/src/test/java/org/apache/commons/text/StrSubstitutorTest.java new file mode 100644 index 0000000..858b43b --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/StrSubstitutorTest.java @@ -0,0 +1,793 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text; + +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.apache.commons.lang3.mutable.MutableObject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test class for {@link StrSubstitutor}. + * + * @deprecated This class will be removed in 2.0. + */ +@Deprecated +public class StrSubstitutorTest { + + private Map values; + + private void doTestNoReplace(final String replaceTemplate) { + final StrSubstitutor sub = new StrSubstitutor(values); + + if (replaceTemplate == null) { + assertNull(sub.replace((String) null)); + assertNull(sub.replace((String) null, 0, 100)); + assertNull(sub.replace((char[]) null)); + assertNull(sub.replace((char[]) null, 0, 100)); + assertNull(sub.replace((StringBuffer) null)); + assertNull(sub.replace((StringBuffer) null, 0, 100)); + assertNull(sub.replace((StrBuilder) null)); + assertNull(sub.replace((StrBuilder) null, 0, 100)); + assertNull(sub.replace((Object) null)); + assertFalse(sub.replaceIn((StringBuffer) null)); + assertFalse(sub.replaceIn((StringBuffer) null, 0, 100)); + assertFalse(sub.replaceIn((StrBuilder) null)); + assertFalse(sub.replaceIn((StrBuilder) null, 0, 100)); + } else { + assertEquals(replaceTemplate, sub.replace(replaceTemplate)); + final StrBuilder bld = new StrBuilder(replaceTemplate); + assertFalse(sub.replaceIn(bld)); + assertEquals(replaceTemplate, bld.toString()); + } + } + + private void doTestReplace(final String expectedResult, final String replaceTemplate, final boolean substring) { + final StrSubstitutor sub = new StrSubstitutor(values); + doTestReplace(sub, expectedResult, replaceTemplate, substring); + } + + private void doTestReplace(final StrSubstitutor sub, final String expectedResult, final String replaceTemplate, + final boolean substring) { + final String expectedShortResult = expectedResult.substring(1, expectedResult.length() - 1); + + // replace using String + assertEquals(expectedResult, sub.replace(replaceTemplate)); + if (substring) { + assertEquals(expectedShortResult, sub.replace(replaceTemplate, 1, replaceTemplate.length() - 2)); + } + + // replace using char[] + final char[] chars = replaceTemplate.toCharArray(); + assertEquals(expectedResult, sub.replace(chars)); + if (substring) { + assertEquals(expectedShortResult, sub.replace(chars, 1, chars.length - 2)); + } + + // replace using StringBuffer + StringBuffer buf = new StringBuffer(replaceTemplate); + assertEquals(expectedResult, sub.replace(buf)); + if (substring) { + assertEquals(expectedShortResult, sub.replace(buf, 1, buf.length() - 2)); + } + + // replace using StringBuilder + StringBuilder builder = new StringBuilder(replaceTemplate); + assertEquals(expectedResult, sub.replace(builder)); + if (substring) { + assertEquals(expectedShortResult, sub.replace(builder, 1, builder.length() - 2)); + } + + // replace using StrBuilder + StrBuilder bld = new StrBuilder(replaceTemplate); + assertEquals(expectedResult, sub.replace(bld)); + if (substring) { + assertEquals(expectedShortResult, sub.replace(bld, 1, bld.length() - 2)); + } + + // replace using object + final MutableObject obj = new MutableObject<>(replaceTemplate); // toString returns template + assertEquals(expectedResult, sub.replace(obj)); + + // replace in StringBuffer + buf = new StringBuffer(replaceTemplate); + assertTrue(sub.replaceIn(buf)); + assertEquals(expectedResult, buf.toString()); + if (substring) { + buf = new StringBuffer(replaceTemplate); + assertTrue(sub.replaceIn(buf, 1, buf.length() - 2)); + assertEquals(expectedResult, buf.toString()); // expect full result as remainder is untouched + } + + // replace in StringBuilder + builder = new StringBuilder(replaceTemplate); + assertTrue(sub.replaceIn(builder)); + assertEquals(expectedResult, builder.toString()); + if (substring) { + builder = new StringBuilder(replaceTemplate); + assertTrue(sub.replaceIn(builder, 1, builder.length() - 2)); + assertEquals(expectedResult, builder.toString()); // expect full result as remainder is untouched + } + + // replace in StrBuilder + bld = new StrBuilder(replaceTemplate); + assertTrue(sub.replaceIn(bld)); + assertEquals(expectedResult, bld.toString()); + if (substring) { + bld = new StrBuilder(replaceTemplate); + assertTrue(sub.replaceIn(bld, 1, bld.length() - 2)); + assertEquals(expectedResult, bld.toString()); // expect full result as remainder is untouched + } + } + + @BeforeEach + public void setUp() throws Exception { + values = new HashMap<>(); + values.put("animal", "quick brown fox"); + values.put("target", "lazy dog"); + } + + @AfterEach + public void tearDown() throws Exception { + values = null; + } + + /** + * Tests constructor. + */ + @Test + public void testConstructorMapFull() { + final Map map = new HashMap<>(); + map.put("name", "commons"); + StrSubstitutor sub = new StrSubstitutor(map, "<", ">", '!'); + assertEquals("Hi < commons", sub.replace("Hi !< ")); + sub = new StrSubstitutor(map, "<", ">", '!', "||"); + assertEquals("Hi < commons", sub.replace("Hi !< ")); + } + + /** + * Tests constructor. + */ + @Test + public void testConstructorMapPrefixSuffix() { + final Map map = new HashMap<>(); + map.put("name", "commons"); + final StrSubstitutor sub = new StrSubstitutor(map, "<", ">"); + assertEquals("Hi < commons", sub.replace("Hi $< ")); + } + + /** + * Tests constructor. + */ + @Test + public void testConstructorNoArgs() { + final StrSubstitutor sub = new StrSubstitutor(); + assertEquals("Hi ${name}", sub.replace("Hi ${name}")); + } + + @Test + public void testCreatesStrSubstitutorTakingStrLookupAndCallsReplaceTakingTwoAndThreeInts() { + final Map map = new HashMap<>(); + final StrLookup.MapStrLookup strLookupMapStrLookup = new StrLookup.MapStrLookup<>(map); + final StrSubstitutor strSubstitutor = new StrSubstitutor(strLookupMapStrLookup); + + assertNull(strSubstitutor.replace((CharSequence) null, 0, 0)); + assertEquals('$', strSubstitutor.getEscapeChar()); + } + + /** + * Tests a cyclic replace operation. + * The cycle should be detected and cause an exception to be thrown. + */ + @Test + public void testCyclicReplacement() { + final Map map = new HashMap<>(); + map.put("animal", "${critter}"); + map.put("target", "${pet}"); + map.put("pet", "${petCharacteristic} dog"); + map.put("petCharacteristic", "lazy"); + map.put("critter", "${critterSpeed} ${critterColor} ${critterType}"); + map.put("critterSpeed", "quick"); + map.put("critterColor", "brown"); + map.put("critterType", "${animal}"); + final StrSubstitutor sub = new StrSubstitutor(map); + assertThrows(IllegalStateException.class, () -> sub.replace("The ${animal} jumps over the ${target}.")); + + // also check even when default value is set. + map.put("critterType", "${animal:-fox}"); + assertThrows(IllegalStateException.class, + () -> new StrSubstitutor(map).replace("The ${animal} jumps over the ${target}.")); + } + + @Test + public void testDefaultValueDelimiters() { + final Map map = new HashMap<>(); + map.put("animal", "fox"); + map.put("target", "dog"); + + StrSubstitutor sub = new StrSubstitutor(map, "${", "}", '$'); + assertEquals("The fox jumps over the lazy dog. 1234567890.", + sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number:-1234567890}.")); + + sub = new StrSubstitutor(map, "${", "}", '$', "?:"); + assertEquals("The fox jumps over the lazy dog. 1234567890.", + sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number?:1234567890}.")); + + sub = new StrSubstitutor(map, "${", "}", '$', "||"); + assertEquals("The fox jumps over the lazy dog. 1234567890.", + sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number||1234567890}.")); + + sub = new StrSubstitutor(map, "${", "}", '$', "!"); + assertEquals("The fox jumps over the lazy dog. 1234567890.", + sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number!1234567890}.")); + + sub = new StrSubstitutor(map, "${", "}", '$', ""); + sub.setValueDelimiterMatcher(null); + assertEquals("The fox jumps over the lazy dog. ${undefined.number!1234567890}.", + sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number!1234567890}.")); + + sub = new StrSubstitutor(map, "${", "}", '$'); + sub.setValueDelimiterMatcher(null); + assertEquals("The fox jumps over the lazy dog. ${undefined.number!1234567890}.", + sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number!1234567890}.")); + } + + @Test + public void testDisableSubstitutionInValues() { + final StrSubstitutor sub = new StrSubstitutor(values); + sub.setDisableSubstitutionInValues(true); + values.put("animal", "${critter}"); + values.put("target", "${pet}"); + values.put("pet", "${petCharacteristic} dog"); + values.put("petCharacteristic", "lazy"); + values.put("critter", "${critterSpeed} ${critterColor} ${critterType}"); + values.put("critterSpeed", "quick"); + values.put("critterColor", "brown"); + values.put("critterType", "fox"); + doTestReplace(sub, "The ${critter} jumps over the ${pet}.", "The ${animal} jumps over the ${target}.", true); + } + + /** + * Tests get set. + */ + @Test + public void testGetSetEscape() { + final StrSubstitutor sub = new StrSubstitutor(); + assertEquals('$', sub.getEscapeChar()); + sub.setEscapeChar('<'); + assertEquals('<', sub.getEscapeChar()); + } + + /** + * Tests get set. + */ + @Test + public void testGetSetPrefix() { + final StrSubstitutor sub = new StrSubstitutor(); + assertTrue(sub.getVariablePrefixMatcher() instanceof StrMatcher.StringMatcher); + sub.setVariablePrefix('<'); + assertTrue(sub.getVariablePrefixMatcher() instanceof StrMatcher.CharMatcher); + + sub.setVariablePrefix("<<"); + assertTrue(sub.getVariablePrefixMatcher() instanceof StrMatcher.StringMatcher); + assertThrows(IllegalArgumentException.class, () -> sub.setVariablePrefix((String) null)); + assertTrue(sub.getVariablePrefixMatcher() instanceof StrMatcher.StringMatcher); + + final StrMatcher matcher = StrMatcher.commaMatcher(); + sub.setVariablePrefixMatcher(matcher); + assertSame(matcher, sub.getVariablePrefixMatcher()); + assertThrows(IllegalArgumentException.class, () -> sub.setVariablePrefixMatcher((StrMatcher) null)); + assertSame(matcher, sub.getVariablePrefixMatcher()); + } + + /** + * Tests get set. + */ + @Test + public void testGetSetSuffix() { + final StrSubstitutor sub = new StrSubstitutor(); + assertTrue(sub.getVariableSuffixMatcher() instanceof StrMatcher.StringMatcher); + sub.setVariableSuffix('<'); + assertTrue(sub.getVariableSuffixMatcher() instanceof StrMatcher.CharMatcher); + + sub.setVariableSuffix("<<"); + assertTrue(sub.getVariableSuffixMatcher() instanceof StrMatcher.StringMatcher); + assertThrows(IllegalArgumentException.class, () -> sub.setVariableSuffix((String) null)); + assertTrue(sub.getVariableSuffixMatcher() instanceof StrMatcher.StringMatcher); + + final StrMatcher matcher = StrMatcher.commaMatcher(); + sub.setVariableSuffixMatcher(matcher); + assertSame(matcher, sub.getVariableSuffixMatcher()); + assertThrows(IllegalArgumentException.class, () -> sub.setVariableSuffixMatcher((StrMatcher) null)); + assertSame(matcher, sub.getVariableSuffixMatcher()); + } + + /** + * Tests get set. + */ + @Test + public void testGetSetValueDelimiter() { + final StrSubstitutor sub = new StrSubstitutor(); + assertTrue(sub.getValueDelimiterMatcher() instanceof StrMatcher.StringMatcher); + sub.setValueDelimiter(':'); + assertTrue(sub.getValueDelimiterMatcher() instanceof StrMatcher.CharMatcher); + + sub.setValueDelimiter("||"); + assertTrue(sub.getValueDelimiterMatcher() instanceof StrMatcher.StringMatcher); + sub.setValueDelimiter((String) null); + assertNull(sub.getValueDelimiterMatcher()); + + final StrMatcher matcher = StrMatcher.commaMatcher(); + sub.setValueDelimiterMatcher(matcher); + assertSame(matcher, sub.getValueDelimiterMatcher()); + sub.setValueDelimiterMatcher((StrMatcher) null); + assertNull(sub.getValueDelimiterMatcher()); + } + + /** + * Test for LANG-1055: StrSubstitutor.replaceSystemProperties does not work consistently + */ + @Test + public void testLANG1055() { + System.setProperty("test_key", "test_value"); + + final String expected = StrSubstitutor.replace("test_key=${test_key}", System.getProperties()); + final String actual = StrSubstitutor.replaceSystemProperties("test_key=${test_key}"); + assertEquals(expected, actual); + } + + /** + * Tests adjacent keys. + */ + @Test + public void testReplaceAdjacentAtEnd() { + values.put("code", "GBP"); + values.put("amount", "12.50"); + final StrSubstitutor sub = new StrSubstitutor(values); + assertEquals("Amount is GBP12.50", sub.replace("Amount is ${code}${amount}")); + } + + /** + * Tests adjacent keys. + */ + @Test + public void testReplaceAdjacentAtStart() { + values.put("code", "GBP"); + values.put("amount", "12.50"); + final StrSubstitutor sub = new StrSubstitutor(values); + assertEquals("GBP12.50 charged", sub.replace("${code}${amount} charged")); + } + + /** + * Tests key replace changing map after initialization (not recommended). + */ + @Test + public void testReplaceChangedMap() { + final StrSubstitutor sub = new StrSubstitutor(values); + values.put("target", "moon"); + assertEquals("The quick brown fox jumps over the moon.", + sub.replace("The ${animal} jumps over the ${target}.")); + } + + /** + * Tests complex escaping. + */ + @Test + public void testReplaceComplexEscaping() { + doTestReplace("The ${quick brown fox} jumps over the lazy dog.", + "The $${${animal}} jumps over the ${target}.", true); + doTestReplace("The ${quick brown fox} jumps over the lazy dog. ${1234567890}.", + "The $${${animal}} jumps over the ${target}. $${${undefined.number:-1234567890}}.", true); + } + + /** + * Tests replace with null. + */ + @Test + public void testReplaceEmpty() { + doTestNoReplace(""); + } + + /** + * Tests when no variable name. + */ + @Test + public void testReplaceEmptyKeys() { + doTestReplace("The ${} jumps over the lazy dog.", "The ${} jumps over the ${target}.", true); + doTestReplace("The animal jumps over the lazy dog.", "The ${:-animal} jumps over the ${target}.", true); + } + + /** + * Tests escaping. + */ + @Test + public void testReplaceEscaping() { + doTestReplace("The ${animal} jumps over the lazy dog.", "The $${animal} jumps over the ${target}.", true); + } + + /** + * Tests when no incomplete prefix. + */ + @Test + public void testReplaceIncompletePrefix() { + doTestReplace("The {animal} jumps over the lazy dog.", "The {animal} jumps over the ${target}.", true); + } + + @Test + public void testReplaceInTakingStringBufferWithNonNull() { + final StrSubstitutor strSubstitutor = + new StrSubstitutor(new HashMap(), "WV@i#y?N*[", "WV@i#y?N*[", '*'); + + assertFalse(strSubstitutor.isPreserveEscapes()); + assertFalse(strSubstitutor.replaceIn(new StringBuffer("WV@i#y?N*["))); + assertEquals('*', strSubstitutor.getEscapeChar()); + } + + @Test + public void testReplaceInTakingStringBuilderWithNonNull() { + final StrLookup strLookup = StrLookup.systemPropertiesLookup(); + final StrSubstitutor strSubstitutor = new StrSubstitutor(strLookup, "b map = new HashMap<>(); + final StrSubstitutor strSubstitutor = new StrSubstitutor(map, "", "", 'T', "K+<'f"); + + assertFalse(strSubstitutor.replaceIn((StringBuilder) null)); + } + + @Test + public void testReplaceInTakingTwoAndThreeIntsReturningFalse() { + final Map hashMap = new HashMap<>(); + final StrLookup.MapStrLookup strLookupMapStrLookup = new StrLookup.MapStrLookup<>(hashMap); + final StrMatcher strMatcher = StrMatcher.tabMatcher(); + final StrSubstitutor strSubstitutor = + new StrSubstitutor(strLookupMapStrLookup, strMatcher, strMatcher, 'b', strMatcher); + + assertFalse(strSubstitutor.replaceIn((StringBuilder) null, 1315, -1369)); + assertEquals('b', strSubstitutor.getEscapeChar()); + assertFalse(strSubstitutor.isPreserveEscapes()); + } + + /** + * Tests whether a variable can be replaced in a variable name. + */ + @Test + public void testReplaceInVariable() { + values.put("animal.1", "fox"); + values.put("animal.2", "mouse"); + values.put("species", "2"); + final StrSubstitutor sub = new StrSubstitutor(values); + sub.setEnableSubstitutionInVariables(true); + assertEquals( + "The mouse jumps over the lazy dog.", + sub.replace("The ${animal.${species}} jumps over the ${target}.")); + values.put("species", "1"); + assertEquals( + "The fox jumps over the lazy dog.", + sub.replace("The ${animal.${species}} jumps over the ${target}.")); + assertEquals( + "The fox jumps over the lazy dog.", + sub.replace("The ${unknown.animal.${unknown.species:-1}:-fox} " + + "jumps over the ${unknow.target:-lazy dog}.")); + } + + /** + * Tests whether substitution in variable names is disabled per default. + */ + @Test + public void testReplaceInVariableDisabled() { + values.put("animal.1", "fox"); + values.put("animal.2", "mouse"); + values.put("species", "2"); + final StrSubstitutor sub = new StrSubstitutor(values); + assertEquals( + "The ${animal.${species}} jumps over the lazy dog.", + sub.replace("The ${animal.${species}} jumps over the ${target}.")); + assertEquals( + "The ${animal.${species:-1}} jumps over the lazy dog.", + sub.replace("The ${animal.${species:-1}} jumps over the ${target}.")); + } + + /** + * Tests complex and recursive substitution in variable names. + */ + @Test + public void testReplaceInVariableRecursive() { + values.put("animal.2", "brown fox"); + values.put("animal.1", "white mouse"); + values.put("color", "white"); + values.put("species.white", "1"); + values.put("species.brown", "2"); + final StrSubstitutor sub = new StrSubstitutor(values); + sub.setEnableSubstitutionInVariables(true); + assertEquals( + "The white mouse jumps over the lazy dog.", + sub.replace("The ${animal.${species.${color}}} jumps over the ${target}.")); + assertEquals( + "The brown fox jumps over the lazy dog.", + sub.replace("The ${animal.${species.${unknownColor:-brown}}} jumps over the ${target}.")); + } + + /** + * Tests when no prefix or suffix. + */ + @Test + public void testReplaceNoPrefixNoSuffix() { + doTestReplace("The animal jumps over the lazy dog.", "The animal jumps over the ${target}.", true); + } + + /** + * Tests when suffix but no prefix. + */ + @Test + public void testReplaceNoPrefixSuffix() { + doTestReplace("The animal} jumps over the lazy dog.", "The animal} jumps over the ${target}.", true); + } + + /** + * Tests replace with no variables. + */ + @Test + public void testReplaceNoVariables() { + doTestNoReplace("The balloon arrived."); + } + + /** + * Tests replace with null. + */ + @Test + public void testReplaceNull() { + doTestNoReplace(null); + } + + /** + * Tests simple key replace. + */ + @Test + public void testReplacePartialString_noReplace() { + final StrSubstitutor sub = new StrSubstitutor(); + assertEquals("${animal} jumps", sub.replace("The ${animal} jumps over the ${target}.", 4, 15)); + } + + /** + * Tests when prefix but no suffix. + */ + @Test + public void testReplacePrefixNoSuffix() { + doTestReplace("The ${animal jumps over the ${target} lazy dog.", + "The ${animal jumps over the ${target} ${target}.", true); + } + + /** + * Tests simple recursive replace. + */ + @Test + public void testReplaceRecursive() { + values.put("animal", "${critter}"); + values.put("target", "${pet}"); + values.put("pet", "${petCharacteristic} dog"); + values.put("petCharacteristic", "lazy"); + values.put("critter", "${critterSpeed} ${critterColor} ${critterType}"); + values.put("critterSpeed", "quick"); + values.put("critterColor", "brown"); + values.put("critterType", "fox"); + doTestReplace("The quick brown fox jumps over the lazy dog.", "The ${animal} jumps over the ${target}.", true); + + values.put("pet", "${petCharacteristicUnknown:-lazy} dog"); + doTestReplace("The quick brown fox jumps over the lazy dog.", "The ${animal} jumps over the ${target}.", true); + } + + /** + * Tests simple key replace. + */ + @Test + public void testReplaceSimple() { + doTestReplace("The quick brown fox jumps over the lazy dog.", "The ${animal} jumps over the ${target}.", true); + } + + /** + * Tests simple key replace. + */ + @Test + public void testReplaceSolo() { + doTestReplace("quick brown fox", "${animal}", false); + } + + /** + * Tests escaping. + */ + @Test + public void testReplaceSoloEscaping() { + doTestReplace("${animal}", "$${animal}", false); + } + + @Test + public void testReplaceTakingCharSequenceReturningNull() { + final StrSubstitutor strSubstitutor = new StrSubstitutor((StrLookup) null); + + assertNull(strSubstitutor.replace((CharSequence) null)); + assertFalse(strSubstitutor.isPreserveEscapes()); + assertEquals('$', strSubstitutor.getEscapeChar()); + } + + @Test + public void testReplaceTakingThreeArgumentsThrowsNullPointerException() { + assertThatNullPointerException().isThrownBy(() -> StrSubstitutor.replace(null, (Properties) null)); + } + + /** + * Tests replace creates output same as input. + */ + @Test + public void testReplaceToIdentical() { + values.put("animal", "$${${thing}}"); + values.put("thing", "animal"); + doTestReplace("The ${animal} jumps.", "The ${animal} jumps.", true); + } + + /** + * Tests unknown key replace. + */ + @Test + public void testReplaceUnknownKey() { + doTestReplace("The ${person} jumps over the lazy dog.", "The ${person} jumps over the ${target}.", true); + doTestReplace("The ${person} jumps over the lazy dog. 1234567890.", + "The ${person} jumps over the ${target}. ${undefined.number:-1234567890}.", true); + } + + /** + * Tests interpolation with weird boundary patterns. + */ + @Test + public void testReplaceWeirdPattens() { + doTestNoReplace(""); + doTestNoReplace("${}"); + doTestNoReplace("${ }"); + doTestNoReplace("${\t}"); + doTestNoReplace("${\n}"); + doTestNoReplace("${\b}"); + doTestNoReplace("${"); + doTestNoReplace("$}"); + doTestNoReplace("}"); + doTestNoReplace("${}$"); + doTestNoReplace("${${"); + doTestNoReplace("${${}}"); + doTestNoReplace("${$${}}"); + doTestNoReplace("${$$${}}"); + doTestNoReplace("${$$${$}}"); + doTestNoReplace("${${}}"); + doTestNoReplace("${${ }}"); + } + + /** + * Tests protected. + */ + @Test + public void testResolveVariable() { + final StrBuilder builder = new StrBuilder("Hi ${name}!"); + final Map map = new HashMap<>(); + map.put("name", "commons"); + final StrSubstitutor sub = new StrSubstitutor(map) { + @Override + protected String resolveVariable(final String variableName, final StrBuilder buf, final int startPos, + final int endPos) { + assertEquals("name", variableName); + assertSame(builder, buf); + assertEquals(3, startPos); + assertEquals(10, endPos); + return "jakarta"; + } + }; + sub.replaceIn(builder); + assertEquals("Hi jakarta!", builder.toString()); + } + + @Test + public void testSamePrefixAndSuffix() { + final Map map = new HashMap<>(); + map.put("greeting", "Hello"); + map.put(" there ", "XXX"); + map.put("name", "commons"); + assertEquals("Hi commons!", StrSubstitutor.replace("Hi @name@!", map, "@", "@")); + assertEquals("Hello there commons!", StrSubstitutor.replace("@greeting@ there @name@!", map, "@", "@")); + } + + /** + * Tests static. + */ + @Test + public void testStaticReplace() { + final Map map = new HashMap<>(); + map.put("name", "commons"); + assertEquals("Hi commons!", StrSubstitutor.replace("Hi ${name}!", map)); + } + + /** + * Tests static. + */ + @Test + public void testStaticReplacePrefixSuffix() { + final Map map = new HashMap<>(); + map.put("name", "commons"); + assertEquals("Hi commons!", StrSubstitutor.replace("Hi !", map, "<", ">")); + } + + /** + * Tests interpolation with system properties. + */ + @Test + public void testStaticReplaceSystemProperties() { + final StrBuilder buf = new StrBuilder(); + buf.append("Hi ").append(System.getProperty("user.name")); + buf.append(", you are working with "); + buf.append(System.getProperty("os.name")); + buf.append(", your home directory is "); + buf.append(System.getProperty("user.home")).append('.'); + assertEquals(buf.toString(), StrSubstitutor.replaceSystemProperties("Hi ${user.name}, you are " + + "working with ${os.name}, your home " + + "directory is ${user.home}.")); + } + + /** + * Test the replace of a properties object + */ + @Test + public void testSubstituteDefaultProperties() { + final String org = "${doesnotwork}"; + System.setProperty("doesnotwork", "It works!"); + + // create a new Properties object with the System.getProperties as default + final Properties props = new Properties(System.getProperties()); + + assertEquals("It works!", StrSubstitutor.replace(org, props)); + } + + @Test + public void testSubstitutePreserveEscape() { + final String org = "${not-escaped} $${escaped}"; + final Map map = new HashMap<>(); + map.put("not-escaped", "value"); + + final StrSubstitutor sub = new StrSubstitutor(map, "${", "}", '$'); + assertFalse(sub.isPreserveEscapes()); + assertEquals("value ${escaped}", sub.replace(org)); + + sub.setPreserveEscapes(true); + assertTrue(sub.isPreserveEscapes()); + assertEquals("value $${escaped}", sub.replace(org)); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/StrTokenizerTest.java b/sources/src/test/java/org/apache/commons/text/StrTokenizerTest.java new file mode 100644 index 0000000..5ec97b6 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/StrTokenizerTest.java @@ -0,0 +1,915 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; + +import org.apache.commons.lang3.ArrayUtils; +import org.junit.jupiter.api.Test; + +/** + * Unit test for {@link StrTokenizer}. + * + * @deprecated This class will be removed in 2.0. + */ +@Deprecated +public class StrTokenizerTest { + + private static final String CSV_SIMPLE_FIXTURE = "A,b,c"; + + private static final String TSV_SIMPLE_FIXTURE = "A\tb\tc"; + + private void checkClone(final StrTokenizer tokenizer) { + assertNotSame(StrTokenizer.getCSVInstance(), tokenizer); + assertNotSame(StrTokenizer.getTSVInstance(), tokenizer); + } + + @Test + public void test1() { + + final String input = "a;b;c;\"d;\"\"e\";f; ; ; "; + final StrTokenizer tok = new StrTokenizer(input); + tok.setDelimiterChar(';'); + tok.setQuoteChar('"'); + tok.setIgnoredMatcher(StrMatcher.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + final String[] tokens = tok.getTokenArray(); + + final String[] expected = {"a", "b", "c", "d;\"e", "f", "", "", ""}; + + assertEquals(expected.length, tokens.length, Arrays.toString(tokens)); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], tokens[i], + "token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'"); + } + + } + + @Test + public void test2() { + + final String input = "a;b;c ;\"d;\"\"e\";f; ; ;"; + final StrTokenizer tok = new StrTokenizer(input); + tok.setDelimiterChar(';'); + tok.setQuoteChar('"'); + tok.setIgnoredMatcher(StrMatcher.noneMatcher()); + tok.setIgnoreEmptyTokens(false); + final String[] tokens = tok.getTokenArray(); + + final String[] expected = {"a", "b", "c ", "d;\"e", "f", " ", " ", ""}; + + assertEquals(expected.length, tokens.length, Arrays.toString(tokens)); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], tokens[i], + "token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'"); + } + + } + + @Test + public void test3() { + + final String input = "a;b; c;\"d;\"\"e\";f; ; ;"; + final StrTokenizer tok = new StrTokenizer(input); + tok.setDelimiterChar(';'); + tok.setQuoteChar('"'); + tok.setIgnoredMatcher(StrMatcher.noneMatcher()); + tok.setIgnoreEmptyTokens(false); + final String[] tokens = tok.getTokenArray(); + + final String[] expected = {"a", "b", " c", "d;\"e", "f", " ", " ", ""}; + + assertEquals(expected.length, tokens.length, Arrays.toString(tokens)); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], tokens[i], + "token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'"); + } + + } + + @Test + public void test4() { + + final String input = "a;b; c;\"d;\"\"e\";f; ; ;"; + final StrTokenizer tok = new StrTokenizer(input); + tok.setDelimiterChar(';'); + tok.setQuoteChar('"'); + tok.setIgnoredMatcher(StrMatcher.trimMatcher()); + tok.setIgnoreEmptyTokens(true); + final String[] tokens = tok.getTokenArray(); + + final String[] expected = {"a", "b", "c", "d;\"e", "f"}; + + assertEquals(expected.length, tokens.length, Arrays.toString(tokens)); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], tokens[i], + "token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'"); + } + + } + + @Test + public void test5() { + + final String input = "a;b; c;\"d;\"\"e\";f; ; ;"; + final StrTokenizer tok = new StrTokenizer(input); + tok.setDelimiterChar(';'); + tok.setQuoteChar('"'); + tok.setIgnoredMatcher(StrMatcher.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + final String[] tokens = tok.getTokenArray(); + + final String[] expected = {"a", "b", "c", "d;\"e", "f", null, null, null}; + + assertEquals(expected.length, tokens.length, Arrays.toString(tokens)); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], tokens[i], + "token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'"); + } + + } + + @Test + public void test6() { + + final String input = "a;b; c;\"d;\"\"e\";f; ; ;"; + final StrTokenizer tok = new StrTokenizer(input); + tok.setDelimiterChar(';'); + tok.setQuoteChar('"'); + tok.setIgnoredMatcher(StrMatcher.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + // tok.setTreatingEmptyAsNull(true); + final String[] tokens = tok.getTokenArray(); + + final String[] expected = {"a", "b", " c", "d;\"e", "f", null, null, null}; + + int nextCount = 0; + while (tok.hasNext()) { + tok.next(); + nextCount++; + } + + int prevCount = 0; + while (tok.hasPrevious()) { + tok.previous(); + prevCount++; + } + + assertEquals(expected.length, tokens.length, Arrays.toString(tokens)); + assertEquals(nextCount, expected.length, "could not cycle through entire token list using the 'hasNext' and 'next' methods"); + assertEquals(prevCount, expected.length, "could not cycle through entire token list using the 'hasPrevious' and 'previous' methods"); + } + + @Test + public void test7() { + + final String input = "a b c \"d e\" f "; + final StrTokenizer tok = new StrTokenizer(input); + tok.setDelimiterMatcher(StrMatcher.spaceMatcher()); + tok.setQuoteMatcher(StrMatcher.doubleQuoteMatcher()); + tok.setIgnoredMatcher(StrMatcher.noneMatcher()); + tok.setIgnoreEmptyTokens(false); + final String[] tokens = tok.getTokenArray(); + + final String[] expected = {"a", "", "", "b", "c", "d e", "f", ""}; + + assertEquals(expected.length, tokens.length, Arrays.toString(tokens)); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], tokens[i], + "token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'"); + } + + } + + @Test + public void test8() { + + final String input = "a b c \"d e\" f "; + final StrTokenizer tok = new StrTokenizer(input); + tok.setDelimiterMatcher(StrMatcher.spaceMatcher()); + tok.setQuoteMatcher(StrMatcher.doubleQuoteMatcher()); + tok.setIgnoredMatcher(StrMatcher.noneMatcher()); + tok.setIgnoreEmptyTokens(true); + final String[] tokens = tok.getTokenArray(); + + final String[] expected = {"a", "b", "c", "d e", "f"}; + + assertEquals(expected.length, tokens.length, Arrays.toString(tokens)); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], tokens[i], + "token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'"); + } + + } + + @Test + public void testBasic1() { + final String input = "a b c"; + final StrTokenizer tok = new StrTokenizer(input); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasic2() { + final String input = "a \nb\fc"; + final StrTokenizer tok = new StrTokenizer(input); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasic3() { + final String input = "a \nb\u0001\fc"; + final StrTokenizer tok = new StrTokenizer(input); + assertEquals("a", tok.next()); + assertEquals("b\u0001", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasic4() { + final String input = "a \"b\" c"; + final StrTokenizer tok = new StrTokenizer(input); + assertEquals("a", tok.next()); + assertEquals("\"b\"", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasic5() { + final String input = "a:b':c"; + final StrTokenizer tok = new StrTokenizer(input, ':', '\''); + assertEquals("a", tok.next()); + assertEquals("b'", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicDelim1() { + final String input = "a:b:c"; + final StrTokenizer tok = new StrTokenizer(input, ':'); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicDelim2() { + final String input = "a:b:c"; + final StrTokenizer tok = new StrTokenizer(input, ','); + assertEquals("a:b:c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicEmpty1() { + final String input = "a b c"; + final StrTokenizer tok = new StrTokenizer(input); + tok.setIgnoreEmptyTokens(false); + assertEquals("a", tok.next()); + assertEquals("", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicEmpty2() { + final String input = "a b c"; + final StrTokenizer tok = new StrTokenizer(input); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertNull(tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicIgnoreTrimmed1() { + final String input = "a: bIGNOREc : "; + final StrTokenizer tok = new StrTokenizer(input, ':'); + tok.setIgnoredMatcher(StrMatcher.stringMatcher("IGNORE")); + tok.setTrimmerMatcher(StrMatcher.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("bc", tok.next()); + assertNull(tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicIgnoreTrimmed2() { + final String input = "IGNOREaIGNORE: IGNORE bIGNOREc IGNORE : IGNORE "; + final StrTokenizer tok = new StrTokenizer(input, ':'); + tok.setIgnoredMatcher(StrMatcher.stringMatcher("IGNORE")); + tok.setTrimmerMatcher(StrMatcher.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("bc", tok.next()); + assertNull(tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicIgnoreTrimmed3() { + final String input = "IGNOREaIGNORE: IGNORE bIGNOREc IGNORE : IGNORE "; + final StrTokenizer tok = new StrTokenizer(input, ':'); + tok.setIgnoredMatcher(StrMatcher.stringMatcher("IGNORE")); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals(" bc ", tok.next()); + assertEquals(" ", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicIgnoreTrimmed4() { + final String input = "IGNOREaIGNORE: IGNORE 'bIGNOREc'IGNORE'd' IGNORE : IGNORE "; + final StrTokenizer tok = new StrTokenizer(input, ':', '\''); + tok.setIgnoredMatcher(StrMatcher.stringMatcher("IGNORE")); + tok.setTrimmerMatcher(StrMatcher.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("bIGNOREcd", tok.next()); + assertNull(tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicQuoted1() { + final String input = "a 'b' c"; + final StrTokenizer tok = new StrTokenizer(input, ' ', '\''); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicQuoted2() { + final String input = "a:'b':"; + final StrTokenizer tok = new StrTokenizer(input, ':', '\''); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertNull(tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicQuoted3() { + final String input = "a:'b''c'"; + final StrTokenizer tok = new StrTokenizer(input, ':', '\''); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("b'c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicQuoted4() { + final String input = "a: 'b' 'c' :d"; + final StrTokenizer tok = new StrTokenizer(input, ':', '\''); + tok.setTrimmerMatcher(StrMatcher.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("b c", tok.next()); + assertEquals("d", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicQuoted5() { + final String input = "a: 'b'x'c' :d"; + final StrTokenizer tok = new StrTokenizer(input, ':', '\''); + tok.setTrimmerMatcher(StrMatcher.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("bxc", tok.next()); + assertEquals("d", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicQuoted6() { + final String input = "a:'b'\"c':d"; + final StrTokenizer tok = new StrTokenizer(input, ':'); + tok.setQuoteMatcher(StrMatcher.quoteMatcher()); + assertEquals("a", tok.next()); + assertEquals("b\"c:d", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicQuoted7() { + final String input = "a:\"There's a reason here\":b"; + final StrTokenizer tok = new StrTokenizer(input, ':'); + tok.setQuoteMatcher(StrMatcher.quoteMatcher()); + assertEquals("a", tok.next()); + assertEquals("There's a reason here", tok.next()); + assertEquals("b", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicQuotedTrimmed1() { + final String input = "a: 'b' :"; + final StrTokenizer tok = new StrTokenizer(input, ':', '\''); + tok.setTrimmerMatcher(StrMatcher.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertNull(tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicTrimmed1() { + final String input = "a: b : "; + final StrTokenizer tok = new StrTokenizer(input, ':'); + tok.setTrimmerMatcher(StrMatcher.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertNull(tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicTrimmed2() { + final String input = "a: b :"; + final StrTokenizer tok = new StrTokenizer(input, ':'); + tok.setTrimmerMatcher(StrMatcher.stringMatcher(" ")); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertNull(tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testChaining() { + final StrTokenizer tok = new StrTokenizer(); + assertEquals(tok, tok.reset()); + assertEquals(tok, tok.reset("")); + assertEquals(tok, tok.reset(ArrayUtils.EMPTY_CHAR_ARRAY)); + assertEquals(tok, tok.setDelimiterChar(' ')); + assertEquals(tok, tok.setDelimiterString(" ")); + assertEquals(tok, tok.setDelimiterMatcher(null)); + assertEquals(tok, tok.setQuoteChar(' ')); + assertEquals(tok, tok.setQuoteMatcher(null)); + assertEquals(tok, tok.setIgnoredChar(' ')); + assertEquals(tok, tok.setIgnoredMatcher(null)); + assertEquals(tok, tok.setTrimmerMatcher(null)); + assertEquals(tok, tok.setEmptyTokenAsNull(false)); + assertEquals(tok, tok.setIgnoreEmptyTokens(false)); + } + + /** + * Tests that the {@link StrTokenizer#clone()} clone method catches + * {@link CloneNotSupportedException} and returns {@code null}. + */ + @Test + public void testCloneNotSupportedException() { + final Object notCloned = new StrTokenizer() { + + @Override + Object cloneReset() throws CloneNotSupportedException { + throw new CloneNotSupportedException("test"); + } + }.clone(); + assertNull(notCloned); + } + + @Test + public void testCloneNull() { + final StrTokenizer tokenizer = new StrTokenizer((char[]) null); + // Start sanity check + assertNull(tokenizer.nextToken()); + tokenizer.reset(); + assertNull(tokenizer.nextToken()); + // End sanity check + final StrTokenizer clonedTokenizer = (StrTokenizer) tokenizer.clone(); + tokenizer.reset(); + assertNull(tokenizer.nextToken()); + assertNull(clonedTokenizer.nextToken()); + } + + @Test + public void testCloneReset() { + final char[] input = {'a'}; + final StrTokenizer tokenizer = new StrTokenizer(input); + // Start sanity check + assertEquals("a", tokenizer.nextToken()); + tokenizer.reset(input); + assertEquals("a", tokenizer.nextToken()); + // End sanity check + final StrTokenizer clonedTokenizer = (StrTokenizer) tokenizer.clone(); + input[0] = 'b'; + tokenizer.reset(input); + assertEquals("b", tokenizer.nextToken()); + assertEquals("a", clonedTokenizer.nextToken()); + } + + @Test + public void testConstructor_charArray() { + StrTokenizer tok = new StrTokenizer("a b".toCharArray()); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertFalse(tok.hasNext()); + + tok = new StrTokenizer(ArrayUtils.EMPTY_CHAR_ARRAY); + assertFalse(tok.hasNext()); + + tok = new StrTokenizer((char[]) null); + assertFalse(tok.hasNext()); + } + + @Test + public void testConstructor_charArray_char() { + StrTokenizer tok = new StrTokenizer("a b".toCharArray(), ' '); + assertEquals(1, tok.getDelimiterMatcher().isMatch(" ".toCharArray(), 0, 0, 1)); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertFalse(tok.hasNext()); + + tok = new StrTokenizer(ArrayUtils.EMPTY_CHAR_ARRAY, ' '); + assertFalse(tok.hasNext()); + + tok = new StrTokenizer((char[]) null, ' '); + assertFalse(tok.hasNext()); + } + + @Test + public void testConstructor_charArray_char_char() { + StrTokenizer tok = new StrTokenizer("a b".toCharArray(), ' ', '"'); + assertEquals(1, tok.getDelimiterMatcher().isMatch(" ".toCharArray(), 0, 0, 1)); + assertEquals(1, tok.getQuoteMatcher().isMatch("\"".toCharArray(), 0, 0, 1)); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertFalse(tok.hasNext()); + + tok = new StrTokenizer(ArrayUtils.EMPTY_CHAR_ARRAY, ' ', '"'); + assertFalse(tok.hasNext()); + + tok = new StrTokenizer((char[]) null, ' ', '"'); + assertFalse(tok.hasNext()); + } + + @Test + public void testConstructor_String() { + StrTokenizer tok = new StrTokenizer("a b"); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertFalse(tok.hasNext()); + + tok = new StrTokenizer(""); + assertFalse(tok.hasNext()); + + tok = new StrTokenizer((String) null); + assertFalse(tok.hasNext()); + } + + @Test + public void testConstructor_String_char() { + StrTokenizer tok = new StrTokenizer("a b", ' '); + assertEquals(1, tok.getDelimiterMatcher().isMatch(" ".toCharArray(), 0, 0, 1)); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertFalse(tok.hasNext()); + + tok = new StrTokenizer("", ' '); + assertFalse(tok.hasNext()); + + tok = new StrTokenizer((String) null, ' '); + assertFalse(tok.hasNext()); + } + + @Test + public void testConstructor_String_char_char() { + StrTokenizer tok = new StrTokenizer("a b", ' ', '"'); + assertEquals(1, tok.getDelimiterMatcher().isMatch(" ".toCharArray(), 0, 0, 1)); + assertEquals(1, tok.getQuoteMatcher().isMatch("\"".toCharArray(), 0, 0, 1)); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertFalse(tok.hasNext()); + + tok = new StrTokenizer("", ' ', '"'); + assertFalse(tok.hasNext()); + + tok = new StrTokenizer((String) null, ' ', '"'); + assertFalse(tok.hasNext()); + } + + private void testCSV(final String data) { + this.testXSVAbc(StrTokenizer.getCSVInstance(data)); + this.testXSVAbc(StrTokenizer.getCSVInstance(data.toCharArray())); + } + + @Test + public void testCSVEmpty() { + this.testEmpty(StrTokenizer.getCSVInstance()); + this.testEmpty(StrTokenizer.getCSVInstance("")); + } + + @Test + public void testCSVSimple() { + this.testCSV(CSV_SIMPLE_FIXTURE); + } + + @Test + public void testCSVSimpleNeedsTrim() { + this.testCSV(" " + CSV_SIMPLE_FIXTURE); + this.testCSV(" \n\t " + CSV_SIMPLE_FIXTURE); + this.testCSV(" \n " + CSV_SIMPLE_FIXTURE + "\n\n\r"); + } + + @Test + public void testDelimMatcher() { + final String input = "a/b\\c"; + final StrMatcher delimMatcher = new StrMatcher.CharSetMatcher(new char[] {'/', '\\'}); + + final StrTokenizer tok = new StrTokenizer(input, delimMatcher); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testDelimMatcherQuoteMatcher() { + final String input = "`a`;`b`;`c`"; + final StrMatcher delimMatcher = new StrMatcher.CharSetMatcher(new char[] {';'}); + final StrMatcher quoteMatcher = new StrMatcher.CharSetMatcher(new char[] {'`'}); + + final StrTokenizer tok = new StrTokenizer(input, delimMatcher, quoteMatcher); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testDelimString() { + final String input = "a##b##c"; + final StrTokenizer tok = new StrTokenizer(input, "##"); + + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + void testEmpty(final StrTokenizer tokenizer) { + this.checkClone(tokenizer); + assertFalse(tokenizer.hasNext()); + assertFalse(tokenizer.hasPrevious()); + assertNull(tokenizer.nextToken()); + assertEquals(0, tokenizer.size()); + assertThrows(NoSuchElementException.class, tokenizer::next); + } + + @Test + public void testGetContent() { + final String input = "a b c \"d e\" f "; + StrTokenizer tok = new StrTokenizer(input); + assertEquals(input, tok.getContent()); + + tok = new StrTokenizer(input.toCharArray()); + assertEquals(input, tok.getContent()); + + tok = new StrTokenizer(); + assertNull(tok.getContent()); + } + + @Test + public void testIteration() { + final StrTokenizer tkn = new StrTokenizer("a b c"); + assertFalse(tkn.hasPrevious()); + assertThrows(NoSuchElementException.class, tkn::previous); + assertTrue(tkn.hasNext()); + + assertEquals("a", tkn.next()); + assertThrows(UnsupportedOperationException.class, tkn::remove); + assertThrows(UnsupportedOperationException.class, () -> tkn.set("x")); + assertThrows(UnsupportedOperationException.class, () -> tkn.add("y")); + assertTrue(tkn.hasPrevious()); + assertTrue(tkn.hasNext()); + + assertEquals("b", tkn.next()); + assertTrue(tkn.hasPrevious()); + assertTrue(tkn.hasNext()); + + assertEquals("c", tkn.next()); + assertTrue(tkn.hasPrevious()); + assertFalse(tkn.hasNext()); + + assertThrows(NoSuchElementException.class, tkn::next); + assertTrue(tkn.hasPrevious()); + assertFalse(tkn.hasNext()); + } + + @Test + public void testListArray() { + final String input = "a b c"; + final StrTokenizer tok = new StrTokenizer(input); + final String[] array = tok.getTokenArray(); + final List list = tok.getTokenList(); + + assertEquals(Arrays.asList(array), list); + assertEquals(3, list.size()); + } + + @Test + public void testPreviousTokenAndSetEmptyTokenAsNull() { + final StrTokenizer strTokenizer = StrTokenizer.getTSVInstance(" \t\n\r\f"); + strTokenizer.setEmptyTokenAsNull(true); + + assertNull(strTokenizer.previousToken()); + } + + @Test + public void testReset() { + final StrTokenizer tok = new StrTokenizer("a b c"); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + + tok.reset(); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testReset_charArray() { + final StrTokenizer tok = new StrTokenizer("x x x"); + + final char[] array = {'a', 'b', 'c'}; + tok.reset(array); + assertEquals("abc", tok.next()); + assertFalse(tok.hasNext()); + + tok.reset((char[]) null); + assertFalse(tok.hasNext()); + } + + @Test + public void testReset_String() { + final StrTokenizer tok = new StrTokenizer("x x x"); + tok.reset("d e"); + assertEquals("d", tok.next()); + assertEquals("e", tok.next()); + assertFalse(tok.hasNext()); + + tok.reset((String) null); + assertFalse(tok.hasNext()); + } + + @Test + public void testStringTokenizerQuoteMatcher() { + final char[] chars = {'\'', 'a', 'c', '\'', 'd'}; + final StrTokenizer tokens = new StrTokenizer(chars, StrMatcher.commaMatcher(), StrMatcher.quoteMatcher()); + assertEquals("acd", tokens.next()); + } + + @Test + public void testStringTokenizerStringMatcher() { + final char[] chars = {'a', 'b', 'c', 'd'}; + final StrTokenizer tokens = new StrTokenizer(chars, "bc"); + assertEquals("a", tokens.next()); + assertEquals("d", tokens.next()); + } + + @Test + public void testStringTokenizerStrMatcher() { + final char[] chars = {'a', ',', 'c'}; + final StrTokenizer tokens = new StrTokenizer(chars, StrMatcher.commaMatcher()); + assertEquals("a", tokens.next()); + assertEquals("c", tokens.next()); + } + + @Test + public void testTokenizeSubclassInputChange() { + final StrTokenizer tkn = new StrTokenizer("a b c d e") { + + @Override + protected List tokenize(final char[] chars, final int offset, final int count) { + return super.tokenize("w x y z".toCharArray(), 2, 5); + } + }; + assertEquals("x", tkn.next()); + assertEquals("y", tkn.next()); + } + + @Test + public void testTokenizeSubclassOutputChange() { + final StrTokenizer tkn = new StrTokenizer("a b c") { + + @Override + protected List tokenize(final char[] chars, final int offset, final int count) { + final List list = super.tokenize(chars, offset, count); + Collections.reverse(list); + return list; + } + }; + assertEquals("c", tkn.next()); + assertEquals("b", tkn.next()); + assertEquals("a", tkn.next()); + } + + @Test + public void testToString() { + final StrTokenizer tkn = new StrTokenizer("a b c d e"); + assertEquals("StrTokenizer[not tokenized yet]", tkn.toString()); + tkn.next(); + assertEquals("StrTokenizer[a, b, c, d, e]", tkn.toString()); + } + + @Test + public void testTSV() { + this.testXSVAbc(StrTokenizer.getTSVInstance(TSV_SIMPLE_FIXTURE)); + this.testXSVAbc(StrTokenizer.getTSVInstance(TSV_SIMPLE_FIXTURE.toCharArray())); + } + + @Test + public void testTSVEmpty() { + this.testEmpty(StrTokenizer.getTSVInstance()); + this.testEmpty(StrTokenizer.getTSVInstance("")); + } + + void testXSVAbc(final StrTokenizer tokenizer) { + this.checkClone(tokenizer); + assertEquals(-1, tokenizer.previousIndex()); + assertEquals(0, tokenizer.nextIndex()); + assertNull(tokenizer.previousToken()); + assertEquals("A", tokenizer.nextToken()); + assertEquals(1, tokenizer.nextIndex()); + assertEquals("b", tokenizer.nextToken()); + assertEquals(2, tokenizer.nextIndex()); + assertEquals("c", tokenizer.nextToken()); + assertEquals(3, tokenizer.nextIndex()); + assertNull(tokenizer.nextToken()); + assertEquals(3, tokenizer.nextIndex()); + assertEquals("c", tokenizer.previousToken()); + assertEquals(2, tokenizer.nextIndex()); + assertEquals("b", tokenizer.previousToken()); + assertEquals(1, tokenizer.nextIndex()); + assertEquals("A", tokenizer.previousToken()); + assertEquals(0, tokenizer.nextIndex()); + assertNull(tokenizer.previousToken()); + assertEquals(0, tokenizer.nextIndex()); + assertEquals(-1, tokenizer.previousIndex()); + assertEquals(3, tokenizer.size()); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/StringEscapeUtilsTest.java b/sources/src/test/java/org/apache/commons/text/StringEscapeUtilsTest.java new file mode 100644 index 0000000..de1ebdd --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/StringEscapeUtilsTest.java @@ -0,0 +1,644 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import static org.apache.commons.text.StringEscapeUtils.escapeXSI; +import static org.apache.commons.text.StringEscapeUtils.unescapeXSI; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.io.StringWriter; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link StringEscapeUtils}. + * + *

+ * This code has been adapted from Apache Commons Lang 3.5. + *

+ */ +public class StringEscapeUtilsTest { + private static final String FOO = "foo"; + + private static final String[][] HTML_ESCAPES = { + {"no escaping", "plain text", "plain text"}, + {"no escaping", "plain text", "plain text"}, + {"empty string", "", ""}, + {"null", null, null}, + {"ampersand", "bread & butter", "bread & butter"}, + {"quotes", ""bread" & butter", "\"bread\" & butter"}, + {"final character only", "greater than >", "greater than >"}, + {"first character only", "< less than", "< less than"}, + {"apostrophe", "Huntington's chorea", "Huntington's chorea"}, + {"languages", "English,Français,\u65E5\u672C\u8A9E (nihongo)", + "English,Fran\u00E7ais,\u65E5\u672C\u8A9E (nihongo)"}, + {"8-bit ascii shouldn't number-escape", "\u0080\u009F", "\u0080\u009F"}, + }; + + private void assertEscapeJava(final String escaped, final String original) throws IOException { + assertEscapeJava(escaped, original, null); + } + + private void assertEscapeJava(final String expected, final String original, String message) throws IOException { + final String converted = StringEscapeUtils.escapeJava(original); + message = "escapeJava(String) failed" + (message == null ? "" : ": " + message); + assertEquals(expected, converted, message); + + final StringWriter writer = new StringWriter(); + StringEscapeUtils.ESCAPE_JAVA.translate(original, writer); + assertEquals(expected, writer.toString()); + } + + private void assertUnescapeJava(final String unescaped, final String original) throws IOException { + assertUnescapeJava(unescaped, original, null); + } + + private void assertUnescapeJava(final String unescaped, final String original, final String message) + throws IOException { + final String actual = StringEscapeUtils.unescapeJava(original); + + assertEquals(unescaped, actual, "unescape(String) failed" + + (message == null ? "" : ": " + message) + + ": expected '" + StringEscapeUtils.escapeJava(unescaped) + // we escape this so we can see it in the error message + + "' actual '" + StringEscapeUtils.escapeJava(actual) + "'"); + + final StringWriter writer = new StringWriter(); + StringEscapeUtils.UNESCAPE_JAVA.translate(original, writer); + assertEquals(unescaped, writer.toString()); + } + + private void checkCsvEscapeWriter(final String expected, final String value) { + try { + final StringWriter writer = new StringWriter(); + StringEscapeUtils.ESCAPE_CSV.translate(value, writer); + assertEquals(expected, writer.toString()); + } catch (final IOException e) { + fail("Threw: " + e); + } + } + + private void checkCsvUnescapeWriter(final String expected, final String value) { + try { + final StringWriter writer = new StringWriter(); + StringEscapeUtils.UNESCAPE_CSV.translate(value, writer); + assertEquals(expected, writer.toString()); + } catch (final IOException e) { + fail("Threw: " + e); + } + } + + @Test + public void testBuilder() { + final String result = + StringEscapeUtils.builder(StringEscapeUtils.ESCAPE_XML10).escape("<").append(">").toString(); + assertEquals("<>", result); + } + + @Test + public void testConstructor() { + assertNotNull(new StringEscapeUtils()); + final Constructor[] cons = StringEscapeUtils.class.getDeclaredConstructors(); + assertEquals(1, cons.length); + assertTrue(Modifier.isPublic(cons[0].getModifiers())); + assertTrue(Modifier.isPublic(StringEscapeUtils.class.getModifiers())); + assertFalse(Modifier.isFinal(StringEscapeUtils.class.getModifiers())); + } + + + // HTML and XML + @Test + public void testDeleteCharacter() { + final String deleteString = "Delete: \u007F"; + assertEquals("Delete: \\u007F", StringEscapeUtils.escapeJson(deleteString)); + } + + @Test + public void testEscapeCsvString() { + assertEquals("foo.bar", StringEscapeUtils.escapeCsv("foo.bar")); + assertEquals("\"foo,bar\"", StringEscapeUtils.escapeCsv("foo,bar")); + assertEquals("\"foo\nbar\"", StringEscapeUtils.escapeCsv("foo\nbar")); + assertEquals("\"foo\rbar\"", StringEscapeUtils.escapeCsv("foo\rbar")); + assertEquals("\"foo\"\"bar\"", StringEscapeUtils.escapeCsv("foo\"bar")); + assertEquals("foo\uD84C\uDFB4bar", StringEscapeUtils.escapeCsv("foo\uD84C\uDFB4bar")); + assertEquals("", StringEscapeUtils.escapeCsv("")); + assertNull(StringEscapeUtils.escapeCsv(null)); + } + + @Test + public void testEscapeCsvWriter() { + checkCsvEscapeWriter("foo.bar", "foo.bar"); + checkCsvEscapeWriter("\"foo,bar\"", "foo,bar"); + checkCsvEscapeWriter("\"foo\nbar\"", "foo\nbar"); + checkCsvEscapeWriter("\"foo\rbar\"", "foo\rbar"); + checkCsvEscapeWriter("\"foo\"\"bar\"", "foo\"bar"); + checkCsvEscapeWriter("foo\uD84C\uDFB4bar", "foo\uD84C\uDFB4bar"); + checkCsvEscapeWriter("", null); + checkCsvEscapeWriter("", ""); + } + +@Test +public void testEscapeEcmaScript() { + assertNull(StringEscapeUtils.escapeEcmaScript(null)); + try { + StringEscapeUtils.ESCAPE_ECMASCRIPT.translate(null, null); + fail("Exception expected!"); + } catch (final IOException ex) { + fail("Exception expected!"); + } catch (final IllegalArgumentException ex) { + // expected + } + try { + StringEscapeUtils.ESCAPE_ECMASCRIPT.translate("", null); + fail("Exception expected!"); + } catch (final IOException ex) { + fail("Exception expected!"); + } catch (final IllegalArgumentException ex) { + // expected + } + + assertEquals("He didn\\'t say, \\\"stop!\\\"", StringEscapeUtils.escapeEcmaScript("He didn't say, \"stop!\"")); + assertEquals("document.getElementById(\\\"test\\\").value = \\'';")); +} + + /** + * Tests https://issues.apache.org/jira/browse/LANG-339 + */ + @Test + public void testEscapeHiragana() { + // Some random Japanese Unicode characters + final String original = "\u304B\u304C\u3068"; + final String escaped = StringEscapeUtils.escapeHtml4(original); + assertEquals(original, escaped, + "Hiragana character Unicode behavior should not be being escaped by escapeHtml4"); + + final String unescaped = StringEscapeUtils.unescapeHtml4(escaped); + + assertEquals(escaped, unescaped, "Hiragana character Unicode behavior has changed - expected no unescaping"); + } + + @Test + public void testEscapeHtml3() { + for (final String[] element : HTML_ESCAPES) { + final String message = element[0]; + final String expected = element[1]; + final String original = element[2]; + assertEquals(expected, StringEscapeUtils.escapeHtml4(original), message); + final StringWriter sw = new StringWriter(); + try { + StringEscapeUtils.ESCAPE_HTML3.translate(original, sw); + } catch (final IOException e) { + // expected + } + final String actual = original == null ? null : sw.toString(); + assertEquals(expected, actual, message); + } + } + + @Test + public void testEscapeHtml4() { + for (final String[] element : HTML_ESCAPES) { + final String message = element[0]; + final String expected = element[1]; + final String original = element[2]; + assertEquals(expected, StringEscapeUtils.escapeHtml4(original), message); + final StringWriter sw = new StringWriter(); + try { + StringEscapeUtils.ESCAPE_HTML4.translate(original, sw); + } catch (final IOException e) { + // expected + } + final String actual = original == null ? null : sw.toString(); + assertEquals(expected, actual, message); + } + } + + /** + * Tests // https://issues.apache.org/jira/browse/LANG-480 + */ + @Test + public void testEscapeHtmlHighUnicode() { + // this is the utf8 representation of the character: + // COUNTING ROD UNIT DIGIT THREE + // in Unicode + // code point: U+1D362 + final byte[] data = {(byte) 0xF0, (byte) 0x9D, (byte) 0x8D, (byte) 0xA2}; + + final String original = new String(data, StandardCharsets.UTF_8); + + final String escaped = StringEscapeUtils.escapeHtml4(original); + assertEquals(original, escaped, "High Unicode should not have been escaped"); + + final String unescaped = StringEscapeUtils.unescapeHtml4(escaped); + assertEquals(original, unescaped, "High Unicode should have been unchanged"); + + // TODO: I think this should hold, needs further investigation + // String unescapedFromEntity = StringEscapeUtils.unescapeHtml4("𝍢"); + // assertEquals("High Unicode should have been unescaped", original, unescapedFromEntity); + } + + + + @Test + public void testEscapeHtmlThree() { + assertNull(StringEscapeUtils.escapeHtml3(null)); + assertEquals("a", StringEscapeUtils.escapeHtml3("a")); + assertEquals("<b>a", StringEscapeUtils.escapeHtml3("a")); + } + + @Test + public void testEscapeHtmlVersions() { + assertEquals("Β", StringEscapeUtils.escapeHtml4("\u0392")); + assertEquals("\u0392", StringEscapeUtils.unescapeHtml4("Β")); + + // TODO: refine API for escaping/unescaping specific HTML versions + } + + @Test + public void testEscapeJava() throws IOException { + assertNull(StringEscapeUtils.escapeJava(null)); + try { + StringEscapeUtils.ESCAPE_JAVA.translate(null, null); + fail("Exception expected!"); + } catch (final IOException ex) { + fail("Exception expected!"); + } catch (final IllegalArgumentException ex) { + // expected + } + try { + StringEscapeUtils.ESCAPE_JAVA.translate("", null); + fail("Exception expected!"); + } catch (final IOException ex) { + fail("Exception expected!"); + } catch (final IllegalArgumentException ex) { + // expected + } + + assertEscapeJava("", "", "empty string"); + assertEscapeJava(FOO, FOO); + assertEscapeJava("\\t", "\t", "tab"); + assertEscapeJava("\\\\", "\\", "backslash"); + assertEscapeJava("'", "'", "single quote should not be escaped"); + assertEscapeJava("\\\\\\b\\t\\r", "\\\b\t\r"); + assertEscapeJava("\\u1234", "\u1234"); + assertEscapeJava("\\u0234", "\u0234"); + assertEscapeJava("\\u00EF", "\u00ef"); + assertEscapeJava("\\u0001", "\u0001"); + assertEscapeJava("\\uABCD", "\uabcd", "Should use capitalized Unicode hex"); + + assertEscapeJava("He didn't say, \\\"stop!\\\"", + "He didn't say, \"stop!\""); + assertEscapeJava("This space is non-breaking:" + "\\u00A0", "This space is non-breaking:\u00a0", + "non-breaking space"); + assertEscapeJava("\\uABCD\\u1234\\u012C", + "\uABCD\u1234\u012C"); + } + + /** + * Tests https://issues.apache.org/jira/browse/LANG-421 + */ + @Test + public void testEscapeJavaWithSlash() { + final String input = "String with a slash (/) in it"; + + final String actual = StringEscapeUtils.escapeJava(input); + + /* + * In 2.4 StringEscapeUtils.escapeJava(String) escapes '/' characters, which are not a valid character + * to escape in a Java string. + */ + assertEquals(input, actual); + } + + @Test + public void testEscapeJson() { + assertNull(StringEscapeUtils.escapeJson(null)); + try { + StringEscapeUtils.ESCAPE_JSON.translate(null, null); + fail("Exception expected!"); + } catch (final IOException ex) { + fail("Exception expected!"); + } catch (final IllegalArgumentException ex) { + // expected + } + try { + StringEscapeUtils.ESCAPE_JSON.translate("", null); + fail("Exception expected!"); + } catch (final IOException ex) { + fail("Exception expected!"); + } catch (final IllegalArgumentException ex) { + // expected + } + + assertEquals("He didn't say, \\\"stop!\\\"", StringEscapeUtils.escapeJson("He didn't say, \"stop!\"")); + + final String expected = "\\\"foo\\\" isn't \\\"bar\\\". specials: \\b\\r\\n\\f\\t\\\\\\/"; + final String input = "\"foo\" isn't \"bar\". specials: \b\r\n\f\t\\/"; + + assertEquals(expected, StringEscapeUtils.escapeJson(input)); + } + + @Test + public void testEscapeXml10() { + assertEquals("a<b>c"d'e&f", StringEscapeUtils.escapeXml10("ac\"d'e&f")); + assertEquals("a\tb\rc\nd", StringEscapeUtils.escapeXml10("a\tb\rc\nd"), + "XML 1.0 should not escape \t \n \r"); + assertEquals("ab", StringEscapeUtils.escapeXml10("a\u0000\u0001\u0008\u000b\u000c\u000e\u001fb"), + "XML 1.0 should omit most #x0-x8 | #xb | #xc | #xe-#x19"); + assertEquals("a\ud7ff \ue000b", StringEscapeUtils.escapeXml10("a\ud7ff\ud800 \udfff \ue000b"), + "XML 1.0 should omit #xd800-#xdfff"); + assertEquals("a\ufffdb", StringEscapeUtils.escapeXml10("a\ufffd\ufffe\uffffb"), + "XML 1.0 should omit #xfffe | #xffff"); + assertEquals("a\u007e„\u0085†Ÿ\u00a0b", + StringEscapeUtils.escapeXml10("a\u007e\u007f\u0084\u0085\u0086\u009f\u00a0b"), + "XML 1.0 should escape #x7f-#x84 | #x86 - #x9f, for XML 1.1 compatibility"); + } + + @Test + public void testEscapeXml11() { + assertEquals("a<b>c"d'e&f", StringEscapeUtils.escapeXml11("ac\"d'e&f")); + assertEquals("a\tb\rc\nd", StringEscapeUtils.escapeXml11("a\tb\rc\nd"), + "XML 1.1 should not escape \t \n \r"); + assertEquals("ab", StringEscapeUtils.escapeXml11("a\u0000b"), + "XML 1.1 should omit #x0"); + assertEquals("a b", + StringEscapeUtils.escapeXml11("a\u0001\u0008\u000b\u000c\u000e\u001fb"), + "XML 1.1 should escape #x1-x8 | #xb | #xc | #xe-#x19"); + assertEquals("a\u007e„\u0085†Ÿ\u00a0b", + StringEscapeUtils.escapeXml11("a\u007e\u007f\u0084\u0085\u0086\u009f\u00a0b"), + "XML 1.1 should escape #x7F-#x84 | #x86-#x9F"); + assertEquals("a\ud7ff \ue000b", StringEscapeUtils.escapeXml11("a\ud7ff\ud800 \udfff \ue000b"), + "XML 1.1 should omit #xd800-#xdfff"); + assertEquals("a\ufffdb", StringEscapeUtils.escapeXml11("a\ufffd\ufffe\uffffb"), + "XML 1.1 should omit #xfffe | #xffff"); + } + + @Test + public void testEscapeXSI() { + assertNull(null, escapeXSI(null)); + assertEquals("He\\ didn\\'t\\ say,\\ \\\"Stop!\\\"", escapeXSI("He didn't say, \"Stop!\"")); + assertEquals("\\\\", escapeXSI("\\")); + assertEquals("", escapeXSI("\n")); + } + + @Test + public void testLang313() { + assertEquals("& &", StringEscapeUtils.unescapeHtml4("& &")); + } + + /** + * Tests https://issues.apache.org/jira/browse/LANG-708 + * + * @throws IOException + * if an I/O error occurs + */ + @Test + public void testLang708() throws IOException { + final byte[] inputBytes = Files.readAllBytes(Paths.get("src/test/resources/org/apache/commons/text/stringEscapeUtilsTestData.txt")); + final String input = new String(inputBytes, StandardCharsets.UTF_8); + final String escaped = StringEscapeUtils.escapeEcmaScript(input); + // just the end: + assertTrue(escaped.endsWith("}]"), escaped); + // a little more: + assertTrue(escaped.endsWith("\"valueCode\\\":\\\"\\\"}]"), escaped); + } + + /** + * Tests https://issues.apache.org/jira/browse/LANG-911 + */ + @Test + public void testLang911() { + final String bellsTest = "\ud83d\udc80\ud83d\udd14"; + final String value = StringEscapeUtils.escapeJava(bellsTest); + final String valueTest = StringEscapeUtils.unescapeJava(value); + assertEquals(bellsTest, valueTest); + } + + // Tests issue #38569 + // https://issues.apache.org/bugzilla/show_bug.cgi?id=38569 + @Test + public void testStandaloneAmphersand() { + assertEquals("", StringEscapeUtils.unescapeHtml4("<P&O>")); + assertEquals("test & <", StringEscapeUtils.unescapeHtml4("test & <")); + assertEquals("", StringEscapeUtils.unescapeXml("<P&O>")); + assertEquals("test & <", StringEscapeUtils.unescapeXml("test & <")); + } + + @Test + public void testUnescapeCsvString() { + assertEquals("foo.bar", StringEscapeUtils.unescapeCsv("foo.bar")); + assertEquals("foo,bar", StringEscapeUtils.unescapeCsv("\"foo,bar\"")); + assertEquals("foo\nbar", StringEscapeUtils.unescapeCsv("\"foo\nbar\"")); + assertEquals("foo\rbar", StringEscapeUtils.unescapeCsv("\"foo\rbar\"")); + assertEquals("foo\"bar", StringEscapeUtils.unescapeCsv("\"foo\"\"bar\"")); + assertEquals("foo\uD84C\uDFB4bar", StringEscapeUtils.unescapeCsv("foo\uD84C\uDFB4bar")); + assertEquals("", StringEscapeUtils.unescapeCsv("")); + assertNull(StringEscapeUtils.unescapeCsv(null)); + + assertEquals("foo.bar", StringEscapeUtils.unescapeCsv("\"foo.bar\"")); + } + + @Test + public void testUnescapeCsvWriter() { + checkCsvUnescapeWriter("foo.bar", "foo.bar"); + checkCsvUnescapeWriter("foo,bar", "\"foo,bar\""); + checkCsvUnescapeWriter("foo\nbar", "\"foo\nbar\""); + checkCsvUnescapeWriter("foo\rbar", "\"foo\rbar\""); + checkCsvUnescapeWriter("foo\"bar", "\"foo\"\"bar\""); + checkCsvUnescapeWriter("foo\uD84C\uDFB4bar", "foo\uD84C\uDFB4bar"); + checkCsvUnescapeWriter("", null); + checkCsvUnescapeWriter("", ""); + + checkCsvUnescapeWriter("foo.bar", "\"foo.bar\""); + } + + @Test + public void testUnescapeEcmaScript() { + assertNull(StringEscapeUtils.unescapeEcmaScript(null)); + assertEquals("8lvc1u+6B#-I", StringEscapeUtils.unescapeEcmaScript("8lvc1u+6B#-I")); + assertEquals("", + StringEscapeUtils.unescapeEcmaScript("")); + assertEquals(">", + StringEscapeUtils.unescapeEcmaScript(">")); + } + + @Test + public void testUnescapeHexCharsHtml() { + // Simple easy to grok test + assertEquals("\u0080\u009F", StringEscapeUtils.unescapeHtml4("€Ÿ"), "hex number unescape"); + assertEquals("\u0080\u009F", StringEscapeUtils.unescapeHtml4("€Ÿ"), "hex number unescape"); + // Test all Character values: + for (char i = Character.MIN_VALUE; i < Character.MAX_VALUE; i++) { + final char c2 = (char) (i + 1); + final String expected = Character.toString(i) + Character.toString(c2); + final String escapedC1 = "&#x" + Integer.toHexString(i) + ";"; + final String escapedC2 = "&#x" + Integer.toHexString(c2) + ";"; + assertEquals(expected, StringEscapeUtils.unescapeHtml4(escapedC1 + escapedC2), + "hex number unescape index " + i); + } + } + + @Test + public void testUnescapeHtml3() { + for (final String[] element : HTML_ESCAPES) { + final String message = element[0]; + final String expected = element[2]; + final String original = element[1]; + assertEquals(expected, StringEscapeUtils.unescapeHtml3(original), message); + + final StringWriter sw = new StringWriter(); + try { + StringEscapeUtils.UNESCAPE_HTML3.translate(original, sw); + } catch (final IOException e) { + // expected + } + final String actual = original == null ? null : sw.toString(); + assertEquals(expected, actual, message); + } + // \u00E7 is a cedilla (c with wiggle under) + // note that the test string must be 7-bit-clean (Unicode escaped) or else it will compile incorrectly + // on some locales + assertEquals("Fran\u00E7ais", StringEscapeUtils.unescapeHtml3("Fran\u00E7ais"), "funny chars pass through OK"); + + assertEquals("Hello&;World", StringEscapeUtils.unescapeHtml3("Hello&;World")); + assertEquals("Hello&#;World", StringEscapeUtils.unescapeHtml3("Hello&#;World")); + assertEquals("Hello&# ;World", StringEscapeUtils.unescapeHtml3("Hello&# ;World")); + assertEquals("Hello&##;World", StringEscapeUtils.unescapeHtml3("Hello&##;World")); + } + + @Test + public void testUnescapeHtml4() { + for (final String[] element : HTML_ESCAPES) { + final String message = element[0]; + final String expected = element[2]; + final String original = element[1]; + assertEquals(expected, StringEscapeUtils.unescapeHtml4(original), message); + + final StringWriter sw = new StringWriter(); + try { + StringEscapeUtils.UNESCAPE_HTML4.translate(original, sw); + } catch (final IOException e) { + // expected + } + final String actual = original == null ? null : sw.toString(); + assertEquals(expected, actual, message); + } + // \u00E7 is a cedilla (c with wiggle under) + // note that the test string must be 7-bit-clean (Unicode escaped) or else it will compile incorrectly + // on some locales + assertEquals("Fran\u00E7ais", StringEscapeUtils.unescapeHtml4("Fran\u00E7ais"), "funny chars pass through OK"); + + assertEquals("Hello&;World", StringEscapeUtils.unescapeHtml4("Hello&;World")); + assertEquals("Hello&#;World", StringEscapeUtils.unescapeHtml4("Hello&#;World")); + assertEquals("Hello&# ;World", StringEscapeUtils.unescapeHtml4("Hello&# ;World")); + assertEquals("Hello&##;World", StringEscapeUtils.unescapeHtml4("Hello&##;World")); + } + + @Test + public void testUnescapeJava() throws IOException { + assertNull(StringEscapeUtils.unescapeJava(null)); + try { + StringEscapeUtils.UNESCAPE_JAVA.translate(null, null); + fail("Exception expected!"); + } catch (final IOException ex) { + fail("Exception expected!"); + } catch (final IllegalArgumentException ex) { + // expected + } + try { + StringEscapeUtils.UNESCAPE_JAVA.translate("", null); + fail("Exception expected!"); + } catch (final IOException ex) { + fail("Exception expected!"); + } catch (final IllegalArgumentException ex) { + // expected + } + assertThrows(RuntimeException.class, () -> StringEscapeUtils.unescapeJava("\\u02-3")); + + assertUnescapeJava("", ""); + assertUnescapeJava("test", "test"); + assertUnescapeJava("\ntest\b", "\\ntest\\b"); + assertUnescapeJava("\u123425foo\ntest\b", "\\u123425foo\\ntest\\b"); + assertUnescapeJava("'\foo\teste\r", "\\'\\foo\\teste\\r"); + assertUnescapeJava("", "\\"); + //foo + assertUnescapeJava("\uABCDx", "\\uabcdx", "lowercase Unicode"); + assertUnescapeJava("\uABCDx", "\\uABCDx", "uppercase Unicode"); + assertUnescapeJava("\uABCD", "\\uabcd", "Unicode as final character"); + } + + @Test + public void testUnescapeJson() { + final String jsonString = + "{\"age\":100,\"name\":\"kyong.com\n\",\"messages\":[\"msg 1\",\"msg 2\",\"msg 3\"]}"; + + assertEquals("", StringEscapeUtils.unescapeJson("")); + assertEquals(" ", StringEscapeUtils.unescapeJson(" ")); + assertEquals("a:b", StringEscapeUtils.unescapeJson("a:b")); + assertEquals(jsonString, StringEscapeUtils.unescapeJson(jsonString)); + } + + @Test // TEXT-120 + public void testUnescapeJsonDoubleQuoteAndForwardSlash() { + final String escapedJsonString = "double quote: \\\" and a forward slash: \\/"; + final String jsonString = "double quote: \" and a forward slash: /"; + + assertEquals(jsonString, StringEscapeUtils.unescapeJson(escapedJsonString)); + } + + @Test + public void testUnescapeUnknownEntity() { + assertEquals("&zzzz;", StringEscapeUtils.unescapeHtml4("&zzzz;")); + } + + /** + * Reverse of the above. + * + * @see LANG-729 + */ + @Test + public void testUnescapeXmlSupplementaryCharacters() { + assertEquals("\uD84C\uDFB4", StringEscapeUtils.unescapeXml("𣎴"), + "Supplementary character must be represented using a single escape"); + + assertEquals("a b c \uD84C\uDFB4", StringEscapeUtils.unescapeXml("a b c 𣎴"), + "Supplementary characters mixed with basic characters should be decoded correctly"); + } + + @Test + public void testUnscapeXSI() { + assertNull(null, unescapeXSI(null)); + assertEquals("\"", unescapeXSI("\\\"")); + assertEquals("He didn't say, \"Stop!\"", unescapeXSI("He\\ didn\\'t\\ say,\\ \\\"Stop!\\\"")); + assertEquals("\\", unescapeXSI("\\\\")); + assertEquals("", unescapeXSI("\\")); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/StringSubstitutorTest.java b/sources/src/test/java/org/apache/commons/text/StringSubstitutorTest.java new file mode 100644 index 0000000..93c585e --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/StringSubstitutorTest.java @@ -0,0 +1,1112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.mutable.MutableObject; +import org.apache.commons.text.lookup.StringLookup; +import org.apache.commons.text.lookup.StringLookupFactory; +import org.apache.commons.text.matcher.StringMatcher; +import org.apache.commons.text.matcher.StringMatcherFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +/** + * Test class for {@link StringSubstitutor}. + */ +@TestMethodOrder(MethodOrderer.MethodName.class) // temp, for my sanity during dev +public class StringSubstitutorTest { + + private static final String ACTUAL_ANIMAL = "quick brown fox"; + private static final String ACTUAL_TARGET = "lazy dog"; + private static final String CLASSIC_RESULT = "The quick brown fox jumps over the lazy dog."; + private static final String CLASSIC_TEMPLATE = "The ${animal} jumps over the ${target}."; + private static final String EMPTY_EXPR = "${}"; + + protected Map values; + + private void assertEqualsCharSeq(final CharSequence expected, final CharSequence actual) { + assertEquals(expected, actual, () -> String.format("expected.length()=%,d, actual.length()=%,d", + StringUtils.length(expected), StringUtils.length(actual))); + } + + protected void doNotReplace(final String replaceTemplate) throws IOException { + doTestNoReplace(new StringSubstitutor(values), replaceTemplate); + } + + protected void doReplace(final String expectedResult, final String replaceTemplate, final boolean substring) + throws IOException { + doTestReplace(new StringSubstitutor(values), expectedResult, replaceTemplate, substring); + } + + protected void doTestNoReplace(final StringSubstitutor substitutor, final String replaceTemplate) + throws IOException { + if (replaceTemplate == null) { + assertNull(replace(substitutor, (String) null)); + assertNull(substitutor.replace((String) null, 0, 100)); + assertNull(substitutor.replace((char[]) null)); + assertNull(substitutor.replace((char[]) null, 0, 100)); + assertNull(substitutor.replace((StringBuffer) null)); + assertNull(substitutor.replace((StringBuffer) null, 0, 100)); + assertNull(substitutor.replace((TextStringBuilder) null)); + assertNull(substitutor.replace((TextStringBuilder) null, 0, 100)); + assertNull(substitutor.replace((Object) null)); + assertFalse(substitutor.replaceIn((StringBuffer) null)); + assertFalse(substitutor.replaceIn((StringBuffer) null, 0, 100)); + assertFalse(substitutor.replaceIn((TextStringBuilder) null)); + assertFalse(substitutor.replaceIn((TextStringBuilder) null, 0, 100)); + } else { + assertEquals(replaceTemplate, replace(substitutor, replaceTemplate)); + final TextStringBuilder builder = new TextStringBuilder(replaceTemplate); + assertFalse(substitutor.replaceIn(builder)); + assertEquals(replaceTemplate, builder.toString()); + } + } + + protected void doTestReplace(final StringSubstitutor sub, final String expectedResult, final String replaceTemplate, + final boolean substring) throws IOException { + final String expectedShortResult = substring ? expectedResult.substring(1, expectedResult.length() - 1) + : expectedResult; + + // replace using String + final String actual = replace(sub, replaceTemplate); + assertEquals(expectedResult, actual, + () -> String.format("Index of difference: %,d", StringUtils.indexOfDifference(expectedResult, actual))); + if (substring) { + assertEquals(expectedShortResult, sub.replace(replaceTemplate, 1, replaceTemplate.length() - 2)); + } + + // replace using char[] + final char[] chars = replaceTemplate.toCharArray(); + assertEquals(expectedResult, sub.replace(chars)); + if (substring) { + assertEquals(expectedShortResult, sub.replace(chars, 1, chars.length - 2)); + } + + // replace using StringBuffer + StringBuffer buf = new StringBuffer(replaceTemplate); + assertEquals(expectedResult, sub.replace(buf)); + if (substring) { + assertEquals(expectedShortResult, sub.replace(buf, 1, buf.length() - 2)); + } + + // replace using StringBuilder + StringBuilder builder = new StringBuilder(replaceTemplate); + assertEquals(expectedResult, sub.replace(builder)); + if (substring) { + assertEquals(expectedShortResult, sub.replace(builder, 1, builder.length() - 2)); + } + + // replace using TextStringBuilder + TextStringBuilder bld = new TextStringBuilder(replaceTemplate); + assertEquals(expectedResult, sub.replace(bld)); + if (substring) { + assertEquals(expectedShortResult, sub.replace(bld, 1, bld.length() - 2)); + } + + // replace using object + final MutableObject obj = new MutableObject<>(replaceTemplate); // toString returns template + assertEquals(expectedResult, sub.replace(obj)); + + // replace in StringBuffer + buf = new StringBuffer(replaceTemplate); + assertTrue(sub.replaceIn(buf), replaceTemplate); + assertEquals(expectedResult, buf.toString()); + if (substring) { + buf = new StringBuffer(replaceTemplate); + assertTrue(sub.replaceIn(buf, 1, buf.length() - 2)); + assertEquals(expectedResult, buf.toString()); // expect full result as remainder is untouched + } + + // replace in StringBuilder + builder = new StringBuilder(replaceTemplate); + assertTrue(sub.replaceIn(builder)); + assertEquals(expectedResult, builder.toString()); + if (substring) { + builder = new StringBuilder(replaceTemplate); + assertTrue(sub.replaceIn(builder, 1, builder.length() - 2)); + assertEquals(expectedResult, builder.toString()); // expect full result as remainder is untouched + } + + // replace in TextStringBuilder + bld = new TextStringBuilder(replaceTemplate); + assertTrue(sub.replaceIn(bld)); + assertEquals(expectedResult, bld.toString()); + if (substring) { + bld = new TextStringBuilder(replaceTemplate); + assertTrue(sub.replaceIn(bld, 1, bld.length() - 2)); + assertEquals(expectedResult, bld.toString()); // expect full result as remainder is untouched + } + } + + /** + * For subclasses to override. + * + * @throws IOException Thrown by subclasses. + */ + protected String replace(final StringSubstitutor stringSubstitutor, final String template) throws IOException { + return stringSubstitutor.replace(template); + } + + @BeforeEach + public void setUp() throws Exception { + values = new HashMap<>(); + // shortest key and value. + values.put("a", "1"); + values.put("aa", "11"); + values.put("aaa", "111"); + values.put("b", "2"); + values.put("bb", "22"); + values.put("bbb", "222"); + values.put("a2b", "b"); + // normal key and value. + values.put("animal", ACTUAL_ANIMAL); + values.put("target", ACTUAL_TARGET); + } + + @AfterEach + public void tearDown() throws Exception { + values = null; + } + + @Test + public void testConstructorNullMap() { + final Map parameters = null; + final StringSubstitutor s = new StringSubstitutor(parameters, "prefix", "suffix"); + assertNull(s.getStringLookup().lookup("X")); + } + + @Test + public void testConstructorStringSubstitutor() { + final StringSubstitutor source = new StringSubstitutor(); + source.setDisableSubstitutionInValues(true); + source.setEnableSubstitutionInVariables(true); + source.setEnableUndefinedVariableException(true); + source.setEscapeChar('e'); + source.setValueDelimiter('d'); + source.setVariablePrefix('p'); + source.setVariableResolver(StringLookupFactory.INSTANCE.nullStringLookup()); + source.setVariableSuffix('s'); + // + final StringSubstitutor target = new StringSubstitutor(source); + // + assertTrue(target.isDisableSubstitutionInValues()); + assertTrue(target.isEnableSubstitutionInVariables()); + assertTrue(target.isEnableUndefinedVariableException()); + assertEquals('e', target.getEscapeChar()); + assertTrue(target.getValueDelimiterMatcher().toString().endsWith("['d']"), + target.getValueDelimiterMatcher().toString()); + assertTrue(target.getVariablePrefixMatcher().toString().endsWith("['p']"), + target.getValueDelimiterMatcher().toString()); + assertTrue(target.getVariableSuffixMatcher().toString().endsWith("['s']"), + target.getValueDelimiterMatcher().toString()); + } + + /** + * Tests get set. + */ + @Test + public void testGetSetEscape() { + final StringSubstitutor sub = new StringSubstitutor(); + assertEquals('$', sub.getEscapeChar()); + sub.setEscapeChar('<'); + assertEquals('<', sub.getEscapeChar()); + } + + /** + * Test for LANG-1055: StringSubstitutor.replaceSystemProperties does not work consistently + */ + @Test + public void testLANG1055() { + System.setProperty("test_key", "test_value"); + + final String expected = StringSubstitutor.replace("test_key=${test_key}", System.getProperties()); + final String actual = StringSubstitutor.replaceSystemProperties("test_key=${test_key}"); + assertEquals(expected, actual); + } + + /** + * Tests interpolation with weird boundary patterns. + */ + @Test + public void testReplace_JiraText178_WeirdPatterns1() throws IOException { + doNotReplace("$${"); + doNotReplace("$${a"); + doNotReplace("$$${"); + doNotReplace("$$${a"); + doNotReplace("$${${a"); + doNotReplace("${${a}"); // "${a" is not a registered variable name. + doNotReplace("${$${a}"); + } + + /** + * Tests interpolation with weird boundary patterns. + */ + @Test + public void testReplace_JiraText178_WeirdPatterns2() throws IOException { + doReplace("${1}", "$${${a}}", false); + } + + /** + * Tests interpolation with weird boundary patterns. + */ + @Test + @Disabled + public void testReplace_JiraText178_WeirdPatterns3() throws IOException { + doReplace("${${a}", "$${${a}", false); // not "$${1" or "${1" + } + + /** + * Tests adjacent keys. + */ + @Test + public void testReplaceAdjacentAtEnd() throws IOException { + values.put("code", "GBP"); + values.put("amount", "12.50"); + final StringSubstitutor sub = new StringSubstitutor(values); + assertEqualsCharSeq("Amount is GBP12.50", replace(sub, "Amount is ${code}${amount}")); + } + + /** + * Tests adjacent keys. + */ + @Test + public void testReplaceAdjacentAtStart() throws IOException { + values.put("code", "GBP"); + values.put("amount", "12.50"); + final StringSubstitutor sub = new StringSubstitutor(values); + assertEqualsCharSeq("GBP12.50 charged", replace(sub, "${code}${amount} charged")); + } + + /** + * Tests key replace changing map after initialization (not recommended). + */ + @Test + public void testReplaceChangedMap() throws IOException { + final StringSubstitutor sub = new StringSubstitutor(values); + // no map change + final String template = CLASSIC_TEMPLATE; + assertEqualsCharSeq(CLASSIC_RESULT, replace(sub, template)); + // map change + values.put("target", "moon"); + assertEqualsCharSeq("The quick brown fox jumps over the moon.", replace(sub, template)); + } + + /** + * Tests complex escaping. + */ + @Test + public void testReplaceComplexEscaping() throws IOException { + doReplace("${1}", "$${${a}}", false); + doReplace("${11}", "$${${aa}}", false); + doReplace("${111}", "$${${aaa}}", false); + doReplace("${quick brown fox}", "$${${animal}}", false); + doReplace("The ${quick brown fox} jumps over the lazy dog.", "The $${${animal}} jumps over the ${target}.", + true); + doReplace("${${a}}", "$${$${a}}", false); + doReplace("${${aa}}", "$${$${aa}}", false); + doReplace("${${aaa}}", "$${$${aaa}}", false); + doReplace("${${animal}}", "$${$${animal}}", false); + doReplace(".${${animal}}", ".$${$${animal}}", false); + doReplace("${${animal}}.", "$${$${animal}}.", false); + doReplace(".${${animal}}.", ".$${$${animal}}.", false); + doReplace("The ${${animal}} jumps over the lazy dog.", "The $${$${animal}} jumps over the ${target}.", true); + doReplace("The ${quick brown fox} jumps over the lazy dog. ${1234567890}.", + "The $${${animal}} jumps over the ${target}. $${${undefined.number:-1234567890}}.", true); + } + + /** + * Tests when no variable name. + */ + @Test + public void testReplaceEmptyKey() throws IOException { + doReplace("The ${} jumps over the lazy dog.", "The ${} jumps over the ${target}.", true); + } + + /** + * Tests when no variable name. + */ + @Test + public void testReplaceEmptyKeyExtraFirst() throws IOException { + assertEqualsCharSeq("." + EMPTY_EXPR, replace(new StringSubstitutor(values), "." + EMPTY_EXPR)); + } + + /** + * Tests when no variable name. + */ + @Test + public void testReplaceEmptyKeyExtraLast() throws IOException { + assertEqualsCharSeq(EMPTY_EXPR + ".", replace(new StringSubstitutor(values), EMPTY_EXPR + ".")); + } + + /** + * Tests when no variable name. + */ + @Test + public void testReplaceEmptyKeyOnly() throws IOException { + assertEquals(EMPTY_EXPR, replace(new StringSubstitutor(values), EMPTY_EXPR)); + } + + /** + * Tests when no variable name. + */ + @Test + public void testReplaceEmptyKeyShortest() throws IOException { + doNotReplace(EMPTY_EXPR); + } + + /** + * Tests when no variable name. + */ + @Test + public void testReplaceEmptyKeyWithDefault() throws IOException { + doReplace("The animal jumps over the lazy dog.", "The ${:-animal} jumps over the ${target}.", true); + } + + /** + * Tests when no variable name. + */ + @Test + public void testReplaceEmptyKeyWithDefaultOnly() throws IOException { + doReplace("animal", "${:-animal}", false); + } + + /** + * Tests when no variable name. + */ + @Test + public void testReplaceEmptyKeyWithDefaultOnlyEmpty() throws IOException { + doReplace("", "${:-}", false); + } + + /** + * Tests when no variable name. + */ + @Test + public void testReplaceEmptyKeyWithDefaultOnlyShortest() throws IOException { + doReplace("a", "${:-a}", false); + } + + /** + * Tests replace with null. + */ + @Test + public void testReplaceEmptyString() throws IOException { + doNotReplace(StringUtils.EMPTY); + } + + /** + * Tests escaping. + */ + @Test + public void testReplaceEscaping() throws IOException { + doReplace("The ${animal} jumps over the lazy dog.", "The $${animal} jumps over the ${target}.", true); + doReplace("${a}", "$${a}", false); + doReplace("${a${a}}", "$${a$${a}}", false); + doReplace("${a${a${a}}}", "$${a$${a$${a}}}", false); + } + + /** + * Tests replace with fail on undefined variable. + */ + @Test + public void testReplaceFailOnUndefinedVariable() throws IOException { + values.put("animal.1", "fox"); + values.put("animal.2", "mouse"); + values.put("species", "2"); + final StringSubstitutor sub = new StringSubstitutor(values); + sub.setEnableUndefinedVariableException(true); + + assertThatIllegalArgumentException() + .isThrownBy(() -> replace(sub, "The ${animal.${species}} jumps over the ${target}.")) + .withMessage("Cannot resolve variable 'animal.${species' (enableSubstitutionInVariables=false)."); + + assertThatIllegalArgumentException() + .isThrownBy(() -> replace(sub, "The ${animal.${species:-1}} jumps over the ${target}.")) + .withMessage("Cannot resolve variable 'animal.${species:-1' (enableSubstitutionInVariables=false)."); + + assertThatIllegalArgumentException() + .isThrownBy(() -> replace(sub, "The ${test:-statement} is a sample for missing ${unknown}.")) + .withMessage("Cannot resolve variable 'unknown' (enableSubstitutionInVariables=false)."); + + // if default value is available, exception will not be thrown + assertEqualsCharSeq("The statement is a sample for missing variable.", + replace(sub, "The ${test:-statement} is a sample for missing ${unknown:-variable}.")); + + assertEqualsCharSeq("The fox jumps over the lazy dog.", + replace(sub, "The ${animal.1} jumps over the ${target}.")); + } + + /** + * Tests whether replace with fail on undefined variable with substitution in variable names enabled. + */ + @Test + public void testReplaceFailOnUndefinedVariableWithReplaceInVariable() throws IOException { + values.put("animal.1", "fox"); + values.put("animal.2", "mouse"); + values.put("species", "2"); + values.put("statement.1", "2"); + values.put("recursive", "1"); + values.put("word", "variable"); + values.put("testok.2", "statement"); + final StringSubstitutor sub = new StringSubstitutor(values); + sub.setEnableUndefinedVariableException(true); + sub.setEnableSubstitutionInVariables(true); + + assertEqualsCharSeq("The mouse jumps over the lazy dog.", + replace(sub, "The ${animal.${species}} jumps over the ${target}.")); + values.put("species", "1"); + assertEqualsCharSeq("The fox jumps over the lazy dog.", + replace(sub, "The ${animal.${species}} jumps over the ${target}.")); + + // exception is thrown here because variable with name test.1 is missing + assertThatIllegalArgumentException() + .isThrownBy(() -> replace(sub, "The ${test.${statement}} is a sample for missing ${word}.")) + .withMessage("Cannot resolve variable 'statement' (enableSubstitutionInVariables=true)."); + + // exception is thrown here because variable with name test.2 is missing + assertThatIllegalArgumentException() + .isThrownBy(() -> replace(sub, "The ${test.${statement.${recursive}}} is a sample for missing ${word}.")) + .withMessage("Cannot resolve variable 'test.2' (enableSubstitutionInVariables=true)."); + + assertEqualsCharSeq("statement", replace(sub, "${testok.${statement.${recursive}}}")); + + assertEqualsCharSeq("${testok.2}", replace(sub, "$${testok.${statement.${recursive}}}")); + + assertEqualsCharSeq("The statement is a sample for missing variable.", + replace(sub, "The ${testok.${statement.${recursive}}} is a sample for missing ${word}.")); + } + + /** + * Tests when no incomplete prefix. + */ + @Test + public void testReplaceIncompletePrefix() throws IOException { + doReplace("The {animal} jumps over the lazy dog.", "The {animal} jumps over the ${target}.", true); + } + + @Test + public void testReplaceInTakingStringBufferWithNonNull() { + final StringSubstitutor strSubstitutor = new StringSubstitutor(new HashMap(), "WV@i#y?N*[", + "WV@i#y?N*[", '*'); + + assertFalse(strSubstitutor.isPreserveEscapes()); + assertFalse(strSubstitutor.replaceIn(new StringBuffer("WV@i#y?N*["))); + assertEquals('*', strSubstitutor.getEscapeChar()); + } + + @Test + public void testReplaceInTakingStringBuilderWithNonNull() { + final StringLookup strLookup = StringLookupFactory.INSTANCE.systemPropertyStringLookup(); + final StringSubstitutor strSubstitutor = new StringSubstitutor(strLookup, "b map = new HashMap<>(); + final StringSubstitutor strSubstitutor = new StringSubstitutor(map, StringUtils.EMPTY, StringUtils.EMPTY, 'T', + "K+<'f"); + + assertFalse(strSubstitutor.replaceIn((StringBuilder) null)); + } + + @Test + public void testReplaceInTakingTwoAndThreeIntsReturningFalse() { + final Map hashMap = new HashMap<>(); + final StringLookup mapStringLookup = StringLookupFactory.INSTANCE.mapStringLookup(hashMap); + final StringMatcher strMatcher = StringMatcherFactory.INSTANCE.tabMatcher(); + final StringSubstitutor strSubstitutor = new StringSubstitutor(mapStringLookup, strMatcher, strMatcher, 'b', + strMatcher); + + assertFalse(strSubstitutor.replaceIn((StringBuilder) null, 1315, -1369)); + assertEquals('b', strSubstitutor.getEscapeChar()); + assertFalse(strSubstitutor.isPreserveEscapes()); + } + + /** + * Tests whether a variable can be replaced in a variable name. + */ + @Test + public void testReplaceInVariable() throws IOException { + values.put("animal.1", "fox"); + values.put("animal.2", "mouse"); + values.put("species", "2"); + final StringSubstitutor sub = new StringSubstitutor(values); + sub.setEnableSubstitutionInVariables(true); + assertEqualsCharSeq("The mouse jumps over the lazy dog.", + replace(sub, "The ${animal.${species}} jumps over the ${target}.")); + values.put("species", "1"); + assertEqualsCharSeq("The fox jumps over the lazy dog.", + replace(sub, "The ${animal.${species}} jumps over the ${target}.")); + assertEqualsCharSeq("The fox jumps over the lazy dog.", replace(sub, + "The ${unknown.animal.${unknown.species:-1}:-fox} " + "jumps over the ${unknow.target:-lazy dog}.")); + } + + /** + * Tests whether substitution in variable names is disabled per default. + */ + @Test + public void testReplaceInVariableDisabled() throws IOException { + values.put("animal.1", "fox"); + values.put("animal.2", "mouse"); + values.put("species", "2"); + final StringSubstitutor sub = new StringSubstitutor(values); + assertEqualsCharSeq("The ${animal.${species}} jumps over the lazy dog.", + replace(sub, "The ${animal.${species}} jumps over the ${target}.")); + assertEqualsCharSeq("The ${animal.${species:-1}} jumps over the lazy dog.", + replace(sub, "The ${animal.${species:-1}} jumps over the ${target}.")); + } + + /** + * Tests complex and recursive substitution in variable names. + */ + @Test + public void testReplaceInVariableRecursive() throws IOException { + values.put("animal.2", "brown fox"); + values.put("animal.1", "white mouse"); + values.put("color", "white"); + values.put("species.white", "1"); + values.put("species.brown", "2"); + final StringSubstitutor sub = new StringSubstitutor(values); + sub.setEnableSubstitutionInVariables(true); + assertEqualsCharSeq("white mouse", replace(sub, "${animal.${species.${color}}}")); + assertEqualsCharSeq("The white mouse jumps over the lazy dog.", + replace(sub, "The ${animal.${species.${color}}} jumps over the ${target}.")); + assertEqualsCharSeq("The brown fox jumps over the lazy dog.", + replace(sub, "The ${animal.${species.${unknownColor:-brown}}} jumps over the ${target}.")); + } + + /** + * Tests when no variable name. + */ + @Test + public void testReplaceKeyStartChars() throws IOException { + final String substring = StringSubstitutor.DEFAULT_VAR_START + "a"; + assertEqualsCharSeq(substring, replace(new StringSubstitutor(values), substring)); + } + + /** + * Tests when no variable name. + */ + @Test + public void testReplaceKeyStartChars1Only() throws IOException { + final String substring = StringSubstitutor.DEFAULT_VAR_START.substring(0, 1); + assertEqualsCharSeq(substring, replace(new StringSubstitutor(values), substring)); + } + + /** + * Tests when no variable name. + */ + @Test + public void testReplaceKeyStartChars2Only() throws IOException { + final String substring = StringSubstitutor.DEFAULT_VAR_START.substring(0, 2); + assertEqualsCharSeq(substring, replace(new StringSubstitutor(values), substring)); + } + + /** + * Tests when no prefix or suffix. + */ + @Test + public void testReplaceNoPrefixNoSuffix() throws IOException { + doReplace("The animal jumps over the lazy dog.", "The animal jumps over the ${target}.", true); + } + + /** + * Tests when suffix but no prefix. + */ + @Test + public void testReplaceNoPrefixSuffix() throws IOException { + doReplace("The animal} jumps over the lazy dog.", "The animal} jumps over the ${target}.", true); + } + + /** + * Tests replace with no variables. + */ + @Test + public void testReplaceNoVariables() throws IOException { + doNotReplace("The balloon arrived."); + } + + /** + * Tests replace with null. + */ + @Test + public void testReplaceNull() throws IOException { + doNotReplace(null); + } + + /** + * Tests simple key replace. + */ + @Test + public void testReplacePartialString_noReplace() { + final StringSubstitutor sub = new StringSubstitutor(); + assertEqualsCharSeq("${animal} jumps", sub.replace(CLASSIC_TEMPLATE, 4, 15)); + } + + /** + * Tests when prefix but no suffix. + */ + @Test + public void testReplacePrefixNoSuffix() throws IOException { + doReplace("The ${animal jumps over the ${target} lazy dog.", "The ${animal jumps over the ${target} ${target}.", + true); + } + + /** + * Tests simple recursive replace. + */ + @Test + public void testReplaceRecursive() throws IOException { + values.put("animal", "${critter}"); + values.put("target", "${pet}"); + values.put("pet", "${petCharacteristic} dog"); + values.put("petCharacteristic", "lazy"); + values.put("critter", "${critterSpeed} ${critterColor} ${critterType}"); + values.put("critterSpeed", "quick"); + values.put("critterColor", "brown"); + values.put("critterType", "fox"); + doReplace(CLASSIC_RESULT, CLASSIC_TEMPLATE, true); + + values.put("pet", "${petCharacteristicUnknown:-lazy} dog"); + doReplace(CLASSIC_RESULT, CLASSIC_TEMPLATE, true); + } + + /** + * Tests simple key replace. + */ + @Test + public void testReplaceSimple() throws IOException { + doReplace(CLASSIC_RESULT, CLASSIC_TEMPLATE, true); + } + + /** + * Tests simple key replace. + */ + @Test + public void testReplaceSimpleKeySize1() throws IOException { + doReplace("1", "${a}", false); + } + + /** + * Tests simple key replace. + */ + @Test + public void testReplaceSimpleKeySize2() throws IOException { + doReplace("11", "${aa}", false); + } + + /** + * Tests simple key replace. + */ + @Test + public void testReplaceSimpleKeySize3() throws IOException { + doReplace("111", "${aaa}", false); + } + + @Test + public void testReplaceTakingCharSequenceReturningNull() { + final StringSubstitutor strSubstitutor = new StringSubstitutor((StringLookup) null); + + assertNull(strSubstitutor.replace((CharSequence) null)); + assertFalse(strSubstitutor.isPreserveEscapes()); + assertEquals('$', strSubstitutor.getEscapeChar()); + } + + @Test + public void testReplaceTakingThreeArgumentsThrowsNullPointerException() { + assertThatNullPointerException().isThrownBy(() -> StringSubstitutor.replace(null, (Properties) null)); + } + + @Test + public void testReplaceThrowsStringIndexOutOfBoundsException() { + final StringSubstitutor sub = new StringSubstitutor(); + + // replace(char[], int, int) + final char[] emptyCharArray = {}; + // offset greater than array length + assertThatExceptionOfType(StringIndexOutOfBoundsException.class).isThrownBy( + () -> sub.replace(emptyCharArray, 0, 1)); + // source != null && (offset > source.length || offset < 0) + assertThatExceptionOfType(StringIndexOutOfBoundsException.class).isThrownBy( + () -> sub.replace(emptyCharArray, 1, 0)); + + // replace(String, int, int) + // offset greater than source length + assertThatExceptionOfType(StringIndexOutOfBoundsException.class).isThrownBy( + () -> sub.replace("", 1, 1)); + // source != null && offset >= 0 && offset <= source.length() && (length > -offset + source.length() || length < 0) + assertThatExceptionOfType(StringIndexOutOfBoundsException.class).isThrownBy( + () -> sub.replace("", 0, 1)); + } + + /** + * Tests replace creates output same as input. + */ + @Test + public void testReplaceToIdentical() throws IOException { + values.put("animal", "$${${thing}}"); + values.put("thing", "animal"); + doReplace("The ${animal} jumps.", "The ${animal} jumps.", true); + } + + /** + * Tests unknown key replace. + */ + @Test + public void testReplaceUnknownKey() throws IOException { + doReplace("The ${person} jumps over the lazy dog.", "The ${person} jumps over the ${target}.", true); + } + + /** + * Tests unknown key replace. + */ + @Test + public void testReplaceUnknownKeyDefaultValue() throws IOException { + doReplace("The ${person} jumps over the lazy dog. 1234567890.", + "The ${person} jumps over the ${target}. ${undefined.number:-1234567890}.", true); + } + + /** + * Tests unknown key replace. + */ + @Test + public void testReplaceUnknownKeyOnly() throws IOException { + final String expected = "${person}"; + assertEqualsCharSeq(expected, replace(new StringSubstitutor(values), expected)); + } + + /** + * Tests unknown key replace. + */ + @Test + public void testReplaceUnknownKeyOnlyExtraFirst() throws IOException { + final String expected = ".${person}"; + assertEqualsCharSeq(expected, replace(new StringSubstitutor(values), expected)); + } + + /** + * Tests unknown key replace. + */ + @Test + public void testReplaceUnknownKeyOnlyExtraLast() throws IOException { + final String expected = "${person}."; + assertEqualsCharSeq(expected, replace(new StringSubstitutor(values), expected)); + } + + /** + * Tests unknown key replace. + */ + @Test + public void testReplaceUnknownShortestKeyOnly() throws IOException { + final String expected = "${U}"; + assertEqualsCharSeq(expected, replace(new StringSubstitutor(values), expected)); + } + + /** + * Tests unknown key replace. + */ + @Test + public void testReplaceUnknownShortestKeyOnlyExtraFirst() throws IOException { + final String expected = ".${U}"; + assertEqualsCharSeq(expected, replace(new StringSubstitutor(values), expected)); + } + + /** + * Tests unknown key replace. + */ + @Test + public void testReplaceUnknownShortestKeyOnlyExtraLast() throws IOException { + final String expected = "${U}."; + assertEqualsCharSeq(expected, replace(new StringSubstitutor(values), expected)); + } + + /** + * Tests simple key replace. + */ + @Test + public void testReplaceVariablesCount1() throws IOException { + doReplace(ACTUAL_ANIMAL, "${animal}", false); + } + + /** + * Tests escaping. + */ + @Test + public void testReplaceVariablesCount1Escaping2To1() throws IOException { + doReplace("${a}", "$${a}", false); + doReplace("${animal}", "$${animal}", false); + } + + /** + * Tests escaping. + */ + @Test + public void testReplaceVariablesCount1Escaping3To2() throws IOException { + doReplace("$${a}", "$$${a}", false); + doReplace("$${animal}", "$$${animal}", false); + } + + /** + * Tests escaping. + */ + @Test + public void testReplaceVariablesCount1Escaping4To3() throws IOException { + doReplace("$$${a}", "$$$${a}", false); + doReplace("$$${animal}", "$$$${animal}", false); + } + + /** + * Tests escaping. + */ + @Test + public void testReplaceVariablesCount1Escaping5To4() throws IOException { + doReplace("$$$${a}", "$$$$${a}", false); + doReplace("$$$${animal}", "$$$$${animal}", false); + } + + /** + * Tests escaping. + */ + @Test + public void testReplaceVariablesCount1Escaping6To4() throws IOException { + doReplace("$$$$${a}", "$$$$$${a}", false); + doReplace("$$$$${animal}", "$$$$$${animal}", false); + } + + /** + * Tests simple key replace. + */ + @Test + public void testReplaceVariablesCount2() throws IOException { + // doTestReplace("12", "${a}${b}", false); + doReplace("1122", "${aa}${bb}", false); + doReplace(ACTUAL_ANIMAL + ACTUAL_ANIMAL, "${animal}${animal}", false); + doReplace(ACTUAL_TARGET + ACTUAL_TARGET, "${target}${target}", false); + doReplace(ACTUAL_ANIMAL + ACTUAL_TARGET, "${animal}${target}", false); + } + + /** + * Tests simple key replace. + */ + @Test + public void testReplaceVariablesCount2NonAdjacent() throws IOException { + doReplace("1 2", "${a} ${b}", false); + doReplace("11 22", "${aa} ${bb}", false); + doReplace(ACTUAL_ANIMAL + " " + ACTUAL_ANIMAL, "${animal} ${animal}", false); + doReplace(ACTUAL_ANIMAL + " " + ACTUAL_ANIMAL, "${animal} ${animal}", false); + doReplace(ACTUAL_ANIMAL + " " + ACTUAL_ANIMAL, "${animal} ${animal}", false); + } + + /** + * Tests simple key replace. + */ + @Test + public void testReplaceVariablesCount3() throws IOException { + doReplace("121", "${a}${b}${a}", false); + doReplace("112211", "${aa}${bb}${aa}", false); + doReplace(ACTUAL_ANIMAL + ACTUAL_ANIMAL + ACTUAL_ANIMAL, "${animal}${animal}${animal}", false); + doReplace(ACTUAL_TARGET + ACTUAL_TARGET + ACTUAL_TARGET, "${target}${target}${target}", false); + } + + /** + * Tests simple key replace. + */ + @Test + public void testReplaceVariablesCount3NonAdjacent() throws IOException { + doReplace("1 2 1", "${a} ${b} ${a}", false); + doReplace("11 22 11", "${aa} ${bb} ${aa}", false); + doReplace(ACTUAL_ANIMAL + " " + ACTUAL_ANIMAL + " " + ACTUAL_ANIMAL, "${animal} ${animal} ${animal}", false); + doReplace(ACTUAL_TARGET + " " + ACTUAL_TARGET + " " + ACTUAL_TARGET, "${target} ${target} ${target}", false); + } + + /** + * Tests interpolation with weird boundary patterns. + */ + @Test + public void testReplaceWeirdPattens() throws IOException { + doNotReplace(StringUtils.EMPTY); + doNotReplace(EMPTY_EXPR); + doNotReplace("${ }"); + doNotReplace("${\t}"); + doNotReplace("${\n}"); + doNotReplace("${\b}"); + doNotReplace("${"); + doNotReplace("$}"); + doNotReplace("$$}"); + doNotReplace("}"); + doNotReplace("${}$"); + doNotReplace("${}$$"); + doNotReplace("${${"); + doNotReplace("${${}}"); + doNotReplace("${$${}}"); + doNotReplace("${$$${}}"); + doNotReplace("${$$${$}}"); + doNotReplace("${${}}"); + doNotReplace("${${ }}"); + // + doNotReplace("${$${a}}"); + doNotReplace("${$$${a}}"); + doNotReplace("${${a}}"); + doNotReplace("${${${a}"); + doNotReplace("${ ${a}"); + doNotReplace("${ ${ ${a}"); + // + doReplace("${1}", "$${${a}}", false); + doReplace("${ 1}", "$${ ${a}}", false); + doReplace("${12}", "$${${a}${b}}", false); + doReplace("${ 1 2 }", "$${ ${a} ${b} }", false); + doReplace("${${${a}2", "${${${a}${b}", false); + } + + /** + * Tests protected. + */ + @Test + public void testResolveVariable() { + final TextStringBuilder builder = new TextStringBuilder("Hi ${name}!"); + final Map map = new HashMap<>(); + map.put("name", "commons"); + final StringSubstitutor sub = new StringSubstitutor(map) { + @Override + protected String resolveVariable(final String variableName, final TextStringBuilder buf, final int startPos, + final int endPos) { + assertEquals("name", variableName); + assertSame(builder, buf); + assertEquals(3, startPos); + assertEquals(10, endPos); + return "jakarta"; + } + }; + sub.replaceIn(builder); + assertEqualsCharSeq("Hi jakarta!", builder.toString()); + } + + @Test + public void testSamePrefixAndSuffix() { + final Map map = new HashMap<>(); + map.put("greeting", "Hello"); + map.put(" there ", "XXX"); + map.put("name", "commons"); + assertEqualsCharSeq("Hi commons!", StringSubstitutor.replace("Hi @name@!", map, "@", "@")); + assertEqualsCharSeq("Hello there commons!", + StringSubstitutor.replace("@greeting@ there @name@!", map, "@", "@")); + } + + /** + * Tests static. + */ + @Test + public void testStaticReplace() { + final Map map = new HashMap<>(); + map.put("name", "commons"); + assertEqualsCharSeq("Hi commons!", StringSubstitutor.replace("Hi ${name}!", map)); + } + + /** + * Tests static. + */ + @Test + public void testStaticReplacePrefixSuffix() { + final Map map = new HashMap<>(); + map.put("name", "commons"); + assertEqualsCharSeq("Hi commons!", StringSubstitutor.replace("Hi !", map, "<", ">")); + } + + /** + * Tests interpolation with system properties. + */ + @Test + public void testStaticReplaceSystemProperties() { + final TextStringBuilder buf = new TextStringBuilder(); + buf.append("Hi ").append(System.getProperty("user.name")); + buf.append(", you are working with "); + buf.append(System.getProperty("os.name")); + buf.append(", your home directory is "); + buf.append(System.getProperty("user.home")).append('.'); + assertEqualsCharSeq(buf.toString(), StringSubstitutor.replaceSystemProperties( + "Hi ${user.name}, you are " + "working with ${os.name}, your home " + "directory is ${user.home}.")); + } + + /** + * Tests interpolation with system properties. + */ + @Test + public void testStaticReplaceSystemPropertiesWithUpdate() { + System.setProperty("foo", "bar1"); + try { + assertEqualsCharSeq("bar1", StringSubstitutor.replaceSystemProperties("${foo}")); + System.setProperty("foo", "bar2"); + assertEqualsCharSeq("bar2", StringSubstitutor.replaceSystemProperties("${foo}")); + } finally { + System.getProperties().remove("foo"); + } + } + + /** + * Test the replace of a properties object + */ + @Test + public void testSubstituteDefaultProperties() { + final String org = "${doesnotwork}"; + System.setProperty("doesnotwork", "It works!"); + + // create a new Properties object with the System.getProperties as default + final Properties props = new Properties(System.getProperties()); + + assertEqualsCharSeq("It works!", StringSubstitutor.replace(org, props)); + } + + @Test + public void testSubstitutePreserveEscape() throws IOException { + final String org = "${not-escaped} $${escaped}"; + final Map map = new HashMap<>(); + map.put("not-escaped", "value"); + + final StringSubstitutor sub = new StringSubstitutor(map, "${", "}", '$'); + assertFalse(sub.isPreserveEscapes()); + assertEqualsCharSeq("value ${escaped}", replace(sub, org)); + + sub.setPreserveEscapes(true); + assertTrue(sub.isPreserveEscapes()); + assertEqualsCharSeq("value $${escaped}", replace(sub, org)); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/StringSubstitutorWithInterpolatorStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/StringSubstitutorWithInterpolatorStringLookupTest.java new file mode 100644 index 0000000..f9ccc62 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/StringSubstitutorWithInterpolatorStringLookupTest.java @@ -0,0 +1,263 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.text.lookup.DefaultStringLookup; +import org.apache.commons.text.lookup.StringLookup; +import org.apache.commons.text.lookup.StringLookupFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class StringSubstitutorWithInterpolatorStringLookupTest { + + private static StringLookup createInterpolatorWithLookups(final DefaultStringLookup... lookups) { + final Map lookupMap = new HashMap<>(); + for (final DefaultStringLookup lookup : lookups) { + lookupMap.put(lookup.getKey().toLowerCase(), lookup.getStringLookup()); + } + + return StringLookupFactory.INSTANCE.interpolatorStringLookup(lookupMap, null, false); + } + + @Test + public void testCustomFunctionWithDefaults() { + testCustomFunctionWithDefaults(true); + } + + private void testCustomFunctionWithDefaults(final boolean addDefaultLookups) { + final String key = "key"; + final String value = "value"; + final Map map = new HashMap<>(); + map.put(key, value); + final StringLookup mapStringLookup = StringLookupFactory.INSTANCE.functionStringLookup(map::get); + final Map stringLookupMap = new HashMap<>(); + stringLookupMap.put("customLookup", mapStringLookup); + final StringSubstitutor strSubst = new StringSubstitutor( + StringLookupFactory.INSTANCE.interpolatorStringLookup(stringLookupMap, null, addDefaultLookups)); + if (addDefaultLookups) { + final String spKey = "user.name"; + Assertions.assertEquals(System.getProperty(spKey), strSubst.replace("${sys:" + spKey + "}")); + } + Assertions.assertEquals("value", strSubst.replace("${customLookup:key}")); + } + + @Test + public void testCustomFunctionWithoutDefaults() { + testCustomFunctionWithDefaults(false); + } + + @Test + public void testCustomMapWithDefaults() { + testCustomMapWithDefaults(true); + } + + private void testCustomMapWithDefaults(final boolean addDefaultLookups) { + final String key = "key"; + final String value = "value"; + final Map map = new HashMap<>(); + map.put(key, value); + final StringLookup mapStringLookup = StringLookupFactory.INSTANCE.mapStringLookup(map); + final Map stringLookupMap = new HashMap<>(); + stringLookupMap.put("customLookup", mapStringLookup); + final StringSubstitutor strSubst = new StringSubstitutor( + StringLookupFactory.INSTANCE.interpolatorStringLookup(stringLookupMap, null, addDefaultLookups)); + if (addDefaultLookups) { + final String spKey = "user.name"; + Assertions.assertEquals(System.getProperty(spKey), strSubst.replace("${sys:" + spKey + "}")); + } + Assertions.assertEquals("value", strSubst.replace("${customLookup:key}")); + Assertions.assertEquals("${UnknownLookup:key}", strSubst.replace("${UnknownLookup:key}")); + } + + @Test + public void testCustomMapWithoutDefaults() { + testCustomMapWithDefaults(false); + } + @Test + public void testDefaultInterpolator() { + // Used to cut and paste into the docs. + // @formatter:off + final StringSubstitutor interpolator = StringSubstitutor.createInterpolator(); + final String text = interpolator.replace( + "Base64 Decoder: ${base64Decoder:SGVsbG9Xb3JsZCE=}\n" + + "Base64 Encoder: ${base64Encoder:HelloWorld!}\n" + + "Java Constant: ${const:java.awt.event.KeyEvent.VK_ESCAPE}\n" + + "Date: ${date:yyyy-MM-dd}\n" + + "Environment Variable: ${env:USERNAME}\n" + + "File Content: ${file:UTF-8:src/test/resources/org/apache/commons/text/document.properties}\n" + + "Java: ${java:version}\n" + + "Localhost: ${localhost:canonical-name}\n" + + "Properties File: ${properties:src/test/resources/org/apache/commons/text/document.properties::mykey}\n" + + "Resource Bundle: ${resourceBundle:org.apache.commons.text.example.testResourceBundleLookup:mykey}\n" + + "System Property: ${sys:user.dir}\n" + + "URL Decoder: ${urlDecoder:Hello%20World%21}\n" + + "URL Encoder: ${urlEncoder:Hello World!}\n" + + "XML XPath: ${xml:src/test/resources/org/apache/commons/text/document.xml:/root/path/to/node}\n" + ); + // @formatter:on + Assertions.assertNotNull(text); + // TEXT-171: + Assertions.assertFalse(text.contains("${base64Decoder:SGVsbG9Xb3JsZCE=}")); + Assertions.assertFalse(text.contains("${base64Encoder:HelloWorld!}")); + Assertions.assertFalse(text.contains("${urlDecoder:Hello%20World%21}")); + Assertions.assertFalse(text.contains("${urlEncoder:Hello World!}")); + Assertions.assertFalse(text.contains("${resourceBundle:org.apache.commons.text.example.testResourceBundleLookup:mykey}")); + } + + @Test + public void testDefaultValueForMissingKeyInResourceBundle() { + final StringLookup interpolatorStringLookup = StringLookupFactory.INSTANCE.interpolatorStringLookup( + StringLookupFactory.INSTANCE.resourceBundleStringLookup("org.apache.commons.text.example.testResourceBundleLookup")); + assertEquals("${missingKey:-defaultValue}", interpolatorStringLookup.lookup("keyWithMissingKey")); + final StringSubstitutor stringSubstitutor = new StringSubstitutor(interpolatorStringLookup); + // The following would throw a MissingResourceException before TEXT-165. + assertEquals("defaultValue", stringSubstitutor.replace("${keyWithMissingKey}")); + } + + @Test + public void testDnsLookup() throws UnknownHostException { + final StringSubstitutor strSubst = + new StringSubstitutor(createInterpolatorWithLookups(DefaultStringLookup.DNS)); + final String hostName = InetAddress.getLocalHost().getHostName(); + Assertions.assertEquals(InetAddress.getByName(hostName).getHostAddress(), + strSubst.replace("${dns:" + hostName + "}")); + } + + @Test + public void testDnsLookup_disabledByDefault() throws UnknownHostException { + final StringSubstitutor strSubst = StringSubstitutor.createInterpolator(); + final String hostName = InetAddress.getLocalHost().getHostName(); + final String input = "${dns:" + hostName + "}"; + Assertions.assertEquals(input, strSubst.replace(input)); + } + + @Test + public void testDnsLookupAddress() throws UnknownHostException { + final StringSubstitutor strSubst = + new StringSubstitutor(createInterpolatorWithLookups(DefaultStringLookup.DNS)); + Assertions.assertEquals(InetAddress.getByName("apache.org").getHostAddress(), + strSubst.replace("${dns:address|apache.org}")); + } + + @Test + public void testDnsLookupCanonicalName() throws UnknownHostException { + final StringSubstitutor strSubst = + new StringSubstitutor(createInterpolatorWithLookups(DefaultStringLookup.DNS)); + final String address = InetAddress.getLocalHost().getHostAddress(); + final InetAddress inetAddress = InetAddress.getByName(address); + Assertions.assertEquals(inetAddress.getCanonicalHostName(), + strSubst.replace("${dns:canonical-name|" + address + "}")); + } + + @Test + public void testDnsLookupName() throws UnknownHostException { + final StringSubstitutor strSubst = + new StringSubstitutor(createInterpolatorWithLookups(DefaultStringLookup.DNS)); + final String address = InetAddress.getLocalHost().getHostAddress(); + final InetAddress inetAddress = InetAddress.getByName(address); + Assertions.assertEquals(inetAddress.getHostName(), strSubst.replace("${dns:name|" + address + "}")); + } + + @Test + public void testDnsLookupNameUntrimmed() throws UnknownHostException { + final StringSubstitutor strSubst = + new StringSubstitutor(createInterpolatorWithLookups(DefaultStringLookup.DNS)); + final String address = InetAddress.getLocalHost().getHostAddress(); + final InetAddress inetAddress = InetAddress.getByName(address); + Assertions.assertEquals(inetAddress.getHostName(), strSubst.replace("${dns:name| " + address + " }")); + } + + @Test + public void testDnsLookupUnknown() { + final StringSubstitutor strSubst = + new StringSubstitutor(createInterpolatorWithLookups(DefaultStringLookup.DNS)); + final String unknown = "${dns: u n k n o w n}"; + Assertions.assertEquals(unknown, strSubst.replace(unknown)); + } + + @Test + public void testJavaScript() { + final StringSubstitutor strSubst = + new StringSubstitutor(createInterpolatorWithLookups(DefaultStringLookup.SCRIPT)); + + Assertions.assertEquals("Hello World!", strSubst.replace("${script:javascript:\"Hello World!\"}")); + Assertions.assertEquals("7", strSubst.replace("${script:javascript:3 + 4}")); + } + + @Test + public void testJavaScript_disabledByDefault() { + final StringSubstitutor strSubst = StringSubstitutor.createInterpolator(); + + Assertions.assertEquals("${script:javascript:3 + 4}", strSubst.replace("${script:javascript:3 + 4}")); + } + + @Test + public void testLocalHostLookup_Address() throws UnknownHostException { + final StringSubstitutor strSubst = StringSubstitutor.createInterpolator(); + Assertions.assertEquals(InetAddress.getLocalHost().getHostAddress(), strSubst.replace("${localhost:address}")); + } + + @Test + public void testLocalHostLookup_CanonicalName() throws UnknownHostException { + final StringSubstitutor strSubst = StringSubstitutor.createInterpolator(); + Assertions.assertEquals(InetAddress.getLocalHost().getCanonicalHostName(), + strSubst.replace("${localhost:canonical-name}")); + } + + @Test + public void testLocalHostLookup_Name() throws UnknownHostException { + final StringSubstitutor strSubst = StringSubstitutor.createInterpolator(); + Assertions.assertEquals(InetAddress.getLocalHost().getHostName(), strSubst.replace("${localhost:name}")); + } + + @Test + public void testMapAndSystemProperty() { + final String key = "key"; + final String value = "value"; + final Map map = new HashMap<>(); + map.put(key, value); + final StringSubstitutor strSubst = new StringSubstitutor( + StringLookupFactory.INSTANCE.interpolatorStringLookup(map)); + final String spKey = "user.name"; + Assertions.assertEquals(System.getProperty(spKey), strSubst.replace("${sys:" + spKey + "}")); + Assertions.assertEquals(value, strSubst.replace("${" + key + "}")); + } + + @Test + public void testSystemProperty() { + final StringSubstitutor strSubst = StringSubstitutor.createInterpolator(); + final String spKey = "user.name"; + Assertions.assertEquals(System.getProperty(spKey), strSubst.replace("${sys:" + spKey + "}")); + } + + @Test + public void testSystemPropertyDefaultStringLookup() { + final StringSubstitutor strSubst = new StringSubstitutor( + StringLookupFactory.INSTANCE.interpolatorStringLookup(StringLookupFactory.INSTANCE.systemPropertyStringLookup())); + final String spKey = "user.name"; + Assertions.assertEquals(System.getProperty(spKey), strSubst.replace("${" + spKey + "}")); + Assertions.assertEquals(System.getProperty(spKey), strSubst.replace("${sys:" + spKey + "}")); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/StringTokenizerTest.java b/sources/src/test/java/org/apache/commons/text/StringTokenizerTest.java new file mode 100644 index 0000000..458cc81 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/StringTokenizerTest.java @@ -0,0 +1,935 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.text.matcher.StringMatcher; +import org.apache.commons.text.matcher.StringMatcherFactory; +import org.junit.jupiter.api.Test; + +/** + * Unit test for {@link StringTokenizer}. + */ +public class StringTokenizerTest { + + private static final String CSV_SIMPLE_FIXTURE = "A,b,c"; + + private static final String TSV_SIMPLE_FIXTURE = "A\tb\tc"; + + private void checkClone(final StringTokenizer tokenizer) { + assertNotSame(StringTokenizer.getCSVInstance(), tokenizer); + assertNotSame(StringTokenizer.getTSVInstance(), tokenizer); + } + + @Test + public void test1() { + + final String input = "a;b;c;\"d;\"\"e\";f; ; ; "; + final StringTokenizer tok = new StringTokenizer(input); + tok.setDelimiterChar(';'); + tok.setQuoteChar('"'); + tok.setIgnoredMatcher(StringMatcherFactory.INSTANCE.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + final String[] tokens = tok.getTokenArray(); + + final String[] expected = {"a", "b", "c", "d;\"e", "f", "", "", "" }; + + assertEquals(expected.length, tokens.length, Arrays.toString(tokens)); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], tokens[i], + "token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'"); + } + + } + + @Test + public void test2() { + + final String input = "a;b;c ;\"d;\"\"e\";f; ; ;"; + final StringTokenizer tok = new StringTokenizer(input); + tok.setDelimiterChar(';'); + tok.setQuoteChar('"'); + tok.setIgnoredMatcher(StringMatcherFactory.INSTANCE.noneMatcher()); + tok.setIgnoreEmptyTokens(false); + final String[] tokens = tok.getTokenArray(); + + final String[] expected = {"a", "b", "c ", "d;\"e", "f", " ", " ", "" }; + + assertEquals(expected.length, tokens.length, Arrays.toString(tokens)); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], tokens[i], + "token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'"); + } + + } + + @Test + public void test3() { + + final String input = "a;b; c;\"d;\"\"e\";f; ; ;"; + final StringTokenizer tok = new StringTokenizer(input); + tok.setDelimiterChar(';'); + tok.setQuoteChar('"'); + tok.setIgnoredMatcher(StringMatcherFactory.INSTANCE.noneMatcher()); + tok.setIgnoreEmptyTokens(false); + final String[] tokens = tok.getTokenArray(); + + final String[] expected = {"a", "b", " c", "d;\"e", "f", " ", " ", "" }; + + assertEquals(expected.length, tokens.length, Arrays.toString(tokens)); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], tokens[i], + "token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'"); + } + + } + + @Test + public void test4() { + + final String input = "a;b; c;\"d;\"\"e\";f; ; ;"; + final StringTokenizer tok = new StringTokenizer(input); + tok.setDelimiterChar(';'); + tok.setQuoteChar('"'); + tok.setIgnoredMatcher(StringMatcherFactory.INSTANCE.trimMatcher()); + tok.setIgnoreEmptyTokens(true); + final String[] tokens = tok.getTokenArray(); + + final String[] expected = {"a", "b", "c", "d;\"e", "f" }; + + assertEquals(expected.length, tokens.length, Arrays.toString(tokens)); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], tokens[i], + "token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'"); + } + + } + + @Test + public void test5() { + + final String input = "a;b; c;\"d;\"\"e\";f; ; ;"; + final StringTokenizer tok = new StringTokenizer(input); + tok.setDelimiterChar(';'); + tok.setQuoteChar('"'); + tok.setIgnoredMatcher(StringMatcherFactory.INSTANCE.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + final String[] tokens = tok.getTokenArray(); + + final String[] expected = {"a", "b", "c", "d;\"e", "f", null, null, null }; + + assertEquals(expected.length, tokens.length, Arrays.toString(tokens)); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], tokens[i], + "token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'"); + } + + } + + @Test + public void test6() { + + final String input = "a;b; c;\"d;\"\"e\";f; ; ;"; + final StringTokenizer tok = new StringTokenizer(input); + tok.setDelimiterChar(';'); + tok.setQuoteChar('"'); + tok.setIgnoredMatcher(StringMatcherFactory.INSTANCE.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + // tok.setTreatingEmptyAsNull(true); + final String[] tokens = tok.getTokenArray(); + + final String[] expected = {"a", "b", " c", "d;\"e", "f", null, null, null }; + + int nextCount = 0; + while (tok.hasNext()) { + tok.next(); + nextCount++; + } + + int prevCount = 0; + while (tok.hasPrevious()) { + tok.previous(); + prevCount++; + } + + assertEquals(expected.length, tokens.length, Arrays.toString(tokens)); + assertEquals(nextCount, expected.length, "could not cycle through entire token list using the 'hasNext' and 'next' methods"); + assertEquals(prevCount, expected.length, "could not cycle through entire token list using the 'hasPrevious' and 'previous' methods"); + } + + @Test + public void test7() { + + final String input = "a b c \"d e\" f "; + final StringTokenizer tok = new StringTokenizer(input); + tok.setDelimiterMatcher(StringMatcherFactory.INSTANCE.spaceMatcher()); + tok.setQuoteMatcher(StringMatcherFactory.INSTANCE.doubleQuoteMatcher()); + tok.setIgnoredMatcher(StringMatcherFactory.INSTANCE.noneMatcher()); + tok.setIgnoreEmptyTokens(false); + final String[] tokens = tok.getTokenArray(); + + final String[] expected = {"a", "", "", "b", "c", "d e", "f", "" }; + + assertEquals(expected.length, tokens.length, Arrays.toString(tokens)); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], tokens[i], + "token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'"); + } + + } + + @Test + public void test8() { + + final String input = "a b c \"d e\" f "; + final StringTokenizer tok = new StringTokenizer(input); + tok.setDelimiterMatcher(StringMatcherFactory.INSTANCE.spaceMatcher()); + tok.setQuoteMatcher(StringMatcherFactory.INSTANCE.doubleQuoteMatcher()); + tok.setIgnoredMatcher(StringMatcherFactory.INSTANCE.noneMatcher()); + tok.setIgnoreEmptyTokens(true); + final String[] tokens = tok.getTokenArray(); + + final String[] expected = {"a", "b", "c", "d e", "f" }; + + assertEquals(expected.length, tokens.length, Arrays.toString(tokens)); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], tokens[i], + "token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'"); + } + + } + + @Test + public void testBasic1() { + final String input = "a b c"; + final StringTokenizer tok = new StringTokenizer(input); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasic2() { + final String input = "a \nb\fc"; + final StringTokenizer tok = new StringTokenizer(input); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasic3() { + final String input = "a \nb\u0001\fc"; + final StringTokenizer tok = new StringTokenizer(input); + assertEquals("a", tok.next()); + assertEquals("b\u0001", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasic4() { + final String input = "a \"b\" c"; + final StringTokenizer tok = new StringTokenizer(input); + assertEquals("a", tok.next()); + assertEquals("\"b\"", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasic5() { + final String input = "a:b':c"; + final StringTokenizer tok = new StringTokenizer(input, ':', '\''); + assertEquals("a", tok.next()); + assertEquals("b'", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicDelim1() { + final String input = "a:b:c"; + final StringTokenizer tok = new StringTokenizer(input, ':'); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicDelim2() { + final String input = "a:b:c"; + final StringTokenizer tok = new StringTokenizer(input, ','); + assertEquals("a:b:c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicEmpty1() { + final String input = "a b c"; + final StringTokenizer tok = new StringTokenizer(input); + tok.setIgnoreEmptyTokens(false); + assertEquals("a", tok.next()); + assertEquals("", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicEmpty2() { + final String input = "a b c"; + final StringTokenizer tok = new StringTokenizer(input); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertNull(tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicIgnoreTrimmed1() { + final String input = "a: bIGNOREc : "; + final StringTokenizer tok = new StringTokenizer(input, ':'); + tok.setIgnoredMatcher(StringMatcherFactory.INSTANCE.stringMatcher("IGNORE")); + tok.setTrimmerMatcher(StringMatcherFactory.INSTANCE.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("bc", tok.next()); + assertNull(tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicIgnoreTrimmed2() { + final String input = "IGNOREaIGNORE: IGNORE bIGNOREc IGNORE : IGNORE "; + final StringTokenizer tok = new StringTokenizer(input, ':'); + tok.setIgnoredMatcher(StringMatcherFactory.INSTANCE.stringMatcher("IGNORE")); + tok.setTrimmerMatcher(StringMatcherFactory.INSTANCE.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("bc", tok.next()); + assertNull(tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicIgnoreTrimmed3() { + final String input = "IGNOREaIGNORE: IGNORE bIGNOREc IGNORE : IGNORE "; + final StringTokenizer tok = new StringTokenizer(input, ':'); + tok.setIgnoredMatcher(StringMatcherFactory.INSTANCE.stringMatcher("IGNORE")); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals(" bc ", tok.next()); + assertEquals(" ", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicIgnoreTrimmed4() { + final String input = "IGNOREaIGNORE: IGNORE 'bIGNOREc'IGNORE'd' IGNORE : IGNORE "; + final StringTokenizer tok = new StringTokenizer(input, ':', '\''); + tok.setIgnoredMatcher(StringMatcherFactory.INSTANCE.stringMatcher("IGNORE")); + tok.setTrimmerMatcher(StringMatcherFactory.INSTANCE.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("bIGNOREcd", tok.next()); + assertNull(tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicQuoted1() { + final String input = "a 'b' c"; + final StringTokenizer tok = new StringTokenizer(input, ' ', '\''); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicQuoted2() { + final String input = "a:'b':"; + final StringTokenizer tok = new StringTokenizer(input, ':', '\''); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertNull(tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicQuoted3() { + final String input = "a:'b''c'"; + final StringTokenizer tok = new StringTokenizer(input, ':', '\''); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("b'c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicQuoted4() { + final String input = "a: 'b' 'c' :d"; + final StringTokenizer tok = new StringTokenizer(input, ':', '\''); + tok.setTrimmerMatcher(StringMatcherFactory.INSTANCE.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("b c", tok.next()); + assertEquals("d", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicQuoted5() { + final String input = "a: 'b'x'c' :d"; + final StringTokenizer tok = new StringTokenizer(input, ':', '\''); + tok.setTrimmerMatcher(StringMatcherFactory.INSTANCE.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("bxc", tok.next()); + assertEquals("d", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicQuoted6() { + final String input = "a:'b'\"c':d"; + final StringTokenizer tok = new StringTokenizer(input, ':'); + tok.setQuoteMatcher(StringMatcherFactory.INSTANCE.quoteMatcher()); + assertEquals("a", tok.next()); + assertEquals("b\"c:d", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicQuoted7() { + final String input = "a:\"There's a reason here\":b"; + final StringTokenizer tok = new StringTokenizer(input, ':'); + tok.setQuoteMatcher(StringMatcherFactory.INSTANCE.quoteMatcher()); + assertEquals("a", tok.next()); + assertEquals("There's a reason here", tok.next()); + assertEquals("b", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicQuotedTrimmed1() { + final String input = "a: 'b' :"; + final StringTokenizer tok = new StringTokenizer(input, ':', '\''); + tok.setTrimmerMatcher(StringMatcherFactory.INSTANCE.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertNull(tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicTrimmed1() { + final String input = "a: b : "; + final StringTokenizer tok = new StringTokenizer(input, ':'); + tok.setTrimmerMatcher(StringMatcherFactory.INSTANCE.trimMatcher()); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertNull(tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testBasicTrimmed2() { + final String input = "a: b :"; + final StringTokenizer tok = new StringTokenizer(input, ':'); + tok.setTrimmerMatcher(StringMatcherFactory.INSTANCE.stringMatcher(" ")); + tok.setIgnoreEmptyTokens(false); + tok.setEmptyTokenAsNull(true); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertNull(tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testChaining() { + final StringTokenizer tok = new StringTokenizer(); + assertEquals(tok, tok.reset()); + assertEquals(tok, tok.reset("")); + assertEquals(tok, tok.reset(ArrayUtils.EMPTY_CHAR_ARRAY)); + assertEquals(tok, tok.setDelimiterChar(' ')); + assertEquals(tok, tok.setDelimiterString(" ")); + assertEquals(tok, tok.setDelimiterMatcher(null)); + assertEquals(tok, tok.setQuoteChar(' ')); + assertEquals(tok, tok.setQuoteMatcher(null)); + assertEquals(tok, tok.setIgnoredChar(' ')); + assertEquals(tok, tok.setIgnoredMatcher(null)); + assertEquals(tok, tok.setTrimmerMatcher(null)); + assertEquals(tok, tok.setEmptyTokenAsNull(false)); + assertEquals(tok, tok.setIgnoreEmptyTokens(false)); + } + + /** + * Tests that the {@link StringTokenizer#clone()} clone method catches {@link CloneNotSupportedException} and + * returns {@code null}. + */ + @Test + public void testCloneNotSupportedException() { + final Object notCloned = new StringTokenizer() { + + @Override + Object cloneReset() throws CloneNotSupportedException { + throw new CloneNotSupportedException("test"); + } + }.clone(); + assertNull(notCloned); + } + + @Test + public void testCloneNull() { + final StringTokenizer tokenizer = new StringTokenizer((char[]) null); + // Start sanity check + assertNull(tokenizer.nextToken()); + tokenizer.reset(); + assertNull(tokenizer.nextToken()); + // End sanity check + final StringTokenizer clonedTokenizer = (StringTokenizer) tokenizer.clone(); + tokenizer.reset(); + assertNull(tokenizer.nextToken()); + assertNull(clonedTokenizer.nextToken()); + } + + @Test + public void testCloneReset() { + final char[] input = { 'a' }; + final StringTokenizer tokenizer = new StringTokenizer(input); + // Start sanity check + assertEquals("a", tokenizer.nextToken()); + tokenizer.reset(input); + assertEquals("a", tokenizer.nextToken()); + // End sanity check + final StringTokenizer clonedTokenizer = (StringTokenizer) tokenizer.clone(); + input[0] = 'b'; + tokenizer.reset(input); + assertEquals("b", tokenizer.nextToken()); + assertEquals("a", clonedTokenizer.nextToken()); + } + + @Test + public void testConstructor_charArray() { + StringTokenizer tok = new StringTokenizer("a b".toCharArray()); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertFalse(tok.hasNext()); + + tok = new StringTokenizer(ArrayUtils.EMPTY_CHAR_ARRAY); + assertFalse(tok.hasNext()); + + tok = new StringTokenizer((char[]) null); + assertFalse(tok.hasNext()); + } + + @Test + public void testConstructor_charArray_char() { + StringTokenizer tok = new StringTokenizer("a b".toCharArray(), ' '); + assertEquals(1, tok.getDelimiterMatcher().isMatch(" ".toCharArray(), 0, 0, 1)); + assertEquals(1, tok.getDelimiterMatcher().isMatch(" ", 0, 0, 1)); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertFalse(tok.hasNext()); + + tok = new StringTokenizer(ArrayUtils.EMPTY_CHAR_ARRAY, ' '); + assertFalse(tok.hasNext()); + + tok = new StringTokenizer((char[]) null, ' '); + assertFalse(tok.hasNext()); + } + + @Test + public void testConstructor_charArray_char_char() { + StringTokenizer tok = new StringTokenizer("a b".toCharArray(), ' ', '"'); + assertEquals(1, tok.getDelimiterMatcher().isMatch(" ".toCharArray(), 0, 0, 1)); + assertEquals(1, tok.getDelimiterMatcher().isMatch(" ", 0, 0, 1)); + assertEquals(1, tok.getQuoteMatcher().isMatch("\"".toCharArray(), 0, 0, 1)); + assertEquals(1, tok.getQuoteMatcher().isMatch("\"", 0, 0, 1)); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertFalse(tok.hasNext()); + + tok = new StringTokenizer(ArrayUtils.EMPTY_CHAR_ARRAY, ' ', '"'); + assertFalse(tok.hasNext()); + + tok = new StringTokenizer((char[]) null, ' ', '"'); + assertFalse(tok.hasNext()); + } + + @Test + public void testConstructor_String() { + StringTokenizer tok = new StringTokenizer("a b"); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertFalse(tok.hasNext()); + + tok = new StringTokenizer(""); + assertFalse(tok.hasNext()); + + tok = new StringTokenizer((String) null); + assertFalse(tok.hasNext()); + } + + @Test + public void testConstructor_String_char() { + StringTokenizer tok = new StringTokenizer("a b", ' '); + assertEquals(1, tok.getDelimiterMatcher().isMatch(" ".toCharArray(), 0, 0, 1)); + assertEquals(1, tok.getDelimiterMatcher().isMatch(" ", 0, 0, 1)); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertFalse(tok.hasNext()); + + tok = new StringTokenizer("", ' '); + assertFalse(tok.hasNext()); + + tok = new StringTokenizer((String) null, ' '); + assertFalse(tok.hasNext()); + } + + @Test + public void testConstructor_String_char_char() { + StringTokenizer tok = new StringTokenizer("a b", ' ', '"'); + assertEquals(1, tok.getDelimiterMatcher().isMatch(" ".toCharArray(), 0, 0, 1)); + assertEquals(1, tok.getDelimiterMatcher().isMatch(" ", 0, 0, 1)); + assertEquals(1, tok.getQuoteMatcher().isMatch("\"".toCharArray(), 0, 0, 1)); + assertEquals(1, tok.getQuoteMatcher().isMatch("\"", 0, 0, 1)); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertFalse(tok.hasNext()); + + tok = new StringTokenizer("", ' ', '"'); + assertFalse(tok.hasNext()); + + tok = new StringTokenizer((String) null, ' ', '"'); + assertFalse(tok.hasNext()); + } + + private void testCSV(final String data) { + this.testXSVAbc(StringTokenizer.getCSVInstance(data)); + this.testXSVAbc(StringTokenizer.getCSVInstance(data.toCharArray())); + } + + @Test + public void testCSVEmpty() { + this.testEmpty(StringTokenizer.getCSVInstance()); + this.testEmpty(StringTokenizer.getCSVInstance("")); + } + + @Test + public void testCSVSimple() { + this.testCSV(CSV_SIMPLE_FIXTURE); + } + + @Test + public void testCSVSimpleNeedsTrim() { + this.testCSV(" " + CSV_SIMPLE_FIXTURE); + this.testCSV(" \n\t " + CSV_SIMPLE_FIXTURE); + this.testCSV(" \n " + CSV_SIMPLE_FIXTURE + "\n\n\r"); + } + + @Test + public void testDelimMatcher() { + final String input = "a/b\\c"; + final StringMatcher delimMatcher = StringMatcherFactory.INSTANCE.charSetMatcher('/', '\\'); + + final StringTokenizer tok = new StringTokenizer(input, delimMatcher); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testDelimMatcherQuoteMatcher() { + final String input = "`a`;`b`;`c`"; + final StringMatcher delimMatcher = StringMatcherFactory.INSTANCE.charSetMatcher(';'); + final StringMatcher quoteMatcher = StringMatcherFactory.INSTANCE.charSetMatcher('`'); + + final StringTokenizer tok = new StringTokenizer(input, delimMatcher, quoteMatcher); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testDelimString() { + final String input = "a##b##c"; + final StringTokenizer tok = new StringTokenizer(input, "##"); + + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + void testEmpty(final StringTokenizer tokenizer) { + this.checkClone(tokenizer); + assertFalse(tokenizer.hasNext()); + assertFalse(tokenizer.hasPrevious()); + assertNull(tokenizer.nextToken()); + assertEquals(0, tokenizer.size()); + assertThrows(NoSuchElementException.class, tokenizer::next); + } + + @Test + public void testGetContent() { + final String input = "a b c \"d e\" f "; + StringTokenizer tok = new StringTokenizer(input); + assertEquals(input, tok.getContent()); + + tok = new StringTokenizer(input.toCharArray()); + assertEquals(input, tok.getContent()); + + tok = new StringTokenizer(); + assertNull(tok.getContent()); + } + + @Test + public void testIteration() { + final StringTokenizer tkn = new StringTokenizer("a b c"); + assertFalse(tkn.hasPrevious()); + assertThrows(NoSuchElementException.class, tkn::previous); + assertTrue(tkn.hasNext()); + + assertEquals("a", tkn.next()); + assertThrows(UnsupportedOperationException.class, tkn::remove); + assertThrows(UnsupportedOperationException.class, () -> tkn.set("x")); + assertThrows(UnsupportedOperationException.class, () -> tkn.add("y")); + assertTrue(tkn.hasPrevious()); + assertTrue(tkn.hasNext()); + + assertEquals("b", tkn.next()); + assertTrue(tkn.hasPrevious()); + assertTrue(tkn.hasNext()); + + assertEquals("c", tkn.next()); + assertTrue(tkn.hasPrevious()); + assertFalse(tkn.hasNext()); + + assertThrows(NoSuchElementException.class, tkn::next); + assertTrue(tkn.hasPrevious()); + assertFalse(tkn.hasNext()); + } + + @Test + public void testListArray() { + final String input = "a b c"; + final StringTokenizer tok = new StringTokenizer(input); + final String[] array = tok.getTokenArray(); + final List list = tok.getTokenList(); + + assertEquals(Arrays.asList(array), list); + assertEquals(3, list.size()); + + // Test modification of the returned list + list.set(0, "z"); + list.remove(1); + list.set(1, "y"); + list.add("x"); + + assertEquals(Arrays.asList(new String[] {"z", "y", "x"}), list); + + // The tokenizer is unchanged + assertEquals(Arrays.asList(array), tok.getTokenList()); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + } + + @Test + public void testPreviousTokenAndSetEmptyTokenAsNull() { + final StringTokenizer strTokenizer = StringTokenizer.getTSVInstance(" \t\n\r\f"); + strTokenizer.setEmptyTokenAsNull(true); + + assertNull(strTokenizer.previousToken()); + } + + @Test + public void testReset() { + final StringTokenizer tok = new StringTokenizer("a b c"); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + + tok.reset(); + assertEquals("a", tok.next()); + assertEquals("b", tok.next()); + assertEquals("c", tok.next()); + assertFalse(tok.hasNext()); + } + + @Test + public void testReset_charArray() { + final StringTokenizer tok = new StringTokenizer("x x x"); + + final char[] array = {'a', 'b', 'c' }; + tok.reset(array); + assertEquals("abc", tok.next()); + assertFalse(tok.hasNext()); + + tok.reset((char[]) null); + assertFalse(tok.hasNext()); + } + + @Test + public void testReset_String() { + final StringTokenizer tok = new StringTokenizer("x x x"); + tok.reset("d e"); + assertEquals("d", tok.next()); + assertEquals("e", tok.next()); + assertFalse(tok.hasNext()); + + tok.reset((String) null); + assertFalse(tok.hasNext()); + } + + @Test + public void testStringTokenizerQuoteMatcher() { + final char[] chars = {'\'', 'a', 'c', '\'', 'd' }; + final StringTokenizer tokens = new StringTokenizer(chars, StringMatcherFactory.INSTANCE.commaMatcher(), + StringMatcherFactory.INSTANCE.quoteMatcher()); + assertEquals("acd", tokens.next()); + } + + @Test + public void testStringTokenizerStringMatcher() { + final char[] chars = {'a', 'b', 'c', 'd' }; + final StringTokenizer tokens = new StringTokenizer(chars, "bc"); + assertEquals("a", tokens.next()); + assertEquals("d", tokens.next()); + } + + @Test + public void testStringTokenizerStrMatcher() { + final char[] chars = {'a', ',', 'c' }; + final StringTokenizer tokens = new StringTokenizer(chars, StringMatcherFactory.INSTANCE.commaMatcher()); + assertEquals("a", tokens.next()); + assertEquals("c", tokens.next()); + } + + @Test + public void testTokenizeSubclassInputChange() { + final StringTokenizer tkn = new StringTokenizer("a b c d e") { + + @Override + protected List tokenize(final char[] chars, final int offset, final int count) { + return super.tokenize("w x y z".toCharArray(), 2, 5); + } + }; + assertEquals("x", tkn.next()); + assertEquals("y", tkn.next()); + } + + @Test + public void testTokenizeSubclassOutputChange() { + final StringTokenizer tkn = new StringTokenizer("a b c") { + + @Override + protected List tokenize(final char[] chars, final int offset, final int count) { + final List list = super.tokenize(chars, offset, count); + Collections.reverse(list); + return list; + } + }; + assertEquals("c", tkn.next()); + assertEquals("b", tkn.next()); + assertEquals("a", tkn.next()); + } + + @Test + public void testToString() { + final StringTokenizer tkn = new StringTokenizer("a b c d e"); + assertEquals("StringTokenizer[not tokenized yet]", tkn.toString()); + tkn.next(); + assertEquals("StringTokenizer[a, b, c, d, e]", tkn.toString()); + } + + @Test + public void testTSV() { + this.testXSVAbc(StringTokenizer.getTSVInstance(TSV_SIMPLE_FIXTURE)); + this.testXSVAbc(StringTokenizer.getTSVInstance(TSV_SIMPLE_FIXTURE.toCharArray())); + } + + @Test + public void testTSVEmpty() { + this.testEmpty(StringTokenizer.getTSVInstance()); + this.testEmpty(StringTokenizer.getTSVInstance("")); + } + + void testXSVAbc(final StringTokenizer tokenizer) { + this.checkClone(tokenizer); + assertEquals(-1, tokenizer.previousIndex()); + assertEquals(0, tokenizer.nextIndex()); + assertNull(tokenizer.previousToken()); + assertEquals("A", tokenizer.nextToken()); + assertEquals(1, tokenizer.nextIndex()); + assertEquals("b", tokenizer.nextToken()); + assertEquals(2, tokenizer.nextIndex()); + assertEquals("c", tokenizer.nextToken()); + assertEquals(3, tokenizer.nextIndex()); + assertNull(tokenizer.nextToken()); + assertEquals(3, tokenizer.nextIndex()); + assertEquals("c", tokenizer.previousToken()); + assertEquals(2, tokenizer.nextIndex()); + assertEquals("b", tokenizer.previousToken()); + assertEquals(1, tokenizer.nextIndex()); + assertEquals("A", tokenizer.previousToken()); + assertEquals(0, tokenizer.nextIndex()); + assertNull(tokenizer.previousToken()); + assertEquals(0, tokenizer.nextIndex()); + assertEquals(-1, tokenizer.previousIndex()); + assertEquals(3, tokenizer.size()); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/TextStringBuilderAppendInsertTest.java b/sources/src/test/java/org/apache/commons/text/TextStringBuilderAppendInsertTest.java new file mode 100644 index 0000000..3ea31f7 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/TextStringBuilderAppendInsertTest.java @@ -0,0 +1,1181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.text.DecimalFormatSymbols; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; + +import org.apache.commons.lang3.ArrayUtils; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link TextStringBuilder}. + */ +public class TextStringBuilderAppendInsertTest { + + /** The system line separator. */ + private static final String SEP = System.lineSeparator(); + + /** Test subclass of Object, with a toString method. */ + private static final Object FOO = new Object() { + @Override + public String toString() { + return "foo"; + } + }; + + @Test + public void testAppend_Boolean() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.append(true); + assertThat(sb.toString()).isEqualTo("true"); + + sb.append(false); + assertThat(sb.toString()).isEqualTo("truefalse"); + + sb.append('!'); + assertThat(sb.toString()).isEqualTo("truefalse!"); + } + + @Test + public void testAppend_CharArray() { + TextStringBuilder sb = new TextStringBuilder(); + sb.setNullText("NULL").append((char[]) null); + assertThat(sb.toString()).isEqualTo("NULL"); + + sb = new TextStringBuilder(); + sb.append(ArrayUtils.EMPTY_CHAR_ARRAY); + assertThat(sb.toString()).isEqualTo(""); + + sb.append(new char[] {'f', 'o', 'o'}); + assertThat(sb.toString()).isEqualTo("foo"); + } + + @Test + public void testAppend_CharArray_int_int() { + final TextStringBuilder sb0 = new TextStringBuilder(); + sb0.setNullText("NULL").append((char[]) null, 0, 1); + assertThat(sb0.toString()).isEqualTo("NULL"); + + final TextStringBuilder sb = new TextStringBuilder(); + sb.append(new char[] {'f', 'o', 'o'}, 0, 3); + assertThat(sb.toString()).isEqualTo("foo"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new char[] {'b', 'a', 'r'}, -1, 1), + "append(char[], -1,) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new char[] {'b', 'a', 'r'}, 3, 1), + "append(char[], 3,) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new char[] {'b', 'a', 'r'}, 1, -1), + "append(char[],, -1) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new char[] {'b', 'a', 'r'}, 1, 3), + "append(char[], 1, 3) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new char[] {'b', 'a', 'r'}, -1, 3), + "append(char[], -1, 3) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new char[] {'b', 'a', 'r'}, 4, 0), + "append(char[], 4, 0) expected IndexOutOfBoundsException"); + + sb.append(new char[] {'b', 'a', 'r'}, 3, 0); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new char[] {'a', 'b', 'c', 'b', 'a', 'r', 'd', 'e', 'f'}, 3, 3); + assertThat(sb.toString()).isEqualTo("foobar"); + } + + @Test + public void testAppend_FormattedString() { + TextStringBuilder sb; + + sb = new TextStringBuilder(); + sb.append("Hi", (Object[]) null); + assertThat(sb.toString()).isEqualTo("Hi"); + + sb = new TextStringBuilder(); + sb.append("Hi", "Alice"); + assertThat(sb.toString()).isEqualTo("Hi"); + + sb = new TextStringBuilder(); + sb.append("Hi %s", "Alice"); + assertThat(sb.toString()).isEqualTo("Hi Alice"); + + sb = new TextStringBuilder(); + sb.append("Hi %s %,d", "Alice", 5000); + // group separator depends on system locale + final char groupingSeparator = DecimalFormatSymbols.getInstance().getGroupingSeparator(); + final String expected = "Hi Alice 5" + groupingSeparator + "000"; + assertThat(sb.toString()).isEqualTo(expected); + } + + @Test + public void testAppend_Object() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendNull(); + assertThat(sb.toString()).isEqualTo(""); + + sb.append((Object) null); + assertThat(sb.toString()).isEqualTo(""); + + sb.append(FOO); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append((StringBuffer) null); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new StringBuffer("baz")); + assertThat(sb.toString()).isEqualTo("foobaz"); + + sb.append(new TextStringBuilder("yes")); + assertThat(sb.toString()).isEqualTo("foobazyes"); + + sb.append((CharSequence) "Seq"); + assertThat(sb.toString()).isEqualTo("foobazyesSeq"); + + sb.append(new StringBuilder("bld")); // Check it supports StringBuilder + assertThat(sb.toString()).isEqualTo("foobazyesSeqbld"); + } + + @Test + public void testAppend_PrimitiveNumber() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.append(0); + assertThat(sb.toString()).isEqualTo("0"); + + sb.append(1L); + assertThat(sb.toString()).isEqualTo("01"); + + sb.append(2.3f); + assertThat(sb.toString()).isEqualTo("012.3"); + + sb.append(4.5d); + assertThat(sb.toString()).isEqualTo("012.34.5"); + } + + @Test + public void testAppend_String() { + TextStringBuilder sb = new TextStringBuilder(); + sb.setNullText("NULL").append((String) null); + assertThat(sb.toString()).isEqualTo("NULL"); + + sb = new TextStringBuilder(); + sb.append("foo"); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(""); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append("bar"); + assertThat(sb.toString()).isEqualTo("foobar"); + } + + @Test + public void testAppend_String_int_int() { + final TextStringBuilder sb0 = new TextStringBuilder(); + sb0.setNullText("NULL").append((String) null, 0, 1); + assertThat(sb0.toString()).isEqualTo("NULL"); + + final TextStringBuilder sb = new TextStringBuilder(); + sb.append("foo", 0, 3); + assertThat(sb.toString()).isEqualTo("foo"); + assertThrows(IndexOutOfBoundsException.class, () -> sb.append("bar", -1, 1), "append(char[], -1,) expected IndexOutOfBoundsException"); + assertThrows(IndexOutOfBoundsException.class, () -> sb.append("bar", 3, 1), "append(char[], 3,) expected IndexOutOfBoundsException"); + assertThrows(IndexOutOfBoundsException.class, () -> sb.append("bar", 1, -1), "append(char[],, -1) expected IndexOutOfBoundsException"); + assertThrows(IndexOutOfBoundsException.class, () -> sb.append("bar", 1, 3), "append(char[], 1, 3) expected IndexOutOfBoundsException"); + assertThrows(IndexOutOfBoundsException.class, () -> sb.append("bar", -1, 3), "append(char[], -1, 3) expected IndexOutOfBoundsException"); + assertThrows(IndexOutOfBoundsException.class, () -> sb.append("bar", 4, 0), "append(char[], 4, 0) expected IndexOutOfBoundsException"); + assertThrows(IndexOutOfBoundsException.class, () -> sb.append((CharSequence) "bar", 2, 1), "append(char[], 2, 1) expected IndexOutOfBoundsException"); + assertThrows(IndexOutOfBoundsException.class, () -> sb.append((CharSequence) "bar", 2, 2), "append(char[], 2, 2) expected IndexOutOfBoundsException"); + assertThrows(IndexOutOfBoundsException.class, () -> sb.append((CharSequence) "bar", 2, -2), "append(char[], 2, -2) expected IndexOutOfBoundsException"); + assertThrows(IndexOutOfBoundsException.class, () -> sb.append((CharSequence) "bar", 2, 0), "append(char[], 2, 0) expected IndexOutOfBoundsException"); + + sb.append("bar", 3, 0); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append("abcbardef", 3, 3); + assertThat(sb.toString()).isEqualTo("foobar"); + + sb.append((CharSequence) "abcbardef", 4, 7); + assertThat(sb.toString()).isEqualTo("foobarard"); + } + + @Test + public void testAppend_StringBuffer() { + TextStringBuilder sb = new TextStringBuilder(); + sb.setNullText("NULL").append((StringBuffer) null); + assertThat(sb.toString()).isEqualTo("NULL"); + + sb = new TextStringBuilder(); + sb.append(new StringBuffer("foo")); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new StringBuffer("")); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new StringBuffer("bar")); + assertThat(sb.toString()).isEqualTo("foobar"); + } + + @Test + public void testAppend_StringBuffer_int_int() { + final TextStringBuilder sb0 = new TextStringBuilder(); + sb0.setNullText("NULL").append((StringBuffer) null, 0, 1); + assertThat(sb0.toString()).isEqualTo("NULL"); + + final TextStringBuilder sb = new TextStringBuilder(); + sb.append(new StringBuffer("foo"), 0, 3); + assertThat(sb.toString()).isEqualTo("foo"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new StringBuffer("bar"), -1, 1), + "append(char[], -1,) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new StringBuffer("bar"), 3, 1), "append(char[], 3,) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new StringBuffer("bar"), 1, -1), + "append(char[],, -1) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new StringBuffer("bar"), 1, 3), + "append(char[], 1, 3) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new StringBuffer("bar"), -1, 3), + "append(char[], -1, 3) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new StringBuffer("bar"), 4, 0), + "append(char[], 4, 0) expected IndexOutOfBoundsException"); + + sb.append(new StringBuffer("bar"), 3, 0); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new StringBuffer("abcbardef"), 3, 3); + assertThat(sb.toString()).isEqualTo("foobar"); + } + + @Test + public void testAppend_StringBuilder() { + TextStringBuilder sb = new TextStringBuilder(); + sb.setNullText("NULL").append((String) null); + assertThat(sb.toString()).isEqualTo("NULL"); + + sb = new TextStringBuilder(); + sb.append(new StringBuilder("foo")); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new StringBuilder("")); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new StringBuilder("bar")); + assertThat(sb.toString()).isEqualTo("foobar"); + } + + @Test + public void testAppend_StringBuilder_int_int() { + final TextStringBuilder sb0 = new TextStringBuilder(); + sb0.setNullText("NULL").append((String) null, 0, 1); + assertThat(sb0.toString()).isEqualTo("NULL"); + + final TextStringBuilder sb = new TextStringBuilder(); + sb.append(new StringBuilder("foo"), 0, 3); + assertThat(sb.toString()).isEqualTo("foo"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new StringBuilder("bar"), -1, 1), + "append(StringBuilder, -1,) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new StringBuilder("bar"), 3, 1), + "append(StringBuilder, 3,) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new StringBuilder("bar"), 1, -1), + "append(StringBuilder,, -1) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new StringBuilder("bar"), 1, 3), + "append(StringBuilder, 1, 3) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new StringBuilder("bar"), -1, 3), + "append(StringBuilder, -1, 3) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new StringBuilder("bar"), 4, 0), + "append(StringBuilder, 4, 0) expected IndexOutOfBoundsException"); + + sb.append(new StringBuilder("bar"), 3, 0); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new StringBuilder("abcbardef"), 3, 3); + assertThat(sb.toString()).isEqualTo("foobar"); + + sb.append(new StringBuilder("abcbardef"), 4, 3); + assertThat(sb.toString()).isEqualTo("foobarard"); + } + + @Test + public void testAppend_TextStringBuilder() { + TextStringBuilder sb = new TextStringBuilder(); + sb.setNullText("NULL").append((TextStringBuilder) null); + assertThat(sb.toString()).isEqualTo("NULL"); + + sb = new TextStringBuilder(); + sb.append(new TextStringBuilder("foo")); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new TextStringBuilder("")); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new TextStringBuilder("bar")); + assertThat(sb.toString()).isEqualTo("foobar"); + } + + @Test + public void testAppend_TextStringBuilder_int_int() { + final TextStringBuilder sb0 = new TextStringBuilder(); + sb0.setNullText("NULL").append((TextStringBuilder) null, 0, 1); + assertThat(sb0.toString()).isEqualTo("NULL"); + + final TextStringBuilder sb = new TextStringBuilder(); + sb.append(new TextStringBuilder("foo"), 0, 3); + assertThat(sb.toString()).isEqualTo("foo"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new TextStringBuilder("bar"), -1, 1), + "append(char[], -1,) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new TextStringBuilder("bar"), 3, 1), + "append(char[], 3,) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new TextStringBuilder("bar"), 1, -1), + "append(char[],, -1) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new TextStringBuilder("bar"), 1, 3), + "append(char[], 1, 3) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new TextStringBuilder("bar"), -1, 3), + "append(char[], -1, 3) expected IndexOutOfBoundsException"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.append(new TextStringBuilder("bar"), 4, 0), + "append(char[], 4, 0) expected IndexOutOfBoundsException"); + + sb.append(new TextStringBuilder("bar"), 3, 0); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.append(new TextStringBuilder("abcbardef"), 3, 3); + assertThat(sb.toString()).isEqualTo("foobar"); + } + + @Test + public void testAppendAll_Array() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendAll((Object[]) null); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendAll(); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendAll("foo", "bar", "baz"); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + + sb.clear(); + sb.appendAll("foo", "bar", "baz"); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + } + + @Test + public void testAppendAll_Collection() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendAll((Collection) null); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendAll(Collections.EMPTY_LIST); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendAll(Arrays.asList("foo", "bar", "baz")); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + } + + @Test + public void testAppendAll_Iterator() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendAll((Iterator) null); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendAll(Collections.EMPTY_LIST.iterator()); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendAll(Arrays.asList("foo", "bar", "baz").iterator()); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + } + + @Test + public void testAppendFixedWidthPadLeft() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendFixedWidthPadLeft("foo", -1, '-'); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendFixedWidthPadLeft("foo", 0, '-'); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendFixedWidthPadLeft("foo", 1, '-'); + assertThat(sb.toString()).isEqualTo("o"); + + sb.clear(); + sb.appendFixedWidthPadLeft("foo", 2, '-'); + assertThat(sb.toString()).isEqualTo("oo"); + + sb.clear(); + sb.appendFixedWidthPadLeft("foo", 3, '-'); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.clear(); + sb.appendFixedWidthPadLeft("foo", 4, '-'); + assertThat(sb.toString()).isEqualTo("-foo"); + + sb.clear(); + sb.appendFixedWidthPadLeft("foo", 10, '-'); + assertThat(sb.length()).isEqualTo(10); + // 1234567890 + assertThat(sb.toString()).isEqualTo("-------foo"); + + sb.clear(); + sb.setNullText("null"); + sb.appendFixedWidthPadLeft(null, 5, '-'); + assertThat(sb.toString()).isEqualTo("-null"); + } + + @Test + public void testAppendFixedWidthPadLeft_int() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendFixedWidthPadLeft(123, -1, '-'); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendFixedWidthPadLeft(123, 0, '-'); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendFixedWidthPadLeft(123, 1, '-'); + assertThat(sb.toString()).isEqualTo("3"); + + sb.clear(); + sb.appendFixedWidthPadLeft(123, 2, '-'); + assertThat(sb.toString()).isEqualTo("23"); + + sb.clear(); + sb.appendFixedWidthPadLeft(123, 3, '-'); + assertThat(sb.toString()).isEqualTo("123"); + + sb.clear(); + sb.appendFixedWidthPadLeft(123, 4, '-'); + assertThat(sb.toString()).isEqualTo("-123"); + + sb.clear(); + sb.appendFixedWidthPadLeft(123, 10, '-'); + assertThat(sb.length()).isEqualTo(10); + // 1234567890 + assertThat(sb.toString()).isEqualTo("-------123"); + } + + @Test + public void testAppendFixedWidthPadRight() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendFixedWidthPadRight("foo", -1, '-'); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendFixedWidthPadRight("foo", 0, '-'); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendFixedWidthPadRight("foo", 1, '-'); + assertThat(sb.toString()).isEqualTo("f"); + + sb.clear(); + sb.appendFixedWidthPadRight("foo", 2, '-'); + assertThat(sb.toString()).isEqualTo("fo"); + + sb.clear(); + sb.appendFixedWidthPadRight("foo", 3, '-'); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.clear(); + sb.appendFixedWidthPadRight("foo", 4, '-'); + assertThat(sb.toString()).isEqualTo("foo-"); + + sb.clear(); + sb.appendFixedWidthPadRight("foo", 10, '-'); + assertThat(sb.length()).isEqualTo(10); + // 1234567890 + assertThat(sb.toString()).isEqualTo("foo-------"); + + sb.clear(); + sb.setNullText("null"); + sb.appendFixedWidthPadRight(null, 5, '-'); + assertThat(sb.toString()).isEqualTo("null-"); + } + + @Test + public void testAppendFixedWidthPadRight_int() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendFixedWidthPadRight(123, -1, '-'); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendFixedWidthPadRight(123, 0, '-'); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendFixedWidthPadRight(123, 1, '-'); + assertThat(sb.toString()).isEqualTo("1"); + + sb.clear(); + sb.appendFixedWidthPadRight(123, 2, '-'); + assertThat(sb.toString()).isEqualTo("12"); + + sb.clear(); + sb.appendFixedWidthPadRight(123, 3, '-'); + assertThat(sb.toString()).isEqualTo("123"); + + sb.clear(); + sb.appendFixedWidthPadRight(123, 4, '-'); + assertThat(sb.toString()).isEqualTo("123-"); + + sb.clear(); + sb.appendFixedWidthPadRight(123, 10, '-'); + assertThat(sb.length()).isEqualTo(10); + // 1234567890 + assertThat(sb.toString()).isEqualTo("123-------"); + } + + @Test + public void testAppendln_Boolean() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendln(true); + assertThat(sb.toString()).isEqualTo("true" + SEP); + + sb.clear(); + sb.appendln(false); + assertThat(sb.toString()).isEqualTo("false" + SEP); + } + + @Test + public void testAppendln_CharArray() { + final TextStringBuilder sb = spy(new TextStringBuilder()); + final char[] input = "foo".toCharArray(); + sb.appendln(input); + + assertThat(sb.toString()).isEqualTo("foo" + SEP); + + verify(sb, times(1)).append(input); + verify(sb, times(1)).appendNewLine(); + } + + @Test + public void testAppendln_CharArray_int_int() { + final TextStringBuilder sb = spy(new TextStringBuilder()); + final char[] input = "foo".toCharArray(); + sb.appendln(input, 0, 3); + + assertThat(sb.toString()).isEqualTo("foo" + SEP); + + verify(sb, times(1)).append(input, 0, 3); + verify(sb, times(1)).appendNewLine(); + } + + @Test + public void testAppendln_FormattedString() { + final TextStringBuilder sb = spy(new TextStringBuilder()); + sb.appendln("Hello %s", "Alice"); + + assertThat(sb.toString()).isEqualTo("Hello Alice" + SEP); + + verify(sb, times(2)).append(anyString()); // appendNewLine() calls append(String) + verify(sb, times(1)).appendNewLine(); + } + + @Test + public void testAppendln_Object() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendln((Object) null); + assertThat(sb.toString()).isEqualTo("" + SEP); + + sb.appendln(FOO); + assertThat(sb.toString()).isEqualTo(SEP + "foo" + SEP); + + sb.appendln(Integer.valueOf(6)); + assertThat(sb.toString()).isEqualTo(SEP + "foo" + SEP + "6" + SEP); + } + + @Test + public void testAppendln_PrimitiveNumber() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendln(0); + assertThat(sb.toString()).isEqualTo("0" + SEP); + + sb.clear(); + sb.appendln(1L); + assertThat(sb.toString()).isEqualTo("1" + SEP); + + sb.clear(); + sb.appendln(2.3f); + assertThat(sb.toString()).isEqualTo("2.3" + SEP); + + sb.clear(); + sb.appendln(4.5d); + assertThat(sb.toString()).isEqualTo("4.5" + SEP); + } + + @Test + public void testAppendln_String() { + final TextStringBuilder sb = spy(new TextStringBuilder()); + sb.appendln("foo"); + + assertThat(sb.toString()).isEqualTo("foo" + SEP); + + verify(sb, times(2)).append(anyString()); // appendNewLine() calls append(String) + verify(sb, times(1)).appendNewLine(); + } + + @Test + public void testAppendln_String_int_int() { + final TextStringBuilder sb = spy(new TextStringBuilder()); + sb.appendln("foo", 0, 3); + + assertThat(sb.toString()).isEqualTo("foo" + SEP); + + verify(sb, times(2)).append(anyString(), anyInt(), anyInt()); // appendNewLine() calls append(String) + verify(sb, times(1)).appendNewLine(); + } + + @Test + public void testAppendln_StringBuffer() { + final TextStringBuilder sb = spy(new TextStringBuilder()); + sb.appendln(new StringBuffer("foo")); + + assertThat(sb.toString()).isEqualTo("foo" + SEP); + + verify(sb, times(1)).append(any(StringBuffer.class)); + verify(sb, times(1)).appendNewLine(); + } + + @Test + public void testAppendln_StringBuffer_int_int() { + final TextStringBuilder sb = spy(new TextStringBuilder()); + sb.appendln(new StringBuffer("foo"), 0, 3); + + assertThat(sb.toString()).isEqualTo("foo" + SEP); + + verify(sb, times(1)).append(any(StringBuffer.class), anyInt(), anyInt()); + verify(sb, times(1)).appendNewLine(); + } + + @Test + public void testAppendln_StringBuilder() { + final TextStringBuilder sb = spy(new TextStringBuilder()); + sb.appendln(new StringBuilder("foo")); + + assertThat(sb.toString()).isEqualTo("foo" + SEP); + + verify(sb, times(1)).append(any(StringBuilder.class)); + verify(sb, times(1)).appendNewLine(); + } + + @Test + public void testAppendln_StringBuilder_int_int() { + final TextStringBuilder sb = spy(new TextStringBuilder()); + sb.appendln(new StringBuilder("foo"), 0, 3); + + assertThat(sb.toString()).isEqualTo("foo" + SEP); + + verify(sb, times(1)).append(any(StringBuilder.class), anyInt(), anyInt()); + verify(sb, times(1)).appendNewLine(); + } + + @Test + public void testAppendln_TextStringBuilder() { + final TextStringBuilder sb = spy(new TextStringBuilder()); + sb.appendln(new TextStringBuilder("foo")); + + assertThat(sb.toString()).isEqualTo("foo" + SEP); + + verify(sb, times(1)).append(any(TextStringBuilder.class)); + verify(sb, times(1)).appendNewLine(); + } + + @Test + public void testAppendln_TextStringBuilder_int_int() { + final TextStringBuilder sb = spy(new TextStringBuilder()); + sb.appendln(new TextStringBuilder("foo"), 0, 3); + + assertThat(sb.toString()).isEqualTo("foo" + SEP); + + verify(sb, times(1)).append(any(TextStringBuilder.class), anyInt(), anyInt()); + verify(sb, times(1)).appendNewLine(); + } + + @Test + public void testAppendNewLine() { + TextStringBuilder sb = new TextStringBuilder("---"); + sb.appendNewLine().append("+++"); + assertThat(sb.toString()).isEqualTo("---" + SEP + "+++"); + + sb = new TextStringBuilder("---"); + sb.setNewLineText("#").appendNewLine().setNewLineText(null).appendNewLine(); + assertThat(sb.toString()).isEqualTo("---#" + SEP); + } + + @Test + public void testAppendPadding() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.append("foo"); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.appendPadding(-1, '-'); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.appendPadding(0, '-'); + assertThat(sb.toString()).isEqualTo("foo"); + + sb.appendPadding(1, '-'); + assertThat(sb.toString()).isEqualTo("foo-"); + + sb.appendPadding(16, '-'); + assertThat(sb.length()).isEqualTo(20); + // 12345678901234567890 + assertThat(sb.toString()).isEqualTo("foo-----------------"); + } + + @Test + public void testAppendSeparator_char() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendSeparator(','); // no effect + assertThat(sb.toString()).isEqualTo(""); + sb.append("foo"); + assertThat(sb.toString()).isEqualTo("foo"); + sb.appendSeparator(','); + assertThat(sb.toString()).isEqualTo("foo,"); + } + + @Test + public void testAppendSeparator_char_char() { + final TextStringBuilder sb = new TextStringBuilder(); + final char startSeparator = ':'; + final char standardSeparator = ','; + final String foo = "foo"; + sb.appendSeparator(standardSeparator, startSeparator); // no effect + assertThat(sb.toString()).isEqualTo(String.valueOf(startSeparator)); + sb.append(foo); + assertThat(sb.toString()).isEqualTo(String.valueOf(startSeparator) + foo); + sb.appendSeparator(standardSeparator, startSeparator); + assertThat(sb.toString()).isEqualTo(String.valueOf(startSeparator) + foo + standardSeparator); + } + + @Test + public void testAppendSeparator_char_int() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendSeparator(',', 0); // no effect + assertThat(sb.toString()).isEqualTo(""); + sb.append("foo"); + assertThat(sb.toString()).isEqualTo("foo"); + sb.appendSeparator(',', 1); + assertThat(sb.toString()).isEqualTo("foo,"); + + sb.appendSeparator(',', -1); // no effect + assertThat(sb.toString()).isEqualTo("foo,"); + } + + @Test + public void testAppendSeparator_String() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendSeparator(","); // no effect + assertThat(sb.toString()).isEqualTo(""); + sb.append("foo"); + assertThat(sb.toString()).isEqualTo("foo"); + sb.appendSeparator(","); + assertThat(sb.toString()).isEqualTo("foo,"); + } + + @Test + public void testAppendSeparator_String_int() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendSeparator(null, -1); // no effect + assertThat(sb.toString()).isEqualTo(""); + sb.appendSeparator(null, 0); // no effect + assertThat(sb.toString()).isEqualTo(""); + sb.appendSeparator(null, 1); // no effect + assertThat(sb.toString()).isEqualTo(""); + sb.appendSeparator(",", -1); // no effect + assertThat(sb.toString()).isEqualTo(""); + sb.appendSeparator(",", 0); // no effect + assertThat(sb.toString()).isEqualTo(""); + sb.append("foo"); + assertThat(sb.toString()).isEqualTo("foo"); + sb.appendSeparator(",", 1); + assertThat(sb.toString()).isEqualTo("foo,"); + + sb.appendSeparator(",", -1); // no effect + assertThat(sb.toString()).isEqualTo("foo,"); + } + + @Test + public void testAppendSeparator_String_String() { + final TextStringBuilder sb = new TextStringBuilder(); + final String startSeparator = "order by "; + final String standardSeparator = ","; + final String foo = "foo"; + sb.appendSeparator(null, null); + assertThat(sb.toString()).isEqualTo(""); + sb.appendSeparator(standardSeparator, null); + assertThat(sb.toString()).isEqualTo(""); + sb.appendSeparator(standardSeparator, startSeparator); + assertThat(sb.toString()).isEqualTo(startSeparator); + sb.appendSeparator(null, null); + assertThat(sb.toString()).isEqualTo(startSeparator); + sb.appendSeparator(null, startSeparator); + assertThat(sb.toString()).isEqualTo(startSeparator); + sb.append(foo); + assertThat(sb.toString()).isEqualTo(startSeparator + foo); + sb.appendSeparator(standardSeparator, startSeparator); + assertThat(sb.toString()).isEqualTo(startSeparator + foo + standardSeparator); + } + + @Test + public void testAppendWithNullText() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.setNullText("NULL"); + assertThat(sb.toString()).isEqualTo(""); + + sb.appendNull(); + assertThat(sb.toString()).isEqualTo("NULL"); + + sb.append((Object) null); + assertThat(sb.toString()).isEqualTo("NULLNULL"); + + sb.append(FOO); + assertThat(sb.toString()).isEqualTo("NULLNULLfoo"); + + sb.append((String) null); + assertThat(sb.toString()).isEqualTo("NULLNULLfooNULL"); + + sb.append(""); + assertThat(sb.toString()).isEqualTo("NULLNULLfooNULL"); + + sb.append("bar"); + assertThat(sb.toString()).isEqualTo("NULLNULLfooNULLbar"); + + sb.append((StringBuffer) null); + assertThat(sb.toString()).isEqualTo("NULLNULLfooNULLbarNULL"); + + sb.append(new StringBuffer("baz")); + assertThat(sb.toString()).isEqualTo("NULLNULLfooNULLbarNULLbaz"); + } + + @Test + public void testAppendWithSeparators_Array() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendWithSeparators((Object[]) null, ","); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendWithSeparators(ArrayUtils.EMPTY_OBJECT_ARRAY, ","); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendWithSeparators(new Object[] {"foo", "bar", "baz"}, ","); + assertThat(sb.toString()).isEqualTo("foo,bar,baz"); + + sb.clear(); + sb.appendWithSeparators(new Object[] {"foo", "bar", "baz"}, null); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + + sb.clear(); + sb.appendWithSeparators(new Object[] {"foo", null, "baz"}, ","); + assertThat(sb.toString()).isEqualTo("foo,,baz"); + } + + @Test + public void testAppendWithSeparators_Collection() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendWithSeparators((Collection) null, ","); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendWithSeparators(Collections.EMPTY_LIST, ","); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendWithSeparators(Arrays.asList("foo", "bar", "baz"), ","); + assertThat(sb.toString()).isEqualTo("foo,bar,baz"); + + sb.clear(); + sb.appendWithSeparators(Arrays.asList("foo", "bar", "baz"), null); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + + sb.clear(); + sb.appendWithSeparators(Arrays.asList("foo", null, "baz"), ","); + assertThat(sb.toString()).isEqualTo("foo,,baz"); + } + + @Test + public void testAppendWithSeparators_Iterator() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendWithSeparators((Iterator) null, ","); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendWithSeparators(Collections.EMPTY_LIST.iterator(), ","); + assertThat(sb.toString()).isEqualTo(""); + + sb.clear(); + sb.appendWithSeparators(Arrays.asList("foo", "bar", "baz").iterator(), ","); + assertThat(sb.toString()).isEqualTo("foo,bar,baz"); + + sb.clear(); + sb.appendWithSeparators(Arrays.asList("foo", "bar", "baz").iterator(), null); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + + sb.clear(); + sb.appendWithSeparators(Arrays.asList("foo", null, "baz").iterator(), ","); + assertThat(sb.toString()).isEqualTo("foo,,baz"); + } + + @Test + public void testAppendWithSeparatorsWithNullText() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.setNullText("null"); + sb.appendWithSeparators(new Object[] {"foo", null, "baz"}, ","); + assertThat(sb.toString()).isEqualTo("foo,null,baz"); + + sb.clear(); + sb.appendWithSeparators(Arrays.asList("foo", null, "baz"), ","); + assertThat(sb.toString()).isEqualTo("foo,null,baz"); + } + + @Test + public void testInsert() { + + final TextStringBuilder sb = new TextStringBuilder(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, FOO)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, FOO)); + + sb.insert(0, (Object) null); + assertThat(sb.toString()).isEqualTo("barbaz"); + + sb.insert(0, FOO); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, "foo")); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, "foo")); + + sb.insert(0, (String) null); + assertThat(sb.toString()).isEqualTo("barbaz"); + + sb.insert(0, "foo"); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, new char[] {'f', 'o', 'o'})); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, new char[] {'f', 'o', 'o'})); + + sb.insert(0, (char[]) null); + assertThat(sb.toString()).isEqualTo("barbaz"); + + sb.insert(0, ArrayUtils.EMPTY_CHAR_ARRAY); + assertThat(sb.toString()).isEqualTo("barbaz"); + + sb.insert(0, new char[] {'f', 'o', 'o'}); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, + () -> sb.insert(-1, new char[] {'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 3, 3)); + + assertThrows(IndexOutOfBoundsException.class, + () -> sb.insert(7, new char[] {'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 3, 3)); + + sb.insert(0, (char[]) null, 0, 0); + assertThat(sb.toString()).isEqualTo("barbaz"); + + sb.insert(0, ArrayUtils.EMPTY_CHAR_ARRAY, 0, 0); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, + () -> sb.insert(0, new char[] {'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, -1, 3)); + + assertThrows(IndexOutOfBoundsException.class, + () -> sb.insert(0, new char[] {'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 10, 3)); + + assertThrows(IndexOutOfBoundsException.class, + () -> sb.insert(0, new char[] {'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 0, -1)); + + assertThrows(IndexOutOfBoundsException.class, + () -> sb.insert(0, new char[] {'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 0, 10)); + + sb.insert(0, new char[] {'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 0, 0); + assertThat(sb.toString()).isEqualTo("barbaz"); + + sb.insert(0, new char[] {'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 3, 3); + assertThat(sb.toString()).isEqualTo("foobarbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, true)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, true)); + + sb.insert(0, true); + assertThat(sb.toString()).isEqualTo("truebarbaz"); + + sb.insert(0, false); + assertThat(sb.toString()).isEqualTo("falsetruebarbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, '!')); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, '!')); + + sb.insert(0, '!'); + assertThat(sb.toString()).isEqualTo("!barbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, 0)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, 0)); + + sb.insert(0, '0'); + assertThat(sb.toString()).isEqualTo("0barbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, 1L)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, 1L)); + + sb.insert(0, 1L); + assertThat(sb.toString()).isEqualTo("1barbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, 2.3F)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, 2.3F)); + + sb.insert(0, 2.3F); + assertThat(sb.toString()).isEqualTo("2.3barbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, 4.5D)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, 4.5D)); + + sb.insert(0, 4.5D); + assertThat(sb.toString()).isEqualTo("4.5barbaz"); + } + + @Test + public void testInsertWithNullText() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.setNullText("null"); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, FOO)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, FOO)); + + sb.insert(0, (Object) null); + assertThat(sb.toString()).isEqualTo("nullbarbaz"); + + sb.insert(0, FOO); + assertThat(sb.toString()).isEqualTo("foonullbarbaz"); + + sb.clear(); + sb.append("barbaz"); + assertThat(sb.toString()).isEqualTo("barbaz"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(-1, "foo")); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.insert(7, "foo")); + + sb.insert(0, (String) null); + assertThat(sb.toString()).isEqualTo("nullbarbaz"); + + sb.insert(0, "foo"); + assertThat(sb.toString()).isEqualTo("foonullbarbaz"); + + sb.insert(0, (char[]) null); + assertThat(sb.toString()).isEqualTo("nullfoonullbarbaz"); + + sb.insert(0, (char[]) null, 0, 0); + assertThat(sb.toString()).isEqualTo("nullnullfoonullbarbaz"); + } + + /** See: https://issues.apache.org/jira/browse/LANG-299 */ + @Test + public void testLang299() { + final TextStringBuilder sb = new TextStringBuilder(1); + sb.appendFixedWidthPadRight("foo", 1, '-'); + assertThat(sb.toString()).isEqualTo("f"); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/TextStringBuilderTest.java b/sources/src/test/java/org/apache/commons/text/TextStringBuilderTest.java new file mode 100644 index 0000000..f5e3181 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/TextStringBuilderTest.java @@ -0,0 +1,2287 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Locale; + +import org.apache.commons.io.output.NullAppendable; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.matcher.StringMatcher; +import org.apache.commons.text.matcher.StringMatcherFactory; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link TextStringBuilder}. + */ +public class TextStringBuilderTest { + + private static class MockReadable implements Readable { + + private final CharBuffer src; + + MockReadable(final String src) { + this.src = CharBuffer.wrap(src); + } + + @Override + public int read(final CharBuffer cb) throws IOException { + return src.read(cb); + } + } + + static final StringMatcher A_NUMBER_MATCHER = (buffer, start, bufferStart, bufferEnd) -> { + if (buffer[start] == 'A') { + start++; + if (start < bufferEnd && buffer[start] >= '0' && buffer[start] <= '9') { + return 2; + } + } + return 0; + }; + + @Test + public void test_LANG_1131_EqualsWithNullTextStringBuilder() throws Exception { + final TextStringBuilder sb = new TextStringBuilder(); + final TextStringBuilder other = null; + assertFalse(sb.equals(other)); + } + + @Test + public void testAppendCharBuffer() { + final TextStringBuilder sb1 = new TextStringBuilder(); + final CharBuffer buf = CharBuffer.allocate(10); + buf.append("0123456789"); + buf.flip(); + sb1.append(buf); + assertEquals("0123456789", sb1.toString()); + + final TextStringBuilder sb2 = new TextStringBuilder(); + sb2.append(buf, 1, 8); + assertEquals("12345678", sb2.toString()); + } + + @Test + public void testAppendCharBufferException() throws Exception { + final TextStringBuilder sb = new TextStringBuilder("1234567890"); + final String text = "Test"; + final CharBuffer buffer = CharBuffer.allocate(sb.size() + text.length()); + buffer.put(text); + buffer.flip(); + try { + sb.append(buffer, -1, 12); + } catch (final StringIndexOutOfBoundsException e) { + assertEquals("startIndex must be valid", e.getMessage()); + } + + try { + sb.append(buffer, 0, -1); + } catch (final StringIndexOutOfBoundsException e) { + assertEquals("length must be valid", e.getMessage()); + } + + sb.append(buffer); + assertEquals("1234567890Test", sb.toString()); + } + + @Test + public void testAppendCharBufferNull() throws Exception { + final TextStringBuilder sb = new TextStringBuilder("1234567890"); + final CharBuffer buffer = null; + sb.append(buffer); + assertEquals("1234567890", sb.toString()); + + final TextStringBuilder sb1 = new TextStringBuilder("1234567890"); + final CharBuffer buffer1 = null; + sb.append(buffer1, 0, 0); + assertEquals("1234567890", sb1.toString()); + } + + @Test + public void testAppendCharSequence() { + final CharSequence obj0 = null; + final CharSequence obj1 = new TextStringBuilder("test1"); + final CharSequence obj2 = new StringBuilder("test2"); + final CharSequence obj3 = new StringBuffer("test3"); + final CharBuffer obj4 = CharBuffer.wrap("test4".toCharArray()); + + final TextStringBuilder sb0 = new TextStringBuilder(); + assertEquals("", sb0.append(obj0).toString()); + + final TextStringBuilder sb1 = new TextStringBuilder(); + assertEquals("test1", sb1.append(obj1).toString()); + + final TextStringBuilder sb2 = new TextStringBuilder(); + assertEquals("test2", sb2.append(obj2).toString()); + + final TextStringBuilder sb3 = new TextStringBuilder(); + assertEquals("test3", sb3.append(obj3).toString()); + + final TextStringBuilder sb4 = new TextStringBuilder(); + assertEquals("test4", sb4.append(obj4).toString()); + + final TextStringBuilder sb5 = new TextStringBuilder(); + assertEquals("", sb5.append(obj0, 0, 0).toString()); + } + + @Test + public void testAppendln() { + final TextStringBuilder sb1 = new TextStringBuilder(); + final char ch = 'c'; + assertEquals("c" + System.lineSeparator(), sb1.appendln(ch).toString()); + } + + @Test + public void testAppendStringBuilderNull() { + final TextStringBuilder sb1 = new TextStringBuilder(); + final StringBuilder b = null; + assertEquals("", sb1.append(b).toString()); + + final TextStringBuilder sb2 = new TextStringBuilder(); + assertEquals("", sb2.append(b, 0, 0).toString()); + } + + @Test + public void testAppendTakingTwoIntsWithIndexOutOfBoundsThrowsStringIndexOutOfBoundsExceptionTwo() { + assertThatExceptionOfType(StringIndexOutOfBoundsException.class).isThrownBy(() -> { + final Charset charset = Charset.defaultCharset(); + final ByteBuffer byteBuffer = charset.encode("asdf"); + final CharBuffer charBuffer = charset.decode(byteBuffer); + + new TextStringBuilder().append(charBuffer, 933, 654); + }); + } + + @Test + public void testAppendTakingTwoIntsWithZeroThrowsStringIndexOutOfBoundsException() { + assertThatExceptionOfType(StringIndexOutOfBoundsException.class).isThrownBy(() -> { + final Charset charset = Charset.defaultCharset(); + final ByteBuffer byteBuffer = charset.encode("end < start"); + final CharBuffer charBuffer = charset.decode(byteBuffer); + + new TextStringBuilder(630).append(charBuffer, 0, 630); + }); + } + + @Test + public void testAppendToAppendable() throws Exception { + final TextStringBuilder sb = new TextStringBuilder("1234567890"); + final StringWriter writer = new StringWriter(); + writer.append("Test"); + sb.appendTo(NullAppendable.INSTANCE); + assertEquals("Test", writer.toString()); + } + + @Test + public void testAppendToCharBuffer() throws Exception { + final TextStringBuilder sb = new TextStringBuilder("1234567890"); + final String text = "Test "; + final CharBuffer buffer = CharBuffer.allocate(sb.size() + text.length()); + buffer.put(text); + + sb.appendTo(buffer); + + buffer.flip(); + assertEquals("Test 1234567890", buffer.toString()); + } + + @Test + public void testAppendToStringBuffer() throws Exception { + final TextStringBuilder sb = new TextStringBuilder("1234567890"); + final StringBuilder buffer = new StringBuilder("Test "); + + sb.appendTo(buffer); + + assertEquals("Test 1234567890", buffer.toString()); + } + + @Test + public void testAppendToStringBuilder() throws Exception { + final TextStringBuilder sb = new TextStringBuilder("1234567890"); + final StringBuilder builder = new StringBuilder("Test "); + + sb.appendTo(builder); + + assertEquals("Test 1234567890", builder.toString()); + } + + @Test + public void testAppendToWriter() throws Exception { + final TextStringBuilder sb = new TextStringBuilder("1234567890"); + final StringWriter writer = new StringWriter(); + writer.append("Test "); + + sb.appendTo(writer); + + assertEquals("Test 1234567890", writer.toString()); + } + + @Test + public void testAsBuilder() { + final TextStringBuilder sb = new TextStringBuilder().appendAll("Lorem", " ", "ipsum", " ", "dolor"); + assertEquals(sb.toString(), sb.build()); + } + + @Test + public void testAsReader() throws Exception { + final TextStringBuilder sb = new TextStringBuilder("some text"); + try (Reader reader = sb.asReader()) { + assertTrue(reader.ready()); + final char[] buf = new char[40]; + assertEquals(9, reader.read(buf)); + assertEquals("some text", new String(buf, 0, 9)); + + assertEquals(-1, reader.read()); + assertFalse(reader.ready()); + assertEquals(0, reader.skip(2)); + assertEquals(0, reader.skip(-1)); + + assertTrue(reader.markSupported()); + } + try (Reader reader = sb.asReader()) { + assertEquals('s', reader.read()); + reader.mark(-1); + final char[] array = new char[3]; + assertEquals(3, reader.read(array, 0, 3)); + assertEquals('o', array[0]); + assertEquals('m', array[1]); + assertEquals('e', array[2]); + reader.reset(); + assertEquals(1, reader.read(array, 1, 1)); + assertEquals('o', array[0]); + assertEquals('o', array[1]); + assertEquals('e', array[2]); + assertEquals(2, reader.skip(2)); + assertEquals(' ', reader.read()); + + assertTrue(reader.ready()); + reader.close(); + assertTrue(reader.ready()); + } + try (Reader reader = sb.asReader()) { + final char[] array = new char[3]; + assertThrows(IndexOutOfBoundsException.class, () -> reader.read(array, -1, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> reader.read(array, 0, -1)); + assertThrows(IndexOutOfBoundsException.class, () -> reader.read(array, 100, 1)); + assertThrows(IndexOutOfBoundsException.class, () -> reader.read(array, 0, 100)); + assertThrows(IndexOutOfBoundsException.class, + () -> reader.read(array, Integer.MAX_VALUE, Integer.MAX_VALUE)); + + assertEquals(0, reader.read(array, 0, 0)); + assertEquals(0, array[0]); + assertEquals(0, array[1]); + assertEquals(0, array[2]); + + reader.skip(9); + assertEquals(-1, reader.read(array, 0, 1)); + + reader.reset(); + final char[] array2 = new char[30]; + assertEquals(9, reader.read(array2, 0, 30)); + } + } + + @Test + public void testAsTokenizer() throws Exception { + // from Javadoc + final TextStringBuilder b = new TextStringBuilder(); + b.append("a b "); + final StringTokenizer t = b.asTokenizer(); + + final String[] tokens1 = t.getTokenArray(); + assertEquals(2, tokens1.length); + assertEquals("a", tokens1[0]); + assertEquals("b", tokens1[1]); + assertEquals(2, t.size()); + + b.append("c d "); + final String[] tokens2 = t.getTokenArray(); + assertEquals(2, tokens2.length); + assertEquals("a", tokens2[0]); + assertEquals("b", tokens2[1]); + assertEquals(2, t.size()); + assertEquals("a", t.next()); + assertEquals("b", t.next()); + + t.reset(); + final String[] tokens3 = t.getTokenArray(); + assertEquals(4, tokens3.length); + assertEquals("a", tokens3[0]); + assertEquals("b", tokens3[1]); + assertEquals("c", tokens3[2]); + assertEquals("d", tokens3[3]); + assertEquals(4, t.size()); + assertEquals("a", t.next()); + assertEquals("b", t.next()); + assertEquals("c", t.next()); + assertEquals("d", t.next()); + + assertEquals("a b c d ", t.getContent()); + } + + @Test + public void testAsWriter() throws Exception { + final TextStringBuilder sb = new TextStringBuilder("base"); + try (Writer writer = sb.asWriter()) { + + writer.write('l'); + assertEquals("basel", sb.toString()); + + writer.write(new char[] {'i', 'n'}); + assertEquals("baselin", sb.toString()); + + writer.write(new char[] {'n', 'e', 'r'}, 1, 2); + assertEquals("baseliner", sb.toString()); + + writer.write(" rout"); + assertEquals("baseliner rout", sb.toString()); + + writer.write("ping that server", 1, 3); + assertEquals("baseliner routing", sb.toString()); + + writer.flush(); // no effect + assertEquals("baseliner routing", sb.toString()); + + writer.close(); // no effect + assertEquals("baseliner routing", sb.toString()); + + writer.write(" hi"); // works after close + assertEquals("baseliner routing hi", sb.toString()); + + sb.setLength(4); // mix and match + writer.write('d'); + assertEquals("based", sb.toString()); + } + } + + @Test + public void testCapacity() { + final TextStringBuilder sb = new TextStringBuilder(); + assertEquals(sb.getBuffer().length, sb.capacity()); + + sb.append("HelloWorldHelloWorldHelloWorldHelloWorld"); + assertEquals(sb.getBuffer().length, sb.capacity()); + } + + @Test + public void testCapacityAndLength() { + final TextStringBuilder sb = new TextStringBuilder(); + assertEquals(32, sb.capacity()); + assertEquals(0, sb.length()); + assertEquals(0, sb.size()); + assertTrue(sb.isEmpty()); + + sb.minimizeCapacity(); + assertEquals(0, sb.capacity()); + assertEquals(0, sb.length()); + assertEquals(0, sb.size()); + assertTrue(sb.isEmpty()); + + sb.ensureCapacity(32); + assertTrue(sb.capacity() >= 32); + assertEquals(0, sb.length()); + assertEquals(0, sb.size()); + assertTrue(sb.isEmpty()); + + sb.append("foo"); + assertTrue(sb.capacity() >= 32); + assertEquals(3, sb.length()); + assertEquals(3, sb.size()); + assertFalse(sb.isEmpty()); + + sb.clear(); + assertTrue(sb.capacity() >= 32); + assertEquals(0, sb.length()); + assertEquals(0, sb.size()); + assertTrue(sb.isEmpty()); + + sb.append("123456789012345678901234567890123"); + assertTrue(sb.capacity() > 32); + assertEquals(33, sb.length()); + assertEquals(33, sb.size()); + assertFalse(sb.isEmpty()); + + sb.ensureCapacity(16); + assertTrue(sb.capacity() > 16); + assertEquals(33, sb.length()); + assertEquals(33, sb.size()); + assertFalse(sb.isEmpty()); + + sb.minimizeCapacity(); + assertEquals(33, sb.capacity()); + assertEquals(33, sb.length()); + assertEquals(33, sb.size()); + assertFalse(sb.isEmpty()); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.setLength(-1)); + + sb.setLength(33); + assertEquals(33, sb.capacity()); + assertEquals(33, sb.length()); + assertEquals(33, sb.size()); + assertFalse(sb.isEmpty()); + + sb.setLength(16); + assertTrue(sb.capacity() >= 16); + assertEquals(16, sb.length()); + assertEquals(16, sb.size()); + assertEquals("1234567890123456", sb.toString()); + assertFalse(sb.isEmpty()); + + sb.setLength(32); + assertTrue(sb.capacity() >= 32); + assertEquals(32, sb.length()); + assertEquals(32, sb.size()); + assertEquals("1234567890123456\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", sb.toString()); + assertFalse(sb.isEmpty()); + + sb.setLength(0); + assertTrue(sb.capacity() >= 32); + assertEquals(0, sb.length()); + assertEquals(0, sb.size()); + assertTrue(sb.isEmpty()); + } + + @Test + public void testChaining() { + final TextStringBuilder sb = new TextStringBuilder(); + assertSame(sb, sb.setNewLineText(null)); + assertSame(sb, sb.setNullText(null)); + assertSame(sb, sb.setLength(1)); + assertSame(sb, sb.setCharAt(0, 'a')); + assertSame(sb, sb.ensureCapacity(0)); + assertSame(sb, sb.minimizeCapacity()); + assertSame(sb, sb.clear()); + assertSame(sb, sb.reverse()); + assertSame(sb, sb.trim()); + } + + @Test + public void testCharAt() { + final TextStringBuilder sb = new TextStringBuilder(); + assertThrows(IndexOutOfBoundsException.class, () -> sb.charAt(0)); + assertThrows(IndexOutOfBoundsException.class, () -> sb.charAt(-1)); + sb.append("foo"); + assertEquals('f', sb.charAt(0)); + assertEquals('o', sb.charAt(1)); + assertEquals('o', sb.charAt(2)); + assertThrows(IndexOutOfBoundsException.class, () -> sb.charAt(-1)); + assertThrows(IndexOutOfBoundsException.class, () -> sb.charAt(3)); + } + + @Test + public void testClear() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.append("Hello"); + sb.clear(); + assertEquals(0, sb.length()); + assertTrue(sb.getBuffer().length >= 5); + } + + @Test + public void testConstructorCharSequence() { + final CharBuffer str = CharBuffer.wrap("A"); + final int length = str.length(); + final TextStringBuilder sb = new TextStringBuilder(str); + assertEquals(TextStringBuilder.CAPACITY + length, sb.capacity()); + assertEquals(length, sb.toCharArray().length); + } + + @Test + public void testConstructorDefault() { + final TextStringBuilder sb = new TextStringBuilder(); + assertEquals(TextStringBuilder.CAPACITY, sb.capacity()); + assertEquals(0, sb.toCharArray().length); + } + + @Test + public void testConstructors() { + final TextStringBuilder sb0 = new TextStringBuilder(); + assertEquals(32, sb0.capacity()); + assertEquals(0, sb0.length()); + assertEquals(0, sb0.size()); + + final TextStringBuilder sb1 = new TextStringBuilder(32); + assertEquals(32, sb1.capacity()); + assertEquals(0, sb1.length()); + assertEquals(0, sb1.size()); + + final TextStringBuilder sb2 = new TextStringBuilder(0); + assertEquals(32, sb2.capacity()); + assertEquals(0, sb2.length()); + assertEquals(0, sb2.size()); + + final TextStringBuilder sb3 = new TextStringBuilder(-1); + assertEquals(32, sb3.capacity()); + assertEquals(0, sb3.length()); + assertEquals(0, sb3.size()); + + final TextStringBuilder sb4 = new TextStringBuilder(1); + assertEquals(1, sb4.capacity()); + assertEquals(0, sb4.length()); + assertEquals(0, sb4.size()); + + final TextStringBuilder sb5 = new TextStringBuilder((String) null); + assertEquals(32, sb5.capacity()); + assertEquals(0, sb5.length()); + assertEquals(0, sb5.size()); + + final TextStringBuilder sb6 = new TextStringBuilder(""); + assertEquals(32, sb6.capacity()); + assertEquals(0, sb6.length()); + assertEquals(0, sb6.size()); + + final TextStringBuilder sb7 = new TextStringBuilder("foo"); + assertEquals(35, sb7.capacity()); + assertEquals(3, sb7.length()); + assertEquals(3, sb7.size()); + } + + @Test + public void testConstructorString() { + final String str = "A"; + final int length = str.length(); + final TextStringBuilder sb = new TextStringBuilder(str); + assertEquals(TextStringBuilder.CAPACITY + length, sb.capacity()); + assertEquals(length, sb.toCharArray().length); + } + + @Test + public void testContains_char() { + final TextStringBuilder sb = new TextStringBuilder("abcdefghijklmnopqrstuvwxyz"); + assertTrue(sb.contains('a')); + assertTrue(sb.contains('o')); + assertTrue(sb.contains('z')); + assertFalse(sb.contains('1')); + } + + @Test + public void testContains_String() { + final TextStringBuilder sb = new TextStringBuilder("abcdefghijklmnopqrstuvwxyz"); + assertTrue(sb.contains("a")); + assertTrue(sb.contains("pq")); + assertTrue(sb.contains("z")); + assertFalse(sb.contains("zyx")); + assertFalse(sb.contains((String) null)); + } + + @Test + public void testContains_StringMatcher() { + TextStringBuilder sb = new TextStringBuilder("abcdefghijklmnopqrstuvwxyz"); + assertTrue(sb.contains(StringMatcherFactory.INSTANCE.charMatcher('a'))); + assertTrue(sb.contains(StringMatcherFactory.INSTANCE.stringMatcher("pq"))); + assertTrue(sb.contains(StringMatcherFactory.INSTANCE.charMatcher('z'))); + assertFalse(sb.contains(StringMatcherFactory.INSTANCE.stringMatcher("zy"))); + assertFalse(sb.contains((StringMatcher) null)); + + sb = new TextStringBuilder(); + assertFalse(sb.contains(A_NUMBER_MATCHER)); + sb.append("B A1 C"); + assertTrue(sb.contains(A_NUMBER_MATCHER)); + } + + @Test + public void testDeleteAll_char() { + TextStringBuilder sb = new TextStringBuilder("abcbccba"); + sb.deleteAll('X'); + assertEquals("abcbccba", sb.toString()); + sb.deleteAll('a'); + assertEquals("bcbccb", sb.toString()); + sb.deleteAll('c'); + assertEquals("bbb", sb.toString()); + sb.deleteAll('b'); + assertEquals("", sb.toString()); + + sb = new TextStringBuilder(""); + sb.deleteAll('b'); + assertEquals("", sb.toString()); + } + + @Test + public void testDeleteAll_String() { + TextStringBuilder sb = new TextStringBuilder("abcbccba"); + sb.deleteAll((String) null); + assertEquals("abcbccba", sb.toString()); + sb.deleteAll(""); + assertEquals("abcbccba", sb.toString()); + + sb.deleteAll("X"); + assertEquals("abcbccba", sb.toString()); + sb.deleteAll("a"); + assertEquals("bcbccb", sb.toString()); + sb.deleteAll("c"); + assertEquals("bbb", sb.toString()); + sb.deleteAll("b"); + assertEquals("", sb.toString()); + + sb = new TextStringBuilder("abcbccba"); + sb.deleteAll("bc"); + assertEquals("acba", sb.toString()); + + sb = new TextStringBuilder(""); + sb.deleteAll("bc"); + assertEquals("", sb.toString()); + } + + @Test + public void testDeleteAll_StringMatcher() { + TextStringBuilder sb = new TextStringBuilder("A0xA1A2yA3"); + sb.deleteAll((StringMatcher) null); + assertEquals("A0xA1A2yA3", sb.toString()); + sb.deleteAll(A_NUMBER_MATCHER); + assertEquals("xy", sb.toString()); + + sb = new TextStringBuilder("Ax1"); + sb.deleteAll(A_NUMBER_MATCHER); + assertEquals("Ax1", sb.toString()); + + sb = new TextStringBuilder(""); + sb.deleteAll(A_NUMBER_MATCHER); + assertEquals("", sb.toString()); + } + + @Test + public void testDeleteCharAt() { + final String str = "abc"; + // + final TextStringBuilder sb1 = new TextStringBuilder(str); + sb1.deleteCharAt(0); + assertEquals("bc", sb1.toString()); + // + final TextStringBuilder sb2 = new TextStringBuilder(str); + sb2.deleteCharAt(str.length() - 1); + assertEquals("ab", sb2.toString()); + // + final TextStringBuilder sb3 = new TextStringBuilder(str); + assertThrows(IndexOutOfBoundsException.class, () -> sb3.deleteCharAt(str.length())); + assertThrows(IndexOutOfBoundsException.class, () -> sb3.deleteCharAt(1000)); + } + + @Test + public void testDeleteCharAtWithNegative() { + assertThatExceptionOfType(StringIndexOutOfBoundsException.class).isThrownBy(() -> new TextStringBuilder().deleteCharAt(-1258)); + } + + @Test + public void testDeleteFirst_char() { + TextStringBuilder sb = new TextStringBuilder("abcba"); + sb.deleteFirst('X'); + assertEquals("abcba", sb.toString()); + sb.deleteFirst('a'); + assertEquals("bcba", sb.toString()); + sb.deleteFirst('c'); + assertEquals("bba", sb.toString()); + sb.deleteFirst('b'); + assertEquals("ba", sb.toString()); + + sb = new TextStringBuilder(""); + sb.deleteFirst('b'); + assertEquals("", sb.toString()); + } + + @Test + public void testDeleteFirst_String() { + TextStringBuilder sb = new TextStringBuilder("abcbccba"); + sb.deleteFirst((String) null); + assertEquals("abcbccba", sb.toString()); + sb.deleteFirst(""); + assertEquals("abcbccba", sb.toString()); + + sb.deleteFirst("X"); + assertEquals("abcbccba", sb.toString()); + sb.deleteFirst("a"); + assertEquals("bcbccba", sb.toString()); + sb.deleteFirst("c"); + assertEquals("bbccba", sb.toString()); + sb.deleteFirst("b"); + assertEquals("bccba", sb.toString()); + + sb = new TextStringBuilder("abcbccba"); + sb.deleteFirst("bc"); + assertEquals("abccba", sb.toString()); + + sb = new TextStringBuilder(""); + sb.deleteFirst("bc"); + assertEquals("", sb.toString()); + } + + @Test + public void testDeleteFirst_StringMatcher() { + TextStringBuilder sb = new TextStringBuilder("A0xA1A2yA3"); + sb.deleteFirst((StringMatcher) null); + assertEquals("A0xA1A2yA3", sb.toString()); + sb.deleteFirst(A_NUMBER_MATCHER); + assertEquals("xA1A2yA3", sb.toString()); + + sb = new TextStringBuilder("Ax1"); + sb.deleteFirst(A_NUMBER_MATCHER); + assertEquals("Ax1", sb.toString()); + + sb = new TextStringBuilder(""); + sb.deleteFirst(A_NUMBER_MATCHER); + assertEquals("", sb.toString()); + } + + @Test + public void testDeleteIntInt() { + final TextStringBuilder sb = new TextStringBuilder("abc"); + sb.delete(0, 1); + assertEquals("bc", sb.toString()); + sb.delete(1, 2); + assertEquals("b", sb.toString()); + sb.delete(0, 1); + assertEquals("", sb.toString()); + sb.delete(0, 1000); + assertEquals("", sb.toString()); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.delete(1, 2)); + assertThrows(IndexOutOfBoundsException.class, () -> sb.delete(-1, 1)); + + assertThrows(IndexOutOfBoundsException.class, () -> new TextStringBuilder("anything").delete(2, 1)); + } + + @Test + public void testDrainChar() { + final String str = "abc"; + // + final TextStringBuilder sb1 = new TextStringBuilder(str); + assertEquals('a', sb1.drainChar(0)); + assertEquals("bc", sb1.toString()); + // + final TextStringBuilder sb2 = new TextStringBuilder(str); + assertEquals('c', sb2.drainChar(str.length() - 1)); + assertEquals("ab", sb2.toString()); + // + final TextStringBuilder sb3 = new TextStringBuilder(str); + assertThrows(IndexOutOfBoundsException.class, () -> sb3.drainChar(str.length())); + assertThrows(IndexOutOfBoundsException.class, () -> sb3.drainChar(1000)); + } + + @Test + public void testDrainCharsIntIntCharArrayInt() { + final String data = "junit"; + final char[] array = new char[data.length()]; + final TextStringBuilder sb = new TextStringBuilder(); + // empty buffer + assertEquals(0, sb.drainChars(0, 5, array, 1)); + assertEquals(0, sb.drainChars(0, 5, ArrayUtils.EMPTY_CHAR_ARRAY, 1)); + // empty buffer, 0 length request + assertEquals(0, sb.drainChars(5, 5, array, 1)); + + sb.append(data); + assertEquals(0, sb.drainChars(5, 5, array, 1)); + assertEquals(5, sb.drainChars(0, 5, array, 0)); + assertArrayEquals(data.toCharArray(), array); + + final char[] b = new char[5]; + sb.set(data); + assertEquals(2, sb.drainChars(0, 2, b, 3)); + assertArrayEquals(new char[] {0, 0, 0, 'j', 'u'}, b); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.drainChars(-1, 0, b, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> sb.drainChars(0, -1, array, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> sb.drainChars(4, 2, array, 0)); + + // get and delete it all. + sb.set(data); + assertEquals(data.length(), sb.drainChars(0, sb.length() + 1, array, 0)); + assertArrayEquals(data.toCharArray(), array); + + // get and delete more than there is + sb.set(data); + assertEquals(data.length(), sb.drainChars(0, sb.length() + 10, array, 0)); + assertArrayEquals(data.toCharArray(), array); + + // get and delete more than can fit + sb.set(data); + int targetIndex = 1; + Arrays.fill(array, '-'); + assertEquals(data.length() - targetIndex, sb.drainChars(0, sb.length() + 10, array, targetIndex)); + assertArrayEquals("-juni".toCharArray(), array); + + // get and delete more than can fit + sb.set(data); + Arrays.fill(array, '-'); + assertEquals(data.length() - targetIndex, sb.drainChars(0, sb.length() + 1, array, targetIndex)); + assertArrayEquals("-juni".toCharArray(), array); + + // get and delete more than can fit + sb.set(data); + targetIndex = 2; + Arrays.fill(array, '-'); + assertEquals(data.length() - targetIndex, sb.drainChars(0, sb.length() + 1, array, targetIndex)); + assertArrayEquals("--jun".toCharArray(), array); + } + + @Test + public void testEndsWith() { + final TextStringBuilder sb = new TextStringBuilder(); + assertFalse(sb.endsWith("a")); + assertFalse(sb.endsWith("c")); + assertTrue(sb.endsWith("")); + assertFalse(sb.endsWith(null)); + sb.append("abc"); + assertTrue(sb.endsWith("c")); + assertTrue(sb.endsWith("bc")); + assertTrue(sb.endsWith("abc")); + assertFalse(sb.endsWith("cba")); + assertFalse(sb.endsWith("abcd")); + assertFalse(sb.endsWith(" abc")); + assertFalse(sb.endsWith("abc ")); + } + + @Test + public void testEnsureCapacity() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.ensureCapacity(2); + assertTrue(sb.capacity() >= 2); + + sb.ensureCapacity(-1); + assertTrue(sb.capacity() >= 0); + + sb.append("HelloWorld"); + sb.ensureCapacity(40); + assertTrue(sb.capacity() >= 40); + } + + @Test + public void testEnsureCapacityOutOfMemoryError() { + final TextStringBuilder sb = new TextStringBuilder(); + // Should not be a NegativeArraySizeException + sb.ensureCapacity(Integer.MIN_VALUE); + sb.ensureCapacity(-1); + sb.ensureCapacity(0); + // Might fail in a CI: + // sb.ensureCapacity(Integer.MAX_VALUE / 2); + sb.ensureCapacity(10_000); + assertThrows(OutOfMemoryError.class, () -> sb.ensureCapacity(Integer.MAX_VALUE)); + } + + @Test + public void testEquals() { + final TextStringBuilder sb1 = new TextStringBuilder(50); + final TextStringBuilder sb2 = new TextStringBuilder(100); + assertTrue(sb1.equals(sb2)); + assertTrue(sb1.equals(sb1)); + assertTrue(sb2.equals(sb2)); + assertEquals(sb1, sb2); + + sb1.append("abc"); + assertFalse(sb1.equals(sb2)); + assertNotEquals(sb1, sb2); + + sb2.append("ABC"); + assertFalse(sb1.equals(sb2)); + assertNotEquals(sb1, sb2); + + sb2.set("abc"); + assertTrue(sb1.equals(sb2)); + assertEquals(sb1, sb2); + + assertNotEquals(sb1, Integer.valueOf(1)); + assertNotEquals("abc", sb1); + } + + @Test + public void testEqualsIgnoreCase() { + final TextStringBuilder sb1 = new TextStringBuilder(); + final TextStringBuilder sb2 = new TextStringBuilder(); + assertTrue(sb1.equalsIgnoreCase(sb1)); + assertTrue(sb1.equalsIgnoreCase(sb2)); + assertTrue(sb2.equalsIgnoreCase(sb2)); + + sb1.append("abc"); + assertFalse(sb1.equalsIgnoreCase(sb2)); + + sb2.append("ABC"); + assertTrue(sb1.equalsIgnoreCase(sb2)); + + sb2.set("abc"); + assertTrue(sb1.equalsIgnoreCase(sb2)); + assertTrue(sb1.equalsIgnoreCase(sb1)); + assertTrue(sb2.equalsIgnoreCase(sb2)); + + sb2.set("aBc"); + assertTrue(sb1.equalsIgnoreCase(sb2)); + + final Locale turkish = Locale.forLanguageTag("tr"); + assertTrue( + new TextStringBuilder("title").equalsIgnoreCase(new TextStringBuilder("title".toLowerCase(turkish)))); + assertTrue( + new TextStringBuilder("title").equalsIgnoreCase(new TextStringBuilder("TITLE".toLowerCase(turkish)))); + assertTrue( + new TextStringBuilder("TITLE").equalsIgnoreCase(new TextStringBuilder("TITLE".toLowerCase(turkish)))); + assertTrue( + new TextStringBuilder("TITLE").equalsIgnoreCase(new TextStringBuilder("title".toLowerCase(turkish)))); + // + // assertTrue(new TextStringBuilder("title").equalsIgnoreCase(new + // TextStringBuilder("title".toUpperCase(turkish)))); + // assertTrue(new TextStringBuilder("title").equalsIgnoreCase(new + // TextStringBuilder("TITLE".toUpperCase(turkish)))); + assertTrue( + new TextStringBuilder("TITLE").equalsIgnoreCase(new TextStringBuilder("TITLE".toUpperCase(turkish)))); + // assertTrue(new TextStringBuilder("TITLE").equalsIgnoreCase(new + // TextStringBuilder("title".toUpperCase(turkish)))); + } + + @Test + public void testGetChars() { + final TextStringBuilder sb = new TextStringBuilder(); + + char[] input = new char[10]; + char[] a = sb.getChars(input); + assertSame(input, a); + assertArrayEquals(new char[10], a); + + sb.append("junit"); + a = sb.getChars(input); + assertSame(input, a); + assertArrayEquals(new char[] {'j', 'u', 'n', 'i', 't', 0, 0, 0, 0, 0}, a); + + a = sb.getChars(null); + assertNotSame(input, a); + assertEquals(5, a.length); + assertArrayEquals("junit".toCharArray(), a); + + input = new char[5]; + a = sb.getChars(input); + assertSame(input, a); + + input = new char[4]; + a = sb.getChars(input); + assertNotSame(input, a); + } + + @Test + public void testGetCharsIntIntCharArrayInt() { + final TextStringBuilder sb = new TextStringBuilder(); + + final char[] array1 = new char[1]; + sb.getChars(0, 0, array1, 0); + assertArrayEquals(new char[1], array1); + + sb.set("junit"); + sb.getChars(0, 1, array1, 0); + assertArrayEquals(new char[] {'j'}, array1); + + final char[] array = new char[5]; + sb.getChars(0, 5, array, 0); + assertArrayEquals(new char[] {'j', 'u', 'n', 'i', 't'}, array); + + final char[] b = new char[5]; + sb.getChars(0, 2, b, 3); + assertArrayEquals(new char[] {0, 0, 0, 'j', 'u'}, b); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.getChars(-1, 0, b, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> sb.getChars(0, -1, array, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> sb.getChars(0, 20, array, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> sb.getChars(4, 2, array, 0)); + } + + @Test + public void testGetSetNewLineText() { + final TextStringBuilder sb = new TextStringBuilder(); + assertNull(sb.getNewLineText()); + + sb.setNewLineText("#"); + assertEquals("#", sb.getNewLineText()); + + sb.setNewLineText(""); + assertEquals("", sb.getNewLineText()); + + sb.setNewLineText((String) null); + assertNull(sb.getNewLineText()); + } + + @Test + public void testGetSetNullText() { + final TextStringBuilder sb = new TextStringBuilder(); + assertNull(sb.getNullText()); + + sb.setNullText("null"); + assertEquals("null", sb.getNullText()); + + sb.setNullText(""); + assertNull(sb.getNullText()); + + sb.setNullText("NULL"); + assertEquals("NULL", sb.getNullText()); + + sb.setNullText((String) null); + assertNull(sb.getNullText()); + } + + @Test + public void testHashCode() { + final TextStringBuilder sb = new TextStringBuilder(); + final int hc1a = sb.hashCode(); + final int hc1b = sb.hashCode(); + assertEquals(hc1a, hc1b); + + // following TEXT-211 : the hashcode of the buffer may not be equals to the hashcode of the TextStringBuilder itself + final int emptyHc = Arrays.hashCode(sb.getBuffer()); + assertNotEquals(emptyHc, hc1a); + + sb.append("abc"); + final int hc2a = sb.hashCode(); + final int hc2b = sb.hashCode(); + assertEquals(hc2a, hc2b); + + final TextStringBuilder sb2 = new TextStringBuilder(100); + final TextStringBuilder sb3 = new TextStringBuilder(10); + final int hc2 = sb2.hashCode(); + final int hc3 = sb3.hashCode(); + assertEquals(hc2, hc3); + + sb2.append("abc"); + sb3.append("abc"); + final int hc2b2 = sb2.hashCode(); + final int hc3b2 = sb3.hashCode(); + assertEquals(hc2b2, hc3b2); + } + + @Test + public void testIndexOf_char() { + final TextStringBuilder sb = new TextStringBuilder("abab"); + assertEquals(0, sb.indexOf('a')); + + // should work like String#indexOf + assertEquals("abab".indexOf('a'), sb.indexOf('a')); + + assertEquals(1, sb.indexOf('b')); + assertEquals("abab".indexOf('b'), sb.indexOf('b')); + + assertEquals(-1, sb.indexOf('z')); + } + + @Test + public void testIndexOf_char_int() { + TextStringBuilder sb = new TextStringBuilder("abab"); + assertEquals(0, sb.indexOf('a', -1)); + assertEquals(0, sb.indexOf('a', 0)); + assertEquals(2, sb.indexOf('a', 1)); + assertEquals(-1, sb.indexOf('a', 4)); + assertEquals(-1, sb.indexOf('a', 5)); + + // should work like String#indexOf + assertEquals("abab".indexOf('a', 1), sb.indexOf('a', 1)); + + assertEquals(3, sb.indexOf('b', 2)); + assertEquals("abab".indexOf('b', 2), sb.indexOf('b', 2)); + + assertEquals(-1, sb.indexOf('z', 2)); + + sb = new TextStringBuilder("xyzabc"); + assertEquals(2, sb.indexOf('z', 0)); + assertEquals(-1, sb.indexOf('z', 3)); + } + + @Test + public void testIndexOf_String() { + final TextStringBuilder sb = new TextStringBuilder("abab"); + + assertEquals(0, sb.indexOf("a")); + // should work like String#indexOf + assertEquals("abab".indexOf("a"), sb.indexOf("a")); + + assertEquals(0, sb.indexOf("ab")); + // should work like String#indexOf + assertEquals("abab".indexOf("ab"), sb.indexOf("ab")); + + assertEquals(1, sb.indexOf("b")); + assertEquals("abab".indexOf("b"), sb.indexOf("b")); + + assertEquals(1, sb.indexOf("ba")); + assertEquals("abab".indexOf("ba"), sb.indexOf("ba")); + + assertEquals(-1, sb.indexOf("z")); + + assertEquals(-1, sb.indexOf((String) null)); + } + + @Test + public void testIndexOf_String_int() { + TextStringBuilder sb = new TextStringBuilder("abab"); + assertEquals(0, sb.indexOf("a", -1)); + assertEquals(0, sb.indexOf("a", 0)); + assertEquals(2, sb.indexOf("a", 1)); + assertEquals(2, sb.indexOf("a", 2)); + assertEquals(-1, sb.indexOf("a", 3)); + assertEquals(-1, sb.indexOf("a", 4)); + assertEquals(-1, sb.indexOf("a", 5)); + + assertEquals(-1, sb.indexOf("abcdef", 0)); + assertEquals(0, sb.indexOf("", 0)); + assertEquals(1, sb.indexOf("", 1)); + + // should work like String#indexOf + assertEquals("abab".indexOf("a", 1), sb.indexOf("a", 1)); + + assertEquals(2, sb.indexOf("ab", 1)); + // should work like String#indexOf + assertEquals("abab".indexOf("ab", 1), sb.indexOf("ab", 1)); + + assertEquals(3, sb.indexOf("b", 2)); + assertEquals("abab".indexOf("b", 2), sb.indexOf("b", 2)); + + assertEquals(1, sb.indexOf("ba", 1)); + assertEquals("abab".indexOf("ba", 2), sb.indexOf("ba", 2)); + + assertEquals(-1, sb.indexOf("z", 2)); + + sb = new TextStringBuilder("xyzabc"); + assertEquals(2, sb.indexOf("za", 0)); + assertEquals(-1, sb.indexOf("za", 3)); + + assertEquals(-1, sb.indexOf((String) null, 2)); + } + + @Test + public void testIndexOf_StringMatcher() { + final TextStringBuilder sb = new TextStringBuilder(); + assertEquals(-1, sb.indexOf((StringMatcher) null)); + assertEquals(-1, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('a'))); + + sb.append("ab bd"); + assertEquals(0, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('a'))); + assertEquals(1, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('b'))); + assertEquals(2, sb.indexOf(StringMatcherFactory.INSTANCE.spaceMatcher())); + assertEquals(4, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('d'))); + assertEquals(-1, sb.indexOf(StringMatcherFactory.INSTANCE.noneMatcher())); + assertEquals(-1, sb.indexOf((StringMatcher) null)); + + sb.append(" A1 junction"); + assertEquals(6, sb.indexOf(A_NUMBER_MATCHER)); + } + + @Test + public void testIndexOf_StringMatcher_int() { + final TextStringBuilder sb = new TextStringBuilder(); + assertEquals(-1, sb.indexOf((StringMatcher) null, 2)); + assertEquals(-1, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('a'), 2)); + assertEquals(-1, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('a'), 0)); + + sb.append("ab bd"); + assertEquals(0, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('a'), -2)); + assertEquals(0, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('a'), 0)); + assertEquals(-1, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('a'), 2)); + assertEquals(-1, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('a'), 20)); + + assertEquals(1, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('b'), -1)); + assertEquals(1, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('b'), 0)); + assertEquals(1, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('b'), 1)); + assertEquals(3, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('b'), 2)); + assertEquals(3, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('b'), 3)); + assertEquals(-1, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('b'), 4)); + assertEquals(-1, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('b'), 5)); + assertEquals(-1, sb.indexOf(StringMatcherFactory.INSTANCE.charMatcher('b'), 6)); + + assertEquals(2, sb.indexOf(StringMatcherFactory.INSTANCE.spaceMatcher(), -2)); + assertEquals(2, sb.indexOf(StringMatcherFactory.INSTANCE.spaceMatcher(), 0)); + assertEquals(2, sb.indexOf(StringMatcherFactory.INSTANCE.spaceMatcher(), 2)); + assertEquals(-1, sb.indexOf(StringMatcherFactory.INSTANCE.spaceMatcher(), 4)); + assertEquals(-1, sb.indexOf(StringMatcherFactory.INSTANCE.spaceMatcher(), 20)); + + assertEquals(-1, sb.indexOf(StringMatcherFactory.INSTANCE.noneMatcher(), 0)); + assertEquals(-1, sb.indexOf((StringMatcher) null, 0)); + + sb.append(" A1 junction with A2"); + assertEquals(6, sb.indexOf(A_NUMBER_MATCHER, 5)); + assertEquals(6, sb.indexOf(A_NUMBER_MATCHER, 6)); + assertEquals(23, sb.indexOf(A_NUMBER_MATCHER, 7)); + assertEquals(23, sb.indexOf(A_NUMBER_MATCHER, 22)); + assertEquals(23, sb.indexOf(A_NUMBER_MATCHER, 23)); + assertEquals(-1, sb.indexOf(A_NUMBER_MATCHER, 24)); + } + + @Test + public void testIndexOfLang294() { + final TextStringBuilder sb = new TextStringBuilder("onetwothree"); + sb.deleteFirst("three"); + assertEquals(-1, sb.indexOf("three")); + } + + @Test + public void testIsEmpty() { + final TextStringBuilder sb = new TextStringBuilder(); + assertTrue(sb.isEmpty()); + + sb.append("Hello"); + assertFalse(sb.isEmpty()); + + sb.clear(); + assertTrue(sb.isEmpty()); + } + + @Test + public void testIsNotEmpty() { + final TextStringBuilder sb = new TextStringBuilder(); + assertFalse(sb.isNotEmpty()); + + sb.append("Hello"); + assertTrue(sb.isNotEmpty()); + + sb.clear(); + assertFalse(sb.isNotEmpty()); + } + + @Test + public void testLang294() { + final TextStringBuilder sb = new TextStringBuilder("\n%BLAH%\nDo more stuff\neven more stuff\n%BLAH%\n"); + sb.deleteAll("\n%BLAH%"); + assertEquals("\nDo more stuff\neven more stuff\n", sb.toString()); + } + + @Test + public void testLang295() { + final TextStringBuilder sb = new TextStringBuilder("onetwothree"); + sb.deleteFirst("three"); + assertFalse(sb.contains('h'), "The contains(char) method is looking beyond the end of the string"); + assertEquals(-1, sb.indexOf('h'), "The indexOf(char) method is looking beyond the end of the string"); + } + + @Test + public void testLang412Left() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendFixedWidthPadLeft(null, 10, '*'); + assertEquals("**********", sb.toString(), "Failed to invoke appendFixedWidthPadLeft correctly"); + } + + @Test + public void testLang412Right() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.appendFixedWidthPadRight(null, 10, '*'); + assertEquals("**********", sb.toString(), "Failed to invoke appendFixedWidthPadRight correctly"); + } + + @Test + public void testLastIndexOf_char() { + final TextStringBuilder sb = new TextStringBuilder("abab"); + + assertEquals(2, sb.lastIndexOf('a')); + // should work like String#lastIndexOf + assertEquals("abab".lastIndexOf('a'), sb.lastIndexOf('a')); + + assertEquals(3, sb.lastIndexOf('b')); + assertEquals("abab".lastIndexOf('b'), sb.lastIndexOf('b')); + + assertEquals(-1, sb.lastIndexOf('z')); + } + + @Test + public void testLastIndexOf_char_int() { + TextStringBuilder sb = new TextStringBuilder("abab"); + assertEquals(-1, sb.lastIndexOf('a', -1)); + assertEquals(0, sb.lastIndexOf('a', 0)); + assertEquals(0, sb.lastIndexOf('a', 1)); + + // should work like String#lastIndexOf + assertEquals("abab".lastIndexOf('a', 1), sb.lastIndexOf('a', 1)); + + assertEquals(1, sb.lastIndexOf('b', 2)); + assertEquals("abab".lastIndexOf('b', 2), sb.lastIndexOf('b', 2)); + + assertEquals(-1, sb.lastIndexOf('z', 2)); + + sb = new TextStringBuilder("xyzabc"); + assertEquals(2, sb.lastIndexOf('z', sb.length())); + assertEquals(-1, sb.lastIndexOf('z', 1)); + } + + @Test + public void testLastIndexOf_String() { + final TextStringBuilder sb = new TextStringBuilder("abab"); + + assertEquals(2, sb.lastIndexOf("a")); + // should work like String#lastIndexOf + assertEquals("abab".lastIndexOf("a"), sb.lastIndexOf("a")); + + assertEquals(2, sb.lastIndexOf("ab")); + // should work like String#lastIndexOf + assertEquals("abab".lastIndexOf("ab"), sb.lastIndexOf("ab")); + + assertEquals(3, sb.lastIndexOf("b")); + assertEquals("abab".lastIndexOf("b"), sb.lastIndexOf("b")); + + assertEquals(1, sb.lastIndexOf("ba")); + assertEquals("abab".lastIndexOf("ba"), sb.lastIndexOf("ba")); + + assertEquals(-1, sb.lastIndexOf("z")); + + assertEquals(-1, sb.lastIndexOf((String) null)); + } + + @Test + public void testLastIndexOf_String_int() { + TextStringBuilder sb = new TextStringBuilder("abab"); + assertEquals(-1, sb.lastIndexOf("a", -1)); + assertEquals(0, sb.lastIndexOf("a", 0)); + assertEquals(0, sb.lastIndexOf("a", 1)); + assertEquals(2, sb.lastIndexOf("a", 2)); + assertEquals(2, sb.lastIndexOf("a", 3)); + assertEquals(2, sb.lastIndexOf("a", 4)); + assertEquals(2, sb.lastIndexOf("a", 5)); + + assertEquals(-1, sb.lastIndexOf("abcdef", 3)); + assertEquals("abab".lastIndexOf("", 3), sb.lastIndexOf("", 3)); + assertEquals("abab".lastIndexOf("", 1), sb.lastIndexOf("", 1)); + + // should work like String#lastIndexOf + assertEquals("abab".lastIndexOf("a", 1), sb.lastIndexOf("a", 1)); + + assertEquals(0, sb.lastIndexOf("ab", 1)); + // should work like String#lastIndexOf + assertEquals("abab".lastIndexOf("ab", 1), sb.lastIndexOf("ab", 1)); + + assertEquals(1, sb.lastIndexOf("b", 2)); + assertEquals("abab".lastIndexOf("b", 2), sb.lastIndexOf("b", 2)); + + assertEquals(1, sb.lastIndexOf("ba", 2)); + assertEquals("abab".lastIndexOf("ba", 2), sb.lastIndexOf("ba", 2)); + + assertEquals(-1, sb.lastIndexOf("z", 2)); + + sb = new TextStringBuilder("xyzabc"); + assertEquals(2, sb.lastIndexOf("za", sb.length())); + assertEquals(-1, sb.lastIndexOf("za", 1)); + + assertEquals(-1, sb.lastIndexOf((String) null, 2)); + } + + @Test + public void testLastIndexOf_StringMatcher() { + final TextStringBuilder sb = new TextStringBuilder(); + assertEquals(-1, sb.lastIndexOf((StringMatcher) null)); + assertEquals(-1, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('a'))); + + sb.append("ab bd"); + assertEquals(0, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('a'))); + assertEquals(3, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('b'))); + assertEquals(2, sb.lastIndexOf(StringMatcherFactory.INSTANCE.spaceMatcher())); + assertEquals(4, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('d'))); + assertEquals(-1, sb.lastIndexOf(StringMatcherFactory.INSTANCE.noneMatcher())); + assertEquals(-1, sb.lastIndexOf((StringMatcher) null)); + + sb.append(" A1 junction"); + assertEquals(6, sb.lastIndexOf(A_NUMBER_MATCHER)); + } + + @Test + public void testLastIndexOf_StringMatcher_int() { + final TextStringBuilder sb = new TextStringBuilder(); + assertEquals(-1, sb.lastIndexOf((StringMatcher) null, 2)); + assertEquals(-1, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('a'), 2)); + assertEquals(-1, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('a'), 0)); + assertEquals(-1, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('a'), -1)); + + sb.append("ab bd"); + assertEquals(-1, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('a'), -2)); + assertEquals(0, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('a'), 0)); + assertEquals(0, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('a'), 2)); + assertEquals(0, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('a'), 20)); + + assertEquals(-1, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('b'), -1)); + assertEquals(-1, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('b'), 0)); + assertEquals(1, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('b'), 1)); + assertEquals(1, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('b'), 2)); + assertEquals(3, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('b'), 3)); + assertEquals(3, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('b'), 4)); + assertEquals(3, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('b'), 5)); + assertEquals(3, sb.lastIndexOf(StringMatcherFactory.INSTANCE.charMatcher('b'), 6)); + + assertEquals(-1, sb.lastIndexOf(StringMatcherFactory.INSTANCE.spaceMatcher(), -2)); + assertEquals(-1, sb.lastIndexOf(StringMatcherFactory.INSTANCE.spaceMatcher(), 0)); + assertEquals(2, sb.lastIndexOf(StringMatcherFactory.INSTANCE.spaceMatcher(), 2)); + assertEquals(2, sb.lastIndexOf(StringMatcherFactory.INSTANCE.spaceMatcher(), 4)); + assertEquals(2, sb.lastIndexOf(StringMatcherFactory.INSTANCE.spaceMatcher(), 20)); + + assertEquals(-1, sb.lastIndexOf(StringMatcherFactory.INSTANCE.noneMatcher(), 0)); + assertEquals(-1, sb.lastIndexOf((StringMatcher) null, 0)); + + sb.append(" A1 junction with A2"); + assertEquals(-1, sb.lastIndexOf(A_NUMBER_MATCHER, 5)); + assertEquals(-1, sb.lastIndexOf(A_NUMBER_MATCHER, 6)); // A matches, 1 + // is outside + // bounds + assertEquals(6, sb.lastIndexOf(A_NUMBER_MATCHER, 7)); + assertEquals(6, sb.lastIndexOf(A_NUMBER_MATCHER, 22)); + assertEquals(6, sb.lastIndexOf(A_NUMBER_MATCHER, 23)); // A matches, 2 + // is outside + // bounds + assertEquals(23, sb.lastIndexOf(A_NUMBER_MATCHER, 24)); + } + + @Test + public void testLeftString() { + final TextStringBuilder sb = new TextStringBuilder("left right"); + assertEquals("left", sb.leftString(4)); + assertEquals("", sb.leftString(0)); + assertEquals("", sb.leftString(-5)); + assertEquals("left right", sb.leftString(15)); + } + + @Test + public void testLength() { + final TextStringBuilder sb = new TextStringBuilder(); + assertEquals(0, sb.length()); + + sb.append("Hello"); + assertEquals(5, sb.length()); + } + + @Test + public void testMidString() { + final TextStringBuilder sb = new TextStringBuilder("hello goodbye hello"); + assertEquals("goodbye", sb.midString(6, 7)); + assertEquals("hello", sb.midString(0, 5)); + assertEquals("hello", sb.midString(-5, 5)); + assertEquals("", sb.midString(0, -1)); + assertEquals("", sb.midString(20, 2)); + assertEquals("hello", sb.midString(14, 22)); + } + + @Test + public void testMinimizeCapacity() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.minimizeCapacity(); + assertEquals(0, sb.capacity()); + + sb.append("HelloWorld"); + sb.minimizeCapacity(); + assertEquals(10, sb.capacity()); + } + + @Test + public void testReadFromCharBuffer() throws Exception { + String s = ""; + for (int i = 0; i < 100; ++i) { + final TextStringBuilder sb = new TextStringBuilder(); + final int len = sb.readFrom(CharBuffer.wrap(s)); + + assertEquals(s.length(), len); + assertEquals(s, sb.toString()); + + s += Integer.toString(i); + } + } + + @Test + public void testReadFromCharBufferAppendsToEnd() throws Exception { + final TextStringBuilder sb = new TextStringBuilder("Test"); + sb.readFrom(CharBuffer.wrap(" 123")); + assertEquals("Test 123", sb.toString()); + } + + @Test + public void testReadFromReadable() throws Exception { + String s = ""; + for (int i = 0; i < 100; ++i) { + final TextStringBuilder sb = new TextStringBuilder(); + final int len = sb.readFrom(new MockReadable(s)); + + assertEquals(s.length(), len); + assertEquals(s, sb.toString()); + + s += Integer.toString(i); + } + } + + @Test + public void testReadFromReadableAppendsToEnd() throws Exception { + final TextStringBuilder sb = new TextStringBuilder("Test"); + sb.readFrom(new MockReadable(" 123")); + assertEquals("Test 123", sb.toString()); + } + + @Test + public void testReadFromReader() throws Exception { + String s = "1"; + for (int i = 0; i < 100; ++i) { + final TextStringBuilder sb = new TextStringBuilder(); + final int len = sb.readFrom(new StringReader(s)); + + assertEquals(s.length(), len); + assertEquals(s, sb.toString()); + + s += Integer.toString(i); + } + } + + @Test + public void testReadFromReaderAppendsToEnd() throws Exception { + final TextStringBuilder sb = new TextStringBuilder("Test"); + sb.readFrom(new StringReader(" 123")); + assertEquals("Test 123", sb.toString()); + } + + @Test + public void testReadFromReaderEmpty() throws Exception { + final TextStringBuilder sb = new TextStringBuilder(); + final int len = sb.readFrom(new StringReader(StringUtils.EMPTY)); + assertEquals(-1, len); + assertEquals(StringUtils.EMPTY, sb.toString()); + } + + @Test + public void testReadFromReaderInt() throws Exception { + String str = ""; + for (int i = 0; i < 100; ++i) { + final TextStringBuilder sb = new TextStringBuilder(); + final int len = sb.readFrom(new StringReader(str), str.length()); + + assertEquals(str.length(), len); + assertEquals(str, sb.toString()); + + str += Integer.toString(i); + } + // + TextStringBuilder sb; + int count; + int target; + final String source = "abc"; + final int sourceLen = source.length(); + // empty + target = -1; + sb = new TextStringBuilder(); + count = sb.readFrom(new StringReader(StringUtils.EMPTY), target); + assertEquals(0, count); + assertEquals(0, sb.size()); + assertEquals(source.substring(0, 0), sb.toString()); + // + target = -1; + sb = new TextStringBuilder(); + count = sb.readFrom(new StringReader(source), target); + assertEquals(0, count); + assertEquals(0, sb.size()); + assertEquals(source.substring(0, 0), sb.toString()); + // + target = 0; + sb = new TextStringBuilder(); + count = sb.readFrom(new StringReader(source), target); + assertEquals(target, count); + assertEquals(target, sb.size()); + assertEquals(source.substring(0, target), sb.toString()); + // + target = 1; + sb = new TextStringBuilder(); + count = sb.readFrom(new StringReader(source), target); + assertEquals(target, count); + assertEquals(target, sb.size()); + assertEquals(source.substring(0, target), sb.toString()); + // + target = 2; + sb = new TextStringBuilder(); + count = sb.readFrom(new StringReader(source), target); + assertEquals(target, count); + assertEquals(target, sb.size()); + assertEquals(source.substring(0, target), sb.toString()); + // + target = 3; + sb = new TextStringBuilder(); + count = sb.readFrom(new StringReader(source), target); + assertEquals(target, count); + assertEquals(target, sb.size()); + assertEquals(source.substring(0, target), sb.toString()); + // + target = 4; + sb = new TextStringBuilder(); + count = sb.readFrom(new StringReader(source), target); + assertEquals(sourceLen, count); + assertEquals(sourceLen, sb.size()); + assertEquals(source.substring(0, sourceLen), sb.toString()); + } + + @Test + public void testReadFromReaderIntEmpty() throws Exception { + final TextStringBuilder sb = new TextStringBuilder(); + final int len = sb.readFrom(new StringReader(StringUtils.EMPTY), 1); + assertEquals(-1, len); + assertEquals(StringUtils.EMPTY, sb.toString()); + } + + @Test + public void testReplace_int_int_String() { + final TextStringBuilder sb = new TextStringBuilder("abc"); + sb.replace(0, 1, "d"); + assertEquals("dbc", sb.toString()); + sb.replace(0, 1, "aaa"); + assertEquals("aaabc", sb.toString()); + sb.replace(0, 3, ""); + assertEquals("bc", sb.toString()); + sb.replace(1, 2, (String) null); + assertEquals("b", sb.toString()); + sb.replace(1, 1000, "text"); + assertEquals("btext", sb.toString()); + sb.replace(0, 1000, "text"); + assertEquals("text", sb.toString()); + + final TextStringBuilder builder = new TextStringBuilder("atext"); + builder.replace(1, 1, "ny"); + assertEquals("anytext", builder.toString()); + + assertThrows(IndexOutOfBoundsException.class, () -> builder.replace(2, 1, "anything")); + + builder.clear(); + assertThrows(IndexOutOfBoundsException.class, () -> builder.replace(1, 2, "anything")); + assertThrows(IndexOutOfBoundsException.class, () -> builder.replace(-1, 1, "anything")); + } + + @Test + public void testReplace_StringMatcher_String_int_int_int_VaryCount() { + TextStringBuilder sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 10, -1); + assertEquals("-x--y-", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 10, 0); + assertEquals("aaxaaaayaa", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 10, 1); + assertEquals("-xaaaayaa", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 10, 2); + assertEquals("-x-aayaa", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 10, 3); + assertEquals("-x--yaa", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 10, 4); + assertEquals("-x--y-", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 10, 5); + assertEquals("-x--y-", sb.toString()); + } + + @Test + public void testReplace_StringMatcher_String_int_int_int_VaryEndIndex() { + TextStringBuilder sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 0, -1); + assertEquals("aaxaaaayaa", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 2, -1); + assertEquals("-xaaaayaa", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 3, -1); + assertEquals("-xaaaayaa", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 4, -1); + assertEquals("-xaaaayaa", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 5, -1); + assertEquals("-x-aayaa", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 6, -1); + assertEquals("-x-aayaa", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 7, -1); + assertEquals("-x--yaa", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 8, -1); + assertEquals("-x--yaa", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 9, -1); + assertEquals("-x--yaa", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 10, -1); + assertEquals("-x--y-", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, 1000, -1); + assertEquals("-x--y-", sb.toString()); + + final TextStringBuilder builder = new TextStringBuilder("aaxaaaayaa"); + assertThrows(IndexOutOfBoundsException.class, + () -> builder.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 2, 1, -1)); + assertEquals("aaxaaaayaa", builder.toString()); + } + + @Test + public void testReplace_StringMatcher_String_int_int_int_VaryMatcher() { + TextStringBuilder sb = new TextStringBuilder("abcbccba"); + sb.replace((StringMatcher) null, "x", 0, sb.length(), -1); + assertEquals("abcbccba", sb.toString()); + + sb.replace(StringMatcherFactory.INSTANCE.charMatcher('a'), "x", 0, sb.length(), -1); + assertEquals("xbcbccbx", sb.toString()); + + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("cb"), "x", 0, sb.length(), -1); + assertEquals("xbxcxx", sb.toString()); + + sb = new TextStringBuilder("A1-A2A3-A4"); + sb.replace(A_NUMBER_MATCHER, "***", 0, sb.length(), -1); + assertEquals("***-******-***", sb.toString()); + + sb = new TextStringBuilder(); + sb.replace(A_NUMBER_MATCHER, "***", 0, sb.length(), -1); + assertEquals("", sb.toString()); + } + + @Test + public void testReplace_StringMatcher_String_int_int_int_VaryReplace() { + TextStringBuilder sb = new TextStringBuilder("abcbccba"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("cb"), "cb", 0, sb.length(), -1); + assertEquals("abcbccba", sb.toString()); + + sb = new TextStringBuilder("abcbccba"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("cb"), "-", 0, sb.length(), -1); + assertEquals("ab-c-a", sb.toString()); + + sb = new TextStringBuilder("abcbccba"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("cb"), "+++", 0, sb.length(), -1); + assertEquals("ab+++c+++a", sb.toString()); + + sb = new TextStringBuilder("abcbccba"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("cb"), "", 0, sb.length(), -1); + assertEquals("abca", sb.toString()); + + sb = new TextStringBuilder("abcbccba"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("cb"), null, 0, sb.length(), -1); + assertEquals("abca", sb.toString()); + } + + @Test + public void testReplace_StringMatcher_String_int_int_int_VaryStartIndex() { + TextStringBuilder sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 0, sb.length(), -1); + assertEquals("-x--y-", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 1, sb.length(), -1); + assertEquals("aax--y-", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 2, sb.length(), -1); + assertEquals("aax--y-", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 3, sb.length(), -1); + assertEquals("aax--y-", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 4, sb.length(), -1); + assertEquals("aaxa-ay-", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 5, sb.length(), -1); + assertEquals("aaxaa-y-", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 6, sb.length(), -1); + assertEquals("aaxaaaay-", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 7, sb.length(), -1); + assertEquals("aaxaaaay-", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 8, sb.length(), -1); + assertEquals("aaxaaaay-", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 9, sb.length(), -1); + assertEquals("aaxaaaayaa", sb.toString()); + + sb = new TextStringBuilder("aaxaaaayaa"); + sb.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 10, sb.length(), -1); + assertEquals("aaxaaaayaa", sb.toString()); + + final TextStringBuilder builder = new TextStringBuilder("aaxaaaayaa"); + assertThrows(IndexOutOfBoundsException.class, + () -> builder.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", 11, builder.length(), -1)); + assertEquals("aaxaaaayaa", builder.toString()); + + builder.set("aaxaaaayaa"); + assertThrows(IndexOutOfBoundsException.class, + () -> builder.replace(StringMatcherFactory.INSTANCE.stringMatcher("aa"), "-", -1, builder.length(), -1)); + assertEquals("aaxaaaayaa", builder.toString()); + } + + @Test + public void testReplaceAll_char_char() { + final TextStringBuilder sb = new TextStringBuilder("abcbccba"); + sb.replaceAll('x', 'y'); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll('a', 'd'); + assertEquals("dbcbccbd", sb.toString()); + sb.replaceAll('b', 'e'); + assertEquals("dececced", sb.toString()); + sb.replaceAll('c', 'f'); + assertEquals("defeffed", sb.toString()); + sb.replaceAll('d', 'd'); + assertEquals("defeffed", sb.toString()); + } + + @Test + public void testReplaceAll_String_String() { + TextStringBuilder sb = new TextStringBuilder("abcbccba"); + sb.replaceAll((String) null, null); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll((String) null, "anything"); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll("", null); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll("", "anything"); + assertEquals("abcbccba", sb.toString()); + + sb.replaceAll("x", "y"); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll("a", "d"); + assertEquals("dbcbccbd", sb.toString()); + sb.replaceAll("d", null); + assertEquals("bcbccb", sb.toString()); + sb.replaceAll("cb", "-"); + assertEquals("b-c-", sb.toString()); + + sb = new TextStringBuilder("abcba"); + sb.replaceAll("b", "xbx"); + assertEquals("axbxcxbxa", sb.toString()); + + sb = new TextStringBuilder("bb"); + sb.replaceAll("b", "xbx"); + assertEquals("xbxxbx", sb.toString()); + } + + @Test + public void testReplaceAll_StringMatcher_String() { + TextStringBuilder sb = new TextStringBuilder("abcbccba"); + sb.replaceAll((StringMatcher) null, null); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll((StringMatcher) null, "anything"); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll(StringMatcherFactory.INSTANCE.noneMatcher(), null); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll(StringMatcherFactory.INSTANCE.noneMatcher(), "anything"); + assertEquals("abcbccba", sb.toString()); + + sb.replaceAll(StringMatcherFactory.INSTANCE.charMatcher('x'), "y"); + assertEquals("abcbccba", sb.toString()); + sb.replaceAll(StringMatcherFactory.INSTANCE.charMatcher('a'), "d"); + assertEquals("dbcbccbd", sb.toString()); + sb.replaceAll(StringMatcherFactory.INSTANCE.charMatcher('d'), null); + assertEquals("bcbccb", sb.toString()); + sb.replaceAll(StringMatcherFactory.INSTANCE.stringMatcher("cb"), "-"); + assertEquals("b-c-", sb.toString()); + + sb = new TextStringBuilder("abcba"); + sb.replaceAll(StringMatcherFactory.INSTANCE.charMatcher('b'), "xbx"); + assertEquals("axbxcxbxa", sb.toString()); + + sb = new TextStringBuilder("bb"); + sb.replaceAll(StringMatcherFactory.INSTANCE.charMatcher('b'), "xbx"); + assertEquals("xbxxbx", sb.toString()); + + sb = new TextStringBuilder("A1-A2A3-A4"); + sb.replaceAll(A_NUMBER_MATCHER, "***"); + assertEquals("***-******-***", sb.toString()); + + sb = new TextStringBuilder("Dear X, hello X."); + sb.replaceAll(StringMatcherFactory.INSTANCE.stringMatcher("X"), "012345678901234567"); + assertEquals("Dear 012345678901234567, hello 012345678901234567.", sb.toString()); + } + + @Test + public void testReplaceFirst_char_char() { + final TextStringBuilder sb = new TextStringBuilder("abcbccba"); + sb.replaceFirst('x', 'y'); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst('a', 'd'); + assertEquals("dbcbccba", sb.toString()); + sb.replaceFirst('b', 'e'); + assertEquals("decbccba", sb.toString()); + sb.replaceFirst('c', 'f'); + assertEquals("defbccba", sb.toString()); + sb.replaceFirst('d', 'd'); + assertEquals("defbccba", sb.toString()); + } + + @Test + public void testReplaceFirst_String_String() { + TextStringBuilder sb = new TextStringBuilder("abcbccba"); + sb.replaceFirst((String) null, null); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst((String) null, "anything"); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst("", null); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst("", "anything"); + assertEquals("abcbccba", sb.toString()); + + sb.replaceFirst("x", "y"); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst("a", "d"); + assertEquals("dbcbccba", sb.toString()); + sb.replaceFirst("d", null); + assertEquals("bcbccba", sb.toString()); + sb.replaceFirst("cb", "-"); + assertEquals("b-ccba", sb.toString()); + + sb = new TextStringBuilder("abcba"); + sb.replaceFirst("b", "xbx"); + assertEquals("axbxcba", sb.toString()); + + sb = new TextStringBuilder("bb"); + sb.replaceFirst("b", "xbx"); + assertEquals("xbxb", sb.toString()); + } + + @Test + public void testReplaceFirst_StringMatcher_String() { + TextStringBuilder sb = new TextStringBuilder("abcbccba"); + sb.replaceFirst((StringMatcher) null, null); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst((StringMatcher) null, "anything"); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst(StringMatcherFactory.INSTANCE.noneMatcher(), null); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst(StringMatcherFactory.INSTANCE.noneMatcher(), "anything"); + assertEquals("abcbccba", sb.toString()); + + sb.replaceFirst(StringMatcherFactory.INSTANCE.charMatcher('x'), "y"); + assertEquals("abcbccba", sb.toString()); + sb.replaceFirst(StringMatcherFactory.INSTANCE.charMatcher('a'), "d"); + assertEquals("dbcbccba", sb.toString()); + sb.replaceFirst(StringMatcherFactory.INSTANCE.charMatcher('d'), null); + assertEquals("bcbccba", sb.toString()); + sb.replaceFirst(StringMatcherFactory.INSTANCE.stringMatcher("cb"), "-"); + assertEquals("b-ccba", sb.toString()); + + sb = new TextStringBuilder("abcba"); + sb.replaceFirst(StringMatcherFactory.INSTANCE.charMatcher('b'), "xbx"); + assertEquals("axbxcba", sb.toString()); + + sb = new TextStringBuilder("bb"); + sb.replaceFirst(StringMatcherFactory.INSTANCE.charMatcher('b'), "xbx"); + assertEquals("xbxb", sb.toString()); + + sb = new TextStringBuilder("A1-A2A3-A4"); + sb.replaceFirst(A_NUMBER_MATCHER, "***"); + assertEquals("***-A2A3-A4", sb.toString()); + } + + @Test + public void testReverse() { + final TextStringBuilder sb = new TextStringBuilder(); + assertEquals("", sb.reverse().toString()); + + sb.clear().append(true); + assertEquals("eurt", sb.reverse().toString()); + assertEquals("true", sb.reverse().toString()); + } + + @Test + public void testRightString() { + final TextStringBuilder sb = new TextStringBuilder("left right"); + assertEquals("right", sb.rightString(5)); + assertEquals("", sb.rightString(0)); + assertEquals("", sb.rightString(-5)); + assertEquals("left right", sb.rightString(15)); + } + + @Test + public void testSetCharAt() { + final TextStringBuilder sb = new TextStringBuilder(); + assertThrows(IndexOutOfBoundsException.class, () -> sb.setCharAt(0, 'f')); + assertThrows(IndexOutOfBoundsException.class, () -> sb.setCharAt(-1, 'f')); + sb.append("foo"); + sb.setCharAt(0, 'b'); + sb.setCharAt(1, 'a'); + sb.setCharAt(2, 'r'); + assertThrows(IndexOutOfBoundsException.class, () -> sb.setCharAt(3, '!')); + assertEquals("bar", sb.toString()); + } + + @Test + public void testSetLength() { + final TextStringBuilder sb = new TextStringBuilder(); + sb.append("Hello"); + sb.setLength(2); // shorten + assertEquals("He", sb.toString()); + sb.setLength(2); // no change + assertEquals("He", sb.toString()); + sb.setLength(3); // lengthen + assertEquals("He\0", sb.toString()); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.setLength(-1)); + } + + @Test + public void testSize() { + final TextStringBuilder sb = new TextStringBuilder(); + assertEquals(0, sb.size()); + + sb.append("Hello"); + assertEquals(5, sb.size()); + } + + @Test + public void testStartsWith() { + final TextStringBuilder sb = new TextStringBuilder(); + assertFalse(sb.startsWith("a")); + assertFalse(sb.startsWith(null)); + assertTrue(sb.startsWith("")); + sb.append("abc"); + assertTrue(sb.startsWith("a")); + assertTrue(sb.startsWith("ab")); + assertTrue(sb.startsWith("abc")); + assertFalse(sb.startsWith("cba")); + } + + @Test + public void testSubSequenceIntInt() { + final TextStringBuilder sb = new TextStringBuilder("hello goodbye"); + // Start index is negative + assertThrows(IndexOutOfBoundsException.class, () -> sb.subSequence(-1, 5)); + + // End index is negative + assertThrows(IndexOutOfBoundsException.class, () -> sb.subSequence(2, -1)); + + // End index greater than length() + assertThrows(IndexOutOfBoundsException.class, () -> sb.subSequence(2, sb.length() + 1)); + + // Start index greater then end index + assertThrows(IndexOutOfBoundsException.class, () -> sb.subSequence(3, 2)); + + // Normal cases + assertEquals("hello", sb.subSequence(0, 5)); + assertEquals("hello goodbye".subSequence(0, 6), sb.subSequence(0, 6)); + assertEquals("goodbye", sb.subSequence(6, 13)); + assertEquals("hello goodbye".subSequence(6, 13), sb.subSequence(6, 13)); + } + + @Test + public void testSubstringInt() { + final TextStringBuilder sb = new TextStringBuilder("hello goodbye"); + assertEquals("goodbye", sb.substring(6)); + assertEquals("hello goodbye".substring(6), sb.substring(6)); + assertEquals("hello goodbye", sb.substring(0)); + assertEquals("hello goodbye".substring(0), sb.substring(0)); + assertThrows(IndexOutOfBoundsException.class, () -> sb.substring(-1)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.substring(15)); + + } + + @Test + public void testSubstringIntInt() { + final TextStringBuilder sb = new TextStringBuilder("hello goodbye"); + assertEquals("hello", sb.substring(0, 5)); + assertEquals("hello goodbye".substring(0, 6), sb.substring(0, 6)); + + assertEquals("goodbye", sb.substring(6, 13)); + assertEquals("hello goodbye".substring(6, 13), sb.substring(6, 13)); + + assertEquals("goodbye", sb.substring(6, 20)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.substring(-1, 5)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.substring(15, 20)); + } + + @Test + public void testToCharArray() { + final TextStringBuilder sb = new TextStringBuilder(); + assertEquals(0, sb.toCharArray().length); + + char[] a = sb.toCharArray(); + assertNotNull(a, "toCharArray() result is null"); + assertEquals(0, a.length, "toCharArray() result is too large"); + + sb.append("junit"); + a = sb.toCharArray(); + assertEquals(5, a.length, "toCharArray() result incorrect length"); + assertArrayEquals("junit".toCharArray(), a, "toCharArray() result does not match"); + } + + @Test + public void testToCharArrayIntInt() { + final TextStringBuilder sb = new TextStringBuilder(); + assertEquals(0, sb.toCharArray(0, 0).length); + + sb.append("junit"); + char[] a = sb.toCharArray(0, 20); // too large test + assertEquals(5, a.length, "toCharArray(int,int) result incorrect length"); + assertArrayEquals("junit".toCharArray(), a, "toCharArray(int,int) result does not match"); + + a = sb.toCharArray(0, 4); + assertEquals(4, a.length, "toCharArray(int,int) result incorrect length"); + assertArrayEquals("juni".toCharArray(), a, "toCharArray(int,int) result does not match"); + + a = sb.toCharArray(0, 4); + assertEquals(4, a.length, "toCharArray(int,int) result incorrect length"); + assertArrayEquals("juni".toCharArray(), a, "toCharArray(int,int) result does not match"); + + a = sb.toCharArray(0, 1); + assertNotNull(a, "toCharArray(int,int) result is null"); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.toCharArray(-1, 5)); + + assertThrows(IndexOutOfBoundsException.class, () -> sb.toCharArray(6, 5)); + } + + @Test + public void testToString() { + final TextStringBuilder sb = new TextStringBuilder("abc"); + assertEquals("abc", sb.toString()); + } + + @Test + public void testToStringBuffer() { + final TextStringBuilder sb = new TextStringBuilder(); + assertEquals(new StringBuffer().toString(), sb.toStringBuffer().toString()); + + sb.append("junit"); + assertEquals(new StringBuffer("junit").toString(), sb.toStringBuffer().toString()); + } + + @Test + public void testToStringBuilder() { + final TextStringBuilder sb = new TextStringBuilder(); + assertEquals(new StringBuilder().toString(), sb.toStringBuilder().toString()); + + sb.append("junit"); + assertEquals(new StringBuilder("junit").toString(), sb.toStringBuilder().toString()); + } + + @Test + public void testTrim() { + final TextStringBuilder sb = new TextStringBuilder(); + assertEquals("", sb.reverse().toString()); + + sb.set(" \u0000 "); + assertEquals("", sb.trim().toString()); + + sb.set(" \u0000 a b c"); + assertEquals("a b c", sb.trim().toString()); + + sb.set("a b c \u0000 "); + assertEquals("a b c", sb.trim().toString()); + + sb.set(" \u0000 a b c \u0000 "); + assertEquals("a b c", sb.trim().toString()); + + sb.set("a b c"); + assertEquals("a b c", sb.trim().toString()); + } + + @Test + public void testWrap_CharArray() { + assertThrows(NullPointerException.class, () -> TextStringBuilder.wrap(null)); + // + final TextStringBuilder initEmpty = TextStringBuilder.wrap(ArrayUtils.EMPTY_CHAR_ARRAY); + assertEquals(0, initEmpty.size()); + assertEquals(0, initEmpty.length()); + initEmpty.append('a'); + assertEquals(1, initEmpty.size()); + assertEquals(1, initEmpty.length()); + // + final char[] test = "abc".toCharArray(); + final TextStringBuilder builder = TextStringBuilder.wrap(test); + assertArrayEquals(test, builder.getBuffer()); + assertEquals(test.length, builder.length()); + assertEquals(test.length, builder.size()); + assertFalse(builder.isReallocated()); + builder.ensureCapacity(builder.capacity() * 2); + assertFalse(Arrays.equals(test, builder.getBuffer())); + assertTrue(builder.isReallocated()); + } + + private void testWrap_CharArray_Int(final String test, final int expectedLen) { + final char[] input = "abc".toCharArray(); + final TextStringBuilder builder = TextStringBuilder.wrap(input, expectedLen); + assertArrayEquals(input, builder.getBuffer()); + assertEquals(expectedLen, builder.length()); + assertEquals(expectedLen, builder.size()); + assertFalse(builder.isReallocated()); + builder.ensureCapacity(builder.capacity() * 2); + assertFalse(Arrays.equals(input, builder.getBuffer())); + assertTrue(builder.isReallocated()); + } + + @Test + public void testWrap_CharArray_Int_0() { + testWrap_CharArray_Int("abc", 0); + } + + @Test + public void testWrap_CharArray_Int_1() { + testWrap_CharArray_Int("abc", 1); + } + + @Test + public void testWrap_CharArray_Int_2() { + testWrap_CharArray_Int("abc", 2); + } + + @Test + public void testWrap_CharArray_Int_3() { + testWrap_CharArray_Int("abc", 3); + } + + @Test + public void testWrap_CharArray_Int_Empty_0() { + final TextStringBuilder initEmpty = TextStringBuilder.wrap(ArrayUtils.EMPTY_CHAR_ARRAY, 0); + assertEquals(0, initEmpty.size()); + assertEquals(0, initEmpty.length()); + initEmpty.append('a'); + assertEquals(1, initEmpty.size()); + assertEquals(1, initEmpty.length()); + } + + @Test + public void testWrap_CharArray_Int_Exceptions() { + assertThrows(NullPointerException.class, () -> TextStringBuilder.wrap(null, 0)); + assertThrows(IllegalArgumentException.class, () -> TextStringBuilder.wrap("abc".toCharArray(), -1)); + assertThrows(IllegalArgumentException.class, () -> TextStringBuilder.wrap(ArrayUtils.EMPTY_CHAR_ARRAY, 1)); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/WordUtilsTest.java b/sources/src/test/java/org/apache/commons/text/WordUtilsTest.java new file mode 100644 index 0000000..6c8d97b --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/WordUtilsTest.java @@ -0,0 +1,550 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link WordUtils} class. + */ +public class WordUtilsTest { + + @Test + public void testAbbreviateForLowerThanMinusOneValues() { + assertThatIllegalArgumentException().isThrownBy(() -> assertThat(WordUtils.abbreviate("01 23 45 67 89", 9, -10, null)).isEqualTo("01 23 45 67")); + } + + @Test + public void testAbbreviateForLowerValue() { + assertThat(WordUtils.abbreviate("012 3456789", 0, 5, null)).isEqualTo("012"); + assertThat(WordUtils.abbreviate("01234 56789", 5, 10, null)).isEqualTo("01234"); + assertThat(WordUtils.abbreviate("01 23 45 67 89", 9, -1, null)).isEqualTo("01 23 45 67"); + assertThat(WordUtils.abbreviate("01 23 45 67 89", 9, 10, null)).isEqualTo("01 23 45 6"); + assertThat(WordUtils.abbreviate("0123456789", 15, 20, null)).isEqualTo("0123456789"); + } + + @Test + public void testAbbreviateForLowerValueAndAppendedString() { + assertThat(WordUtils.abbreviate("012 3456789", 0, 5, null)).isEqualTo("012"); + assertThat(WordUtils.abbreviate("01234 56789", 5, 10, "-")).isEqualTo("01234-"); + assertThat(WordUtils.abbreviate("01 23 45 67 89", 9, -1, "abc")).isEqualTo("01 23 45 67abc"); + assertThat(WordUtils.abbreviate("01 23 45 67 89", 9, 10, "")).isEqualTo("01 23 45 6"); + } + + @Test + public void testAbbreviateForNullAndEmptyString() { + assertThat(WordUtils.abbreviate(null, 1, -1, "")).isNull(); + assertThat(WordUtils.abbreviate("", 1, -1, "")).isEqualTo(StringUtils.EMPTY); + assertThat(WordUtils.abbreviate("0123456790", 0, 0, "")).isEqualTo(""); + assertThat(WordUtils.abbreviate(" 0123456790", 0, -1, "")).isEqualTo(""); + } + + @Test + public void testAbbreviateForUpperLimit() { + assertThat(WordUtils.abbreviate("0123456789", 0, 5, "")).isEqualTo("01234"); + assertThat(WordUtils.abbreviate("012 3456789", 2, 5, "")).isEqualTo("012"); + assertThat(WordUtils.abbreviate("0123456789", 0, -1, "")).isEqualTo("0123456789"); + } + + @Test + public void testAbbreviateForUpperLimitAndAppendedString() { + assertThat(WordUtils.abbreviate("0123456789", 0, 5, "-")).isEqualTo("01234-"); + assertThat(WordUtils.abbreviate("012 3456789", 2, 5, null)).isEqualTo("012"); + assertThat(WordUtils.abbreviate("0123456789", 0, -1, "")).isEqualTo("0123456789"); + } + + @Test + public void testAbbreviateUpperLessThanLowerValues() { + assertThatIllegalArgumentException().isThrownBy(() -> assertThat(WordUtils.abbreviate("0123456789", 5, 2, "")).isEqualTo("01234")); + } + + @Test + public void testCapitalize_String() { + assertThat(WordUtils.capitalize(null)).isNull(); + assertThat(WordUtils.capitalize("")).isEqualTo(""); + assertThat(WordUtils.capitalize(" ")).isEqualTo(" "); + + assertThat(WordUtils.capitalize("I")).isEqualTo("I"); + assertThat(WordUtils.capitalize("i")).isEqualTo("I"); + assertThat(WordUtils.capitalize("i am here 123")).isEqualTo("I Am Here 123"); + assertThat(WordUtils.capitalize("I Am Here 123")).isEqualTo("I Am Here 123"); + assertThat(WordUtils.capitalize("i am HERE 123")).isEqualTo("I Am HERE 123"); + assertThat(WordUtils.capitalize("I AM HERE 123")).isEqualTo("I AM HERE 123"); + } + + @Test + public void testCapitalizeFully_String() { + assertThat(WordUtils.capitalizeFully(null)).isNull(); + assertThat(WordUtils.capitalizeFully("")).isEqualTo(""); + assertThat(WordUtils.capitalizeFully(" ")).isEqualTo(" "); + + assertThat(WordUtils.capitalizeFully("I")).isEqualTo("I"); + assertThat(WordUtils.capitalizeFully("i")).isEqualTo("I"); + assertThat(WordUtils.capitalizeFully("i am here 123")).isEqualTo("I Am Here 123"); + assertThat(WordUtils.capitalizeFully("I Am Here 123")).isEqualTo("I Am Here 123"); + assertThat(WordUtils.capitalizeFully("i am HERE 123")).isEqualTo("I Am Here 123"); + assertThat(WordUtils.capitalizeFully("I AM HERE 123")).isEqualTo("I Am Here 123"); + assertThat(WordUtils.capitalizeFully("alphabet")).isEqualTo("Alphabet"); // single word + } + + @Test + public void testCapitalizeFully_Text88() { + assertThat(WordUtils.capitalizeFully("i am fine now", new char[] {})).isEqualTo("I am fine now"); + } + + @Test + public void testCapitalizeFullyWithDelimiters_String() { + assertThat(WordUtils.capitalizeFully(null, null)).isNull(); + assertThat(WordUtils.capitalizeFully("", ArrayUtils.EMPTY_CHAR_ARRAY)).isEqualTo(""); + assertThat(WordUtils.capitalizeFully(" ", ArrayUtils.EMPTY_CHAR_ARRAY)).isEqualTo(" "); + + char[] chars = {'-', '+', ' ', '@'}; + assertThat(WordUtils.capitalizeFully("I", chars)).isEqualTo("I"); + assertThat(WordUtils.capitalizeFully("i", chars)).isEqualTo("I"); + assertThat(WordUtils.capitalizeFully("i-am here+123", chars)).isEqualTo("I-Am Here+123"); + assertThat(WordUtils.capitalizeFully("I Am+Here-123", chars)).isEqualTo("I Am+Here-123"); + assertThat(WordUtils.capitalizeFully("i+am-HERE 123", chars)).isEqualTo("I+Am-Here 123"); + assertThat(WordUtils.capitalizeFully("I-AM HERE+123", chars)).isEqualTo("I-Am Here+123"); + chars = new char[] {'.'}; + assertThat(WordUtils.capitalizeFully("i aM.fine", chars)).isEqualTo("I am.Fine"); + assertThat(WordUtils.capitalizeFully("i am.fine", null)).isEqualTo("I Am.fine"); + assertThat(WordUtils.capitalizeFully("alphabet", null)).isEqualTo("Alphabet"); // single word + assertThat(WordUtils.capitalizeFully("alphabet", new char[] {'!'})).isEqualTo("Alphabet"); // no matching delim + } + + @Test + public void testCapitalizeWithDelimiters_String() { + assertThat(WordUtils.capitalize(null, null)).isNull(); + assertThat(WordUtils.capitalize("", ArrayUtils.EMPTY_CHAR_ARRAY)).isEqualTo(""); + assertThat(WordUtils.capitalize(" ", ArrayUtils.EMPTY_CHAR_ARRAY)).isEqualTo(" "); + + char[] chars = {'-', '+', ' ', '@'}; + assertThat(WordUtils.capitalize("I", chars)).isEqualTo("I"); + assertThat(WordUtils.capitalize("i", chars)).isEqualTo("I"); + assertThat(WordUtils.capitalize("i-am here+123", chars)).isEqualTo("I-Am Here+123"); + assertThat(WordUtils.capitalize("I Am+Here-123", chars)).isEqualTo("I Am+Here-123"); + assertThat(WordUtils.capitalize("i+am-HERE 123", chars)).isEqualTo("I+Am-HERE 123"); + assertThat(WordUtils.capitalize("I-AM HERE+123", chars)).isEqualTo("I-AM HERE+123"); + chars = new char[] {'.'}; + assertThat(WordUtils.capitalize("i aM.fine", chars)).isEqualTo("I aM.Fine"); + assertThat(WordUtils.capitalize("i am.fine", null)).isEqualTo("I Am.fine"); + } + + @Test + public void testConstructor() { + assertThat(new WordUtils()).isNotNull(); + final Constructor[] cons = WordUtils.class.getDeclaredConstructors(); + assertThat(cons.length).isEqualTo(1); + assertThat(Modifier.isPublic(cons[0].getModifiers())).isTrue(); + assertThat(Modifier.isPublic(WordUtils.class.getModifiers())).isTrue(); + assertThat(Modifier.isFinal(WordUtils.class.getModifiers())).isFalse(); + } + + @Test + public void testContainsAllWords_StringString() { + assertThat(WordUtils.containsAllWords(null, (String) null)).isFalse(); + assertThat(WordUtils.containsAllWords(null, "")).isFalse(); + assertThat(WordUtils.containsAllWords(null, "ab")).isFalse(); + + assertThat(WordUtils.containsAllWords("", (String) null)).isFalse(); + assertThat(WordUtils.containsAllWords("", "")).isFalse(); + assertThat(WordUtils.containsAllWords("", "ab")).isFalse(); + + assertThat(WordUtils.containsAllWords("foo", (String) null)).isFalse(); + assertThat(WordUtils.containsAllWords("bar", "")).isFalse(); + assertThat(WordUtils.containsAllWords("zzabyycdxx", "by")).isFalse(); + assertThat(WordUtils.containsAllWords("lorem ipsum dolor sit amet", "ipsum", "lorem", "dolor")).isTrue(); + assertThat(WordUtils.containsAllWords("lorem ipsum dolor sit amet", "ipsum", null, "lorem", "dolor")).isFalse(); + assertThat(WordUtils.containsAllWords("lorem ipsum null dolor sit amet", "ipsum", null, "lorem", "dolor")) + .isFalse(); + assertThat(WordUtils.containsAllWords("ab", "b")).isFalse(); + assertThat(WordUtils.containsAllWords("ab", "z")).isFalse(); + } + + @Test + public void testContainsAllWordsWithNull() { + assertThat(WordUtils.containsAllWords("M", (CharSequence) null)).isFalse(); + } + + @Test + public void testInitials_String() { + assertThat(WordUtils.initials(null)).isNull(); + assertThat(WordUtils.initials("")).isEqualTo(""); + assertThat(WordUtils.initials(" ")).isEqualTo(""); + + assertThat(WordUtils.initials("I")).isEqualTo("I"); + assertThat(WordUtils.initials("i")).isEqualTo("i"); + assertThat(WordUtils.initials("Ben John Lee")).isEqualTo("BJL"); + assertThat(WordUtils.initials(" Ben \n John\tLee\t")).isEqualTo("BJL"); + assertThat(WordUtils.initials("Ben J.Lee")).isEqualTo("BJ"); + assertThat(WordUtils.initials(" Ben John . Lee")).isEqualTo("BJ.L"); + assertThat(WordUtils.initials("i am here 123")).isEqualTo("iah1"); + } + + @Test + public void testInitials_String_charArray() { + char[] array = null; + assertThat(WordUtils.initials(null, array)).isNull(); + assertThat(WordUtils.initials("", array)).isEqualTo(""); + assertThat(WordUtils.initials(" ", array)).isEqualTo(""); + assertThat(WordUtils.initials("I", array)).isEqualTo("I"); + assertThat(WordUtils.initials("i", array)).isEqualTo("i"); + assertThat(WordUtils.initials("SJC", array)).isEqualTo("S"); + assertThat(WordUtils.initials("Ben John Lee", array)).isEqualTo("BJL"); + assertThat(WordUtils.initials(" Ben \n John\tLee\t", array)).isEqualTo("BJL"); + assertThat(WordUtils.initials("Ben J.Lee", array)).isEqualTo("BJ"); + assertThat(WordUtils.initials(" Ben John . Lee", array)).isEqualTo("BJ.L"); + assertThat(WordUtils.initials("Kay O'Murphy", array)).isEqualTo("KO"); + assertThat(WordUtils.initials("i am here 123", array)).isEqualTo("iah1"); + + array = ArrayUtils.EMPTY_CHAR_ARRAY; + assertThat(WordUtils.initials(null, array)).isNull(); + assertThat(WordUtils.initials("", array)).isEqualTo(""); + assertThat(WordUtils.initials(" ", array)).isEqualTo(""); + assertThat(WordUtils.initials("I", array)).isEqualTo(""); + assertThat(WordUtils.initials("i", array)).isEqualTo(""); + assertThat(WordUtils.initials("SJC", array)).isEqualTo(""); + assertThat(WordUtils.initials("Ben John Lee", array)).isEqualTo(""); + assertThat(WordUtils.initials(" Ben \n John\tLee\t", array)).isEqualTo(""); + assertThat(WordUtils.initials("Ben J.Lee", array)).isEqualTo(""); + assertThat(WordUtils.initials(" Ben John . Lee", array)).isEqualTo(""); + assertThat(WordUtils.initials("Kay O'Murphy", array)).isEqualTo(""); + assertThat(WordUtils.initials("i am here 123", array)).isEqualTo(""); + + array = " ".toCharArray(); + assertThat(WordUtils.initials(null, array)).isNull(); + assertThat(WordUtils.initials("", array)).isEqualTo(""); + assertThat(WordUtils.initials(" ", array)).isEqualTo(""); + assertThat(WordUtils.initials("I", array)).isEqualTo("I"); + assertThat(WordUtils.initials("i", array)).isEqualTo("i"); + assertThat(WordUtils.initials("SJC", array)).isEqualTo("S"); + assertThat(WordUtils.initials("Ben John Lee", array)).isEqualTo("BJL"); + assertThat(WordUtils.initials("Ben J.Lee", array)).isEqualTo("BJ"); + assertThat(WordUtils.initials(" Ben \n John\tLee\t", array)).isEqualTo("B\nJ"); + assertThat(WordUtils.initials(" Ben John . Lee", array)).isEqualTo("BJ.L"); + assertThat(WordUtils.initials("Kay O'Murphy", array)).isEqualTo("KO"); + assertThat(WordUtils.initials("i am here 123", array)).isEqualTo("iah1"); + + array = " .".toCharArray(); + assertThat(WordUtils.initials(null, array)).isNull(); + assertThat(WordUtils.initials("", array)).isEqualTo(""); + assertThat(WordUtils.initials(" ", array)).isEqualTo(""); + assertThat(WordUtils.initials("I", array)).isEqualTo("I"); + assertThat(WordUtils.initials("i", array)).isEqualTo("i"); + assertThat(WordUtils.initials("SJC", array)).isEqualTo("S"); + assertThat(WordUtils.initials("Ben John Lee", array)).isEqualTo("BJL"); + assertThat(WordUtils.initials("Ben J.Lee", array)).isEqualTo("BJL"); + assertThat(WordUtils.initials(" Ben John . Lee", array)).isEqualTo("BJL"); + assertThat(WordUtils.initials("Kay O'Murphy", array)).isEqualTo("KO"); + assertThat(WordUtils.initials("i am here 123", array)).isEqualTo("iah1"); + + array = " .'".toCharArray(); + assertThat(WordUtils.initials(null, array)).isNull(); + assertThat(WordUtils.initials("", array)).isEqualTo(""); + assertThat(WordUtils.initials(" ", array)).isEqualTo(""); + assertThat(WordUtils.initials("I", array)).isEqualTo("I"); + assertThat(WordUtils.initials("i", array)).isEqualTo("i"); + assertThat(WordUtils.initials("SJC", array)).isEqualTo("S"); + assertThat(WordUtils.initials("Ben John Lee", array)).isEqualTo("BJL"); + assertThat(WordUtils.initials("Ben J.Lee", array)).isEqualTo("BJL"); + assertThat(WordUtils.initials(" Ben John . Lee", array)).isEqualTo("BJL"); + assertThat(WordUtils.initials("Kay O'Murphy", array)).isEqualTo("KOM"); + assertThat(WordUtils.initials("i am here 123", array)).isEqualTo("iah1"); + + array = "SIJo1".toCharArray(); + assertThat(WordUtils.initials(null, array)).isNull(); + assertThat(WordUtils.initials("", array)).isEqualTo(""); + assertThat(WordUtils.initials(" ", array)).isEqualTo(" "); + assertThat(WordUtils.initials("I", array)).isEqualTo(""); + assertThat(WordUtils.initials("i", array)).isEqualTo("i"); + assertThat(WordUtils.initials("SJC", array)).isEqualTo("C"); + assertThat(WordUtils.initials("Ben John Lee", array)).isEqualTo("Bh"); + assertThat(WordUtils.initials("Ben J.Lee", array)).isEqualTo("B."); + assertThat(WordUtils.initials(" Ben John . Lee", array)).isEqualTo(" h"); + assertThat(WordUtils.initials("Kay O'Murphy", array)).isEqualTo("K"); + assertThat(WordUtils.initials("i am here 123", array)).isEqualTo("i2"); + } + + @Test + public void testInitialsSurrogatePairs() { + // Tests with space as default delimiter + assertThat(WordUtils.initials("\uD800\uDF00\uD800\uDF01 \uD800\uDF02\uD800\uDF03")) + .isEqualTo("\uD800\uDF00\uD800\uDF02"); + assertThat(WordUtils.initials("\uD800\uDF00\uD800\uDF01 \uD800\uDF02\uD800\uDF03", null)) + .isEqualTo("\uD800\uDF00\uD800\uDF02"); + assertThat(WordUtils.initials("\uD800\uDF00 \uD800\uDF02 ", null)).isEqualTo("\uD800\uDF00\uD800\uDF02"); + + // Tests with UTF-16 as delimiters + assertThat(WordUtils.initials("\uD800\uDF00\uD800\uDF01.\uD800\uDF02\uD800\uDF03", new char[] {'.'})) + .isEqualTo("\uD800\uDF00\uD800\uDF02"); + assertThat(WordUtils.initials("\uD800\uDF00\uD800\uDF01A\uD800\uDF02\uD800\uDF03", new char[] {'A'})) + .isEqualTo("\uD800\uDF00\uD800\uDF02"); + + // Tests with UTF-32 as delimiters + assertThat(WordUtils.initials( + "\uD800\uDF00\uD800\uDF01\uD800\uDF14\uD800\uDF02\uD800\uDF03", new char[] {'\uD800', '\uDF14'})) + .isEqualTo("\uD800\uDF00\uD800\uDF02"); + assertThat(WordUtils.initials("\uD800\uDF00\uD800\uDF01\uD800\uDF14\uD800\uDF18\uD800\uDF02\uD800\uDF03", + new char[] {'\uD800', '\uDF14', '\uD800', '\uDF18'})) + .isEqualTo("\uD800\uDF00\uD800\uDF02"); + } + + @Test + public void testLANG1292() { + // Prior to fix, this was throwing StringIndexOutOfBoundsException + WordUtils.wrap("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa " + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa " + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 70); + } + + @Test + public void testLANG673() { + assertThat(WordUtils.abbreviate("01 23 45 67 89", 0, 40, "")).isEqualTo("01"); + assertThat(WordUtils.abbreviate("01 23 45 67 89", 10, 40, "")).isEqualTo("01 23 45 67"); + assertThat(WordUtils.abbreviate("01 23 45 67 89", 40, 40, "")).isEqualTo("01 23 45 67 89"); + } + + @Test + public void testSwapCase_String() { + assertThat(WordUtils.swapCase(null)).isNull(); + assertThat(WordUtils.swapCase("")).isEqualTo(""); + assertThat(WordUtils.swapCase(" ")).isEqualTo(" "); + + assertThat(WordUtils.swapCase("I")).isEqualTo("i"); + assertThat(WordUtils.swapCase("i")).isEqualTo("I"); + assertThat(WordUtils.swapCase("i am here 123")).isEqualTo("I AM HERE 123"); + assertThat(WordUtils.swapCase("I Am Here 123")).isEqualTo("i aM hERE 123"); + assertThat(WordUtils.swapCase("i am HERE 123")).isEqualTo("I AM here 123"); + assertThat(WordUtils.swapCase("I AM HERE 123")).isEqualTo("i am here 123"); + + final String test = "This String contains a TitleCase character: \u01C8"; + final String expect = "tHIS sTRING CONTAINS A tITLEcASE CHARACTER: \u01C9"; + assertThat(WordUtils.swapCase(test)).isEqualTo(expect); + } + + @Test + public void testText123() throws Exception { + // Prior to fix, this was throwing StringIndexOutOfBoundsException + WordUtils.wrap("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa " + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Integer.MAX_VALUE); + } + + @Test + public void testUncapitalize_String() { + assertThat(WordUtils.uncapitalize(null)).isNull(); + assertThat(WordUtils.uncapitalize("")).isEqualTo(""); + assertThat(WordUtils.uncapitalize(" ")).isEqualTo(" "); + + assertThat(WordUtils.uncapitalize("I")).isEqualTo("i"); + assertThat(WordUtils.uncapitalize("i")).isEqualTo("i"); + assertThat(WordUtils.uncapitalize("i am here 123")).isEqualTo("i am here 123"); + assertThat(WordUtils.uncapitalize("I Am Here 123")).isEqualTo("i am here 123"); + assertThat(WordUtils.uncapitalize("i am HERE 123")).isEqualTo("i am hERE 123"); + assertThat(WordUtils.uncapitalize("I AM HERE 123")).isEqualTo("i aM hERE 123"); + } + + @Test + public void testUnCapitalize_Text88() { + assertThat(WordUtils.uncapitalize("I am fine now", new char[] {})).isEqualTo("i am fine now"); + } + + @Test + public void testUncapitalizeWithDelimiters_String() { + assertThat(WordUtils.uncapitalize(null, null)).isNull(); + assertThat(WordUtils.uncapitalize("", ArrayUtils.EMPTY_CHAR_ARRAY)).isEqualTo(""); + assertThat(WordUtils.uncapitalize(" ", ArrayUtils.EMPTY_CHAR_ARRAY)).isEqualTo(" "); + + char[] chars = {'-', '+', ' ', '@'}; + assertThat(WordUtils.uncapitalize("I", chars)).isEqualTo("i"); + assertThat(WordUtils.uncapitalize("i", chars)).isEqualTo("i"); + assertThat(WordUtils.uncapitalize("i am-here+123", chars)).isEqualTo("i am-here+123"); + assertThat(WordUtils.uncapitalize("I+Am Here-123", chars)).isEqualTo("i+am here-123"); + assertThat(WordUtils.uncapitalize("i-am+HERE 123", chars)).isEqualTo("i-am+hERE 123"); + assertThat(WordUtils.uncapitalize("I AM-HERE+123", chars)).isEqualTo("i aM-hERE+123"); + chars = new char[] {'.'}; + assertThat(WordUtils.uncapitalize("I AM.FINE", chars)).isEqualTo("i AM.fINE"); + assertThat(WordUtils.uncapitalize("I AM.FINE", null)).isEqualTo("i aM.FINE"); + } + + @Test + public void testWrap_StringInt() { + assertThat(WordUtils.wrap(null, 20)).isNull(); + assertThat(WordUtils.wrap(null, -1)).isNull(); + + assertThat(WordUtils.wrap("", 20)).isEqualTo(""); + assertThat(WordUtils.wrap("", -1)).isEqualTo(""); + + // normal + final String systemNewLine = System.lineSeparator(); + String input = "Here is one line of text that is going to be wrapped after 20 columns."; + String expected = "Here is one line of" + systemNewLine + "text that is going" + systemNewLine + + "to be wrapped after" + systemNewLine + "20 columns."; + assertThat(WordUtils.wrap(input, 20)).isEqualTo(expected); + + // long word at end + input = "Click here to jump to the commons website - https://commons.apache.org"; + expected = "Click here to jump" + systemNewLine + "to the commons" + systemNewLine + "website -" + systemNewLine + + "https://commons.apache.org"; + assertThat(WordUtils.wrap(input, 20)).isEqualTo(expected); + + // long word in middle + input = "Click here, https://commons.apache.org, to jump to the commons website"; + expected = "Click here," + systemNewLine + "https://commons.apache.org," + systemNewLine + "to jump to the" + + systemNewLine + "commons website"; + assertThat(WordUtils.wrap(input, 20)).isEqualTo(expected); + + // leading spaces on a new line are stripped + // trailing spaces are not stripped + input = "word1 word2 word3"; + expected = "word1 " + systemNewLine + "word2 " + systemNewLine + "word3"; + assertThat(WordUtils.wrap(input, 7)).isEqualTo(expected); + } + + @Test + public void testWrap_StringIntStringBoolean() { + assertThat(WordUtils.wrap(null, 20, "\n", false)).isNull(); + assertThat(WordUtils.wrap(null, 20, "\n", true)).isNull(); + assertThat(WordUtils.wrap(null, 20, null, true)).isNull(); + assertThat(WordUtils.wrap(null, 20, null, false)).isNull(); + assertThat(WordUtils.wrap(null, -1, null, true)).isNull(); + assertThat(WordUtils.wrap(null, -1, null, false)).isNull(); + + assertThat(WordUtils.wrap("", 20, "\n", false)).isEqualTo(""); + assertThat(WordUtils.wrap("", 20, "\n", true)).isEqualTo(""); + assertThat(WordUtils.wrap("", 20, null, false)).isEqualTo(""); + assertThat(WordUtils.wrap("", 20, null, true)).isEqualTo(""); + assertThat(WordUtils.wrap("", -1, null, false)).isEqualTo(""); + assertThat(WordUtils.wrap("", -1, null, true)).isEqualTo(""); + + // normal + String input = "Here is one line of text that is going to be wrapped after 20 columns."; + String expected = "Here is one line of\ntext that is going\nto be wrapped after\n20 columns."; + assertThat(WordUtils.wrap(input, 20, "\n", false)).isEqualTo(expected); + assertThat(WordUtils.wrap(input, 20, "\n", true)).isEqualTo(expected); + + // unusual newline char + input = "Here is one line of text that is going to be wrapped after 20 columns."; + expected = "Here is one line of
text that is going
to be wrapped after
20 columns."; + assertThat(WordUtils.wrap(input, 20, "
", false)).isEqualTo(expected); + assertThat(WordUtils.wrap(input, 20, "
", true)).isEqualTo(expected); + + // short line length + input = "Here is one line"; + expected = "Here\nis one\nline"; + assertThat(WordUtils.wrap(input, 6, "\n", false)).isEqualTo(expected); + expected = "Here\nis\none\nline"; + assertThat(WordUtils.wrap(input, 2, "\n", false)).isEqualTo(expected); + assertThat(WordUtils.wrap(input, -1, "\n", false)).isEqualTo(expected); + + // system newline char + final String systemNewLine = System.lineSeparator(); + input = "Here is one line of text that is going to be wrapped after 20 columns."; + expected = "Here is one line of" + systemNewLine + "text that is going" + systemNewLine + "to be wrapped after" + + systemNewLine + "20 columns."; + assertThat(WordUtils.wrap(input, 20, null, false)).isEqualTo(expected); + assertThat(WordUtils.wrap(input, 20, null, true)).isEqualTo(expected); + + // with extra spaces + input = " Here: is one line of text that is going to be wrapped after 20 columns."; + expected = "Here: is one line\nof text that is \ngoing to be \nwrapped after 20 \ncolumns."; + assertThat(WordUtils.wrap(input, 20, "\n", false)).isEqualTo(expected); + assertThat(WordUtils.wrap(input, 20, "\n", true)).isEqualTo(expected); + + // with tab + input = "Here is\tone line of text that is going to be wrapped after 20 columns."; + expected = "Here is\tone line of\ntext that is going\nto be wrapped after\n20 columns."; + assertThat(WordUtils.wrap(input, 20, "\n", false)).isEqualTo(expected); + assertThat(WordUtils.wrap(input, 20, "\n", true)).isEqualTo(expected); + + // with tab at wrapColumn + input = "Here is one line of\ttext that is going to be wrapped after 20 columns."; + expected = "Here is one line\nof\ttext that is\ngoing to be wrapped\nafter 20 columns."; + assertThat(WordUtils.wrap(input, 20, "\n", false)).isEqualTo(expected); + assertThat(WordUtils.wrap(input, 20, "\n", true)).isEqualTo(expected); + + // difference because of long word + input = "Click here to jump to the commons website - https://commons.apache.org"; + expected = "Click here to jump\nto the commons\nwebsite -\nhttps://commons.apache.org"; + assertThat(WordUtils.wrap(input, 20, "\n", false)).isEqualTo(expected); + expected = "Click here to jump\nto the commons\nwebsite -\nhttps://commons.apac\nhe.org"; + assertThat(WordUtils.wrap(input, 20, "\n", true)).isEqualTo(expected); + + // difference because of long word in middle + input = "Click here, https://commons.apache.org, to jump to the commons website"; + expected = "Click here,\nhttps://commons.apache.org,\nto jump to the\ncommons website"; + assertThat(WordUtils.wrap(input, 20, "\n", false)).isEqualTo(expected); + expected = "Click here,\nhttps://commons.apac\nhe.org, to jump to\nthe commons website"; + assertThat(WordUtils.wrap(input, 20, "\n", true)).isEqualTo(expected); + } + + @Test + public void testWrap_StringIntStringBooleanString() { + + // no changes test + String input = "flammable/inflammable"; + String expected = "flammable/inflammable"; + assertThat(WordUtils.wrap(input, 30, "\n", false, "/")).isEqualTo(expected); + + // wrap on / and small width + expected = "flammable\ninflammable"; + assertThat(WordUtils.wrap(input, 2, "\n", false, "/")).isEqualTo(expected); + + // wrap long words on / 1 + expected = "flammable\ninflammab\nle"; + assertThat(WordUtils.wrap(input, 9, "\n", true, "/")).isEqualTo(expected); + + // wrap long words on / 2 + expected = "flammable\ninflammable"; + assertThat(WordUtils.wrap(input, 15, "\n", true, "/")).isEqualTo(expected); + + // wrap long words on / 3 + input = "flammableinflammable"; + expected = "flammableinflam\nmable"; + assertThat(WordUtils.wrap(input, 15, "\n", true, "/")).isEqualTo(expected); + } + + @Test + public void testWrapAtMiddleTwice() { + assertThat(WordUtils.wrap("abcdefggabcdef", 2, "\n", false, "(?=g)")).isEqualTo("abcdef\n\nabcdef"); + } + + + + @Test + public void testWrapAtStartAndEnd() { + assertThat(WordUtils.wrap("nabcdefabcdefn", 2, "\n", false, "(?=n)")).isEqualTo("\nabcdefabcdef\n"); + } + + @Test + public void testWrapWithMultipleRegexMatchOfLength0() { + assertThat(WordUtils.wrap("abcdefabcdef", 2, "\n", false, "(?=d)")).isEqualTo("abc\ndefabc\ndef"); + } + + @Test + public void testWrapWithRegexMatchOfLength0() { + assertThat(WordUtils.wrap("abcdef", 2, "\n", false, "(?=d)")).isEqualTo("abc\ndef"); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/diff/ReplacementsFinderTest.java b/sources/src/test/java/org/apache/commons/text/diff/ReplacementsFinderTest.java new file mode 100644 index 0000000..001ee5a --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/diff/ReplacementsFinderTest.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.diff; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.apache.commons.lang3.ArrayUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests for the ReplacementsFinder. + */ +public class ReplacementsFinderTest { + + // Helper ReplacementsHandler implementation for testing + private static class SimpleHandler implements ReplacementsHandler { + private int skipped; + private final List from; + private final List to; + + SimpleHandler() { + skipped = 0; + from = new ArrayList<>(); + to = new ArrayList<>(); + } + + public List getFrom() { + return from; + } + + public int getSkipped() { + return skipped; + } + + public List getTo() { + return to; + } + + @Override + public void handleReplacement(final int skipped, final List from, final List to) { + this.skipped += skipped; + this.from.addAll(from); + this.to.addAll(to); + } + } + + public static Stream parameters() { + return Stream.of(Arguments.of("branco", "blanco", 1, new Character[] {'r'}, new Character[] {'l'}), + Arguments.of("test the blocks before you use it", "try the blocks before you put it", 25, + new Character[] {'e', 's', 't', 's', 'e'}, new Character[] {'r', 'y', 'p', 't'})); + } + + private SimpleHandler handler; + + @BeforeEach + public void setUp() { + handler = new SimpleHandler(); + } + + @ParameterizedTest + @MethodSource("parameters") + public void testReplacementsHandler(final String left, final String right, final int skipped, + final Character[] from, final Character[] to) { + final StringsComparator sc = new StringsComparator(left, right); + final ReplacementsFinder replacementFinder = new ReplacementsFinder<>(handler); + sc.getScript().visit(replacementFinder); + assertThat(handler.getSkipped()).as("Skipped characters do not match").isEqualTo(skipped); + assertArrayEquals(handler.getFrom().toArray(ArrayUtils.EMPTY_CHARACTER_OBJECT_ARRAY), from, + "From characters do not match"); + assertArrayEquals(to, handler.getTo().toArray(ArrayUtils.EMPTY_CHARACTER_OBJECT_ARRAY), + "To characters do not match"); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/diff/StringsComparatorTest.java b/sources/src/test/java/org/apache/commons/text/diff/StringsComparatorTest.java new file mode 100644 index 0000000..9c76d47 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/diff/StringsComparatorTest.java @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.diff; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for the StringsComparator. + */ +public class StringsComparatorTest { + private static class ExecutionVisitor implements CommandVisitor { + + private final StringBuilder v; + + ExecutionVisitor() { + v = new StringBuilder(); + } + + public String getString() { + return v.toString(); + } + + @Override + public void visitDeleteCommand(final T object) { + // noop + } + + @Override + public void visitInsertCommand(final T object) { + v.append(object); + } + @Override + public void visitKeepCommand(final T object) { + v.append(object); + } + } + private List before; + private List after; + private int[] length; + + private int[] lcs; + + @BeforeEach + public void setUp() { + before = Arrays.asList( + "bottle", + "nematode knowledge", + "", + "aa", + "prefixed string", + "ABCABBA", + "glop glop", + "coq", + "spider-man"); + after = Arrays.asList( + "noodle", + "empty bottle", + "", + "C", + "prefix", + "CBABAC", + "pas glop pas glop", + "ane", + "klingon"); + length = new int[] { + 6, + 16, + 0, + 3, + 9, + 5, + 8, + 6, + 13 + }; + lcs = new int[] { + 3, + 7, + 0, + 0, + 6, + 4, + 9, + 0, + 2 + }; + } + + @AfterEach + public void tearDown() { + before = null; + after = null; + length = null; + } + + @Test + public void testExecution() { + for (int i = 0; i < before.size(); ++i) { + final ExecutionVisitor ev = new ExecutionVisitor<>(); + new StringsComparator(before.get(i), after.get(i)).getScript().visit(ev); + assertThat(ev.getString()).isEqualTo(after.get(i)); + } + } + + @Test + public void testLength() { + for (int i = 0; i < before.size(); ++i) { + final StringsComparator comparator = new StringsComparator(before.get(i), after.get(i)); + assertThat(comparator.getScript().getModifications()).isEqualTo(length[i]); + } + } + @Test + public void testLongestCommonSubsequence() { + for (int i = 0; i < before.size(); ++i) { + final StringsComparator comparator = new StringsComparator(before.get(i), after.get(i)); + assertThat(comparator.getScript().getLCSLength()).isEqualTo(lcs[i]); + } + } +} diff --git a/sources/src/test/java/org/apache/commons/text/io/StringSubstitutorFilterReaderTest.java b/sources/src/test/java/org/apache/commons/text/io/StringSubstitutorFilterReaderTest.java new file mode 100644 index 0000000..bdae41d --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/io/StringSubstitutorFilterReaderTest.java @@ -0,0 +1,257 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text.io; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.NullReader; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringSubstitutor; +import org.apache.commons.text.StringSubstitutorTest; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link StringSubstitutorReader}. + */ +public class StringSubstitutorFilterReaderTest extends StringSubstitutorTest { + + private StringSubstitutorReader createReader(final StringSubstitutor substitutor, final String template) { + return new StringSubstitutorReader(new StringReader(template), substitutor); + } + + @Override + protected void doTestNoReplace(final StringSubstitutor substitutor, final String replaceTemplate) + throws IOException { + super.doTestNoReplace(substitutor, replaceTemplate); + doTestNoReplaceInSteps(replaceTemplate, substitutor); + } + + private void doTestNoReplaceInSteps(final String replaceTemplate, final StringSubstitutor substitutor) + throws IOException { + doTestReplaceInCharSteps(substitutor, replaceTemplate, replaceTemplate, false); + final int minTargetSize = 1; + int maxTargetSize = 1024 * 8; + for (int targetSize = minTargetSize; targetSize <= maxTargetSize; targetSize++) { + doTestReplaceInCharArraySteps(substitutor, replaceTemplate, replaceTemplate, false, targetSize); + } + maxTargetSize = 400; + for (int targetSize = minTargetSize; targetSize <= maxTargetSize; targetSize++) { + for (int targetIndex = 0; targetIndex < targetSize; targetIndex++) { + doTestReplaceInCharArrayAtSteps(substitutor, replaceTemplate, replaceTemplate, false, targetIndex, + targetSize); + } + } + } + + @Override + protected void doTestReplace(final StringSubstitutor sub, final String expectedResult, final String replaceTemplate, + final boolean substring) throws IOException { + doTestReplaceInCharSteps(sub, expectedResult, replaceTemplate, substring); + super.doTestReplace(sub, expectedResult, replaceTemplate, substring); + } + + private void doTestReplaceInCharArrayAtSteps(final StringSubstitutor substitutor, final String expectedResult, + final String replaceTemplate, final boolean substring, final int targetIndex, final int targetSize) + throws IOException { + final StringWriter actualResultWriter = new StringWriter(); + final StringWriter expectedResultWriter = new StringWriter(); + final AtomicInteger index = new AtomicInteger(); + final int expectedResultLen = StringUtils.length(expectedResult); + try (Reader expectedResultReader = toReader(expectedResult); + Reader actualReader = new StringSubstitutorReader(toReader(replaceTemplate), substitutor)) { + final char[] actualCh = new char[targetSize]; + final char[] expectedCh = new char[targetSize]; + int actualCount; + while ((actualCount = actualReader.read(actualCh, targetIndex, targetSize - targetIndex)) != -1) { + final int expectedCount = expectedResultReader.read(expectedCh, targetIndex, targetSize - targetIndex); + if (expectedCount != -1) { + expectedResultWriter.write(expectedCh, targetIndex, expectedCount); + } + // stream can chunk in smaller sizes + if (expectedCount == actualCount) { + assertEquals(expectedCount, actualCount, () -> String.format("Step size %,d", targetSize)); + assertArrayEquals(expectedCh, actualCh, + () -> String.format("[%,d] '%s' != '%s', result so far: \"%s\"", index.get(), + String.valueOf(expectedCh), String.valueOf(actualCh), actualResultWriter.toString())); + } else if (actualCount < expectedCount) { + assertTrue(expectedResultWriter.toString().startsWith(actualResultWriter.toString())); + } + if (actualCount != -1) { + actualResultWriter.write(actualCh, targetIndex, actualCount); + } else { + // fails + assertEquals(expectedCount, actualCount, () -> String.format("Step size %,d", targetSize)); + } + index.incrementAndGet(); + assertFalse(index.get() > expectedResultLen, () -> "Index: " + index.get()); + // simpler to debug if we zero out the buffers. + Arrays.fill(actualCh, (char) 0); + Arrays.fill(expectedCh, (char) 0); + } + } + assertEquals(Objects.toString(expectedResult, StringUtils.EMPTY), actualResultWriter.toString()); + } + + private void doTestReplaceInCharArraySteps(final StringSubstitutor substitutor, final String expectedResult, + final String replaceTemplate, final boolean substring, final int targetSize) throws IOException { + final StringWriter actualResultWriter = new StringWriter(); + final StringWriter expectedResultWriter = new StringWriter(); + final AtomicInteger index = new AtomicInteger(); + final int expectedResultLen = StringUtils.length(expectedResult); + try (Reader expectedResultReader = toReader(expectedResult); + Reader actualReader = new StringSubstitutorReader(toReader(replaceTemplate), substitutor)) { + final char[] actualCh = new char[targetSize]; + final char[] expectedCh = new char[targetSize]; + int actualCount; + while ((actualCount = actualReader.read(actualCh)) != -1) { + final int expectedCount = expectedResultReader.read(expectedCh); + if (expectedCount != -1) { + expectedResultWriter.write(expectedCh, 0, expectedCount); + } + // stream can chunk in smaller sizes + if (expectedCount == actualCount) { + assertEquals(expectedCount, actualCount, () -> String.format("Step size %,d", targetSize)); + assertArrayEquals(expectedCh, actualCh, + () -> String.format("[%,d] '%s' != '%s', result so far: \"%s\"", index.get(), + String.valueOf(expectedCh), String.valueOf(actualCh), actualResultWriter.toString())); + } else if (actualCount < expectedCount) { + assertTrue(expectedResultWriter.toString().startsWith(actualResultWriter.toString())); + } + if (actualCount != -1) { + actualResultWriter.write(actualCh, 0, actualCount); + } else { + // fails + assertEquals(expectedCount, actualCount, () -> String.format("Step size %,d", targetSize)); + } + index.incrementAndGet(); + assertFalse(index.get() > expectedResultLen, () -> "Index: " + index.get()); + // simpler to debug if we zero out the buffers. + Arrays.fill(actualCh, (char) 0); + Arrays.fill(expectedCh, (char) 0); + } + } + assertEquals(Objects.toString(expectedResult, StringUtils.EMPTY), actualResultWriter.toString()); + } + + private void doTestReplaceInCharSteps(final StringSubstitutor substitutor, final String expectedResult, + final String replaceTemplate, final boolean substring) throws IOException { + final StringWriter actualResultWriter = new StringWriter(); + final AtomicInteger index = new AtomicInteger(); + final int expectedResultLen = StringUtils.length(expectedResult); + try (Reader expectedResultReader = toReader(expectedResult); + Reader actualReader = new StringSubstitutorReader(toReader(replaceTemplate), substitutor)) { + int actualCh; + while ((actualCh = actualReader.read()) != -1) { + final int expectedCh = expectedResultReader.read(); + final int actualCh2 = actualCh; + assertEquals(expectedCh, actualCh, () -> String.format("[%,d] '%s' != '%s', result so far: \"%s\"", + index.get(), toStringChar(expectedCh), toStringChar(actualCh2), actualResultWriter.toString())); + if (actualCh != -1) { + actualResultWriter.write(actualCh); + } + index.incrementAndGet(); + assertFalse(index.get() > expectedResultLen, () -> "Index: " + index.get()); + } + } + assertEquals(Objects.toString(expectedResult, StringUtils.EMPTY), actualResultWriter.toString()); + } + + private int getMinExpressionLength(final StringSubstitutor substitutor) { + return substitutor.getVariablePrefixMatcher().size() + 1 + substitutor.getVariableSuffixMatcher().size(); + } + + @Override + protected String replace(final StringSubstitutor substitutor, final String source) throws IOException { + if (source == null) { + return null; + } + try (Reader reader = createReader(substitutor, source)) { + return IOUtils.toString(reader); + } + } + + @Test + public void testReadMixedBufferLengths1ToVarLenPlusNoReplace() throws IOException { + final StringSubstitutor substitutor = new StringSubstitutor(values); + final String template = "123456"; + assertTrue(template.length() > getMinExpressionLength(substitutor) + 1); + try (Reader reader = createReader(substitutor, template)) { + assertEquals('1', reader.read()); + final char[] cbuf = new char[template.length() - 1]; + reader.read(cbuf); + final String result = String.valueOf(cbuf); + assertEquals(template.substring(1), result); + } + } + + @Test + public void testReadMixedBufferLengthsReplace() throws IOException { + final String template = "${aa}${bb}"; + final StringSubstitutor substitutor = new StringSubstitutor(values); + try (Reader reader = createReader(substitutor, template)) { + assertEquals('1', reader.read()); + final char[] cbuf = new char[3]; + assertEquals(0, reader.read(cbuf, 0, 0)); + reader.read(cbuf); + final String result = String.valueOf(cbuf); + assertEquals("122", result, () -> String.format("length %,d", result.length())); + } + } + + @Test + public void testReadMixedBufferLengthsVarLenPlusToNoReplace() throws IOException { + final StringSubstitutor substitutor = new StringSubstitutor(values); + final String template = "123456"; + assertTrue(template.length() > getMinExpressionLength(substitutor) + 1); + try (Reader reader = createReader(substitutor, template)) { + final int endIndex = template.length() - 1; + final char[] cbuf = new char[endIndex]; + reader.read(cbuf); + final String result = String.valueOf(cbuf); + assertEquals(template.substring(0, endIndex), result); + assertEquals('6', reader.read()); + } + } + + private Reader toReader(final String expectedResult) { + return expectedResult != null ? new StringReader(expectedResult) : new NullReader(); + } + + private String toStringChar(final int ch) { + switch (ch) { + case -1: + return "EOS"; + case 0: + return "NUL"; + default: + return String.valueOf((char) ch); + } + } +} diff --git a/sources/src/test/java/org/apache/commons/text/jmh/DoubleFormatPerformance.java b/sources/src/test/java/org/apache/commons/text/jmh/DoubleFormatPerformance.java new file mode 100644 index 0000000..4b58491 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/jmh/DoubleFormatPerformance.java @@ -0,0 +1,273 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.jmh; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.util.concurrent.TimeUnit; +import java.util.function.DoubleFunction; + +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.simple.RandomSource; +import org.apache.commons.text.numbers.DoubleFormat; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +/** Benchmarks for the {@link DoubleFormat} class. + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(value = 1, jvmArgs = {"-server", "-Xms512M", "-Xmx512M"}) +public class DoubleFormatPerformance { + + /** Benchmark input providing a source of random double values. */ + @State(Scope.Thread) + public static class DoubleInput { + + /** The number of doubles in the input array. */ + @Param({"10000"}) + private int size; + + /** Minimum base 2 exponent for random input doubles. */ + @Param("-100") + private int minExp; + + /** Maximum base 2 exponent for random input doubles. */ + @Param("100") + private int maxExp; + + /** Double input array. */ + private double[] input; + + /** Get the input doubles. + * @return the input doubles + */ + public double[] getInput() { + return input; + } + + /** Set up the instance for the benchmark. */ + @Setup(Level.Iteration) + public void setup() { + input = randomDoubleArray(size, minExp, maxExp, + RandomSource.create(RandomSource.XO_RO_SHI_RO_128_PP)); + } + } + + /** Decimal format pattern for plain output. */ + private static final String PLAIN_PATTERN = "0.0##"; + + /** Decimal format pattern for plain output with thousands grouping. */ + private static final String PLAIN_GROUPED_PATTERN = "#,##0.0##"; + + /** Decimal format pattern for scientific output. */ + private static final String SCI_PATTERN = "0.0##E0"; + + /** Decimal format pattern for engineering output. */ + private static final String ENG_PATTERN = "##0.0##E0"; + + /** Create a random double value with exponent in the range {@code [minExp, maxExp]}. + * @param minExp minimum exponent; must be less than {@code maxExp} + * @param maxExp maximum exponent; must be greater than {@code minExp} + * @param rng random number generator + * @return random double + */ + private static double randomDouble(final int minExp, final int maxExp, final UniformRandomProvider rng) { + // Create random doubles using random bits in the sign bit and the mantissa. + final long mask = (1L << 52) - 1 | 1L << 63; + final long bits = rng.nextLong() & mask; + // The exponent must be unsigned so + 1023 to the signed exponent + final long exp = rng.nextInt(maxExp - minExp + 1) + minExp + 1023; + return Double.longBitsToDouble(bits | exp << 52); + } + + /** Create an array with the given length containing random doubles with exponents in the range + * {@code [minExp, maxExp]}. + * @param len array length + * @param minExp minimum exponent; must be less than {@code maxExp} + * @param maxExp maximum exponent; must be greater than {@code minExp} + * @param rng random number generator + * @return array of random doubles + */ + private static double[] randomDoubleArray(final int len, final int minExp, final int maxExp, + final UniformRandomProvider rng) { + final double[] arr = new double[len]; + for (int i = 0; i < arr.length; ++i) { + arr[i] = randomDouble(minExp, maxExp, rng); + } + return arr; + } + + /** Run a benchmark test on a function accepting a double argument. + * @param function output type + * @param input double array + * @param bh jmh blackhole for consuming output + * @param fn function to call + */ + private static void runDoubleFunction(final DoubleInput input, final Blackhole bh, + final DoubleFunction fn) { + for (final double d : input.getInput()) { + bh.consume(fn.apply(d)); + } + } + + /** Benchmark testing just the overhead of the benchmark harness. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void baseline(final DoubleInput input, final Blackhole bh) { + runDoubleFunction(input, bh, d -> "0.0"); + } + + /** Benchmark testing the BigDecimal formatting performance. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void bigDecimal(final DoubleInput input, final Blackhole bh) { + final DoubleFunction fn = d -> BigDecimal.valueOf(d) + .setScale(3, RoundingMode.HALF_EVEN) + .stripTrailingZeros() + .toString(); + runDoubleFunction(input, bh, fn); + } + + /** Benchmark testing the {@link DecimalFormat} class with engineering format. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void decimalFormatEngineering(final DoubleInput input, final Blackhole bh) { + final DecimalFormat fmt = new DecimalFormat(ENG_PATTERN); + runDoubleFunction(input, bh, fmt::format); + } + + /** Benchmark testing the {@link DecimalFormat} class. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void decimalFormatPlain(final DoubleInput input, final Blackhole bh) { + final DecimalFormat fmt = new DecimalFormat(PLAIN_PATTERN); + runDoubleFunction(input, bh, fmt::format); + } + + /** Benchmark testing the {@link DecimalFormat} class with thousands grouping. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void decimalFormatPlainGrouped(final DoubleInput input, final Blackhole bh) { + final DecimalFormat fmt = new DecimalFormat(PLAIN_GROUPED_PATTERN); + runDoubleFunction(input, bh, fmt::format); + } + + /** Benchmark testing the {@link DecimalFormat} class with scientific format. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void decimalFormatScientific(final DoubleInput input, final Blackhole bh) { + final DecimalFormat fmt = new DecimalFormat(SCI_PATTERN); + runDoubleFunction(input, bh, fmt::format); + } + + /** Benchmark testing the {@link DoubleFormat#ENGINEERING} format. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void doubleFormatEngineering(final DoubleInput input, final Blackhole bh) { + final DoubleFunction fmt = DoubleFormat.ENGINEERING.builder() + .maxPrecision(6) + .alwaysIncludeExponent(true) + .build(); + runDoubleFunction(input, bh, fmt); + } + + /** Benchmark testing the {@link DoubleFormat#PLAIN} format. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void doubleFormatPlain(final DoubleInput input, final Blackhole bh) { + final DoubleFunction fmt = DoubleFormat.PLAIN.builder() + .minDecimalExponent(-3) + .build(); + runDoubleFunction(input, bh, fmt); + } + + /** Benchmark testing the {@link DoubleFormat#PLAIN} format with + * thousands grouping. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void doubleFormatPlainGrouped(final DoubleInput input, final Blackhole bh) { + final DoubleFunction fmt = DoubleFormat.PLAIN.builder() + .minDecimalExponent(-3) + .groupThousands(true) + .build(); + runDoubleFunction(input, bh, fmt); + } + + /** Benchmark testing the {@link DoubleFormat#SCIENTIFIC} format. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void doubleFormatScientific(final DoubleInput input, final Blackhole bh) { + final DoubleFunction fmt = DoubleFormat.SCIENTIFIC.builder() + .maxPrecision(4) + .alwaysIncludeExponent(true) + .build(); + runDoubleFunction(input, bh, fmt); + } + + /** Benchmark testing the {@link Double#toString()} method. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void doubleToString(final DoubleInput input, final Blackhole bh) { + runDoubleFunction(input, bh, Double::toString); + } + + /** Benchmark testing the {@link String#format(String, Object...)} method. + * @param input benchmark state input + * @param bh jmh blackhole for consuming output + */ + @Benchmark + public void stringFormat(final DoubleInput input, final Blackhole bh) { + runDoubleFunction(input, bh, d -> String.format("%f", d)); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/jmh/LongestCommonSubsequencePerformance.java b/sources/src/test/java/org/apache/commons/text/jmh/LongestCommonSubsequencePerformance.java new file mode 100644 index 0000000..5b2787f --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/jmh/LongestCommonSubsequencePerformance.java @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.jmh; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.text.similarity.LongestCommonSubsequence; +import org.apache.commons.text.similarity.SimilarityScore; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Performance analysis for LongestCommonSubsequence + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(value = 1, jvmArgs = {"-server", "-Xms512M", "-Xmx512M"}) +public class LongestCommonSubsequencePerformance { + /** + * Older implementation of LongestCommonSubsequence. + * Code is copied from Apache Commons Text version 1.10.0-SNAPSHOT + */ + private static class BaselineLongestCommonSubsequence implements SimilarityScore { + @Override + public Integer apply(final CharSequence left, final CharSequence right) { + if (left == null || right == null) { + throw new IllegalArgumentException("Inputs must not be null"); + } + return longestCommonSubsequence(left, right).length(); + } + + public CharSequence longestCommonSubsequence(final CharSequence left, final CharSequence right) { + if (left == null || right == null) { + throw new IllegalArgumentException("Inputs must not be null"); + } + final StringBuilder longestCommonSubstringArray = new StringBuilder(Math.max(left.length(), right.length())); + final int[][] lcsLengthArray = longestCommonSubstringLengthArray(left, right); + int i = left.length() - 1; + int j = right.length() - 1; + int k = lcsLengthArray[left.length()][right.length()] - 1; + while (k >= 0) { + if (left.charAt(i) == right.charAt(j)) { + longestCommonSubstringArray.append(left.charAt(i)); + i = i - 1; + j = j - 1; + k = k - 1; + } else if (lcsLengthArray[i + 1][j] < lcsLengthArray[i][j + 1]) { + i = i - 1; + } else { + j = j - 1; + } + } + return longestCommonSubstringArray.reverse().toString(); + } + + public int[][] longestCommonSubstringLengthArray(final CharSequence left, final CharSequence right) { + final int[][] lcsLengthArray = new int[left.length() + 1][right.length() + 1]; + for (int i = 0; i < left.length(); i++) { + for (int j = 0; j < right.length(); j++) { + if (i == 0) { + lcsLengthArray[i][j] = 0; + } + if (j == 0) { + lcsLengthArray[i][j] = 0; + } + if (left.charAt(i) == right.charAt(j)) { + lcsLengthArray[i + 1][j + 1] = lcsLengthArray[i][j] + 1; + } else { + lcsLengthArray[i + 1][j + 1] = Math.max(lcsLengthArray[i + 1][j], lcsLengthArray[i][j + 1]); + } + } + } + return lcsLengthArray; + } + } + + @State(Scope.Benchmark) + public static class InputData { + final List> inputs = new ArrayList<>(); + + @Setup(Level.Trial) + public void setup() { + final ClassLoader classloader = Thread.currentThread().getContextClassLoader(); + try (InputStream is = classloader.getResourceAsStream("org/apache/commons/text/lcs-perf-analysis-inputs.csv"); + InputStreamReader isr = new InputStreamReader(Objects.requireNonNull(is)); + BufferedReader br = new BufferedReader(isr)) { + String line; + while ((line = br.readLine()) != null && !line.trim().isEmpty()) { + line = line.trim(); + final int indexOfComma = line.indexOf(','); + final String inputA = line.substring(0, indexOfComma); + final String inputB = line.substring(1 + indexOfComma); + this.inputs.add(ImmutablePair.of(inputA, inputB)); + } + } catch (final IOException exception) { + throw new UncheckedIOException(exception.getMessage(), exception); + } + } + } + + @Benchmark + public void testLCS(final InputData data) { + final LongestCommonSubsequence lcs = new LongestCommonSubsequence(); + for (final Pair input : data.inputs) { + lcs.longestCommonSubsequence(input.getLeft(), input.getRight()); + } + } + + @Benchmark + public void testLCSBaseline(final InputData data) { + final BaselineLongestCommonSubsequence lcs = new BaselineLongestCommonSubsequence(); + for (final Pair input : data.inputs) { + lcs.longestCommonSubsequence(input.getLeft(), input.getRight()); + } + } + + @Benchmark + public void testLCSLen(final InputData data) { + final LongestCommonSubsequence lcs = new LongestCommonSubsequence(); + for (final Pair input : data.inputs) { + lcs.apply(input.getLeft(), input.getRight()); + } + } + + @Benchmark + public void testLCSLenBaseline(final InputData data) { + final BaselineLongestCommonSubsequence lcs = new BaselineLongestCommonSubsequence(); + for (final Pair input : data.inputs) { + lcs.apply(input.getLeft(), input.getRight()); + } + } +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/AbstractStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/AbstractStringLookupTest.java new file mode 100644 index 0000000..3f4a435 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/AbstractStringLookupTest.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; + +/** + * Tests forwarding methods provided by {@link AbstractStringLookup}. + */ +public class AbstractStringLookupTest { + + private static class TestStringLookup extends AbstractStringLookup { + + @Override + public String lookup(final String key) { + // noop + return null; + } + + } + + @SuppressWarnings("deprecation") + @Test + public void testForwarding_substringAfter() { + assertEquals(StringUtils.substringAfterLast("abc", 'a'), new TestStringLookup().substringAfterLast("abc", 'a')); + } + + @SuppressWarnings("deprecation") + @Test + public void testForwarding_substringAfterChar() { + assertEquals(StringUtils.substringAfter("abc", 'a'), new TestStringLookup().substringAfter("abc", 'a')); + } + + @SuppressWarnings("deprecation") + @Test + public void testForwarding_substringAfterString() { + assertEquals(StringUtils.substringAfter("abc", "a"), new TestStringLookup().substringAfter("abc", "a")); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/Base64DecoderStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/Base64DecoderStringLookupTest.java new file mode 100644 index 0000000..bf8c40f --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/Base64DecoderStringLookupTest.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link StringLookupFactory#INSTANCE_BASE64_DECODER}. + */ +public class Base64DecoderStringLookupTest { + + @Test + public void test() { + Assertions.assertEquals("HelloWorld!", StringLookupFactory.INSTANCE_BASE64_DECODER.lookup("SGVsbG9Xb3JsZCE=")); + } + + @Test + public void testNull() { + Assertions.assertNull(StringLookupFactory.INSTANCE_BASE64_DECODER.lookup(null)); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(StringLookupFactory.INSTANCE_BASE64_DECODER.toString().isEmpty()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/Base64EncoderStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/Base64EncoderStringLookupTest.java new file mode 100644 index 0000000..b31e9b9 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/Base64EncoderStringLookupTest.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link StringLookupFactory#INSTANCE_BASE64_ENCODER}. + */ +public class Base64EncoderStringLookupTest { + + @Test + public void test() { + Assertions.assertEquals("SGVsbG9Xb3JsZCE=", StringLookupFactory.INSTANCE_BASE64_ENCODER.lookup("HelloWorld!")); + } + + @Test + public void testNull() { + Assertions.assertNull(StringLookupFactory.INSTANCE_BASE64_ENCODER.lookup(null)); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(StringLookupFactory.INSTANCE_BASE64_ENCODER.toString().isEmpty()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/BiFunctionStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/BiFunctionStringLookupTest.java new file mode 100644 index 0000000..85e0741 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/BiFunctionStringLookupTest.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link FunctionStringLookup}. + */ +public class BiFunctionStringLookupTest { + + private static final String SEPARATOR = "."; + + @SuppressWarnings("unchecked") + private final BiFunction, Object> nestedMapBiFunction = (k, m) -> { + final String keyCandidate = StringUtils.substringBefore(k, SEPARATOR); + if (keyCandidate.isEmpty()) { + return m.get(k); + } + final Object value = m.get(keyCandidate); + if (value instanceof Map) { + return this.nestedMapBiFunction.apply(StringUtils.substringAfter(k, SEPARATOR), + (Map) value); + } + return value; + }; + + @Test + public void testBiFunctionForNestedMap() { + // Build map + final String subSubKey = "subsubkeyMap"; + final String subSubValue = "subsubvalue"; + final Map subSubMap = new HashMap<>(); + subSubMap.put(subSubKey, subSubValue); + // + final String subKey = "subkey"; + final String subKeyMap = "subkeyMap"; + final String subValue = "subvalue"; + final Map rootSubMap = new HashMap<>(); + rootSubMap.put(subKey, subValue); + rootSubMap.put(subKeyMap, subSubMap); + // + final String rootKey = "keyMap"; + final String rootKey2 = "key2"; + final String rootValue2 = "value2"; + final Map rootMap = new HashMap<>(); + rootMap.put(rootKey, rootSubMap); + rootMap.put(rootKey2, rootValue2); + // Use map + final BiStringLookup> stringLookup = StringLookupFactory.INSTANCE + .biFunctionStringLookup(nestedMapBiFunction); + Assertions.assertEquals(rootValue2, stringLookup.lookup(rootKey2, rootMap)); + Assertions.assertEquals(subValue, stringLookup.lookup(rootKey + SEPARATOR + subKey, rootMap)); + Assertions.assertEquals(subSubValue, + stringLookup.lookup(rootKey + SEPARATOR + subKeyMap + SEPARATOR + subSubKey, rootMap)); + } + + @Test + public void testConcurrentHashMapNull() { + Assertions.assertNull(BiFunctionStringLookup.on(new ConcurrentHashMap<>()).lookup(null)); + } + + @Test + public void testHashMapNull() { + Assertions.assertNull(BiFunctionStringLookup.on(new HashMap<>()).lookup(null)); + } + + @Test + public void testNullBiFunction() { + Assertions.assertNull(BiFunctionStringLookup.on((BiFunction) null).lookup(null)); + } + + @Test + public void testOne() { + final String key = "key"; + final String value = "value"; + final Map map = new HashMap<>(); + map.put(key, value); + Assertions.assertEquals(value, FunctionStringLookup.on(map).lookup(key)); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(BiFunctionStringLookup.on(new HashMap<>()).toString().isEmpty()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/BiStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/BiStringLookupTest.java new file mode 100644 index 0000000..8d54dd5 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/BiStringLookupTest.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link BiStringLookup}. + */ +public class BiStringLookupTest { + + @Test + public void testDefaultMethod() { + Assertions.assertEquals("a", ((BiStringLookup) key -> key).lookup("a", "b")); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/ConstantStringLookupBasicTest.java b/sources/src/test/java/org/apache/commons/text/lookup/ConstantStringLookupBasicTest.java new file mode 100644 index 0000000..ce40d72 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/ConstantStringLookupBasicTest.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link ConstantStringLookup}. + */ +public class ConstantStringLookupBasicTest { + + /** + * Test fixture. + */ + public static final String NULL_STRING_FIXTURE = null; + + /** + * Test fixture. + */ + public static final String STRING_FIXTURE = "Hello World!"; + + /** + * Clears the test environment. Here the static cache of the constant lookup class is wiped out. + */ + @AfterEach + public void afterEach() { + ConstantStringLookup.clear(); + } + + /** + * Clears the test environment. Here the static cache of the constant lookup class is wiped out. + */ + @BeforeEach + public void beforeEach() { + ConstantStringLookup.clear(); + } + + @Test + public void testNull() { + Assertions.assertNull(ConstantStringLookup.INSTANCE.lookup(null)); + } + + @Test + public void testNullClassFetch() { + Assertions.assertNull(new ConstantStringLookup() { + @Override + protected Class fetchClass(final String className) throws ClassNotFoundException { + return null; + } + }.lookup(ConstantStringLookupBasicTest.class.getName() + ".STRING_FIXTURE")); + } + + @Test + public void testNullValue() { + Assertions.assertEquals(NULL_STRING_FIXTURE, ConstantStringLookup.INSTANCE + .lookup(ConstantStringLookupBasicTest.class.getName() + ".NULL_STRING_FIXTURE")); + } + + @Test + public void testOne() { + Assertions.assertEquals(STRING_FIXTURE, + ConstantStringLookup.INSTANCE.lookup(ConstantStringLookupBasicTest.class.getName() + ".STRING_FIXTURE")); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(ConstantStringLookup.INSTANCE.toString().isEmpty()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/ConstantStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/ConstantStringLookupTest.java new file mode 100644 index 0000000..aa25946 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/ConstantStringLookupTest.java @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.lookup; + +import java.awt.event.KeyEvent; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link ConstantStringLookup}. + *

+ * This class was adapted from Apache Commons Configuration. + *

+ */ +public class ConstantStringLookupTest { + + /** A public field that can be read by the lookup. */ + public static final String FIELD = "Field that can be read"; + + /** A private field that cannot be read by the lookup. */ + @SuppressWarnings("unused") + private static final String PRIVATE_FIELD = "PRIVATE"; + + /** The lookup object to be tested. */ + private ConstantStringLookup stringLookup; + + /** + * Clears the test environment. Here the static cache of the constant lookup class is wiped out. + */ + @AfterEach + public void afterEach() { + ConstantStringLookup.clear(); + } + + /** + * Clears the test environment. Here the static cache of the constant lookup class is wiped out. + */ + @BeforeEach + public void beforeEach() { + stringLookup = ConstantStringLookup.INSTANCE; + } + + /** + * Tests accessing the cache by querying a variable twice. + */ + @Test + public void testLookupCache() { + testLookupConstant(); + testLookupConstant(); + } + + /** + * Tests resolving a valid constant. + */ + @Test + public void testLookupConstant() { + Assertions.assertEquals(FIELD, stringLookup.lookup(variable("FIELD")), "Wrong value of constant"); + } + + /** + * Tries to resolve a variable with an invalid syntax: The name does not contain a dot as a field separator. + */ + @Test + public void testLookupInvalidSyntax() { + Assertions.assertNull(stringLookup.lookup("InvalidVariableName"), + "Non null return value for invalid variable name"); + } + + /** + * Tests resolving a non existing constant. Result should be null. + */ + @Test + public void testLookupNonExisting() { + Assertions.assertNull(stringLookup.lookup(variable("NO_FIELD")), + "Non null return value for non existing constant"); + } + + /** + * Tests resolving a non string constant. Then looks the same variable up from the cache. + */ + @Test + public void testLookupNonString() { + final String ref = KeyEvent.class.getName() + ".VK_ESCAPE"; + final String expected = Integer.toString(KeyEvent.VK_ESCAPE); + Assertions.assertEquals(expected, stringLookup.lookup(ref), "Wrong result of first lookup"); + Assertions.assertEquals(expected, stringLookup.lookup(ref), "Wrong result of 2nd lookup"); + } + + /** + * Tests looking up a null variable. + */ + @Test + public void testLookupNull() { + Assertions.assertNull(stringLookup.lookup(null), "Non null return value for null variable"); + } + + /** + * Tests resolving a private constant. Because a private field cannot be accessed this should again yield null. + */ + @Test + public void testLookupPrivate() { + Assertions.assertNull(stringLookup.lookup(variable("PRIVATE_FIELD")), + "Non null return value for non accessible field"); + } + + /** + * Tests resolving a field from an unknown class. + */ + @Test + public void testLookupUnknownClass() { + Assertions.assertNull(stringLookup.lookup("org.apache.commons.configuration.NonExistingConfig." + FIELD), + "Non null return value for unknown class"); + } + + /** + * Generates the name of a variable for a lookup operation based on the given field name of this class. + * + * @param field the field name + * @return the variable for looking up this field + */ + private String variable(final String field) { + return getClass().getName() + '.' + field; + } +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/DateStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/DateStringLookupTest.java new file mode 100644 index 0000000..6550883 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/DateStringLookupTest.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link DateStringLookup}. + */ +public class DateStringLookupTest { + + @Test + public void testBadFormat() { + assertThrows(IllegalArgumentException.class, + () -> DateStringLookup.INSTANCE.lookup("this-is-a-bad-format-dontcha-know")); + } + + @Test + public void testDefault() throws ParseException { + final String formatted = DateStringLookup.INSTANCE.lookup(null); + DateFormat.getInstance().parse(formatted); // throws ParseException + + } + + @Test + public void testFormat() { + final String format = "yyyy-MM-dd"; + final String value = DateStringLookup.INSTANCE.lookup(format); + // System.out.println(value); + assertNotNull(value, "No Date"); + final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format); + final String today = simpleDateFormat.format(new Date()); + assertEquals(value, today); + + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(DateStringLookup.INSTANCE.toString().isEmpty()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/DefaultStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/DefaultStringLookupTest.java new file mode 100644 index 0000000..24621a6 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/DefaultStringLookupTest.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ +package org.apache.commons.text.lookup; + +import static org.junit.jupiter.api.Assertions.assertSame; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link DefaultStringLookup}. + */ +public class DefaultStringLookupTest { + + @Test + public void testEnumValues() { + final Map stringLookupMap = new HashMap<>(); + StringLookupFactory.INSTANCE.addDefaultStringLookups(stringLookupMap); + // Loop through all enums + for (final DefaultStringLookup stringLookup : DefaultStringLookup.values()) { + assertSame(stringLookupMap.get(stringLookup.getKey()), stringLookupMap.get(stringLookup.getKey())); + } + } + + @Test + public void testIndividualEnums() { + assertSame(DefaultStringLookup.BASE64_DECODER.getStringLookup(), + StringLookupFactory.INSTANCE.base64DecoderStringLookup()); + assertSame(DefaultStringLookup.BASE64_ENCODER.getStringLookup(), + StringLookupFactory.INSTANCE.base64EncoderStringLookup()); + assertSame(DefaultStringLookup.CONST.getStringLookup(), StringLookupFactory.INSTANCE.constantStringLookup()); + assertSame(DefaultStringLookup.DATE.getStringLookup(), StringLookupFactory.INSTANCE.dateStringLookup()); + assertSame(DefaultStringLookup.DNS.getStringLookup(), StringLookupFactory.INSTANCE.dnsStringLookup()); + assertSame(DefaultStringLookup.ENVIRONMENT.getStringLookup(), + StringLookupFactory.INSTANCE.environmentVariableStringLookup()); + assertSame(DefaultStringLookup.FILE.getStringLookup(), StringLookupFactory.INSTANCE.fileStringLookup()); + assertSame(DefaultStringLookup.JAVA.getStringLookup(), StringLookupFactory.INSTANCE.javaPlatformStringLookup()); + assertSame(DefaultStringLookup.LOCAL_HOST.getStringLookup(), + StringLookupFactory.INSTANCE.localHostStringLookup()); + assertSame(DefaultStringLookup.PROPERTIES.getStringLookup(), + StringLookupFactory.INSTANCE.propertiesStringLookup()); + assertSame(DefaultStringLookup.RESOURCE_BUNDLE.getStringLookup(), + StringLookupFactory.INSTANCE.resourceBundleStringLookup()); + assertSame(DefaultStringLookup.SCRIPT.getStringLookup(), StringLookupFactory.INSTANCE.scriptStringLookup()); + assertSame(DefaultStringLookup.SYSTEM_PROPERTIES.getStringLookup(), + StringLookupFactory.INSTANCE.systemPropertyStringLookup()); + assertSame(DefaultStringLookup.URL.getStringLookup(), StringLookupFactory.INSTANCE.urlStringLookup()); + assertSame(DefaultStringLookup.URL_DECODER.getStringLookup(), + StringLookupFactory.INSTANCE.urlDecoderStringLookup()); + assertSame(DefaultStringLookup.URL_ENCODER.getStringLookup(), + StringLookupFactory.INSTANCE.urlEncoderStringLookup()); + assertSame(DefaultStringLookup.XML.getStringLookup(), StringLookupFactory.INSTANCE.xmlStringLookup()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/DnsStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/DnsStringLookupTest.java new file mode 100644 index 0000000..f72e55d --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/DnsStringLookupTest.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link DnsStringLookup}. + */ +public class DnsStringLookupTest { + + @Test + public void testAddressFromHostAddress() throws UnknownHostException { + final InetAddress localHost = InetAddress.getLocalHost(); + Assertions.assertEquals(localHost.getHostAddress(), + DnsStringLookup.INSTANCE.lookup("address|" + localHost.getHostAddress())); + } + + @Test + public void testAddressFromHostName() throws UnknownHostException { + final InetAddress localHost = InetAddress.getLocalHost(); + Assertions.assertEquals(localHost.getHostAddress(), + DnsStringLookup.INSTANCE.lookup("address|" + localHost.getHostName())); + } + + @Test + public void testCanonicalNameFromHostAddress() throws UnknownHostException { + final InetAddress localHost = InetAddress.getLocalHost(); + Assertions.assertEquals(localHost.getCanonicalHostName(), + DnsStringLookup.INSTANCE.lookup("canonical-name|" + localHost.getHostAddress())); + } + + @Test + public void testCanonicalNameFromHostName() throws UnknownHostException { + final InetAddress localHost = InetAddress.getLocalHost(); + Assertions.assertEquals(localHost.getCanonicalHostName(), + DnsStringLookup.INSTANCE.lookup("canonical-name|" + localHost.getHostName())); + } + + @Test + public void testName() throws UnknownHostException { + final String address = InetAddress.getLocalHost().getHostAddress(); + final InetAddress[] localHostAll = InetAddress.getAllByName(address); + boolean matched = false; + for (final InetAddress localHost : localHostAll) { + if (localHost.getHostName().equals(DnsStringLookup.INSTANCE.lookup("name|" + address + ""))) { + matched = true; + } + } + Assertions.assertTrue(matched); + } + + @Test + public void testNull() { + Assertions.assertNull(DnsStringLookup.INSTANCE.lookup(null)); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(DnsStringLookup.INSTANCE.toString().isEmpty()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/EnvironmentVariableStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/EnvironmentVariableStringLookupTest.java new file mode 100644 index 0000000..aa5c51d --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/EnvironmentVariableStringLookupTest.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import org.apache.commons.lang3.SystemUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link StringLookupFactory#INSTANCE_ENVIRONMENT_VARIABLES}. + */ +public class EnvironmentVariableStringLookupTest { + + @Test + public void testNull() { + Assertions.assertNull(StringLookupFactory.INSTANCE_ENVIRONMENT_VARIABLES.lookup(null)); + } + + @Test + public void testOne() { + if (SystemUtils.IS_OS_WINDOWS) { + final String key = "PATH"; + Assertions.assertEquals(System.getenv(key), StringLookupFactory.INSTANCE_ENVIRONMENT_VARIABLES.lookup(key)); + } else if (SystemUtils.IS_OS_LINUX) { + final String key = "USER"; + Assertions.assertEquals(System.getenv(key), StringLookupFactory.INSTANCE_ENVIRONMENT_VARIABLES.lookup(key)); + } + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(StringLookupFactory.INSTANCE_ENVIRONMENT_VARIABLES.toString().isEmpty()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/FileStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/FileStringLookupTest.java new file mode 100644 index 0000000..7c3fc25 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/FileStringLookupTest.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link FileStringLookup}. + */ +public class FileStringLookupTest { + + @Test + public void testBadCharsetName() { + assertThrows(IllegalArgumentException.class, + () -> FileStringLookup.INSTANCE.lookup("BAD_CHARSET_NAME:src/test/resources/org/apache/commons/text/document.properties")); + } + + @Test + public void testBadDocumentPath() { + assertThrows(IllegalArgumentException.class, () -> FileStringLookup.INSTANCE.lookup("BAD_CHARSET_NAME:src/test/resources/DOCUMENT_NOT_FOUND.TXT")); + } + + @Test + public void testMissingFilePart() { + assertThrows(IllegalArgumentException.class, () -> FileStringLookup.INSTANCE.lookup(StandardCharsets.UTF_8.name())); + } + + @Test + public void testNull() { + Assertions.assertNull(FileStringLookup.INSTANCE.lookup(null)); + } + + @Test + public void testOne() throws Exception { + final byte[] expectedBytes = Files.readAllBytes(Paths.get("src/test/resources/org/apache/commons/text/document.properties")); + final String expectedString = new String(expectedBytes, StandardCharsets.UTF_8); + Assertions.assertEquals(expectedString, FileStringLookup.INSTANCE.lookup("UTF-8:src/test/resources/org/apache/commons/text/document.properties")); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(FileStringLookup.INSTANCE.toString().isEmpty()); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/FunctionStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/FunctionStringLookupTest.java new file mode 100644 index 0000000..1a8f75c --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/FunctionStringLookupTest.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link FunctionStringLookup}. + */ +public class FunctionStringLookupTest { + + @Test + public void testConcurrentHashMapNull() { + Assertions.assertNull(FunctionStringLookup.on(new ConcurrentHashMap<>()).lookup(null)); + } + + @Test + public void testHashMapNull() { + Assertions.assertNull(FunctionStringLookup.on(new HashMap<>()).lookup(null)); + } + + @Test + public void testNullFunction() { + Assertions.assertNull(FunctionStringLookup.on((Function) null).lookup(null)); + } + + @Test + public void testOne() { + final String key = "key"; + final String value = "value"; + final Map map = new HashMap<>(); + map.put(key, value); + Assertions.assertEquals(value, FunctionStringLookup.on(map).lookup(key)); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(FunctionStringLookup.on(new HashMap<>()).toString().isEmpty()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/InterpolatorStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/InterpolatorStringLookupTest.java new file mode 100644 index 0000000..6df8a89 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/InterpolatorStringLookupTest.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ +package org.apache.commons.text.lookup; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link InterpolatorStringLookup}. + */ +public class InterpolatorStringLookupTest { + + private static final String TESTKEY = "TestKey"; + + private static final String TESTKEY2 = "TestKey2"; + + private static final String TESTVAL = "TestValue"; + + @AfterAll + public static void afterAll() { + System.clearProperty(TESTKEY); + System.clearProperty(TESTKEY2); + } + + @BeforeAll + public static void beforeAll() { + System.setProperty(TESTKEY, TESTVAL); + System.setProperty(TESTKEY2, TESTVAL); + } + + private void assertLookupNotEmpty(final StringLookup lookup, final String key) { + final String value = lookup.lookup(key); + assertNotNull(value); + assertFalse(value.isEmpty()); + // System.out.println(key + " = " + value); + } + + private void check(final StringLookup lookup) { + String value = lookup.lookup("sys:" + TESTKEY); + assertEquals(TESTVAL, value); + value = lookup.lookup("env:PATH"); + assertNotNull(value); + value = lookup.lookup("date:yyyy-MM-dd"); + assertNotNull(value, "No Date"); + final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); + final String today = format.format(new Date()); + assertEquals(value, today); + assertLookupNotEmpty(lookup, "java:version"); + assertLookupNotEmpty(lookup, "java:runtime"); + assertLookupNotEmpty(lookup, "java:vm"); + assertLookupNotEmpty(lookup, "java:os"); + assertLookupNotEmpty(lookup, "java:locale"); + assertLookupNotEmpty(lookup, "java:hardware"); + } + + @Test + public void testLookup() { + final Map map = new HashMap<>(); + map.put(TESTKEY, TESTVAL); + final StringLookup lookup = new InterpolatorStringLookup(StringLookupFactory.INSTANCE.mapStringLookup(map)); + String value = lookup.lookup(TESTKEY); + assertEquals(TESTVAL, value); + value = lookup.lookup("ctx:" + TESTKEY); + assertEquals(TESTVAL, value); + value = lookup.lookup("sys:" + TESTKEY); + assertEquals(TESTVAL, value); + value = lookup.lookup("BadKey"); + assertNull(value); + value = lookup.lookup("ctx:" + TESTKEY); + assertEquals(TESTVAL, value); + } + + @Test + public void testLookupKeys() { + final InterpolatorStringLookup lookup = new InterpolatorStringLookup((Map) null); + final Map stringLookupMap = lookup.getStringLookupMap(); + StringLookupFactoryTest.assertDefaultKeys(stringLookupMap); + } + + @Test + public void testLookupWithDefaultInterpolator() { + check(new InterpolatorStringLookup()); + } + + @Test + public void testLookupWithNullDefaultInterpolator() { + check(new InterpolatorStringLookup((StringLookup) null)); + } + + @Test + public void testNull() { + Assertions.assertNull(InterpolatorStringLookup.INSTANCE.lookup(null)); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(new InterpolatorStringLookup().toString().isEmpty()); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/JavaPlatformStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/JavaPlatformStringLookupTest.java new file mode 100644 index 0000000..37782af --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/JavaPlatformStringLookupTest.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.commons.lang3.ArrayUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link JavaPlatformStringLookup}. + */ +public class JavaPlatformStringLookupTest { + + @Test + public void testBadKey() { + assertThrows(IllegalArgumentException.class, () -> JavaPlatformStringLookup.INSTANCE.lookup("BADKEY")); + } + + @Test + void testMain() { + JavaPlatformStringLookup.main(ArrayUtils.EMPTY_STRING_ARRAY); + } + + @Test + public void testNull() { + Assertions.assertNull(JavaPlatformStringLookup.INSTANCE.lookup(null)); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(JavaPlatformStringLookup.INSTANCE.toString().isEmpty()); + } + + @Test + public void testVm() { + final String key = "vm"; + assertTrue(JavaPlatformStringLookup.INSTANCE.lookup(key).contains(System.getProperty("java.vm.name"))); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/LocalHostStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/LocalHostStringLookupTest.java new file mode 100644 index 0000000..df65a6c --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/LocalHostStringLookupTest.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link LocalHostStringLookup}. + */ +public class LocalHostStringLookupTest { + + @Test + public void testAddress() throws UnknownHostException { + Assertions.assertEquals(InetAddress.getLocalHost().getHostAddress(), + LocalHostStringLookup.INSTANCE.lookup("address")); + } + + @Test + public void testBadKey() { + Assertions.assertThrows(IllegalArgumentException.class, () -> LocalHostStringLookup.INSTANCE.lookup("FOO")); + } + + @Test + public void testCanonicalName() throws UnknownHostException { + Assertions.assertEquals(InetAddress.getLocalHost().getCanonicalHostName(), + LocalHostStringLookup.INSTANCE.lookup("canonical-name")); + } + + @Test + public void testName() throws UnknownHostException { + Assertions.assertEquals(InetAddress.getLocalHost().getHostName(), + LocalHostStringLookup.INSTANCE.lookup("name")); + } + + @Test + public void testNull() { + Assertions.assertNull(LocalHostStringLookup.INSTANCE.lookup(null)); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(LocalHostStringLookup.INSTANCE.toString().isEmpty()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/NullStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/NullStringLookupTest.java new file mode 100644 index 0000000..917455b --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/NullStringLookupTest.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link StringLookupFactory#INSTANCE_NULL}. + */ +public class NullStringLookupTest { + + @Test + public void test() { + Assertions.assertNull(StringLookupFactory.INSTANCE_NULL.lookup("EverythingIsNull")); + Assertions.assertNull(StringLookupFactory.INSTANCE_NULL.lookup(null)); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(StringLookupFactory.INSTANCE_NULL.toString().isEmpty()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/PropertiesStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/PropertiesStringLookupTest.java new file mode 100644 index 0000000..b154296 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/PropertiesStringLookupTest.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.text.StringSubstitutor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link PropertiesStringLookup}. + */ +public class PropertiesStringLookupTest { + + private static final String DOC_PATH = "src/test/resources/org/apache/commons/text/document.properties"; + private static final String KEY = "mykey"; + private static final String KEY_PATH = PropertiesStringLookup.toPropertyKey(DOC_PATH, KEY); + + @Test + public void testInterpolator() { + final StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator(); + assertEquals("Hello World!", stringSubstitutor.replace("${properties:" + KEY_PATH + "}")); + } + + @Test + public void testInterpolatorNestedColon() { + final StringSubstitutor stringSubstitutor = StringSubstitutor.createInterpolator(); + // Need to handle "C:" in the sys prop user.dir. + final String replaced = stringSubstitutor.replace("$${properties:${sys:user.dir}/" + KEY_PATH + "}"); + assertEquals( + "${properties:" + System.getProperty("user.dir") + "/src/test/resources/org/apache/commons/text/document.properties::mykey}", + replaced); + assertEquals("Hello World!", stringSubstitutor.replace(replaced)); + } + + @Test + public void testInterpolatorWithParameterizedKey() { + final Map map = new HashMap<>(); + map.put("KeyIsHere", KEY); + final StringSubstitutor stringSubstitutor = new StringSubstitutor( + StringLookupFactory.INSTANCE.interpolatorStringLookup(map)); + final String replaced = stringSubstitutor + .replace("$${properties:" + PropertiesStringLookup.toPropertyKey(DOC_PATH, "${KeyIsHere}}")); + assertEquals("${properties:" + PropertiesStringLookup.toPropertyKey(DOC_PATH, "mykey}"), replaced); + assertEquals("Hello World!", stringSubstitutor.replace(replaced)); + } + + @Test + public void testInterpolatorWithParameterizedKey2() { + final Map map = new HashMap<>(); + map.put("KeyIsHere", KEY); + final StringSubstitutor stringSubstitutor = new StringSubstitutor( + StringLookupFactory.INSTANCE.interpolatorStringLookup(map)); + final String replaced = stringSubstitutor.replace( + "$${properties:${sys:user.dir}/" + PropertiesStringLookup.toPropertyKey(DOC_PATH, "${KeyIsHere}}")); + assertEquals("${properties:" + System.getProperty("user.dir") + "/" + + PropertiesStringLookup.toPropertyKey(DOC_PATH, "mykey}"), replaced); + assertEquals("Hello World!", stringSubstitutor.replace(replaced)); + } + + @Test + public void testMissingFile() { + assertThrows(IllegalArgumentException.class, () -> PropertiesStringLookup.INSTANCE.lookup("MissingFile")); + } + + @Test + public void testMissingFileWithKey() { + assertThrows(IllegalArgumentException.class, () -> PropertiesStringLookup.INSTANCE + .lookup(PropertiesStringLookup.toPropertyKey("MissingFile", "AnyKey"))); + } + + @Test + public void testMissingKey() { + assertThrows(IllegalArgumentException.class, () -> PropertiesStringLookup.INSTANCE.lookup(DOC_PATH)); + } + + @Test + public void testNull() { + Assertions.assertNull(PropertiesStringLookup.INSTANCE.lookup(null)); + } + + @Test + public void testOne() { + assertEquals("Hello World!", PropertiesStringLookup.INSTANCE.lookup(KEY_PATH)); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(PropertiesStringLookup.INSTANCE.toString().isEmpty()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/ResourceBundleStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/ResourceBundleStringLookupTest.java new file mode 100644 index 0000000..7fb0f70 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/ResourceBundleStringLookupTest.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.util.ResourceBundle; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link ResourceBundleStringLookup}. + */ +public class ResourceBundleStringLookupTest { + + private static final String KEY = "key"; + private static final String TEST_RESOURCE_BUNDLE = "org.apache.commons.text.example.testResourceBundleLookup"; + + @Test + public void testAny() { + final String bundleName = TEST_RESOURCE_BUNDLE; + final String bundleKey = KEY; + Assertions.assertEquals(ResourceBundle.getBundle(bundleName).getString(bundleKey), + ResourceBundleStringLookup.INSTANCE.lookup(AbstractStringLookup.toLookupKey(bundleName, bundleKey))); + } + + @Test + public void testBadKey() { + final String bundleName = TEST_RESOURCE_BUNDLE; + final String bundleKey = "bad_key"; + assertNull(new ResourceBundleStringLookup(bundleName).lookup(bundleKey)); + assertNull(ResourceBundleStringLookup.INSTANCE.lookup(AbstractStringLookup.toLookupKey(bundleName, bundleKey))); + } + + @Test + public void testBadNames() { + assertNull(ResourceBundleStringLookup.INSTANCE + .lookup(AbstractStringLookup.toLookupKey("BAD_RESOURCE_BUNDLE_NAME", "KEY"))); + } + + @Test + public void testDoubleBundle() { + assertThrows(IllegalArgumentException.class, () -> new ResourceBundleStringLookup(TEST_RESOURCE_BUNDLE) + .lookup(AbstractStringLookup.toLookupKey("OtherBundle", KEY))); + } + + @Test + public void testExceptionGettingString() { + final ResourceBundleStringLookup mockLookup = spy(ResourceBundleStringLookup.class); + when(mockLookup.getString(TEST_RESOURCE_BUNDLE, KEY)).thenThrow(ClassCastException.class); + assertThrows(IllegalArgumentException.class, () -> mockLookup.lookup(AbstractStringLookup.toLookupKey(TEST_RESOURCE_BUNDLE, KEY))); + } + + @Test + public void testMissingKeyInSpec() { + assertThrows(IllegalArgumentException.class, () -> ResourceBundleStringLookup.INSTANCE.lookup(TEST_RESOURCE_BUNDLE + ":")); + } + + @Test + public void testNull() { + Assertions.assertNull(ResourceBundleStringLookup.INSTANCE.lookup(null)); + } + + @Test + public void testOne() { + Assertions.assertEquals(ResourceBundle.getBundle(TEST_RESOURCE_BUNDLE).getString(KEY), + new ResourceBundleStringLookup(TEST_RESOURCE_BUNDLE).lookup(KEY)); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(ResourceBundleStringLookup.INSTANCE.toString().isEmpty()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/ScriptEngineFactoryHelper.java b/sources/src/test/java/org/apache/commons/text/lookup/ScriptEngineFactoryHelper.java new file mode 100644 index 0000000..edd4465 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/ScriptEngineFactoryHelper.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import javax.script.ScriptEngineFactory; +import javax.script.ScriptEngineManager; + +/** Helper to see what ScriptEngineFactory are the classpath. */ +public class ScriptEngineFactoryHelper { + + public static void main(final String[] args) { + final String indent = " "; + for (final ScriptEngineFactory factory : new ScriptEngineManager().getEngineFactories()) { + System.out.println(factory); + System.out.println(indent + factory.getEngineName()); + System.out.println(indent + factory.getEngineVersion()); + System.out.println(indent + factory.getLanguageName()); + System.out.println(indent + factory.getLanguageVersion()); + System.out.println(indent + factory.getClass()); + System.out.println(indent + factory.getExtensions()); + System.out.println(indent + factory.getMimeTypes()); + System.out.println(indent + factory.getNames()); + } + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/ScriptStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/ScriptStringLookupTest.java new file mode 100644 index 0000000..4685346 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/ScriptStringLookupTest.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import javax.script.ScriptEngineManager; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link ScriptStringLookup}. + */ +public class ScriptStringLookupTest { + + private static final String JS_NAME = "JavaScript"; + + @Test + public void testBadEngineName() { + assertThrows(IllegalArgumentException.class, () -> ScriptStringLookup.INSTANCE.lookup("BAD_ENGINE_NAME:\"Hello World!\"")); + } + + @Test + public void testBadScript() { + assertThrows(IllegalArgumentException.class, () -> ScriptStringLookup.INSTANCE.lookup(JS_NAME + ":X")); + } + + @Test + public void testNoScript() { + assertThrows(IllegalArgumentException.class, () -> ScriptStringLookup.INSTANCE.lookup("ENGINE_NAME:")); + } + + @Test + public void testNull() { + Assertions.assertNull(ScriptStringLookup.INSTANCE.lookup(null)); + } + + @Test + public void testOne() { + Assertions.assertEquals("Hello World!", ScriptStringLookup.INSTANCE.lookup(JS_NAME + ":\"Hello World!\"")); + } + + @Test + public void testSanityCheck() { + Assertions.assertNotNull(new ScriptEngineManager().getEngineByName(JS_NAME), JS_NAME); + } + + @Test + public void testScriptMissingColon() { + assertThrows(IllegalArgumentException.class, () -> ScriptStringLookup.INSTANCE.lookup("JavaScript=\"test\"")); + } + + @Test + public void testScriptUsingMultipleColons() { + Assertions.assertEquals("It Works", + ScriptStringLookup.INSTANCE.lookup(JS_NAME + ":true ? \"It Works\" : \"It Does Not Work\" ")); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(ScriptStringLookup.INSTANCE.toString().isEmpty()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/StringLookupFactoryTest.java b/sources/src/test/java/org/apache/commons/text/lookup/StringLookupFactoryTest.java new file mode 100644 index 0000000..682cdbd --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/StringLookupFactoryTest.java @@ -0,0 +1,231 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ +package org.apache.commons.text.lookup; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link StringLookupFactory}. + */ +public class StringLookupFactoryTest { + + public static void assertDefaultKeys(final Map stringLookupMap) { + // included + assertMappedLookups(stringLookupMap, + "base64", + StringLookupFactory.KEY_BASE64_DECODER, + StringLookupFactory.KEY_BASE64_ENCODER, + StringLookupFactory.KEY_CONST, + StringLookupFactory.KEY_DATE, + StringLookupFactory.KEY_ENV, + StringLookupFactory.KEY_FILE, + StringLookupFactory.KEY_JAVA, + StringLookupFactory.KEY_LOCALHOST, + StringLookupFactory.KEY_PROPERTIES, + StringLookupFactory.KEY_RESOURCE_BUNDLE, + StringLookupFactory.KEY_SYS, + StringLookupFactory.KEY_URL_DECODER, + StringLookupFactory.KEY_URL_ENCODER, + StringLookupFactory.KEY_XML); + } + + private static void assertMappedLookups(final Map lookupMap, final String... keys) { + final Set remainingKeys = new HashSet<>(lookupMap.keySet()); + + for (final String key : keys) { + final String normalizedKey = StringLookupFactory.toKey(key); + Assertions.assertNotNull(normalizedKey, () -> "Expected map to contain string lookup for key " + key); + + remainingKeys.remove(normalizedKey); + } + + Assertions.assertTrue(remainingKeys.isEmpty(), () -> "Unexpected keys in lookup map: " + remainingKeys); + } + + private static void checkDefaultStringLookupsHolder(final Properties props, final String... keys) { + final StringLookupFactory.DefaultStringLookupsHolder holder = + new StringLookupFactory.DefaultStringLookupsHolder(props); + + final Map lookupMap = holder.getDefaultStringLookups(); + + assertMappedLookups(lookupMap, keys); + } + + /** + * Main method used to verify the default string lookups resolved during JVM execution. + * @param args + */ + public static void main(final String[] args) { + final Map lookupMap = new HashMap<>(); + StringLookupFactory.INSTANCE.addDefaultStringLookups(lookupMap); + + System.out.println("Default string lookups"); + for (final String key : lookupMap.keySet()) { + System.out.println("- " + key); + } + } + + @Test + public void testAddDefaultStringLookupsMap() { + final Map stringLookupMap = new HashMap<>(); + StringLookupFactory.INSTANCE.addDefaultStringLookups(stringLookupMap); + assertDefaultKeys(stringLookupMap); + } + + @Test + public void testAddDefaultStringLookupsNull() { + StringLookupFactory.INSTANCE.addDefaultStringLookups(null); + } + + @Test + public void testDefaultStringLookupsHolder_allLookups() { + final Properties props = new Properties(); + props.setProperty(StringLookupFactory.DEFAULT_STRING_LOOKUPS_PROPERTY, + "BASE64_DECODER BASE64_ENCODER const, date, dns, environment " + + "file ,java, local_host properties, resource_bundle,script,system_properties " + + "url url_decoder , url_encoder, xml"); + + checkDefaultStringLookupsHolder(props, + "base64", + StringLookupFactory.KEY_BASE64_DECODER, + StringLookupFactory.KEY_BASE64_ENCODER, + StringLookupFactory.KEY_CONST, + StringLookupFactory.KEY_DATE, + StringLookupFactory.KEY_ENV, + StringLookupFactory.KEY_FILE, + StringLookupFactory.KEY_JAVA, + StringLookupFactory.KEY_LOCALHOST, + StringLookupFactory.KEY_PROPERTIES, + StringLookupFactory.KEY_RESOURCE_BUNDLE, + StringLookupFactory.KEY_SYS, + StringLookupFactory.KEY_URL_DECODER, + StringLookupFactory.KEY_URL_ENCODER, + StringLookupFactory.KEY_XML, + + StringLookupFactory.KEY_DNS, + StringLookupFactory.KEY_URL, + StringLookupFactory.KEY_SCRIPT); + } + + @Test + public void testDefaultStringLookupsHolder_givenSingleLookup() { + final Properties props = new Properties(); + props.setProperty(StringLookupFactory.DEFAULT_STRING_LOOKUPS_PROPERTY, "base64_encoder"); + + checkDefaultStringLookupsHolder(props, + "base64", + StringLookupFactory.KEY_BASE64_ENCODER); + } + + @Test + public void testDefaultStringLookupsHolder_givenSingleLookup_weirdString() { + final Properties props = new Properties(); + props.setProperty(StringLookupFactory.DEFAULT_STRING_LOOKUPS_PROPERTY, " \n \t ,, DnS , , "); + + checkDefaultStringLookupsHolder(props, StringLookupFactory.KEY_DNS); + } + + @Test + public void testDefaultStringLookupsHolder_invalidLookupsDefinition() { + final Properties props = new Properties(); + props.setProperty(StringLookupFactory.DEFAULT_STRING_LOOKUPS_PROPERTY, "base64_encoder nope"); + + final Exception exc = Assertions.assertThrows(IllegalArgumentException.class, + () -> new StringLookupFactory.DefaultStringLookupsHolder(props)); + Assertions.assertEquals("Invalid default string lookups definition: base64_encoder nope", exc.getMessage()); + } + + @Test + public void testDefaultStringLookupsHolder_lookupsPropertyEmptyAndBlank() { + final Properties propsWithNull = new Properties(); + propsWithNull.setProperty(StringLookupFactory.DEFAULT_STRING_LOOKUPS_PROPERTY, ""); + + checkDefaultStringLookupsHolder(propsWithNull); + + final Properties propsWithBlank = new Properties(); + propsWithBlank.setProperty(StringLookupFactory.DEFAULT_STRING_LOOKUPS_PROPERTY, " "); + + checkDefaultStringLookupsHolder(propsWithBlank); + } + + @Test + public void testDefaultStringLookupsHolder_lookupsPropertyNotPresent() { + checkDefaultStringLookupsHolder(new Properties(), + "base64", + StringLookupFactory.KEY_BASE64_DECODER, + StringLookupFactory.KEY_BASE64_ENCODER, + StringLookupFactory.KEY_CONST, + StringLookupFactory.KEY_DATE, + StringLookupFactory.KEY_ENV, + StringLookupFactory.KEY_FILE, + StringLookupFactory.KEY_JAVA, + StringLookupFactory.KEY_LOCALHOST, + StringLookupFactory.KEY_PROPERTIES, + StringLookupFactory.KEY_RESOURCE_BUNDLE, + StringLookupFactory.KEY_SYS, + StringLookupFactory.KEY_URL_DECODER, + StringLookupFactory.KEY_URL_ENCODER, + StringLookupFactory.KEY_XML); + } + + @Test + public void testDefaultStringLookupsHolder_multipleLookups() { + final Properties props = new Properties(); + props.setProperty(StringLookupFactory.DEFAULT_STRING_LOOKUPS_PROPERTY, "dns, url script "); + + checkDefaultStringLookupsHolder(props, + StringLookupFactory.KEY_DNS, + StringLookupFactory.KEY_URL, + StringLookupFactory.KEY_SCRIPT); + } + + /** + * Tests that we return the singleton. + */ + @Test + public void testSingletons() { + final StringLookupFactory stringLookupFactory = StringLookupFactory.INSTANCE; + Assertions.assertSame(StringLookupFactory.INSTANCE_BASE64_DECODER, + stringLookupFactory.base64DecoderStringLookup()); + Assertions.assertSame(StringLookupFactory.INSTANCE_BASE64_ENCODER, + stringLookupFactory.base64EncoderStringLookup()); + Assertions.assertSame(ConstantStringLookup.INSTANCE, stringLookupFactory.constantStringLookup()); + Assertions.assertSame(DateStringLookup.INSTANCE, stringLookupFactory.dateStringLookup()); + Assertions.assertSame(DnsStringLookup.INSTANCE, stringLookupFactory.dnsStringLookup()); + Assertions.assertSame(StringLookupFactory.INSTANCE_ENVIRONMENT_VARIABLES, + stringLookupFactory.environmentVariableStringLookup()); + Assertions.assertSame(InterpolatorStringLookup.INSTANCE, stringLookupFactory.interpolatorStringLookup()); + Assertions.assertSame(JavaPlatformStringLookup.INSTANCE, stringLookupFactory.javaPlatformStringLookup()); + Assertions.assertSame(LocalHostStringLookup.INSTANCE, stringLookupFactory.localHostStringLookup()); + Assertions.assertSame(StringLookupFactory.INSTANCE_NULL, stringLookupFactory.nullStringLookup()); + Assertions.assertSame(ResourceBundleStringLookup.INSTANCE, stringLookupFactory.resourceBundleStringLookup()); + Assertions.assertSame(ScriptStringLookup.INSTANCE, stringLookupFactory.scriptStringLookup()); + Assertions.assertSame(StringLookupFactory.INSTANCE_SYSTEM_PROPERTIES, + stringLookupFactory.systemPropertyStringLookup()); + Assertions.assertSame(UrlDecoderStringLookup.INSTANCE, stringLookupFactory.urlDecoderStringLookup()); + Assertions.assertSame(UrlEncoderStringLookup.INSTANCE, stringLookupFactory.urlEncoderStringLookup()); + Assertions.assertSame(UrlStringLookup.INSTANCE, stringLookupFactory.urlStringLookup()); + Assertions.assertSame(XmlStringLookup.INSTANCE, stringLookupFactory.xmlStringLookup()); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/SystemPropertyStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/SystemPropertyStringLookupTest.java new file mode 100644 index 0000000..483c695 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/SystemPropertyStringLookupTest.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link StringLookupFactory#INSTANCE_SYSTEM_PROPERTIES}. + */ +public class SystemPropertyStringLookupTest { + + @Test + public void testNull() { + Assertions.assertNull(StringLookupFactory.INSTANCE_SYSTEM_PROPERTIES.lookup(null)); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(StringLookupFactory.INSTANCE_SYSTEM_PROPERTIES.toString().isEmpty()); + } + + @Test + public void testUserName() { + final String key = "user.name"; + Assertions.assertEquals(System.getProperty(key), StringLookupFactory.INSTANCE_SYSTEM_PROPERTIES.lookup(key)); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/UrlDecoderStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/UrlDecoderStringLookupTest.java new file mode 100644 index 0000000..56488ba --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/UrlDecoderStringLookupTest.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link UrlDecoderStringLookup}. + */ +public class UrlDecoderStringLookupTest { + + private static final String DATA = "Hello World!"; + + @Test + public void testAllPercent() { + Assertions.assertEquals(DATA, UrlDecoderStringLookup.INSTANCE.lookup("Hello%20World%21")); + } + + @Test + public void testExceptionGettingString() throws UnsupportedEncodingException { + final UrlDecoderStringLookup mockLookup = spy(UrlDecoderStringLookup.class); + when(mockLookup.decode(DATA, StandardCharsets.UTF_8.displayName())) + .thenThrow(UnsupportedEncodingException.class); + assertThrows(IllegalArgumentException.class, () -> mockLookup.lookup(DATA)); + } + + @Test + public void testExclamation() { + Assertions.assertEquals(DATA, UrlDecoderStringLookup.INSTANCE.lookup("Hello%20World!")); + } + + @Test + public void testNull() { + Assertions.assertNull(UrlDecoderStringLookup.INSTANCE.lookup(null)); + } + + @Test + public void testPlus() { + Assertions.assertEquals(DATA, UrlDecoderStringLookup.INSTANCE.lookup("Hello+World!")); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(UrlDecoderStringLookup.INSTANCE.toString().isEmpty()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/UrlEncoderStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/UrlEncoderStringLookupTest.java new file mode 100644 index 0000000..8cfe95f --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/UrlEncoderStringLookupTest.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link UrlEncoderStringLookup}. + */ +public class UrlEncoderStringLookupTest { + + private static final String DATA = "Hello+World%21"; + + @Test + public void test() { + Assertions.assertEquals(DATA, UrlEncoderStringLookup.INSTANCE.lookup("Hello World!")); + } + + @Test + public void testExceptionGettingString() throws UnsupportedEncodingException { + final UrlEncoderStringLookup mockLookup = spy(UrlEncoderStringLookup.class); + when(mockLookup.encode(DATA, StandardCharsets.UTF_8.displayName())) + .thenThrow(UnsupportedEncodingException.class); + assertThrows(IllegalArgumentException.class, () -> mockLookup.lookup(DATA)); + } + + @Test + public void testNull() { + Assertions.assertNull(UrlEncoderStringLookup.INSTANCE.lookup(null)); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(UrlEncoderStringLookup.INSTANCE.toString().isEmpty()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/UrlStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/UrlStringLookupTest.java new file mode 100644 index 0000000..93b83fe --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/UrlStringLookupTest.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link UrlStringLookup}. + */ +public class UrlStringLookupTest { + + @Test + public void testBadCharsetName() { + assertThrows(IllegalArgumentException.class, () -> UrlStringLookup.INSTANCE.lookup("BAD_CHARSET_NAME:BAD_URL")); + } + + @Test + public void testBadEncoding() { + assertThrows(IllegalArgumentException.class, () -> UrlStringLookup.INSTANCE.lookup("FOO:https://www.google.com")); + } + + @Test + public void testBadUrl() { + assertThrows(IllegalArgumentException.class, () -> UrlStringLookup.INSTANCE.lookup("UTF-8:BAD_URL")); + } + + @Test + public void testFileScheme() throws Exception { + final Path path = Paths.get("src/test/resources/org/apache/commons/text/document.properties"); + final URI uri = path.toUri(); + // System.out.println(uri); + final byte[] expectedBytes = Files.readAllBytes(path); + final String expectedString = new String(expectedBytes, StandardCharsets.UTF_8); + Assertions.assertEquals(expectedString, UrlStringLookup.INSTANCE.lookup("UTF-8:" + uri.toString())); + } + + @Test + public void testHttpScheme() { + Assertions.assertNotNull(UrlStringLookup.INSTANCE.lookup("UTF-8:https://www.apache.org")); + Assertions.assertNotNull(UrlStringLookup.INSTANCE.lookup("UTF-8:https://www.google.com")); + } + + @Test + public void testMissingUrl() { + assertThrows(IllegalArgumentException.class, () -> UrlStringLookup.INSTANCE.lookup("UTF-8")); + } + + @Test + public void testNull() { + Assertions.assertNull(UrlStringLookup.INSTANCE.lookup(null)); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(UrlStringLookup.INSTANCE.toString().isEmpty()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/lookup/XmlStringLookupTest.java b/sources/src/test/java/org/apache/commons/text/lookup/XmlStringLookupTest.java new file mode 100644 index 0000000..6a43731 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/lookup/XmlStringLookupTest.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ + +package org.apache.commons.text.lookup; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link XmlStringLookup}. + */ +public class XmlStringLookupTest { + + private static final String DOC_PATH = "src/test/resources/org/apache/commons/text/document.xml"; + + @Test + public void testBadXPath() { + assertThrows(IllegalArgumentException.class, () -> XmlStringLookup.INSTANCE.lookup("docName")); + } + + @Test + public void testMissingXPath() { + assertThrows(IllegalArgumentException.class, () -> XmlStringLookup.INSTANCE.lookup(DOC_PATH + ":" + "!JUNK!")); + } + + @Test + public void testNull() { + Assertions.assertNull(XmlStringLookup.INSTANCE.lookup(null)); + } + + @Test + public void testOne() { + final String xpath = "/root/path/to/node"; + Assertions.assertEquals("Hello World!", XmlStringLookup.INSTANCE.lookup(DOC_PATH + ":" + xpath)); + } + + @Test + public void testToString() { + // does not blow up and gives some kind of string. + Assertions.assertFalse(XmlStringLookup.INSTANCE.toString().isEmpty()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/matcher/StringMatcherFactoryTest.java b/sources/src/test/java/org/apache/commons/text/matcher/StringMatcherFactoryTest.java new file mode 100644 index 0000000..057eb5c --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/matcher/StringMatcherFactoryTest.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text.matcher; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link StringMatcherFactory}. + */ +public class StringMatcherFactoryTest { + + private static class StringMatcherDefaults implements StringMatcher { + + @Override + public int isMatch(final char[] buffer, final int start, final int bufferStart, final int bufferEnd) { + return 2; + } + + } + + @Test + public void test_andMatcher() { + assertNotNull(StringMatcherFactory.INSTANCE.andMatcher(StringMatcherFactory.INSTANCE.charMatcher('1'), + StringMatcherFactory.INSTANCE.stringMatcher("2"))); + assertNotNull(StringMatcherFactory.INSTANCE.andMatcher(null, StringMatcherFactory.INSTANCE.stringMatcher("2"))); + assertNotNull(StringMatcherFactory.INSTANCE.andMatcher(null, null)); + StringMatcher andMatcher = StringMatcherFactory.INSTANCE.andMatcher(); + assertNotNull(andMatcher); + assertEquals(0, andMatcher.size()); + andMatcher = StringMatcherFactory.INSTANCE.andMatcher(StringMatcherFactory.INSTANCE.charMatcher('1')); + assertNotNull(andMatcher); + assertEquals(1, andMatcher.size()); + } + + @Test + public void test_charMatcher() { + final StringMatcher charMatcher = StringMatcherFactory.INSTANCE.charMatcher('1'); + assertNotNull(charMatcher); + assertNotNull(charMatcher.toString()); + assertEquals(1, charMatcher.size()); + } + + @Test + public void test_charSetMatcher_char() { + final StringMatcher charSetMatcher = StringMatcherFactory.INSTANCE.charSetMatcher('1'); + assertNotNull(charSetMatcher); + assertNotNull(charSetMatcher.toString()); + assertEquals(1, charSetMatcher.size()); + } + + @Test + public void test_charSetMatcher_String() { + final StringMatcher charSetMatcher = StringMatcherFactory.INSTANCE.charSetMatcher("1"); + assertNotNull(charSetMatcher); + assertNotNull(charSetMatcher.toString()); + assertEquals(1, charSetMatcher.size()); + } + + @Test + public void test_commaMatcher() { + final StringMatcher commaMatcher = StringMatcherFactory.INSTANCE.commaMatcher(); + assertNotNull(commaMatcher); + assertNotNull(commaMatcher.toString()); + assertEquals(1, commaMatcher.size()); + } + + @Test + public void test_doubleQuoteMatcher() { + final StringMatcher doubleQuoteMatcher = StringMatcherFactory.INSTANCE.doubleQuoteMatcher(); + assertNotNull(doubleQuoteMatcher); + assertNotNull(doubleQuoteMatcher.toString()); + assertEquals(1, doubleQuoteMatcher.size()); + } + + @Test + public void test_noneMatcher() { + final StringMatcher noneMatcher = StringMatcherFactory.INSTANCE.noneMatcher(); + assertNotNull(noneMatcher); + assertNotNull(noneMatcher.toString()); + assertEquals(0, noneMatcher.size()); + } + + @Test + public void test_quoteMatcher() { + final StringMatcher quoteMatcher = StringMatcherFactory.INSTANCE.quoteMatcher(); + assertNotNull(quoteMatcher); + assertNotNull(quoteMatcher.toString()); + assertEquals(1, quoteMatcher.size()); + } + + @Test + public void test_singleQuoteMatcher() { + final StringMatcher singleQuoteMatcher = StringMatcherFactory.INSTANCE.singleQuoteMatcher(); + assertNotNull(singleQuoteMatcher); + assertNotNull(singleQuoteMatcher.toString()); + assertEquals(1, singleQuoteMatcher.size()); + } + + @Test + public void test_spaceMatcher() { + final StringMatcher spaceMatcher = StringMatcherFactory.INSTANCE.spaceMatcher(); + assertNotNull(spaceMatcher); + assertNotNull(spaceMatcher.toString()); + assertEquals(1, spaceMatcher.size()); + } + + @Test + public void test_splitMatcher() { + final StringMatcher splitMatcher = StringMatcherFactory.INSTANCE.splitMatcher(); + assertNotNull(splitMatcher); + assertNotNull(splitMatcher.toString()); + assertEquals(1, splitMatcher.size()); + } + + @Test + public void test_stringMatcher() { + StringMatcher stringMatcher = StringMatcherFactory.INSTANCE.stringMatcher("1"); + assertNotNull(stringMatcher); + assertNotNull(stringMatcher.toString()); + assertEquals(1, stringMatcher.size()); + // + stringMatcher = StringMatcherFactory.INSTANCE.stringMatcher(); + assertNotNull(stringMatcher); + assertNotNull(stringMatcher.toString()); + assertEquals(0, stringMatcher.size()); + } + + @Test + public void test_stringMatcherChars() { + StringMatcher stringMatcher = StringMatcherFactory.INSTANCE.stringMatcher('1', '2'); + assertNotNull(stringMatcher); + assertNotNull(stringMatcher.toString()); + assertEquals(2, stringMatcher.size()); + // + stringMatcher = StringMatcherFactory.INSTANCE.stringMatcher('1'); + assertNotNull(stringMatcher); + assertNotNull(stringMatcher.toString()); + assertEquals(1, stringMatcher.size()); + // + stringMatcher = StringMatcherFactory.INSTANCE.stringMatcher(); + assertNotNull(stringMatcher); + assertNotNull(stringMatcher.toString()); + assertEquals(0, stringMatcher.size()); + } + + @Test + public void test_tabMatcher() { + final StringMatcher charMatcher = StringMatcherFactory.INSTANCE.charMatcher('1'); + assertNotNull(charMatcher); + assertNotNull(charMatcher.toString()); + assertEquals(1, charMatcher.size()); + } + + @Test + public void test_trimMatcher() { + final StringMatcher charMatcher = StringMatcherFactory.INSTANCE.charMatcher('1'); + assertNotNull(charMatcher); + assertNotNull(charMatcher.toString()); + assertEquals(1, charMatcher.size()); + } + + @Test + public void testDefaultMethods() { + final StringMatcherDefaults stringMatcher = new StringMatcherDefaults(); + assertEquals(0, stringMatcher.size()); + assertEquals(2, stringMatcher.isMatch("1", 0)); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/matcher/StringMatcherOnCharArrayTest.java b/sources/src/test/java/org/apache/commons/text/matcher/StringMatcherOnCharArrayTest.java new file mode 100644 index 0000000..7014301 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/matcher/StringMatcherOnCharArrayTest.java @@ -0,0 +1,362 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.matcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link StringMatcher}. + */ +public class StringMatcherOnCharArrayTest { + + private static final char[] INPUT1 = "0,1\t2 3\n\r\f\u0000'\"".toCharArray(); + + private static final char[] INPUT2 = "abcdef".toCharArray(); + + private static final int INPUT2_LENGTH = INPUT2.length; + + private void checkAndMatcher_char(final StringMatcher matcher) { + assertThat(matcher.isMatch(INPUT2, 0, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 1, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 2, 0, INPUT2_LENGTH)).isEqualTo(3); + assertThat(matcher.isMatch(INPUT2, 3, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 5, 0, INPUT2_LENGTH)).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT2, 0)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 1)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 2)).isEqualTo(3); + assertThat(matcher.isMatch(INPUT2, 3)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 5)).isEqualTo(0); + } + + @Test + public void testAndMatcher_char() { + final StringMatcher matcher1 = StringMatcherFactory.INSTANCE.andMatcher( + StringMatcherFactory.INSTANCE.charMatcher('c'), StringMatcherFactory.INSTANCE.stringMatcher("de")); + assertEquals(3, matcher1.size()); + checkAndMatcher_char(matcher1); + // + final StringMatcher matcher2 = StringMatcherFactory.INSTANCE.andMatcher(null, + StringMatcherFactory.INSTANCE.charMatcher('c'), null, StringMatcherFactory.INSTANCE.stringMatcher("de"), + null); + assertEquals(3, matcher2.size()); + checkAndMatcher_char(matcher2); + } + + @Test + public void testCharMatcher_char() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.charMatcher('c'); + assertEquals(1, matcher.size()); + // + assertThat(matcher.isMatch(INPUT2, 0, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 1, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 2, 0, INPUT2_LENGTH)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 3, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 5, 0, INPUT2_LENGTH)).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT2, 0)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 1)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 2)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 3)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 5)).isEqualTo(0); + } + + @Test + public void testCharSetMatcher_charArray() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.charSetMatcher("ace".toCharArray()); + assertEquals(1, matcher.size()); + // + assertThat(matcher.isMatch(INPUT2, 0, 0, INPUT2_LENGTH)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 1, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 2, 0, INPUT2_LENGTH)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 3, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4, 0, INPUT2_LENGTH)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 5, 0, INPUT2_LENGTH)).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT2, 0)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 1)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 2)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 3)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 5)).isEqualTo(0); + // + assertThat(StringMatcherFactory.INSTANCE.charSetMatcher()) + .isSameAs(StringMatcherFactory.INSTANCE.noneMatcher()); + assertThat(StringMatcherFactory.INSTANCE.charSetMatcher((char[]) null)) + .isSameAs(StringMatcherFactory.INSTANCE.noneMatcher()); + assertThat(StringMatcherFactory.INSTANCE + .charSetMatcher("a".toCharArray()) instanceof AbstractStringMatcher.CharMatcher).isTrue(); + } + + @Test + public void testCharSetMatcher_String() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.charSetMatcher("ace"); + assertEquals(1, matcher.size()); + // + assertThat(matcher.isMatch(INPUT2, 0, 0, INPUT2_LENGTH)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 1, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 2, 0, INPUT2_LENGTH)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 3, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4, 0, INPUT2_LENGTH)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 5, 0, INPUT2_LENGTH)).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT2, 0)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 1)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 2)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 3)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 5)).isEqualTo(0); + // + assertThat(StringMatcherFactory.INSTANCE.charSetMatcher("")) + .isSameAs(StringMatcherFactory.INSTANCE.noneMatcher()); + assertThat(StringMatcherFactory.INSTANCE.charSetMatcher((String) null)) + .isSameAs(StringMatcherFactory.INSTANCE.noneMatcher()); + assertThat(StringMatcherFactory.INSTANCE.charSetMatcher("a") instanceof AbstractStringMatcher.CharMatcher) + .isTrue(); + } + + @Test + public void testCommaMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.commaMatcher(); + assertEquals(1, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.commaMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 0, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 1, 0, INPUT1.length)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 2, 0, INPUT1.length)).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT1, 0)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 1)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 2)).isEqualTo(0); + } + + @Test + public void testDoubleQuoteMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.doubleQuoteMatcher(); + assertEquals(1, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.doubleQuoteMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 11, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 12, 0, INPUT1.length)).isEqualTo(1); + // + assertThat(matcher.isMatch(INPUT1, 11)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 12)).isEqualTo(1); + } + + @Test + public void testMatcherIndices() { + // remember that the API contract is tight for the isMatch() method + // all the onus is on the caller, so invalid inputs are not + // the concern of StringMatcher, and are not bugs + final StringMatcher matcher = StringMatcherFactory.INSTANCE.stringMatcher("bc"); + assertEquals(2, matcher.size()); + assertThat(matcher.isMatch(INPUT2, 1, 1, INPUT2_LENGTH)).isEqualTo(2); + assertThat(matcher.isMatch(INPUT2, 1, 0, 3)).isEqualTo(2); + assertThat(matcher.isMatch(INPUT2, 1, 0, 2)).isEqualTo(0); + } + + @Test + public void testNoneMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.noneMatcher(); + assertEquals(0, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.noneMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 0, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 1, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 2, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 3, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 4, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 5, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 6, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 7, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 8, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 9, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 10, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 11, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 12, 0, INPUT1.length)).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT1, 0)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 1)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 2)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 3)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 4)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 5)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 6)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 7)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 8)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 9)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 10)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 11)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 12)).isEqualTo(0); + } + + @Test + public void testQuoteMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.quoteMatcher(); + assertEquals(1, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.quoteMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 10, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 11, 0, INPUT1.length)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 12, 0, INPUT1.length)).isEqualTo(1); + // + assertThat(matcher.isMatch(INPUT1, 10)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 11)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 12)).isEqualTo(1); + } + + @Test + public void testSingleQuoteMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.singleQuoteMatcher(); + assertEquals(1, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.singleQuoteMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 10, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 11, 0, INPUT1.length)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 12, 0, INPUT1.length)).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT1, 10)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 11)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 12)).isEqualTo(0); + } + + @Test + public void testSpaceMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.spaceMatcher(); + assertEquals(1, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.spaceMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 4, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 5, 0, INPUT1.length)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 6, 0, INPUT1.length)).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT1, 4)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 5)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 6)).isEqualTo(0); + } + + @Test + public void testSplitMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.splitMatcher(); + assertEquals(1, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.splitMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 2, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 3, 0, INPUT1.length)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 4, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 5, 0, INPUT1.length)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 6, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 7, 0, INPUT1.length)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 8, 0, INPUT1.length)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 9, 0, INPUT1.length)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 10, 0, INPUT1.length)).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT1, 2)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 3)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 4)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 5)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 6)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 7)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 8)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 9)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 10)).isEqualTo(0); + } + + private void testStringMatcher_String(final StringMatcher matcher) { + assertEquals(2, matcher.size()); + // + assertThat(matcher.isMatch(INPUT2, 0, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 1, 0, INPUT2_LENGTH)).isEqualTo(2); + assertThat(matcher.isMatch(INPUT2, 2, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 3, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 5, 0, INPUT2_LENGTH)).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT2, 0)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 1)).isEqualTo(2); + assertThat(matcher.isMatch(INPUT2, 2)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 3)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 5)).isEqualTo(0); + // + assertThat(StringMatcherFactory.INSTANCE.stringMatcher("")) + .isSameAs(StringMatcherFactory.INSTANCE.noneMatcher()); + assertThat(StringMatcherFactory.INSTANCE.stringMatcher((String) null)) + .isSameAs(StringMatcherFactory.INSTANCE.noneMatcher()); + } + + @Test + public void testStringMatcher_String_fromChars() { + testStringMatcher_String(StringMatcherFactory.INSTANCE.stringMatcher('b', 'c')); + testStringMatcher_String(StringMatcherFactory.INSTANCE.stringMatcher(new char[] {'b', 'c'})); + } + + @Test + public void testStringMatcher_String_fromString() { + testStringMatcher_String(StringMatcherFactory.INSTANCE.stringMatcher("bc")); + } + + @Test + public void testTabMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.tabMatcher(); + assertEquals(1, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.tabMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 2, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 3, 0, INPUT1.length)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 4, 0, INPUT1.length)).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT1, 2)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 3)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 4)).isEqualTo(0); + } + + @Test + public void testTrimMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.trimMatcher(); + assertEquals(1, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.trimMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 2, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 3, 0, INPUT1.length)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 4, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 5, 0, INPUT1.length)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 6, 0, INPUT1.length)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 7, 0, INPUT1.length)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 8, 0, INPUT1.length)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 9, 0, INPUT1.length)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 10, 0, INPUT1.length)).isEqualTo(1); + // + assertThat(matcher.isMatch(INPUT1, 2)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 3)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 4)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 5)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 6)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 7)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 8)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 9)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 10)).isEqualTo(1); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/matcher/StringMatcherOnCharSequenceStringTest.java b/sources/src/test/java/org/apache/commons/text/matcher/StringMatcherOnCharSequenceStringTest.java new file mode 100644 index 0000000..1c71fe1 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/matcher/StringMatcherOnCharSequenceStringTest.java @@ -0,0 +1,353 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.matcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link StringMatcher}. + */ +public class StringMatcherOnCharSequenceStringTest { + + private static final String INPUT1 = "0,1\t2 3\n\r\f\u0000'\""; + + private static final String INPUT2 = "abcdef"; + + private static final int INPUT2_LENGTH = INPUT2.length(); + + private void checkAndMatcher_char(final StringMatcher matcher) { + assertThat(matcher.isMatch(INPUT2, 0, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 1, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 2, 0, INPUT2_LENGTH)).isEqualTo(3); + assertThat(matcher.isMatch(INPUT2, 3, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 5, 0, INPUT2_LENGTH)).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT2, 0)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 1)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 2)).isEqualTo(3); + assertThat(matcher.isMatch(INPUT2, 3)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 5)).isEqualTo(0); + } + + @Test + public void testAndMatcher_char() { + final StringMatcher matcher1 = StringMatcherFactory.INSTANCE.andMatcher( + StringMatcherFactory.INSTANCE.charMatcher('c'), StringMatcherFactory.INSTANCE.stringMatcher("de")); + assertEquals(3, matcher1.size()); + checkAndMatcher_char(matcher1); + // + final StringMatcher matcher2 = StringMatcherFactory.INSTANCE.andMatcher(null, + StringMatcherFactory.INSTANCE.charMatcher('c'), null, StringMatcherFactory.INSTANCE.stringMatcher("de"), + null); + assertEquals(3, matcher2.size()); + checkAndMatcher_char(matcher2); + } + + @Test + public void testCharMatcher_char() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.charMatcher('c'); + assertEquals(1, matcher.size()); + // + assertThat(matcher.isMatch(INPUT2, 0, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 1, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 2, 0, INPUT2_LENGTH)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 3, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 5, 0, INPUT2_LENGTH)).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT2, 0)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 1)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 2)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 3)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 5)).isEqualTo(0); + } + + @Test + public void testCharSetMatcher_charArray() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.charSetMatcher("ace".toCharArray()); + assertEquals(1, matcher.size()); + // + assertThat(matcher.isMatch(INPUT2, 0, 0, INPUT2_LENGTH)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 1, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 2, 0, INPUT2_LENGTH)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 3, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4, 0, INPUT2_LENGTH)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 5, 0, INPUT2_LENGTH)).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT2, 0)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 1)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 2)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 3)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 5)).isEqualTo(0); + // + assertThat(StringMatcherFactory.INSTANCE.charSetMatcher()) + .isSameAs(StringMatcherFactory.INSTANCE.noneMatcher()); + assertThat(StringMatcherFactory.INSTANCE.charSetMatcher((char[]) null)) + .isSameAs(StringMatcherFactory.INSTANCE.noneMatcher()); + assertThat(StringMatcherFactory.INSTANCE + .charSetMatcher("a".toCharArray()) instanceof AbstractStringMatcher.CharMatcher).isTrue(); + } + + @Test + public void testCharSetMatcher_String() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.charSetMatcher("ace"); + assertEquals(1, matcher.size()); + // + assertThat(matcher.isMatch(INPUT2, 0, 0, INPUT2_LENGTH)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 1, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 2, 0, INPUT2_LENGTH)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 3, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4, 0, INPUT2_LENGTH)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 5, 0, INPUT2_LENGTH)).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT2, 0)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 1)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 2)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 3)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT2, 5)).isEqualTo(0); + // + assertThat(StringMatcherFactory.INSTANCE.charSetMatcher("")) + .isSameAs(StringMatcherFactory.INSTANCE.noneMatcher()); + assertThat(StringMatcherFactory.INSTANCE.charSetMatcher((String) null)) + .isSameAs(StringMatcherFactory.INSTANCE.noneMatcher()); + assertThat(StringMatcherFactory.INSTANCE.charSetMatcher("a") instanceof AbstractStringMatcher.CharMatcher) + .isTrue(); + } + + @Test + public void testCommaMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.commaMatcher(); + assertEquals(1, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.commaMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 0, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 1, 0, INPUT1.length())).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 2, 0, INPUT1.length())).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT1, 0)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 1)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 2)).isEqualTo(0); + } + + @Test + public void testDoubleQuoteMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.doubleQuoteMatcher(); + assertEquals(1, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.doubleQuoteMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 11, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 12, 0, INPUT1.length())).isEqualTo(1); + // + assertThat(matcher.isMatch(INPUT1, 11)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 12)).isEqualTo(1); + } + + @Test + public void testMatcherIndices() { + // remember that the API contract is tight for the isMatch() method + // all the onus is on the caller, so invalid inputs are not + // the concern of StringMatcher, and are not bugs + final StringMatcher matcher = StringMatcherFactory.INSTANCE.stringMatcher("bc"); + assertEquals(2, matcher.size()); + assertThat(matcher.isMatch(INPUT2, 1, 1, INPUT2_LENGTH)).isEqualTo(2); + assertThat(matcher.isMatch(INPUT2, 1, 0, 3)).isEqualTo(2); + assertThat(matcher.isMatch(INPUT2, 1, 0, 2)).isEqualTo(0); + } + + @Test + public void testNoneMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.noneMatcher(); + assertEquals(0, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.noneMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 0, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 1, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 2, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 3, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 4, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 5, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 6, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 7, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 8, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 9, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 10, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 11, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 12, 0, INPUT1.length())).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT1, 0)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 1)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 2)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 3)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 4)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 5)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 6)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 7)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 8)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 9)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 10)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 11)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 12)).isEqualTo(0); + } + + @Test + public void testQuoteMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.quoteMatcher(); + assertEquals(1, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.quoteMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 10, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 11, 0, INPUT1.length())).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 12, 0, INPUT1.length())).isEqualTo(1); + // + assertThat(matcher.isMatch(INPUT1, 10)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 11)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 12)).isEqualTo(1); + } + + @Test + public void testSingleQuoteMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.singleQuoteMatcher(); + assertEquals(1, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.singleQuoteMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 10, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 11, 0, INPUT1.length())).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 12, 0, INPUT1.length())).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT1, 10)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 11)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 12)).isEqualTo(0); + } + + @Test + public void testSpaceMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.spaceMatcher(); + assertEquals(1, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.spaceMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 4, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 5, 0, INPUT1.length())).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 6, 0, INPUT1.length())).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT1, 4)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 5)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 6)).isEqualTo(0); + } + + @Test + public void testSplitMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.splitMatcher(); + assertEquals(1, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.splitMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 2, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 3, 0, INPUT1.length())).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 4, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 5, 0, INPUT1.length())).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 6, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 7, 0, INPUT1.length())).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 8, 0, INPUT1.length())).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 9, 0, INPUT1.length())).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 10, 0, INPUT1.length())).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT1, 2)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 3)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 4)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 5)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 6)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 7)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 8)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 9)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 10)).isEqualTo(0); + } + + @Test + public void testStringMatcher_String() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.stringMatcher("bc"); + assertEquals(2, matcher.size()); + // + assertThat(matcher.isMatch(INPUT2, 0, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 1, 0, INPUT2_LENGTH)).isEqualTo(2); + assertThat(matcher.isMatch(INPUT2, 2, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 3, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4, 0, INPUT2_LENGTH)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 5, 0, INPUT2_LENGTH)).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT2, 0)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 1)).isEqualTo(2); + assertThat(matcher.isMatch(INPUT2, 2)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 3)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 4)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT2, 5)).isEqualTo(0); + // + assertThat(StringMatcherFactory.INSTANCE.stringMatcher("")) + .isSameAs(StringMatcherFactory.INSTANCE.noneMatcher()); + assertThat(StringMatcherFactory.INSTANCE.stringMatcher((String) null)) + .isSameAs(StringMatcherFactory.INSTANCE.noneMatcher()); + } + + @Test + public void testTabMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.tabMatcher(); + assertEquals(1, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.tabMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 2, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 3, 0, INPUT1.length())).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 4, 0, INPUT1.length())).isEqualTo(0); + // + assertThat(matcher.isMatch(INPUT1, 2)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 3)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 4)).isEqualTo(0); + } + + @Test + public void testTrimMatcher() { + final StringMatcher matcher = StringMatcherFactory.INSTANCE.trimMatcher(); + assertEquals(1, matcher.size()); + assertThat(StringMatcherFactory.INSTANCE.trimMatcher()).isSameAs(matcher); + // + assertThat(matcher.isMatch(INPUT1, 2, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 3, 0, INPUT1.length())).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 4, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 5, 0, INPUT1.length())).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 6, 0, INPUT1.length())).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 7, 0, INPUT1.length())).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 8, 0, INPUT1.length())).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 9, 0, INPUT1.length())).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 10, 0, INPUT1.length())).isEqualTo(1); + // + assertThat(matcher.isMatch(INPUT1, 2)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 3)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 4)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 5)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 6)).isEqualTo(0); + assertThat(matcher.isMatch(INPUT1, 7)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 8)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 9)).isEqualTo(1); + assertThat(matcher.isMatch(INPUT1, 10)).isEqualTo(1); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/matcher/StringSubstitutorGetSetTest.java b/sources/src/test/java/org/apache/commons/text/matcher/StringSubstitutorGetSetTest.java new file mode 100644 index 0000000..b4faae4 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/matcher/StringSubstitutorGetSetTest.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text.matcher; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.commons.text.StringSubstitutor; +import org.junit.jupiter.api.Test; + +/** + * Test class for {@link StringSubstitutor}. + */ +public class StringSubstitutorGetSetTest { + + /** + * Tests get set. + */ + @Test + public void testGetSetPrefix() { + final StringSubstitutor sub = new StringSubstitutor(); + assertTrue(sub.getVariablePrefixMatcher() instanceof AbstractStringMatcher.CharArrayMatcher); + sub.setVariablePrefix('<'); + assertTrue(sub.getVariablePrefixMatcher() instanceof AbstractStringMatcher.CharMatcher); + + sub.setVariablePrefix("<<"); + assertTrue(sub.getVariablePrefixMatcher() instanceof AbstractStringMatcher.CharArrayMatcher); + assertThrows(IllegalArgumentException.class, () -> sub.setVariablePrefix((String) null)); + assertTrue(sub.getVariablePrefixMatcher() instanceof AbstractStringMatcher.CharArrayMatcher); + + final StringMatcher matcher = StringMatcherFactory.INSTANCE.commaMatcher(); + sub.setVariablePrefixMatcher(matcher); + assertSame(matcher, sub.getVariablePrefixMatcher()); + assertThrows(IllegalArgumentException.class, () -> sub.setVariablePrefixMatcher((StringMatcher) null)); + assertSame(matcher, sub.getVariablePrefixMatcher()); + } + + /** + * Tests get set. + */ + @Test + public void testGetSetSuffix() { + final StringSubstitutor sub = new StringSubstitutor(); + assertTrue(sub.getVariableSuffixMatcher() instanceof AbstractStringMatcher.CharMatcher); + sub.setVariableSuffix('<'); + assertTrue(sub.getVariableSuffixMatcher() instanceof AbstractStringMatcher.CharMatcher); + + sub.setVariableSuffix("<<"); + assertTrue(sub.getVariableSuffixMatcher() instanceof AbstractStringMatcher.CharArrayMatcher); + assertThrows(IllegalArgumentException.class, () -> sub.setVariableSuffix((String) null)); + assertTrue(sub.getVariableSuffixMatcher() instanceof AbstractStringMatcher.CharArrayMatcher); + + final StringMatcher matcher = StringMatcherFactory.INSTANCE.commaMatcher(); + sub.setVariableSuffixMatcher(matcher); + assertSame(matcher, sub.getVariableSuffixMatcher()); + assertThrows(IllegalArgumentException.class, () -> sub.setVariableSuffixMatcher((StringMatcher) null)); + assertSame(matcher, sub.getVariableSuffixMatcher()); + } + + /** + * Tests get set. + */ + @Test + public void testGetSetValueDelimiter() { + final StringSubstitutor sub = new StringSubstitutor(); + assertTrue(sub.getValueDelimiterMatcher() instanceof AbstractStringMatcher.CharArrayMatcher); + sub.setValueDelimiter(':'); + assertTrue(sub.getValueDelimiterMatcher() instanceof AbstractStringMatcher.CharMatcher); + + sub.setValueDelimiter("||"); + assertTrue(sub.getValueDelimiterMatcher() instanceof AbstractStringMatcher.CharArrayMatcher); + sub.setValueDelimiter((String) null); + assertNull(sub.getValueDelimiterMatcher()); + + final StringMatcher matcher = StringMatcherFactory.INSTANCE.commaMatcher(); + sub.setValueDelimiterMatcher(matcher); + assertSame(matcher, sub.getValueDelimiterMatcher()); + sub.setValueDelimiterMatcher((StringMatcher) null); + assertNull(sub.getValueDelimiterMatcher()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/numbers/DoubleFormatTest.java b/sources/src/test/java/org/apache/commons/text/numbers/DoubleFormatTest.java new file mode 100644 index 0000000..402bb93 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/numbers/DoubleFormatTest.java @@ -0,0 +1,595 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.numbers; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; +import java.util.Random; +import java.util.function.DoubleFunction; +import java.util.function.Function; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class DoubleFormatTest { + + private static void assertLocalizedFormatsAreEqual(final double d, final DecimalFormat df, + final DoubleFunction fmt, final Locale loc) { + // NOTE: Perform the string comparison only on non-format characters. This is required because + // JDK 16 adds directionality characters to strings for certain locales, such as Arabic, whereas + // previous JDKs do not. We will match the behavior of the previous versions here and ignore formatting + // for test purposes. + final String dfStr = trimFormatChars(df.format(d)); + final String fmtStr = trimFormatChars(fmt.apply(d)); + + Assertions.assertEquals(dfStr, fmtStr, + () -> "Unexpected output for locale [" + loc.toLanguageTag() + "] and double value " + d); + } + + private static void checkDefaultFormatSpecial(final DoubleFunction fmt) { + checkFormat(fmt, 0.0, "0.0"); + checkFormat(fmt, -0.0, "-0.0"); + checkFormat(fmt, Double.NaN, "NaN"); + checkFormat(fmt, Double.POSITIVE_INFINITY, "Infinity"); + checkFormat(fmt, Double.NEGATIVE_INFINITY, "-Infinity"); + } + + private static void checkFormat(final DoubleFunction fmt, final double d, final String str) { + Assertions.assertEquals(str, fmt.apply(d)); + } + + /** Check that the given double value can be exactly recovered from formatted string representation + * produced by the format instance. + * @param fmt format instance + * @param d input double value + */ + private static void checkFormatAccuracy(final DoubleFunction fmt, final double d) { + final String str = fmt.apply(d); + final double parsed = Double.parseDouble(str); + Assertions.assertEquals(d, parsed, () -> "Formatted double string [" + str + "] did not match input value"); + } + + /** Check that the given format type correctly formats doubles when using the + * default configuration options. The format itself is not checked; only the + * fact that the input double can be successfully recovered using {@link Double#parseDouble(String)} + * is asserted. + * @param type format type + */ + private static void checkFormatAccuracyWithDefaults(final DoubleFormat type) { + final DoubleFunction fmt = type.builder().build(); + + checkDefaultFormatSpecial(fmt); + + checkFormatAccuracy(fmt, Double.MIN_VALUE); + checkFormatAccuracy(fmt, -Double.MIN_VALUE); + + checkFormatAccuracy(fmt, Double.MIN_NORMAL); + checkFormatAccuracy(fmt, -Double.MIN_NORMAL); + + checkFormatAccuracy(fmt, Double.MAX_VALUE); + checkFormatAccuracy(fmt, -Double.MAX_VALUE); + + checkFormatAccuracy(fmt, Math.PI); + checkFormatAccuracy(fmt, Math.E); + + final Random rnd = new Random(10L); + final int cnt = 1000; + for (int i = 0; i < cnt; ++i) { + checkFormatAccuracy(fmt, randomDouble(rnd)); + } + } + + private static void checkLocalizedFormat(final Locale loc, final String pattern, + final Function> factory) { + // arrange + final DecimalFormat df = new DecimalFormat(pattern, DecimalFormatSymbols.getInstance(loc)); + final DoubleFunction fmt = factory.apply(loc); + + // act/assert + assertLocalizedFormatsAreEqual(0.0, df, fmt, loc); + assertLocalizedFormatsAreEqual(Double.POSITIVE_INFINITY, df, fmt, loc); + assertLocalizedFormatsAreEqual(Double.NEGATIVE_INFINITY, df, fmt, loc); + assertLocalizedFormatsAreEqual(Double.NaN, df, fmt, loc); + + assertLocalizedFormatsAreEqual(1.0, df, fmt, loc); + assertLocalizedFormatsAreEqual(-1.0, df, fmt, loc); + assertLocalizedFormatsAreEqual(Math.PI, df, fmt, loc); + assertLocalizedFormatsAreEqual(Math.E, df, fmt, loc); + + final Random rnd = new Random(12L); + final int minExp = -100; + final int maxExp = 100; + final int cnt = 1000; + for (int i = 0; i < cnt; ++i) { + assertLocalizedFormatsAreEqual(randomDouble(minExp, maxExp, rnd), df, fmt, loc); + } + } + + private static void checkLocalizedFormats(final String pattern, + final Function> factory) { + for (final Locale loc : Locale.getAvailableLocales()) { + checkLocalizedFormat(loc, pattern, factory); + } + } + + /** Create a random double value with exponent in the range {@code [minExp, maxExp]}. + * @param minExp minimum exponent; must be less than {@code maxExp} + * @param maxExp maximum exponent; must be greater than {@code minExp} + * @param rnd random number generator + * @return random double + */ + private static double randomDouble(final int minExp, final int maxExp, final Random rnd) { + // Create random doubles using random bits in the sign bit and the mantissa. + final long mask = (1L << 52) - 1 | 1L << 63; + final long bits = rnd.nextLong() & mask; + // The exponent must be unsigned so + 1023 to the signed exponent + final long exp = rnd.nextInt(maxExp - minExp + 1) + minExp + 1023; + return Double.longBitsToDouble(bits | exp << 52); + } + + /** Create a random double value using the full range of exponent values. + * @param rnd random number generator + * @return random double + */ + private static double randomDouble(final Random rnd) { + return randomDouble(Double.MIN_EXPONENT, Double.MAX_EXPONENT, rnd); + } + + /** Remove Unicode {@link Character#FORMAT format} characters from the given string. + * @param str input string + * @return input string with format characters removed + */ + private static String trimFormatChars(final String str) { + final StringBuilder sb = new StringBuilder(); + for (final char c : str.toCharArray()) { + if (Character.getType(c) != Character.FORMAT) { + sb.append(c); + } + } + return sb.toString(); + } + + @Test + void testBuilder_illegalArgs() { + // arrange + final DoubleFormat.Builder builder = DoubleFormat.PLAIN.builder(); + + // act/assert + Assertions.assertThrows(NullPointerException.class, () -> builder.digits(null)); + Assertions.assertThrows(IllegalArgumentException.class, () -> builder.digits("a")); + Assertions.assertThrows(IllegalArgumentException.class, () -> builder.digits("0123456789a")); + + Assertions.assertThrows(NullPointerException.class, () -> builder.exponentSeparator(null)); + Assertions.assertThrows(NullPointerException.class, () -> builder.infinity(null)); + Assertions.assertThrows(NullPointerException.class, () -> builder.nan(null)); + Assertions.assertThrows(NullPointerException.class, () -> builder.formatSymbols(null)); + } + + @Test + void testCustomDigitString() { + // arrange + final String digits = "abcdefghij"; + final DoubleFunction plain = DoubleFormat.PLAIN.builder().digits(digits).build(); + final DoubleFunction sci = DoubleFormat.SCIENTIFIC.builder().digits(digits).build(); + final DoubleFunction eng = DoubleFormat.ENGINEERING.builder().digits(digits).build(); + final DoubleFunction mixed = DoubleFormat.MIXED.builder().digits(digits).build(); + + // act/assert + checkFormat(plain, 9876543210.0, "jihgfedcba.a"); + checkFormat(sci, 9876543210.0, "j.ihgfedcbEj"); + checkFormat(eng, 9876543210.0, "j.ihgfedcbEj"); + checkFormat(mixed, 9876543210.0, "j.ihgfedcbEj"); + } + + @Test + void testEngineering_custom() { + // act + final DoubleFunction fmt = DoubleFormat.ENGINEERING.builder() + .maxPrecision(3) + .minDecimalExponent(-3) + .allowSignedZero(false) + .includeFractionPlaceholder(false) + .decimalSeparator(',') + .exponentSeparator("e") + .infinity("inf") + .nan("nan") + .minusSign('!') + .build(); + + // act/assert + checkFormat(fmt, Double.NaN, "nan"); + checkFormat(fmt, Double.POSITIVE_INFINITY, "inf"); + checkFormat(fmt, Double.NEGATIVE_INFINITY, "!inf"); + + checkFormat(fmt, 0.00001, "0"); + checkFormat(fmt, -0.0001, "0"); + checkFormat(fmt, 0.001, "1e!3"); + checkFormat(fmt, -0.01, "!10e!3"); + checkFormat(fmt, 0.1, "100e!3"); + checkFormat(fmt, -0.0, "0"); + checkFormat(fmt, 0.0, "0"); + checkFormat(fmt, -1.0, "!1"); + checkFormat(fmt, 10.0, "10"); + checkFormat(fmt, -100.0, "!100"); + checkFormat(fmt, 1000.0, "1e3"); + checkFormat(fmt, -10000.0, "!10e3"); + checkFormat(fmt, 100000.0, "100e3"); + checkFormat(fmt, -1000000.0, "!1e6"); + checkFormat(fmt, 10000000.0, "10e6"); + checkFormat(fmt, -100000000.0, "!100e6"); + + checkFormat(fmt, 1.25e-3, "1e!3"); + checkFormat(fmt, -9.975e-4, "!1e!3"); + checkFormat(fmt, 12345, "12,3e3"); + checkFormat(fmt, -9_999_999, "!10e6"); + checkFormat(fmt, 1.00001e7, "10e6"); + + checkFormat(fmt, Double.MAX_VALUE, "180e306"); + checkFormat(fmt, Double.MIN_VALUE, "0"); + checkFormat(fmt, Double.MIN_NORMAL, "0"); + checkFormat(fmt, Math.PI, "3,14"); + checkFormat(fmt, Math.E, "2,72"); + } + + @Test + void testEngineering_defaults() { + // act + final DoubleFunction fmt = DoubleFormat.ENGINEERING.builder() + .build(); + + // act/assert + checkDefaultFormatSpecial(fmt); + + checkFormat(fmt, 0.00001, "10.0E-6"); + checkFormat(fmt, -0.0001, "-100.0E-6"); + checkFormat(fmt, 0.001, "1.0E-3"); + checkFormat(fmt, -0.01, "-10.0E-3"); + checkFormat(fmt, 0.1, "100.0E-3"); + checkFormat(fmt, -0.0, "-0.0"); + checkFormat(fmt, 0.0, "0.0"); + checkFormat(fmt, -1.0, "-1.0"); + checkFormat(fmt, 10.0, "10.0"); + checkFormat(fmt, -100.0, "-100.0"); + checkFormat(fmt, 1000.0, "1.0E3"); + checkFormat(fmt, -10000.0, "-10.0E3"); + checkFormat(fmt, 100000.0, "100.0E3"); + checkFormat(fmt, -1000000.0, "-1.0E6"); + checkFormat(fmt, 10000000.0, "10.0E6"); + checkFormat(fmt, -100000000.0, "-100.0E6"); + + checkFormat(fmt, 1.25e-3, "1.25E-3"); + checkFormat(fmt, -9.975e-4, "-997.5E-6"); + checkFormat(fmt, 12345, "12.345E3"); + checkFormat(fmt, -9_999_999, "-9.999999E6"); + checkFormat(fmt, 1.00001e7, "10.0001E6"); + + checkFormat(fmt, Double.MAX_VALUE, "179.76931348623157E306"); + checkFormat(fmt, Double.MIN_VALUE, "4.9E-324"); + checkFormat(fmt, Double.MIN_NORMAL, "22.250738585072014E-309"); + checkFormat(fmt, Math.PI, "3.141592653589793"); + checkFormat(fmt, Math.E, "2.718281828459045"); + } + + @Test + void testEngineering_localeFormatComparison() { + // act/assert + checkLocalizedFormats("##0.0##E0", loc -> DoubleFormat.ENGINEERING.builder() + .maxPrecision(6) + .alwaysIncludeExponent(true) + .formatSymbols(DecimalFormatSymbols.getInstance(loc)) + .build()); + } + + @Test + void testFormatAccuracy() { + // act/assert + checkFormatAccuracyWithDefaults(DoubleFormat.PLAIN); + checkFormatAccuracyWithDefaults(DoubleFormat.MIXED); + checkFormatAccuracyWithDefaults(DoubleFormat.SCIENTIFIC); + checkFormatAccuracyWithDefaults(DoubleFormat.ENGINEERING); + } + + @Test + void testMixed_custom() { + // arrange + final DoubleFunction fmt = DoubleFormat.MIXED.builder() + .maxPrecision(3) + .minDecimalExponent(-3) + .allowSignedZero(false) + .includeFractionPlaceholder(false) + .decimalSeparator(',') + .plainFormatMaxDecimalExponent(4) + .plainFormatMinDecimalExponent(-1) + .exponentSeparator("e") + .infinity("inf") + .nan("nan") + .minusSign('!') + .build(); + + // act/assert + checkFormat(fmt, Double.NaN, "nan"); + checkFormat(fmt, Double.POSITIVE_INFINITY, "inf"); + checkFormat(fmt, Double.NEGATIVE_INFINITY, "!inf"); + + checkFormat(fmt, 0.00001, "0"); + checkFormat(fmt, -0.0001, "0"); + checkFormat(fmt, 0.001, "1e!3"); + checkFormat(fmt, -0.01, "!1e!2"); + checkFormat(fmt, 0.1, "0,1"); + checkFormat(fmt, -0.0, "0"); + checkFormat(fmt, 0.0, "0"); + checkFormat(fmt, -1.0, "!1"); + checkFormat(fmt, 10.0, "10"); + checkFormat(fmt, -100.0, "!100"); + checkFormat(fmt, 1000.0, "1000"); + checkFormat(fmt, -10000.0, "!10000"); + checkFormat(fmt, 100000.0, "1e5"); + checkFormat(fmt, -1000000.0, "!1e6"); + checkFormat(fmt, 10000000.0, "1e7"); + checkFormat(fmt, -100000000.0, "!1e8"); + + checkFormat(fmt, 1.25e-3, "1e!3"); + checkFormat(fmt, -9.975e-4, "!1e!3"); + checkFormat(fmt, 12345, "12300"); + checkFormat(fmt, -9_999_999, "!1e7"); + checkFormat(fmt, 1.00001e7, "1e7"); + + checkFormat(fmt, Double.MAX_VALUE, "1,8e308"); + checkFormat(fmt, Double.MIN_VALUE, "0"); + checkFormat(fmt, Double.MIN_NORMAL, "0"); + checkFormat(fmt, Math.PI, "3,14"); + checkFormat(fmt, Math.E, "2,72"); + } + + @Test + void testMixed_defaults() { + // arrange + final DoubleFunction fmt = DoubleFormat.MIXED.builder().build(); + + // act/assert + checkDefaultFormatSpecial(fmt); + + checkFormat(fmt, 0.00001, "1.0E-5"); + checkFormat(fmt, -0.0001, "-1.0E-4"); + checkFormat(fmt, 0.001, "0.001"); + checkFormat(fmt, -0.01, "-0.01"); + checkFormat(fmt, 0.1, "0.1"); + checkFormat(fmt, -0.0, "-0.0"); + checkFormat(fmt, 0.0, "0.0"); + checkFormat(fmt, -1.0, "-1.0"); + checkFormat(fmt, 10.0, "10.0"); + checkFormat(fmt, -100.0, "-100.0"); + checkFormat(fmt, 1000.0, "1000.0"); + checkFormat(fmt, -10000.0, "-10000.0"); + checkFormat(fmt, 100000.0, "100000.0"); + checkFormat(fmt, -1000000.0, "-1000000.0"); + checkFormat(fmt, 10000000.0, "1.0E7"); + checkFormat(fmt, -100000000.0, "-1.0E8"); + + checkFormat(fmt, 1.25e-3, "0.00125"); + checkFormat(fmt, -9.975e-4, "-9.975E-4"); + checkFormat(fmt, 12345, "12345.0"); + checkFormat(fmt, -9_999_999, "-9999999.0"); + checkFormat(fmt, 1.00001e7, "1.00001E7"); + + checkFormat(fmt, Double.MAX_VALUE, "1.7976931348623157E308"); + checkFormat(fmt, Double.MIN_VALUE, "4.9E-324"); + checkFormat(fmt, Double.MIN_NORMAL, "2.2250738585072014E-308"); + checkFormat(fmt, Math.PI, "3.141592653589793"); + checkFormat(fmt, Math.E, "2.718281828459045"); + } + + @Test + void testPlain_custom() { + // arrange + final DoubleFunction fmt = DoubleFormat.PLAIN.builder() + .maxPrecision(3) + .minDecimalExponent(-3) + .allowSignedZero(false) + .includeFractionPlaceholder(false) + .decimalSeparator(',') + .exponentSeparator("e") + .infinity("inf") + .nan("nan") + .minusSign('!') + .build(); + + // act/assert + checkFormat(fmt, Double.NaN, "nan"); + checkFormat(fmt, Double.POSITIVE_INFINITY, "inf"); + checkFormat(fmt, Double.NEGATIVE_INFINITY, "!inf"); + + checkFormat(fmt, 0.00001, "0"); + checkFormat(fmt, -0.0001, "0"); + checkFormat(fmt, 0.001, "0,001"); + checkFormat(fmt, -0.01, "!0,01"); + checkFormat(fmt, 0.1, "0,1"); + checkFormat(fmt, -0.0, "0"); + checkFormat(fmt, 0.0, "0"); + checkFormat(fmt, -1.0, "!1"); + checkFormat(fmt, 10.0, "10"); + checkFormat(fmt, -100.0, "!100"); + checkFormat(fmt, 1000.0, "1000"); + checkFormat(fmt, -10000.0, "!10000"); + checkFormat(fmt, 100000.0, "100000"); + checkFormat(fmt, -1000000.0, "!1000000"); + checkFormat(fmt, 10000000.0, "10000000"); + checkFormat(fmt, -100000000.0, "!100000000"); + + checkFormat(fmt, 1.25e-3, "0,001"); + checkFormat(fmt, -9.975e-4, "!0,001"); + checkFormat(fmt, 12345, "12300"); + checkFormat(fmt, -9_999_999, "!10000000"); + checkFormat(fmt, 1.00001e7, "10000000"); + + checkFormat(fmt, Float.MAX_VALUE, "340000000000000000000000000000000000000"); + checkFormat(fmt, -Float.MIN_VALUE, "0"); + checkFormat(fmt, Float.MIN_NORMAL, "0"); + checkFormat(fmt, Math.PI, "3,14"); + checkFormat(fmt, Math.E, "2,72"); + } + + @Test + void testPlain_defaults() { + // arrange + final DoubleFunction fmt = DoubleFormat.PLAIN.builder() + .build(); + + // act/assert + checkFormat(fmt, 0.00001, "0.00001"); + checkFormat(fmt, -0.0001, "-0.0001"); + checkFormat(fmt, 0.001, "0.001"); + checkFormat(fmt, -0.01, "-0.01"); + checkFormat(fmt, 0.1, "0.1"); + checkFormat(fmt, -0.0, "-0.0"); + checkFormat(fmt, 0.0, "0.0"); + checkFormat(fmt, -1.0, "-1.0"); + checkFormat(fmt, 10.0, "10.0"); + checkFormat(fmt, -100.0, "-100.0"); + checkFormat(fmt, 1000.0, "1000.0"); + checkFormat(fmt, -10000.0, "-10000.0"); + checkFormat(fmt, 100000.0, "100000.0"); + checkFormat(fmt, -1000000.0, "-1000000.0"); + checkFormat(fmt, 10000000.0, "10000000.0"); + checkFormat(fmt, -100000000.0, "-100000000.0"); + + checkFormat(fmt, 1.25e-3, "0.00125"); + checkFormat(fmt, -9.975e-4, "-0.0009975"); + checkFormat(fmt, 12345, "12345.0"); + checkFormat(fmt, -9_999_999, "-9999999.0"); + checkFormat(fmt, 1.00001e7, "10000100.0"); + + checkFormat(fmt, Float.MAX_VALUE, "340282346638528860000000000000000000000.0"); + checkFormat(fmt, -Float.MIN_VALUE, "-0.000000000000000000000000000000000000000000001401298464324817"); + checkFormat(fmt, Float.MIN_NORMAL, "0.000000000000000000000000000000000000011754943508222875"); + checkFormat(fmt, Math.PI, "3.141592653589793"); + checkFormat(fmt, Math.E, "2.718281828459045"); + } + + @Test + void testPlain_localeFormatComparison() { + // act/assert + checkLocalizedFormats("0.0##", loc -> DoubleFormat.PLAIN.builder() + .minDecimalExponent(-3) + .formatSymbols(DecimalFormatSymbols.getInstance(loc)) + .build()); + checkLocalizedFormats("#,##0.0##", loc -> DoubleFormat.PLAIN.builder() + .minDecimalExponent(-3) + .groupThousands(true) + .formatSymbols(DecimalFormatSymbols.getInstance(loc)) + .build()); + } + + @Test + void testScientific_custom() { + // arrange + final DoubleFunction fmt = DoubleFormat.SCIENTIFIC.builder() + .maxPrecision(3) + .minDecimalExponent(-3) + .allowSignedZero(false) + .includeFractionPlaceholder(false) + .decimalSeparator(',') + .exponentSeparator("e") + .infinity("inf") + .nan("nan") + .minusSign('!') + .build(); + + // act/assert + checkFormat(fmt, Double.NaN, "nan"); + checkFormat(fmt, Double.POSITIVE_INFINITY, "inf"); + checkFormat(fmt, Double.NEGATIVE_INFINITY, "!inf"); + + checkFormat(fmt, 0.00001, "0"); + checkFormat(fmt, -0.0001, "0"); + checkFormat(fmt, 0.001, "1e!3"); + checkFormat(fmt, -0.01, "!1e!2"); + checkFormat(fmt, 0.1, "1e!1"); + checkFormat(fmt, -0.0, "0"); + checkFormat(fmt, 0.0, "0"); + checkFormat(fmt, -1.0, "!1"); + checkFormat(fmt, 10.0, "1e1"); + checkFormat(fmt, -100.0, "!1e2"); + checkFormat(fmt, 1000.0, "1e3"); + checkFormat(fmt, -10000.0, "!1e4"); + checkFormat(fmt, 100000.0, "1e5"); + checkFormat(fmt, -1000000.0, "!1e6"); + checkFormat(fmt, 10000000.0, "1e7"); + checkFormat(fmt, -100000000.0, "!1e8"); + + checkFormat(fmt, 1.25e-3, "1e!3"); + checkFormat(fmt, -9.975e-4, "!1e!3"); + checkFormat(fmt, 12345, "1,23e4"); + checkFormat(fmt, -9_999_999, "!1e7"); + checkFormat(fmt, 1.00001e7, "1e7"); + + checkFormat(fmt, Double.MAX_VALUE, "1,8e308"); + checkFormat(fmt, Double.MIN_VALUE, "0"); + checkFormat(fmt, Double.MIN_NORMAL, "0"); + checkFormat(fmt, Math.PI, "3,14"); + checkFormat(fmt, Math.E, "2,72"); + } + + @Test + void testScientific_defaults() { + // arrange + final DoubleFunction fmt = DoubleFormat.SCIENTIFIC.builder().build(); + + // act/assert + checkDefaultFormatSpecial(fmt); + + checkFormat(fmt, 0.00001, "1.0E-5"); + checkFormat(fmt, -0.0001, "-1.0E-4"); + checkFormat(fmt, 0.001, "1.0E-3"); + checkFormat(fmt, -0.01, "-1.0E-2"); + checkFormat(fmt, 0.1, "1.0E-1"); + checkFormat(fmt, -0.0, "-0.0"); + checkFormat(fmt, 0.0, "0.0"); + checkFormat(fmt, -1.0, "-1.0"); + checkFormat(fmt, 10.0, "1.0E1"); + checkFormat(fmt, -100.0, "-1.0E2"); + checkFormat(fmt, 1000.0, "1.0E3"); + checkFormat(fmt, -10000.0, "-1.0E4"); + checkFormat(fmt, 100000.0, "1.0E5"); + checkFormat(fmt, -1000000.0, "-1.0E6"); + checkFormat(fmt, 10000000.0, "1.0E7"); + checkFormat(fmt, -100000000.0, "-1.0E8"); + + checkFormat(fmt, 1.25e-3, "1.25E-3"); + checkFormat(fmt, -9.975e-4, "-9.975E-4"); + checkFormat(fmt, 12345, "1.2345E4"); + checkFormat(fmt, -9_999_999, "-9.999999E6"); + checkFormat(fmt, 1.00001e7, "1.00001E7"); + + checkFormat(fmt, Double.MAX_VALUE, "1.7976931348623157E308"); + checkFormat(fmt, Double.MIN_VALUE, "4.9E-324"); + checkFormat(fmt, Double.MIN_NORMAL, "2.2250738585072014E-308"); + checkFormat(fmt, Math.PI, "3.141592653589793"); + checkFormat(fmt, Math.E, "2.718281828459045"); + } + + @Test + void testScientific_localeFormatComparison() { + // act/assert + checkLocalizedFormats("0.0##E0", loc -> DoubleFormat.SCIENTIFIC.builder() + .maxPrecision(4) + .alwaysIncludeExponent(true) + .formatSymbols(DecimalFormatSymbols.getInstance(loc)) + .build()); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/numbers/ParsedDecimalTest.java b/sources/src/test/java/org/apache/commons/text/numbers/ParsedDecimalTest.java new file mode 100644 index 0000000..76c2e16 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/numbers/ParsedDecimalTest.java @@ -0,0 +1,732 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.numbers; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.util.function.BiFunction; + +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.simple.RandomSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +class ParsedDecimalTest { + + private static final class FormatOptionsImpl implements ParsedDecimal.FormatOptions { + + private boolean includeFractionPlaceholder = true; + + private boolean signedZero = true; + + private char[] digits = "0123456789".toCharArray(); + + private char decimalSeparator = '.'; + + private char thousandsGroupingSeparator = ','; + + private boolean groupThousands = false; + + private char minusSign = '-'; + + private String exponentSeparator = "E"; + + private boolean alwaysIncludeExponent = false; + + @Override + public char getDecimalSeparator() { + return decimalSeparator; + } + + @Override + public char[] getDigits() { + return digits; + } + + @Override + public char[] getExponentSeparatorChars() { + return exponentSeparator.toCharArray(); + } + + @Override + public char getGroupingSeparator() { + return thousandsGroupingSeparator; + } + + @Override + public char getMinusSign() { + return minusSign; + } + + @Override + public boolean isAlwaysIncludeExponent() { + return alwaysIncludeExponent; + } + + @Override + public boolean isGroupThousands() { + return groupThousands; + } + + @Override + public boolean isIncludeFractionPlaceholder() { + return includeFractionPlaceholder; + } + + @Override + public boolean isSignedZero() { + return signedZero; + } + + public void setAlwaysIncludeExponent(final boolean alwaysIncludeExponent) { + this.alwaysIncludeExponent = alwaysIncludeExponent; + } + + public void setDecimalSeparator(final char decimalSeparator) { + this.decimalSeparator = decimalSeparator; + } + + public void setDigitsFromString(final String digits) { + this.digits = digits.toCharArray(); + } + + public void setExponentSeparator(final String exponentSeparator) { + this.exponentSeparator = exponentSeparator; + } + + public void setGroupThousands(final boolean groupThousands) { + this.groupThousands = groupThousands; + } + + public void setIncludeFractionPlaceholder(final boolean includeFractionPlaceholder) { + this.includeFractionPlaceholder = includeFractionPlaceholder; + } + + public void setMinusSign(final char minusSign) { + this.minusSign = minusSign; + } + + public void setSignedZero(final boolean signedZero) { + this.signedZero = signedZero; + } + + public void setThousandsGroupingSeparator(final char thousandsGroupingSeparator) { + this.thousandsGroupingSeparator = thousandsGroupingSeparator; + } + } + + private static void assertMaxPrecision(final double d, final int maxPrecision, + final boolean negative, final String digits, final int exponent) { + final ParsedDecimal dec = ParsedDecimal.from(d); + dec.maxPrecision(maxPrecision); + + assertSimpleDecimal(dec, negative, digits, exponent); + } + + private static void assertRound(final double d, final int roundExponent, + final boolean negative, final String digits, final int exponent) { + final ParsedDecimal dec = ParsedDecimal.from(d); + dec.round(roundExponent); + + assertSimpleDecimal(dec, negative, digits, exponent); + } + + private static void assertSimpleDecimal(final ParsedDecimal parsed, final boolean negative, final String digits, + final int exponent) { + Assertions.assertEquals(negative, parsed.negative); + Assertions.assertEquals(digits, digitString(parsed)); + Assertions.assertEquals(exponent, parsed.getExponent()); + Assertions.assertEquals(digits.length(), parsed.digitCount); + Assertions.assertEquals(exponent, parsed.getScientificExponent() - digits.length() + 1); + } + + private static void assertThrowsWithMessage(final Executable fn, final Class type, + final String msg) { + final Throwable exc = Assertions.assertThrows(type, fn); + Assertions.assertEquals(msg, exc.getMessage()); + } + + private static void checkFrom(final double d, final String digits, final int exponent) { + final boolean negative = Math.signum(d) < 0; + + assertSimpleDecimal(ParsedDecimal.from(d), negative, digits, exponent); + assertSimpleDecimal(ParsedDecimal.from(-d), !negative, digits, exponent); + } + + private static void checkToEngineeringString(final double d, final String expected, + final ParsedDecimal.FormatOptions opts) { + checkToStringMethod(d, expected, ParsedDecimal::toEngineeringString, opts); + + // check the exponent value to make sure it is a multiple of 3 + final String pos = ParsedDecimal.from(d).toEngineeringString(opts); + Assertions.assertEquals(0, parseExponent(pos, opts) % 3); + + final String neg = ParsedDecimal.from(-d).toEngineeringString(opts); + Assertions.assertEquals(0, parseExponent(neg, opts) % 3); + } + + private static void checkToPlainString(final double d, final String expected, + final ParsedDecimal.FormatOptions opts) { + checkToStringMethod(d, expected, ParsedDecimal::toPlainString, opts); + } + + private static void checkToScientificString(final double d, final String expected, + final ParsedDecimal.FormatOptions opts) { + checkToStringMethod(d, expected, ParsedDecimal::toScientificString, opts); + } + + private static void checkToStringMethod(final double d, final String expected, + final BiFunction fn, + final ParsedDecimal.FormatOptions opts) { + + final ParsedDecimal pos = ParsedDecimal.from(d); + final String actual = fn.apply(pos, opts); + + Assertions.assertEquals(expected, actual); + } + + private static double createRandomDouble(final UniformRandomProvider rng) { + final long mask = (1L << 52) - 1 | 1L << 63; + final long bits = rng.nextLong() & mask; + final long exp = rng.nextInt(2045) + 1; + return Double.longBitsToDouble(bits | exp << 52); + } + + /** Get the raw digits in the given decimal as a string. + * @param dec decimal instancE + * @return decimal digits as a string + */ + private static String digitString(final ParsedDecimal dec) { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < dec.digitCount; ++i) { + sb.append(dec.digits[i]); + } + return sb.toString(); + } + + private static int parseExponent(final String str, final ParsedDecimal.FormatOptions opts) { + final char[] expSep = opts.getExponentSeparatorChars(); + + final int expStartIdx = str.indexOf(String.valueOf(expSep)); + if (expStartIdx > -1) { + int expIdx = expStartIdx + expSep.length; + + boolean neg = false; + if (str.charAt(expIdx) == opts.getMinusSign()) { + ++expIdx; + neg = true; + } + + final String expStr = str.substring(expIdx); + final int val = Integer.parseInt(expStr); + return neg + ? -val + : val; + } + + return 0; + } + + @Test + void testFrom() { + // act/assert + checkFrom(0.0, "0", 0); + + checkFrom(1.0, "1", 0); + checkFrom(10.0, "1", 1); + checkFrom(100.0, "1", 2); + checkFrom(1000.0, "1", 3); + checkFrom(10000.0, "1", 4); + + checkFrom(0.1, "1", -1); + checkFrom(0.01, "1", -2); + checkFrom(0.001, "1", -3); + checkFrom(0.0001, "1", -4); + checkFrom(0.00001, "1", -5); + + checkFrom(1.2, "12", -1); + checkFrom(0.00971, "971", -5); + checkFrom(56300, "563", 2); + + checkFrom(123.0, "123", 0); + checkFrom(1230.0, "123", 1); + checkFrom(12300.0, "123", 2); + checkFrom(123000.0, "123", 3); + + checkFrom(12.3, "123", -1); + checkFrom(1.23, "123", -2); + checkFrom(0.123, "123", -3); + checkFrom(0.0123, "123", -4); + + checkFrom(1.987654321e270, "1987654321", 261); + checkFrom(1.987654321e-270, "1987654321", -279); + + checkFrom(Math.PI, "3141592653589793", -15); + checkFrom(Math.E, "2718281828459045", -15); + + checkFrom(Double.MAX_VALUE, "17976931348623157", 292); + checkFrom(Double.MIN_VALUE, "49", -325); + checkFrom(Double.MIN_NORMAL, "22250738585072014", -324); + } + + @Test + void testFrom_notFinite() { + // arrange + final String msg = "Double is not finite"; + + // act/assert + assertThrowsWithMessage(() -> ParsedDecimal.from(Double.NaN), + IllegalArgumentException.class, msg); + assertThrowsWithMessage(() -> ParsedDecimal.from(Double.NEGATIVE_INFINITY), + IllegalArgumentException.class, msg); + assertThrowsWithMessage(() -> ParsedDecimal.from(Double.POSITIVE_INFINITY), + IllegalArgumentException.class, msg); + } + + @Test + void testIsZero() { + // act/assert + Assertions.assertTrue(ParsedDecimal.from(0.0).isZero()); + Assertions.assertTrue(ParsedDecimal.from(-0.0).isZero()); + + Assertions.assertFalse(ParsedDecimal.from(1.0).isZero()); + Assertions.assertFalse(ParsedDecimal.from(-1.0).isZero()); + + Assertions.assertFalse(ParsedDecimal.from(Double.MIN_NORMAL).isZero()); + Assertions.assertFalse(ParsedDecimal.from(-Double.MIN_NORMAL).isZero()); + + Assertions.assertFalse(ParsedDecimal.from(Double.MAX_VALUE).isZero()); + Assertions.assertFalse(ParsedDecimal.from(-Double.MIN_VALUE).isZero()); + } + + @Test + void testMaxPrecision() { + // arrange + final double d = 1.02576552; + + // act + assertMaxPrecision(d, 10, false, "102576552", -8); + assertMaxPrecision(d, 9, false, "102576552", -8); + assertMaxPrecision(d, 8, false, "10257655", -7); + assertMaxPrecision(d, 7, false, "1025766", -6); + assertMaxPrecision(d, 6, false, "102577", -5); + assertMaxPrecision(d, 5, false, "10258", -4); + assertMaxPrecision(d, 4, false, "1026", -3); + assertMaxPrecision(d, 3, false, "103", -2); + assertMaxPrecision(d, 2, false, "1", 0); + assertMaxPrecision(d, 1, false, "1", 0); + + assertMaxPrecision(d, 0, false, "102576552", -8); + } + + @Test + void testMaxPrecision_carry() { + // arrange + final double d = -999.0999e50; + + // act + assertMaxPrecision(d, 8, true, "9990999", 46); + assertMaxPrecision(d, 7, true, "9990999", 46); + assertMaxPrecision(d, 6, true, "9991", 49); + assertMaxPrecision(d, 5, true, "9991", 49); + assertMaxPrecision(d, 4, true, "9991", 49); + assertMaxPrecision(d, 3, true, "999", 50); + assertMaxPrecision(d, 2, true, "1", 53); + assertMaxPrecision(d, 1, true, "1", 53); + + assertMaxPrecision(d, 0, true, "9990999", 46); + } + + @Test + void testMaxPrecision_halfEvenRounding() { + // act/assert + // Test values taken from RoundingMode.HALF_EVEN javadocs + assertMaxPrecision(5.5, 1, false, "6", 0); + assertMaxPrecision(2.5, 1, false, "2", 0); + assertMaxPrecision(1.6, 1, false, "2", 0); + assertMaxPrecision(1.1, 1, false, "1", 0); + assertMaxPrecision(1.0, 1, false, "1", 0); + + assertMaxPrecision(-1.0, 1, true, "1", 0); + assertMaxPrecision(-1.1, 1, true, "1", 0); + assertMaxPrecision(-1.6, 1, true, "2", 0); + assertMaxPrecision(-2.5, 1, true, "2", 0); + assertMaxPrecision(-5.5, 1, true, "6", 0); + } + + @Test + void testMaxPrecision_random() { + // arrange + final UniformRandomProvider rand = RandomSource.create(RandomSource.XO_RO_SHI_RO_128_PP, 0L); + final ParsedDecimal.FormatOptions opts = new FormatOptionsImpl(); + + for (int i = 0; i < 10_000; ++i) { + final double d = createRandomDouble(rand); + final int precision = rand.nextInt(20) + 1; + final MathContext ctx = new MathContext(precision, RoundingMode.HALF_EVEN); + + final ParsedDecimal dec = ParsedDecimal.from(d); + + // act + dec.maxPrecision(precision); + + // assert + Assertions.assertEquals(new BigDecimal(Double.toString(d), ctx).doubleValue(), + Double.parseDouble(dec.toScientificString(opts))); + } + } + + @Test + void testMaxPrecision_singleDigits() { + // act + assertMaxPrecision(9.0, 1, false, "9", 0); + assertMaxPrecision(1.0, 1, false, "1", 0); + assertMaxPrecision(0.0, 1, false, "0", 0); + assertMaxPrecision(-0.0, 1, true, "0", 0); + assertMaxPrecision(-1.0, 1, true, "1", 0); + assertMaxPrecision(-9.0, 1, true, "9", 0); + } + + @Test + void testRound_mixed() { + // arrange + final double a = 9.94e-10; + final double b = -3.1415; + final double c = 5.55e10; + + // act/assert + assertRound(a, -13, false, "994", -12); + assertRound(a, -12, false, "994", -12); + assertRound(a, -11, false, "99", -11); + assertRound(a, -10, false, "1", -9); + assertRound(a, -9, false, "1", -9); + assertRound(a, -8, false, "0", 0); + + assertRound(b, -5, true, "31415", -4); + assertRound(b, -4, true, "31415", -4); + assertRound(b, -3, true, "3142", -3); + assertRound(b, -2, true, "314", -2); + assertRound(b, -1, true, "31", -1); + assertRound(b, 0, true, "3", 0); + assertRound(b, 1, true, "0", 0); + assertRound(b, 2, true, "0", 0); + + assertRound(c, 7, false, "555", 8); + assertRound(c, 8, false, "555", 8); + assertRound(c, 9, false, "56", 9); + assertRound(c, 10, false, "6", 10); + assertRound(c, 11, false, "1", 11); + assertRound(c, 12, false, "0", 0); + } + + @Test + void testRound_nine() { + // arrange + final double a = 9e-10; + final double b = -9; + final double c = 9e10; + + // act/assert + assertRound(a, -11, false, "9", -10); + assertRound(a, -10, false, "9", -10); + assertRound(a, -9, false, "1", -9); + + assertRound(b, -1, true, "9", 0); + assertRound(b, 0, true, "9", 0); + assertRound(b, 1, true, "1", 1); + + assertRound(c, 9, false, "9", 10); + assertRound(c, 10, false, "9", 10); + assertRound(c, 11, false, "1", 11); + } + + @Test + void testRound_one() { + // arrange + final double a = 1e-10; + final double b = -1; + final double c = 1e10; + + // act/assert + assertRound(a, -11, false, "1", -10); + assertRound(a, -10, false, "1", -10); + assertRound(a, -9, false, "0", 0); + + assertRound(b, -1, true, "1", 0); + assertRound(b, 0, true, "1", 0); + assertRound(b, 1, true, "0", 0); + + assertRound(c, 9, false, "1", 10); + assertRound(c, 10, false, "1", 10); + assertRound(c, 11, false, "0", 0); + } + + @Test + void testStringMethodAccuracy_random() { + // arrange + final UniformRandomProvider rand = RandomSource.create(RandomSource.XO_RO_SHI_RO_128_PP, 0L); + + final FormatOptionsImpl stdOpts = new FormatOptionsImpl(); + final FormatOptionsImpl altOpts = new FormatOptionsImpl(); + altOpts.setExponentSeparator("e"); + altOpts.setIncludeFractionPlaceholder(false); + + double d; + for (int i = 0; i < 10_000; ++i) { + d = createRandomDouble(rand); + + // act/assert + Assertions.assertEquals(d, Double.parseDouble(ParsedDecimal.from(d).toScientificString(stdOpts))); + Assertions.assertEquals(d, Double.parseDouble(ParsedDecimal.from(d).toScientificString(altOpts))); + + Assertions.assertEquals(d, Double.parseDouble(ParsedDecimal.from(d).toEngineeringString(stdOpts))); + Assertions.assertEquals(d, Double.parseDouble(ParsedDecimal.from(d).toEngineeringString(altOpts))); + + Assertions.assertEquals(d, Double.parseDouble(ParsedDecimal.from(d).toPlainString(stdOpts))); + Assertions.assertEquals(d, Double.parseDouble(ParsedDecimal.from(d).toPlainString(altOpts))); + } + } + + @Test + void testStringMethodAccuracy_sequence() { + // arrange + final double min = -1000; + final double max = 1000; + final double delta = 0.1; + + final FormatOptionsImpl stdOpts = new FormatOptionsImpl(); + final FormatOptionsImpl altOpts = new FormatOptionsImpl(); + altOpts.setExponentSeparator("e"); + altOpts.setIncludeFractionPlaceholder(false); + + Assertions.assertEquals(10.0, Double.parseDouble(ParsedDecimal.from(10.0).toScientificString(stdOpts))); + + for (double d = min; d <= max; d += delta) { + // act/assert + Assertions.assertEquals(d, Double.parseDouble(ParsedDecimal.from(d).toScientificString(stdOpts))); + Assertions.assertEquals(d, Double.parseDouble(ParsedDecimal.from(d).toScientificString(altOpts))); + + Assertions.assertEquals(d, Double.parseDouble(ParsedDecimal.from(d).toEngineeringString(stdOpts))); + Assertions.assertEquals(d, Double.parseDouble(ParsedDecimal.from(d).toEngineeringString(altOpts))); + + Assertions.assertEquals(d, Double.parseDouble(ParsedDecimal.from(d).toPlainString(stdOpts))); + Assertions.assertEquals(d, Double.parseDouble(ParsedDecimal.from(d).toPlainString(altOpts))); + } + } + + @Test + void testStringMethods_customDigits() { + // arrange + final FormatOptionsImpl opts = new FormatOptionsImpl(); + opts.setDigitsFromString("abcdefghij"); + + // act/assert + Assertions.assertEquals("b.a", ParsedDecimal.from(1.0).toPlainString(opts)); + Assertions.assertEquals("-a.abcd", ParsedDecimal.from(-0.0123).toPlainString(opts)); + Assertions.assertEquals("bc.de", ParsedDecimal.from(12.34).toPlainString(opts)); + Assertions.assertEquals("baaaa.a", ParsedDecimal.from(10000).toPlainString(opts)); + Assertions.assertEquals("jihgfedcba.a", ParsedDecimal.from(9876543210d).toPlainString(opts)); + + Assertions.assertEquals("b.a", ParsedDecimal.from(1.0).toScientificString(opts)); + Assertions.assertEquals("-b.cdE-c", ParsedDecimal.from(-0.0123).toScientificString(opts)); + Assertions.assertEquals("b.cdeEb", ParsedDecimal.from(12.34).toScientificString(opts)); + Assertions.assertEquals("b.aEe", ParsedDecimal.from(10000).toScientificString(opts)); + Assertions.assertEquals("j.ihgfedcbEj", ParsedDecimal.from(9876543210d).toScientificString(opts)); + + Assertions.assertEquals("b.a", ParsedDecimal.from(1.0).toEngineeringString(opts)); + Assertions.assertEquals("-bc.dE-d", ParsedDecimal.from(-0.0123).toEngineeringString(opts)); + Assertions.assertEquals("bc.de", ParsedDecimal.from(12.34).toEngineeringString(opts)); + Assertions.assertEquals("ba.aEd", ParsedDecimal.from(10000).toEngineeringString(opts)); + Assertions.assertEquals("j.ihgfedcbEj", ParsedDecimal.from(9876543210d).toEngineeringString(opts)); + } + + @Test + void testToEngineeringString_altFormat() { + // arrange + final FormatOptionsImpl opts = new FormatOptionsImpl(); + opts.setIncludeFractionPlaceholder(false); + opts.setSignedZero(false); + opts.setDecimalSeparator(','); + opts.setMinusSign('!'); + opts.setExponentSeparator("x10^"); + opts.setAlwaysIncludeExponent(true); + + // act/assert + checkToEngineeringString(0.0, "0x10^0", opts); + checkToEngineeringString(-0.0, "0x10^0", opts); + checkToEngineeringString(1.0, "1x10^0", opts); + checkToEngineeringString(1.5, "1,5x10^0", opts); + + checkToEngineeringString(10, "10x10^0", opts); + + checkToEngineeringString(-0.000000123, "!123x10^!9", opts); + checkToEngineeringString(12300000, "12,3x10^6", opts); + + checkToEngineeringString(Math.PI, "3,141592653589793x10^0", opts); + checkToEngineeringString(Math.E, "2,718281828459045x10^0", opts); + + checkToEngineeringString(-Double.MAX_VALUE, "!179,76931348623157x10^306", opts); + checkToEngineeringString(Double.MIN_VALUE, "4,9x10^!324", opts); + checkToEngineeringString(Double.MIN_NORMAL, "22,250738585072014x10^!309", opts); + } + + @Test + void testToEngineeringString_defaults() { + // arrange + final FormatOptionsImpl opts = new FormatOptionsImpl(); + + // act/assert + checkToEngineeringString(0.0, "0.0", opts); + checkToEngineeringString(-0.0, "-0.0", opts); + checkToEngineeringString(1.0, "1.0", opts); + checkToEngineeringString(1.5, "1.5", opts); + + checkToEngineeringString(10, "10.0", opts); + + checkToEngineeringString(-0.000000123, "-123.0E-9", opts); + checkToEngineeringString(12300000, "12.3E6", opts); + + checkToEngineeringString(Math.PI, "3.141592653589793", opts); + checkToEngineeringString(Math.E, "2.718281828459045", opts); + + checkToEngineeringString(-Double.MAX_VALUE, "-179.76931348623157E306", opts); + checkToEngineeringString(Double.MIN_VALUE, "4.9E-324", opts); + checkToEngineeringString(Double.MIN_NORMAL, "22.250738585072014E-309", opts); + } + + @Test + void testToPlainString_altFormat() { + // arrange + final FormatOptionsImpl opts = new FormatOptionsImpl(); + opts.setIncludeFractionPlaceholder(false); + opts.setSignedZero(false); + opts.setDecimalSeparator(','); + opts.setMinusSign('!'); + opts.setThousandsGroupingSeparator('_'); + opts.setGroupThousands(true); + + // act/assert + checkToPlainString(0.0, "0", opts); + checkToPlainString(-0.0, "0", opts); + checkToPlainString(1.0, "1", opts); + checkToPlainString(1.5, "1,5", opts); + + checkToPlainString(12, "12", opts); + checkToPlainString(123, "123", opts); + checkToPlainString(1234, "1_234", opts); + checkToPlainString(12345, "12_345", opts); + checkToPlainString(123456, "123_456", opts); + checkToPlainString(1234567, "1_234_567", opts); + checkToPlainString(12345678, "12_345_678", opts); + checkToPlainString(123456789, "123_456_789", opts); + checkToPlainString(1234567890, "1_234_567_890", opts); + + checkToPlainString(-0.000123, "!0,000123", opts); + checkToPlainString(12301, "12_301", opts); + + checkToPlainString(Math.PI, "3,141592653589793", opts); + checkToPlainString(Math.E, "2,718281828459045", opts); + + checkToPlainString(-12345.6789, "!12_345,6789", opts); + checkToPlainString(1.23e12, "1_230_000_000_000", opts); + checkToPlainString(1.23e-12, "0,00000000000123", opts); + } + + @Test + void testToPlainString_defaults() { + // arrange + final FormatOptionsImpl opts = new FormatOptionsImpl(); + + // act/assert + checkToPlainString(0.0, "0.0", opts); + checkToPlainString(-0.0, "-0.0", opts); + checkToPlainString(1.0, "1.0", opts); + checkToPlainString(1.5, "1.5", opts); + + checkToPlainString(12, "12.0", opts); + checkToPlainString(123, "123.0", opts); + checkToPlainString(1234, "1234.0", opts); + checkToPlainString(12345, "12345.0", opts); + checkToPlainString(123456, "123456.0", opts); + checkToPlainString(1234567, "1234567.0", opts); + checkToPlainString(12345678, "12345678.0", opts); + checkToPlainString(123456789, "123456789.0", opts); + checkToPlainString(1234567890, "1234567890.0", opts); + + checkToPlainString(-0.000123, "-0.000123", opts); + checkToPlainString(12301, "12301.0", opts); + + checkToPlainString(Math.PI, "3.141592653589793", opts); + checkToPlainString(Math.E, "2.718281828459045", opts); + + checkToPlainString(-12345.6789, "-12345.6789", opts); + checkToPlainString(1.23e12, "1230000000000.0", opts); + checkToPlainString(1.23e-12, "0.00000000000123", opts); + } + + @Test + void testToScientificString_altFormats() { + // arrange + final FormatOptionsImpl opts = new FormatOptionsImpl(); + opts.setIncludeFractionPlaceholder(false); + opts.setSignedZero(false); + opts.setDecimalSeparator(','); + opts.setMinusSign('!'); + opts.setExponentSeparator("x10^"); + opts.setAlwaysIncludeExponent(true); + + // act/assert + checkToScientificString(0.0, "0x10^0", opts); + checkToScientificString(-0.0, "0x10^0", opts); + checkToScientificString(1.0, "1x10^0", opts); + checkToScientificString(1.5, "1,5x10^0", opts); + + checkToScientificString(-0.000123, "!1,23x10^!4", opts); + checkToScientificString(12301, "1,2301x10^4", opts); + + checkToScientificString(Math.PI, "3,141592653589793x10^0", opts); + checkToScientificString(Math.E, "2,718281828459045x10^0", opts); + + checkToScientificString(-Double.MAX_VALUE, "!1,7976931348623157x10^308", opts); + checkToScientificString(Double.MIN_VALUE, "4,9x10^!324", opts); + checkToScientificString(Double.MIN_NORMAL, "2,2250738585072014x10^!308", opts); + } + + @Test + void testToScientificString_defaults() { + // arrange + final FormatOptionsImpl opts = new FormatOptionsImpl(); + + // act/assert + checkToScientificString(0.0, "0.0", opts); + checkToScientificString(-0.0, "-0.0", opts); + checkToScientificString(1.0, "1.0", opts); + checkToScientificString(1.5, "1.5", opts); + + checkToScientificString(-0.000123, "-1.23E-4", opts); + checkToScientificString(12301, "1.2301E4", opts); + + checkToScientificString(Math.PI, "3.141592653589793", opts); + checkToScientificString(Math.E, "2.718281828459045", opts); + + checkToScientificString(-Double.MAX_VALUE, "-1.7976931348623157E308", opts); + checkToScientificString(Double.MIN_VALUE, "4.9E-324", opts); + checkToScientificString(Double.MIN_NORMAL, "2.2250738585072014E-308", opts); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/CosineDistanceTest.java b/sources/src/test/java/org/apache/commons/text/similarity/CosineDistanceTest.java new file mode 100644 index 0000000..2781e77 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/CosineDistanceTest.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link CosineSimilarity}. + */ +public class CosineDistanceTest { + + /** + * Cosine distance under test. + */ + private static CosineDistance cosineDistance; + + /** + * Creates the cosine distance object used throughout the tests. + */ + @BeforeAll + public static void setUp() { + cosineDistance = new CosineDistance(); + } + + /** + * Rounds up a value. + * + * @param value a value + * @return rounded up value + */ + private Double roundValue(final Double value) { + return new BigDecimal(value).setScale(2, RoundingMode.HALF_UP).doubleValue(); + } + + /** + * Tests the cosine distance with several inputs. + */ + @Test + public void testCosineDistance() { + assertThat(roundValue(cosineDistance.apply("the house", "da house"))).isEqualTo(Double.valueOf(0.5d)); + assertThat(roundValue(cosineDistance.apply("AB", "AB"))).isEqualTo(Double.valueOf(0.0d)); + assertThat(roundValue(cosineDistance.apply("AB", "BA"))).isEqualTo(Double.valueOf(1.0d)); + assertThat(roundValue(cosineDistance.apply( + "the boy was from tamana shi, kumamoto ken, and the girl was from rio de janeiro, rio", + "the boy was from tamana shi, kumamoto, and the boy was from rio de janeiro, rio de janeiro"))) + .isEqualTo(Double.valueOf(0.08d)); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/CosineSimilarityTest.java b/sources/src/test/java/org/apache/commons/text/similarity/CosineSimilarityTest.java new file mode 100644 index 0000000..8d968e8 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/CosineSimilarityTest.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.within; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +public class CosineSimilarityTest { + + @Test + public void testCosineSimilarityReturningDoubleWhereByteValueIsZero() { + final Map hashMap = new HashMap<>(); + assertThat(CosineSimilarity.INSTANCE.cosineSimilarity(hashMap, hashMap)).isEqualTo(0.0, within(0.01)); + } + + @Test + public void testCosineSimilarityThrowsIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> { + final Map map = new HashMap<>(); + CosineSimilarity.INSTANCE.cosineSimilarity(map, null); + }); + } + + @Test + public void testCosineSimilarityWithNonEmptyMap() { + final Map hashMap = new HashMap<>(); + final Integer integer = -397; + hashMap.put("3J/$3.L", integer); + final Map hashMapTwo = new HashMap<>(); + + assertThat(CosineSimilarity.INSTANCE.cosineSimilarity(hashMap, hashMapTwo)).isEqualTo(0.0, within(0.01)); + } + + @Test + public void testCosineSimilarityWithNull() { + assertThatIllegalArgumentException().isThrownBy(() -> { + CosineSimilarity.INSTANCE.cosineSimilarity(null, null); + }); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/FuzzyScoreTest.java b/sources/src/test/java/org/apache/commons/text/similarity/FuzzyScoreTest.java new file mode 100644 index 0000000..3a5ac0d --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/FuzzyScoreTest.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link FuzzyScore}. + */ +public class FuzzyScoreTest { + + private static final FuzzyScore ENGLISH_SCORE = new FuzzyScore(Locale.ENGLISH); + + @Test + public void testGetFuzzyScore() { + assertThat(ENGLISH_SCORE.fuzzyScore("", "")).isEqualTo(0); + assertThat(ENGLISH_SCORE.fuzzyScore("Workshop", "b")).isEqualTo(0); + assertThat(ENGLISH_SCORE.fuzzyScore("Room", "o")).isEqualTo(1); + assertThat(ENGLISH_SCORE.fuzzyScore("Workshop", "w")).isEqualTo(1); + assertThat(ENGLISH_SCORE.fuzzyScore("Workshop", "ws")).isEqualTo(2); + assertThat(ENGLISH_SCORE.fuzzyScore("Workshop", "wo")).isEqualTo(4); + assertThat(ENGLISH_SCORE.fuzzyScore("Apache Software Foundation", "asf")).isEqualTo(3); + } + + @Test + public void testGetFuzzyScore_NullNullLocale() { + assertThatIllegalArgumentException().isThrownBy(() -> ENGLISH_SCORE.fuzzyScore(null, null)); + } + + @Test + public void testGetFuzzyScore_NullStringLocale() { + assertThatIllegalArgumentException().isThrownBy(() -> ENGLISH_SCORE.fuzzyScore(null, "not null")); + } + + @Test + public void testGetFuzzyScore_StringNullLocale() { + assertThatIllegalArgumentException().isThrownBy(() -> ENGLISH_SCORE.fuzzyScore("not null", null)); + } + + @Test + public void testGetLocale() { + final Locale locale = Locale.CANADA_FRENCH; + final FuzzyScore fuzzyScore = new FuzzyScore(locale); + final Locale localeTwo = fuzzyScore.getLocale(); + + assertThat(localeTwo).isSameAs(locale); + } + + @Test + public void testMissingLocale() { + assertThatIllegalArgumentException().isThrownBy(() -> new FuzzyScore((Locale) null)); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/HammingDistanceTest.java b/sources/src/test/java/org/apache/commons/text/similarity/HammingDistanceTest.java new file mode 100644 index 0000000..fde321d --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/HammingDistanceTest.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link HammingDistance}. + */ +public class HammingDistanceTest { + + private static HammingDistance distance; + + @BeforeAll + public static void setUp() { + distance = new HammingDistance(); + } + + @Test + public void testHammingDistance() { + assertThat(distance.apply("", "")).isEqualTo(0); + assertThat(distance.apply("pappa", "pappa")).isEqualTo(0); + assertThat(distance.apply("papaa", "pappa")).isEqualTo(1); + assertThat(distance.apply("karolin", "kathrin")).isEqualTo(3); + assertThat(distance.apply("karolin", "kerstin")).isEqualTo(3); + assertThat(distance.apply("1011101", "1001001")).isEqualTo(2); + assertThat(distance.apply("2173896", "2233796")).isEqualTo(3); + assertThat(distance.apply("ATCG", "ACCC")).isEqualTo(2); + } + + @Test + public void testHammingDistance_nullLeftValue() { + assertThatIllegalArgumentException().isThrownBy(() -> distance.apply(null, "")); + } + + @Test + public void testHammingDistance_nullRightValue() { + assertThatIllegalArgumentException().isThrownBy(() -> distance.apply("", null)); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/IntersectionResultTest.java b/sources/src/test/java/org/apache/commons/text/similarity/IntersectionResultTest.java new file mode 100644 index 0000000..f9c53df --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/IntersectionResultTest.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import java.util.HashMap; +import java.util.concurrent.ThreadLocalRandom; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link IntersectionResult}. + */ +public class IntersectionResultTest { + @Test + public void testEquals() { + final IntersectionResult[] results = { + new IntersectionResult(0, 0, 0), + new IntersectionResult(10, 0, 0), + new IntersectionResult(10, 10, 0), + new IntersectionResult(10, 10, 10), + }; + + // Test a different instance with same values + Assertions.assertEquals(results[0], new IntersectionResult(0, 0, 0)); + + final Object something = new Object(); + for (int i = 0; i < results.length; i++) { + Assertions.assertNotEquals(results[i], something); + Assertions.assertNotEquals(null, results[i]); + for (int j = 0; j < results.length; j++) { + Assertions.assertEquals(results[i].equals(results[j]), i == j); + } + } + } + + @Test + public void testHashCode() { + final IntersectionResult[] results = { + new IntersectionResult(10, 0, 0), + new IntersectionResult(10, 10, 0), + new IntersectionResult(10, 10, 10), + }; + final HashMap map = new HashMap<>(); + final int offset = 123; + for (int i = 0; i < results.length; i++) { + map.put(results[i], i + offset); + } + for (int i = 0; i < results.length; i++) { + Assertions.assertEquals(i + offset, map.get(results[i])); + } + } + + @Test + public void testNewIntersectionResult_WithIntersectionAboveSizeAorB() { + final int sizeA = 1; + final int sizeB = 2; + final int intersection = Math.max(sizeA, sizeB) + 1; + Assertions.assertThrows(IllegalArgumentException.class, () -> new IntersectionResult(sizeA, sizeB, intersection)); + Assertions.assertThrows(IllegalArgumentException.class, () -> new IntersectionResult(sizeB, sizeA, intersection)); + } + + @Test + public void testNewIntersectionResult_WithNegativeIntersection() { + final int sizeA = 0; + final int sizeB = 0; + final int intersection = -1; + Assertions.assertThrows(IllegalArgumentException.class, () -> new IntersectionResult(sizeA, sizeB, intersection)); + } + + @Test + public void testNewIntersectionResult_WithNegativeSizeA() { + final int sizeA = -1; + final int sizeB = 0; + final int intersection = 0; + Assertions.assertThrows(IllegalArgumentException.class, () -> new IntersectionResult(sizeA, sizeB, intersection)); + } + + @Test + public void testNewIntersectionResult_WithNegativeSizeB() { + final int sizeA = 0; + final int sizeB = -1; + final int intersection = 0; + Assertions.assertThrows(IllegalArgumentException.class, () -> new IntersectionResult(sizeA, sizeB, intersection)); + } + + @Test + public void testNewIntersectionResult_WithZeros() { + final int sizeA = 0; + final int sizeB = 0; + final int intersection = 0; + new IntersectionResult(sizeA, sizeB, intersection); + } + + @Test + public void testProperties() { + final ThreadLocalRandom rand = ThreadLocalRandom.current(); + final int max = 1024; + for (int i = 0; i < 5; i++) { + // Ensure the min is above 0 + final int sizeA = rand.nextInt(max) + 1; + final int sizeB = rand.nextInt(max) + 1; + final int intersection = rand.nextInt(Math.min(sizeA, sizeB)); + final IntersectionResult result = new IntersectionResult(sizeA, sizeB, intersection); + Assertions.assertEquals(sizeA, result.getSizeA()); + Assertions.assertEquals(sizeB, result.getSizeB()); + Assertions.assertEquals(intersection, result.getIntersection()); + } + } + + @Test + public void testToString() { + final ThreadLocalRandom rand = ThreadLocalRandom.current(); + final int max = 9; + for (int i = 0; i < 5; i++) { + // Ensure the min is above 0 + final int sizeA = rand.nextInt(max) + 1; + final int sizeB = rand.nextInt(max) + 1; + final int intersection = rand.nextInt(Math.min(sizeA, sizeB)); + final IntersectionResult result = new IntersectionResult(sizeA, sizeB, intersection); + final String string = result.toString(); + // Not perfect as this will match substrings too. The chance of error + // is limited by restricting the numbers to a max of 10. + Assertions.assertTrue(string.contains(String.valueOf(sizeA))); + Assertions.assertTrue(string.contains(String.valueOf(sizeB))); + Assertions.assertTrue(string.contains(String.valueOf(intersection))); + } + } +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/IntersectionSimilarityTest.java b/sources/src/test/java/org/apache/commons/text/similarity/IntersectionSimilarityTest.java new file mode 100644 index 0000000..76b1d92 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/IntersectionSimilarityTest.java @@ -0,0 +1,330 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link IntersectionSimilarity}. + */ +public class IntersectionSimilarityTest { + private static void assertIntersection(final IntersectionSimilarity similarity, final CharSequence cs1, final CharSequence cs2, final int sizeA, + final int sizeB, final int intersection) { + final IntersectionResult result = similarity.apply(cs1, cs2); + assertEquals(sizeA, result.getSizeA(), "Size A error"); + assertEquals(sizeB, result.getSizeB(), "Size B error"); + assertEquals(intersection, result.getIntersection(), "Intersection error"); + } + + /** + * Convert the {@link CharSequence} to a {@link List} of bigrams (pairs of characters). These are represented using 2 + * 16-bit chars packed into a 32-bit int. + * + * @param sequence the sequence + * @return the list + */ + private static List toBigramList(final CharSequence sequence) { + final int length = sequence.length(); + final List list = new ArrayList<>(length); + if (length > 1) { + char ch2 = sequence.charAt(0); + for (int i = 1; i < length; i++) { + final char ch1 = ch2; + ch2 = sequence.charAt(i); + list.add(Integer.valueOf(ch1 << 16 | ch2)); + } + } + return list; + } + + /** + * Convert the {@link CharSequence} to a {@link Set} of bigrams (pairs of characters). These are represented using 2 + * 16-bit chars packed into a 32-bit int. + * + * @param sequence the sequence + * @return the set + */ + private static Set toBigramSet(final CharSequence sequence) { + final int length = sequence.length(); + final Set set = new HashSet<>(length); + if (length > 1) { + char ch2 = sequence.charAt(0); + for (int i = 1; i < length; i++) { + final char ch1 = ch2; + ch2 = sequence.charAt(i); + set.add(Integer.valueOf(ch1 << 16 | ch2)); + } + } + return set; + } + + /** + * Convert the {@link CharSequence} to a {@link List} of {@link Character}s. + * + * @param sequence the sequence + * @return the list + */ + private static List toCharacterList(final CharSequence sequence) { + final int length = sequence.length(); + final List list = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + list.add(sequence.charAt(i)); + } + return list; + } + + /** + * Convert the {@link CharSequence} to a {@link Set} of {@link Character}s. + * + * @param sequence the sequence + * @return the set + */ + private static Set toCharacterSet(final CharSequence sequence) { + final int length = sequence.length(); + final Set set = new HashSet<>(length); + for (int i = 0; i < length; i++) { + set.add(sequence.charAt(i)); + } + return set; + } + + private static int toF1ScorePercent(final IntersectionResult result) { + final double value = 2.0 * result.getIntersection() / (result.getSizeA() + result.getSizeB()); + // Convert to percentage + return (int) Math.round(value * 100); + } + + @Test + public void testApplyNullNull() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new IntersectionSimilarity<>(cs -> new HashSet<>(Collections.singletonList(cs))).apply(null, null)); + } + + @Test + public void testApplyNullString() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new IntersectionSimilarity<>(cs -> new HashSet<>(Collections.singletonList(cs))).apply(null, "right")); + } + + @Test + public void testApplyStringNull() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new IntersectionSimilarity<>(cs -> new HashSet<>(Collections.singletonList(cs))).apply("left", null)); + } + + @Test + public void testConstructorWithNullConverterThrows() { + assertThatIllegalArgumentException().isThrownBy(() -> new IntersectionSimilarity<>(null)); + } + + @Test + public void testF1ScoreUsingListWordBigrams() { + // Example of a word letter pairs algorithm by Simon White: + // http://www.catalysoft.com/articles/StrikeAMatch.html + // This splits into words using whitespace and then computes uppercase + // bigrams for each word. + + // Split on whitespace + final Pattern pattern = Pattern.compile("\\s+"); + + // Compute using pairs of characters (bigrams) for each word. + // This can be done using a 32-bit int to store two 16-bit characters + final Function> converter = cs -> { + final List set = new ArrayList<>(); + for (final String word : pattern.split(cs)) { + if (word.length() > 1) { + // The strings are converted to upper case + char ch2 = Character.toUpperCase(word.charAt(0)); + for (int i = 1; i < word.length(); i++) { + final char ch1 = ch2; + ch2 = Character.toUpperCase(word.charAt(i)); + set.add(Integer.valueOf(ch1 << 16 | ch2)); + } + } + } + return set; + }; + final IntersectionSimilarity similarity = new IntersectionSimilarity<>(converter); + + String bookTitle; + final String search1 = "Web Database Applications"; + final String search2 = "PHP Web Applications"; + final String search3 = "Web Aplications"; + bookTitle = "Web Database Applications with PHP & MySQL"; + assertEquals(82, toF1ScorePercent(similarity.apply(bookTitle, search1))); + assertEquals(68, toF1ScorePercent(similarity.apply(bookTitle, search2))); + assertEquals(59, toF1ScorePercent(similarity.apply(bookTitle, search3))); + bookTitle = "Creating Database Web Applications with PHP and ASP"; + assertEquals(71, toF1ScorePercent(similarity.apply(bookTitle, search1))); + assertEquals(59, toF1ScorePercent(similarity.apply(bookTitle, search2))); + assertEquals(50, toF1ScorePercent(similarity.apply(bookTitle, search3))); + bookTitle = "Building Database Applications on the Web Using PHP3"; + assertEquals(70, toF1ScorePercent(similarity.apply(bookTitle, search1))); + assertEquals(58, toF1ScorePercent(similarity.apply(bookTitle, search2))); + assertEquals(49, toF1ScorePercent(similarity.apply(bookTitle, search3))); + bookTitle = "Building Web Database Applications with Visual Studio 6"; + assertEquals(67, toF1ScorePercent(similarity.apply(bookTitle, search1))); + assertEquals(47, toF1ScorePercent(similarity.apply(bookTitle, search2))); + assertEquals(46, toF1ScorePercent(similarity.apply(bookTitle, search3))); + bookTitle = "Web Application Development With PHP"; + assertEquals(51, toF1ScorePercent(similarity.apply(bookTitle, search1))); + assertEquals(67, toF1ScorePercent(similarity.apply(bookTitle, search2))); + assertEquals(56, toF1ScorePercent(similarity.apply(bookTitle, search3))); + bookTitle = "WebRAD: Building Database Applications on the Web with Visual FoxPro and Web Connection"; + assertEquals(49, toF1ScorePercent(similarity.apply(bookTitle, search1))); + assertEquals(34, toF1ScorePercent(similarity.apply(bookTitle, search2))); + assertEquals(32, toF1ScorePercent(similarity.apply(bookTitle, search3))); + bookTitle = "Structural Assessment: The Role of Large and Full-Scale Testing"; + assertEquals(12, toF1ScorePercent(similarity.apply(bookTitle, search1))); + assertEquals(7, toF1ScorePercent(similarity.apply(bookTitle, search2))); + assertEquals(7, toF1ScorePercent(similarity.apply(bookTitle, search3))); + bookTitle = "How to Find a Scholarship Online"; + assertEquals(10, toF1ScorePercent(similarity.apply(bookTitle, search1))); + assertEquals(11, toF1ScorePercent(similarity.apply(bookTitle, search2))); + assertEquals(12, toF1ScorePercent(similarity.apply(bookTitle, search3))); + } + + @Test + public void testIntersectionUsingListBigrams() { + // Compute using pairs of characters (bigrams). + // This can be done using a 32-bit int to store two 16-bit characters. + // This test uses a list and so duplicates should be matched. + final IntersectionSimilarity similarity = new IntersectionSimilarity<>(IntersectionSimilarityTest::toBigramList); + + // Expected: + // size A or B = sequence length - 1 + // intersection = count of matching bigrams (include duplicates) + assertIntersection(similarity, "", "", 0, 0, 0); + assertIntersection(similarity, "a", "", 0, 0, 0); + assertIntersection(similarity, "a", "a", 0, 0, 0); + assertIntersection(similarity, "a", "b", 0, 0, 0); + assertIntersection(similarity, "aa", "ab", 1, 1, 0); + assertIntersection(similarity, "ab", "ab", 1, 1, 1); + assertIntersection(similarity, "aaba", "abaa", 3, 3, 3); + assertIntersection(similarity, "aaaa", "aa", 3, 1, 1); + assertIntersection(similarity, "aa", "aaaa", 1, 3, 1); + assertIntersection(similarity, "aaaa", "aaa", 3, 2, 2); + assertIntersection(similarity, "aabab", "ababa", 4, 4, 3); + assertIntersection(similarity, "the same", "the same", 7, 7, 7); + assertIntersection(similarity, "abcdefghijklm", "ab_defg ijklm", 12, 12, 8); + } + + @Test + public void testIntersectionUsingListCharacter() { + // Compute using single characters. + // This test uses a list and so duplicates should be matched. + final IntersectionSimilarity similarity = new IntersectionSimilarity<>(IntersectionSimilarityTest::toCharacterList); + + // Expected: + // size A or B = sequence length + // intersection = count of matching characters (include duplicates) + assertIntersection(similarity, "", "", 0, 0, 0); + assertIntersection(similarity, "a", "", 1, 0, 0); + assertIntersection(similarity, "a", "a", 1, 1, 1); + assertIntersection(similarity, "a", "b", 1, 1, 0); + assertIntersection(similarity, "aa", "ab", 2, 2, 1); + assertIntersection(similarity, "ab", "ab", 2, 2, 2); + assertIntersection(similarity, "aaba", "abaa", 4, 4, 4); + assertIntersection(similarity, "aaaa", "aa", 4, 2, 2); + assertIntersection(similarity, "aa", "aaaa", 2, 4, 2); + assertIntersection(similarity, "aaaa", "aaa", 4, 3, 3); + assertIntersection(similarity, "aabab", "ababa", 5, 5, 5); + assertIntersection(similarity, "the same", "the same", 8, 8, 8); + assertIntersection(similarity, "abcdefghijklm", "ab_defg ijklm", 13, 13, 11); + } + + @Test + public void testIntersectionUsingSetBigrams() { + // Compute using pairs of characters (bigrams). + // This can be done using a 32-bit int to store two 16-bit characters. + // This test uses a set and so should not allow duplicates. + final IntersectionSimilarity similarity = new IntersectionSimilarity<>(IntersectionSimilarityTest::toBigramSet); + + // Expected: + // size A or B = count of unique bigrams (exclude duplicates) + // intersection = count of unique matching bigrams (exclude duplicates) + assertIntersection(similarity, "", "", 0, 0, 0); + assertIntersection(similarity, "a", "", 0, 0, 0); + assertIntersection(similarity, "a", "a", 0, 0, 0); + assertIntersection(similarity, "a", "b", 0, 0, 0); + assertIntersection(similarity, "aa", "ab", 1, 1, 0); + assertIntersection(similarity, "ab", "ab", 1, 1, 1); + assertIntersection(similarity, "aaba", "abaa", 3, 3, 3); + assertIntersection(similarity, "aaaa", "aa", 1, 1, 1); + assertIntersection(similarity, "aa", "aaaa", 1, 1, 1); + assertIntersection(similarity, "aaaa", "aaa", 1, 1, 1); + assertIntersection(similarity, "aabab", "ababa", 3, 2, 2); + assertIntersection(similarity, "the same", "the same", 7, 7, 7); + assertIntersection(similarity, "abcdefghijklm", "ab_defg ijklm", 12, 12, 8); + } + + @Test + public void testIntersectionUsingSetCharacter() { + // Compute using single characters. + // This test uses a set and so should not allow duplicates. + final IntersectionSimilarity similarity = new IntersectionSimilarity<>(IntersectionSimilarityTest::toCharacterSet); + + // Expected: + // size A or B = count of unique characters (exclude duplicates) + // intersection = count of unique matching characters (exclude duplicates) + assertIntersection(similarity, "", "", 0, 0, 0); + assertIntersection(similarity, "a", "", 1, 0, 0); + assertIntersection(similarity, "a", "a", 1, 1, 1); + assertIntersection(similarity, "a", "b", 1, 1, 0); + assertIntersection(similarity, "aa", "ab", 1, 2, 1); + assertIntersection(similarity, "ab", "ab", 2, 2, 2); + assertIntersection(similarity, "aaba", "abaa", 2, 2, 2); + assertIntersection(similarity, "aaaa", "aa", 1, 1, 1); + assertIntersection(similarity, "aa", "aaaa", 1, 1, 1); + assertIntersection(similarity, "aaaa", "aaa", 1, 1, 1); + assertIntersection(similarity, "aabab", "ababa", 2, 2, 2); + assertIntersection(similarity, "the same", "the same", 7, 7, 7); + assertIntersection(similarity, "abcdefghijklm", "ab_defg ijklm", 13, 13, 11); + } + + @Test + public void testIntersectionUsingSetCharacterListCharacter() { + // Compute using a custom converter that returns a Set and a List. + // This is an edge-case test. + final HashMap> converter = new HashMap<>(); + final String sequence1 = "aabbccdd"; + final String sequence2 = "aaaaaabbbfffff"; + converter.put(sequence1, toCharacterSet(sequence1)); + converter.put(sequence2, toCharacterList(sequence2)); + final IntersectionSimilarity similarity = new IntersectionSimilarity<>(converter::get); + + // Expected: + // size A = count of unique characters (exclude duplicates) + // size B = sequence length + // intersection = count of matching characters (exclude duplicates) + assertIntersection(similarity, sequence1, sequence2, 4, sequence2.length(), 2); + assertIntersection(similarity, sequence2, sequence1, sequence2.length(), 4, 2); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/JaccardDistanceTest.java b/sources/src/test/java/org/apache/commons/text/similarity/JaccardDistanceTest.java new file mode 100644 index 0000000..bf724a2 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/JaccardDistanceTest.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link JaccardDistance}. + */ +public class JaccardDistanceTest { + + private static JaccardDistance classBeingTested; + + @BeforeAll + public static void setUp() { + classBeingTested = new JaccardDistance(); + } + + @Test + public void testGettingJaccardDistance() { + // Expected Jaccard distance = 1.0 - (intersect / union) + assertEquals(0.0, classBeingTested.apply("", "")); + assertEquals(1.0, classBeingTested.apply("left", "")); + assertEquals(1.0, classBeingTested.apply("", "right")); + assertEquals(1.0 - 3.0 / 4, classBeingTested.apply("frog", "fog")); + assertEquals(1.0, classBeingTested.apply("fly", "ant")); + assertEquals(1.0 - 2.0 / 9, classBeingTested.apply("elephant", "hippo")); + assertEquals(1.0 - 7.0 / 11, classBeingTested.apply("ABC Corporation", "ABC Corp")); + assertEquals(1.0 - 13.0 / 17, + classBeingTested.apply("D N H Enterprises Inc", "D & H Enterprises, Inc.")); + assertEquals(1.0 - 16.0 / 18, + classBeingTested.apply("My Gym Children's Fitness Center", "My Gym. Childrens Fitness")); + assertEquals(1.0 - 9.0 / 10, classBeingTested.apply("PENNSYLVANIA", "PENNCISYLVNIA")); + assertEquals(1.0 - 1.0 / 8, classBeingTested.apply("left", "right")); + assertEquals(1.0 - 1.0 / 8, classBeingTested.apply("leettteft", "ritttght")); + assertEquals(0.0, classBeingTested.apply("the same string", "the same string")); + } + + @Test + public void testGettingJaccardDistanceNullNull() { + assertThatIllegalArgumentException().isThrownBy(() -> classBeingTested.apply(null, null)); + } + + @Test + public void testGettingJaccardDistanceNullString() { + assertThatIllegalArgumentException().isThrownBy(() -> classBeingTested.apply(null, "right")); + } + + @Test + public void testGettingJaccardDistanceStringNull() { + assertThatIllegalArgumentException().isThrownBy(() -> classBeingTested.apply(" ", null)); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/JaccardSimilarityTest.java b/sources/src/test/java/org/apache/commons/text/similarity/JaccardSimilarityTest.java new file mode 100644 index 0000000..c43a2d7 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/JaccardSimilarityTest.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link JaccardSimilarity}. + */ +public class JaccardSimilarityTest { + + private static JaccardSimilarity classBeingTested; + + @BeforeAll + public static void setUp() { + classBeingTested = new JaccardSimilarity(); + } + + @Test + public void testGettingJaccardSimilarity() { + // Expected Jaccard similarity = (intersect / union) + assertEquals(1.0, classBeingTested.apply("", "")); + assertEquals(0.0, classBeingTested.apply("left", "")); + assertEquals(0.0, classBeingTested.apply("", "right")); + assertEquals(3.0 / 4, classBeingTested.apply("frog", "fog")); + assertEquals(0.0, classBeingTested.apply("fly", "ant")); + assertEquals(2.0 / 9, classBeingTested.apply("elephant", "hippo")); + assertEquals(7.0 / 11, classBeingTested.apply("ABC Corporation", "ABC Corp")); + assertEquals(13.0 / 17, + classBeingTested.apply("D N H Enterprises Inc", "D & H Enterprises, Inc.")); + assertEquals(16.0 / 18, + classBeingTested.apply("My Gym Children's Fitness Center", "My Gym. Childrens Fitness")); + assertEquals(9.0 / 10, classBeingTested.apply("PENNSYLVANIA", "PENNCISYLVNIA")); + assertEquals(1.0 / 8, classBeingTested.apply("left", "right")); + assertEquals(1.0 / 8, classBeingTested.apply("leettteft", "ritttght")); + assertEquals(1.0, classBeingTested.apply("the same string", "the same string")); + } + + @Test + public void testGettingJaccardSimilarityNullNull() { + assertThatIllegalArgumentException().isThrownBy(() -> classBeingTested.apply(null, null)); + } + + @Test + public void testGettingJaccardSimilarityNullString() { + assertThatIllegalArgumentException().isThrownBy(() -> classBeingTested.apply(null, "right")); + } + + @Test + public void testGettingJaccardSimilarityStringNull() { + assertThatIllegalArgumentException().isThrownBy(() -> classBeingTested.apply(" ", null)); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/JaroWinklerDistanceTest.java b/sources/src/test/java/org/apache/commons/text/similarity/JaroWinklerDistanceTest.java new file mode 100644 index 0000000..cb34a13 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/JaroWinklerDistanceTest.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link JaroWinklerDistance}. + */ +public class JaroWinklerDistanceTest { + + private static JaroWinklerDistance distance; + + @BeforeAll + public static void setUp() { + distance = new JaroWinklerDistance(); + } + + @Test + public void testGetJaroWinklerDistance_NullNull() { + assertThatIllegalArgumentException().isThrownBy(() -> distance.apply(null, null)); + } + + @Test + public void testGetJaroWinklerDistance_NullString() { + assertThatIllegalArgumentException().isThrownBy(() -> distance.apply(null, "clear")); + } + + @Test + public void testGetJaroWinklerDistance_StringNull() { + assertThatIllegalArgumentException().isThrownBy(() -> distance.apply(" ", null)); + } + + @Test + public void testGetJaroWinklerDistance_StringString() { + assertEquals(0.07501d, distance.apply("frog", "fog"), 0.00001d); + assertEquals(1.0d, distance.apply("fly", "ant"), 0.00000000000000000001d); + assertEquals(0.55834d, distance.apply("elephant", "hippo"), 0.00001d); + assertEquals(0.09334d, distance.apply("ABC Corporation", "ABC Corp"), 0.00001d); + assertEquals(0.04749d, distance.apply("D N H Enterprises Inc", "D & H Enterprises, Inc."), 0.00001d); + assertEquals(0.058d, distance.apply("My Gym Children's Fitness Center", "My Gym. Childrens Fitness"), 0.00001d); + assertEquals(0.101982d, distance.apply("PENNSYLVANIA", "PENNCISYLVNIA"), 0.00001d); + assertEquals(0.028572d, distance.apply("/opt/software1", "/opt/software2"), 0.00001d); + assertEquals(0.058334d, distance.apply("aaabcd", "aaacdb"), 0.00001d); + assertEquals(0.088889d, distance.apply("John Horn", "John Hopkins"), 0.00001d); + assertEquals(0d, distance.apply("", ""), 0.00001d); + assertEquals(0d, distance.apply("foo", "foo"), 0.00001d); + assertEquals(1 - 0.94166d, distance.apply("foo", "foo "), 0.00001d); + assertEquals(1 - 0.90666d, distance.apply("foo", "foo "), 0.00001d); + assertEquals(1 - 0.86666d, distance.apply("foo", " foo "), 0.00001d); + assertEquals(1 - 0.51111d, distance.apply("foo", " foo"), 0.00001d); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/JaroWinklerSimilarityTest.java b/sources/src/test/java/org/apache/commons/text/similarity/JaroWinklerSimilarityTest.java new file mode 100644 index 0000000..f7b25f9 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/JaroWinklerSimilarityTest.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link JaroWinklerSimilarity}. + */ +public class JaroWinklerSimilarityTest { + + private static JaroWinklerSimilarity similarity; + + @BeforeAll + public static void setUp() { + similarity = new JaroWinklerSimilarity(); + } + + /** + * Wrap the string to a {@link CharSequence}. This ensures that using the + * {@link Object#equals(Object)} method on the input CharSequence to test for + * equality will fail. + * + * @param string the string + * @return the char sequence + */ + private static CharSequence wrap(final String string) { + return new CharSequence() { + @Override + public char charAt(final int index) { + return string.charAt(index); + } + @Override + public int length() { + return string.length(); + } + @Override + public CharSequence subSequence(final int start, final int end) { + return string.subSequence(start, end); + } + }; + } + + @Test + public void testGetJaroWinklerSimilarity_NullNull() { + assertThatIllegalArgumentException().isThrownBy(() -> similarity.apply(null, null)); + } + + @Test + public void testGetJaroWinklerSimilarity_NullString() { + assertThatIllegalArgumentException().isThrownBy(() -> similarity.apply(null, "clear")); + } + + @Test + public void testGetJaroWinklerSimilarity_StringNull() { + assertThatIllegalArgumentException().isThrownBy(() -> similarity.apply(" ", null)); + } + + @Test + public void testGetJaroWinklerSimilarity_StringString() { + assertEquals(1d, similarity.apply(wrap(""), ""), 0.00001d); + assertEquals(1d, similarity.apply(wrap("foo"), "foo"), 0.00001d); + assertEquals(0.94166d, similarity.apply(wrap("foo"), "foo "), 0.00001d); + assertEquals(0.90666d, similarity.apply(wrap("foo"), "foo "), 0.00001d); + assertEquals(0.86666d, similarity.apply(wrap("foo"), " foo "), 0.00001d); + assertEquals(0.51111d, similarity.apply(wrap("foo"), " foo"), 0.00001d); + assertEquals(0.92499d, similarity.apply(wrap("frog"), "fog"), 0.00001d); + assertEquals(0.0d, similarity.apply(wrap("fly"), "ant"), 0.00000000000000000001d); + assertEquals(0.44166d, similarity.apply(wrap("elephant"), "hippo"), 0.00001d); + assertEquals(0.90666d, similarity.apply(wrap("ABC Corporation"), "ABC Corp"), 0.00001d); + assertEquals(0.95251d, similarity.apply(wrap("D N H Enterprises Inc"), "D & H Enterprises, Inc."), 0.00001d); + assertEquals(0.942d, + similarity.apply(wrap("My Gym Children's Fitness Center"), "My Gym. Childrens Fitness"), 0.00001d); + assertEquals(0.898018d, similarity.apply(wrap("PENNSYLVANIA"), "PENNCISYLVNIA"), 0.00001d); + assertEquals(0.971428d, similarity.apply(wrap("/opt/software1"), "/opt/software2"), 0.00001d); + assertEquals(0.941666d, similarity.apply(wrap("aaabcd"), "aaacdb"), 0.00001d); + assertEquals(0.911111d, similarity.apply(wrap("John Horn"), "John Hopkins"), 0.00001d); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/LevenshteinDetailedDistanceTest.java b/sources/src/test/java/org/apache/commons/text/similarity/LevenshteinDetailedDistanceTest.java new file mode 100644 index 0000000..010659e --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/LevenshteinDetailedDistanceTest.java @@ -0,0 +1,462 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import org.apache.commons.text.TextStringBuilder; +import org.junit.jupiter.api.Test; + +public class LevenshteinDetailedDistanceTest { + + private static final LevenshteinDetailedDistance UNLIMITED_DISTANCE = new LevenshteinDetailedDistance(); + + @Test + public void testApplyThrowsIllegalArgumentExceptionAndCreatesLevenshteinDetailedDistanceTakingInteger() { + assertThatIllegalArgumentException().isThrownBy(() -> { + final LevenshteinDetailedDistance levenshteinDetailedDistance = new LevenshteinDetailedDistance(0); + final CharSequence charSequence = new TextStringBuilder(); + + levenshteinDetailedDistance.apply(charSequence, null); + }); + } + + @Test + public void testApplyWithNull() { + assertThatIllegalArgumentException().isThrownBy(() -> new LevenshteinDetailedDistance(0).apply(null, null)); + } + + @Test + public void testConstructorWithNegativeThreshold() { + assertThatIllegalArgumentException().isThrownBy(() -> new LevenshteinDetailedDistance(-1)); + } + + @Test + public void testCreatesLevenshteinDetailedDistanceTakingInteger6() { + final LevenshteinDetailedDistance levenshteinDetailedDistance = new LevenshteinDetailedDistance(0); + final LevenshteinResults levenshteinResults = + levenshteinDetailedDistance.apply("", "Distance: 38, Insert: 0, Delete: 0, Substitute: 0"); + + assertThat(levenshteinResults.getSubstituteCount()).isEqualTo(0); + assertThat(levenshteinResults.getDeleteCount()).isEqualTo(0); + + assertThat(levenshteinResults.getInsertCount()).isEqualTo(0); + assertThat(levenshteinResults.getDistance()).isEqualTo(-1); + } + + @Test + public void testEquals() { + final LevenshteinDetailedDistance classBeingTested = new LevenshteinDetailedDistance(); + LevenshteinResults actualResult = classBeingTested.apply("hello", "hallo"); + LevenshteinResults expectedResult = new LevenshteinResults(1, 0, 0, 1); + assertThat(expectedResult).isEqualTo(actualResult); + + actualResult = classBeingTested.apply("zzzzzzzz", "hippo"); + expectedResult = new LevenshteinResults(8, 0, 3, 5); + assertThat(expectedResult).isEqualTo(actualResult); + assertThat(actualResult).isEqualTo(actualResult); //intentionally added + + actualResult = classBeingTested.apply("", ""); + expectedResult = new LevenshteinResults(0, 0, 0, 0); + assertThat(expectedResult).isEqualTo(actualResult); + } + + @Test + public void testGetDefaultInstanceOne() { + final LevenshteinDetailedDistance levenshteinDetailedDistance = + LevenshteinDetailedDistance.getDefaultInstance(); + final LevenshteinResults levenshteinResults = + levenshteinDetailedDistance.apply("Distance: -2147483643, Insert: 0, Delete: 0, Substitute: 0", + "Distance: 0, Insert: 2147483536, Delete: 0, Substitute: 0"); + + assertThat(levenshteinResults.getDistance()).isEqualTo(21); + } + + @Test + public void testGetDefaultInstanceTwo() { + final LevenshteinDetailedDistance levenshteinDetailedDistance = + LevenshteinDetailedDistance.getDefaultInstance(); + final LevenshteinResults levenshteinResults = + levenshteinDetailedDistance.apply("Distance: 2147483647, Insert: 0, Delete: 0, Substitute: 0", + "Distance: 0, Insert: 2147483647, Delete: 0, Substitute: 0"); + + assertThat(levenshteinResults.getDistance()).isEqualTo(20); + } + + @Test + public void testGetLevenshteinDetailedDistance_NullString() { + assertThatIllegalArgumentException().isThrownBy(() -> UNLIMITED_DISTANCE.apply("a", null)); + } + + @Test + public void testGetLevenshteinDetailedDistance_NullStringInt() { + assertThatIllegalArgumentException().isThrownBy(() -> UNLIMITED_DISTANCE.apply(null, "a")); + } + + @Test + public void testGetLevenshteinDetailedDistance_StringNull() { + assertThatIllegalArgumentException().isThrownBy(() -> UNLIMITED_DISTANCE.apply(null, "a")); + } + + @Test + public void testGetLevenshteinDetailedDistance_StringNullInt() { + assertThatIllegalArgumentException().isThrownBy(() -> UNLIMITED_DISTANCE.apply("a", null)); + } + + @Test + public void testGetLevenshteinDetailedDistance_StringString() { + LevenshteinResults result = UNLIMITED_DISTANCE.apply("", ""); + assertThat(result.getDistance()).isEqualTo(0); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = UNLIMITED_DISTANCE.apply("", "a"); + assertThat(result.getDistance()).isEqualTo(1); + assertThat(result.getInsertCount()).isEqualTo(1); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = UNLIMITED_DISTANCE.apply("aaapppp", ""); + assertThat(result.getDistance()).isEqualTo(7); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(7); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = UNLIMITED_DISTANCE.apply("frog", "fog"); + assertThat(result.getDistance()).isEqualTo(1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(1); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = UNLIMITED_DISTANCE.apply("fly", "ant"); + assertThat(result.getDistance()).isEqualTo(3); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(3); + + result = UNLIMITED_DISTANCE.apply("elephant", "hippo"); + assertThat(result.getDistance()).isEqualTo(7); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(3); + assertThat(result.getSubstituteCount()).isEqualTo(4); + + result = UNLIMITED_DISTANCE.apply("hippo", "elephant"); + assertThat(result.getDistance()).isEqualTo(7); + assertThat(result.getInsertCount()).isEqualTo(3); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(4); + + result = UNLIMITED_DISTANCE.apply("hippo", "zzzzzzzz"); + assertThat(result.getDistance()).isEqualTo(8); + assertThat(result.getInsertCount()).isEqualTo(3); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(5); + + result = UNLIMITED_DISTANCE.apply("zzzzzzzz", "hippo"); + assertThat(result.getDistance()).isEqualTo(8); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(3); + assertThat(result.getSubstituteCount()).isEqualTo(5); + + result = UNLIMITED_DISTANCE.apply("hello", "hallo"); + assertThat(result.getDistance()).isEqualTo(1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(1); + } + + @Test + public void testGetLevenshteinDetailedDistance_StringStringInt() { + + LevenshteinResults result = new LevenshteinDetailedDistance(0).apply("", ""); + + assertThat(result.getDistance()).isEqualTo(0); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(8).apply("aaapppp", ""); + assertThat(result.getDistance()).isEqualTo(7); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(7); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(7).apply("aaapppp", ""); + assertThat(result.getDistance()).isEqualTo(7); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(7); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(6).apply("aaapppp", ""); + assertThat(result.getDistance()).isEqualTo(-1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(0).apply("b", "a"); + assertThat(result.getDistance()).isEqualTo(-1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(0).apply("a", "b"); + assertThat(result.getDistance()).isEqualTo(-1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(0).apply("aa", "aa"); + assertThat(result.getDistance()).isEqualTo(0); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(2).apply("aa", "aa"); + assertThat(result.getDistance()).isEqualTo(0); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(2).apply("aaa", "bbb"); + assertThat(result.getDistance()).isEqualTo(-1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(3).apply("aaa", "bbb"); + assertThat(result.getDistance()).isEqualTo(3); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(3); + + result = new LevenshteinDetailedDistance(10).apply("aaaaaa", "b"); + assertThat(result.getDistance()).isEqualTo(6); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(5); + assertThat(result.getSubstituteCount()).isEqualTo(1); + + result = new LevenshteinDetailedDistance(8).apply("aaapppp", "b"); + assertThat(result.getDistance()).isEqualTo(7); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(6); + assertThat(result.getSubstituteCount()).isEqualTo(1); + + result = new LevenshteinDetailedDistance(4).apply("a", "bbb"); + assertThat(result.getDistance()).isEqualTo(3); + assertThat(result.getInsertCount()).isEqualTo(2); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(1); + + result = new LevenshteinDetailedDistance(7).apply("aaapppp", "b"); + assertThat(result.getDistance()).isEqualTo(7); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(6); + assertThat(result.getSubstituteCount()).isEqualTo(1); + + result = new LevenshteinDetailedDistance(3).apply("a", "bbb"); + assertThat(result.getDistance()).isEqualTo(3); + assertThat(result.getInsertCount()).isEqualTo(2); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(1); + + result = new LevenshteinDetailedDistance(2).apply("a", "bbb"); + assertThat(result.getDistance()).isEqualTo(-1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(2).apply("bbb", "a"); + assertThat(result.getDistance()).isEqualTo(-1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(6).apply("aaapppp", "b"); + assertThat(result.getDistance()).isEqualTo(-1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(1).apply("a", "bbb"); + assertThat(result.getDistance()).isEqualTo(-1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(1).apply("bbb", "a"); + assertThat(result.getDistance()).isEqualTo(-1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(1).apply("12345", "1234567"); + assertThat(result.getDistance()).isEqualTo(-1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(1).apply("1234567", "12345"); + assertThat(result.getDistance()).isEqualTo(-1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(1).apply("frog", "fog"); + assertThat(result.getDistance()).isEqualTo(1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(1); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(3).apply("fly", "ant"); + assertThat(result.getDistance()).isEqualTo(3); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(3); + + result = new LevenshteinDetailedDistance(7).apply("elephant", "hippo"); + assertThat(result.getDistance()).isEqualTo(7); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(3); + assertThat(result.getSubstituteCount()).isEqualTo(4); + + result = new LevenshteinDetailedDistance(6).apply("elephant", "hippo"); + assertThat(result.getDistance()).isEqualTo(-1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(7).apply("hippo", "elephant"); + assertThat(result.getDistance()).isEqualTo(7); + assertThat(result.getInsertCount()).isEqualTo(3); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(4); + + result = new LevenshteinDetailedDistance(7).apply("hippo", "elephant"); + assertThat(result.getDistance()).isEqualTo(7); + assertThat(result.getInsertCount()).isEqualTo(3); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(4); + + result = new LevenshteinDetailedDistance(6).apply("hippo", "elephant"); + assertThat(result.getDistance()).isEqualTo(-1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(8).apply("hippo", "zzzzzzzz"); + assertThat(result.getDistance()).isEqualTo(8); + assertThat(result.getInsertCount()).isEqualTo(3); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(5); + + result = new LevenshteinDetailedDistance(8).apply("zzzzzzzz", "hippo"); + assertThat(result.getDistance()).isEqualTo(8); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(3); + assertThat(result.getSubstituteCount()).isEqualTo(5); + + result = new LevenshteinDetailedDistance(1).apply("hello", "hallo"); + assertThat(result.getDistance()).isEqualTo(1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(1); + + result = new LevenshteinDetailedDistance(Integer.MAX_VALUE).apply("frog", "fog"); + assertThat(result.getDistance()).isEqualTo(1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(1); + assertThat(result.getSubstituteCount()).isEqualTo(0); + + result = new LevenshteinDetailedDistance(Integer.MAX_VALUE).apply("fly", "ant"); + assertThat(result.getDistance()).isEqualTo(3); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(3); + + result = new LevenshteinDetailedDistance(Integer.MAX_VALUE).apply("elephant", "hippo"); + assertThat(result.getDistance()).isEqualTo(7); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(3); + assertThat(result.getSubstituteCount()).isEqualTo(4); + + result = new LevenshteinDetailedDistance(Integer.MAX_VALUE).apply("hippo", "elephant"); + assertThat(result.getDistance()).isEqualTo(7); + assertThat(result.getInsertCount()).isEqualTo(3); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(4); + + result = new LevenshteinDetailedDistance(Integer.MAX_VALUE).apply("hippo", "zzzzzzzz"); + assertThat(result.getDistance()).isEqualTo(8); + assertThat(result.getInsertCount()).isEqualTo(3); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(5); + + result = new LevenshteinDetailedDistance(Integer.MAX_VALUE).apply("zzzzzzzz", "hippo"); + assertThat(result.getDistance()).isEqualTo(8); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(3); + assertThat(result.getSubstituteCount()).isEqualTo(5); + + result = new LevenshteinDetailedDistance(Integer.MAX_VALUE).apply("hello", "hallo"); + assertThat(result.getDistance()).isEqualTo(1); + assertThat(result.getInsertCount()).isEqualTo(0); + assertThat(result.getDeleteCount()).isEqualTo(0); + assertThat(result.getSubstituteCount()).isEqualTo(1); + } + + @Test + public void testGetThreshold() { + final LevenshteinDetailedDistance levenshteinDetailedDistance = new LevenshteinDetailedDistance(0); + + assertThat(levenshteinDetailedDistance.getThreshold()).isEqualTo(0); + } + + @Test + public void testHashCode() { + final LevenshteinDetailedDistance classBeingTested = new LevenshteinDetailedDistance(); + LevenshteinResults actualResult = classBeingTested.apply("aaapppp", ""); + LevenshteinResults expectedResult = new LevenshteinResults(7, 0, 7, 0); + assertThat(expectedResult.hashCode()).isEqualTo(actualResult.hashCode()); + + actualResult = classBeingTested.apply("frog", "fog"); + expectedResult = new LevenshteinResults(1, 0, 1, 0); + assertThat(expectedResult.hashCode()).isEqualTo(actualResult.hashCode()); + + actualResult = classBeingTested.apply("elephant", "hippo"); + expectedResult = new LevenshteinResults(7, 0, 3, 4); + assertThat(expectedResult.hashCode()).isEqualTo(actualResult.hashCode()); + } + + @Test + public void testToString() { + final LevenshteinDetailedDistance classBeingTested = new LevenshteinDetailedDistance(); + LevenshteinResults actualResult = classBeingTested.apply("fly", "ant"); + LevenshteinResults expectedResult = new LevenshteinResults(3, 0, 0, 3); + assertThat(expectedResult.toString()).isEqualTo(actualResult.toString()); + + actualResult = classBeingTested.apply("hippo", "elephant"); + expectedResult = new LevenshteinResults(7, 3, 0, 4); + assertThat(expectedResult.toString()).isEqualTo(actualResult.toString()); + + actualResult = classBeingTested.apply("", "a"); + expectedResult = new LevenshteinResults(1, 1, 0, 0); + assertThat(expectedResult.toString()).isEqualTo(actualResult.toString()); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/LevenshteinDistanceTest.java b/sources/src/test/java/org/apache/commons/text/similarity/LevenshteinDistanceTest.java new file mode 100644 index 0000000..83bda60 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/LevenshteinDistanceTest.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link LevenshteinDistance}. + */ +public class LevenshteinDistanceTest { + + private static final LevenshteinDistance UNLIMITED_DISTANCE = new LevenshteinDistance(); + + @Test + public void testApplyThrowsIllegalArgumentExceptionAndCreatesLevenshteinDistanceTakingInteger() { + assertThatIllegalArgumentException().isThrownBy(() -> new LevenshteinDistance(0).apply(null, null)); + } + + @Test + public void testConstructorWithNegativeThreshold() { + assertThatIllegalArgumentException().isThrownBy(() -> new LevenshteinDistance(-1)); + } + + @Test + public void testGetLevenshteinDistance_NullString() { + assertThatIllegalArgumentException().isThrownBy(() -> UNLIMITED_DISTANCE.apply("a", null)); + } + + @Test + public void testGetLevenshteinDistance_NullStringInt() { + assertThatIllegalArgumentException().isThrownBy(() -> UNLIMITED_DISTANCE.apply(null, "a")); + } + + @Test + public void testGetLevenshteinDistance_StringNull() { + assertThatIllegalArgumentException().isThrownBy(() -> UNLIMITED_DISTANCE.apply(null, "a")); + } + + @Test + public void testGetLevenshteinDistance_StringNullInt() { + assertThatIllegalArgumentException().isThrownBy(() -> UNLIMITED_DISTANCE.apply("a", null)); + } + + @Test + public void testGetLevenshteinDistance_StringString() { + assertThat(UNLIMITED_DISTANCE.apply("", "")).isEqualTo(0); + assertThat(UNLIMITED_DISTANCE.apply("", "a")).isEqualTo(1); + assertThat(UNLIMITED_DISTANCE.apply("aaapppp", "")).isEqualTo(7); + assertThat(UNLIMITED_DISTANCE.apply("frog", "fog")).isEqualTo(1); + assertThat(UNLIMITED_DISTANCE.apply("fly", "ant")).isEqualTo(3); + assertThat(UNLIMITED_DISTANCE.apply("elephant", "hippo")).isEqualTo(7); + assertThat(UNLIMITED_DISTANCE.apply("hippo", "elephant")).isEqualTo(7); + assertThat(UNLIMITED_DISTANCE.apply("hippo", "zzzzzzzz")).isEqualTo(8); + assertThat(UNLIMITED_DISTANCE.apply("zzzzzzzz", "hippo")).isEqualTo(8); + assertThat(UNLIMITED_DISTANCE.apply("hello", "hallo")).isEqualTo(1); + } + + @Test + public void testGetLevenshteinDistance_StringStringInt() { + // empty strings + assertThat(new LevenshteinDistance(0).apply("", "")).isEqualTo(0); + assertThat(new LevenshteinDistance(8).apply("aaapppp", "")).isEqualTo(7); + assertThat(new LevenshteinDistance(7).apply("aaapppp", "")).isEqualTo(7); + assertThat(new LevenshteinDistance(6).apply("aaapppp", "")).isEqualTo(-1); + + // unequal strings, zero threshold + assertThat(new LevenshteinDistance(0).apply("b", "a")).isEqualTo(-1); + assertThat(new LevenshteinDistance(0).apply("a", "b")).isEqualTo(-1); + + // equal strings + assertThat(new LevenshteinDistance(0).apply("aa", "aa")).isEqualTo(0); + assertThat(new LevenshteinDistance(2).apply("aa", "aa")).isEqualTo(0); + + // same length + assertThat(new LevenshteinDistance(2).apply("aaa", "bbb")).isEqualTo(-1); + assertThat(new LevenshteinDistance(3).apply("aaa", "bbb")).isEqualTo(3); + + // big stripe + assertThat(new LevenshteinDistance(10).apply("aaaaaa", "b")).isEqualTo(6); + + // distance less than threshold + assertThat(new LevenshteinDistance(8).apply("aaapppp", "b")).isEqualTo(7); + assertThat(new LevenshteinDistance(4).apply("a", "bbb")).isEqualTo(3); + + // distance equal to threshold + assertThat(new LevenshteinDistance(7).apply("aaapppp", "b")).isEqualTo(7); + assertThat(new LevenshteinDistance(3).apply("a", "bbb")).isEqualTo(3); + + // distance greater than threshold + assertThat(new LevenshteinDistance(2).apply("a", "bbb")).isEqualTo(-1); + assertThat(new LevenshteinDistance(2).apply("bbb", "a")).isEqualTo(-1); + assertThat(new LevenshteinDistance(6).apply("aaapppp", "b")).isEqualTo(-1); + + // stripe runs off array, strings not similar + assertThat(new LevenshteinDistance(1).apply("a", "bbb")).isEqualTo(-1); + assertThat(new LevenshteinDistance(1).apply("bbb", "a")).isEqualTo(-1); + + // stripe runs off array, strings are similar + assertThat(new LevenshteinDistance(1).apply("12345", "1234567")).isEqualTo(-1); + assertThat(new LevenshteinDistance(1).apply("1234567", "12345")).isEqualTo(-1); + + // old getLevenshteinDistance test cases + assertThat(new LevenshteinDistance(1).apply("frog", "fog")).isEqualTo(1); + assertThat(new LevenshteinDistance(3).apply("fly", "ant")).isEqualTo(3); + assertThat(new LevenshteinDistance(7).apply("elephant", "hippo")).isEqualTo(7); + assertThat(new LevenshteinDistance(6).apply("elephant", "hippo")).isEqualTo(-1); + assertThat(new LevenshteinDistance(7).apply("hippo", "elephant")).isEqualTo(7); + assertThat(new LevenshteinDistance(6).apply("hippo", "elephant")).isEqualTo(-1); + assertThat(new LevenshteinDistance(8).apply("hippo", "zzzzzzzz")).isEqualTo(8); + assertThat(new LevenshteinDistance(8).apply("zzzzzzzz", "hippo")).isEqualTo(8); + assertThat(new LevenshteinDistance(1).apply("hello", "hallo")).isEqualTo(1); + + assertThat(new LevenshteinDistance(Integer.MAX_VALUE).apply("frog", "fog")).isEqualTo(1); + assertThat(new LevenshteinDistance(Integer.MAX_VALUE).apply("fly", "ant")).isEqualTo(3); + assertThat(new LevenshteinDistance(Integer.MAX_VALUE).apply("elephant", "hippo")).isEqualTo(7); + assertThat(new LevenshteinDistance(Integer.MAX_VALUE).apply("hippo", "elephant")).isEqualTo(7); + assertThat(new LevenshteinDistance(Integer.MAX_VALUE).apply("hippo", "zzzzzzzz")).isEqualTo(8); + assertThat(new LevenshteinDistance(Integer.MAX_VALUE).apply("zzzzzzzz", "hippo")).isEqualTo(8); + assertThat(new LevenshteinDistance(Integer.MAX_VALUE).apply("hello", "hallo")).isEqualTo(1); + assertThat(new LevenshteinDistance(1).apply("abc", "acb")).isEqualTo(-1); + } + + @Test + public void testGetThresholdDirectlyAfterObjectInstantiation() { + assertThat(new LevenshteinDistance().getThreshold()).isNull(); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/LevenshteinResultsTest.java b/sources/src/test/java/org/apache/commons/text/similarity/LevenshteinResultsTest.java new file mode 100644 index 0000000..6804656 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/LevenshteinResultsTest.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class LevenshteinResultsTest { + + @Test + public void testEqualsDifferenceInSubstitutionCount() { + final Integer integer = 1662; + final LevenshteinResults levenshteinResults = new LevenshteinResults(integer, integer, integer, integer); + final LevenshteinResults levenshteinResultsTwo = new LevenshteinResults(integer, integer, integer, null); + + assertThat(levenshteinResults.equals(levenshteinResultsTwo)).isFalse(); + } + + @Test + public void testEqualsReturningFalse() { + final Integer integerOne = 1662; + final Integer integerTwo = 1164; + final LevenshteinResults levenshteinResults = + new LevenshteinResults(integerOne, integerOne, integerOne, integerOne); + final LevenshteinResults levenshteinResultsTwo = + new LevenshteinResults(integerOne, integerOne, integerTwo, integerTwo); + + assertThat(levenshteinResults.equals(levenshteinResultsTwo)).isFalse(); + } + + @Test + public void testEqualsSameObject() { + final Integer integer = 1662; + final LevenshteinResults levenshteinResults = new LevenshteinResults(integer, integer, integer, null); + + assertThat(levenshteinResults.equals(levenshteinResults)).isTrue(); + } + + @Test + public void testEqualsWithNonNull() { + final Integer integer = 1; + final LevenshteinResults levenshteinResults = new LevenshteinResults(null, integer, integer, null); + final LevenshteinResults levenshteinResultsTwo = new LevenshteinResults(null, null, null, null); + + assertThat(levenshteinResults.equals(levenshteinResultsTwo)).isFalse(); + } + + @Test + public void testEqualsWithNull() { + final Integer integer = -647; + final LevenshteinResults levenshteinResults = new LevenshteinResults(integer, null, null, integer); + + assertThat(levenshteinResults.equals(null)).isFalse(); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/LongestCommonSubsequenceDistanceTest.java b/sources/src/test/java/org/apache/commons/text/similarity/LongestCommonSubsequenceDistanceTest.java new file mode 100644 index 0000000..e0ccac5 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/LongestCommonSubsequenceDistanceTest.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link LongestCommonSubsequenceDistance}. + */ +public class LongestCommonSubsequenceDistanceTest { + + private static LongestCommonSubsequenceDistance subject; + + @BeforeAll + public static void setup() { + subject = new LongestCommonSubsequenceDistance(); + } + + @Test + public void testGettingLongestCommonSubsequenceDistance() { + assertThat(subject.apply("", "")).isEqualTo(0); + assertThat(subject.apply("left", "")).isEqualTo(4); + assertThat(subject.apply("", "right")).isEqualTo(5); + assertThat(subject.apply("frog", "fog")).isEqualTo(1); + assertThat(subject.apply("fly", "ant")).isEqualTo(6); + assertThat(subject.apply("elephant", "hippo")).isEqualTo(11); + assertThat(subject.apply("ABC Corporation", "ABC Corp")).isEqualTo(7); + assertThat(subject.apply("D N H Enterprises Inc", "D & H Enterprises, Inc.")).isEqualTo(4); + assertThat(subject.apply("My Gym Children's Fitness Center", "My Gym. Childrens Fitness")).isEqualTo(9); + assertThat(subject.apply("PENNSYLVANIA", "PENNCISYLVNIA")).isEqualTo(3); + assertThat(subject.apply("left", "right")).isEqualTo(7); + assertThat(subject.apply("leettteft", "ritttght")).isEqualTo(9); + assertThat(subject.apply("the same string", "the same string")).isEqualTo(0); + } + + @Test + public void testGettingLongestCommonSubsequenceDistanceNullNull() { + assertThatIllegalArgumentException().isThrownBy(() -> subject.apply(null, null)); + } + + @Test + public void testGettingLongestCommonSubsequenceDistanceNullString() { + assertThatIllegalArgumentException().isThrownBy(() -> subject.apply(null, "right")); + } + + @Test + public void testGettingLongestCommonSubsequenceDistanceStringNull() { + assertThatIllegalArgumentException().isThrownBy(() -> subject.apply(" ", null)); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/LongestCommonSubsequenceTest.java b/sources/src/test/java/org/apache/commons/text/similarity/LongestCommonSubsequenceTest.java new file mode 100644 index 0000000..8115240 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/LongestCommonSubsequenceTest.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link LongestCommonSubsequence}. + */ +public class LongestCommonSubsequenceTest { + + private static LongestCommonSubsequence subject; + + @BeforeAll + public static void setup() { + subject = new LongestCommonSubsequence(); + } + + @Test + @SuppressWarnings("deprecation") + public void testGettingLogestCommonSubsequenceNullNull() { + assertThatIllegalArgumentException().isThrownBy(() -> subject.logestCommonSubsequence(null, null)); + } + + @Test + @SuppressWarnings("deprecation") + public void testGettingLogestCommonSubsequenceNullString() { + assertThatIllegalArgumentException().isThrownBy(() -> subject.logestCommonSubsequence(null, "right")); + } + + @Test + @SuppressWarnings("deprecation") + public void testGettingLogestCommonSubsequenceStringNull() { + assertThatIllegalArgumentException().isThrownBy(() -> subject.logestCommonSubsequence(" ", null)); + } + + @Test + public void testGettingLongestCommonSubsequenceApplyNullNull() { + assertThatIllegalArgumentException().isThrownBy(() -> subject.apply(null, null)); + } + + @Test + public void testGettingLongestCommonSubsequenceApplyNullString() { + assertThatIllegalArgumentException().isThrownBy(() -> subject.apply(null, "right")); + } + + @Test + public void testGettingLongestCommonSubsequenceApplyStringNull() { + assertThatIllegalArgumentException().isThrownBy(() -> subject.apply(" ", null)); + } + + @Test + public void testGettingLongestCommonSubsequenceNullNull() { + assertThatIllegalArgumentException().isThrownBy(() -> subject.longestCommonSubsequence(null, null)); + } + + @Test + public void testGettingLongestCommonSubsequenceNullString() { + assertThatIllegalArgumentException().isThrownBy(() -> subject.longestCommonSubsequence(null, "right")); + } + + @Test + public void testGettingLongestCommonSubsequenceStringNull() { + assertThatIllegalArgumentException().isThrownBy(() -> subject.longestCommonSubsequence(" ", null)); + } + + @Test + @Deprecated + public void testLogestCommonSubsequence() { + assertThat(subject.logestCommonSubsequence("", "")).isEqualTo(""); + assertThat(subject.logestCommonSubsequence("left", "")).isEqualTo(""); + assertThat(subject.logestCommonSubsequence("", "right")).isEqualTo(""); + assertThat(subject.logestCommonSubsequence("frog", "fog")).isEqualTo("fog"); + assertThat(subject.logestCommonSubsequence("fly", "ant")).isEqualTo(""); + assertThat(subject.logestCommonSubsequence("elephant", "hippo")).isEqualTo("h"); + assertThat(subject.logestCommonSubsequence("ABC Corporation", "ABC Corp")).isEqualTo("ABC Corp"); + assertThat(subject.logestCommonSubsequence("D N H Enterprises Inc", "D & H Enterprises, Inc.")) + .isEqualTo("D H Enterprises Inc"); + assertThat(subject.logestCommonSubsequence("My Gym Children's Fitness Center", "My Gym. Childrens Fitness")) + .isEqualTo("My Gym Childrens Fitness"); + assertThat(subject.logestCommonSubsequence("PENNSYLVANIA", "PENNCISYLVNIA")).isEqualTo("PENNSYLVNIA"); + assertThat(subject.logestCommonSubsequence("left", "right")).isEqualTo("t"); + assertThat(subject.logestCommonSubsequence("leettteft", "ritttght")).isEqualTo("tttt"); + assertThat(subject.logestCommonSubsequence("the same string", "the same string")).isEqualTo("the same string"); + } + + @Test + public void testLongestCommonSubsequence() { + assertThat(subject.longestCommonSubsequence("", "")).isEqualTo(""); + assertThat(subject.longestCommonSubsequence("left", "")).isEqualTo(""); + assertThat(subject.longestCommonSubsequence("", "right")).isEqualTo(""); + assertThat(subject.longestCommonSubsequence("frog", "fog")).isEqualTo("fog"); + assertThat(subject.longestCommonSubsequence("fly", "ant")).isEqualTo(""); + assertThat(subject.longestCommonSubsequence("elephant", "hippo")).isEqualTo("h"); + assertThat(subject.longestCommonSubsequence("ABC Corporation", "ABC Corp")).isEqualTo("ABC Corp"); + assertThat(subject.longestCommonSubsequence("D N H Enterprises Inc", "D & H Enterprises, Inc.")) + .isEqualTo("D H Enterprises Inc"); + assertThat(subject.longestCommonSubsequence("My Gym Children's Fitness Center", "My Gym. Childrens Fitness")) + .isEqualTo("My Gym Childrens Fitness"); + assertThat(subject.longestCommonSubsequence("PENNSYLVANIA", "PENNCISYLVNIA")).isEqualTo("PENNSYLVNIA"); + assertThat(subject.longestCommonSubsequence("left", "right")).isEqualTo("t"); + assertThat(subject.longestCommonSubsequence("leettteft", "ritttght")).isEqualTo("tttt"); + assertThat(subject.longestCommonSubsequence("the same string", "the same string")).isEqualTo("the same string"); + } + + @Test + public void testLongestCommonSubsequenceApply() { + assertThat(subject.apply("", "")).isEqualTo(0); + assertThat(subject.apply("left", "")).isEqualTo(0); + assertThat(subject.apply("", "right")).isEqualTo(0); + assertThat(subject.apply("frog", "fog")).isEqualTo(3); + assertThat(subject.apply("fly", "ant")).isEqualTo(0); + assertThat(subject.apply("elephant", "hippo")).isEqualTo(1); + assertThat(subject.apply("ABC Corporation", "ABC Corp")).isEqualTo(8); + assertThat(subject.apply("D N H Enterprises Inc", "D & H Enterprises, Inc.")).isEqualTo(20); + assertThat(subject.apply("My Gym Children's Fitness Center", "My Gym. Childrens Fitness")).isEqualTo(24); + assertThat(subject.apply("PENNSYLVANIA", "PENNCISYLVNIA")).isEqualTo(11); + assertThat(subject.apply("left", "right")).isEqualTo(1); + assertThat(subject.apply("leettteft", "ritttght")).isEqualTo(4); + assertThat(subject.apply("the same string", "the same string")).isEqualTo(15); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/ParameterizedEditDistanceFromTest.java b/sources/src/test/java/org/apache/commons/text/similarity/ParameterizedEditDistanceFromTest.java new file mode 100644 index 0000000..edc496d --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/ParameterizedEditDistanceFromTest.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + + +/** + * Tests {@link EditDistanceFrom}. + * + * @param The {@link EditDistance} return type. + */ +public class ParameterizedEditDistanceFromTest { + + public static Stream parameters() { + return Stream.of( + Arguments.of(new HammingDistance(), "Sam I am.", "Ham I am.", 1), + Arguments.of(new HammingDistance(), "Japtheth, Ham, Shem", "Japtheth, HAM, Shem", 2), + Arguments.of(new HammingDistance(), "Hamming", "Hamming", 0), + + Arguments.of(new LevenshteinDistance(), "Apache", "a patchy", 4), + Arguments.of(new LevenshteinDistance(), "go", "no go", 3), + Arguments.of(new LevenshteinDistance(), "go", "go", 0), + + Arguments.of(new LevenshteinDistance(4), "Apache", "a patchy", 4), + Arguments.of(new LevenshteinDistance(4), "go", "no go", 3), + Arguments.of(new LevenshteinDistance(0), "go", "go", 0), + + Arguments.of( + (EditDistance) (left, right) -> left == right || left != null && left.equals(right), + "Bob's your uncle.", + "Every good boy does fine.", + false)); + } + + @ParameterizedTest + @MethodSource("parameters") + public void test(final EditDistance editDistance, final CharSequence left, final CharSequence right, + final R distance) { + final EditDistanceFrom editDistanceFrom = new EditDistanceFrom<>(editDistance, left); + assertThat(editDistanceFrom.apply(right)).isEqualTo(distance); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/ParameterizedLevenshteinDistanceTest.java b/sources/src/test/java/org/apache/commons/text/similarity/ParameterizedLevenshteinDistanceTest.java new file mode 100644 index 0000000..2c272e7 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/ParameterizedLevenshteinDistanceTest.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests {@link LevenshteinDistance}. + */ +public class ParameterizedLevenshteinDistanceTest { + + public static Stream parameters() { + return Stream.of( + /* empty strings */ + Arguments.of(0, "", "", 0), + Arguments.of(8, "aaapppp", "", 7), + Arguments.of(7, "aaapppp", "", 7), + Arguments.of(6, "aaapppp", "", -1), + + /* unequal strings, zero threshold */ + Arguments.of(0, "b", "a", -1), + Arguments.of(0, "a", "b", -1), + + /* equal strings */ + Arguments.of(0, "aa", "aa", 0), + Arguments.of(2, "aa", "aa", 0), + + /* same length */ + Arguments.of(2, "aaa", "bbb", -1), + Arguments.of(3, "aaa", "bbb", 3), + + /* big stripe */ + Arguments.of(10, "aaaaaa", "b", 6), + + /* distance less than threshold */ + Arguments.of(8, "aaapppp", "b", 7), + Arguments.of(4, "a", "bbb", 3), + + /* distance equal to threshold */ + Arguments.of(7, "aaapppp", "b", 7), + Arguments.of(3, "a", "bbb", 3), + + /* distance greater than threshold */ + Arguments.of(2, "a", "bbb", -1), + Arguments.of(2, "bbb", "a", -1), + Arguments.of(6, "aaapppp", "b", -1), + + /* stripe runs off array, strings not similar */ + Arguments.of(1, "a", "bbb", -1), + Arguments.of(1, "bbb", "a", -1), + + /* stripe runs off array, strings are similar */ + Arguments.of(1, "12345", "1234567", -1), + Arguments.of(1, "1234567", "12345", -1), + + /* old getLevenshteinDistance test cases */ + Arguments.of(1, "frog", "fog", 1), + Arguments.of(3, "fly", "ant", 3), + Arguments.of(7, "elephant", "hippo", 7), + Arguments.of(6, "elephant", "hippo", -1), + Arguments.of(7, "hippo", "elephant", 7), + Arguments.of(6, "hippo", "elephant", -1), + Arguments.of(8, "hippo", "zzzzzzzz", 8), + Arguments.of(8, "zzzzzzzz", "hippo", 8), + Arguments.of(1, "hello", "hallo", 1), + + Arguments.of(Integer.MAX_VALUE, "frog", "fog", 1), + Arguments.of(Integer.MAX_VALUE, "fly", "ant", 3), + Arguments.of(Integer.MAX_VALUE, "elephant", "hippo", 7), + Arguments.of(Integer.MAX_VALUE, "hippo", "elephant", 7), + Arguments.of(Integer.MAX_VALUE, "hippo", "zzzzzzzz", 8), + Arguments.of(Integer.MAX_VALUE, "zzzzzzzz", "hippo", 8), + Arguments.of(Integer.MAX_VALUE, "hello", "hallo", 1)); + } + + @ParameterizedTest + @MethodSource("parameters") + public void test(final Integer threshold, final CharSequence left, final CharSequence right, + final Integer distance) { + final LevenshteinDistance metric = new LevenshteinDistance(threshold); + assertThat(metric.apply(left, right)).isEqualTo(distance); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/ParameterizedSimilarityScoreFromTest.java b/sources/src/test/java/org/apache/commons/text/similarity/ParameterizedSimilarityScoreFromTest.java new file mode 100644 index 0000000..5feda36 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/ParameterizedSimilarityScoreFromTest.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests {@link SimilarityScoreFrom}. + * + * @param The {@link SimilarityScore} return type. + */ +public class ParameterizedSimilarityScoreFromTest { + + public static Stream parameters() { + return Stream.of( + Arguments.of(new LevenshteinDistance(), "elephant", "hippo", 7), + Arguments.of(new LevenshteinDistance(), "hippo", "elephant", 7), + Arguments.of(new LevenshteinDistance(), "hippo", "zzzzzzzz", 8), + + Arguments.of( + (SimilarityScore) (left, right) -> left == right + || left != null && left.equals(right), + "Bob's your uncle.", + "Every good boy does fine.", + false + )); + } + + @ParameterizedTest + @MethodSource("parameters") + public void test(final SimilarityScore similarityScore, final CharSequence left, final CharSequence right, + final R distance) { + final SimilarityScoreFrom similarityScoreFrom = new SimilarityScoreFrom<>(similarityScore, left); + assertThat(similarityScoreFrom.apply(right)).isEqualTo(distance); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/SimilarityScoreFromTest.java b/sources/src/test/java/org/apache/commons/text/similarity/SimilarityScoreFromTest.java new file mode 100644 index 0000000..24eac1d --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/SimilarityScoreFromTest.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import org.junit.jupiter.api.Test; + +public class SimilarityScoreFromTest { + + @Test + public void testApply() { + final LongestCommonSubsequence longestCommonSubsequence = new LongestCommonSubsequence(); + final SimilarityScoreFrom similarityScoreFrom = + new SimilarityScoreFrom<>(longestCommonSubsequence, "asdf"); + + assertThat(similarityScoreFrom.apply("s")).isEqualTo(1); + } + + @Test + public void testFailsToCreateSimilarityScoreFromThrowsIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> new SimilarityScoreFrom<>(null, "")); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/similarity/StringMetricFromTest.java b/sources/src/test/java/org/apache/commons/text/similarity/StringMetricFromTest.java new file mode 100644 index 0000000..7f0c33f --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/similarity/StringMetricFromTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.similarity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link EditDistanceFrom}. + */ +public class StringMetricFromTest { + + @Test + public void testEquivalence() { + final EditDistance metric = new LevenshteinDistance(); + final String left = "Apache"; + final String right = "a patchy"; + final Integer distance = 4; + final EditDistanceFrom metricFrom = new EditDistanceFrom<>(metric, left); + + assertThat(metricFrom.apply(right)).isEqualTo(distance); + assertThat(metricFrom.apply(right)).isEqualTo(metric.apply(left, right)); + } + + @Test + public void testJavadocExample() { + final EditDistance metric = new LevenshteinDistance(); + final String target = "Apache"; + final EditDistanceFrom metricFrom = + new EditDistanceFrom<>(metric, target); + String mostSimilar = null; + Integer shortestDistance = null; + + for (final String test : new String[] {"Appaloosa", "a patchy", "apple" }) { + final Integer distance = metricFrom.apply(test); + if (shortestDistance == null || distance < shortestDistance) { + shortestDistance = distance; + mostSimilar = test; + } + } + assertThat(mostSimilar).isEqualTo("a patchy"); + assertThat(shortestDistance).isEqualTo(4); + } + + @Test + public void testMissingMetric() { + assertThatIllegalArgumentException().isThrownBy(() -> new EditDistanceFrom(null, "no go")); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/translate/AggregateTranslatorTest.java b/sources/src/test/java/org/apache/commons/text/translate/AggregateTranslatorTest.java new file mode 100644 index 0000000..a3bdc9d --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/translate/AggregateTranslatorTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.translate; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link AggregateTranslator}. + */ +public class AggregateTranslatorTest { + + @Test + public void testNonNull() throws IOException { + final Map oneTwoMap = new HashMap<>(); + oneTwoMap.put("one", "two"); + final Map threeFourMap = new HashMap<>(); + threeFourMap.put("three", "four"); + final CharSequenceTranslator translator1 = new LookupTranslator(oneTwoMap); + final CharSequenceTranslator translator2 = new LookupTranslator(threeFourMap); + final AggregateTranslator subject = new AggregateTranslator(translator1, translator2); + final StringWriter out1 = new StringWriter(); + final int result1 = subject.translate(new StringBuffer("one"), 0, out1); + assertThat(result1).as("Incorrect code point consumption").isEqualTo(3); + assertThat(out1.toString()).as("Incorrect value").isEqualTo("two"); + final StringWriter out2 = new StringWriter(); + final int result2 = subject.translate(new StringBuffer("three"), 0, out2); + assertThat(result2).as("Incorrect code point consumption").isEqualTo(5); + assertThat(out2.toString()).as("Incorrect value").isEqualTo("four"); + } + + @Test + public void testNullConstructor() { + final String testString = "foo"; + final AggregateTranslator subject = new AggregateTranslator((CharSequenceTranslator[]) null); + assertThat(subject.translate(testString)).isEqualTo(testString); + } + + @Test + public void testNullVarargConstructor() { + final String testString = "foo"; + final AggregateTranslator subject = new AggregateTranslator((CharSequenceTranslator) null); + assertThat(subject.translate(testString)).isEqualTo(testString); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/translate/CodePointTranslatorTest.java b/sources/src/test/java/org/apache/commons/text/translate/CodePointTranslatorTest.java new file mode 100644 index 0000000..e7fe55a --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/translate/CodePointTranslatorTest.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.translate; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.io.PipedReader; +import java.io.PipedWriter; + +import org.junit.jupiter.api.Test; + +public class CodePointTranslatorTest { + + @Test + public void testAboveReturningNonNull() throws IOException { + final NumericEntityEscaper numericEntityEscaper = NumericEntityEscaper.above(0); + final UnicodeEscaper unicodeEscaper = new UnicodeEscaper(); + final String string = unicodeEscaper.toUtf16Escape(0); + try (PipedReader pipedReader = new PipedReader(); PipedWriter pipedWriter = new PipedWriter(pipedReader)) { + assertThat(numericEntityEscaper.translate(string, 0, pipedWriter)).isEqualTo(1); + } + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/translate/CsvTranslatorsTest.java b/sources/src/test/java/org/apache/commons/text/translate/CsvTranslatorsTest.java new file mode 100644 index 0000000..49b1db7 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/translate/CsvTranslatorsTest.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.translate; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; + +import org.apache.commons.lang3.CharUtils; +import org.junit.jupiter.api.Test; + +public class CsvTranslatorsTest { + + @Test + public void csvEscaperCommaTest() throws IOException { + final CsvTranslators.CsvEscaper escaper = new CsvTranslators.CsvEscaper(); + final Writer writer = new StringWriter(); + final String input = "hi,this,is,a,test"; + escaper.translateWhole(input, writer); + final String data = writer.toString(); + assertThat(data).isEqualTo("\"hi,this,is,a,test\""); + } + + @Test + public void csvEscaperCRTest() throws IOException { + final CsvTranslators.CsvEscaper escaper = new CsvTranslators.CsvEscaper(); + final Writer writer = new StringWriter(); + final String input = "hi,this,is,a,CR,test" + String.valueOf(CharUtils.CR); + escaper.translateWhole(input, writer); + final String data = writer.toString(); + assertThat(data).isEqualTo("\"hi,this,is,a,CR,test" + String.valueOf(CharUtils.CR) + "\""); + } + + @Test + public void csvEscaperLFTest() throws IOException { + final CsvTranslators.CsvEscaper escaper = new CsvTranslators.CsvEscaper(); + final Writer writer = new StringWriter(); + final String input = "hi,this,is,a,LF,test" + String.valueOf(CharUtils.LF); + escaper.translateWhole(input, writer); + final String data = writer.toString(); + assertThat(data).isEqualTo("\"hi,this,is,a,LF,test" + String.valueOf(CharUtils.LF) + "\""); + } + + @Test + public void csvEscaperPlaneTextTest() throws IOException { + final CsvTranslators.CsvEscaper escaper = new CsvTranslators.CsvEscaper(); + final Writer writer = new StringWriter(); + final String input = "hi this is just a plane text nothing to do with csv!"; + escaper.translateWhole(input, writer); + final String data = writer.toString(); + assertThat(input).isEqualTo(data); + } + + @Test + public void csvEscaperQuoteTest() throws IOException { + final CsvTranslators.CsvEscaper escaper = new CsvTranslators.CsvEscaper(); + final Writer writer = new StringWriter(); + final String input = "hi,this,is,a,\"quote,test"; + escaper.translateWhole(input, writer); + final String data = writer.toString(); + assertThat(data).isEqualTo("\"hi,this,is,a,\"\"quote,test\""); + } + + @Test + public void csvUnEscaperPlaneTextTest() throws IOException { + final CsvTranslators.CsvUnescaper escaper = new CsvTranslators.CsvUnescaper(); + final Writer writer = new StringWriter(); + final String input = "hi,this,is,unescape,test"; + escaper.translateWhole(input, writer); + final String data = writer.toString(); + assertThat(data).isEqualTo("hi,this,is,unescape,test"); + } + + @Test + public void csvUnEscaperTest1() throws IOException { + final CsvTranslators.CsvUnescaper escaper = new CsvTranslators.CsvUnescaper(); + final Writer writer = new StringWriter(); + final String input = "\"hi,this,is,unescape,test\""; + escaper.translateWhole(input, writer); + final String data = writer.toString(); + assertThat(data).isEqualTo("hi,this,is,unescape,test"); + } + + @Test + public void csvUnEscaperTest2() throws IOException { + final CsvTranslators.CsvUnescaper escaper = new CsvTranslators.CsvUnescaper(); + final Writer writer = new StringWriter(); + final String input = "\"hi,this,is,unescape,test"; + escaper.translateWhole(input, writer); + final String data = writer.toString(); + assertThat(input).isEqualTo(data); + } + + @Test + public void csvUnEscaperTest3() throws IOException { + final CsvTranslators.CsvUnescaper escaper = new CsvTranslators.CsvUnescaper(); + final Writer writer = new StringWriter(); + final String input = "hi,this,is,unescape,test\""; + escaper.translateWhole(input, writer); + final String data = writer.toString(); + assertThat(input).isEqualTo(data); + } + + @Test + public void csvUnEscaperTest4() throws IOException { + final CsvTranslators.CsvUnescaper escaper = new CsvTranslators.CsvUnescaper(); + final Writer writer = new StringWriter(); + final String input = "\"hi,this,is,\"unescape,test\""; + escaper.translateWhole(input, writer); + final String data = writer.toString(); + assertThat(data).isEqualTo("hi,this,is,\"unescape,test"); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/translate/EntityArraysTest.java b/sources/src/test/java/org/apache/commons/text/translate/EntityArraysTest.java new file mode 100644 index 0000000..d9b45f6 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/translate/EntityArraysTest.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text.translate; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link EntityArrays}. + */ +public class EntityArraysTest { + + @Test + public void testAposMap() { + testEscapeVsUnescapeMaps(EntityArrays.APOS_ESCAPE, EntityArrays.APOS_UNESCAPE); + } + + @Test + public void testBasicMap() { + testEscapeVsUnescapeMaps(EntityArrays.BASIC_ESCAPE, EntityArrays.BASIC_UNESCAPE); + } + + @Test + public void testConstructorExists() { + new EntityArrays(); + } + + private void testEscapeVsUnescapeMaps(final Map escapeMap, + final Map unescapeMap) { + for (final CharSequence escapeKey : escapeMap.keySet()) { + for (final CharSequence unescapeKey : unescapeMap.keySet()) { + if (escapeKey == unescapeMap.get(unescapeKey)) { + assertThat(unescapeKey).isEqualTo(escapeMap.get(escapeKey)); + } + } + } + } + + // LANG-659, LANG-658 - avoid duplicate entries + @Test + public void testForDuplicatedDeclaredMapKeys() throws Exception { + final String packageDirectory = EntityArraysTest.class.getPackage().getName().replace(".", "/"); + try (BufferedReader br = new BufferedReader(new FileReader("src/main/java/" + packageDirectory + + "/EntityArrays.java"))) { + String line; + int mapDeclarationCounter = 0; + while ((line = br.readLine()) != null) { + //Start with map declaration and count put lines + if (line.contains("new HashMap<>();")) { + mapDeclarationCounter = 0; + } else if (line.contains(".put(")) { + mapDeclarationCounter++; + } else if (line.contains("Collections.unmodifiableMap(initialMap);")) { + final String mapVariableName = line.split("=")[0].trim(); + @SuppressWarnings("unchecked") // This is test code + final + Map mapValue = (Map) + EntityArrays.class.getDeclaredField(mapVariableName).get(EntityArrays.class); + // Validate that we are not inserting into the same key twice in the map declaration. If this, + // indeed was the case the keySet().size() would be smaller than the number of put() statements + assertThat(mapValue.size()).isEqualTo(mapDeclarationCounter); + } + } + } + } + + @Test + public void testForDuplicateDeclaredMapValuesAposMap() { + assertThat(EntityArrays.APOS_ESCAPE.keySet()).hasSameSizeAs( + EntityArrays.APOS_UNESCAPE.keySet()); + } + + @Test + public void testForDuplicateDeclaredMapValuesBasicMap() { + assertThat(EntityArrays.BASIC_ESCAPE.keySet()).hasSameSizeAs( + EntityArrays.BASIC_UNESCAPE.keySet()); + } + + @Test + public void testForDuplicateDeclaredMapValuesHtml40ExtendedMap() { + assertThat(EntityArrays.HTML40_EXTENDED_ESCAPE.keySet()).hasSameSizeAs( + EntityArrays.HTML40_EXTENDED_UNESCAPE.keySet()); + } + + @Test + public void testForDuplicateDeclaredMapValuesISO8859Map() { + assertThat(EntityArrays.ISO8859_1_ESCAPE.keySet()).hasSameSizeAs( + EntityArrays.ISO8859_1_UNESCAPE.keySet()); + } + + @Test + public void testForDuplicateDeclaredMapValuesJavaCtrlCharsMap() { + assertThat(EntityArrays.JAVA_CTRL_CHARS_ESCAPE.keySet()).hasSameSizeAs( + EntityArrays.JAVA_CTRL_CHARS_UNESCAPE.keySet()); + } + + @Test + public void testHtml40ExtendedMap() { + testEscapeVsUnescapeMaps(EntityArrays.HTML40_EXTENDED_ESCAPE, EntityArrays.HTML40_EXTENDED_UNESCAPE); + } + + @Test + public void testISO8859Map() { + testEscapeVsUnescapeMaps(EntityArrays.ISO8859_1_ESCAPE, EntityArrays.ISO8859_1_UNESCAPE); + } + + @Test + public void testJavaCtrlCharsMap() { + testEscapeVsUnescapeMaps(EntityArrays.JAVA_CTRL_CHARS_ESCAPE, EntityArrays.JAVA_CTRL_CHARS_UNESCAPE); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/translate/JavaUnicodeEscaperTest.java b/sources/src/test/java/org/apache/commons/text/translate/JavaUnicodeEscaperTest.java new file mode 100644 index 0000000..98a6a9a --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/translate/JavaUnicodeEscaperTest.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.translate; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link JavaUnicodeEscaper}. + */ +public class JavaUnicodeEscaperTest { + + @Test + public void testAbove() { + final JavaUnicodeEscaper jue = JavaUnicodeEscaper.above('F'); + + final String input = "ADFGZ"; + final String result = jue.translate(input); + assertThat(result).as("Failed to escape Unicode characters via the above method") + .isEqualTo("ADF\\u0047\\u005A"); + } + + @Test + public void testBelow() { + final JavaUnicodeEscaper jue = JavaUnicodeEscaper.below('F'); + + final String input = "ADFGZ"; + final String result = jue.translate(input); + assertThat(result).as("Failed to escape Unicode characters via the below method") + .isEqualTo("\\u0041\\u0044FGZ"); + } + + @Test + public void testBetween() { + final JavaUnicodeEscaper jue = JavaUnicodeEscaper.between('F', 'L'); + + final String input = "ADFGZ"; + final String result = jue.translate(input); + assertThat(result).as("Failed to escape Unicode characters via the between method") + .isEqualTo("AD\\u0046\\u0047Z"); + } + + @Test + public void testToUtf16Escape() { + final JavaUnicodeEscaper jue = JavaUnicodeEscaper.below('F'); + // According to https://en.wikipedia.org/wiki/UTF-16#Code_points_U.2B10000..U.2B10FFFF, + // Character ?, U+24B62, Binary Code Point 0010 0100 1011 0110 0010, + // Binary UTF-167 1101 1000 0101 0010 1101 1111 0110 0010, UTF-16 Hex Code Units D852 DF62 + final String encoding = jue.toUtf16Escape(Integer.parseInt("024B62", 16)); + assertThat(encoding).isEqualTo("\\uD852\\uDF62"); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/translate/LookupTranslatorTest.java b/sources/src/test/java/org/apache/commons/text/translate/LookupTranslatorTest.java new file mode 100644 index 0000000..548434f --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/translate/LookupTranslatorTest.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text.translate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import java.io.IOException; +import java.io.StringWriter; +import java.security.InvalidParameterException; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link LookupTranslator}. + */ +public class LookupTranslatorTest { + + @Test + public void testBasicLookup() throws IOException { + final Map translatorMap = new HashMap<>(); + translatorMap.put("one", "two"); + final LookupTranslator lt = new LookupTranslator(translatorMap); + final StringWriter out = new StringWriter(); + final int result = lt.translate("one", 0, out); + assertThat(result).as("Incorrect code point consumption").isEqualTo(3); + assertThat(out.toString()).as("Incorrect value").isEqualTo("two"); + } + + @Test + public void testFailsToCreateLookupTranslatorThrowsInvalidParameterException() { + assertThatExceptionOfType(InvalidParameterException.class).isThrownBy(() -> new LookupTranslator(null)); + } + + // Tests: https://issues.apache.org/jira/browse/LANG-882 + @Test + public void testLang882() throws IOException { + final Map translatorMap = new HashMap<>(); + translatorMap.put(new StringBuffer("one"), new StringBuffer("two")); + final LookupTranslator lt = new LookupTranslator(translatorMap); + final StringWriter out = new StringWriter(); + final int result = lt.translate(new StringBuffer("one"), 0, out); + assertThat(result).as("Incorrect code point consumption").isEqualTo(3); + assertThat(out.toString()).as("Incorrect value").isEqualTo("two"); + } + + @Test + public void testTranslateSupplementaryCharacter() { + /* Key: string with Mathematical double-struck capital A (U+1D538) */ + final String symbol = new StringBuilder().appendCodePoint(0x1D538).toString(); + /* Map U+1D538 to "A" */ + final Map map = new HashMap<>(); + map.put(symbol, "A"); + final LookupTranslator translator = new LookupTranslator(map); + final String translated = translator.translate(symbol + "=A"); + /* we should get "A=A". */ + assertThat(translated).as("Incorrect value").isEqualTo("A=A"); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/translate/NumericEntityEscaperTest.java b/sources/src/test/java/org/apache/commons/text/translate/NumericEntityEscaperTest.java new file mode 100644 index 0000000..ff0cddf --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/translate/NumericEntityEscaperTest.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text.translate; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link NumericEntityEscaper}. + */ +public class NumericEntityEscaperTest { + + @Test + public void testAbove() { + final NumericEntityEscaper nee = NumericEntityEscaper.above('F'); + + final String input = "ADFGZ"; + final String result = nee.translate(input); + assertThat(result).as("Failed to escape numeric entities via the above method").isEqualTo("ADFGZ"); + } + + @Test + public void testBelow() { + final NumericEntityEscaper nee = NumericEntityEscaper.below('F'); + + final String input = "ADFGZ"; + final String result = nee.translate(input); + assertThat(result).as("Failed to escape numeric entities via the below method").isEqualTo("ADFGZ"); + } + + @Test + public void testBetween() { + final NumericEntityEscaper nee = NumericEntityEscaper.between('F', 'L'); + + final String input = "ADFGZ"; + final String result = nee.translate(input); + assertThat(result).as("Failed to escape numeric entities via the between method").isEqualTo("ADFGZ"); + } + + /** See LANG-617 */ + @Test + public void testSupplementary() { + final NumericEntityEscaper nee = new NumericEntityEscaper(); + final String input = "\uD803\uDC22"; + final String expected = "𐰢"; + + final String result = nee.translate(input); + assertThat(result).as("Failed to escape numeric entities supplementary characters").isEqualTo(expected); + + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/translate/NumericEntityUnescaperTest.java b/sources/src/test/java/org/apache/commons/text/translate/NumericEntityUnescaperTest.java new file mode 100644 index 0000000..5e9e86e --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/translate/NumericEntityUnescaperTest.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text.translate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link NumericEntityUnescaper}. + */ +public class NumericEntityUnescaperTest { + + @Test + public void testCreatesNumericEntityUnescaperOne() { + final NumericEntityUnescaper.OPTION[] numericEntityUnescaperOPTIONArray = {}; + final NumericEntityUnescaper numericEntityUnescaper = + new NumericEntityUnescaper(numericEntityUnescaperOPTIONArray); + + assertThat(numericEntityUnescaper.translate("2|y|O7y`&#uVWj")).isEqualTo("2|y|O7y`&#uVWj"); + } + + @Test + public void testCreatesNumericEntityUnescaperTwo() { + final NumericEntityUnescaper.OPTION[] numericEntityUnescaperOPTIONArray = {}; + final NumericEntityUnescaper numericEntityUnescaper = + new NumericEntityUnescaper(numericEntityUnescaperOPTIONArray); + + assertThat(numericEntityUnescaper.translate("Ws2v8|O=7NR&#cB")).isEqualTo("Ws2v8|O=7NR&#cB"); + } + + @Test + public void testOutOfBounds() { + final NumericEntityUnescaper neu = new NumericEntityUnescaper(); + + assertThat(neu.translate("Test &")).as("Failed to ignore when last character is &").isEqualTo("Test &"); + assertThat(neu.translate("Test &#")).as("Failed to ignore when last character is &").isEqualTo("Test &#"); + assertThat(neu.translate("Test &#x")).as("Failed to ignore when last character is &").isEqualTo("Test &#x"); + assertThat(neu.translate("Test &#X")).as("Failed to ignore when last character is &").isEqualTo("Test &#X"); + } + + @Test + public void testSupplementaryUnescaping() { + final NumericEntityUnescaper neu = new NumericEntityUnescaper(); + final String input = "𐰢"; + final String expected = "\uD803\uDC22"; + + final String result = neu.translate(input); + assertThat(result).as("Failed to unescape numeric entities supplementary characters").isEqualTo(expected); + } + + @Test + public void testUnfinishedEntity() { + // parse it + NumericEntityUnescaper neu = new NumericEntityUnescaper(NumericEntityUnescaper.OPTION.semiColonOptional); + String input = "Test 0 not test"; + String expected = "Test \u0030 not test"; + + String result = neu.translate(input); + assertThat(result).as("Failed to support unfinished entities (i.e. missing semicolon)").isEqualTo(expected); + + // ignore it + neu = new NumericEntityUnescaper(); + input = "Test 0 not test"; + expected = input; + + result = neu.translate(input); + assertThat(result).as("Failed to ignore unfinished entities (i.e. missing semicolon)").isEqualTo(expected); + + // fail it + neu = new NumericEntityUnescaper(NumericEntityUnescaper.OPTION.errorIfNoSemiColon); + input = "Test 0 not test"; + + try { + result = neu.translate(input); + fail("IllegalArgumentException expected"); + } catch (final IllegalArgumentException iae) { + // expected + } + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/translate/OctalUnescaperTest.java b/sources/src/test/java/org/apache/commons/text/translate/OctalUnescaperTest.java new file mode 100644 index 0000000..a6f60ad --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/translate/OctalUnescaperTest.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text.translate; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link OctalUnescaper}. + */ +public class OctalUnescaperTest { + + @Test + public void testBetween() { + final OctalUnescaper oue = new OctalUnescaper(); //.between("1", "377"); + + String input = "\\45"; + String result = oue.translate(input); + assertThat(result).as("Failed to unescape octal characters via the between method").isEqualTo("\45"); + + input = "\\377"; + result = oue.translate(input); + assertThat(result).as("Failed to unescape octal characters via the between method").isEqualTo("\377"); + + input = "\\377 and"; + result = oue.translate(input); + assertThat(result).as("Failed to unescape octal characters via the between method").isEqualTo("\377 and"); + + input = "\\378 and"; + result = oue.translate(input); + assertThat(result).as("Failed to unescape octal characters via the between method").isEqualTo("\37" + "8 and"); + + input = "\\378"; + result = oue.translate(input); + assertThat(result).as("Failed to unescape octal characters via the between method").isEqualTo("\37" + "8"); + + input = "\\1"; + result = oue.translate(input); + assertThat(result).as("Failed to unescape octal characters via the between method").isEqualTo("\1"); + + input = "\\036"; + result = oue.translate(input); + assertThat(result).as("Failed to unescape octal characters via the between method").isEqualTo("\036"); + + input = "\\0365"; + result = oue.translate(input); + assertThat(result).as("Failed to unescape octal characters via the between method").isEqualTo("\036" + "5"); + + input = "\\003"; + result = oue.translate(input); + assertThat(result).as("Failed to unescape octal characters via the between method").isEqualTo("\003"); + + input = "\\0003"; + result = oue.translate(input); + assertThat(result).as("Failed to unescape octal characters via the between method").isEqualTo("\000" + "3"); + + input = "\\279"; + result = oue.translate(input); + assertThat(result).as("Failed to unescape octal characters via the between method").isEqualTo("\279"); + + input = "\\999"; + result = oue.translate(input); + assertThat(result).as("Failed to ignore an out of range octal character via the between method") + .isEqualTo("\\999"); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/translate/SinglePassTranslatorTest.java b/sources/src/test/java/org/apache/commons/text/translate/SinglePassTranslatorTest.java new file mode 100644 index 0000000..3d2b119 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/translate/SinglePassTranslatorTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.translate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit test for {@link SinglePassTranslator}. + */ +public class SinglePassTranslatorTest { + + private final SinglePassTranslator dummyTranslator = new SinglePassTranslator() { + + @Override + void translateWhole(final CharSequence input, final Writer writer) throws IOException { + // noop + } + }; + + private StringWriter out; + + @BeforeEach + public void before() { + out = new StringWriter(); + } + + @Test + public void codePointsAreReturned() throws Exception { + assertThat(dummyTranslator.translate("", 0, out)).isEqualTo(0); + assertThat(dummyTranslator.translate("abc", 0, out)).isEqualTo(3); + assertThat(dummyTranslator.translate("abcdefg", 0, out)).isEqualTo(7); + } + + @Test + public void indexIsValidated() { + assertThatIllegalArgumentException().isThrownBy(() -> dummyTranslator.translate("abc", 1, out)); + } + + @Test + public void testTranslateThrowsIllegalArgumentException() { + assertThatIllegalArgumentException().isThrownBy(() -> dummyTranslator.translate("(,Fk", 647, null)); + } + +} diff --git a/sources/src/test/java/org/apache/commons/text/translate/UnicodeEscaperTest.java b/sources/src/test/java/org/apache/commons/text/translate/UnicodeEscaperTest.java new file mode 100644 index 0000000..6e0f9b1 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/translate/UnicodeEscaperTest.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text.translate; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link UnicodeEscaper}. + */ +public class UnicodeEscaperTest { + + @Test + public void testAbove() { + final UnicodeEscaper ue = UnicodeEscaper.above('F'); + + final String input = "ADFGZ"; + final String result = ue.translate(input); + assertThat(result).as("Failed to escape Unicode characters via the above method") + .isEqualTo("ADF\\u0047\\u005A"); + } + + @Test + public void testBelow() { + final UnicodeEscaper ue = UnicodeEscaper.below('F'); + + final String input = "ADFGZ"; + final String result = ue.translate(input); + assertThat(result).as("Failed to escape Unicode characters via the below method") + .isEqualTo("\\u0041\\u0044FGZ"); + } + + @Test + public void testBetween() { + final UnicodeEscaper ue = UnicodeEscaper.between('F', 'L'); + + final String input = "ADFGZ"; + final String result = ue.translate(input); + assertThat(result).as("Failed to escape Unicode characters via the between method") + .isEqualTo("AD\\u0046\\u0047Z"); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/translate/UnicodeUnescaperTest.java b/sources/src/test/java/org/apache/commons/text/translate/UnicodeUnescaperTest.java new file mode 100644 index 0000000..4a72994 --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/translate/UnicodeUnescaperTest.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.text.translate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link UnicodeEscaper}. + */ +public class UnicodeUnescaperTest { + + @Test + public void testLessThanFour() { + final UnicodeUnescaper uu = new UnicodeUnescaper(); + + final String input = "\\0047\\u006"; + assertThrows(IllegalArgumentException.class, () -> uu.translate(input)); + } + + // Requested in LANG-507 + @Test + public void testUPlus() { + final UnicodeUnescaper uu = new UnicodeUnescaper(); + + final String input = "\\u+0047"; + assertThat(uu.translate(input)).as("Failed to unescape Unicode characters with 'u+' notation").isEqualTo("G"); + } + + @Test + public void testUuuuu() { + final UnicodeUnescaper uu = new UnicodeUnescaper(); + + final String input = "\\uuuuuuuu0047"; + final String result = uu.translate(input); + assertThat(result).as("Failed to unescape Unicode characters with many 'u' characters").isEqualTo("G"); + } +} diff --git a/sources/src/test/java/org/apache/commons/text/translate/UnicodeUnpairedSurrogateRemoverTest.java b/sources/src/test/java/org/apache/commons/text/translate/UnicodeUnpairedSurrogateRemoverTest.java new file mode 100644 index 0000000..20e3c8e --- /dev/null +++ b/sources/src/test/java/org/apache/commons/text/translate/UnicodeUnpairedSurrogateRemoverTest.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.text.translate; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.CharArrayWriter; +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@link UnicodeUnpairedSurrogateRemover}. + */ +public class UnicodeUnpairedSurrogateRemoverTest { + final UnicodeUnpairedSurrogateRemover subject = new UnicodeUnpairedSurrogateRemover(); + final CharArrayWriter writer = new CharArrayWriter(); // nothing is ever written to it + + @Test + public void testInvalidCharacters() throws IOException { + assertThat(subject.translate(0xd800, writer)).isTrue(); + assertThat(subject.translate(0xdfff, writer)).isTrue(); + assertThat(writer.size()).isZero(); + } + + @Test + public void testValidCharacters() throws IOException { + assertThat(subject.translate(0xd7ff, writer)).isFalse(); + assertThat(subject.translate(0xe000, writer)).isFalse(); + assertThat(writer.size()).isZero(); + } +} + diff --git a/sources/src/test/resources/org/apache/commons/text/document.properties b/sources/src/test/resources/org/apache/commons/text/document.properties new file mode 100644 index 0000000..2f3bc8c --- /dev/null +++ b/sources/src/test/resources/org/apache/commons/text/document.properties @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +mykey = Hello World! diff --git a/sources/src/test/resources/org/apache/commons/text/document.xml b/sources/src/test/resources/org/apache/commons/text/document.xml new file mode 100644 index 0000000..f1310f4 --- /dev/null +++ b/sources/src/test/resources/org/apache/commons/text/document.xml @@ -0,0 +1,24 @@ + + + + + + Hello World! + + + diff --git a/sources/src/test/resources/org/apache/commons/text/example/testResourceBundleLookup.properties b/sources/src/test/resources/org/apache/commons/text/example/testResourceBundleLookup.properties new file mode 100644 index 0000000..822001e --- /dev/null +++ b/sources/src/test/resources/org/apache/commons/text/example/testResourceBundleLookup.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +key = value +number = 2 +keyWithMissingKey = ${missingKey:-defaultValue} +mykey = Hello World! diff --git a/sources/src/test/resources/org/apache/commons/text/lcs-perf-analysis-inputs.csv b/sources/src/test/resources/org/apache/commons/text/lcs-perf-analysis-inputs.csv new file mode 100644 index 0000000..d000a26 --- /dev/null +++ b/sources/src/test/resources/org/apache/commons/text/lcs-perf-analysis-inputs.csv @@ -0,0 +1,5 @@ +"This code is free software; you can redistribute it and/or modify it","under the terms of the GNU General Public License version 2 only, as" +"You should have received a copy of the GNU General Public License version","2 along with this work; if not, write to the Free Software Foundation," +"Here, the field iterations will be populated with appropriate values from the @Param annotation by the JMH when it is passed to the benchmark method. The @Setup annotated method is invoked before each invocation of the benchmark and creates a new Hasher ensuring isolation. When the execution is finished, we'll get a result similar to the one below: When running microbenchmarks, it's very important to be aware of optimizations. Otherwise, they may affect the","benchmark results in a very misleading way. To make matters a bit more concrete, let's consider an example: We expect object allocation costs more than doing nothing at all. However, if we run the benchmarks: Apparently finding a place in the TLAB, creating and initializing an object is almost free! Just by looking at these numbers, we should know that something does not quite add up here." +"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. This code is free software; you can redistribute it and/or modify it published by the Free Software Foundation. Oracle designates this This code is distributed in the hope that it will be useful, but WITHOUT FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License You should have received a copy of the GNU General Public License version","Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? under the terms of the GNU General Public License version 2 only, as particular file as subject to the *Classpath* exception as provided ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or version 2 for more details (a copy is included in the LICENSE file that 2 along with this work; if not, write to the Free Software Foundation," +"But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure? Here, the field iterations will be populated with appropriate values from the @Param annotation by the JMH when it is passed to the benchmark method. The @Setup annotated method is invoked before each invocation of the benchmark and creates a new Hasher ensuring isolation. When the execution is finished, we'll get a result similar to the one below: When running microbenchmarks, it's very important to be aware of optimizations. Otherwise, they may affect the","At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat. benchmark results in a very misleading way. To make matters a bit more concrete, let's consider an example: We expect object allocation costs more than doing nothing at all. However, if we run the benchmarks: Apparently finding a place in the TLAB, creating and initializing an object is almost free! Just by looking at these numbers, we should know that something does not quite add up here." diff --git a/sources/src/test/resources/org/apache/commons/text/stringEscapeUtilsTestData.txt b/sources/src/test/resources/org/apache/commons/text/stringEscapeUtilsTestData.txt new file mode 100644 index 0000000..4e75f67 --- /dev/null +++ b/sources/src/test/resources/org/apache/commons/text/stringEscapeUtilsTestData.txt @@ -0,0 +1 @@ +[{"geonameFeatureClass":"L","values":{"eu":"Mundua","ro":"Pamânt","it":"Globo","ca":"el món","tr":"Yeryüzü","no":"Jorden","hu":"Föld","lv":"Zeme","de":"Welt","el":"Υδρόγειος","fi":"Maa","la":"Terra","fr":"Monde","eo":"Mondo","en":"World","ru":"Земля","es":"el planeta","nl":"Aarde"},"geonameFeatureCode":"AREA","_id":32,"name":"","auto":true,"type":"GEO","geonameId":6295630,"valueCode":""},{"geonameFeatureClass":"L","values":{"ro":"Europa","zh":"欧洲","ca":"Europa","vi":"Châu Âu","tr":"Avrupa","no":"Europa","hu":"Európa","lv":"Eiropa","hi":"यूरोप","lt":"Europa","bs":"Evropa","ga":"an Eoraip","th":"ยุโรป","id":"Eropa","de":"Europa","fi":"Eurooppa","fr":"Europe","sv":"Europa","bg":"Европа","da":"Europa","eu":"Europa","is":"Evrópa","it":"Europa","cy":"Ewrop","ar":"أوروبا","se":"Eurohpá","he":"אירופה","cs":"Evropa","el":"Ευρώπη","nb":"Europa","pl":"Europa","la":"Europa","pt":"Europa","eo":"Eŭropo","en":"Europe","ru":"Европа","es":"Europa","ja":"ヨーロッパ","nl":"Europa"},"geonameFeatureCode":"CONT","_id":33,"name":"","auto":true,"type":"GEO","geonameId":6255148,"valueCode":""},{"geonameFeatureClass":"A","values":{"no":"Spania","nn":"Spania","fy":"Spanje","gd":"An Spàinn","ga":"An Spáinn","oc":"Espanha","arc":"ܐܣܦܢܝܐ","fi":"Espanja","fr":"Espagne","fo":"Spania","udm":"Испания","os":"Испани","he":"ספרד","gn":"Epaña","gl":"España","gv":"Yn Spaainey","pl":"Hiszpania","gu":"સ્પેઇન","lo":"ສະເປນ","ln":"Espania","vi":"Tây Ban Nha","dz":"Spain","pms":"Spagna","lv":"Spānija","lt":"Ispanija","vo":"Spanyän","de":"Spanien","mg":"Espaina","fur":"Spagne","mk":"Шпанија","ml":"സ്പെയ്ന്\u200D","ceb":"Espanya","mi":"Pāniora","uk":"Іспанія","eu":"Espainia","mr":"स्पेन","ug":"ئىسپانىيە","mt":"Spanja","ms":"Sepanyol","ur":"سپین","fa":"اسپانیا","ty":"Paniora","new":"स्पेन","na":"Pain","el":"Ισπανία","nb":"Spania","ne":"स्पेन","vls":"Spanje","eo":"Hispanio","en":"Kingdom of Spain","et":"Hispaania","es":"la Madre Patria","nl":"Spanje","vec":"Spagna","to":"Sepeni","ca":"Espanya","tl":"Espanya","tr":"İspanya","tg":"Испониё","haw":"Sepania","bs":"Španija","br":"Spagn","th":"ประเทศสเปน","bn":"স্পেন","bo":"སི་པན།","ta":"ஸ்பெயின்","sv":"Spanien","bg":"Испания","ka":"ესპანეთი","st":"Spain","sw":"Hispania","be":"Іспанія","kw":"Spayn","sl":"Španija","sk":"Španielsko","da":"Spanien","ang":"Spēonland","nds":"Spanien","ks":"Spēna","so":"Isbeyn","ku":"Spanya","sr":"Шпанија","sq":"Spanja","ko":"에스파냐","sc":"Ispagna","cy":"Sbaen","se":"Espánjja","sh":"Španija","cv":"Испани","km":"អេស្ប៉ាញ","cs":"Španělsko","li":"Spanje","co":"Spagna","default":"Spain","jbo":"sangu'e","la":"Hesperia","ru":"Испания","lb":"Spuenien","sco":"Spain","tet":"España","scn":"Spagna","hr":"Španjolska","zh":"西班牙","ro":"Spania","rm":"Spagna","ht":"Espay","hu":"Spanyolország","ast":"España","hi":"स्पेन","hsb":"Španiska","nah":"Caxtillān","war":"Espanya","lad":"Espanya","id":"Spanyol","ia":"Espania","nrm":"Espangne","hy":"Իսպանիա","qu":"Ispaña","ilo":"Espania","az":"İspaniya","is":"Spánn","it":"Spagna","tpi":"Spen","ar":"أسبانيا","io":"Hispania","pam":"Espanya","frp":"Èspagne","am":"እስፓንያ","an":"España","csb":"Szpańskô","pt":"Espanha","ja":"スペイン","ps":"اسپانيا","yi":"שפאניע","af":"Spanje"},"geonameFeatureCode":"PCLI","_id":260,"name":"","auto":true,"type":"GEO","geonameId":2510769,"valueCode":""},{"geonameFeatureClass":"A","values":{"ca":"Andalusia","tr":"Endülüs","krc":"Андалусия","no":"Andalucía","fy":"Andalûsje","bs":"Andaluzija","br":"Andalouzia","ext":"Andaluzia","ga":"An Andalúis","th":"แคว้นอันดาลูซีอา","bn":"আন্দালুসিয়া","oc":"Andalosia","ka":"ანდალუსია","sv":"Andalusien","fr":"Andalousie","bg":"Андалусия","glk":"آندالوسیا","be":"Андалусія","kw":"Andalousi","sk":"Andalúzia","os":"Андалуси","da":"Andalusien","sr":"Андалузија","ku":"Endulus","ko":"안달루시아 지방","he":"אנדלוסיה","sh":"Andaluzija","arz":"اندلوسيا","cs":"Andalusie","default":"Andalusia","stq":"Andalusien","la":"Vandalitia","pl":"Andaluzja","ru":"Андалусия","lb":"Andalusien","tet":"Andaluzia","got":"𐍅𐌰𐌽𐌳𐌰𐌻𐌹𐍄𐌾𐌰","hr":"Andaluzija","zh":"安達魯西亞","ro":"Andaluzia","hu":"Andalúzia","pms":"Andalusìa","lv":"Andalūzija","lt":"Andalūzija","nah":"Andalucia","lad":"Andaluziya","de":"Andalusien","als":"Andalusien","qu":"Andalusiya","hy":"Անդալուզիա","eu":"Andaluzia","is":"Andalúsía","uk":"Андалусія","az":"Andalusiya","mr":"आंदालुसिया","ug":"Andalusiye","fa":"اندلس","ar":"أندلوسيا","rmy":"Andalusiya","io":"Andaluzia","el":"Ανδαλουσία","frp":"Andalosie","pt":"Andaluzia","eo":"Andaluzio","en":"Andalusia","et":"Andaluusia","es":"Andalucía","ja":"アンダルシア州","nl":"Andalusië","af":"Andalusië","vec":"Andalusìa"},"geonameFeatureCode":"ADM1","_id":261,"name":"","auto":true,"type":"GEO","geonameId":2593109,"valueCode":""},{"geonameFeatureClass":"A","values":{"de":"Granada","default":"Province of Granada","fr":"Grenade","en":"Province of Granada","es":"Provincia de Granada","ja":"グラナダ"},"geonameFeatureCode":"ADM2","_id":262,"name":"","auto":true,"type":"GEO","geonameId":2517115,"valueCode":""},{"geonameFeatureClass":"A","values":{"default":"Monachil"},"geonameFeatureCode":"ADM3","_id":263,"name":"","auto":true,"type":"GEO","geonameId":6357744,"valueCode":""},{"geonameFeatureClass":"P","values":{"default":"Sierra Nevada"},"geonameFeatureCode":"PPL","_id":264,"name":"","auto":true,"type":"GEO","geonameId":6544329,"valueCode":""}] \ No newline at end of file