# -*- coding: utf-8 -*-
from pyparsing import Group, Word, ZeroOrMore, alphas, nums, ParseException
from bisect import bisect_left
from fractions import Fraction
from decimal import Decimal
from numbers import Number
def _cache(f):
"""
Auxiliary decorator used by ``cached_property``.
Parameters
----------
f : callable
A method with no argument (except ``self``).
Returns
-------
callable
The same function, but with a `caching' behavior.
"""
name = f.__name__
# noinspection PyProtectedMember
def _f(*args):
try:
return args[0]._cached_properties[name]
except KeyError:
# Not stored in cache
value = f(*args)
args[0]._cached_properties[name] = value
return value
except AttributeError:
# cache does not even exist
value = f(*args)
args[0]._cached_properties = {name: value}
return value
_f.__doc__ = f.__doc__
return _f
[docs]def cached_property(f):
"""
Decorator used in replacement of @property to put the value in cache automatically.
The first time the attribute is used, it is computed on-demand and put in cache. Later accesses to the
attributes will use the cached value.
Cf. :class:`DeleteCacheMixin` for an example.
"""
return property(_cache(f))
[docs]class DeleteCacheMixin:
"""
Mixin used to delete cached properties.
Cf. decorator :meth:`cached_property`.
Examples
--------
>>> class Example(DeleteCacheMixin):
... @cached_property
... def x(self):
... print('Big computation...')
... return 6 * 7
>>> a = Example()
>>> a.x
Big computation...
42
>>> a.x
42
>>> a.delete_cache()
>>> a.x
Big computation...
42
"""
# noinspection PyAttributeOutsideInit
def delete_cache(self) -> None:
self._cached_properties = dict()
[docs]def parse_weak_order(s: str) -> list:
"""
Convert a string representing a weak order to a list of sets.
Parameters
----------
s : str
Returns
-------
list
A list of sets, where each set is an indifference class. The first set of the list contains the top (= most
liked) candidates, while the last set of the list contains the bottom (= most disliked) candidates.
Examples
--------
>>> s = 'Alice ~ Bob ~ Catherine32 > me > you ~ us > them'
>>> parse_weak_order(s) == [{'Alice', 'Bob', 'Catherine32'}, {'me'}, {'you', 'us'}, {'them'}]
True
"""
# Build the parser
candidate = Word(alphas.upper() + alphas.lower() + nums + '_')
equiv_class = Group(candidate + ZeroOrMore(Word('~').suppress() + candidate))
weak_preference = equiv_class + ZeroOrMore(Word('>').suppress() + equiv_class)
empty_preference = ZeroOrMore(' ')
# if s = 'Jean ~ Titi ~ tata32 > me > you ~ us > them', then
# parsed = [['Jean', 'Titi', 'tata32'], ['me'], ['you', 'us'], ['them']]
try:
parsed = empty_preference.parseString(s, parseAll=True).asList()
except ParseException:
parsed = weak_preference.parseString(s, parseAll=True).asList()
# Final conversion to format [{'Jean', 'tata32', 'Titi'}, {'me'}, {'us', 'you'}, {'them'}]
return [NiceSet(s) for s in parsed]
[docs]def set_to_list(s: set) -> list:
"""
Convert a set to a list.
Parameters
----------
s : set
Returns
-------
list
The result is similar to list(s), but if the elements of the set are comparable, they appear in ascending order.
Examples
--------
>>> set_to_list({2, 42, 12})
[2, 12, 42]
"""
try:
return sorted(s)
except TypeError:
return list(s)
[docs]def set_to_str(s: set) -> str:
"""
Convert a set to a string.
Parameters
----------
s : set
Returns
-------
str
The result is similar to str(s), but if the elements of the set are comparable, they appear in ascending order.
Examples
--------
>>> set_to_str({2, 42, 12})
'{2, 12, 42}'
"""
try:
return '{' + str(sorted(s))[1:-1] + '}'
except TypeError:
return str(s)
[docs]class NiceSet(set):
"""
A set that prints in order (when the elements are comparable).
Examples
--------
>>> my_set = NiceSet({'b', 'a', 'c'})
>>> my_set
{'a', 'b', 'c'}
"""
def __repr__(self):
try:
return '{' + str(sorted(self))[1:-1] + '}'
except TypeError:
return str(set(self))
[docs]def dict_to_items(d: dict) -> list:
"""
Convert a dict to a list of pairs (key, value).
Parameters
----------
d : dict
Returns
-------
list of pairs
The result is similar to d.items(), but if the keys are comparable, they appear in ascending order.
Examples
--------
>>> dict_to_items({'b': 2, 'c': 0, 'a': 1})
[('a', 1), ('b', 2), ('c', 0)]
"""
try:
return [(k, d[k]) for k in sorted(d.keys())]
except TypeError:
return list(d.items())
[docs]def dict_to_str(d: dict) -> str:
"""
Convert dict to string.
Parameters
----------
d : dict
Returns
-------
str
The result is similar to str(d), but if the keys are comparable, they appear in ascending order.
Examples
--------
>>> dict_to_str({'b': 2, 'c': 0, 'a': 1})
"{'a': 1, 'b': 2, 'c': 0}"
"""
try:
return '{' + ', '.join(['%r: %r' % (k, d[k]) for k in sorted(d.keys())]) + '}'
except TypeError:
return str(d)
[docs]class NiceDict(dict):
"""
A dict that prints in the order of the keys (when they are comparable).
Examples
--------
>>> my_dict = NiceDict({'b': 51, 'a': 42, 'c': 12})
>>> my_dict
{'a': 42, 'b': 51, 'c': 12}
"""
def __repr__(self) -> str:
return dict_to_str(self)
[docs]def take_closest(my_list, my_number):
"""
In a list, take the closest element to a given number.
From https://stackoverflow.com/questions/12141150/from-list-of-integers-get-number-closest-to-a-given-value .
Parameters
----------
my_list : list
A list sorted in ascending order.
my_number : Number
Returns
-------
Number
The element of ``my_list`` that is closest to ``my_number``. If two numbers are equally close, return the
smallest number.
Examples
--------
>>> take_closest([0, 5, 10], 3)
5
"""
pos = bisect_left(my_list, my_number)
if pos == 0:
return my_list[0]
if pos == len(my_list):
return my_list[-1]
before = my_list[pos - 1]
after = my_list[pos]
if after - my_number < my_number - before:
return after
else:
return before
[docs]def convert_number(x: Number):
"""
Try to convert a number to a fraction (or an integer).
Parameters
----------
x : Number
Returns
-------
Number
``x``, trying to convert it into a fraction (or an integer).
Examples
--------
>>> convert_number(2.5)
Fraction(5, 2)
>>> convert_number(2.0)
2
"""
if isinstance(x, float):
x = str(x)
try:
value = Fraction(x)
if value.denominator == 1:
return value.numerator
else:
return value
except (TypeError, ValueError):
return x
[docs]def my_division(x: Number, y: Number, divide_by_zero: Number = None):
"""
Division of two numbers, trying to be exact if it is reasonable.
Parameters
----------
x : Number
y : Number
divide_by_zero : Number
The value to be returned in case of division by zero. If None (default), then it raises a ZeroDivisionError.
Returns
-------
Number
The division of `x` by `y`.
Examples
--------
>>> my_division(5, 2)
Fraction(5, 2)
If `x` or `y` is a float, then the result is a float:
>>> my_division(Fraction(5, 2), 0.1)
25.0
>>> my_division(0.1, Fraction(5, 2))
0.04
If `x` and `y` are integers, decimals or fractions, then the result is a fraction:
>>> my_division(2, Fraction(5, 2))
Fraction(4, 5)
>>> my_division(Decimal('0.1'), Fraction(5, 2))
Fraction(1, 25)
You can specify a particular return value in case of division by zero:
>>> my_division(1, 0, divide_by_zero=42)
42
"""
if y == 0:
if divide_by_zero is None:
raise ZeroDivisionError
return divide_by_zero
if isinstance(x, float) or isinstance(y, float):
return x / y
try:
return convert_number(Fraction(x) / Fraction(y))
except ValueError:
raise NotImplementedError