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:
2026-10-13 16:00:10 -05:00
co-authored by Claude Opus 4.8
parent 32ea4d0524
commit a631e4421a
4 changed files with 93 additions and 17 deletions
+37
View File
@@ -12,6 +12,7 @@
#include <sys/wait.h>
#include <unistd.h>
#endif
#include "util/text_format.h"
#include "chat/chat_crypto.h"
#include "chat/chat_identity.h"
#include "chat/chat_service.h"
@@ -18062,6 +18063,41 @@ void testFillStrip() {
EXPECT_NEAR(tri, trap, 1e-3f);
}
void testContactsTextHelpers() {
// compareIgnoreCase == lower+compare, including UTF-8 bytes and length ties
auto ref = [](std::string a, std::string b) {
for (auto& c : a) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
for (auto& c : b) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
int r = a.compare(b); return r < 0 ? -1 : r > 0 ? 1 : 0;
};
const std::vector<std::string> v = {"", "a", "A", "ab", "AB", "Zed", "zed", "\xC3\x89t\xC3\xA9", "\xC3\xA9", "z", "Z1", "z10"};
for (auto& x : v) for (auto& y : v) {
int r = dragonx::util::compareIgnoreCase(x, y); r = r < 0 ? -1 : r > 0 ? 1 : 0;
EXPECT_EQ(r, ref(x, y));
}
// truncateMiddleToFit == the old linear loop for every width
auto meas = [](const std::string& t) { float w = 0; for (unsigned char c : t) w += 5 + (c % 7); return w; };
auto oldFit = [&](const std::string& s, int mf, int mb, float maxW) {
const std::string floor = dragonx::util::truncateMiddle(s, mf, mb);
if (meas(s) <= maxW) return s;
if (maxW <= 0.0f) return floor;
const std::string ell = "\xE2\x80\xA6";
size_t head = s.size() / 2, tail = s.size() - head;
while (head + tail > static_cast<size_t>(mf + mb)) {
std::string cand = s.substr(0, head) + ell + s.substr(s.size() - tail);
if (meas(cand) <= maxW) return cand;
if (head >= tail) --head; else --tail;
}
return floor;
};
for (size_t len : {8u, 20u, 77u, 78u, 79u}) {
std::string s(len, 'a');
for (size_t i = 0; i < len; ++i) s[i] = static_cast<char>('a' + (i * 7) % 26);
for (float maxW = -1; maxW < 700; maxW += 13.0f)
EXPECT_EQ(dragonx::util::truncateMiddleToFit(s, 5, 5, maxW, meas), oldFit(s, 5, 5, maxW));
}
}
} // namespace
int main()
@@ -18365,6 +18401,7 @@ int main()
testLocalTime();
testChatThreadLayout();
testTextFit();
testContactsTextHelpers();
testTextFitBanner();
testLoadLanguageFromLooseFiles();
testWriteFileIfDifferent();