aboutsummaryrefslogtreecommitdiff
path: root/Lib/fontTools/misc/classifyTools.py
blob: 2235bbd7f8c7678ea4999b2e7a71f7f798a4c935 (plain)
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
171
""" fontTools.misc.classifyTools.py -- tools for classifying things.
"""


class Classifier(object):

    """
    Main Classifier object, used to classify things into similar sets.
    """

    def __init__(self, sort=True):
        self._things = set()  # set of all things known so far
        self._sets = []  # list of class sets produced so far
        self._mapping = {}  # map from things to their class set
        self._dirty = False
        self._sort = sort

    def add(self, set_of_things):
        """
        Add a set to the classifier.  Any iterable is accepted.
        """
        if not set_of_things:
            return

        self._dirty = True

        things, sets, mapping = self._things, self._sets, self._mapping

        s = set(set_of_things)
        intersection = s.intersection(things)  # existing things
        s.difference_update(intersection)  # new things
        difference = s
        del s

        # Add new class for new things
        if difference:
            things.update(difference)
            sets.append(difference)
            for thing in difference:
                mapping[thing] = difference
        del difference

        while intersection:
            # Take one item and process the old class it belongs to
            old_class = mapping[next(iter(intersection))]
            old_class_intersection = old_class.intersection(intersection)

            # Update old class to remove items from new set
            old_class.difference_update(old_class_intersection)

            # Remove processed items from todo list
            intersection.difference_update(old_class_intersection)

            # Add new class for the intersection with old class
            sets.append(old_class_intersection)
            for thing in old_class_intersection:
                mapping[thing] = old_class_intersection
            del old_class_intersection

    def update(self, list_of_sets):
        """
        Add a a list of sets to the classifier.  Any iterable of iterables is accepted.
        """
        for s in list_of_sets:
            self.add(s)

    def _process(self):
        if not self._dirty:
            return

        # Do any deferred processing
        sets = self._sets
        self._sets = [s for s in sets if s]

        if self._sort:
            self._sets = sorted(self._sets, key=lambda s: (-len(s), sorted(s)))

        self._dirty = False

    # Output methods

    def getThings(self):
        """Returns the set of all things known so far.

        The return value belongs to the Classifier object and should NOT
        be modified while the classifier is still in use.
        """
        self._process()
        return self._things

    def getMapping(self):
        """Returns the mapping from things to their class set.

        The return value belongs to the Classifier object and should NOT
        be modified while the classifier is still in use.
        """
        self._process()
        return self._mapping

    def getClasses(self):
        """Returns the list of class sets.

        The return value belongs to the Classifier object and should NOT
        be modified while the classifier is still in use.
        """
        self._process()
        return self._sets


def classify(list_of_sets, sort=True):
    """
    Takes a iterable of iterables (list of sets from here on; but any
    iterable works.), and returns the smallest list of sets such that
    each set, is either a subset, or is disjoint from, each of the input
    sets.

    In other words, this function classifies all the things present in
    any of the input sets, into similar classes, based on which sets
    things are a member of.

    If sort=True, return class sets are sorted by decreasing size and
    their natural sort order within each class size.  Otherwise, class
    sets are returned in the order that they were identified, which is
    generally not significant.

    >>> classify([]) == ([], {})
    True
    >>> classify([[]]) == ([], {})
    True
    >>> classify([[], []]) == ([], {})
    True
    >>> classify([[1]]) == ([{1}], {1: {1}})
    True
    >>> classify([[1,2]]) == ([{1, 2}], {1: {1, 2}, 2: {1, 2}})
    True
    >>> classify([[1],[2]]) == ([{1}, {2}], {1: {1}, 2: {2}})
    True
    >>> classify([[1,2],[2]]) == ([{1}, {2}], {1: {1}, 2: {2}})
    True
    >>> classify([[1,2],[2,4]]) == ([{1}, {2}, {4}], {1: {1}, 2: {2}, 4: {4}})
    True
    >>> classify([[1,2],[2,4,5]]) == (
    ...     [{4, 5}, {1}, {2}], {1: {1}, 2: {2}, 4: {4, 5}, 5: {4, 5}})
    True
    >>> classify([[1,2],[2,4,5]], sort=False) == (
    ...     [{1}, {4, 5}, {2}], {1: {1}, 2: {2}, 4: {4, 5}, 5: {4, 5}})
    True
    >>> classify([[1,2,9],[2,4,5]], sort=False) == (
    ...     [{1, 9}, {4, 5}, {2}], {1: {1, 9}, 2: {2}, 4: {4, 5}, 5: {4, 5},
    ...     9: {1, 9}})
    True
    >>> classify([[1,2,9,15],[2,4,5]], sort=False) == (
    ...     [{1, 9, 15}, {4, 5}, {2}], {1: {1, 9, 15}, 2: {2}, 4: {4, 5},
    ...     5: {4, 5}, 9: {1, 9, 15}, 15: {1, 9, 15}})
    True
    >>> classes, mapping = classify([[1,2,9,15],[2,4,5],[15,5]], sort=False)
    >>> set([frozenset(c) for c in classes]) == set(
    ...     [frozenset(s) for s in ({1, 9}, {4}, {2}, {5}, {15})])
    True
    >>> mapping == {1: {1, 9}, 2: {2}, 4: {4}, 5: {5}, 9: {1, 9}, 15: {15}}
    True
    """
    classifier = Classifier(sort=sort)
    classifier.update(list_of_sets)
    return classifier.getClasses(), classifier.getMapping()


if __name__ == "__main__":
    import sys, doctest

    sys.exit(doctest.testmod(optionflags=doctest.ELLIPSIS).failed)