Repository: cuthbertLab/music21
Pull request #1457 (2022-10-10 19:02:58)

`__eq__` comparison on time signatures


Patch:
diff --git a/music21/meter/base.py b/music21/meter/base.py
index 380969f6d..861d2720b 100644
--- a/music21/meter/base.py
+++ b/music21/meter/base.py
@@ -447,10 +447,67 @@ class TimeSignature(TimeSignatureBase):
     '2/4+3/16' or '11/16 (2/4+3/16)'.  Or you might want the written
     TimeSignature to contradict what the notes imply.  All this can be done
     with .displaySequence.
+
+    **Equality**
+
+    For two time signatures to be considered equal,
+    they have the same name and internal structure.
+
+    The name is tested by the :attr:`~music21.meter.TimeSignature.symbol`.
+    This helps distinguish between 'Cut' and '2/2', for example.
+
+    >>> tsCut = meter.TimeSignature('Cut')
+    >>> ts22 = meter.TimeSignature('2/2')
+    >>> tsCut == ts22
+    False
+
+    The internal structure is currently tested simply by the
+    :attr:`~music21.meter.TimeSignature.beatCount` and
+    :attr:`~music21.meter.TimeSignature.ratioString` attributes.
+
+    The check of :attr:`~music21.meter.TimeSignature.beatCount`
+    helps to distinguish the 'fast' (2-beat) from 'slow' (6-beat)
+    versions of 6/8, for example.
+
+    >>> fast68 = meter.TimeSignature('fast 6/8')
+    >>> slow68 = meter.TimeSignature('slow 6/8')
+    >>> fast68 == slow68
+    False
+
+    Complementing this,
+    :attr:`~music21.meter.TimeSignature.ratioString`
+    provides a check of the internal divsions such that
+    '2/8+3/8' is different from '3/8+2/8', for example,
+    despite the fact that they could both be written as '5/8'.
+
+    >>> ts2n3 = meter.TimeSignature('2/8+3/8')
+    >>> ts3n2 = meter.TimeSignature('3/8+2/8')
+    >>> ts2n3 == ts3n2
+    False
+
+    For a less restrictive test of this, see
+    :meth:`~music21.meter.TimeSignature.ratioEqual`
+    which returns True for all cases of '5/8'.
+
+    >>> ts2n3.ratioEqual(ts3n2)
+    True
+
+    Yes, equality is ever True:
+
+    >>> one44 = meter.TimeSignature('4/4')
+    >>> another44 = meter.TimeSignature()  # '4/4' by default
+    >>> one44 == another44
+    True
+
     '''
     _styleClass = style.TextStyle
     classSortOrder = 4
 
+    equalityAttributes = ('symbol',  # '2/2' != 'Cut'
+                          'ratioString',  # '2+3' != '3+2'
+                          'beatCount'  # 'slow 6/8' != 'fast 6/8'
+                          )
+
     _DOC_ATTR: dict[str, str] = {
         'beatSequence': 'A :class:`~music21.meter.MeterSequence` governing beat partitioning.',
         'beamSequence': 'A :class:`~music21.meter.MeterSequence` governing automatic beaming.',


Test patch:
diff --git a/music21/meter/tests.py b/music21/meter/tests.py
index a4edd99ee..fef72e44d 100644
--- a/music21/meter/tests.py
+++ b/music21/meter/tests.py
@@ -91,7 +91,7 @@ class Test(unittest.TestCase):
         a.load('4/4', 4)
         b = copy.deepcopy(a)
         self.assertIsNot(a, b)
-        # TODO: this is work in progress.
+        # TODO: equity of meter sequences not yet defined.
         # self.assertEqual(a, b)
 
     def testTimeSignatureDeepcopy(self):
@@ -100,10 +100,9 @@ class Test(unittest.TestCase):
         self.assertIsNot(c, d)
         self.assertEqual(c, d)
 
-        # TODO: this is work in progress
-        # e = TimeSignature('slow 6/8')
-        # f = TimeSignature('fast 6/8')
-        # self.assertNotEqual(e, f)
+        e = TimeSignature('slow 6/8')
+        f = TimeSignature('fast 6/8')
+        self.assertNotEqual(e, f)
 
     def testGetBeams(self):
         ts = TimeSignature('6/8')
@@ -619,6 +618,48 @@ class Test(unittest.TestCase):
         beatSeq = ts328.beamSequence
         self.assertEqual(str(beatSeq), '{3/8+2/8}')
 
+    def testEquality(self):
+        '''
+        Additional tests of TimeSignature object equality.
+        Apart from this and the doc tests,
+        see also testTimeSignatureDeepcopy for a test of
+        time signatures with different structure with same ratioString
+        (fast vs slow 6/8).
+        '''
+
+        # 5/8: but different internal structure
+
+        oneKindOf5 = TimeSignature('2+3/8')
+        sameKindOf5 = TimeSignature('2+3/8')
+        self.assertEqual(oneKindOf5.ratioString, '2/8+3/8')
+        self.assertEqual(str(oneKindOf5.beatSequence), '{{1/8+1/8}+{1/8+1/8+1/8}}')
+        self.assertEqual(sameKindOf5.ratioString, '2/8+3/8')
+        self.assertEqual(str(sameKindOf5.beatSequence), '{{1/8+1/8}+{1/8+1/8+1/8}}')
+        self.assertEqual(oneKindOf5, sameKindOf5)
+
+        otherKindOf5 = TimeSignature('3+2/8')
+        self.assertEqual(otherKindOf5.ratioString, '3/8+2/8')
+        self.assertEqual(str(otherKindOf5.beatSequence), '{{1/8+1/8+1/8}+{1/8+1/8}}')
+        self.assertNotEqual(oneKindOf5, otherKindOf5)
+
+        # 4/4: same internal structure, different written time signature.
+
+        oneKindOf44 = TimeSignature('4/4')
+        sameKindOf44 = TimeSignature()
+        self.assertEqual(oneKindOf44, sameKindOf44)
+
+        otherKindOf44 = TimeSignature('Cut')
+        self.assertEqual(otherKindOf44.ratioString, '2/2')
+        self.assertNotEqual(oneKindOf5, otherKindOf44)
+        # Likewise, Different 'symbol'
+        self.assertNotEqual(TimeSignature('Cut'), TimeSignature('2/2'))
+
+        # Completely different class (either way round)
+
+        self.assertNotEqual(TimeSignature(), note.Note())
+        self.assertNotEqual(note.Note(), TimeSignature())
+        # NB: not self.assertRaises(AttributeError). Do we want that instead?
+
 
 if __name__ == '__main__':
     import music21