1class InvalidToneError(Exception):
2 def __init__(self, tone):
3 self._message = f"Invalid tone: '{tone}'."
4 super().__init__(self._message)
5
6
7class InvalidIntervalOperationError(Exception):
8 def __init__(self, message="Invalid interval operation."):
9 self._message = message
10 super().__init__(self._message)
11
12
13class Tone:
14 TONES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
15
16 def __init__(self, name):
17 if name not in self.TONES:
18 raise InvalidToneError(name)
19 self.name = name
20
21 def __repr__(self):
22 return self.name
23
24 def __str__(self):
25 return self.name
26
27 def __eq__(self, other):
28 if isinstance(other, Tone):
29 return self.name == other.name
30 return False
31
32 def __hash__(self):
33 return hash(self.name)
34
35 def __add__(self, other):
36 if isinstance(other, Tone):
37 return Chord(self, other)
38 elif isinstance(other, Interval):
39 return Tone(self.calculate_new_tone(self, other))
40 else:
41 raise TypeError("Invalid operation")
42
43 def __sub__(self, other):
44 if isinstance(other, Interval):
45 inverted_interval = Interval(-other.semitones)
46 return Tone(self.calculate_new_tone(self, inverted_interval))
47 elif isinstance(other, Tone):
48 return self.calculate_interval(self, other)
49 else:
50 raise TypeError("Invalid operation")
51
52 @staticmethod
53 def calculate_new_tone(tone, interval):
54 index = Tone.TONES.index(tone.name)
55 new_index = (index + interval.semitones) % len(Tone.TONES)
56 return Tone.TONES[new_index]
57
58 @staticmethod
59 def calculate_interval(tone1, tone2):
60 index1 = Tone.TONES.index(tone1.name)
61 index2 = Tone.TONES.index(tone2.name)
62 semitones = (index2 - index1) % len(Tone.TONES)
63 return Interval(semitones)
64
65
66class Interval:
67 _interval_names = {
68 0: "unison",
69 1: "minor 2nd",
70 2: "major 2nd",
71 3: "minor 3rd",
72 4: "major 3rd",
73 5: "perfect 4th",
74 6: "diminished 5th",
75 7: "perfect 5th",
76 8: "minor 6th",
77 9: "major 6th",
78 10: "minor 7th",
79 11: "major 7th"
80 }
81
82 def __init__(self, semitones):
83 if not isinstance(semitones, int):
84 raise InvalidIntervalOperationError("Interval must be an integer.")
85 self.semitones = semitones % 12
86
87 def __str__(self):
88 return self._interval_names.get(self.semitones, "unknown")
89
90 def __add__(self, other):
91 if isinstance(other, Interval):
92 return Interval(self.semitones + other.semitones)
93 raise InvalidIntervalOperationError("Can only add another Interval.")
94
95 def __neg__(self):
96 return Interval(-self.semitones)
97
98
99class Chord:
100 def __init__(self, *tones):
101 unique_tones = set(tones)
102 if len(unique_tones) < 2:
103 raise ValueError("Chord must contain at least two unique tones.")
104 self.tones = unique_tones
105 self.root = min(unique_tones, key=lambda tone: Tone.TONES.index(tone.name))
106
107 def __str__(self):
108 ordered_tones = sorted(self.tones, key=lambda tone: Tone.TONES.index(tone.name))
109 return "-".join(tone.name for tone in ordered_tones)
110
111 def __add__(self, other):
112 if isinstance(other, Tone):
113 return Chord(*(self.tones | {other}))
114 elif isinstance(other, Chord):
115 return Chord(*(self.tones | other.tones))
116 raise TypeError("Invalid operation.")
117
118 def __sub__(self, other):
119 if isinstance(other, Tone):
120 if other not in self.tones:
121 raise ValueError(f"Tone {other} not in the chord.")
122 new_tones = self.tones - {other}
123 if len(new_tones) < 2:
124 return None
125 return Chord(*new_tones)
126
127 raise TypeError("Invalid operation.")
128
129 def transposed(self, interval):
130 transposed_tones = [Tone(tone.calculate_new_tone(tone, interval)) for tone in self.tones]
131 return Chord(*transposed_tones)
132
133 def calculate_interval(self, tone1, tone2):
134 return Tone.calculate_interval(tone1, tone2)
135
136 def is_minor(self):
137 return any(self.calculate_interval(self.root, tone).semitones == 3 for tone in self.tones)
138
139 def is_major(self):
140 return any(self.calculate_interval(self.root, tone).semitones == 4 for tone in self.tones)
141
142 def is_power_chord(self):
143 has_minor_third = any(self.calculate_interval(self.root, tone).semitones == 3 for tone in self.tones)
144 has_major_third = any(self.calculate_interval(self.root, tone).semitones == 4 for tone in self.tones)
145 return not (has_minor_third or has_major_third)
EFFFFF...FF.......E..FFF...F...E.EFF.
======================================================================
ERROR: test_chord_not_enough_tones (test.TestBasicChordFunctionality.test_chord_not_enough_tones)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 79, in test_chord_not_enough_tones
Chord(a_sharp)
File "/tmp/solution.py", line 103, in __init__
raise ValueError("Chord must contain at least two unique tones.")
ValueError: Chord must contain at least two unique tones.
======================================================================
ERROR: test_add_interval_to_tone_left_side_error (test.TestOperations.test_add_interval_to_tone_left_side_error)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 210, in test_add_interval_to_tone_left_side_error
Interval(2) + g
~~~~~~~~~~~~^~~
File "/tmp/solution.py", line 93, in __add__
raise InvalidIntervalOperationError("Can only add another Interval.")
solution.InvalidIntervalOperationError: Can only add another Interval.
======================================================================
ERROR: test_subtract_tone_from_chord_error (test.TestOperations.test_subtract_tone_from_chord_error)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 292, in test_subtract_tone_from_chord_error
c5_chord - a
~~~~~~~~~^~~
File "/tmp/solution.py", line 121, in __sub__
raise ValueError(f"Tone {other} not in the chord.")
ValueError: Tone A not in the chord.
======================================================================
ERROR: test_tone_addition_same_tone (test.TestOperations.test_tone_addition_same_tone)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 167, in test_tone_addition_same_tone
c + another_c
~~^~~~~~~~~~~
File "/tmp/solution.py", line 37, in __add__
return Chord(self, other)
^^^^^^^^^^^^^^^^^^
File "/tmp/solution.py", line 103, in __init__
raise ValueError("Chord must contain at least two unique tones.")
ValueError: Chord must contain at least two unique tones.
======================================================================
FAIL: test_chord_ordering (test.TestBasicChordFunctionality.test_chord_ordering)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 65, in test_chord_ordering
self.assertEqual(str(f_sixth_ninth_chord), "F-A-C-E")
AssertionError: 'C-E-F-A' != 'F-A-C-E'
- C-E-F-A
+ F-A-C-E
======================================================================
FAIL: test_chord_str (test.TestBasicChordFunctionality.test_chord_str)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 60, in test_chord_str
self.assertEqual(str(a_major_chord), "A-C#-E")
AssertionError: 'C#-E-A' != 'A-C#-E'
- C#-E-A
+ A-C#-E
======================================================================
FAIL: test_chord_tone_repetition (test.TestBasicChordFunctionality.test_chord_tone_repetition)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 74, in test_chord_tone_repetition
self.assertEqual(str(a_minor_chord), "A-C-E")
AssertionError: 'C-E-A' != 'A-C-E'
- C-E-A
+ A-C-E
======================================================================
FAIL: test_is_major (test.TestBasicChordFunctionality.test_is_major)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 103, in test_is_major
self.assertFalse(a_minor_chord.is_major())
AssertionError: True is not false
======================================================================
FAIL: test_is_minor (test.TestBasicChordFunctionality.test_is_minor)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 90, in test_is_minor
self.assertTrue(a_minor_chord.is_minor())
AssertionError: False is not true
======================================================================
FAIL: test_transposed_negative_overflow (test.TestBasicChordFunctionality.test_transposed_negative_overflow)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 152, in test_transposed_negative_overflow
self.assertEqual(str(result_chord), "G#-B-D#")
AssertionError: 'D#-G#-B' != 'G#-B-D#'
- D#-G#-B
+ G#-B-D#
======================================================================
FAIL: test_transposed_overflow (test.TestBasicChordFunctionality.test_transposed_overflow)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 145, in test_transposed_overflow
self.assertEqual(str(result_chord), "A#-D-F")
AssertionError: 'D-F-A#' != 'A#-D-F'
- D-F-A#
+ A#-D-F
======================================================================
FAIL: test_add_tone_to_chord (test.TestOperations.test_add_tone_to_chord)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 254, in test_add_tone_to_chord
self.assertEqual(str(result_chord), "F-A-C-D")
AssertionError: 'C-D-F-A' != 'F-A-C-D'
- C-D-F-A
+ F-A-C-D
======================================================================
FAIL: test_add_tone_to_chord_existing_tone (test.TestOperations.test_add_tone_to_chord_existing_tone)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 270, in test_add_tone_to_chord_existing_tone
self.assertEqual(str(result_chord), "F-G#-C")
AssertionError: 'C-F-G#' != 'F-G#-C'
- C-F-G#
+ F-G#-C
======================================================================
FAIL: test_add_tone_to_chord_order (test.TestOperations.test_add_tone_to_chord_order)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 261, in test_add_tone_to_chord_order
self.assertEqual(str(result_chord), "F-G-A-C")
AssertionError: 'C-F-G-A' != 'F-G-A-C'
- C-F-G-A
? --
+ F-G-A-C
? ++
======================================================================
FAIL: test_subtract_interval_from_tone_left_side_error (test.TestOperations.test_subtract_interval_from_tone_left_side_error)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 235, in test_subtract_interval_from_tone_left_side_error
self.assertEqual(str(err.exception), INVALID_OPERATION)
AssertionError: "unsupported operand type(s) for -: 'Interval' and 'Tone'" != 'Invalid operation'
- unsupported operand type(s) for -: 'Interval' and 'Tone'
+ Invalid operation
======================================================================
FAIL: test_tone_subtraction (test.TestOperations.test_tone_subtraction)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 173, in test_tone_subtraction
self.assertEqual(str(perfect_5th), "perfect 5th")
AssertionError: 'perfect 4th' != 'perfect 5th'
- perfect 4th
? ^
+ perfect 5th
? ^
======================================================================
FAIL: test_tone_subtraction_inverse (test.TestOperations.test_tone_subtraction_inverse)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 178, in test_tone_subtraction_inverse
self.assertEqual(str(perfect_4th), "perfect 4th")
AssertionError: 'perfect 5th' != 'perfect 4th'
- perfect 5th
? ^
+ perfect 4th
? ^
----------------------------------------------------------------------
Ran 37 tests in 0.005s
FAILED (failures=13, errors=4)
07.11.2024 11:19
07.11.2024 11:23
07.11.2024 11:23
07.11.2024 11:24
07.11.2024 11:25
07.11.2024 11:22
07.11.2024 11:26