From 783ff922112e455eda0525cd8b87a8ae74a9095d Mon Sep 17 00:00:00 2001 From: priyanshubtp Date: Sun, 25 Oct 2026 15:24:43 +0530 Subject: [PATCH] feat: use due date as secondary key for priority sorting --- .../home/controllers/home_controller.dart | 2 + lib/app/modules/home/views/show_tasks.dart | 40 +++++++- .../home/views/show_tasks_replica.dart | 21 +++- lib/app/utils/taskfunctions/comparator.dart | 38 +++++++- test/utils/taskfunctions/comparator_test.dart | 96 +++++++++++++++++++ 5 files changed, 189 insertions(+), 8 deletions(-) diff --git a/lib/app/modules/home/controllers/home_controller.dart b/lib/app/modules/home/controllers/home_controller.dart index 4e650641..866bba41 100644 --- a/lib/app/modules/home/controllers/home_controller.dart +++ b/lib/app/modules/home/controllers/home_controller.dart @@ -321,6 +321,8 @@ class HomeController extends GetxController { int result; if (sortColumn == 'id') { result = a.id!.compareTo(b.id!); + } else if (sortColumn == 'Priority') { + return compareTasks(selectedSort.value)(a, b); } else { result = compareTasks(sortColumn)(a, b); } diff --git a/lib/app/modules/home/views/show_tasks.dart b/lib/app/modules/home/views/show_tasks.dart index 8421e18d..b2cf4062 100644 --- a/lib/app/modules/home/views/show_tasks.dart +++ b/lib/app/modules/home/views/show_tasks.dart @@ -61,13 +61,19 @@ class TaskViewBuilder extends StatelessWidget { case 'Modified-': return b.modified!.compareTo(a.modified!); case 'Due till+': - return a.due!.compareTo(b.due!); + return _compareDue(a.due, b.due); case 'Due till-': - return b.due!.compareTo(a.due!); + return _compareDue(b.due, a.due); case 'Priority-': - return b.priority!.compareTo(a.priority!); + final p = _priorityRank(b.priority) + .compareTo(_priorityRank(a.priority)); + if (p != 0) return p; + return _compareDue(a.due, b.due); case 'Priority+': - return a.priority!.compareTo(b.priority!); + final p = _priorityRank(a.priority) + .compareTo(_priorityRank(b.priority)); + if (p != 0) return p; + return _compareDue(a.due, b.due); case 'Project+': return a.project!.compareTo(b.project!); case 'Project-': @@ -236,6 +242,32 @@ class TaskViewBuilder extends StatelessWidget { taskDatabase.markTaskAsDeleted(uuid); } + int _priorityRank(String? priority) { + switch (priority) { + case 'H': + return 3; + case 'M': + return 2; + case 'L': + return 1; + default: + return 0; + } + } + + int _compareDue(String? dueA, String? dueB) { + final DateTime? da = dueA != null ? DateTime.tryParse(dueA) : null; + final DateTime? db = dueB != null ? DateTime.tryParse(dueB) : null; + if (da != null && db != null) { + return da.compareTo(db); + } else if (da != null) { + return -1; + } else if (db != null) { + return 1; + } + return 0; + } + Color _getPriorityColor(String priority) { switch (priority) { case 'H': diff --git a/lib/app/modules/home/views/show_tasks_replica.dart b/lib/app/modules/home/views/show_tasks_replica.dart index 3bcb831e..37601c9b 100644 --- a/lib/app/modules/home/views/show_tasks_replica.dart +++ b/lib/app/modules/home/views/show_tasks_replica.dart @@ -84,11 +84,15 @@ class TaskReplicaViewBuilder extends StatelessWidget { case 'Due till-': return (b.due ?? '').compareTo(a.due ?? ''); case 'Priority+': - return _priorityRank(a.priority) + final p = _priorityRank(a.priority) .compareTo(_priorityRank(b.priority)); + if (p != 0) return p; + return _compareDue(a.due, b.due); case 'Priority-': - return _priorityRank(b.priority) + final p = _priorityRank(b.priority) .compareTo(_priorityRank(a.priority)); + if (p != 0) return p; + return _compareDue(a.due, b.due); case 'Project+': return (a.project ?? '').compareTo(b.project ?? ''); case 'Project-': @@ -253,6 +257,19 @@ class TaskReplicaViewBuilder extends StatelessWidget { return tags.join(' '); } + int _compareDue(String? dueA, String? dueB) { + final DateTime? da = dueA != null ? DateTime.tryParse(dueA) : null; + final DateTime? db = dueB != null ? DateTime.tryParse(dueB) : null; + if (da != null && db != null) { + return da.compareTo(db); + } else if (da != null) { + return -1; + } else if (db != null) { + return 1; + } + return 0; + } + // Rank priorities by severity (H > M > L > none) so Priority sort is // meaningful rather than alphabetical (which would order H < L < M). int _priorityRank(String? priority) { diff --git a/lib/app/utils/taskfunctions/comparator.dart b/lib/app/utils/taskfunctions/comparator.dart index a20300e2..59209d38 100644 --- a/lib/app/utils/taskfunctions/comparator.dart +++ b/lib/app/utils/taskfunctions/comparator.dart @@ -4,6 +4,30 @@ import 'package:taskwarrior/app/models/json/task.dart'; import 'package:taskwarrior/app/utils/taskfunctions/urgency.dart'; +int _priorityRank(String? priority) { + switch (priority) { + case 'H': + return 3; + case 'M': + return 2; + case 'L': + return 1; + default: + return 0; + } +} + +int _compareDue(DateTime? a, DateTime? b) { + if (a != null && b != null) { + return a.compareTo(b); + } else if (a != null) { + return -1; + } else if (b != null) { + return 1; + } + return 0; +} + int Function(Task, Task) compareTasks(String column) { return (a, b) { int? result; @@ -45,9 +69,19 @@ int Function(Task, Task) compareTasks(String column) { } break; case 'Priority': - var compare = {'H': 2, 'M': 1, 'L': 0}; + case 'Priority+': result = - (compare[a.priority] ?? -1).compareTo(compare[b.priority] ?? -1); + _priorityRank(a.priority).compareTo(_priorityRank(b.priority)); + if (result == 0) { + result = _compareDue(a.due, b.due); + } + break; + case 'Priority-': + result = + _priorityRank(b.priority).compareTo(_priorityRank(a.priority)); + if (result == 0) { + result = _compareDue(a.due, b.due); + } break; case 'Project': result = (a.project ?? '').compareTo(b.project ?? ''); diff --git a/test/utils/taskfunctions/comparator_test.dart b/test/utils/taskfunctions/comparator_test.dart index 5957beee..d6b2631f 100644 --- a/test/utils/taskfunctions/comparator_test.dart +++ b/test/utils/taskfunctions/comparator_test.dart @@ -70,6 +70,102 @@ void main() { expect(comparator(task2, task1), lessThan(0)); }); + test('should compare tasks with same priority by due date (earlier first)', () { + final taskEarly = createTask(priority: 'H', due: DateTime(2024, 1, 1)); + final taskLate = createTask(priority: 'H', due: DateTime(2024, 1, 10)); + + final comparator = compareTasks('Priority'); + final comparatorPlus = compareTasks('Priority+'); + final comparatorMinus = compareTasks('Priority-'); + + expect(comparator(taskEarly, taskLate), lessThan(0)); + expect(comparator(taskLate, taskEarly), greaterThan(0)); + + expect(comparatorPlus(taskEarly, taskLate), lessThan(0)); + expect(comparatorPlus(taskLate, taskEarly), greaterThan(0)); + + expect(comparatorMinus(taskEarly, taskLate), lessThan(0)); + expect(comparatorMinus(taskLate, taskEarly), greaterThan(0)); + }); + + test('should sort tasks without due date after tasks with due date for same priority', () { + final taskWithDue = createTask(priority: 'M', due: DateTime(2024, 1, 1)); + final taskWithoutDue = createTask(priority: 'M', due: null); + + final comparator = compareTasks('Priority'); + final comparatorPlus = compareTasks('Priority+'); + final comparatorMinus = compareTasks('Priority-'); + + expect(comparator(taskWithDue, taskWithoutDue), lessThan(0)); + expect(comparator(taskWithoutDue, taskWithDue), greaterThan(0)); + + expect(comparatorPlus(taskWithDue, taskWithoutDue), lessThan(0)); + expect(comparatorPlus(taskWithoutDue, taskWithDue), greaterThan(0)); + + expect(comparatorMinus(taskWithDue, taskWithoutDue), lessThan(0)); + expect(comparatorMinus(taskWithoutDue, taskWithDue), greaterThan(0)); + }); + + test('should return 0 when same priority and both missing due date', () { + final task1 = createTask(priority: 'L', due: null); + final task2 = createTask(priority: 'L', due: null); + + final comparator = compareTasks('Priority'); + final comparatorPlus = compareTasks('Priority+'); + final comparatorMinus = compareTasks('Priority-'); + + expect(comparator(task1, task2), equals(0)); + expect(comparatorPlus(task1, task2), equals(0)); + expect(comparatorMinus(task1, task2), equals(0)); + }); + + test('should handle null priority safely and break ties with due date', () { + final taskNoPriorityEarly = createTask(priority: null, due: DateTime(2024, 1, 1)); + final taskNoPriorityLate = createTask(priority: null, due: DateTime(2024, 1, 10)); + final taskNoPriorityUndated = createTask(priority: null, due: null); + final taskWithPriority = createTask(priority: 'H', due: null); + + final comparator = compareTasks('Priority'); + final comparatorPlus = compareTasks('Priority+'); + final comparatorMinus = compareTasks('Priority-'); + + // Null priorities compared to each other should order by due date (earlier first) + expect(comparator(taskNoPriorityEarly, taskNoPriorityLate), lessThan(0)); + expect(comparator(taskNoPriorityLate, taskNoPriorityEarly), greaterThan(0)); + expect(comparator(taskNoPriorityEarly, taskNoPriorityUndated), lessThan(0)); + + expect(comparatorPlus(taskNoPriorityEarly, taskNoPriorityLate), lessThan(0)); + expect(comparatorMinus(taskNoPriorityEarly, taskNoPriorityLate), lessThan(0)); + + // Null priority vs non-null priority + // In Priority-: H precedes null + expect(comparatorMinus(taskWithPriority, taskNoPriorityEarly), lessThan(0)); + expect(comparatorMinus(taskNoPriorityEarly, taskWithPriority), greaterThan(0)); + + // In Priority+: null precedes H + expect(comparatorPlus(taskNoPriorityEarly, taskWithPriority), lessThan(0)); + expect(comparatorPlus(taskWithPriority, taskNoPriorityEarly), greaterThan(0)); + }); + + test('should compare tasks correctly in both Priority+ and Priority- directions', () { + final taskH = createTask(priority: 'H'); + final taskM = createTask(priority: 'M'); + final taskL = createTask(priority: 'L'); + + final comparatorPlus = compareTasks('Priority+'); + final comparatorMinus = compareTasks('Priority-'); + + // Priority+: ascending (L < M < H) + expect(comparatorPlus(taskL, taskM), lessThan(0)); + expect(comparatorPlus(taskM, taskH), lessThan(0)); + expect(comparatorPlus(taskH, taskL), greaterThan(0)); + + // Priority-: descending (H > M > L) + expect(comparatorMinus(taskH, taskM), lessThan(0)); + expect(comparatorMinus(taskM, taskL), lessThan(0)); + expect(comparatorMinus(taskL, taskH), greaterThan(0)); + }); + test('should compare tasks by Project', () { final task1 = createTask(project: 'ProjectA'); final task2 = createTask(project: 'ProjectB');