"""A Collection of useful miscellaneous functions. misc.py: Collection of useful miscellaneous functions. :Author: Hannes Breytenbach (hannes@saao.ac.za) """ import collections.abc import itertools import operator def first_true_index(iterable, pred=None, default=None): """find the first index position for the which the callable pred returns True""" if pred is None: func = operator.itemgetter(1) else: func = lambda x: pred(x[1]) ii = next(filter(func, enumerate(iterable)), default) # either index-item pair or default return ii[0] if ii else default def first_false_index(iterable, pred=None, default=None): """find the first index position for the which the callable pred returns False""" if pred is None: func = operator.not_ else: func = lambda x: not pred(x) return first_true_index(iterable, func, default) def sortmore(*args, **kw): """ Sorts any number of lists according to: optionally given item sorting key function(s) and/or a global sorting key function. Parameters ---------- One or more lists Keywords -------- globalkey : None revert to sorting by key function globalkey : callable Sort by evaluated value for all items in the lists (call signature of this function needs to be such that it accepts an argument tuple of items from each list. eg.: ``globalkey = lambda *l: sum(l)`` will order all the lists by the sum of the items from each list if key: None sorting done by value of first input list (in this case the objects in the first iterable need the comparison methods __lt__ etc...) if key: callable sorting done by value of key(item) for items in first iterable if key: tuple sorting done by value of (key(item_0), ..., key(item_n)) for items in the first n iterables (where n is the length of the key tuple) i.e. the first callable is the primary sorting criterion, and the rest act as tie-breakers. Returns ------- Sorted lists Examples -------- Capture sorting indices:: l = list('CharacterS') In [1]: sortmore( l, range(len(l)) ) Out[1]: (['C', 'S', 'a', 'a', 'c', 'e', 'h', 'r', 'r', 't'], [0, 9, 2, 4, 5, 7, 1, 3, 8, 6]) In [2]: sortmore( l, range(len(l)), key=str.lower ) Out[2]: (['a', 'a', 'C', 'c', 'e', 'h', 'r', 'r', 'S', 't'], [2, 4, 0, 5, 7, 1, 3, 8, 9, 6]) """ first = list(args[0]) if not len(first): return args globalkey = kw.get('globalkey') key = kw.get('key') if key is None: if globalkey: # if global sort function given and no local (secondary) key given, ==> no tiebreakers key = lambda x: 0 else: key = lambda x: x # if no global sort and no local sort keys given, sort by item values if globalkey is None: globalkey = lambda *x: 0 if not isinstance(globalkey, collections.abc.Callable): raise ValueError('globalkey needs to be callable') if isinstance(key, collections.abc.Callable): k = lambda x: (globalkey(*x), key(x[0])) elif isinstance(key, tuple): key = (k if k else lambda x: 0 for k in key) k = lambda x: (globalkey(*x),) + tuple(f(z) for (f, z) in zip(key, x)) else: raise KeyError( "kw arg 'key' should be None, callable, or a sequence of callables, not {}" .format(type(key))) res = sorted(list(zip(*args)), key=k) if 'order' in kw: if kw['order'].startswith(('descend', 'reverse')): res = reversed(res) return tuple(map(list, zip(*res))) def groupmore(func=None, *its): """Extends the itertools.groupby functionality to arbitrary number of iterators.""" if not func: func = lambda x: x its = sortmore(*its, key=func) nfunc = lambda x: func(x[0]) zipper = itertools.groupby(zip(*its), nfunc) unzipper = ((key, zip(*groups)) for key, groups in zipper) return unzipper