refactor(contacts): share case-insensitive compare, truncation helpers
The contacts list carried its own lowercase-and-compare sort, its own lowercase substring search, and an inline linear middle-truncation loop for fitting an address to a column width. These duplicated logic that belongs in the shared text helpers. - A new allocation-free case-insensitive compare replaces the per-sort lowercasing; it lowercases ASCII per byte and leaves bytes >= 0x80 alone, so UTF-8 labels sort exactly as before. - Search now uses the shared case-insensitive contains helper. - Middle-truncation to a pixel width moves to a shared template that binary-searches the fit (about log2(n) width measurements instead of up to n) while producing the same result as the old linear shrink. Tests: the compare matches lowercase-then-compare across ASCII, UTF-8 bytes and length ties, and the fit helper matches the old linear loop across lengths and widths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
#include "text_format.h"
|
||||
#include "i18n.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
@@ -90,6 +91,17 @@ std::string truncateMiddle(const std::string& s, int front, int back)
|
||||
return s.substr(0, front) + "..." + s.substr(s.length() - back);
|
||||
}
|
||||
|
||||
int compareIgnoreCase(std::string_view a, std::string_view b)
|
||||
{
|
||||
const std::size_t n = std::min(a.size(), b.size());
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
const unsigned char ca = static_cast<unsigned char>(std::tolower(static_cast<unsigned char>(a[i])));
|
||||
const unsigned char cb = static_cast<unsigned char>(std::tolower(static_cast<unsigned char>(b[i])));
|
||||
if (ca != cb) return ca < cb ? -1 : 1;
|
||||
}
|
||||
return a.size() == b.size() ? 0 : (a.size() < b.size() ? -1 : 1);
|
||||
}
|
||||
|
||||
bool containsIgnoreCase(std::string_view haystack, std::string_view needle)
|
||||
{
|
||||
if (needle.empty()) return true;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -37,6 +38,11 @@ std::string formatTimeAgoShort(std::int64_t timestamp);
|
||||
// An empty needle matches everything. Safe for per-frame filtering of large lists.
|
||||
bool containsIgnoreCase(std::string_view haystack, std::string_view needle);
|
||||
|
||||
// Case-insensitive ASCII three-way compare, allocation-free. Equivalent to
|
||||
// toLower(a).compare(toLower(b)) with C-locale tolower per byte (bytes >= 0x80 are left alone and
|
||||
// compared as unsigned), so UTF-8 labels sort exactly as before. Returns <0, 0, >0.
|
||||
int compareIgnoreCase(std::string_view a, std::string_view b);
|
||||
|
||||
// Middle-ellipsis truncation, half/half form: "prefix...suffix" so the total (including "...")
|
||||
// is at most maxLen. Guards maxLen <= 3 (returns s unchanged) to avoid unsigned underflow on the
|
||||
// (maxLen - 3) split. Strings already at/under maxLen are returned unchanged.
|
||||
@@ -46,5 +52,35 @@ std::string truncateMiddle(const std::string& s, int maxLen);
|
||||
// Returns s unchanged when it is no longer than front + back + 3 (the "..." would not save space).
|
||||
std::string truncateMiddle(const std::string& s, int front, int back);
|
||||
|
||||
// Middle-ellipsis fit-to-width. Same result as a linear shrink (alternating head/tail removal from the
|
||||
// half/half split, ellipsis U+2026), but ~log2(n) measurements instead of up to n. `width(const std::string&)`
|
||||
// returns the rendered width. Assumes width is non-increasing as characters are removed (true for ImGui
|
||||
// text: positive advances, no kerning). Falls back to truncateMiddle(s, minFront, minBack) when nothing fits.
|
||||
template <class Measure>
|
||||
std::string truncateMiddleToFit(const std::string& s, int minFront, int minBack, float maxW, Measure&& width)
|
||||
{
|
||||
if (width(s) <= maxW) return s; // fits in full
|
||||
if (maxW <= 0.0f) return truncateMiddle(s, minFront, minBack); // nothing to measure against
|
||||
const std::size_t n = s.size();
|
||||
const std::size_t minKeep = static_cast<std::size_t>(minFront + minBack);
|
||||
if (n <= minKeep) return truncateMiddle(s, minFront, minBack);
|
||||
const std::size_t head0 = n / 2, tail0 = n - head0;
|
||||
// Candidate after removing k chars (k = 0 is "s with an ellipsis in the middle"): removal strictly
|
||||
// alternates, from the head first when head >= tail, else from the tail.
|
||||
auto build = [&](std::size_t k) {
|
||||
std::size_t h, t;
|
||||
if (head0 >= tail0) { h = head0 - (k + 1) / 2; t = tail0 - k / 2; } // even n: head first
|
||||
else { t = tail0 - (k + 1) / 2; h = head0 - k / 2; } // odd n: tail first
|
||||
return s.substr(0, h) + "\xE2\x80\xA6" + s.substr(n - t);
|
||||
};
|
||||
std::size_t lo = 0, hi = n - minKeep - 1;
|
||||
if (width(build(hi)) > maxW) return truncateMiddle(s, minFront, minBack);
|
||||
while (lo < hi) {
|
||||
const std::size_t mid = lo + (hi - lo) / 2;
|
||||
if (width(build(mid)) <= maxW) hi = mid; else lo = mid + 1;
|
||||
}
|
||||
return build(lo);
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
|
||||
Reference in New Issue
Block a user