Commit 3c3b4369 authored by Bram Schoenmakers's avatar Bram Schoenmakers

Add class (and tests) that sorts todo lists.

parent 21a4cd9e
""" This module provides functionality to sort lists with todo items. """
import datetime
import re
from Importance import importance
def is_priority_field(p_field):
""" Returns True when the field name denotes the priority. """
return p_field.startswith('prio')
def get_field_function(p_field):
"""
Given a property (string) of a todo, return a function that attempts to
access that property. If the property could not be located, return the
identity function.
"""
result = lambda a: a
if is_priority_field(p_field):
result = lambda a: a.priority()
elif p_field == 'creationdate' or p_field == 'creation':
# when a task has no creation date, push it to the end by assigning it
# the maximum possible date.
result = (lambda a: a.creation_date() if a.creation_date() \
else datetime.date.max)
elif p_field == 'done' or p_field == 'completed' or p_field == 'completion':
result = (lambda a: a.completion_date() if a.completion_date() \
else datetime.date.max)
elif p_field == 'importance':
result = importance
elif p_field == 'text':
result = lambda a: a.text()
else:
# try to find the corresponding tag
# when a tag is not present, push it to the end of the list by giving
# it an artificially higher value
result = (lambda a: "0" + a.tag_value(p_field) if a.has_tag(p_field) \
else "1")
return result
class Sorter(object):
"""
This class sorts a todo list.
Upon instantiation, a sort string should be passed to the class. Then, a
list of todos must be passed to the sort method, which returns a copy of
the list according to the sort string.
A sort string is a comma separated line of field names, possibly prefixed
with 'asc:' or 'desc' to denote the order in which that field must be
sorted.
Example:
desc:importance,priority,asc:creation
Meaning: a descending sort on the importance value, if equal, an ascending
sort on priority and finally, if still equal an ascending sort on the
creation field. Note that ascending is the default.
The idea is that a list of sort functions is gathered, where the most
specific search is done first. This relies on the fact that sorting is
stable.
"""
def __init__(self, p_sortstring="desc:priority"):
self.sortstring = p_sortstring
self.functions = []
self._parse()
def sort(self, p_todos):
"""
Sorts the list of todos given as a parameter, returns a new sorted
list.
The list is traversed in reverse order, such that the most specific
sort operation is done first, relying on the stability of the sorted()
function.
"""
sorted_todos = p_todos
for function, order in reversed(self.functions):
sorted_todos = sorted(sorted_todos, None, function, order == 'desc')
return sorted_todos
def _parse(self):
"""
Parses a sort string and returns a list of functions and the
desired order.
"""
fields = self.sortstring.lower().split(',')
for field in fields:
parsed_field = re.match( \
r'((?P<order>asc(ending)?|desc(ending)?):)?(?P<field>\S+)', \
field)
if not parsed_field:
continue
order = parsed_field.group('order')
order = 'desc' if order and order.startswith('desc') else 'asc'
field = parsed_field.group('field')
if field:
function = get_field_function(field)
# reverse order for priority: lower characters have higher
# priority
if is_priority_field(field):
order = 'asc' if order == 'desc' else 'desc'
self.functions.append((function, order))
import pdb # FIXME remove this
import unittest
import Sorter
from TestFacilities import load_file, todolist_to_string
class SorterTest(unittest.TestCase):
def sort_file(self,p_filename, p_filename_ref, p_sorter):
"""
Sorts a file and compares it with a reference result.
Also check that the sort algorithm hasn't touched the original data.
"""
todos = load_file(p_filename)
text_before = todolist_to_string(todos)
todos_sorted = todolist_to_string(p_sorter.sort(todos))
todos_ref = todolist_to_string(load_file(p_filename_ref))
self.assertEquals(todos_sorted, todos_ref)
self.assertEquals(todolist_to_string(todos), text_before)
def test_sort1(self):
""" Alphabetically sorted """
sorter = Sorter.Sorter('text')
self.sort_file('data/SorterTest1.txt', 'data/SorterTest1-result.txt', sorter)
def test_sort2a(self):
"""
Ascendingly sorted by priority. Also checks stableness of the sort.
"""
sorter = Sorter.Sorter('prio')
self.sort_file('data/SorterTest2.txt', 'data/SorterTest2-result.txt', sorter)
def test_sort2b(self):
"""
Ascendingly sorted by priority. Also checks stableness of the sort.
"""
sorter = Sorter.Sorter('asc:prio')
self.sort_file('data/SorterTest2.txt', 'data/SorterTest2-result.txt', sorter)
def test_sort3(self):
"""
Descendingly sorted by priority. Also checks stableness of the
sort.
"""
sorter = Sorter.Sorter('desc:prio')
self.sort_file('data/SorterTest3.txt', 'data/SorterTest3-result.txt', sorter)
def test_sort4(self):
""" Ascendingly sorted by due date """
sorter = Sorter.Sorter('due')
self.sort_file('data/SorterTest4.txt', 'data/SorterTest4-result.txt', sorter)
def test_sort5(self):
""" Descendingly sorted by due date """
sorter = Sorter.Sorter('desc:due')
self.sort_file('data/SorterTest5.txt', 'data/SorterTest5-result.txt', sorter)
def test_sort6(self):
""" Ascendingly sorted by creation date """
sorter = Sorter.Sorter('creation')
self.sort_file('data/SorterTest6.txt', 'data/SorterTest6-result.txt', sorter)
def test_sort7(self):
""" Ascendingly sorted by completion date. """
sorter = Sorter.Sorter('completion')
self.sort_file('data/SorterTest7.txt', 'data/SorterTest7-result.txt', sorter)
def test_sort8(self):
""" Descendingly sorted by importance """
sorter = Sorter.Sorter('desc:importance')
self.sort_file('data/SorterTest8.txt', 'data/SorterTest8-result.txt', sorter)
def test_sort9(self):
"""
Sort on multiple levels: first descending importance, then
ascending priority.
"""
sorter = Sorter.Sorter('desc:importance,priority')
self.sort_file('data/SorterTest9.txt', 'data/SorterTest9-result.txt', sorter)
def test_sort10(self):
""" Deal with garbage input. """
sorter = Sorter.Sorter('')
self.sort_file('data/SorterTest9.txt', 'data/SorterTest9.txt', sorter)
def test_sort11(self):
""" Deal with garbage input. """
sorter = Sorter.Sorter('fnord')
self.sort_file('data/SorterTest9.txt', 'data/SorterTest9.txt', sorter)
First
(A) Foo
2014-06-14 Last
(A) Foo
2014-06-14 Last
First
(C) Useless
(B) Minor
(A) Important
(A) Another important task star:1
(B) Minor
(C) Useless
(A) Important
(A) Another important task star:1
(A) Important
(A) Another important task star:1
(B) Minor
(C) Useless
(B) Minor
(C) Useless
(A) Important
(A) Another important task star:1
(C) 2014-05-20 Bar due:2014-06-15
(A) 2014-04-28 Foo due:2014-06-20
(B) 2014-06-14 Baz
(B) 2014-06-14 Baz
(A) 2014-04-28 Foo due:2014-06-20
(C) 2014-05-20 Bar due:2014-06-15
(B) 2014-06-14 Baz
(A) 2014-04-28 Foo due:2014-06-20
(C) 2014-05-20 Bar due:2014-06-15
(B) 2014-06-14 Baz
(A) 2014-04-28 Foo due:2014-06-20
(C) 2014-05-20 Bar due:2014-06-15
(A) 2014-04-28 Foo due:2014-06-20
(C) 2014-05-20 Bar due:2014-06-15
(B) Baz
(B) Baz
(A) 2014-04-28 Foo due:2014-06-20
(C) 2014-05-20 Bar due:2014-06-15
x 2014-05-19 Completed
x 2014-05-20 Completed
2014-05-19 Not completed
(C) 2014-05-17 Not completed (yet)
x 2014-05-20 Completed
2014-05-19 Not completed
(C) 2014-05-17 Not completed (yet)
x 2014-05-19 Completed
(C) Pretty much overdue due:2014-01-01
(B) Equal importance of task below star:1
(A) High priority
(C) This one however is not overdue yet due:3000-01-01
x 2014-01-01 A completed high priority task due:2014-01-01
(B) Equal importance of task below star:1
(A) High priority
x 2014-01-01 A completed high priority task due:2014-01-01
(C) Pretty much overdue due:2014-01-01
(C) This one however is not overdue yet due:3000-01-01
(C) Pretty much overdue due:2014-01-01
(B) Equal importance of task below star:1
(A) High priority
(C) This one however is not overdue yet due:3000-01-01
x 2014-01-01 A completed high priority task due:2014-01-01
(B) Equal importance of task below star:1
(A) High priority
x 2014-01-01 A completed high priority task due:2014-01-01
(C) Pretty much overdue due:2014-01-01
(C) This one however is not overdue yet due:3000-01-01
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment