1INTERVAL_NAMES = {
2 0: "unison", 1: "minor 2nd", 2: "major 2nd", 3: "minor 3rd",
3 4: "major 3rd", 5: "perfect 4th", 6: "diminished 5th",
4 7: "perfect 5th", 8: "minor 6th", 9: "major 6th",
5 10: "minor 7th", 11: "major 7th"
6}
7NOTES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
8NOTES_COUNT = 12
9
10
11class Tone:
12 """Represents a musical tone."""
13
14 def __init__(self, tone):
15 """ Initializes Tone."""
16 self._tone = tone
17
18 def __str__(self):
19 """ Returns the tone as a string."""
20 return self._tone
21
22 @property
23 def tone(self):
24 """ Returns tone name."""
25 return self._tone
26
27 def __sub__(self, other):
28 """Calculates the interval between this tone and another tone or interval."""
29 if isinstance(other, Tone):
30 distance = (NOTES.index(self._tone) - NOTES.index(other.tone)) % NOTES_COUNT
31 return Interval(distance)
32 elif isinstance(other, Interval):
33 new_index = (NOTES.index(self._tone) - other.semitones_count) % NOTES_COUNT
34 return Tone(NOTES[new_index])
35 raise TypeError("Invalid operation")
36
37 def __add__(self, other):
38 """Adds an interval to the tone or combines it with another tone to form a chord."""
39 if isinstance(other, Tone):
40 return Chord(self, other)
41 elif isinstance(other, Interval):
42 new_index = (NOTES.index(self._tone) + other.semitones_count) % NOTES_COUNT
43 return Tone(NOTES[new_index])
44 raise TypeError("Invalid operation")
45
46 def __eq__(self, other):
47 """Compares this Tone with other"""
48 if isinstance(other, Tone):
49 return self._tone == other._tone
50 return False
51
52
53class Interval:
54 """Represents a musical interval."""
55
56 def __init__(self, semitones_count):
57 """ Initializes Interval"""
58 self._semitones_count = semitones_count % NOTES_COUNT
59
60 def __str__(self):
61 """ Returns Interval as a string."""
62 return INTERVAL_NAMES[self._semitones_count]
63
64 @property
65 def semitones_count(self):
66 """Returns number of semitones in the interval"""
67 return self._semitones_count
68
69 def __add__(self, other):
70 """Adds two intervals together"""
71 if isinstance(other, Interval):
72 return Interval((self.semitones_count + other.semitones_count) % NOTES_COUNT)
73 raise TypeError("Invalid operation")
74
75 def __neg__(self):
76 """Returns the negation of the interval"""
77 return Interval(-self._semitones_count)
78
79
80class Chord:
81 """ Represents a musical chord"""
82
83 def _sort_tones(self):
84 """Sorts the tones in the chord based on their distance from the root tone"""
85 root_index = NOTES.index(str(self._root))
86 return sorted(self._tones, key=lambda tone: (NOTES.index(str(tone)) - root_index) % NOTES_COUNT)
87
88 @staticmethod
89 def _init_tones(root, tones):
90 """Initializes the list of tones for a chord, ensuring uniqueness"""
91 memory = {str(root)}
92 result = [root]
93 for tone in tones:
94 if not str(tone) in memory:
95 memory.add(str(tone))
96 result.append(tone)
97 return result
98
99 def __init__(self, root, *tones):
100 """ Initializes a Chord"""
101 if not tones or (len(tones) == 1 and str(root) == str(tones[0])):
102 raise TypeError("Cannot have a chord made of only 1 unique tone")
103
104 self._root = root
105 self._tones = self._init_tones(root, tones)
106 self._tones = self._sort_tones()
107
108 def __str__(self):
109 """Returns the chord as a string"""
110 return "-".join(str(tone) for tone in self._tones)
111
112 def _is_interval(self, target_interval):
113 """Checks if the chord contains a specific interval"""
114 for tone in self._tones:
115 current_interval = tone - self._root
116 if current_interval.semitones_count == target_interval:
117 return True
118 return False
119
120 def is_major(self):
121 """Checks if the chord is major"""
122 return self._is_interval(4)
123
124 def is_minor(self):
125 """ Checks if the chord is minor"""
126 return self._is_interval(3)
127
128 def is_power_chord(self):
129 """Checks if the chord is a power chord"""
130 return not self.is_minor() and not self.is_major()
131
132 def __add__(self, other):
133 """ Adds a tone or chord to the current chord"""
134 if isinstance(other, Tone):
135 return Chord(self._root, *self._tones, other)
136 if isinstance(other, Chord):
137 return Chord(self._root, *self._tones, *other._tones)
138 raise TypeError("Invalid operation")
139
140 def _remove_element(self, other):
141 """Removes a specific tone from the chord."""
142 result = []
143 for tone in self._tones:
144 if str(tone) != str(other):
145 result.append(tone)
146 return result
147
148 def __sub__(self, other):
149 """Removes a tone from the chord"""
150 if not isinstance(other, Tone):
151 raise TypeError("Invalid operation")
152 if other not in self._tones:
153 raise TypeError(f"Cannot remove tone {other} from chord {self}")
154 remaining_tones = self._remove_element(other)
155 if len(remaining_tones) < 2:
156 raise TypeError("Cannot have a chord made of only 1 unique tone")
157 new_root = remaining_tones[0] if self._root == other else self._root
158 return Chord(new_root, *remaining_tones)
159
160 def transposed(self, interval):
161 """Transposes all tones in the chord by a given interval"""
162 transposed_tones = []
163 for tone in self._tones:
164 transposed_tones.append(tone + interval)
165 return Chord(transposed_tones[0], *transposed_tones[1:])
F..........................F.........
======================================================================
FAIL: test_chord_not_enough_tones (test.TestBasicChordFunctionality.test_chord_not_enough_tones)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 83, in test_chord_not_enough_tones
with self.assertRaises(TypeError) as err:
AssertionError: TypeError not raised
======================================================================
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
----------------------------------------------------------------------
Ran 37 tests in 0.001s
FAILED (failures=2)
Виктор Бечев
05.11.2024 15:36Доста чисто решение, браво!
Единственото нещо, което мога да добавя, отвъд дребните коментари, е още по-дребно - по конвенция използваме пасивната форма на глаголите в докстринговете, ето ти пример от стандартната библиотека:
```
help(map)
Help on class map in module builtins:
class map(object)
| map(func, *iterables) --> map object
|
| Make an iterator that computes the function using arguments from
| each of the iterables. Stops when the shortest iterable is exhausted.
```
С други думи при теб следва да бъде `"""Remove a tone from the chord"""`, а не "Remove**s**".
|
05.11.2024 15:26
05.11.2024 15:27
05.11.2024 15:27
05.11.2024 15:33