diff --git a/src/ai/ai_scanner.cpp b/src/ai/ai_scanner.cpp index dc5eab49fa..342afffb6e 100644 --- a/src/ai/ai_scanner.cpp +++ b/src/ai/ai_scanner.cpp @@ -123,7 +123,7 @@ AIInfo *AIScannerInfo::FindInfo(const char *nameParam, int versionParam, bool fo * version which allows loading the requested version */ for (const auto &item : this->info_list) { AIInfo *i = static_cast(item.second); - if (strcasecmp(ai_name, i->GetName()) == 0 && i->CanLoadFromVersion(versionParam) && (version == -1 || i->GetVersion() > version)) { + if (StrEqualsIgnoreCase(ai_name, i->GetName()) && i->CanLoadFromVersion(versionParam) && (version == -1 || i->GetVersion() > version)) { version = item.second->GetVersion(); info = i; } diff --git a/src/blitter/factory.hpp b/src/blitter/factory.hpp index 393b9836b7..296f6842b6 100644 --- a/src/blitter/factory.hpp +++ b/src/blitter/factory.hpp @@ -128,7 +128,7 @@ public: Blitters::iterator it = GetBlitters().begin(); for (; it != GetBlitters().end(); it++) { BlitterFactory *b = (*it).second; - if (strcasecmp(bname, b->name.c_str()) == 0) { + if (StrEqualsIgnoreCase(bname, b->name)) { return b->IsUsable() ? b : nullptr; } } diff --git a/src/console_cmds.cpp b/src/console_cmds.cpp index b16320bbd6..6d0ca5cdf2 100644 --- a/src/console_cmds.cpp +++ b/src/console_cmds.cpp @@ -1868,7 +1868,7 @@ static ContentType StringToContentType(const char *str) { static const char * const inv_lookup[] = { "", "base", "newgrf", "ai", "ailib", "scenario", "heightmap" }; for (uint i = 1 /* there is no type 0 */; i < lengthof(inv_lookup); i++) { - if (strcasecmp(str, inv_lookup[i]) == 0) return (ContentType)i; + if (StrEqualsIgnoreCase(str, inv_lookup[i])) return (ContentType)i; } return CONTENT_TYPE_END; } @@ -1926,17 +1926,17 @@ DEF_CONSOLE_CMD(ConContent) return true; } - if (strcasecmp(argv[1], "update") == 0) { + if (StrEqualsIgnoreCase(argv[1], "update")) { _network_content_client.RequestContentList((argc > 2) ? StringToContentType(argv[2]) : CONTENT_TYPE_END); return true; } - if (strcasecmp(argv[1], "upgrade") == 0) { + if (StrEqualsIgnoreCase(argv[1], "upgrade")) { _network_content_client.SelectUpgrade(); return true; } - if (strcasecmp(argv[1], "select") == 0) { + if (StrEqualsIgnoreCase(argv[1], "select")) { if (argc <= 2) { /* List selected content */ IConsolePrint(CC_WHITE, "id, type, state, name"); @@ -1944,7 +1944,7 @@ DEF_CONSOLE_CMD(ConContent) if ((*iter)->state != ContentInfo::SELECTED && (*iter)->state != ContentInfo::AUTOSELECTED) continue; OutputContentState(*iter); } - } else if (strcasecmp(argv[2], "all") == 0) { + } else if (StrEqualsIgnoreCase(argv[2], "all")) { /* The intention of this function was that you could download * everything after a filter was applied; but this never really * took off. Instead, a select few people used this functionality @@ -1958,12 +1958,12 @@ DEF_CONSOLE_CMD(ConContent) return true; } - if (strcasecmp(argv[1], "unselect") == 0) { + if (StrEqualsIgnoreCase(argv[1], "unselect")) { if (argc <= 2) { IConsolePrint(CC_ERROR, "You must enter the id."); return false; } - if (strcasecmp(argv[2], "all") == 0) { + if (StrEqualsIgnoreCase(argv[2], "all")) { _network_content_client.UnselectAll(); } else { _network_content_client.Unselect((ContentID)atoi(argv[2])); @@ -1971,7 +1971,7 @@ DEF_CONSOLE_CMD(ConContent) return true; } - if (strcasecmp(argv[1], "state") == 0) { + if (StrEqualsIgnoreCase(argv[1], "state")) { IConsolePrint(CC_WHITE, "id, type, state, name"); for (ConstContentIterator iter = _network_content_client.Begin(); iter != _network_content_client.End(); iter++) { if (argc > 2 && strcasestr((*iter)->name.c_str(), argv[2]) == nullptr) continue; @@ -1980,7 +1980,7 @@ DEF_CONSOLE_CMD(ConContent) return true; } - if (strcasecmp(argv[1], "download") == 0) { + if (StrEqualsIgnoreCase(argv[1], "download")) { uint files; uint bytes; _network_content_client.DownloadSelectedContent(files, bytes); @@ -2007,7 +2007,7 @@ DEF_CONSOLE_CMD(ConFont) FontSize argfs; for (argfs = FS_BEGIN; argfs < FS_END; argfs++) { - if (argc > 1 && strcasecmp(argv[1], FontSizeToName(argfs)) == 0) break; + if (argc > 1 && StrEqualsIgnoreCase(argv[1], FontSizeToName(argfs))) break; } /* First argument must be a FontSize. */ @@ -2021,7 +2021,7 @@ DEF_CONSOLE_CMD(ConFont) byte arg_index = 2; /* We may encounter "aa" or "noaa" but it must be the last argument. */ - if (strcasecmp(argv[arg_index], "aa") == 0 || strcasecmp(argv[arg_index], "noaa") == 0) { + if (StrEqualsIgnoreCase(argv[arg_index], "aa") || StrEqualsIgnoreCase(argv[arg_index], "noaa")) { aa = strncasecmp(argv[arg_index++], "no", 2) != 0; if (argc > arg_index) return false; } else { @@ -2043,7 +2043,7 @@ DEF_CONSOLE_CMD(ConFont) if (argc > arg_index) { /* Last argument must be "aa" or "noaa". */ - if (strcasecmp(argv[arg_index], "aa") != 0 && strcasecmp(argv[arg_index], "noaa") != 0) return false; + if (!StrEqualsIgnoreCase(argv[arg_index], "aa") && !StrEqualsIgnoreCase(argv[arg_index], "noaa")) return false; aa = strncasecmp(argv[arg_index++], "no", 2) != 0; if (argc > arg_index) return false; } @@ -2171,7 +2171,7 @@ DEF_CONSOLE_CMD(ConListDirs) std::set seen_dirs; for (const SubdirNameMap &sdn : subdir_name_map) { - if (strcasecmp(argv[1], sdn.name) != 0) continue; + if (!StrEqualsIgnoreCase(argv[1], sdn.name)) continue; bool found = false; for (Searchpath sp : _valid_searchpaths) { /* Get the directory */ @@ -2256,7 +2256,7 @@ DEF_CONSOLE_CMD(ConNewGRFProfile) /* "unselect" sub-command */ if (strncasecmp(argv[1], "uns", 3) == 0 && argc >= 3) { for (size_t argnum = 2; argnum < argc; ++argnum) { - if (strcasecmp(argv[argnum], "all") == 0) { + if (StrEqualsIgnoreCase(argv[argnum], "all")) { _newgrf_profilers.clear(); break; } @@ -2501,17 +2501,17 @@ DEF_CONSOLE_CMD(ConDumpInfo) return true; } - if (strcasecmp(argv[1], "roadtypes") == 0) { + if (StrEqualsIgnoreCase(argv[1], "roadtypes")) { ConDumpRoadTypes(); return true; } - if (strcasecmp(argv[1], "railtypes") == 0) { + if (StrEqualsIgnoreCase(argv[1], "railtypes")) { ConDumpRailTypes(); return true; } - if (strcasecmp(argv[1], "cargotypes") == 0) { + if (StrEqualsIgnoreCase(argv[1], "cargotypes")) { ConDumpCargoTypes(); return true; } diff --git a/src/driver.cpp b/src/driver.cpp index 29f1946fad..b6bf486427 100644 --- a/src/driver.cpp +++ b/src/driver.cpp @@ -160,7 +160,7 @@ bool DriverFactoryBase::SelectDriverImpl(const std::string &name, Driver::Type t if (d->type != type) continue; /* Check driver name */ - if (strcasecmp(dname.c_str(), d->name) != 0) continue; + if (!StrEqualsIgnoreCase(dname, d->name)) continue; /* Found our driver, let's try it */ Driver *newd = d->CreateInstance(); diff --git a/src/fileio.cpp b/src/fileio.cpp index 9a41a281e6..73b13e3117 100644 --- a/src/fileio.cpp +++ b/src/fileio.cpp @@ -1142,7 +1142,7 @@ static bool MatchesExtension(const char *extension, const char *filename) if (extension == nullptr) return true; const char *ext = strrchr(filename, extension[0]); - return ext != nullptr && strcasecmp(ext, extension) == 0; + return ext != nullptr && StrEqualsIgnoreCase(ext, extension); } /** diff --git a/src/fios.cpp b/src/fios.cpp index 793a4fd433..23fa6af2f7 100644 --- a/src/fios.cpp +++ b/src/fios.cpp @@ -216,7 +216,7 @@ static std::string FiosMakeFilename(const std::string *path, const char *name, c /* Don't append the extension if it is already there */ const char *period = strrchr(name, '.'); - if (period != nullptr && strcasecmp(period, ext) == 0) ext = ""; + if (period != nullptr && StrEqualsIgnoreCase(period, ext)) ext = ""; return buf + PATHSEP + name + ext; } @@ -473,14 +473,14 @@ FiosType FiosGetSavegameListCallback(SaveLoadOperation fop, const std::string &f /* Don't crash if we supply no extension */ if (ext == nullptr) return FIOS_TYPE_INVALID; - if (strcasecmp(ext, ".sav") == 0) { + if (StrEqualsIgnoreCase(ext, ".sav")) { GetFileTitle(file, title, last, SAVE_DIR); return FIOS_TYPE_FILE; } if (fop == SLO_LOAD) { - if (strcasecmp(ext, ".ss1") == 0 || strcasecmp(ext, ".sv1") == 0 || - strcasecmp(ext, ".sv2") == 0) { + if (StrEqualsIgnoreCase(ext, ".ss1") || StrEqualsIgnoreCase(ext, ".sv1") || + StrEqualsIgnoreCase(ext, ".sv2")) { if (title != nullptr) GetOldSaveGameName(file, title, last); return FIOS_TYPE_OLDFILE; } @@ -523,13 +523,13 @@ static FiosType FiosGetScenarioListCallback(SaveLoadOperation fop, const std::st * .SCN OpenTTD style scenario file * .SV0 Transport Tycoon Deluxe (Patch) scenario * .SS0 Transport Tycoon Deluxe preset scenario */ - if (strcasecmp(ext, ".scn") == 0) { + if (StrEqualsIgnoreCase(ext, ".scn")) { GetFileTitle(file, title, last, SCENARIO_DIR); return FIOS_TYPE_SCENARIO; } if (fop == SLO_LOAD) { - if (strcasecmp(ext, ".sv0") == 0 || strcasecmp(ext, ".ss0") == 0 ) { + if (StrEqualsIgnoreCase(ext, ".sv0") || StrEqualsIgnoreCase(ext, ".ss0")) { GetOldSaveGameName(file, title, last); return FIOS_TYPE_OLD_SCENARIO; } @@ -568,10 +568,10 @@ static FiosType FiosGetHeightmapListCallback(SaveLoadOperation fop, const std::s FiosType type = FIOS_TYPE_INVALID; #ifdef WITH_PNG - if (strcasecmp(ext, ".png") == 0) type = FIOS_TYPE_PNG; + if (StrEqualsIgnoreCase(ext, ".png")) type = FIOS_TYPE_PNG; #endif /* WITH_PNG */ - if (strcasecmp(ext, ".bmp") == 0) type = FIOS_TYPE_BMP; + if (StrEqualsIgnoreCase(ext, ".bmp")) type = FIOS_TYPE_BMP; if (type == FIOS_TYPE_INVALID) return FIOS_TYPE_INVALID; @@ -760,7 +760,7 @@ FiosNumberedSaveName::FiosNumberedSaveName(const std::string &prefix) : prefix(p /* Callback for FiosFileScanner. */ static fios_getlist_callback_proc *proc = [](SaveLoadOperation fop, const std::string &file, const char *ext, char *title, const char *last) { - if (strcasecmp(ext, ".sav") == 0 && StrStartsWith(file, _prefix)) return FIOS_TYPE_FILE; + if (StrEqualsIgnoreCase(ext, ".sav") && StrStartsWith(file, _prefix)) return FIOS_TYPE_FILE; return FIOS_TYPE_INVALID; }; diff --git a/src/game/game_scanner.cpp b/src/game/game_scanner.cpp index 1935b78137..367645083b 100644 --- a/src/game/game_scanner.cpp +++ b/src/game/game_scanner.cpp @@ -62,7 +62,7 @@ GameInfo *GameScannerInfo::FindInfo(const char *nameParam, int versionParam, boo * version which allows loading the requested version */ for (const auto &item : this->info_list) { GameInfo *i = static_cast(item.second); - if (strcasecmp(game_name, i->GetName()) == 0 && i->CanLoadFromVersion(versionParam) && (version == -1 || i->GetVersion() > version)) { + if (StrEqualsIgnoreCase(game_name, i->GetName()) && i->CanLoadFromVersion(versionParam) && (version == -1 || i->GetVersion() > version)) { version = item.second->GetVersion(); info = i; } diff --git a/src/newgrf_config.cpp b/src/newgrf_config.cpp index 6610183c2c..0a4860c667 100644 --- a/src/newgrf_config.cpp +++ b/src/newgrf_config.cpp @@ -630,7 +630,7 @@ bool GRFFileScanner::AddFile(const std::string &filename, size_t basepath_length /* Because there can be multiple grfs with the same name, make sure we checked all grfs with the same name, * before inserting the entry. So insert a new grf at the end of all grfs with the same name, instead of * just after the first with the same name. Avoids doubles in the list. */ - if (strcasecmp(c->GetName(), d->GetName()) <= 0) { + if (StrCompareIgnoreCase(c->GetName(), d->GetName()) <= 0) { stop = true; } else if (stop) { break; diff --git a/src/os/unix/font_unix.cpp b/src/os/unix/font_unix.cpp index e2315e3fc1..5027929be0 100644 --- a/src/os/unix/font_unix.cpp +++ b/src/os/unix/font_unix.cpp @@ -70,12 +70,12 @@ FT_Error GetFontByFaceName(const char *font_name, FT_Face *face) FcPatternGetString(fs->fonts[i], FC_STYLE, 0, &style) == FcResultMatch) { /* The correct style? */ - if (font_style != nullptr && strcasecmp(font_style, (char *)style) != 0) continue; + if (font_style != nullptr && !StrEqualsIgnoreCase(font_style, (char *)style)) continue; /* Font config takes the best shot, which, if the family name is spelled * wrongly a 'random' font, so check whether the family name is the * same as the supplied name */ - if (strcasecmp(font_family, (char *)family) == 0) { + if (StrEqualsIgnoreCase(font_family, (char *)family)) { err = FT_New_Face(_library, (char *)file, 0, face); } } diff --git a/src/script/script_scanner.cpp b/src/script/script_scanner.cpp index 3b76b7b651..b395b0ed59 100644 --- a/src/script/script_scanner.cpp +++ b/src/script/script_scanner.cpp @@ -115,7 +115,7 @@ void ScriptScanner::RegisterScript(ScriptInfo *info) /* This script was already registered */ #ifdef _WIN32 /* Windows doesn't care about the case */ - if (strcasecmp(this->info_list[script_name]->GetMainScript(), info->GetMainScript()) == 0) { + if (StrEqualsIgnoreCase(this->info_list[script_name]->GetMainScript(), info->GetMainScript()) == 0) { #else if (strcmp(this->info_list[script_name]->GetMainScript(), info->GetMainScript()) == 0) { #endif @@ -231,7 +231,7 @@ static bool IsSameScript(const ContentInfo *ci, bool md5sum, ScriptInfo *info, S /* Check the extension. */ const char *ext = strrchr(tar.first.c_str(), '.'); - if (ext == nullptr || strcasecmp(ext, ".nut") != 0) continue; + if (ext == nullptr || !StrEqualsIgnoreCase(ext, ".nut")) continue; checksum.AddFile(tar.first, 0, tar_filename); } diff --git a/src/string.cpp b/src/string.cpp index ff91f95ca3..0f853f9112 100644 --- a/src/string.cpp +++ b/src/string.cpp @@ -368,6 +368,59 @@ bool StrEndsWith(const std::string_view str, const std::string_view suffix) return str.compare(str.size() - suffix_len, suffix_len, suffix, 0, suffix_len) == 0; } +/** Case insensitive implementation of the standard character type traits. */ +struct CaseInsensitiveCharTraits : public std::char_traits { + static bool eq(char c1, char c2) { return toupper(c1) == toupper(c2); } + static bool ne(char c1, char c2) { return toupper(c1) != toupper(c2); } + static bool lt(char c1, char c2) { return toupper(c1) < toupper(c2); } + + static int compare(const char *s1, const char *s2, size_t n) + { + while (n-- != 0) { + if (toupper(*s1) < toupper(*s2)) return -1; + if (toupper(*s1) > toupper(*s2)) return 1; + ++s1; ++s2; + } + return 0; + } + + static const char *find(const char *s, int n, char a) + { + while (n-- > 0 && toupper(*s) != toupper(a)) { + ++s; + } + return s; + } +}; + +/** Case insensitive string view. */ +typedef std::basic_string_view CaseInsensitiveStringView; + +/** + * Compares two string( view)s, while ignoring the case of the characters. + * @param str1 The first string. + * @param str2 The second string. + * @return Less than zero if str1 < str2, zero if str1 == str2, greater than + * zero if str1 > str2. All ignoring the case of the characters. + */ +int StrCompareIgnoreCase(const std::string_view str1, const std::string_view str2) +{ + CaseInsensitiveStringView ci_str1{ str1.data(), str1.size() }; + CaseInsensitiveStringView ci_str2{ str2.data(), str2.size() }; + return ci_str1.compare(ci_str2); +} + +/** + * Compares two string( view)s for equality, while ignoring the case of the characters. + * @param str1 The first string. + * @param str2 The second string. + * @return True iff both strings are equal, barring the case of the characters. + */ +bool StrEqualsIgnoreCase(const std::string_view str1, const std::string_view str2) +{ + if (str1.size() != str2.size()) return false; + return StrCompareIgnoreCase(str1, str2) == 0; +} /** Scans the string for colour codes and strips them */ void str_strip_colours(char *str) @@ -728,7 +781,7 @@ int strnatcmp(const char *s1, const char *s2, bool ignore_garbage_at_front) #endif /* Do a normal comparison if ICU is missing or if we cannot create a collator. */ - return strcasecmp(s1, s2); + return StrCompareIgnoreCase(s1, s2); } #ifdef WITH_UNISCRIBE diff --git a/src/string_func.h b/src/string_func.h index 816f85cf57..5293691807 100644 --- a/src/string_func.h +++ b/src/string_func.h @@ -54,6 +54,9 @@ void StrTrimInPlace(std::string &str); bool StrStartsWith(const std::string_view str, const std::string_view prefix); bool StrEndsWith(const std::string_view str, const std::string_view suffix); +[[nodiscard]] int StrCompareIgnoreCase(const std::string_view str1, const std::string_view str2); +[[nodiscard]] bool StrEqualsIgnoreCase(const std::string_view str1, const std::string_view str2); + /** * Check if a string buffer is empty. * diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index b920fd6fdb..6f5134d46f 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -1,5 +1,6 @@ add_test_files( landscape_partial_pixel_z.cpp math_func.cpp + string_func.cpp test_main.cpp ) diff --git a/src/tests/math_func.cpp b/src/tests/math_func.cpp index 0e58b79e42..6cc43f0146 100644 --- a/src/tests/math_func.cpp +++ b/src/tests/math_func.cpp @@ -5,7 +5,7 @@ * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . */ -/** @file math_func_test.cpp Test functionality from core/math_func. */ +/** @file math_func.cpp Test functionality from core/math_func. */ #include "../stdafx.h" diff --git a/src/tests/string_func.cpp b/src/tests/string_func.cpp new file mode 100644 index 0000000000..3797f894e7 --- /dev/null +++ b/src/tests/string_func.cpp @@ -0,0 +1,160 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file string_func.cpp Test functionality from string_func. */ + +#include "../stdafx.h" + +#include "../3rdparty/catch2/catch.hpp" + +#include "../string_func.h" + +TEST_CASE("StrCompareIgnoreCase - std::string") +{ + /* Same string, with different cases. */ + CHECK(StrCompareIgnoreCase(std::string{""}, std::string{""}) == 0); + CHECK(StrCompareIgnoreCase(std::string{"a"}, std::string{"a"}) == 0); + CHECK(StrCompareIgnoreCase(std::string{"a"}, std::string{"A"}) == 0); + CHECK(StrCompareIgnoreCase(std::string{"A"}, std::string{"a"}) == 0); + CHECK(StrCompareIgnoreCase(std::string{"A"}, std::string{"A"}) == 0); + + /* Not the same string. */ + CHECK(StrCompareIgnoreCase(std::string{""}, std::string{"b"}) < 0); + CHECK(StrCompareIgnoreCase(std::string{"a"}, std::string{""}) > 0); + + CHECK(StrCompareIgnoreCase(std::string{"a"}, std::string{"b"}) < 0); + CHECK(StrCompareIgnoreCase(std::string{"b"}, std::string{"a"}) > 0); + CHECK(StrCompareIgnoreCase(std::string{"a"}, std::string{"B"}) < 0); + CHECK(StrCompareIgnoreCase(std::string{"b"}, std::string{"A"}) > 0); + CHECK(StrCompareIgnoreCase(std::string{"A"}, std::string{"b"}) < 0); + CHECK(StrCompareIgnoreCase(std::string{"B"}, std::string{"a"}) > 0); + + CHECK(StrCompareIgnoreCase(std::string{"a"}, std::string{"aa"}) < 0); + CHECK(StrCompareIgnoreCase(std::string{"aa"}, std::string{"a"}) > 0); +} + +TEST_CASE("StrCompareIgnoreCase - char pointer") +{ + /* Same string, with different cases. */ + CHECK(StrCompareIgnoreCase("", "") == 0); + CHECK(StrCompareIgnoreCase("a", "a") == 0); + CHECK(StrCompareIgnoreCase("a", "A") == 0); + CHECK(StrCompareIgnoreCase("A", "a") == 0); + CHECK(StrCompareIgnoreCase("A", "A") == 0); + + /* Not the same string. */ + CHECK(StrCompareIgnoreCase("", "b") < 0); + CHECK(StrCompareIgnoreCase("a", "") > 0); + + CHECK(StrCompareIgnoreCase("a", "b") < 0); + CHECK(StrCompareIgnoreCase("b", "a") > 0); + CHECK(StrCompareIgnoreCase("a", "B") < 0); + CHECK(StrCompareIgnoreCase("b", "A") > 0); + CHECK(StrCompareIgnoreCase("A", "b") < 0); + CHECK(StrCompareIgnoreCase("B", "a") > 0); + + CHECK(StrCompareIgnoreCase("a", "aa") < 0); + CHECK(StrCompareIgnoreCase("aa", "a") > 0); +} + +TEST_CASE("StrCompareIgnoreCase - std::string_view") +{ + /* + * With std::string_view the only way to access the data is via .data(), + * which does not guarantee the termination that would be required by + * things such as stricmp/strcasecmp. So, just passing .data() into stricmp + * or strcasecmp would fail if it does not account for the length of the + * view. Thus, contrary to the string/char* tests, this uses the same base + * string but gets different sections to trigger these corner cases. + */ + std::string_view base{"aaAbB"}; + + /* Same string, with different cases. */ + CHECK(StrCompareIgnoreCase(base.substr(0, 0), base.substr(1, 0)) == 0); // Different positions + CHECK(StrCompareIgnoreCase(base.substr(0, 1), base.substr(1, 1)) == 0); // Different positions + CHECK(StrCompareIgnoreCase(base.substr(0, 1), base.substr(2, 1)) == 0); + CHECK(StrCompareIgnoreCase(base.substr(2, 1), base.substr(1, 1)) == 0); + CHECK(StrCompareIgnoreCase(base.substr(2, 1), base.substr(2, 1)) == 0); + + /* Not the same string. */ + CHECK(StrCompareIgnoreCase(base.substr(3, 0), base.substr(3, 1)) < 0); // Same position, different lengths + CHECK(StrCompareIgnoreCase(base.substr(0, 1), base.substr(0, 0)) > 0); // Same position, different lengths + + CHECK(StrCompareIgnoreCase(base.substr(0, 1), base.substr(3, 1)) < 0); + CHECK(StrCompareIgnoreCase(base.substr(3, 1), base.substr(0, 1)) > 0); + CHECK(StrCompareIgnoreCase(base.substr(0, 1), base.substr(4, 1)) < 0); + CHECK(StrCompareIgnoreCase(base.substr(3, 1), base.substr(2, 1)) > 0); + CHECK(StrCompareIgnoreCase(base.substr(2, 1), base.substr(3, 1)) < 0); + CHECK(StrCompareIgnoreCase(base.substr(4, 1), base.substr(0, 1)) > 0); + + CHECK(StrCompareIgnoreCase(base.substr(0, 1), base.substr(0, 2)) < 0); // Same position, different lengths + CHECK(StrCompareIgnoreCase(base.substr(0, 2), base.substr(0, 1)) > 0); // Same position, different lengths +} + +TEST_CASE("StrEqualsIgnoreCase - std::string") +{ + /* Same string, with different cases. */ + CHECK(StrEqualsIgnoreCase(std::string{""}, std::string{""})); + CHECK(StrEqualsIgnoreCase(std::string{"a"}, std::string{"a"})); + CHECK(StrEqualsIgnoreCase(std::string{"a"}, std::string{"A"})); + CHECK(StrEqualsIgnoreCase(std::string{"A"}, std::string{"a"})); + CHECK(StrEqualsIgnoreCase(std::string{"A"}, std::string{"A"})); + + /* Not the same string. */ + CHECK(!StrEqualsIgnoreCase(std::string{""}, std::string{"b"})); + CHECK(!StrEqualsIgnoreCase(std::string{"a"}, std::string{""})); + CHECK(!StrEqualsIgnoreCase(std::string{"a"}, std::string{"b"})); + CHECK(!StrEqualsIgnoreCase(std::string{"b"}, std::string{"a"})); + CHECK(!StrEqualsIgnoreCase(std::string{"a"}, std::string{"aa"})); + CHECK(!StrEqualsIgnoreCase(std::string{"aa"}, std::string{"a"})); +} + +TEST_CASE("StrEqualsIgnoreCase - char pointer") +{ + /* Same string, with different cases. */ + CHECK(StrEqualsIgnoreCase("", "")); + CHECK(StrEqualsIgnoreCase("a", "a")); + CHECK(StrEqualsIgnoreCase("a", "A")); + CHECK(StrEqualsIgnoreCase("A", "a")); + CHECK(StrEqualsIgnoreCase("A", "A")); + + /* Not the same string. */ + CHECK(!StrEqualsIgnoreCase("", "b")); + CHECK(!StrEqualsIgnoreCase("a", "")); + CHECK(!StrEqualsIgnoreCase("a", "b")); + CHECK(!StrEqualsIgnoreCase("b", "a")); + CHECK(!StrEqualsIgnoreCase("a", "aa")); + CHECK(!StrEqualsIgnoreCase("aa", "a")); +} + +TEST_CASE("StrEqualsIgnoreCase - std::string_view") +{ + /* + * With std::string_view the only way to access the data is via .data(), + * which does not guarantee the termination that would be required by + * things such as stricmp/strcasecmp. So, just passing .data() into stricmp + * or strcasecmp would fail if it does not account for the length of the + * view. Thus, contrary to the string/char* tests, this uses the same base + * string but gets different sections to trigger these corner cases. + */ + std::string_view base{"aaAb"}; + + /* Same string, with different cases. */ + CHECK(StrEqualsIgnoreCase(base.substr(0, 0), base.substr(1, 0))); // Different positions + CHECK(StrEqualsIgnoreCase(base.substr(0, 1), base.substr(1, 1))); // Different positions + CHECK(StrEqualsIgnoreCase(base.substr(0, 1), base.substr(2, 1))); + CHECK(StrEqualsIgnoreCase(base.substr(2, 1), base.substr(1, 1))); + CHECK(StrEqualsIgnoreCase(base.substr(2, 1), base.substr(2, 1))); + + /* Not the same string. */ + CHECK(!StrEqualsIgnoreCase(base.substr(3, 0), base.substr(3, 1))); // Same position, different lengths + CHECK(!StrEqualsIgnoreCase(base.substr(0, 1), base.substr(0, 0))); + CHECK(!StrEqualsIgnoreCase(base.substr(0, 1), base.substr(3, 1))); + CHECK(!StrEqualsIgnoreCase(base.substr(3, 1), base.substr(0, 1))); + CHECK(!StrEqualsIgnoreCase(base.substr(0, 1), base.substr(0, 2))); // Same position, different lengths + CHECK(!StrEqualsIgnoreCase(base.substr(0, 2), base.substr(0, 1))); // Same position, different lengths +}