diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py index 730e910fc..9ca43215a 100644 --- a/openlp/plugins/songs/lib/db.py +++ b/openlp/plugins/songs/lib/db.py @@ -39,6 +39,7 @@ from PyQt4 import QtCore from openlp.core.lib.db import BaseModel, init_db + class Author(BaseModel): """ Author model @@ -66,43 +67,34 @@ class Song(BaseModel): """ Song model """ - _DIGITS = 6 # Do not expect a number greater than 999999. - _RE = re.compile(r'(\d+)') # Match any number at start of string. def __init__(self): - self.sort_string = u'' + self.sort_key = () + + def _try_int(self, s): + "Convert to integer if possible." + try: + return int(s) + except: + return QtCore.QString(s.lower()) + + def _natsort_key(self, s): + "Used internally to get a tuple by which s is sorted." + return map(self._try_int, re.findall(r'(\d+|\D+)', s)) # This decorator tells sqlalchemy to call this method everytime - # any data on this object are updated. + # any data on this object is updated. @reconstructor def init_on_load(self): """ - Precompute string to be used for sorting. + Precompute a tuple to be used for sorting. Song sorting is performance sensitive operation. To get maximum speed lets precompute the string used for comparison. """ - title = self.title - # Ensure titles starting with numbers are sorted properly. - # By default titles like '2 bla' and '10 foo' are sorted like - # 10 foo - # 2 bla - # This is not the desired behaviour. They order should be - # 2 foo - # 10 bla - # - # Usually the workaround done by the user would be to add leading zeros - # 002 foo - # 010 bla - # This this trick is implemented for sort_string where leading zeros are - # added if the title starts with a number. - match = Song._RE.match(title) - if match: - match_len = len(match.group()) - title = title.zfill(len(title) + (Song._DIGITS - match_len)) # Avoid the overhead of converting string to lowercase and to QString # with every call to sort(). - self.sort_string = QtCore.QString(title.lower()) + self.sort_key = self._natsort_key(self.title) class Topic(BaseModel): diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 05da69f61..2fcfd2ceb 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -49,6 +49,44 @@ from openlp.plugins.songs.lib.ui import SongStrings log = logging.getLogger(__name__) + +def natcmp(a, b): + """ + Natural string comparison which mimics the behaviour of Python's internal + cmp function. + """ + log.debug('a: %s; b: %s', a, b) + if len(a) <= len(b): + for i, key in enumerate(a): + if isinstance(key, int) and isinstance(b[i], int): + result = cmp(key, b[i]) + elif isinstance(key, int) and not isinstance(b[i], int): + result = locale_direct_compare(QtCore.QString(str(key)), b[i]) + elif not isinstance(key, int) and isinstance(b[i], int): + result = locale_direct_compare(key, QtCore.QString(str(b[i]))) + else: + result = locale_direct_compare(key, b[i]) + if result != 0: + return result + if len(a) == len(b): + return 0 + else: + return -1 + else: + for i, key in enumerate(b): + if isinstance(a[i], int) and isinstance(key, int): + result = cmp(a[i], key) + elif isinstance(a[i], int) and not isinstance(key, int): + result = locale_direct_compare(QtCore.QString(str(a[i])), key) + elif not isinstance(a[i], int) and isinstance(key, int): + result = locale_direct_compare(a[i], QtCore.QString(str(key))) + else: + result = locale_direct_compare(a[i], key) + if result != 0: + return result + return 1 + + class SongSearch(object): """ An enumeration for song search methods. @@ -259,8 +297,7 @@ class SongMediaItem(MediaManagerItem): log.debug(u'display results Song') self.saveAutoSelectId() self.listView.clear() - searchresults.sort( - cmp=locale_direct_compare, key=lambda song: song.sort_string) + searchresults.sort(cmp=natcmp, key=lambda song: song.sort_key) for song in searchresults: # Do not display temporary songs if song.temporary: