forked from cool-RR/python_toolbox
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlogic_tools.py
More file actions
173 lines (130 loc) · 6.17 KB
/
logic_tools.py
File metadata and controls
173 lines (130 loc) · 6.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# Copyright 2009-2017 Ram Rachum.
# This program is distributed under the MIT license.
'''This module defines logic-related tools.'''
import collections
import itertools
import operator
from python_toolbox import cute_iter_tools
def all_equivalent(iterable, relation=operator.eq, *, assume_reflexive=True,
assume_symmetric=True, assume_transitive=True):
'''
Return whether all elements in the iterable are equivalent to each other.
By default "equivalent" means they're all equal to each other in Python.
You can set a different relation to the `relation` argument, as a function
that accepts two arguments and returns whether they're equivalent or not.
You can use this, for example, to test if all items are NOT equal by
passing in `relation=operator.ne`. You can also define any custom relation
you want: `relation=(lambda x, y: x % 7 == y % 7)`.
By default, we assume that the relation we're using is an equivalence
relation (see http://en.wikipedia.org/wiki/Equivalence_relation for
definition.) This means that we assume the relation is reflexive, symmetric
and transitive, so we can do less checks on the elements to save time. You
can use `assume_reflexive=False`, `assume_symmetric=False` and
`assume_transitive=False` to break any of these assumptions and make this
function do more checks that the equivalence holds between any pair of
items from the iterable. (The more assumptions you ask to break, the more
checks this function does before it concludes that the relation holds
between all items.)
'''
from python_toolbox import sequence_tools
if not assume_transitive or not assume_reflexive:
iterable = sequence_tools.ensure_iterable_is_sequence(iterable)
if assume_transitive:
pairs = cute_iter_tools.iterate_overlapping_subsequences(iterable)
else:
from python_toolbox import combi
pairs = tuple(
iterable * comb for comb in combi.CombSpace(len(iterable), 2)
)
# Can't feed the items directly to `CombSpace` because they might not
# be hashable.
if not assume_symmetric:
pairs = itertools.chain(
*itertools.starmap(lambda x, y: ((x, y), (y, x)), pairs)
)
if not assume_reflexive:
pairs = itertools.chain(pairs,
zip(iterable, iterable))
return all(itertools.starmap(relation, pairs))
def get_equivalence_classes(iterable, key=None, container=set, *,
use_ordered_dict=False, sort_ordered_dict=False):
'''
Divide items in `iterable` to equivalence classes, using the key function.
Each item will be put in a set with all other items that had the same
result when put through the `key` function.
Example:
>>> get_equivalence_classes(range(10), lambda x: x % 3)
{0: {0, 9, 3, 6}, 1: {1, 4, 7}, 2: {8, 2, 5}}
Returns a `dict` with keys being the results of the function, and the
values being the sets of items with those values.
Alternate usages:
Instead of a key function you may pass in an attribute name as a
string, and that attribute will be taken from each item as the key.
Instead of an iterable and a key function you may pass in a `dict` (or
similar mapping) into `iterable`, without specifying a `key`, and the
value of each item in the `dict` will be used as the key.
Example:
>>> get_equivalence_classes({1: 2, 3: 4, 'meow': 2})
{2: {1, 'meow'}, 4: {3}}
If you'd like the result to be in an `OrderedDict`, specify
`use_ordered_dict=True`, and the items will be ordered according to
insertion order. If you'd like that `OrderedDict` to be sorted, pass in
`sort_ordered_dict=True`. (It automatically implies
`use_ordered_dict=True`.) You can also pass in a sorting key function or
attribute name as the `sort_ordered_dict` argument.
'''
from python_toolbox import comparison_tools
### Pre-processing input: #################################################
# #
if key is None:
if isinstance(iterable, collections.abc.Mapping):
d = iterable
else:
try:
d = dict(iterable)
except ValueError:
raise Exception(
"You can't put in a non-dict without also supplying a "
"`key` function. We need to know which key to use."
)
else: # key is not None
assert cute_iter_tools.is_iterable(iterable)
key_function = comparison_tools.process_key_function_or_attribute_name(
key
)
d = {key: key_function(key) for key in iterable}
# #
### Finished pre-processing input. ########################################
if use_ordered_dict or sort_ordered_dict:
from python_toolbox import nifty_collections
new_dict = nifty_collections.OrderedDict()
else:
new_dict = {}
for key, value in d.items():
new_dict.setdefault(value, []).append(key)
# Making into desired container:
for key, value in new_dict.copy().items():
new_dict[key] = container(value)
if sort_ordered_dict:
if isinstance(sort_ordered_dict, (collections.abc.Callable, str)):
key_function = comparison_tools. \
process_key_function_or_attribute_name(sort_ordered_dict)
new_dict.sort(key_function)
elif sort_ordered_dict is True:
new_dict.sort()
return new_dict
else:
return new_dict
def logic_max(iterable, relation=lambda a, b: (a >= b)):
'''
Get a list of maximums from the iterable.
That is, get all items that are bigger-or-equal to all the items in the
iterable.
`relation` is allowed to be a partial order.
'''
sequence = list(iterable)
maximal_elements = []
for candidate in sequence:
if all(relation(candidate, thing) for thing in sequence):
maximal_elements.append(candidate)
return maximal_elements