blob: 5626f40ec9576c8cb708d115c27ca7913411b2ec [file]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef UTIL_STRING_UTIL_H_
#define UTIL_STRING_UTIL_H_
#include <stddef.h>
#include <stdint.h>
#include <algorithm>
#include <concepts>
#include <functional>
#include <initializer_list>
#include <iterator>
#include <optional>
#include <ranges>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "platform/base/span.h"
// String query and manipulation utilities.
//
// NOTE: Open Screen provides its own ascii_* classification and conversion
// utilities instead of using <cctype> (e.g. std::isalpha, std::isdigit,
// std::tolower) for the following reasons:
// 1. Safety: <cctype> functions invoke undefined behavior when passed signed
// char values < 0 (such as UTF-8 continuation bytes), requiring tedious
// static_cast<unsigned char>(c).
// 2. Locale Independence: Network protocols, DNS-SD, mDNS, and JSON require
// strict 7-bit US-ASCII behavior independent of the host OS/C locale.
// 3. Constexpr & Performance: These functions are constexpr, branchless, and
// inline, avoiding runtime locale checks and enabling SIMD
// auto-vectorization.
namespace openscreen {
// Determines whether `c` is a valid ASCII alphabetic character code.
template <typename Char>
requires(std::integral<Char>)
constexpr bool ascii_isalpha(Char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
}
// Determines whether `c` is a valid ASCII decimal digit (i.e. [0-9]).
template <typename Char>
requires(std::integral<Char>)
constexpr bool ascii_isdigit(Char c) {
return c >= '0' && c <= '9';
}
// Determines whether `c` is a valid ASCII lower case hexadecimal digit
// (i.e. [a-f0-9]).
template <typename Char>
requires(std::integral<Char>)
constexpr bool ascii_islowerhex(Char c) {
return ascii_isdigit(c) || (c >= 'a' && c <= 'f');
}
// Determines whether `c` is a valid ASCII hexadecimal digit (i.e. [a-fA-F0-9]).
template <typename Char>
requires(std::integral<Char>)
constexpr bool ascii_ishex(Char c) {
return ascii_islowerhex(c) || (c >= 'A' && c <= 'F');
}
// Determines whether `c` is a valid, printable ASCII character.
template <typename Char>
requires(std::integral<Char>)
constexpr bool ascii_isprint(Char c) {
return c >= ' ' && c <= '~';
}
// Determines whether `c` is a whitespace character
// (space, tab, vertical tab, formfeed, linefeed, or carriage return).
template <typename Char>
requires(std::integral<Char>)
constexpr bool ascii_isspace(Char c) {
return c == ' ' || (c >= '\t' && c <= '\r');
}
// If `c` is an upper case ASCII character, returns its lower case equivalent.
// Otherwise, returns `c` unchanged.
template <typename Char>
requires(std::integral<Char>)
constexpr Char ascii_tolower(Char c) {
return (c >= 'A' && c <= 'Z') ? static_cast<Char>(c + ('a' - 'A')) : c;
}
// If `c` is a lower case ASCII character, returns its upper case equivalent.
// Otherwise, returns `c` unchanged.
template <typename Char>
requires(std::integral<Char>)
constexpr Char ascii_toupper(Char c) {
return (c >= 'a' && c <= 'z') ? static_cast<Char>(c - ('a' - 'A')) : c;
}
// Converts `s` to lowercase in-place.
inline void AsciiStrToLower(std::string& s) {
for (char& c : s) {
c = ascii_tolower(c);
}
}
// Creates a lowercase string from a given string_view.
[[nodiscard]] inline std::string AsciiStrToLower(std::string_view s) {
std::string result(s);
AsciiStrToLower(result);
return result;
}
// Converts `s` to uppercase in-place.
inline void AsciiStrToUpper(std::string& s) {
for (char& c : s) {
c = ascii_toupper(c);
}
}
// Creates an uppercase string from a given string_view.
[[nodiscard]] inline std::string AsciiStrToUpper(std::string_view s) {
std::string result(s);
AsciiStrToUpper(result);
return result;
}
// Returns whether given ASCII strings `a` and `b` are equal, ignoring
// case in the comparison.
[[nodiscard]] constexpr bool EqualsIgnoreCase(std::string_view a,
std::string_view b) {
return std::ranges::equal(
a, b, std::equal_to<>{}, [](char c) { return ascii_tolower(c); },
[](char c) { return ascii_tolower(c); });
}
// Returns std::string_view with whitespace stripped from the beginning of the
// given string_view.
[[nodiscard]] constexpr std::string_view StripLeadingAsciiWhitespace(
std::string_view str) {
while (!str.empty() && ascii_isspace(str.front())) {
str.remove_prefix(1);
}
return str;
}
// Returns std::string_view with whitespace stripped from the end of the
// given string_view.
[[nodiscard]] constexpr std::string_view StripTrailingAsciiWhitespace(
std::string_view str) {
while (!str.empty() && ascii_isspace(str.back())) {
str.remove_suffix(1);
}
return str;
}
// Returns std::string_view with whitespace stripped from both ends of the
// given string_view.
[[nodiscard]] constexpr std::string_view StripAsciiWhitespace(
std::string_view str) {
return StripTrailingAsciiWhitespace(StripLeadingAsciiWhitespace(str));
}
// Removes all ASCII whitespace from `s` in-place.
inline std::string& RemoveWhitespace(std::string& s) {
std::erase_if(s, [](char c) { return ascii_isspace(c); });
return s;
}
// Appends string pieces into an existing destination string.
inline void StrAppend(std::string& dest,
std::initializer_list<std::string_view> pieces) {
size_t additional_length = 0;
for (const auto& piece : pieces) {
additional_length += piece.size();
}
dest.reserve(dest.size() + additional_length);
for (const auto& piece : pieces) {
dest.append(piece);
}
}
template <typename... Args>
requires(sizeof...(Args) > 0 &&
(std::convertible_to<Args, std::string_view> && ...))
inline void StrAppend(std::string& dest, const Args&... args) {
StrAppend(dest, {std::string_view(args)...});
}
// Concatenates arguments into a single string.
[[nodiscard]] constexpr std::string StrCat(
std::initializer_list<std::string_view> pieces) {
size_t length = 0;
for (const auto& piece : pieces) {
length += piece.size();
}
std::string result;
result.reserve(length);
for (const auto& piece : pieces) {
result.append(piece);
}
return result;
}
template <typename... Args>
requires(sizeof...(Args) > 0 &&
(std::convertible_to<Args, std::string_view> && ...))
[[nodiscard]] std::string StrCat(const Args&... args) {
return StrCat({std::string_view(args)...});
}
enum class SplitResult {
kDiscardEmpty,
kKeepEmpty,
};
// Splits `value` into tokens separated by `delim`.
// When `result_type` is `kDiscardEmpty` (default), leading and trailing
// delimiters are stripped, and multiple consecutive delimiters are treated as
// one. When `result_type` is `kKeepEmpty`, all delimiters are preserved,
// including empty tokens.
[[nodiscard]] inline std::vector<std::string_view> Split(
std::string_view value,
char delim,
SplitResult result_type = SplitResult::kDiscardEmpty) {
std::vector<std::string_view> tokens;
if (value.empty()) {
return tokens;
}
size_t start = 0;
while (start <= value.size()) {
size_t end = value.find(delim, start);
if (end == std::string_view::npos) {
std::string_view token = value.substr(start);
if (result_type == SplitResult::kKeepEmpty || !token.empty()) {
tokens.push_back(token);
}
break;
}
std::string_view token = value.substr(start, end - start);
if (result_type == SplitResult::kKeepEmpty || !token.empty()) {
tokens.push_back(token);
}
start = end + 1;
}
return tokens;
}
// Splits a string at the first instance of `delim`. Returns std::nullopt if
// `delim` is not found.
[[nodiscard]] constexpr std::optional<
std::pair<std::string_view, std::string_view>>
SplitFirst(std::string_view value, char delim) {
size_t pos = value.find(delim);
if (pos == std::string_view::npos) {
return std::nullopt;
}
return std::pair{value.substr(0, pos), value.substr(pos + 1)};
}
// Splits a string at the last instance of `delim`. Returns std::nullopt if
// `delim` is not found.
[[nodiscard]] constexpr std::optional<
std::pair<std::string_view, std::string_view>>
SplitLast(std::string_view value, char delim) {
size_t pos = value.rfind(delim);
if (pos == std::string_view::npos) {
return std::nullopt;
}
return std::pair{value.substr(0, pos), value.substr(pos + 1)};
}
// Joins a range of strings or string_views with `delimiter`.
template <std::ranges::input_range R>
requires(
std::convertible_to<std::ranges::range_reference_t<R>, std::string_view>)
[[nodiscard]] std::string Join(R&& range, std::string_view delimiter = ", ") {
auto it = std::ranges::begin(range);
auto end = std::ranges::end(range);
if (it == end) {
return {};
}
size_t total_length = 0;
size_t count = 0;
if constexpr (std::ranges::forward_range<R>) {
for (auto cur = it; cur != end; ++cur) {
total_length += std::string_view(*cur).size();
++count;
}
if (count > 1) {
total_length += (count - 1) * delimiter.size();
}
}
std::string result;
if (total_length > 0) {
result.reserve(total_length);
}
result.append(std::string_view(*it));
++it;
for (; it != end; ++it) {
result.append(delimiter);
result.append(std::string_view(*it));
}
return result;
}
// Fallback Join for ranges of other streamable types (e.g. integers).
template <std::ranges::input_range R>
[[nodiscard]] std::string Join(R&& range, std::string_view delimiter = ", ") {
auto it = std::ranges::begin(range);
auto end = std::ranges::end(range);
if (it == end) {
return {};
}
std::stringstream ss;
ss << *it;
++it;
for (; it != end; ++it) {
ss << delimiter << *it;
}
return ss.str();
}
// Returns a string made by concatenating the elements iterated by `[begin,
// end)`, each separated by `delimiter`.
template <std::input_iterator Iterator>
[[nodiscard]] std::string Join(Iterator begin,
Iterator end,
std::string_view delimiter = ", ") {
return Join(std::ranges::subrange{begin, end}, delimiter);
}
// Returns a lowercase hex string representation of the given `bytes`.
inline std::string HexEncode(ByteView bytes) {
static constexpr char kHexChars[] = "0123456789abcdef";
if (bytes.empty()) {
return {};
}
std::string result;
result.resize(bytes.size() * 2);
char* dest = result.data();
for (uint8_t byte : bytes) {
*dest++ = kHexChars[(byte >> 4) & 0x0F];
*dest++ = kHexChars[byte & 0x0F];
}
return result;
}
inline std::string HexEncode(const uint8_t* bytes, size_t len) {
return HexEncode(ByteView(bytes, len));
}
} // namespace openscreen
#endif // UTIL_STRING_UTIL_H_