1020 lines
32 KiB
C++
1020 lines
32 KiB
C++
// *****************************************************************************
|
|
// * This file is part of the FreeFileSync project. It is distributed under *
|
|
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
|
|
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
|
|
// *****************************************************************************
|
|
|
|
#ifndef STRING_TOOLS_H_213458973046
|
|
#define STRING_TOOLS_H_213458973046
|
|
|
|
#include <cstdio> //sprintf
|
|
#include <cwchar> //swprintf
|
|
#include "stl_tools.h"
|
|
#include "string_traits.h"
|
|
#include "legacy_compiler.h" //<charconv> but without the compiler crashes :>
|
|
|
|
|
|
//enhance *any* string class with useful non-member functions:
|
|
namespace zen
|
|
{
|
|
template <class Char> bool isWhiteSpace(Char c);
|
|
template <class Char> bool isLineBreak (Char c);
|
|
template <class Char> bool isDigit (Char c); //not exactly the same as "std::isdigit" -> we consider '0'-'9' only!
|
|
template <class Char> bool isHexDigit (Char c);
|
|
template <class Char> bool isAsciiChar (Char c);
|
|
template <class Char> bool isAsciiAlpha(Char c);
|
|
template <class S > bool isAsciiString(const S& str);
|
|
template <class Char> Char asciiToLower(Char c);
|
|
template <class Char> Char asciiToUpper(Char c);
|
|
|
|
//both S and T can be strings or char/wchar_t arrays or single char/wchar_t
|
|
template <class S, class T, typename = std::enable_if_t<isStringLike<S>/*Astyle hates tripe >*/ >> bool contains(const S& str, const T& term);
|
|
|
|
template <class S, class T> bool startsWith (const S& str, const T& prefix);
|
|
template <class S, class T> bool startsWithAsciiNoCase(const S& str, const T& prefix);
|
|
|
|
template <class S, class T> bool endsWith (const S& str, const T& postfix);
|
|
template <class S, class T> bool endsWithAsciiNoCase(const S& str, const T& postfix);
|
|
|
|
template <class S, class T> bool equalString (const S& lhs, const T& rhs);
|
|
template <class S, class T> bool equalAsciiNoCase(const S& lhs, const T& rhs);
|
|
|
|
template <class S, class T> std::strong_ordering compareString(const S& lhs, const T& rhs);
|
|
template <class S, class T> std::weak_ordering compareAsciiNoCase(const S& lhs, const T& rhs); //basic case-insensitive comparison (considering A-Z only!)
|
|
|
|
//STL container predicates for std::map, std::unordered_set/map
|
|
struct StringHash;
|
|
struct StringEqual;
|
|
|
|
struct LessAsciiNoCase;
|
|
struct StringHashAsciiNoCase;
|
|
struct StringEqualAsciiNoCase;
|
|
|
|
template <class Num, class S> Num hashString(const S& str);
|
|
|
|
enum class IfNotFoundReturn
|
|
{
|
|
all,
|
|
none
|
|
};
|
|
template <class S, class T> S afterLast (const S& str, const T& term, IfNotFoundReturn infr);
|
|
template <class S, class T> S beforeLast (const S& str, const T& term, IfNotFoundReturn infr);
|
|
template <class S, class T> S afterFirst (const S& str, const T& term, IfNotFoundReturn infr);
|
|
template <class S, class T> S beforeFirst(const S& str, const T& term, IfNotFoundReturn infr);
|
|
|
|
enum class SplitOnEmpty
|
|
{
|
|
allow,
|
|
skip
|
|
};
|
|
template <class S, class Char, class Function> void split(const S& str, Char delimiter, Function onStringPart);
|
|
template <class S, class Function1, class Function2> void split2(const S& str, Function1 isDelimiter, Function2 onStringPart);
|
|
template <class S, class Char> [[nodiscard]] std::vector<S> splitCpy(const S& str, Char delimiter, SplitOnEmpty soe);
|
|
|
|
enum class TrimSide
|
|
{
|
|
both,
|
|
left,
|
|
right,
|
|
};
|
|
template <class S> [[nodiscard]] S trimCpy(const S& str, TrimSide side = TrimSide::both);
|
|
template <class S> void trim(S& str, TrimSide side = TrimSide::both);
|
|
template <class S, class Function> void trim(S& str, TrimSide side, Function trimThisChar);
|
|
|
|
|
|
template <class S, class T, class U> [[nodiscard]] S replaceCpy(S str, const T& oldTerm, const U& newTerm);
|
|
template <class S, class T, class U> void replace (S& str, const T& oldTerm, const U& newTerm);
|
|
|
|
template <class S, class T, class U> [[nodiscard]] S replaceCpyAsciiNoCase(S str, const T& oldTerm, const U& newTerm);
|
|
template <class S, class T, class U> void replaceAsciiNoCase (S& str, const T& oldTerm, const U& newTerm);
|
|
|
|
//high-performance conversion between numbers and strings
|
|
template <class S, class Num> S numberTo(const Num& number);
|
|
template <class Num, class S> Num stringTo(const S& str);
|
|
|
|
std::pair<char, char> hexify (unsigned char c, bool upperCase = true);
|
|
char unhexify(char high, char low);
|
|
std::string formatAsHexString(const std::string_view& blob); //bytes -> (human-readable) hex string
|
|
|
|
template <class S, class T, class Num> S printNumber(const T& format, const Num& number); //format a single number using std::snprintf()
|
|
|
|
//string to string conversion: converts string-like type into char-compatible target string class
|
|
template <class T, class S> T copyStringTo(S&& str);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//---------------------- implementation ----------------------
|
|
template <class Char> inline
|
|
bool isWhiteSpace(Char c)
|
|
{
|
|
static_assert(std::is_same_v<Char, char> || std::is_same_v<Char, wchar_t>);
|
|
assert(c != 0); //std C++ does not consider 0 as white space
|
|
return c == static_cast<Char>(' ') || (static_cast<Char>('\t') <= c && c <= static_cast<Char>('\r'));
|
|
//following std::isspace() for default locale but without the interface insanity:
|
|
// - std::isspace() takes an int, but expects an unsigned char
|
|
// - some parts of UTF-8 chars are erroneously seen as whitespace, e.g. the a0 from "\xec\x8b\xa0" (MSVC)
|
|
}
|
|
|
|
template <class Char> inline
|
|
bool isLineBreak(Char c)
|
|
{
|
|
static_assert(std::is_same_v<Char, char> || std::is_same_v<Char, wchar_t>);
|
|
return c == static_cast<Char>('\r') || c == static_cast<Char>('\n');
|
|
}
|
|
|
|
|
|
template <class Char> inline
|
|
bool isDigit(Char c) //similar to implementation of std::isdigit()!
|
|
{
|
|
static_assert(std::is_same_v<Char, char> || std::is_same_v<Char, wchar_t>);
|
|
return static_cast<Char>('0') <= c && c <= static_cast<Char>('9');
|
|
}
|
|
|
|
|
|
template <class Char> inline
|
|
bool isHexDigit(Char c)
|
|
{
|
|
static_assert(std::is_same_v<Char, char> || std::is_same_v<Char, wchar_t>);
|
|
return (static_cast<Char>('0') <= c && c <= static_cast<Char>('9')) ||
|
|
(static_cast<Char>('A') <= c && c <= static_cast<Char>('F')) ||
|
|
(static_cast<Char>('a') <= c && c <= static_cast<Char>('f'));
|
|
}
|
|
|
|
|
|
template <class Char> inline
|
|
bool isAsciiChar(Char c)
|
|
{
|
|
return makeUnsigned(c) < 128;
|
|
}
|
|
|
|
|
|
template <class Char> inline
|
|
bool isAsciiAlpha(Char c)
|
|
{
|
|
static_assert(std::is_same_v<Char, char> || std::is_same_v<Char, wchar_t>);
|
|
return (static_cast<Char>('A') <= c && c <= static_cast<Char>('Z')) ||
|
|
(static_cast<Char>('a') <= c && c <= static_cast<Char>('z'));
|
|
}
|
|
|
|
|
|
template <class S> inline
|
|
bool isAsciiString(const S& str)
|
|
{
|
|
const auto* const first = strBegin(str);
|
|
return std::all_of(first, first + strLength(str), [](auto c) { return isAsciiChar(c); });
|
|
}
|
|
|
|
|
|
template <class Char> inline
|
|
Char asciiToLower(Char c)
|
|
{
|
|
if (static_cast<Char>('A') <= c && c <= static_cast<Char>('Z'))
|
|
return static_cast<Char>(c - static_cast<Char>('A') + static_cast<Char>('a'));
|
|
return c;
|
|
}
|
|
|
|
|
|
template <class Char> inline
|
|
Char asciiToUpper(Char c)
|
|
{
|
|
if (static_cast<Char>('a') <= c && c <= static_cast<Char>('z'))
|
|
return static_cast<Char>(c - static_cast<Char>('a') + static_cast<Char>('A'));
|
|
return c;
|
|
}
|
|
|
|
|
|
namespace impl
|
|
{
|
|
template <class Char> inline
|
|
bool equalSubstring(const Char* lhs, const Char* rhs, size_t len)
|
|
{
|
|
//support embedded 0, unlike strncmp/wcsncmp:
|
|
return std::equal(lhs, lhs + len, rhs);
|
|
}
|
|
|
|
|
|
template <class Char1, class Char2> inline
|
|
std::weak_ordering strcmpAsciiNoCase(const Char1* lhs, const Char2* rhs, size_t len)
|
|
{
|
|
while (len-- > 0)
|
|
{
|
|
const Char1 charL = asciiToLower(*lhs++); //ordering: lower-case chars have higher code points than uppper-case
|
|
const Char2 charR = asciiToLower(*rhs++); //
|
|
if (charL != charR)
|
|
return makeUnsigned(charL) <=> makeUnsigned(charR); //unsigned char-comparison is the convention!
|
|
}
|
|
return std::weak_ordering::equivalent;
|
|
}
|
|
}
|
|
|
|
|
|
template <class S, class T> inline
|
|
bool startsWith(const S& str, const T& prefix)
|
|
{
|
|
const size_t pfLen = strLength(prefix);
|
|
return strLength(str) >= pfLen && impl::equalSubstring(strBegin(str), strBegin(prefix), pfLen);
|
|
}
|
|
|
|
|
|
template <class S, class T> inline
|
|
bool startsWithAsciiNoCase(const S& str, const T& prefix)
|
|
{
|
|
assert(isAsciiString(str) || isAsciiString(prefix));
|
|
const size_t pfLen = strLength(prefix);
|
|
return strLength(str) >= pfLen && impl::strcmpAsciiNoCase(strBegin(str), strBegin(prefix), pfLen) == std::weak_ordering::equivalent;
|
|
}
|
|
|
|
|
|
template <class S, class T> inline
|
|
bool endsWith(const S& str, const T& postfix)
|
|
{
|
|
const size_t strLen = strLength(str);
|
|
const size_t pfLen = strLength(postfix);
|
|
return strLen >= pfLen && impl::equalSubstring(strBegin(str) + strLen - pfLen, strBegin(postfix), pfLen);
|
|
}
|
|
|
|
|
|
template <class S, class T> inline
|
|
bool endsWithAsciiNoCase(const S& str, const T& postfix)
|
|
{
|
|
const size_t strLen = strLength(str);
|
|
const size_t pfLen = strLength(postfix);
|
|
return strLen >= pfLen && impl::strcmpAsciiNoCase(strBegin(str) + strLen - pfLen, strBegin(postfix), pfLen) == std::weak_ordering::equivalent;
|
|
}
|
|
|
|
|
|
template <class S, class T> inline
|
|
bool equalString(const S& lhs, const T& rhs)
|
|
{
|
|
const size_t lhsLen = strLength(lhs);
|
|
return lhsLen == strLength(rhs) && impl::equalSubstring(strBegin(lhs), strBegin(rhs), lhsLen);
|
|
}
|
|
|
|
|
|
template <class S, class T> inline
|
|
bool equalAsciiNoCase(const S& lhs, const T& rhs)
|
|
{
|
|
//assert(isAsciiString(lhs) || isAsciiString(rhs)); -> no, too strict (e.g. comparing file extensions ASCII-CI)
|
|
const size_t lhsLen = strLength(lhs);
|
|
return lhsLen == strLength(rhs) && impl::strcmpAsciiNoCase(strBegin(lhs), strBegin(rhs), lhsLen) == std::weak_ordering::equivalent;
|
|
}
|
|
|
|
|
|
namespace impl
|
|
{
|
|
//support embedded 0 (unlike strncmp/wcsncmp) + compare unsigned[!] char
|
|
inline std::strong_ordering strcmpWithNulls(const char* ptr1, const char* ptr2, size_t num) { return std:: memcmp(ptr1, ptr2, num) <=> 0; }
|
|
inline std::strong_ordering strcmpWithNulls(const wchar_t* ptr1, const wchar_t* ptr2, size_t num) { return std::wmemcmp(ptr1, ptr2, num) <=> 0; }
|
|
}
|
|
|
|
template <class S, class T> inline
|
|
std::strong_ordering compareString(const S& lhs, const T& rhs)
|
|
{
|
|
const size_t lhsLen = strLength(lhs);
|
|
const size_t rhsLen = strLength(rhs);
|
|
|
|
//length check *after* strcmpWithNulls(): we DO care about natural ordering
|
|
if (const std::strong_ordering cmp = impl::strcmpWithNulls(strBegin(lhs), strBegin(rhs), std::min(lhsLen, rhsLen));
|
|
cmp != std::strong_ordering::equal)
|
|
return cmp;
|
|
return lhsLen <=> rhsLen;
|
|
}
|
|
|
|
|
|
template <class S, class T> inline
|
|
std::weak_ordering compareAsciiNoCase(const S& lhs, const T& rhs)
|
|
{
|
|
const size_t lhsLen = strLength(lhs);
|
|
const size_t rhsLen = strLength(rhs);
|
|
|
|
if (const std::weak_ordering cmp = impl::strcmpAsciiNoCase(strBegin(lhs), strBegin(rhs), std::min(lhsLen, rhsLen));
|
|
cmp != std::weak_ordering::equivalent)
|
|
return cmp;
|
|
return lhsLen <=> rhsLen;
|
|
}
|
|
|
|
|
|
template <class S, class T, typename> inline
|
|
bool contains(const S& str, const T& term)
|
|
{
|
|
static_assert(std::is_same_v<GetCharTypeT<S>, GetCharTypeT<T>>);
|
|
const size_t strLen = strLength(str);
|
|
const size_t termLen = strLength(term);
|
|
if (strLen < termLen)
|
|
return false;
|
|
|
|
const auto* const strFirst = strBegin(str);
|
|
const auto* const strLast = strFirst + strLen;
|
|
const auto* const termFirst = strBegin(term);
|
|
|
|
return searchFirst(strFirst, strLast,
|
|
termFirst, termFirst + termLen) != strLast;
|
|
}
|
|
|
|
|
|
template <class S, class T> inline
|
|
S afterLast(const S& str, const T& term, IfNotFoundReturn infr)
|
|
{
|
|
static_assert(std::is_same_v<GetCharTypeT<S>, GetCharTypeT<T>>);
|
|
const size_t termLen = strLength(term);
|
|
assert(termLen > 0);
|
|
|
|
const auto* const strFirst = strBegin(str);
|
|
const auto* const strLast = strFirst + strLength(str);
|
|
const auto* const termFirst = strBegin(term);
|
|
|
|
const auto* it = searchLast(strFirst, strLast,
|
|
termFirst, termFirst + termLen);
|
|
if (it == strLast)
|
|
return infr == IfNotFoundReturn::all ? str : S();
|
|
|
|
it += termLen;
|
|
return S(it, strLast - it);
|
|
}
|
|
|
|
|
|
template <class S, class T> inline
|
|
S beforeLast(const S& str, const T& term, IfNotFoundReturn infr)
|
|
{
|
|
static_assert(std::is_same_v<GetCharTypeT<S>, GetCharTypeT<T>>);
|
|
const size_t termLen = strLength(term);
|
|
assert(termLen > 0);
|
|
|
|
const auto* const strFirst = strBegin(str);
|
|
const auto* const strLast = strFirst + strLength(str);
|
|
const auto* const termFirst = strBegin(term);
|
|
|
|
const auto* it = searchLast(strFirst, strLast,
|
|
termFirst, termFirst + termLen);
|
|
if (it == strLast)
|
|
return infr == IfNotFoundReturn::all ? str : S();
|
|
|
|
return S(strFirst, it - strFirst);
|
|
}
|
|
|
|
|
|
template <class S, class T> inline
|
|
S afterFirst(const S& str, const T& term, IfNotFoundReturn infr)
|
|
{
|
|
static_assert(std::is_same_v<GetCharTypeT<S>, GetCharTypeT<T>>);
|
|
const size_t termLen = strLength(term);
|
|
assert(termLen > 0);
|
|
|
|
const auto* const strFirst = strBegin(str);
|
|
const auto* const strLast = strFirst + strLength(str);
|
|
const auto* const termFirst = strBegin(term);
|
|
|
|
const auto* it = searchFirst(strFirst, strLast,
|
|
termFirst, termFirst + termLen);
|
|
if (it == strLast)
|
|
return infr == IfNotFoundReturn::all ? str : S();
|
|
|
|
it += termLen;
|
|
return S(it, strLast - it);
|
|
}
|
|
|
|
|
|
template <class S, class T> inline
|
|
S beforeFirst(const S& str, const T& term, IfNotFoundReturn infr)
|
|
{
|
|
static_assert(std::is_same_v<GetCharTypeT<S>, GetCharTypeT<T>>);
|
|
const size_t termLen = strLength(term);
|
|
assert(termLen > 0);
|
|
|
|
const auto* const strFirst = strBegin(str);
|
|
const auto* const strLast = strFirst + strLength(str);
|
|
const auto* const termFirst = strBegin(term);
|
|
|
|
auto it = searchFirst(strFirst, strLast,
|
|
termFirst, termFirst + termLen);
|
|
if (it == strLast)
|
|
return infr == IfNotFoundReturn::all ? str : S();
|
|
|
|
return S(strFirst, it - strFirst);
|
|
}
|
|
|
|
|
|
template <class S, class Function1, class Function2> inline
|
|
void split2(const S& str, Function1 isDelimiter, Function2 onStringPart)
|
|
{
|
|
const auto* blockFirst = strBegin(str);
|
|
const auto* const strEnd = blockFirst + strLength(str);
|
|
|
|
for (;;)
|
|
{
|
|
const auto* const blockLast = std::find_if(blockFirst, strEnd, isDelimiter);
|
|
onStringPart(makeStringView(blockFirst, blockLast));
|
|
|
|
if (blockLast == strEnd)
|
|
return;
|
|
|
|
blockFirst = blockLast + 1;
|
|
}
|
|
}
|
|
|
|
|
|
template <class S, class Char, class Function> inline
|
|
void split(const S& str, Char delimiter, Function onStringPart)
|
|
{
|
|
static_assert(std::is_same_v<GetCharTypeT<S>, Char>);
|
|
split2(str, [delimiter](const Char c) { return c == delimiter; }, onStringPart);
|
|
}
|
|
|
|
|
|
template <class S, class Char> inline
|
|
std::vector<S> splitCpy(const S& str, Char delimiter, SplitOnEmpty soe)
|
|
{
|
|
static_assert(std::is_same_v<GetCharTypeT<S>, Char>);
|
|
std::vector<S> output;
|
|
|
|
split2(str, [delimiter](const Char c) { return c == delimiter; }, [&, soe](std::basic_string_view<Char> block)
|
|
{
|
|
if (!block.empty() || soe == SplitOnEmpty::allow)
|
|
output.emplace_back(block.data(), block.size());
|
|
});
|
|
return output;
|
|
}
|
|
|
|
|
|
namespace impl
|
|
{
|
|
ZEN_INIT_DETECT_MEMBER(append)
|
|
|
|
//either call operator+=(S(str, len)) or append(str, len)
|
|
template <class S, class InputIterator, typename = std::enable_if_t<hasMember_append<S>>> inline
|
|
void stringAppend(S& str, InputIterator first, InputIterator last) { str.append(first, last); }
|
|
|
|
//inefficient append: keep disabled until really needed
|
|
//template <class S, class InputIterator, typename = std::enable_if_t<!hasMember_append<S>>> inline
|
|
//void stringAppend(S& str, InputIterator first, InputIterator last) { str += S(first, last); }
|
|
|
|
|
|
template <class S, class T, class U, class CharEq> inline
|
|
void replace(S& str, const T& oldTerm, const U& newTerm, CharEq charEqual)
|
|
{
|
|
static_assert(std::is_same_v<GetCharTypeT<S>, GetCharTypeT<T>>);
|
|
static_assert(std::is_same_v<GetCharTypeT<T>, GetCharTypeT<U>>);
|
|
const size_t oldLen = strLength(oldTerm);
|
|
const size_t newLen = strLength(newTerm);
|
|
//assert(oldLen != 0); -> reasonable check, but challenged by unit-test
|
|
if (oldLen == 0)
|
|
return;
|
|
|
|
const auto* const oldBegin = strBegin(oldTerm);
|
|
const auto* const oldEnd = oldBegin + oldLen;
|
|
|
|
const auto* const newBegin = strBegin(newTerm);
|
|
const auto* const newEnd = newBegin + newLen;
|
|
|
|
using CharType = GetCharTypeT<S>;
|
|
if (oldLen == 1 && newLen == 1) //don't use expensive std::search unless required!
|
|
return std::replace_if(str.begin(), str.end(), [charEqual, charOld = *oldBegin](CharType c) { return charEqual(c, charOld); }, *newBegin);
|
|
|
|
auto* it = strBegin(str); //don't use str.begin() or wxString will return this wxUni* nonsense!
|
|
auto* const strEnd = it + strLength(str);
|
|
|
|
auto itFound = searchFirst(it, strEnd,
|
|
oldBegin, oldEnd, charEqual);
|
|
if (itFound == strEnd)
|
|
return; //optimize "oldTerm not found"
|
|
|
|
S output(it, itFound);
|
|
do
|
|
{
|
|
impl::stringAppend(output, newBegin, newEnd);
|
|
it = itFound + oldLen;
|
|
#if 0
|
|
if (!replaceAll)
|
|
itFound = strEnd;
|
|
else
|
|
#endif
|
|
itFound = searchFirst(it, strEnd,
|
|
oldBegin, oldEnd, charEqual);
|
|
|
|
impl::stringAppend(output, it, itFound);
|
|
}
|
|
while (itFound != strEnd);
|
|
|
|
str = std::move(output);
|
|
}
|
|
}
|
|
|
|
|
|
template <class S, class T, class U> inline
|
|
void replace(S& str, const T& oldTerm, const U& newTerm)
|
|
{ impl::replace(str, oldTerm, newTerm, std::equal_to()); }
|
|
|
|
|
|
template <class S, class T, class U> inline
|
|
S replaceCpy(S str, const T& oldTerm, const U& newTerm)
|
|
{
|
|
replace(str, oldTerm, newTerm);
|
|
return str;
|
|
}
|
|
|
|
|
|
template <class S, class T, class U> inline
|
|
void replaceAsciiNoCase(S& str, const T& oldTerm, const U& newTerm)
|
|
{
|
|
using CharType = GetCharTypeT<S>;
|
|
impl::replace(str, oldTerm, newTerm,
|
|
[](CharType charL, CharType charR) { return asciiToLower(charL) == asciiToLower(charR); });
|
|
}
|
|
|
|
|
|
template <class S, class T, class U> inline
|
|
S replaceCpyAsciiNoCase(S str, const T& oldTerm, const U& newTerm)
|
|
{
|
|
replaceAsciiNoCase(str, oldTerm, newTerm);
|
|
return str;
|
|
}
|
|
|
|
|
|
template <class Char, class Function>
|
|
[[nodiscard]] inline
|
|
std::pair<Char*, Char*> trimCpy2(Char* first, Char* last, TrimSide side, Function trimThisChar)
|
|
{
|
|
if (side == TrimSide::right || side == TrimSide::both)
|
|
while (first != last && trimThisChar(last[-1]))
|
|
--last;
|
|
|
|
if (side == TrimSide::left || side == TrimSide::both)
|
|
while (first != last && trimThisChar(*first))
|
|
++first;
|
|
|
|
return {first, last};
|
|
}
|
|
|
|
|
|
template <class S, class Function> inline
|
|
void trim(S& str, TrimSide side, Function trimThisChar)
|
|
{
|
|
const auto* const oldBegin = strBegin(str);
|
|
const auto [newBegin, newEnd] = trimCpy2(oldBegin, oldBegin + strLength(str), side, trimThisChar);
|
|
|
|
if (newBegin != oldBegin)
|
|
str = S(newBegin, newEnd); //minor inefficiency: in case "str" is not shared, we could save an allocation and do a memory move only
|
|
else
|
|
str.resize(newEnd - newBegin);
|
|
}
|
|
|
|
|
|
template <class S> inline
|
|
void trim(S& str, TrimSide side)
|
|
{
|
|
using CharType = GetCharTypeT<S>;
|
|
trim(str, side, [](CharType c) { return isWhiteSpace(c); });
|
|
}
|
|
|
|
|
|
template <class S> inline
|
|
S trimCpy(const S& str, TrimSide side)
|
|
{
|
|
using CharType = GetCharTypeT<S>;
|
|
const auto* const oldBegin = strBegin(str);
|
|
const auto* const oldEnd = oldBegin + strLength(str);
|
|
|
|
const auto [newBegin, newEnd] = trimCpy2(oldBegin, oldEnd, side, [](CharType c) { return isWhiteSpace(c); });
|
|
|
|
if (newBegin == oldBegin && newEnd == oldEnd)
|
|
return str;
|
|
else
|
|
return S(newBegin, newEnd - newBegin);
|
|
}
|
|
|
|
|
|
namespace impl
|
|
{
|
|
template <class S, class T>
|
|
struct CopyStringToString
|
|
{
|
|
T copy(const S& src) const
|
|
{
|
|
static_assert(!std::is_same_v<std::decay_t<S>, std::decay_t<T>>);
|
|
return {strBegin(src), strLength(src)};
|
|
}
|
|
};
|
|
|
|
template <class T>
|
|
struct CopyStringToString<T, T> //perf: we don't need a deep copy if string types match
|
|
{
|
|
template <class S>
|
|
T copy(S&& str) const { return std::forward<S>(str); }
|
|
};
|
|
}
|
|
|
|
template <class T, class S> inline
|
|
T copyStringTo(S&& str) { return impl::CopyStringToString<std::decay_t<S>, T>().copy(std::forward<S>(str)); }
|
|
|
|
|
|
namespace impl
|
|
{
|
|
template <class Num> inline
|
|
int saferPrintf(char* buffer, size_t bufferSize, const char* format, const Num& number) //there is no such thing as a "safe" printf ;)
|
|
{
|
|
return std::snprintf(buffer, bufferSize, format, number); //C99: returns number of chars written if successful, < 0 or >= bufferSize on error
|
|
}
|
|
|
|
template <class Num> inline
|
|
int saferPrintf(wchar_t* buffer, size_t bufferSize, const wchar_t* format, const Num& number)
|
|
{
|
|
return std::swprintf(buffer, bufferSize, format, number); //C99: returns number of chars written if successful, < 0 on error (including buffer too small)
|
|
}
|
|
}
|
|
|
|
template <class S, class T, class Num> inline
|
|
S printNumber(const T& format, const Num& number) //format a single number using ::sprintf
|
|
{
|
|
static_assert(std::is_same_v<GetCharTypeT<S>, GetCharTypeT<T>>);
|
|
assert(strBegin(format)[strLength(format)] == 0); //format must be null-terminated!
|
|
|
|
S buf(128, static_cast<GetCharTypeT<S>>('0'));
|
|
const int charsWritten = impl::saferPrintf(buf.data(), buf.size(), strBegin(format), number);
|
|
|
|
if (charsWritten < 0 || makeUnsigned(charsWritten) > buf.size())
|
|
{
|
|
assert(false);
|
|
return S();
|
|
}
|
|
|
|
buf.resize(charsWritten);
|
|
return buf;
|
|
}
|
|
|
|
|
|
namespace impl
|
|
{
|
|
enum class NumberType
|
|
{
|
|
signedInt,
|
|
unsignedInt,
|
|
floatingPoint,
|
|
other,
|
|
};
|
|
|
|
|
|
template <class S, class Num> S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::other>) = delete;
|
|
#if 0 //default number to string conversion using streams: convenient, but SLOW, SLOW, SLOW!!!! (~ factor of 20)
|
|
template <class S, class Num> inline
|
|
S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::other>)
|
|
{
|
|
std::basic_ostringstream<GetCharTypeT<S>> ss;
|
|
ss << number;
|
|
return copyStringTo<S>(ss.str());
|
|
}
|
|
#endif
|
|
|
|
|
|
template <class S, class Num> inline
|
|
S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::floatingPoint>)
|
|
{
|
|
//don't use sprintf("%g"): way SLOWWWWWWER than std::to_chars()
|
|
|
|
char buffer[128]; //zero-initialize?
|
|
//let's give some leeway, but 24 chars should suffice: https://www.reddit.com/r/cpp/comments/dgj89g/cppcon_2019_stephan_t_lavavej_floatingpoint/f3j7d3q/
|
|
const char* strEnd = toChars(std::begin(buffer), std::end(buffer), number);
|
|
|
|
S output;
|
|
|
|
for (const char c : makeStringView(static_cast<const char*>(buffer), strEnd))
|
|
output += static_cast<GetCharTypeT<S>>(c);
|
|
|
|
return output;
|
|
}
|
|
|
|
|
|
/*
|
|
perf: integer to string: (executed 10 mio. times)
|
|
std::stringstream - 14796 ms
|
|
std::sprintf - 3086 ms
|
|
formatInteger - 778 ms
|
|
*/
|
|
|
|
template <class OutputIterator, class Num> inline
|
|
void formatNegativeInteger(Num n, OutputIterator& it)
|
|
{
|
|
assert(n < 0);
|
|
using CharType = typename std::iterator_traits<OutputIterator>::value_type;
|
|
do
|
|
{
|
|
const Num tmp = n / 10;
|
|
*--it = static_cast<CharType>('0' + (tmp * 10 - n)); //8% faster than using modulus operator!
|
|
n = tmp;
|
|
}
|
|
while (n != 0);
|
|
|
|
*--it = static_cast<CharType>('-');
|
|
}
|
|
|
|
template <class OutputIterator, class Num> inline
|
|
void formatPositiveInteger(Num n, OutputIterator& it)
|
|
{
|
|
assert(n >= 0);
|
|
using CharType = typename std::iterator_traits<OutputIterator>::value_type;
|
|
do
|
|
{
|
|
const Num tmp = n / 10;
|
|
*--it = static_cast<CharType>('0' + (n - tmp * 10)); //8% faster than using modulus operator!
|
|
n = tmp;
|
|
}
|
|
while (n != 0);
|
|
}
|
|
|
|
|
|
template <class S, class Num> inline
|
|
S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::signedInt>)
|
|
{
|
|
GetCharTypeT<S> buffer[2 + sizeof(Num) * 241 / 100]; //zero-initialize?
|
|
//it's generally faster to use a buffer than to rely on String::operator+=() (in)efficiency
|
|
//required chars (+ sign char): 1 + ceil(ln_10(256^sizeof(n) / 2 + 1)) -> divide by 2 for signed half-range; second +1 since one half starts with 1!
|
|
// <= 1 + ceil(ln_10(256^sizeof(n))) =~ 1 + ceil(sizeof(n) * 2.4082) <= 2 + floor(sizeof(n) * 2.41)
|
|
|
|
//caveat: consider INT_MIN: technically -INT_MIN == INT_MIN
|
|
auto it = std::end(buffer);
|
|
if (number < 0)
|
|
formatNegativeInteger(number, it);
|
|
else
|
|
formatPositiveInteger(number, it);
|
|
assert(it >= std::begin(buffer));
|
|
|
|
return S(&*it, std::end(buffer) - it);
|
|
}
|
|
|
|
|
|
template <class S, class Num> inline
|
|
S numberTo(const Num& number, std::integral_constant<NumberType, NumberType::unsignedInt>)
|
|
{
|
|
GetCharTypeT<S> buffer[1 + sizeof(Num) * 241 / 100]; //zero-initialize?
|
|
//required chars: ceil(ln_10(256^sizeof(n))) =~ ceil(sizeof(n) * 2.4082) <= 1 + floor(sizeof(n) * 2.41)
|
|
|
|
auto it = std::end(buffer);
|
|
formatPositiveInteger(number, it);
|
|
assert(it >= std::begin(buffer));
|
|
|
|
return S(&*it, std::end(buffer) - it);
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------
|
|
|
|
template <class Num, class S> Num stringTo(const S& str, std::integral_constant<NumberType, NumberType::other>) = delete;
|
|
#if 0 //default string to number conversion using streams: convenient, but SLOW
|
|
template <class Num, class S> inline
|
|
Num stringTo(const S& str, std::integral_constant<NumberType, NumberType::other>)
|
|
{
|
|
using CharType = GetCharTypeT<S>;
|
|
Num number = 0;
|
|
std::basic_istringstream<CharType>(copyStringTo<std::basic_string<CharType>>(str)) >> number;
|
|
return number;
|
|
}
|
|
#endif
|
|
|
|
|
|
inline
|
|
double stringToFloat(const char* first, const char* last)
|
|
{
|
|
//don't use std::strtod(): 1. requires null-terminated string 2. SLOWER than std::from_chars()
|
|
return fromChars(first, last);
|
|
}
|
|
|
|
|
|
inline
|
|
double stringToFloat(const wchar_t* first, const wchar_t* last)
|
|
{
|
|
std::string buf; //let's rely on SSO
|
|
|
|
for (const wchar_t c : makeStringView(first, last))
|
|
buf += static_cast<char>(c);
|
|
|
|
return fromChars(buf.c_str(), buf.c_str() + buf.size());
|
|
}
|
|
|
|
|
|
template <class Num, class S> inline
|
|
Num stringTo(const S& str, std::integral_constant<NumberType, NumberType::floatingPoint>)
|
|
{
|
|
const auto* const first = strBegin(str);
|
|
const auto* const last = first + strLength(str);
|
|
return static_cast<Num>(stringToFloat(first, last));
|
|
}
|
|
|
|
|
|
template <class Num, class S>
|
|
Num extractInteger(const S& str, bool& hasMinusSign) //very fast conversion to integers: slightly faster than std::atoi, but more importantly: generic
|
|
{
|
|
using CharType = GetCharTypeT<S>;
|
|
|
|
const CharType* first = strBegin(str);
|
|
const CharType* last = first + strLength(str);
|
|
|
|
while (first != last && isWhiteSpace(*first)) //skip leading whitespace
|
|
++first;
|
|
|
|
hasMinusSign = false;
|
|
if (first != last)
|
|
{
|
|
if (*first == static_cast<CharType>('-'))
|
|
{
|
|
hasMinusSign = true;
|
|
++first;
|
|
}
|
|
else if (*first == static_cast<CharType>('+'))
|
|
++first;
|
|
}
|
|
|
|
Num number = 0;
|
|
|
|
for (const CharType c : makeStringView(first, last))
|
|
if (static_cast<CharType>('0') <= c && c <= static_cast<CharType>('9'))
|
|
{
|
|
number *= 10;
|
|
number += c - static_cast<CharType>('0');
|
|
}
|
|
else //rest of string should contain whitespace only, it's NOT a bug if there is something else!
|
|
break; //assert(std::all_of(it, last, isWhiteSpace<CharType>)); -> this is NO assert situation
|
|
|
|
return number;
|
|
}
|
|
|
|
|
|
template <class Num, class S> inline
|
|
Num stringTo(const S& str, std::integral_constant<NumberType, NumberType::signedInt>)
|
|
{
|
|
bool hasMinusSign = false; //handle minus sign
|
|
const Num number = extractInteger<Num>(str, hasMinusSign);
|
|
return hasMinusSign ? -number : number;
|
|
}
|
|
|
|
|
|
template <class Num, class S> inline
|
|
Num stringTo(const S& str, std::integral_constant<NumberType, NumberType::unsignedInt>) //very fast conversion to integers: slightly faster than std::atoi, but more importantly: generic
|
|
{
|
|
bool hasMinusSign = false; //handle minus sign
|
|
const Num number = extractInteger<Num>(str, hasMinusSign);
|
|
if (hasMinusSign)
|
|
{
|
|
assert(false);
|
|
return -makeSigned(number); //at least make some noise
|
|
}
|
|
return number;
|
|
}
|
|
}
|
|
|
|
|
|
template <class S, class Num> inline
|
|
S numberTo(const Num& number)
|
|
{
|
|
using TypeTag = std::integral_constant<impl::NumberType,
|
|
isSignedInt <Num> ? impl::NumberType::signedInt :
|
|
isUnsignedInt<Num> ? impl::NumberType::unsignedInt :
|
|
isFloat <Num> ? impl::NumberType::floatingPoint :
|
|
impl::NumberType::other>;
|
|
|
|
return impl::numberTo<S>(number, TypeTag());
|
|
}
|
|
|
|
|
|
template <class Num, class S> inline
|
|
Num stringTo(const S& str)
|
|
{
|
|
using TypeTag = std::integral_constant<impl::NumberType,
|
|
isSignedInt <Num> ? impl::NumberType::signedInt :
|
|
isUnsignedInt<Num> ? impl::NumberType::unsignedInt :
|
|
isFloat <Num> ? impl::NumberType::floatingPoint :
|
|
impl::NumberType::other>;
|
|
|
|
return impl::stringTo<Num>(str, TypeTag());
|
|
}
|
|
|
|
|
|
inline //hexify beats "printNumber<std::string>("%02X", c)" by a nice factor of 3!
|
|
std::pair<char, char> hexify(unsigned char c, bool upperCase)
|
|
{
|
|
auto hexifyDigit = [upperCase](int num) -> char //input [0, 15], output 0-9, A-F
|
|
{
|
|
assert(0 <= num&& num <= 15); //guaranteed by design below!
|
|
if (num <= 9)
|
|
return static_cast<char>('0' + num); //no signed/unsigned char problem here!
|
|
|
|
if (upperCase)
|
|
return static_cast<char>('A' + (num - 10));
|
|
else
|
|
return static_cast<char>('a' + (num - 10));
|
|
};
|
|
return {hexifyDigit(c / 16), hexifyDigit(c % 16)};
|
|
}
|
|
|
|
|
|
inline //unhexify beats "::sscanf(&it[3], "%02X", &tmp)" by a factor of 3000 for ~250000 calls!!!
|
|
char unhexify(char high, char low)
|
|
{
|
|
auto unhexifyDigit = [](const char hex) -> int //input 0-9, a-f, A-F; output range: [0, 15]
|
|
{
|
|
if ('0' <= hex && hex <= '9') //no signed/unsigned char problem here!
|
|
return hex - '0';
|
|
else if ('A' <= hex && hex <= 'F')
|
|
return (hex - 'A') + 10;
|
|
else if ('a' <= hex && hex <= 'f')
|
|
return (hex - 'a') + 10;
|
|
assert(false);
|
|
return 0;
|
|
};
|
|
return static_cast<unsigned char>(16 * unhexifyDigit(high) + unhexifyDigit(low)); //[!] convert to unsigned char first, then to char (which may be signed)
|
|
}
|
|
|
|
|
|
inline
|
|
std::string formatAsHexString(const std::string_view& blob)
|
|
{
|
|
std::string output;
|
|
for (const char c : blob)
|
|
{
|
|
const auto [high, low] = hexify(c, false /*upperCase*/);
|
|
output += high;
|
|
output += low;
|
|
}
|
|
return output;
|
|
}
|
|
|
|
|
|
|
|
|
|
template <class Num, class S> inline
|
|
Num hashString(const S& str)
|
|
{
|
|
using CharType = GetCharTypeT<S>;
|
|
const auto* const strFirst = strBegin(str);
|
|
|
|
FNV1aHash<Num> hash;
|
|
std::for_each(strFirst, strFirst + strLength(str), [&hash](CharType c) { hash.add(c); });
|
|
return hash.get();
|
|
}
|
|
|
|
|
|
struct StringHash
|
|
{
|
|
using is_transparent = int; //enable heterogenous lookup!
|
|
|
|
template <class String>
|
|
size_t operator()(const String& str) const { return hashString<size_t>(str); }
|
|
};
|
|
|
|
|
|
struct StringEqual
|
|
{
|
|
using is_transparent = int; //enable heterogenous lookup!
|
|
|
|
template <class String1, class String2>
|
|
bool operator()(const String1& lhs, const String2& rhs) const { return equalString(lhs, rhs); }
|
|
};
|
|
|
|
|
|
struct LessAsciiNoCase
|
|
{
|
|
template <class String>
|
|
bool operator()(const String& lhs, const String& rhs) const { return compareAsciiNoCase(lhs, rhs) < 0; }
|
|
};
|
|
|
|
|
|
struct StringHashAsciiNoCase
|
|
{
|
|
using is_transparent = int; //allow heterogenous lookup!
|
|
|
|
template <class String>
|
|
size_t operator()(const String& str) const
|
|
{
|
|
using CharType = GetCharTypeT<String>;
|
|
const auto* const strFirst = strBegin(str);
|
|
|
|
FNV1aHash<size_t> hash;
|
|
std::for_each(strFirst, strFirst + strLength(str), [&hash](CharType c) { hash.add(asciiToLower(c)); });
|
|
return hash.get();
|
|
}
|
|
};
|
|
|
|
|
|
struct StringEqualAsciiNoCase
|
|
{
|
|
using is_transparent = int; //allow heterogenous lookup!
|
|
|
|
template <class String1, class String2>
|
|
bool operator()(const String1& lhs, const String2& rhs) const
|
|
{
|
|
return equalAsciiNoCase(lhs, rhs);
|
|
}
|
|
};
|
|
}
|
|
|
|
#endif //STRING_TOOLS_H_213458973046
|