diff --git a/src/calibre/db/search.py b/src/calibre/db/search.py index fe9cec79c8..5360c9ef38 100644 --- a/src/calibre/db/search.py +++ b/src/calibre/db/search.py @@ -17,6 +17,22 @@ from calibre.utils.search_query_parser import SearchQueryParser, ParseException # TODO: Thread safety of saved searches +def force_to_bool(val): + if isinstance(val, (str, unicode)): + try: + val = icu_lower(val) + if not val: + val = None + elif val in [_('yes'), _('checked'), 'true', 'yes']: + val = True + elif val in [_('no'), _('unchecked'), 'false', 'no']: + val = False + else: + val = bool(int(val)) + except: + val = None + return val + class DateSearch(object): # {{{ def __init__(self): @@ -225,14 +241,57 @@ class NumericSearch(object): # {{{ # }}} +class BoolenSearch(object): # {{{ + + def __init__(self): + self.local_no = icu_lower(_('no')) + self.local_yes = icu_lower(_('yes')) + self.local_unchecked = icu_lower(_('unchecked')) + self.local_checked = icu_lower(_('checked')) + self.local_empty = icu_lower(_('empty')) + self.local_blank = icu_lower(_('blank')) + self.local_bool_values = { + self.local_no, self.local_unchecked, '_no', 'false', 'no', + self.local_yes, self.local_checked, '_yes', 'true', 'yes', + self.local_empty, self.local_blank, '_empty', 'empty'} + + def __call__(self, query, field_iter, bools_are_tristate): + matches = set() + if query not in self.local_bool_values: + raise ParseException(_('Invalid boolean query "{0}"').format(query)) + for val, book_ids in field_iter(): + val = force_to_bool(val) + if not bools_are_tristate: + if val is None or not val: # item is None or set to false + if query in { self.local_no, self.local_unchecked, 'no', '_no', 'false' }: + matches |= book_ids + else: # item is explicitly set to true + if query in { self.local_yes, self.local_checked, 'yes', '_yes', 'true' }: + matches |= book_ids + else: + if val is None: + if query in { self.local_empty, self.local_blank, 'empty', '_empty', 'false' }: + matches |= book_ids + elif not val: # is not None and false + if query in { self.local_no, self.local_unchecked, 'no', '_no', 'true' }: + matches |= book_ids + else: # item is not None and true + if query in { self.local_yes, self.local_checked, 'yes', '_yes', 'true' }: + matches |= book_ids + return matches + +# }}} + class Parser(SearchQueryParser): def __init__(self, dbcache, all_book_ids, gst, date_search, num_search, - limit_search_columns, limit_search_columns_to, locations): + bool_search, limit_search_columns, limit_search_columns_to, + locations): self.dbcache, self.all_book_ids = dbcache, all_book_ids self.all_search_locations = frozenset(locations) self.grouped_search_terms = gst self.date_search, self.num_search = date_search, num_search + self.bool_search = bool_search self.limit_search_columns, self.limit_search_columns_to = ( limit_search_columns, limit_search_columns_to) super(Parser, self).__init__(locations, optimize=True) @@ -344,6 +403,12 @@ class Parser(SearchQueryParser): self.dbcache.fields[location].iter_counts, candidates), location, dt, candidates) + # take care of boolean special case + if dt == 'bool': + return self.bool_search(icu_lower(query), + partial(self.field_iter, location, candidates), + self.dbcache.pref('bools_are_tristate')) + return matches @@ -353,6 +418,7 @@ class Search(object): self.all_search_locations = all_search_locations self.date_search = DateSearch() self.num_search = NumericSearch() + self.bool_search = BoolenSearch() def change_locations(self, newlocs): self.all_search_locations = newlocs @@ -380,7 +446,8 @@ class Search(object): # 0.000974 seconds. sqp = Parser( dbcache, all_book_ids, dbcache.pref('grouped_search_terms'), - self.date_search, self.num_search, prefs[ 'limit_search_columns' ], + self.date_search, self.num_search, self.bool_search, + prefs[ 'limit_search_columns' ], prefs[ 'limit_search_columns_to' ], self.all_search_locations) try: ret = sqp.parse(query) diff --git a/src/calibre/db/tests/reading.py b/src/calibre/db/tests/reading.py index 4792f498f8..6069de8026 100644 --- a/src/calibre/db/tests/reading.py +++ b/src/calibre/db/tests/reading.py @@ -208,6 +208,10 @@ class ReadingTest(BaseTest): '#float:10.01', 'series_index:1', 'series_index:<3', 'id:1', 'id:>2', + # Bool tests + '#yesno:true', '#yesno:false', '#yesno:yes', '#yesno:no', + '#yesno:empty', + # TODO: Tests for searching the size column and # cover:true|false )}