Source code for music21.sorting

# -----------------------------------------------------------------------------
# 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()