Coverage for /usr/lib/python3/dist-packages/fontTools/misc/classifyTools.py: 17%
59 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1""" fontTools.misc.classifyTools.py -- tools for classifying things.
2"""
5class Classifier(object):
7 """
8 Main Classifier object, used to classify things into similar sets.
9 """
11 def __init__(self, sort=True):
12 self._things = set() # set of all things known so far
13 self._sets = [] # list of class sets produced so far
14 self._mapping = {} # map from things to their class set
15 self._dirty = False
16 self._sort = sort
18 def add(self, set_of_things):
19 """
20 Add a set to the classifier. Any iterable is accepted.
21 """
22 if not set_of_things:
23 return
25 self._dirty = True
27 things, sets, mapping = self._things, self._sets, self._mapping
29 s = set(set_of_things)
30 intersection = s.intersection(things) # existing things
31 s.difference_update(intersection) # new things
32 difference = s
33 del s
35 # Add new class for new things
36 if difference:
37 things.update(difference)
38 sets.append(difference)
39 for thing in difference:
40 mapping[thing] = difference
41 del difference
43 while intersection:
44 # Take one item and process the old class it belongs to
45 old_class = mapping[next(iter(intersection))]
46 old_class_intersection = old_class.intersection(intersection)
48 # Update old class to remove items from new set
49 old_class.difference_update(old_class_intersection)
51 # Remove processed items from todo list
52 intersection.difference_update(old_class_intersection)
54 # Add new class for the intersection with old class
55 sets.append(old_class_intersection)
56 for thing in old_class_intersection:
57 mapping[thing] = old_class_intersection
58 del old_class_intersection
60 def update(self, list_of_sets):
61 """
62 Add a a list of sets to the classifier. Any iterable of iterables is accepted.
63 """
64 for s in list_of_sets:
65 self.add(s)
67 def _process(self):
68 if not self._dirty:
69 return
71 # Do any deferred processing
72 sets = self._sets
73 self._sets = [s for s in sets if s]
75 if self._sort:
76 self._sets = sorted(self._sets, key=lambda s: (-len(s), sorted(s)))
78 self._dirty = False
80 # Output methods
82 def getThings(self):
83 """Returns the set of all things known so far.
85 The return value belongs to the Classifier object and should NOT
86 be modified while the classifier is still in use.
87 """
88 self._process()
89 return self._things
91 def getMapping(self):
92 """Returns the mapping from things to their class set.
94 The return value belongs to the Classifier object and should NOT
95 be modified while the classifier is still in use.
96 """
97 self._process()
98 return self._mapping
100 def getClasses(self):
101 """Returns the list of class sets.
103 The return value belongs to the Classifier object and should NOT
104 be modified while the classifier is still in use.
105 """
106 self._process()
107 return self._sets
110def classify(list_of_sets, sort=True):
111 """
112 Takes a iterable of iterables (list of sets from here on; but any
113 iterable works.), and returns the smallest list of sets such that
114 each set, is either a subset, or is disjoint from, each of the input
115 sets.
117 In other words, this function classifies all the things present in
118 any of the input sets, into similar classes, based on which sets
119 things are a member of.
121 If sort=True, return class sets are sorted by decreasing size and
122 their natural sort order within each class size. Otherwise, class
123 sets are returned in the order that they were identified, which is
124 generally not significant.
126 >>> classify([]) == ([], {})
127 True
128 >>> classify([[]]) == ([], {})
129 True
130 >>> classify([[], []]) == ([], {})
131 True
132 >>> classify([[1]]) == ([{1}], {1: {1}})
133 True
134 >>> classify([[1,2]]) == ([{1, 2}], {1: {1, 2}, 2: {1, 2}})
135 True
136 >>> classify([[1],[2]]) == ([{1}, {2}], {1: {1}, 2: {2}})
137 True
138 >>> classify([[1,2],[2]]) == ([{1}, {2}], {1: {1}, 2: {2}})
139 True
140 >>> classify([[1,2],[2,4]]) == ([{1}, {2}, {4}], {1: {1}, 2: {2}, 4: {4}})
141 True
142 >>> classify([[1,2],[2,4,5]]) == (
143 ... [{4, 5}, {1}, {2}], {1: {1}, 2: {2}, 4: {4, 5}, 5: {4, 5}})
144 True
145 >>> classify([[1,2],[2,4,5]], sort=False) == (
146 ... [{1}, {4, 5}, {2}], {1: {1}, 2: {2}, 4: {4, 5}, 5: {4, 5}})
147 True
148 >>> classify([[1,2,9],[2,4,5]], sort=False) == (
149 ... [{1, 9}, {4, 5}, {2}], {1: {1, 9}, 2: {2}, 4: {4, 5}, 5: {4, 5},
150 ... 9: {1, 9}})
151 True
152 >>> classify([[1,2,9,15],[2,4,5]], sort=False) == (
153 ... [{1, 9, 15}, {4, 5}, {2}], {1: {1, 9, 15}, 2: {2}, 4: {4, 5},
154 ... 5: {4, 5}, 9: {1, 9, 15}, 15: {1, 9, 15}})
155 True
156 >>> classes, mapping = classify([[1,2,9,15],[2,4,5],[15,5]], sort=False)
157 >>> set([frozenset(c) for c in classes]) == set(
158 ... [frozenset(s) for s in ({1, 9}, {4}, {2}, {5}, {15})])
159 True
160 >>> mapping == {1: {1, 9}, 2: {2}, 4: {4}, 5: {5}, 9: {1, 9}, 15: {15}}
161 True
162 """
163 classifier = Classifier(sort=sort)
164 classifier.update(list_of_sets)
165 return classifier.getClasses(), classifier.getMapping()
168if __name__ == "__main__":
169 import sys, doctest
171 sys.exit(doctest.testmod(optionflags=doctest.ELLIPSIS).failed)