# -----------------------------------------------------------------------------
# Name: sorting.py
# Purpose: Music21 class for sorting Music21Objects
#
# Authors: Michael Scott Asato Cuthbert
#
# Copyright: Copyright © 2014-2026 Michael Scott Asato Cuthbert
# License: BSD, see license.txt
# -----------------------------------------------------------------------------
'''
This module defines a single class, SortTuple, which is a named tuple that can
sort against bare offsets and other SortTuples.
This is a performance-critical object.
It also defines three singleton instances of the SortTupleLow class as ZeroSortTupleDefault,
ZeroSortTupleLow and
ZeroSortTupleHigh which are sortTuple at
offset 0.0, priority [0, -inf, inf] respectively:
>>> sorting.ZeroSortTupleDefault
SortTuple(atEnd=0, offset=0.0, priority=0, classSortOrder=0, isNotGrace=1, insertIndex=0)
>>> sorting.ZeroSortTupleLow
SortTuple(atEnd=0, offset=0.0, priority=-inf, classSortOrder=0, isNotGrace=1, insertIndex=0)
>>> sorting.ZeroSortTupleHigh
SortTuple(atEnd=0, offset=0.0, priority=inf, classSortOrder=0, isNotGrace=1, insertIndex=0)
'''
from __future__ import annotations
from math import inf as INFINITY
import typing as t
from music21.common.types import OffsetQL
from music21 import exceptions21
class SortingException(exceptions21.Music21Exception):
pass
[docs]
class SortTuple(t.NamedTuple):
'''
Derived class of namedTuple which allows for comparisons with pure ints/fractions.
>>> n = note.Note()
>>> s = stream.Stream()
>>> s.insert(4, n)
>>> st = n.sortTuple()
>>> st
SortTuple(atEnd=0, offset=4.0, priority=0, classSortOrder=20, isNotGrace=1, insertIndex=...)
>>> st.shortRepr()
'4.0 <0.20...>'
>>> st.atEnd
0
>>> st.offset
4.0
>>> st < 5.0
True
>>> 5.0 > st
True
>>> st > 3.0
True
>>> 3.0 < st
True
>>> st == 4.0
True
>>> ts = bar.Barline('double')
>>> s2 = stream.Stream()
>>> s2.storeAtEnd(ts)
>>> ts_st = ts.sortTuple()
>>> ts_st
SortTuple(atEnd=1, offset=0.0, priority=0, classSortOrder=-5, isNotGrace=1, insertIndex=...)
>>> st < ts_st
True
>>> ts_st > 999999
True
>>> import math
>>> ts_st == math.inf
True
Construct one w/ keywords:
>>> st = sorting.SortTuple(atEnd=0, offset=1.0, priority=0, classSortOrder=20,
... isNotGrace=1, insertIndex=323)
>>> st.shortRepr()
'1.0 <0.20.323>'
or as tuple:
>>> st = sorting.SortTuple(0, 1.0, 0, 20, 1, 323)
>>> st.shortRepr()
'1.0 <0.20.323>'
* Changed in v11: priority and classSortOrder can be floats.
'''
atEnd: int
offset: OffsetQL
priority: int | float
classSortOrder: int | float
isNotGrace: int
insertIndex: int
[docs]
def __eq__(self, other):
if isinstance(other, tuple):
# NB: tuple.__eq__ not super() -- bare super() in a class-syntax NamedTuple
# body triggers a __classcell__ RuntimeError at class creation.
return tuple.__eq__(self, other)
try:
if self.atEnd == 1 and other != INFINITY:
return False
elif self.atEnd == 1:
return True
else:
return self.offset == other
except ValueError:
return NotImplemented
def __lt__(self, other):
if isinstance(other, tuple):
return tuple.__lt__(self, other)
try:
if self.atEnd == 1:
return False
else:
return self.offset < other
except ValueError:
return NotImplemented
def __gt__(self, other):
if isinstance(other, tuple):
return tuple.__gt__(self, other)
try:
if self.atEnd == 1 and other != INFINITY:
return True
elif self.atEnd == 1:
return False
else:
return self.offset > other
except ValueError:
return NotImplemented
def __ne__(self, other):
return not self.__eq__(other)
def __le__(self, other):
return self.__lt__(other) or self.__eq__(other)
def __ge__(self, other):
return self.__gt__(other) or self.__eq__(other)
[docs]
def shortRepr(self):
'''
Returns a nice representation of a SortTuple.
>>> st = sorting.SortTuple(atEnd=0, offset=1.0, priority=0, classSortOrder=20,
... isNotGrace=1, insertIndex=323)
>>> st.shortRepr()
'1.0 <0.20.323>'
>>> st = sorting.SortTuple(atEnd=1, offset=1.0, priority=4, classSortOrder=7,
... isNotGrace=0, insertIndex=200)
>>> st.shortRepr()
'End <4.7.[Grace].200>'
'''
reprParts = []
if self.atEnd:
reprParts.append('End')
else:
reprParts.append(str(self.offset))
reprParts.append(' <')
reprParts.append(str(self.priority))
reprParts.append('.')
reprParts.append(str(self.classSortOrder))
if self.isNotGrace == 0:
reprParts.append('.[Grace]')
reprParts.append('.')
reprParts.append(str(self.insertIndex))
reprParts.append('>')
return ''.join(reprParts)
[docs]
def modify(self, **keywords) -> SortTuple:
'''
Return a new SortTuple identical to the previous, except with
the given keyword modified. Works only with keywords. (Identical to
``_replace()`` and ``copy.replace()`` on Py 3.13+)
>>> st = sorting.SortTuple(atEnd=0, offset=1.0, priority=0, classSortOrder=20,
... isNotGrace=1, insertIndex=32)
>>> st2 = st.modify(offset=2.0)
>>> st2.shortRepr()
'2.0 <0.20.32>'
>>> st2
SortTuple(atEnd=0, offset=2.0, priority=0, classSortOrder=20, isNotGrace=1, insertIndex=32)
>>> st3 = st2.modify(atEnd=1, isNotGrace=0)
>>> st3.shortRepr()
'End <0.20.[Grace].32>'
The original tuple is never modified (hence why it's a tuple):
>>> st.offset
1.0
`Stream.flatten()` uses `.modify` to change offsets while keeping other values identical.
* Changed in v11: a bad field name now raises ValueError
'''
return self._replace(**keywords) # pylint: disable=no-member
[docs]
def add(self, other):
'''
Add all attributes from one sortTuple to another,
returning a new one.
>>> n = note.Note()
>>> n.offset = 10
>>> s = stream.Stream()
>>> s.offset = 10
>>> nt = n.sortTuple()
>>> nt
SortTuple(atEnd=0, offset=10.0, priority=0, classSortOrder=20, isNotGrace=1, insertIndex=0)
>>> st = s.sortTuple()
>>> st
SortTuple(atEnd=0, offset=10.0, priority=0, classSortOrder=-20, isNotGrace=1, insertIndex=0)
>>> st.add(nt)
SortTuple(atEnd=0, offset=20.0, priority=0, classSortOrder=0, isNotGrace=1, insertIndex=0)
Note that `atEnd` and `isNotGrace` are upper bounded at 1 and
take the maxValue of either.
'''
if not isinstance(other, self.__class__):
raise SortingException('Cannot add attributes from a different class')
outList = [max(getattr(self, attr), getattr(other, attr))
if attr in ('atEnd', 'isNotGrace')
else (getattr(self, attr) + getattr(other, attr))
for attr in self._fields] # pylint: disable=no-member
return self.__class__(*outList)
[docs]
def sub(self, other):
'''
Subtract all attributes from another SortTuple.
`atEnd` and `isNotGrace` take the min value of either.
>>> n = note.Note()
>>> n.offset = 10
>>> s = stream.Stream()
>>> s.offset = 10
>>> nt = n.sortTuple()
>>> nt
SortTuple(atEnd=0, offset=10.0, priority=0, classSortOrder=20, isNotGrace=1, insertIndex=0)
>>> st = s.sortTuple()
>>> st
SortTuple(atEnd=0, offset=10.0, priority=0, classSortOrder=-20, isNotGrace=1, insertIndex=0)
>>> st.sub(nt)
SortTuple(atEnd=0, offset=0.0, priority=0, classSortOrder=-40, isNotGrace=1, insertIndex=0)
Note that `atEnd` and `isNotGrace` are lower bounded at 0.
'''
if not isinstance(other, self.__class__):
raise SortingException('Cannot add attributes from a different class')
outList = [min(getattr(self, attr), getattr(other, attr))
if attr in ('atEnd', 'isNotGrace')
else (getattr(self, attr) - getattr(other, attr))
for attr in self._fields] # pylint: disable=no-member
return self.__class__(*outList)
ZeroSortTupleDefault = SortTuple(atEnd=0, offset=0.0, priority=0, classSortOrder=0,
isNotGrace=1, insertIndex=0)
ZeroSortTupleLow = SortTuple(atEnd=0, offset=0.0, priority=-INFINITY, classSortOrder=0,
isNotGrace=1, insertIndex=0)
ZeroSortTupleHigh = SortTuple(atEnd=0, offset=0.0, priority=INFINITY, classSortOrder=0,
isNotGrace=1, insertIndex=0)
# -----------------------------------------------------------------------------
if __name__ == '__main__':
import music21
music21.mainTest()