mu-query-threads: small refinements
Ensure we sort correctly (sort_container), since the last message in a thread is not necessarily the last (when not sorted by thread-date). own_date_key is redundant, so let's remove it.
This commit is contained in:
@ -62,7 +62,7 @@ struct Container {
|
|||||||
return up && up == other->ur_parent();
|
return up && up == other->ur_parent();
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename Func> void for_each_child(Func&& func) {
|
template <typename Func> void for_each_child(const Func& func) {
|
||||||
auto it{children.rbegin()};
|
auto it{children.rbegin()};
|
||||||
while (it != children.rend()) {
|
while (it != children.rend()) {
|
||||||
auto next = std::next(it);
|
auto next = std::next(it);
|
||||||
@ -80,13 +80,6 @@ struct Container {
|
|||||||
|
|
||||||
std::string thread_date_key;
|
std::string thread_date_key;
|
||||||
|
|
||||||
// The container's own date-key, set once from its query-match and never
|
|
||||||
// overwritten by the bubble-up above. Siblings must sort by this, not by
|
|
||||||
// thread_date_key -- otherwise a subthread gaining a new, later message
|
|
||||||
// (e.g. a reply to one message in a patch series) reorders it among its
|
|
||||||
// siblings, even though its own date didn't change.
|
|
||||||
std::string own_date_key;
|
|
||||||
|
|
||||||
Option<QueryMatch&> query_match;
|
Option<QueryMatch&> query_match;
|
||||||
bool is_nuked{};
|
bool is_nuked{};
|
||||||
Container* parent{};
|
Container* parent{};
|
||||||
@ -195,12 +188,14 @@ determine_id_table(QueryResultsType& qres)
|
|||||||
}();
|
}();
|
||||||
|
|
||||||
// We sort by date (ascending), *except* for the root; we don't
|
// We sort by date (ascending), *except* for the root; we don't
|
||||||
// know what query_matchs will be at the root level yet, so remember
|
// know what query_matchs will be at the root level yet, so
|
||||||
// both. Moreover, even when sorting the top-level in descending
|
// remember both.
|
||||||
|
//
|
||||||
|
// Moreover, even when sorting the top-level in descending
|
||||||
// order, still sort the thread levels below that in ascending
|
// order, still sort the thread levels below that in ascending
|
||||||
// order, keeping sibling order stable.
|
// order, keeping sibling order stable.
|
||||||
container.own_date_key = container.thread_date_key =
|
container.thread_date_key = container.query_match->date_key =
|
||||||
container.query_match->date_key = mi.date_str().value_or("");
|
mi.date_str().value_or("");
|
||||||
// initial guess for the thread-date; might be updated
|
// initial guess for the thread-date; might be updated
|
||||||
// later.
|
// later.
|
||||||
|
|
||||||
@ -494,17 +489,24 @@ sort_container(Container& container)
|
|||||||
for (auto& child : container.children)
|
for (auto& child : container.children)
|
||||||
sort_container(*child);
|
sort_container(*child);
|
||||||
|
|
||||||
// now sort this level, by each child's own date, falling back to
|
// now sort this level, by each child's _own_ date, not the (bubbled-up)
|
||||||
// thread_date_key if the container has no message of its own; use a
|
// thread-date -- otherwise a subthread gaining a new message could
|
||||||
// stable sort so messages with equal dates keep their original (mset)
|
// reorder wrongly.
|
||||||
// order.
|
//
|
||||||
std::ranges::stable_sort(container.children, {}, [](const Container* c) {
|
// Fall back to the thread-date for containers without a message of
|
||||||
return c->own_date_key.empty() ? c->thread_date_key : c->own_date_key;
|
// their own; use a stable sort so messages with equal dates keep their
|
||||||
|
// original (mset) order.
|
||||||
|
std::ranges::stable_sort(container.children, {},
|
||||||
|
[](const Container* c) -> const std::string& {
|
||||||
|
return c->query_match ? c->query_match->date_key : c->thread_date_key;
|
||||||
});
|
});
|
||||||
|
|
||||||
// and 'bubble up' the date of the *newest* message with a date. We
|
// and 'bubble up' the date of the *newest* message with a date. We
|
||||||
// reasonably assume that it's later than its parent.
|
// reasonably assume that it's later than its parent. Note that since
|
||||||
const auto& newest_date = container.children.back()->thread_date_key;
|
// children are not sorted by thread-date, the newest is not necessarily
|
||||||
|
// the last child.
|
||||||
|
const auto& newest_date = std::ranges::max(
|
||||||
|
container.children, {}, &Container::thread_date_key)->thread_date_key;
|
||||||
if (!newest_date.empty())
|
if (!newest_date.empty())
|
||||||
container.thread_date_key = newest_date;
|
container.thread_date_key = newest_date;
|
||||||
}
|
}
|
||||||
@ -735,6 +737,32 @@ test_sibling_order_stable()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_thread_date_bubble_up()
|
||||||
|
{
|
||||||
|
// thread A's newest message (date 9) hangs under a middle sibling;
|
||||||
|
// thread B (date 5) is older than that, so ascending order must put A
|
||||||
|
// *after* B.
|
||||||
|
auto results = MockQueryResults{
|
||||||
|
MockQueryResult{"a", "1", {}},
|
||||||
|
MockQueryResult{"a1", "2", {"a"}},
|
||||||
|
MockQueryResult{"a2", "3", {"a"}},
|
||||||
|
MockQueryResult{"reply-a1", "9", {"a", "a1"}},
|
||||||
|
MockQueryResult{"b", "5", {}},
|
||||||
|
};
|
||||||
|
|
||||||
|
calculate_threads(results, false);
|
||||||
|
|
||||||
|
assert_thread_paths(results,
|
||||||
|
{
|
||||||
|
{"b", "0"},
|
||||||
|
{"a", "1"},
|
||||||
|
{"a1", "1:0"},
|
||||||
|
{"reply-a1", "1:0:0"},
|
||||||
|
{"a2", "1:1"},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
test_id_table_inconsistent()
|
test_id_table_inconsistent()
|
||||||
{
|
{
|
||||||
@ -966,6 +994,7 @@ try {
|
|||||||
g_test_add_func("/threader/sort/ascending", test_sort_ascending);
|
g_test_add_func("/threader/sort/ascending", test_sort_ascending);
|
||||||
g_test_add_func("/threader/sort/decending", test_sort_descending);
|
g_test_add_func("/threader/sort/decending", test_sort_descending);
|
||||||
g_test_add_func("/threader/sort/sibling-order-stable", test_sibling_order_stable);
|
g_test_add_func("/threader/sort/sibling-order-stable", test_sibling_order_stable);
|
||||||
|
g_test_add_func("/threader/sort/thread-date-bubble-up", test_thread_date_bubble_up);
|
||||||
|
|
||||||
g_test_add_func("/threader/id-table-inconsistent", test_id_table_inconsistent);
|
g_test_add_func("/threader/id-table-inconsistent", test_id_table_inconsistent);
|
||||||
g_test_add_func("/threader/dups/dup-last", test_dups_dup_last);
|
g_test_add_func("/threader/dups/dup-last", test_dups_dup_last);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
** Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
** Copyright (C) 2021-2026 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||||
**
|
**
|
||||||
** This program is free software; you can redistribute it and/or modify it
|
** This program is free software; you can redistribute it and/or modify it
|
||||||
** under the terms of the GNU General Public License as published by the
|
** under the terms of the GNU General Public License as published by the
|
||||||
@ -28,6 +28,9 @@ namespace Mu {
|
|||||||
* thread-paths for each message, so we can let Xapian order them in the correct
|
* thread-paths for each message, so we can let Xapian order them in the correct
|
||||||
* order.
|
* order.
|
||||||
*
|
*
|
||||||
|
* This is strongly inspired by the JWZ message threading algorithm
|
||||||
|
* https://www.jwz.org/doc/threading.html
|
||||||
|
*
|
||||||
* Note - threads are sorted chronologically, and the messages below the top
|
* Note - threads are sorted chronologically, and the messages below the top
|
||||||
* level are always sorted in ascending orde
|
* level are always sorted in ascending orde
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user