Support sorting by 'random'

This adds a new 'random' sort key, which is stable within a single run
of `task` but will produce a different result for each run. This allows,
for example, selecting a random task with `task all sort:random
limit:1`.
This commit is contained in:
Dustin J. Mitchell 2025-08-18 08:22:36 -04:00
parent f41a16a0a5
commit cfa0370489
No known key found for this signature in database
4 changed files with 104 additions and 1 deletions

View file

@ -1279,6 +1279,10 @@ grouping.
A special sort value of "none" indicates that no sorting is required, and tasks A special sort value of "none" indicates that no sorting is required, and tasks
will be presented in the order (if any) in which they are selected. will be presented in the order (if any) in which they are selected.
A special sort value of "random" indicates that the tasks will be sorted in a
random order. Each time a report with 'sort:random' is run, a new random
order will be generated.
.TP .TP
.B report.X.filter .B report.X.filter
This adds a filter to the report X so that only tasks matching the filter This adds a filter to the report X so that only tasks matching the filter

View file

@ -36,13 +36,16 @@
#include <util.h> #include <util.h>
#include <algorithm> #include <algorithm>
#include <functional>
#include <list> #include <list>
#include <map> #include <map>
#include <random>
#include <string> #include <string>
#include <vector> #include <vector>
static std::vector<Task>* global_data = nullptr; static std::vector<Task>* global_data = nullptr;
static std::vector<std::string> global_keys; static std::vector<std::string> global_keys;
static unsigned int sort_random_seed = 0;
static bool sort_compare(int, int); static bool sort_compare(int, int);
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
@ -53,6 +56,19 @@ void sort_tasks(std::vector<Task>& data, std::vector<int>& order, const std::str
// Split the key defs. // Split the key defs.
global_keys = split(keys, ','); global_keys = split(keys, ',');
// Generate a random seend for sorting by "random".
if (sort_random_seed == 0) {
// For testing purposes, allow the seed to be specified in an undocumented configuration
// setting.
std::string seed_str = Context::getContext().config.get("debug.random.seed");
if (seed_str.empty()) {
std::random_device rd;
sort_random_seed = rd();
} else {
sort_random_seed = std::stoul(seed_str);
}
}
// Only sort if necessary. // Only sort if necessary.
if (order.size()) std::stable_sort(order.begin(), order.end(), sort_compare); if (order.size()) std::stable_sort(order.begin(), order.end(), sort_compare);
@ -110,8 +126,25 @@ static bool sort_compare(int left, int right) {
for (auto& k : global_keys) { for (auto& k : global_keys) {
Context::getContext().decomposeSortField(k, field, ascending, breakIndicator); Context::getContext().decomposeSortField(k, field, ascending, breakIndicator);
// Random.
if (field == "random") {
// For "random" sort, we produce a stable number for each task based on a hash of its
// UUID plus the random seed.
std::string left_uuid = (*global_data)[left].get("uuid");
std::string right_uuid = (*global_data)[right].get("uuid");
std::string left_scrambled =
std::to_string(std::hash<std::string>{}(left_uuid + std::to_string(sort_random_seed)));
std::string right_scrambled =
std::to_string(std::hash<std::string>{}(right_uuid + std::to_string(sort_random_seed)));
if (left_scrambled == right_scrambled) continue;
return ascending ? (left_scrambled < right_scrambled) : (left_scrambled > right_scrambled);
}
// Urgency. // Urgency.
if (field == "urgency") { else if (field == "urgency") {
left_real = (*global_data)[left].urgency(); left_real = (*global_data)[left].urgency();
right_real = (*global_data)[right].urgency(); right_real = (*global_data)[right].urgency();

View file

@ -156,6 +156,7 @@ set (pythonTests
project.test.py project.test.py
purge.test.py purge.test.py
quotes.test.py quotes.test.py
random_sort.test.py
rc.override.test.py rc.override.test.py
read-only.test.py read-only.test.py
recurrence.test.py recurrence.test.py

65
test/random_sort.test.py Executable file
View file

@ -0,0 +1,65 @@
#!/usr/bin/env python3
###############################################################################
#
# Copyright 2006 - 2025, Tomas Babej, Paul Beckingham, Federico Hernandez.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# https://www.opensource.org/licenses/mit-license.php
#
###############################################################################
import sys
import os
import unittest
# Ensure python finds the local simpletap module
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from basetest import Task, TestCase
class TestRandomSort(TestCase):
def setUp(self):
self.t = Task()
self.t("add one")
self.t("add two")
self.t("add three")
self.t("add four")
self.t("add five")
def test_random_sort_deterministic(self):
"""Verify that 'sort:random' with different seeds produces different orderings."""
# With a fixed seed, the order is always the same.
code, out1_seed1, err = self.t("rc.debug.random.seed=123 all rc.report.all.sort:random")
code, out2_seed1, err = self.t("rc.debug.random.seed=123 all rc.report.all.sort:random")
self.assertEqual(out1_seed1, out2_seed1, "Random sort with the same seed should produce the same order")
# With a different fixed seed, the order is different.
code, out1_seed2, err = self.t("rc.debug.random.seed=456 all rc.report.all.sort:random")
self.assertNotEqual(out1_seed1, out1_seed2, "Random sort with different seeds should produce different orders")
def test_random_and_id_sort(self):
"""Verify that 'sort:random,id' is not the same as 'sort:id'."""
code, out_id, err = self.t("all rc.report.all.sort:id")
code, out_random_id, err = self.t("rc.debug.random.seed=123 all rc.report.all.sort:random,id")
self.assertNotEqual(out_id, out_random_id, "random,id sort should be different from id sort")
if __name__ == "__main__":
from simpletap import TAPTestRunner
unittest.main(testRunner=TAPTestRunner())