Added semi-magical reST parsing for move effects.
[zzz-pokedex.git] / pokedex / lookup.py
index cb6d5f6..b5b0c6d 100644 (file)
@@ -2,6 +2,7 @@
 from collections import namedtuple
 import os, os.path
 import pkg_resources
+import random
 import re
 import shutil
 
@@ -18,7 +19,10 @@ from pokedex.db import connect
 import pokedex.db.tables as tables
 from pokedex.roomaji import romanize
 
-__all__ = ['open_index', 'lookup']
+__all__ = ['open_index', 'lookup', 'random_lookup']
+
+INTERMEDIATE_LOOKUP_RESULTS = 25
+MAX_LOOKUP_RESULTS = 10
 
 # Dictionary of table name => table class.
 # Need the table name so we can get the class from the table name after we
@@ -173,6 +177,24 @@ rx_is_number = re.compile('^\d+$')
 
 LookupResult = namedtuple('LookupResult',
                           ['object', 'name', 'language', 'exact'])
+
+def _parse_table_name(name):
+    """Takes a singular table name, table name, or table object and returns the
+    table name.
+
+    Returns None for a bogus name.
+    """
+    if hasattr(name, '__tablename__'):
+        return getattr(name, '__tablename__')
+    elif name in indexed_tables:
+        return name
+    elif name + 's' in indexed_tables:
+        return name + 's'
+    else:
+        # Bogus.  Be nice and return dummy
+        return None
+
+
 def lookup(input, valid_types=[], session=None, indices=None, exact_only=False):
     """Attempts to find some sort of object, given a database session and name.
 
@@ -242,10 +264,19 @@ def lookup(input, valid_types=[], session=None, indices=None, exact_only=False):
             # provided
             valid_types = prefixes
 
-    # If the input provided is a number, match it as an id.  Otherwise, name.
-    # Term objects do an exact match, so we don't have to worry about a query
-    # parser tripping on weird characters in the input
-    if rx_is_number.match(name):
+    # Random lookup
+    if name == 'random':
+        return random_lookup(indices=(index, speller),
+                             session=session,
+                             valid_types=valid_types)
+
+    # Do different things depending what the query looks like
+    # Note: Term objects do an exact match, so we don't have to worry about a
+    # query parser tripping on weird characters in the input
+    if '*' in name or '?' in name:
+        exact_only = True
+        query = whoosh.query.Wildcard(u'name', name)
+    elif rx_is_number.match(name):
         # Don't spell-check numbers!
         exact_only = True
         query = whoosh.query.Term(u'row_id', name)
@@ -264,16 +295,7 @@ def lookup(input, valid_types=[], session=None, indices=None, exact_only=False):
     ### Filter by type of object
     type_terms = []
     for valid_type in valid_types:
-        if hasattr(valid_type, '__tablename__'):
-            table_name = getattr(valid_type, '__tablename__')
-        elif valid_type in indexed_tables:
-            table_name = valid_type
-        elif valid_type + 's' in indexed_tables:
-            table_name = valid_type + 's'
-        else:
-            # Bogus.  Be nice and ignore it
-            continue
-
+        table_name = _parse_table_name(valid_type)
         type_terms.append(whoosh.query.Term(u'table', table_name))
 
     if type_terms:
@@ -285,14 +307,14 @@ def lookup(input, valid_types=[], session=None, indices=None, exact_only=False):
     searcher.weighting = LanguageWeighting()  # XXX kosher?  docs say search()
                                               # takes a weighting kw but it
                                               # certainly does not
-    results = searcher.search(query)
+    results = searcher.search(query, limit=INTERMEDIATE_LOOKUP_RESULTS)
 
     # Look for some fuzzy matches if necessary
     if not exact_only and not results:
         exact = False
         results = []
 
-        for suggestion in speller.suggest(name, 25):
+        for suggestion in speller.suggest(name, INTERMEDIATE_LOOKUP_RESULTS):
             query = whoosh.query.Term('name', suggestion)
             results.extend(searcher.search(query))
 
@@ -319,4 +341,37 @@ def lookup(input, valid_types=[], session=None, indices=None, exact_only=False):
     # should have more than 10 here and lost a few.  The speller returns 25 to
     # give us some padding, and should avoid that problem.  Not a big deal if
     # we lose the 25th-most-likely match anyway.
-    return objects[:10]
+    return objects[:MAX_LOOKUP_RESULTS]
+
+
+def random_lookup(valid_types=[], session=None, indices=None):
+    """Takes similar arguments as `lookup()`, but returns a random lookup
+    result from one of the provided `valid_types`.
+    """
+
+    tables = []
+    for valid_type in valid_types:
+        table_name = _parse_table_name(valid_type)
+        if table_name:
+            tables.append(indexed_tables[table_name])
+
+    if not tables:
+        tables = indexed_tables.values()
+
+    # Rather than create an array of many hundred items and pick randomly from
+    # it, just pick a number up to the total number of potential items, then
+    # pick randomly from that, and partition the whole range into chunks
+    total = 0
+    partitions = []
+    for table in tables:
+        count = session.query(table).count()
+        total += count
+        partitions.append((table, count))
+
+    n = random.randint(1, total)
+    while n > partitions[0][1]:
+        n -= partitions[0][1]
+        partitions.pop(0)
+
+    return lookup(unicode(n), valid_types=[ partitions[0][0] ],
+                  indices=indices, session=session)