// license:BSD-3-Clause // copyright-holders:Aaron Giles /*************************************************************************** corestr.c Core string functions used throughout MAME. ****************************************************************************/ #include "corestr.h" #include #include #include #include #include #include /*------------------------------------------------- core_stricmp - case-insensitive string compare -------------------------------------------------*/ int core_stricmp(std::string_view s1, std::string_view s2) { auto s1_iter = s1.begin(); auto s2_iter = s2.begin(); while (true) { if (s1.end() == s1_iter) return (s2.end() == s2_iter) ? 0 : -1; else if (s2.end() == s2_iter) return 1; const int c1 = tolower(uint8_t(*s1_iter++)); const int c2 = tolower(uint8_t(*s2_iter++)); const int diff = c1 - c2; if (diff) return diff; } } /*------------------------------------------------- core_strnicmp - case-insensitive string compare -------------------------------------------------*/ int core_strnicmp(const char *s1, const char *s2, size_t n) { size_t i; for (i = 0; i < n; i++) { int c1 = tolower((uint8_t)*s1++); int c2 = tolower((uint8_t)*s2++); if (c1 == 0 || c1 != c2) return c1 - c2; } return 0; } /*------------------------------------------------- core_strwildcmp - case-insensitive wildcard string compare -------------------------------------------------*/ int core_strwildcmp(std::string_view s1, std::string_view s2) { // slight tweak of core_stricmp() logic auto s1_iter = s1.begin(); auto s2_iter = s2.begin(); while (true) { if ((s1.end() != s1_iter && *s1_iter == '*') || (s2.end() != s2_iter && *s2_iter == '*')) return 0; if (s1.end() == s1_iter) return (s2.end() == s2_iter) ? 0 : -1; else if (s2.end() == s2_iter) return 1; const int c1 = tolower(uint8_t(*s1_iter++)); const int c2 = tolower(uint8_t(*s2_iter++)); const int diff = (c1 != '?' && c2 != '?') ? c1 - c2 : 0; if (diff) return diff; } } bool core_iswildstr(std::string_view s) { auto iter = std::find_if(s.begin(), s.end(), [](char c) { return c == '?' || c == '*'; }); return iter != s.end(); } /*------------------------------------------------- std::string helpers -------------------------------------------------*/ void strdelchr(std::string& str, char chr) { for (size_t i = 0; i < str.length(); i++) { if (str[i] == chr) { str.erase(i, 1); i--; } } } void strreplacechr(std::string& str, char ch, char newch) { for (auto & elem : str) { if (elem == ch) elem = newch; } } std::string_view strtrimspace(std::string_view str) { std::string_view str2 = strtrimleft(str, [] (char c) { return !isspace(uint8_t(c)); }); return strtrimright(str2, [] (char c) { return !isspace(uint8_t(c)); }); } std::string_view strtrimrightspace(std::string_view str) { return strtrimright(str, [] (char c) { return !isspace(uint8_t(c)); }); } std::string strmakeupper(std::string_view str) { std::string result; std::transform(str.begin(), str.end(), std::back_inserter(result), ::toupper); return result; } /** * @fn std::string strmakelower(std::string_view str) * * @brief Returns a lower case version of the given string. * * @param [in] str The string to make lower case * * @return A new std::string having been changed to lower case */ std::string strmakelower(std::string_view str) { std::string result; std::transform(str.begin(), str.end(), std::back_inserter(result), ::tolower); return result; } /** * @fn int strreplace(std::string &str, const std::string& search, const std::string& replace) * * @brief Strreplaces. * * @param [in,out] str The string. * @param search The search. * @param replace The replace. * * @return An int. */ int strreplace(std::string &str, const std::string& search, const std::string& replace) { int searchlen = search.length(); int replacelen = replace.length(); int matches = 0; for (int curindex = str.find(search, 0); curindex != -1; curindex = str.find(search, curindex + replacelen)) { matches++; str.erase(curindex, searchlen).insert(curindex, replace); } return matches; } namespace util { /** * @fn bool streqlower(std::string_view str, std::string_view lcstr) * * @brief Tests whether a mixed-case string matches a lowercase string. * * @param [in] str First string to compare (may be mixed-case). * @param [in] lcstr Second string to compare (must be all lowercase). * * @return True if the strings match regardless of case. */ bool streqlower(std::string_view str, std::string_view lcstr) { return std::equal(str.begin(), str.end(), lcstr.begin(), lcstr.end(), [] (unsigned char c1, unsigned char c2) { return std::tolower(c1) == c2; }); } /** * @fn bool strequpper(std::string_view str, std::string_view ucstr) * * @brief Tests whether a mixed-case string matches an uppercase string. * * @param [in] str First string to compare (may be mixed-case). * @param [in] ucstr Second string to compare (must be all uppercase). * * @return True if the strings match regardless of case. */ bool strequpper(std::string_view str, std::string_view ucstr) { return std::equal(str.begin(), str.end(), ucstr.begin(), ucstr.end(), [] (unsigned char c1, unsigned char c2) { return std::toupper(c1) == c2; }); } /** * @fn double edit_distance(std::u32string_view lhs, std::u32string_view rhs) * * @brief Compares strings and returns prefix-weighted similarity score (smaller is more similar). * * @param lhs First input. * @param rhs Second input. * * @return Similarity score ranging from 0.0 (totally dissimilar) to 1.0 (identical). */ double edit_distance(std::u32string_view lhs, std::u32string_view rhs) { // based on Jaro-Winkler distance // TODO: this breaks if the lengths don't fit in a long int, but that's not a big limitation constexpr long MAX_PREFIX(4); constexpr double PREFIX_WEIGHT(0.1); constexpr double PREFIX_THRESHOLD(0.7); std::u32string_view const &longer((lhs.length() >= rhs.length()) ? lhs : rhs); std::u32string_view const &shorter((lhs.length() < rhs.length()) ? lhs : rhs); // find matches long const range((std::max)(long(longer.length() / 2) - 1, 0L)); std::unique_ptr match_idx(std::make_unique(shorter.length())); std::unique_ptr match_flg(std::make_unique(longer.length())); std::fill_n(match_idx.get(), shorter.length(), -1); std::fill_n(match_flg.get(), longer.length(), false); long match_cnt(0); for (long i = 0; shorter.length() > i; ++i) { char32_t const ch(shorter[i]); long const n((std::min)(i + range + 1L, long(longer.length()))); for (long j = (std::max)(i - range, 0L); n > j; ++j) { if (!match_flg[j] && (ch == longer[j])) { match_idx[i] = j; match_flg[j] = true; ++match_cnt; break; } } } // early exit if strings are very dissimilar if (!match_cnt) return 1.0; // now find transpositions std::unique_ptr ms(std::make_unique(2 * match_cnt)); std::fill_n(ms.get(), 2 * match_cnt, char32_t(0)); char32_t *const ms1(&ms[0]); char32_t *const ms2(&ms[match_cnt]); for (long i = 0, j = 0; shorter.length() > i; ++i) { if (0 <= match_idx[i]) ms1[j++] = shorter[i]; } match_idx.reset(); for (long i = 0, j = 0; longer.length() > i; ++i) { if (match_flg[i]) ms2[j++] = longer[i]; } match_flg.reset(); long halftrans_cnt(0); for (long i = 0; match_cnt > i; ++i) { if (ms1[i] != ms2[i]) ++halftrans_cnt; } ms.reset(); // simple prefix detection long prefix_len(0); for (long i = 0; ((std::min)(long(shorter.length()), MAX_PREFIX) > i) && (lhs[i] == rhs[i]); ++i) ++prefix_len; // do the weighting double const m(match_cnt); double const t(double(halftrans_cnt) / 2); double const jaro(((m / lhs.length()) + (m / rhs.length()) + ((m - t) / m)) / 3); double const jaro_winkler((PREFIX_THRESHOLD > jaro) ? jaro : (jaro + (PREFIX_WEIGHT * prefix_len * (1.0 - jaro)))); return 1.0 - jaro_winkler; } } // namespace util