1NUMBERS_OF_TONES = 12
2UNIQUE_TONES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
3
4class Tone:
5 def __init__(self, name):
6 self.name = name
7 self.position = UNIQUE_TONES.index(name)
8
9 def __str__(self):
10 return self.name
11
12 def __eq__(self, other):
13 if isinstance(other, Tone):
14 return self.name == other.name
15 return False
16
17 def __hash__(self):
18 return hash(self.name)
19
20 def __add__(self, other):
21 if not isinstance(self, Tone):
22 raise TypeError("Invalid operation")
23
24 if isinstance(other, Tone):
25 return Chord(self, other)
26 elif isinstance(other, Interval):
27 new_position = (self.position + other.semitones) % NUMBERS_OF_TONES
28 return Tone(UNIQUE_TONES[new_position])
29 else:
30 raise TypeError("Invalid operation")
31
32 def __sub__(self, other):
33 if not isinstance(self, Tone):
34 raise TypeError("Invalid operation")
35
36 if isinstance(other, Tone):
37 semitone_difference = ((self.position - other.position) + NUMBERS_OF_TONES) % NUMBERS_OF_TONES
38 return Interval(semitone_difference)
39 elif isinstance(other, Interval):
40 new_position = ((self.position - other.semitones) + NUMBERS_OF_TONES) % NUMBERS_OF_TONES
41 return Tone(UNIQUE_TONES[new_position])
42 else:
43 raise TypeError("Invalid operation")
44
45
46class Interval:
47 SEMITONES = ("unison", "minor 2nd", "major 2nd", "minor 3rd", "major 3rd", "perfect 4th",
48 "diminished 5th", "perfect 5th", "minor 6th", "major 6th", "minor 7th", "major 7th")
49
50 def __init__(self, semitones):
51 self.semitones = semitones % NUMBERS_OF_TONES
52
53 def __str__(self):
54 return self.SEMITONES[self.semitones]
55
56 def __add__(self, other_interval):
57 if not isinstance(other_interval, Interval):
58 raise TypeError("Invalid operation")
59
60 new_position = (self.semitones + other_interval.semitones) % NUMBERS_OF_TONES
61 return self.SEMITONES[new_position]
62
63 def __neg__(self):
64 return Interval(-self.semitones)
65
66
67class Chord:
68
69 def __init__(self, keytone, *other_tones):
70 self.keytone = keytone
71 self.tones = {keytone, *other_tones}
72 self.other_tones_set = {*other_tones}
73 if len(self.tones) < 2:
74 raise TypeError("Cannot have a chord made of only 1 unique tone")
75
76 def __str__(self):
77 start_index = self.keytone.position
78 rotated_scale = (UNIQUE_TONES[start_index:] + UNIQUE_TONES[:start_index])
79 ordered_tones = [tone for note_name in rotated_scale for tone in self.tones if tone.name == note_name]
80 return "-".join(map(str, ordered_tones))
81
82 def is_minor(self):
83 minor_3rd_interval = 3
84 return any((tone.position - self.keytone.position) % NUMBERS_OF_TONES == minor_3rd_interval for tone in self.tones)
85
86 def is_major(self):
87 major_3rd_interval = 4
88 return any((tone.position - self.keytone.position) % NUMBERS_OF_TONES == major_3rd_interval for tone in self.tones)
89
90 def is_power_chord(self):
91 return not (self.is_minor() or self.is_major())
92
93 def __add__(self, other):
94 if isinstance(other, Tone):
95 new_chord = Chord(*self.tones, other)
96 return new_chord
97 elif isinstance(other, Chord):
98 new_tones = self.other_tones_set | other.tones
99 sorted_tones = sorted(new_tones, key=lambda tone: tone.position)
100 return Chord(self.keytone, *sorted_tones)
101
102 def __sub__(self, other):
103 if other.name not in [tone.name for tone in self.tones]:
104 raise TypeError(f"Cannot remove tone {other} from chord {self}")
105
106 remaining_tones = [tone for tone in self.tones if tone.name != other.name]
107 return Chord(*remaining_tones)
108
109 def transposed(self, interval):
110 if not isinstance(interval, Interval):
111 raise TypeError("Interval expected for transposition")
112
113 transported_tones = set()
114 for tone in self.tones:
115 transported_index = (UNIQUE_TONES.index(tone.name) + interval.semitones) % 12
116 transported_tones.add(Tone(UNIQUE_TONES[transported_index]))
117
118 transported_keytone_index = (UNIQUE_TONES.index(self.keytone.name) + interval.semitones) % 12
119 transported_keytone = Tone(UNIQUE_TONES[transported_keytone_index])
120
121 return Chord(transported_keytone, *transported_tones)
.....................FFFFF.F..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: 'A-C-D-F' != 'F-A-C-D'
- A-C-D-F
? --
+ 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: 'A-C-F-G' != 'F-G-A-C'
- A-C-F-G
+ F-G-A-C
======================================================================
FAIL: test_interval_addition (test.TestOperations.test_interval_addition)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 240, in test_interval_addition
self.assertIsInstance(major_3rd, Interval)
AssertionError: 'major 3rd' is not an instance of <class 'solution.Interval'>
======================================================================
FAIL: test_interval_addition_overflow (test.TestOperations.test_interval_addition_overflow)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 246, in test_interval_addition_overflow
self.assertIsInstance(unison, Interval)
AssertionError: 'unison' is not an instance of <class 'solution.Interval'>
======================================================================
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_subtract_tone_from_chord (test.TestOperations.test_subtract_tone_from_chord)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 277, in test_subtract_tone_from_chord
self.assertEqual(str(result_chord), "F-G#")
AssertionError: 'G#-F' != 'F-G#'
- G#-F
+ F-G#
----------------------------------------------------------------------
Ran 37 tests in 0.002s
FAILED (failures=7)
07.11.2024 11:33
07.11.2024 11:34
07.11.2024 11:35
07.11.2024 11:35
07.11.2024 11:36