1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Name:         sorting.py
4# Purpose:      Music21 class for sorting
5#
6# Authors:      Michael Scott Cuthbert
7#
8# Copyright:    Copyright © 2014-2015 Michael Scott Cuthbert and the music21
9#               Project
10# License:      BSD, see license.txt
11# -----------------------------------------------------------------------------
12'''
13This module defines a single class, SortTuple, which is a named tuple that can
14sort against bare offsets and other SortTuples.
15
16This is a performance-critical object.
17
18It also defines three singleton instance of the SortTupleLow class as ZeroSortTupleDefault,
19ZeroSortTupleLow and
20ZeroSortTupleHigh which are sortTuple at
21offset 0.0, priority [0, -inf, inf] respectively:
22
23>>> sorting.ZeroSortTupleDefault
24SortTuple(atEnd=0, offset=0.0, priority=0, classSortOrder=0, isNotGrace=1, insertIndex=0)
25>>> sorting.ZeroSortTupleLow
26SortTuple(atEnd=0, offset=0.0, priority=-inf, classSortOrder=0, isNotGrace=1, insertIndex=0)
27>>> sorting.ZeroSortTupleHigh
28SortTuple(atEnd=0, offset=0.0, priority=inf, classSortOrder=0, isNotGrace=1, insertIndex=0)
29'''
30from collections import namedtuple
31from math import inf as INFINITY
32from music21 import exceptions21
33
34_attrList = ['atEnd', 'offset', 'priority', 'classSortOrder', 'isNotGrace', 'insertIndex']
35
36
37class SortingException(exceptions21.Music21Exception):
38    pass
39
40
41class SortTuple(namedtuple('SortTuple', _attrList)):
42    '''
43    Derived class of namedTuple which allows for comparisons with pure ints/fractions.
44
45    >>> n = note.Note()
46    >>> s = stream.Stream()
47    >>> s.insert(4, n)
48    >>> st = n.sortTuple()
49    >>> st
50    SortTuple(atEnd=0, offset=4.0, priority=0, classSortOrder=20, isNotGrace=1, insertIndex=...)
51    >>> st.shortRepr()
52    '4.0 <0.20...>'
53    >>> st.atEnd
54    0
55    >>> st.offset
56    4.0
57
58    >>> st < 5.0
59    True
60    >>> 5.0 > st
61    True
62    >>> st > 3.0
63    True
64    >>> 3.0 < st
65    True
66
67    >>> st == 4.0
68    True
69
70    >>> ts = bar.Barline('double')
71    >>> t = stream.Stream()
72    >>> t.storeAtEnd(ts)
73    >>> ts_st = ts.sortTuple()
74    >>> ts_st
75    SortTuple(atEnd=1, offset=0.0, priority=0, classSortOrder=-5, isNotGrace=1, insertIndex=...)
76    >>> st < ts_st
77    True
78    >>> ts_st > 999999
79    True
80    >>> import math
81    >>> ts_st == math.inf
82    True
83
84    Construct one w/ keywords:
85
86    >>> st = sorting.SortTuple(atEnd=0, offset=1.0, priority=0, classSortOrder=20,
87    ...           isNotGrace=1, insertIndex=323)
88    >>> st.shortRepr()
89    '1.0 <0.20.323>'
90
91    or as tuple:
92
93    >>> st = sorting.SortTuple(0, 1.0, 0, 20, 1, 323)
94    >>> st.shortRepr()
95    '1.0 <0.20.323>'
96
97    '''
98    def __new__(cls, *tupEls, **kw):
99        # noinspection PyTypeChecker
100        return super(SortTuple, cls).__new__(cls, *tupEls, **kw)
101
102    def __eq__(self, other):
103        if isinstance(other, tuple):
104            return super().__eq__(other)
105        try:
106            if self.atEnd == 1 and other != INFINITY:
107                return False
108            elif self.atEnd == 1:
109                return True
110            else:
111                return self.offset == other
112        except ValueError:
113            return NotImplemented
114
115    def __lt__(self, other):
116        if isinstance(other, tuple):
117            return super().__lt__(other)
118        try:
119            if self.atEnd == 1:
120                return False
121            else:
122                return self.offset < other
123        except ValueError:
124            return NotImplemented
125
126    def __gt__(self, other):
127        if isinstance(other, tuple):
128            return super().__gt__(other)
129        try:
130            if self.atEnd == 1 and other != INFINITY:
131                return True
132            elif self.atEnd == 1:
133                return False
134            else:
135                return self.offset > other
136        except ValueError:
137            return NotImplemented
138
139    def __ne__(self, other):
140        return not self.__eq__(other)
141
142    def __le__(self, other):
143        return self.__lt__(other) or self.__eq__(other)
144
145    def __ge__(self, other):
146        return self.__gt__(other) or self.__eq__(other)
147
148    def shortRepr(self):
149        '''
150        Returns a nice representation of a SortTuple
151
152        >>> st = sorting.SortTuple(atEnd=0, offset=1.0, priority=0, classSortOrder=20,
153        ...           isNotGrace=1, insertIndex=323)
154        >>> st.shortRepr()
155        '1.0 <0.20.323>'
156
157        >>> st = sorting.SortTuple(atEnd=1, offset=1.0, priority=4, classSortOrder=7,
158        ...           isNotGrace=0, insertIndex=200)
159        >>> st.shortRepr()
160        'End <4.7.[Grace].200>'
161        '''
162        reprParts = []
163        if self.atEnd:
164            reprParts.append('End')
165        else:
166            reprParts.append(str(self.offset))
167        reprParts.append(' <')
168        reprParts.append(str(self.priority))
169        reprParts.append('.')
170        reprParts.append(str(self.classSortOrder))
171
172        if self.isNotGrace == 0:
173            reprParts.append('.[Grace]')
174        reprParts.append('.')
175        reprParts.append(str(self.insertIndex))
176        reprParts.append('>')
177        return ''.join(reprParts)
178
179    def modify(self, **kw):
180        '''
181        return a new SortTuple identical to the previous, except with
182        the given keyword modified.  Works only with keywords.
183
184        >>> st = sorting.SortTuple(atEnd=0, offset=1.0, priority=0, classSortOrder=20,
185        ...           isNotGrace=1, insertIndex=32)
186        >>> st2 = st.modify(offset=2.0)
187        >>> st2.shortRepr()
188        '2.0 <0.20.32>'
189        >>> st2
190        SortTuple(atEnd=0, offset=2.0, priority=0, classSortOrder=20, isNotGrace=1, insertIndex=32)
191
192        >>> st3 = st2.modify(atEnd=1, isNotGrace=0)
193        >>> st3.shortRepr()
194        'End <0.20.[Grace].32>'
195
196        The original tuple is never modified (hence tuple):
197
198        >>> st.offset
199        1.0
200
201        Changing offset, but nothing else, helps in creating .flatten() positions.
202        '''
203        outList = [kw.get(attr, getattr(self, attr)) for attr in _attrList]
204        return self.__class__(*outList)
205
206    def add(self, other):
207        '''
208        Add all attributes from one sortTuple to another,
209        returning a new one.
210
211        >>> n = note.Note()
212        >>> n.offset = 10
213        >>> s = stream.Stream()
214        >>> s.offset = 10
215        >>> n.sortTuple()
216        SortTuple(atEnd=0, offset=10.0, priority=0, classSortOrder=20, isNotGrace=1, insertIndex=0)
217        >>> s.sortTuple()
218        SortTuple(atEnd=0, offset=10.0, priority=0, classSortOrder=-20, isNotGrace=1, insertIndex=0)
219        >>> s.sortTuple().add(n.sortTuple())
220        SortTuple(atEnd=0, offset=20.0, priority=0, classSortOrder=0, isNotGrace=1, insertIndex=0)
221
222        Note that atEnd and isNotGrace are equal to other's value. are upper bounded at 1 and
223        take the maxValue of either.
224        '''
225        if not isinstance(other, self.__class__):
226            raise SortingException('Cannot add attributes from a different class')
227
228        outList = [max(getattr(self, attr), getattr(other, attr))
229                    if attr in ('atEnd', 'isNotGrace')
230                    else (getattr(self, attr) + getattr(other, attr))
231                    for attr in _attrList]
232
233        return self.__class__(*outList)
234
235    def sub(self, other):
236        '''
237        Subtract all attributes from to another.  atEnd and isNotGrace take the min value of either.
238
239        >>> n = note.Note()
240        >>> n.offset = 10
241        >>> s = stream.Stream()
242        >>> s.offset = 10
243        >>> n.sortTuple()
244        SortTuple(atEnd=0, offset=10.0, priority=0, classSortOrder=20, isNotGrace=1, insertIndex=0)
245        >>> s.sortTuple()
246        SortTuple(atEnd=0, offset=10.0, priority=0, classSortOrder=-20, isNotGrace=1, insertIndex=0)
247        >>> s.sortTuple().sub(n.sortTuple())
248        SortTuple(atEnd=0, offset=0.0, priority=0, classSortOrder=-40, isNotGrace=1, insertIndex=0)
249
250        Note that atEnd and isNotGrace are lower bounded at 0.
251        '''
252        if not isinstance(other, self.__class__):
253            raise SortingException('Cannot add attributes from a different class')
254
255        outList = [min(getattr(self, attr), getattr(other, attr))
256                    if attr in ('atEnd', 'isNotGrace')
257                    else (getattr(self, attr) - getattr(other, attr))
258                    for attr in _attrList]
259
260        return self.__class__(*outList)
261
262
263ZeroSortTupleDefault = SortTuple(atEnd=0, offset=0.0, priority=0, classSortOrder=0,
264                                 isNotGrace=1, insertIndex=0)
265
266ZeroSortTupleLow = SortTuple(atEnd=0, offset=0.0, priority=-INFINITY, classSortOrder=0,
267                             isNotGrace=1, insertIndex=0)
268
269ZeroSortTupleHigh = SortTuple(atEnd=0, offset=0.0, priority=INFINITY, classSortOrder=0,
270                              isNotGrace=1, insertIndex=0)
271
272
273# -----------------------------------------------------------------------------
274if __name__ == '__main__':
275    import music21
276    music21.mainTest()
277