1INPUT_ERROR_MESSAGE = 'Invalid input type: Expected'
2INVALID_OPERATION_ERROR_MESSAGE = 'Invalid operation'
3VALID_TONES = (
4 'C', 'C#', 'D', 'D#', 'E', 'F',
5 'F#', 'G', 'G#', 'A', 'A#', 'B'
6)
7
8
9class Tone:
10
11 def __init__(self, tone):
12 self.tone = tone
13
14 @property
15 def tone(self):
16 return self._tone
17
18 @tone.setter
19 def tone(self, value):
20 if value not in VALID_TONES:
21 raise ValueError(f'{INPUT_ERROR_MESSAGE} one of {VALID_TONES}')
22 self._tone = value
23
24 def __str__(self):
25 return self.tone
26
27 def __add__(self, other):
28 if isinstance(other, Tone):
29 return Chord(self, other)
30 elif isinstance(other, Interval):
31 starting_index = VALID_TONES.index(self.tone)
32 shifted_tone = VALID_TONES[(starting_index + other.length) % INTERVALS_COUNT]
33 return Tone(shifted_tone)
34
35 raise TypeError(INVALID_OPERATION_ERROR_MESSAGE)
36
37 def __sub__(self, other):
38 if isinstance(other, Tone):
39 first_tone_index = VALID_TONES.index(self.tone)
40 second_tone_index = VALID_TONES.index(other.tone)
41 interval_len = first_tone_index - second_tone_index
42
43 if first_tone_index <= second_tone_index:
44 interval_len += INTERVALS_COUNT
45
46 return Interval(interval_len)
47 elif isinstance(other, Interval):
48 starting_index = VALID_TONES.index(self.tone)
49 shifted_tone = VALID_TONES[starting_index - other.length]
50 return Tone(shifted_tone)
51
52 raise TypeError(INVALID_OPERATION_ERROR_MESSAGE)
53
54
55# The indices correspond to the interval length
56INTERVAL_NAMES = (
57 'unison', 'minor 2nd', 'major 2nd', 'minor 3rd',
58 'major 3rd', 'perfect 4th', 'diminished 5th', 'perfect 5th',
59 'minor 6th', 'major 6th', 'minor 7th', 'major 7th'
60)
61
62SHORTEST_INTERVAL = 0
63LONGEST_INTERVAL = 11
64INTERVALS_COUNT = 12
65
66
67class Interval:
68
69 def __init__(self, length, is_direction_forward=True):
70 self._set_length(length)
71 self._is_direction_forward = is_direction_forward
72
73 def __neg__(self):
74 return Interval(self.length, False)
75
76 @property
77 def is_direction_forward(self):
78 return self._is_direction_forward
79
80 @property
81 def length(self):
82 return self._length
83
84 def _set_length(self, value):
85 if not isinstance(value, int):
86 raise TypeError(f'{INPUT_ERROR_MESSAGE} {str(int)}')
87
88 if value < SHORTEST_INTERVAL:
89 raise ValueError(f'{INPUT_ERROR_MESSAGE} a non-negative integer')
90
91 self._length = value % INTERVALS_COUNT
92
93 def __str__(self):
94 return INTERVAL_NAMES[self.length]
95
96 def __add__(self, other):
97 if isinstance(other, Interval):
98 return Interval(self.length + other.length)
99 raise TypeError(INVALID_OPERATION_ERROR_MESSAGE)
100
101
102MIN_TONES_COUNT_IN_CHORD = 2
103TONES_COUNT_ERROR_MESSAGE = 'Cannot have a chord made of only 1 unique tone'
104
105
106def rearrange_tuple(input_tuple, skip_index):
107 start_index = skip_index + 1
108 end_index = skip_index - 1
109
110
111 if start_index <= end_index:
112 result_tuple = input_tuple[start_index:end_index + 1]
113 else:
114 result_tuple = input_tuple[start_index:] + input_tuple[:end_index + 1]
115
116 if skip_index is not None:
117 skip_tone = input_tuple[skip_index]
118 result_tuple = [tone for tone in result_tuple if tone != skip_tone]
119
120 return result_tuple
121
122
123class Chord:
124 def __init__(self, main_tone, *args, **kwargs):
125 secondary_tones = list(args) + list(kwargs.values())
126 self.tones = Chord._extract_and_sort_tones(main_tone, secondary_tones)
127
128 @property
129 def tones(self):
130 return self._tones
131
132 @property
133 def main_tone(self):
134 return self._tones[0]
135
136 @property
137 def secondary_tones(self):
138 return self._tones[1:]
139
140 @tones.setter
141 def tones(self, value):
142 if len(value) < MIN_TONES_COUNT_IN_CHORD:
143 raise TypeError(TONES_COUNT_ERROR_MESSAGE)
144
145 self._tones = value
146
147 @staticmethod
148 def _extract_and_sort_tones(main_tone, secondary_tones=None):
149
150 if secondary_tones is None:
151 return list(main_tone)
152
153 unique_secondary_tones = set(filter(lambda tone: str(tone) != str(main_tone), secondary_tones))
154 unique_secondary_tones_strs = [str(tone) for tone in unique_secondary_tones]
155
156 main_tone_index = VALID_TONES.index(str(main_tone))
157 rearranged_tones = rearrange_tuple(VALID_TONES, main_tone_index)
158
159 sorted_tones = [main_tone]
160
161 for tone in rearranged_tones:
162 if tone in unique_secondary_tones_strs:
163 sorted_tones.append(next((t for t in unique_secondary_tones if str(t) == tone)))
164
165 return sorted_tones
166
167 def __str__(self):
168 return '-'.join([str(tone) for tone in self.tones])
169
170 def _contains_interval(self, interval):
171 first_possible_tone = VALID_TONES[self.tones.index(self.main_tone) - interval.length]
172 second_possible_tone = VALID_TONES[self.tones.index(self.main_tone) + interval.length]
173
174 if any(str(tone) in [first_possible_tone, second_possible_tone] for tone in self.tones):
175 return True
176
177 return False
178
179 def is_minor(self):
180 return self._contains_interval(Interval(3))
181
182 def is_major(self):
183 return self._contains_interval(Interval(4))
184
185 def is_power_chord(self):
186 return not self.is_minor() and not self.is_major()
187
188 def __add__(self, other):
189 if isinstance(other, Tone):
190 return Chord(self.main_tone, *self.secondary_tones, other)
191 elif isinstance(other, Chord):
192 return Chord(self.main_tone, *self.secondary_tones, *other.tones)
193
194 def __sub__(self, tone_to_remove):
195 if str(tone_to_remove) not in [str(tone) for tone in self.tones]:
196 raise TypeError(f'Cannot remove tone {str(tone_to_remove)} from chord {str(self)}')
197
198 if len(self.tones) <= MIN_TONES_COUNT_IN_CHORD:
199 raise TypeError(TONES_COUNT_ERROR_MESSAGE)
200
201 if str(self.main_tone) == str(tone_to_remove):
202 return Chord(self.tones[1], *self.secondary_tones)
203
204 updated_tones = [tone for tone in self.secondary_tones if str(tone) != str(tone_to_remove)]
205 return Chord(self.main_tone, *updated_tones)
206
207 def transposed(self, interval):
208 if interval.is_direction_forward:
209 shifted_secondary_tones = [tone + interval for tone in self.secondary_tones]
210 return Chord(self.main_tone + interval, *shifted_secondary_tones)
211
212 shifted_secondary_tones = [tone - interval for tone in self.secondary_tones]
213 return Chord(self.main_tone - interval, *shifted_secondary_tones)
....FFF....F...............F.........
======================================================================
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 94, in test_is_minor
self.assertFalse(a_major_chord.is_minor())
AssertionError: True is not false
======================================================================
FAIL: test_is_power_chord (test.TestBasicChordFunctionality.test_is_power_chord)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 124, in test_is_power_chord
self.assertTrue(a_kind_of_power_chord.is_power_chord())
AssertionError: False is not true
======================================================================
FAIL: test_interval_negative (test.TestBasicIntervalFunctionality.test_interval_negative)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 51, in test_interval_negative
self.assertEqual(str(minor_2nd), "minor 2nd")
AssertionError: 'major 7th' != 'minor 2nd'
- major 7th
+ minor 2nd
======================================================================
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.002s
FAILED (failures=5)
Виктор Бечев
04.11.2024 14:18Точно това имахме предвид и да, вариантите за имплементиране са няколко, тъй като Python минава по определен ред през дъндърите на двата обекта от двете страни на операторът.
|
Даниел Стефанов
04.11.2024 11:50Добре а тук след описанието на събиране на тон с интервал има следната забележка:
Забележка: Искаме това да работи само когато тонът е отляво на знакът за събиране. Когато тонът е отдясно, го приемаме за недефинирано поведение и искаме да се възбуди TypeError с текст "Invalid operation". Семпло, но няма да ви вгорчаваме живота… Повече.
На мен идеята ми беше да покрия тази забележка чрез __radd__ в Tone. Тоест едиснтвеното друго решение което се сещам e в __add__ на Interval да хвърля тази грешка. Това ли сте имали предвид в условието?
|
Виктор Бечев
04.11.2024 11:42Добре, аз пък разбрах и твоята идея след двата коментара.
Принципно при операция, при която имаме обекта отдясно - `__radd__` и производните се викат с приоритет, така че не съм сигурен, че е необходимо, but if it works - it works.
|
Даниел Стефанов
04.11.2024 10:34Сега седнах да пиша unit test-ове и май разбрах какво имате предвид. NotImplemented е някакъв сингълтън клас и когато тествам конкретната функция __аdd__ тя не хвърля грешка а връща NotImplemented. По време на изптлнение на програмата ще се пробва да извика __radd__ и ще си върне правилната грешка, но когато тестваме това поведение не се активира и при тестване на __аdd__ ни се връща NonImplemented и тестове нямаше да ми минават като тествате дали __add__ хвърля грешка XD
|
Даниел Стефанов
04.11.2024 09:42Идеята за NotImplemented беше че ако Тone("C#") + 5 не може да се хелдълне от __add__ ще се пробва да изпълни 5 + Tone("C#") чрез __radd__ функцията. Там вече raise-вам правилната грешка. Просто ми се видя като по-добър дизайн. В случая няма някаква полза, но мислех, че е по-добре така да се направи.
|
f | 1 | INPUT_ERROR_MESSAGE = 'Invalid input type: Expected' | f | 1 | INPUT_ERROR_MESSAGE = 'Invalid input type: Expected' |
2 | INVALID_OPERATION_ERROR_MESSAGE = 'Invalid operation' | 2 | INVALID_OPERATION_ERROR_MESSAGE = 'Invalid operation' | ||
3 | VALID_TONES = ( | 3 | VALID_TONES = ( | ||
4 | 'C', 'C#', 'D', 'D#', 'E', 'F', | 4 | 'C', 'C#', 'D', 'D#', 'E', 'F', | ||
5 | 'F#', 'G', 'G#', 'A', 'A#', 'B' | 5 | 'F#', 'G', 'G#', 'A', 'A#', 'B' | ||
6 | ) | 6 | ) | ||
n | n | 7 | |||
7 | 8 | ||||
8 | class Tone: | 9 | class Tone: | ||
9 | 10 | ||||
10 | def __init__(self, tone): | 11 | def __init__(self, tone): | ||
11 | self.tone = tone | 12 | self.tone = tone | ||
12 | 13 | ||||
13 | @property | 14 | @property | ||
14 | def tone(self): | 15 | def tone(self): | ||
15 | return self._tone | 16 | return self._tone | ||
16 | 17 | ||||
17 | @tone.setter | 18 | @tone.setter | ||
18 | def tone(self, value): | 19 | def tone(self, value): | ||
19 | if value not in VALID_TONES: | 20 | if value not in VALID_TONES: | ||
20 | raise ValueError(f'{INPUT_ERROR_MESSAGE} one of {VALID_TONES}') | 21 | raise ValueError(f'{INPUT_ERROR_MESSAGE} one of {VALID_TONES}') | ||
21 | self._tone = value | 22 | self._tone = value | ||
22 | 23 | ||||
23 | def __str__(self): | 24 | def __str__(self): | ||
24 | return self.tone | 25 | return self.tone | ||
25 | 26 | ||||
26 | def __add__(self, other): | 27 | def __add__(self, other): | ||
27 | if isinstance(other, Tone): | 28 | if isinstance(other, Tone): | ||
28 | return Chord(self, other) | 29 | return Chord(self, other) | ||
29 | elif isinstance(other, Interval): | 30 | elif isinstance(other, Interval): | ||
30 | starting_index = VALID_TONES.index(self.tone) | 31 | starting_index = VALID_TONES.index(self.tone) | ||
31 | shifted_tone = VALID_TONES[(starting_index + other.length) % INTERVALS_COUNT] | 32 | shifted_tone = VALID_TONES[(starting_index + other.length) % INTERVALS_COUNT] | ||
32 | return Tone(shifted_tone) | 33 | return Tone(shifted_tone) | ||
33 | 34 | ||||
n | 34 | return NotImplemented | n | ||
35 | |||||
36 | def __radd__(self, other): | ||||
37 | raise TypeError(INVALID_OPERATION_ERROR_MESSAGE) | 35 | raise TypeError(INVALID_OPERATION_ERROR_MESSAGE) | ||
38 | 36 | ||||
39 | def __sub__(self, other): | 37 | def __sub__(self, other): | ||
40 | if isinstance(other, Tone): | 38 | if isinstance(other, Tone): | ||
41 | first_tone_index = VALID_TONES.index(self.tone) | 39 | first_tone_index = VALID_TONES.index(self.tone) | ||
42 | second_tone_index = VALID_TONES.index(other.tone) | 40 | second_tone_index = VALID_TONES.index(other.tone) | ||
43 | interval_len = first_tone_index - second_tone_index | 41 | interval_len = first_tone_index - second_tone_index | ||
44 | 42 | ||||
45 | if first_tone_index <= second_tone_index: | 43 | if first_tone_index <= second_tone_index: | ||
46 | interval_len += INTERVALS_COUNT | 44 | interval_len += INTERVALS_COUNT | ||
47 | 45 | ||||
48 | return Interval(interval_len) | 46 | return Interval(interval_len) | ||
49 | elif isinstance(other, Interval): | 47 | elif isinstance(other, Interval): | ||
50 | starting_index = VALID_TONES.index(self.tone) | 48 | starting_index = VALID_TONES.index(self.tone) | ||
51 | shifted_tone = VALID_TONES[starting_index - other.length] | 49 | shifted_tone = VALID_TONES[starting_index - other.length] | ||
52 | return Tone(shifted_tone) | 50 | return Tone(shifted_tone) | ||
53 | 51 | ||||
n | 54 | return NotImplemented | n | ||
55 | |||||
56 | def __rsub__(self, other): | ||||
57 | raise TypeError(INVALID_OPERATION_ERROR_MESSAGE) | 52 | raise TypeError(INVALID_OPERATION_ERROR_MESSAGE) | ||
58 | 53 | ||||
59 | 54 | ||||
60 | # The indices correspond to the interval length | 55 | # The indices correspond to the interval length | ||
61 | INTERVAL_NAMES = ( | 56 | INTERVAL_NAMES = ( | ||
62 | 'unison', 'minor 2nd', 'major 2nd', 'minor 3rd', | 57 | 'unison', 'minor 2nd', 'major 2nd', 'minor 3rd', | ||
63 | 'major 3rd', 'perfect 4th', 'diminished 5th', 'perfect 5th', | 58 | 'major 3rd', 'perfect 4th', 'diminished 5th', 'perfect 5th', | ||
64 | 'minor 6th', 'major 6th', 'minor 7th', 'major 7th' | 59 | 'minor 6th', 'major 6th', 'minor 7th', 'major 7th' | ||
65 | ) | 60 | ) | ||
66 | 61 | ||||
67 | SHORTEST_INTERVAL = 0 | 62 | SHORTEST_INTERVAL = 0 | ||
68 | LONGEST_INTERVAL = 11 | 63 | LONGEST_INTERVAL = 11 | ||
69 | INTERVALS_COUNT = 12 | 64 | INTERVALS_COUNT = 12 | ||
70 | 65 | ||||
n | n | 66 | |||
71 | class Interval: | 67 | class Interval: | ||
72 | 68 | ||||
73 | def __init__(self, length, is_direction_forward=True): | 69 | def __init__(self, length, is_direction_forward=True): | ||
n | 74 | self.length = length | n | 70 | self._set_length(length) |
75 | self.is_direction_forward = is_direction_forward | 71 | self._is_direction_forward = is_direction_forward | ||
76 | 72 | ||||
77 | def __neg__(self): | 73 | def __neg__(self): | ||
78 | return Interval(self.length, False) | 74 | return Interval(self.length, False) | ||
79 | 75 | ||||
80 | @property | 76 | @property | ||
81 | def is_direction_forward(self): | 77 | def is_direction_forward(self): | ||
82 | return self._is_direction_forward | 78 | return self._is_direction_forward | ||
83 | 79 | ||||
n | 84 | @is_direction_forward.setter | n | ||
85 | def is_direction_forward(self, value): | ||||
86 | self._is_direction_forward = value | ||||
87 | |||||
88 | @property | 80 | @property | ||
89 | def length(self): | 81 | def length(self): | ||
90 | return self._length | 82 | return self._length | ||
91 | 83 | ||||
n | 92 | @length.setter | n | ||
93 | def length(self, value): | 84 | def _set_length(self, value): | ||
94 | if not isinstance(value, int): | 85 | if not isinstance(value, int): | ||
95 | raise TypeError(f'{INPUT_ERROR_MESSAGE} {str(int)}') | 86 | raise TypeError(f'{INPUT_ERROR_MESSAGE} {str(int)}') | ||
96 | 87 | ||||
97 | if value < SHORTEST_INTERVAL: | 88 | if value < SHORTEST_INTERVAL: | ||
98 | raise ValueError(f'{INPUT_ERROR_MESSAGE} a non-negative integer') | 89 | raise ValueError(f'{INPUT_ERROR_MESSAGE} a non-negative integer') | ||
99 | 90 | ||||
100 | self._length = value % INTERVALS_COUNT | 91 | self._length = value % INTERVALS_COUNT | ||
101 | 92 | ||||
102 | def __str__(self): | 93 | def __str__(self): | ||
103 | return INTERVAL_NAMES[self.length] | 94 | return INTERVAL_NAMES[self.length] | ||
104 | 95 | ||||
105 | def __add__(self, other): | 96 | def __add__(self, other): | ||
n | n | 97 | if isinstance(other, Interval): | ||
106 | return Interval(self.length + other.length) | 98 | return Interval(self.length + other.length) | ||
99 | raise TypeError(INVALID_OPERATION_ERROR_MESSAGE) | ||||
107 | 100 | ||||
108 | 101 | ||||
109 | MIN_TONES_COUNT_IN_CHORD = 2 | 102 | MIN_TONES_COUNT_IN_CHORD = 2 | ||
110 | TONES_COUNT_ERROR_MESSAGE = 'Cannot have a chord made of only 1 unique tone' | 103 | TONES_COUNT_ERROR_MESSAGE = 'Cannot have a chord made of only 1 unique tone' | ||
111 | 104 | ||||
112 | 105 | ||||
113 | def rearrange_tuple(input_tuple, skip_index): | 106 | def rearrange_tuple(input_tuple, skip_index): | ||
114 | start_index = skip_index + 1 | 107 | start_index = skip_index + 1 | ||
115 | end_index = skip_index - 1 | 108 | end_index = skip_index - 1 | ||
n | n | 109 | |||
110 | |||||
116 | if start_index <= end_index: | 111 | if start_index <= end_index: | ||
n | 117 | result = input_tuple[start_index:end_index + 1] | n | 112 | result_tuple = input_tuple[start_index:end_index + 1] |
118 | else: | 113 | else: | ||
n | 119 | result = input_tuple[start_index:] + input_tuple[:end_index + 1] | n | 114 | result_tuple = input_tuple[start_index:] + input_tuple[:end_index + 1] |
120 | 115 | ||||
121 | if skip_index is not None: | 116 | if skip_index is not None: | ||
122 | skip_tone = input_tuple[skip_index] | 117 | skip_tone = input_tuple[skip_index] | ||
n | 123 | result = [tone for tone in result if tone != skip_tone] | n | 118 | result_tuple = [tone for tone in result_tuple if tone != skip_tone] |
124 | 119 | ||||
t | 125 | return result | t | 120 | return result_tuple |
126 | 121 | ||||
127 | 122 | ||||
128 | class Chord: | 123 | class Chord: | ||
129 | def __init__(self, main_tone, *args, **kwargs): | 124 | def __init__(self, main_tone, *args, **kwargs): | ||
130 | secondary_tones = list(args) + list(kwargs.values()) | 125 | secondary_tones = list(args) + list(kwargs.values()) | ||
131 | self.tones = Chord._extract_and_sort_tones(main_tone, secondary_tones) | 126 | self.tones = Chord._extract_and_sort_tones(main_tone, secondary_tones) | ||
132 | 127 | ||||
133 | @property | 128 | @property | ||
134 | def tones(self): | 129 | def tones(self): | ||
135 | return self._tones | 130 | return self._tones | ||
136 | 131 | ||||
137 | @property | 132 | @property | ||
138 | def main_tone(self): | 133 | def main_tone(self): | ||
139 | return self._tones[0] | 134 | return self._tones[0] | ||
140 | 135 | ||||
141 | @property | 136 | @property | ||
142 | def secondary_tones(self): | 137 | def secondary_tones(self): | ||
143 | return self._tones[1:] | 138 | return self._tones[1:] | ||
144 | 139 | ||||
145 | @tones.setter | 140 | @tones.setter | ||
146 | def tones(self, value): | 141 | def tones(self, value): | ||
147 | if len(value) < MIN_TONES_COUNT_IN_CHORD: | 142 | if len(value) < MIN_TONES_COUNT_IN_CHORD: | ||
148 | raise TypeError(TONES_COUNT_ERROR_MESSAGE) | 143 | raise TypeError(TONES_COUNT_ERROR_MESSAGE) | ||
149 | 144 | ||||
150 | self._tones = value | 145 | self._tones = value | ||
151 | 146 | ||||
152 | @staticmethod | 147 | @staticmethod | ||
153 | def _extract_and_sort_tones(main_tone, secondary_tones=None): | 148 | def _extract_and_sort_tones(main_tone, secondary_tones=None): | ||
154 | 149 | ||||
155 | if secondary_tones is None: | 150 | if secondary_tones is None: | ||
156 | return list(main_tone) | 151 | return list(main_tone) | ||
157 | 152 | ||||
158 | unique_secondary_tones = set(filter(lambda tone: str(tone) != str(main_tone), secondary_tones)) | 153 | unique_secondary_tones = set(filter(lambda tone: str(tone) != str(main_tone), secondary_tones)) | ||
159 | unique_secondary_tones_strs = [str(tone) for tone in unique_secondary_tones] | 154 | unique_secondary_tones_strs = [str(tone) for tone in unique_secondary_tones] | ||
160 | 155 | ||||
161 | main_tone_index = VALID_TONES.index(str(main_tone)) | 156 | main_tone_index = VALID_TONES.index(str(main_tone)) | ||
162 | rearranged_tones = rearrange_tuple(VALID_TONES, main_tone_index) | 157 | rearranged_tones = rearrange_tuple(VALID_TONES, main_tone_index) | ||
163 | 158 | ||||
164 | sorted_tones = [main_tone] | 159 | sorted_tones = [main_tone] | ||
165 | 160 | ||||
166 | for tone in rearranged_tones: | 161 | for tone in rearranged_tones: | ||
167 | if tone in unique_secondary_tones_strs: | 162 | if tone in unique_secondary_tones_strs: | ||
168 | sorted_tones.append(next((t for t in unique_secondary_tones if str(t) == tone))) | 163 | sorted_tones.append(next((t for t in unique_secondary_tones if str(t) == tone))) | ||
169 | 164 | ||||
170 | return sorted_tones | 165 | return sorted_tones | ||
171 | 166 | ||||
172 | def __str__(self): | 167 | def __str__(self): | ||
173 | return '-'.join([str(tone) for tone in self.tones]) | 168 | return '-'.join([str(tone) for tone in self.tones]) | ||
174 | 169 | ||||
175 | def _contains_interval(self, interval): | 170 | def _contains_interval(self, interval): | ||
176 | first_possible_tone = VALID_TONES[self.tones.index(self.main_tone) - interval.length] | 171 | first_possible_tone = VALID_TONES[self.tones.index(self.main_tone) - interval.length] | ||
177 | second_possible_tone = VALID_TONES[self.tones.index(self.main_tone) + interval.length] | 172 | second_possible_tone = VALID_TONES[self.tones.index(self.main_tone) + interval.length] | ||
178 | 173 | ||||
179 | if any(str(tone) in [first_possible_tone, second_possible_tone] for tone in self.tones): | 174 | if any(str(tone) in [first_possible_tone, second_possible_tone] for tone in self.tones): | ||
180 | return True | 175 | return True | ||
181 | 176 | ||||
182 | return False | 177 | return False | ||
183 | 178 | ||||
184 | def is_minor(self): | 179 | def is_minor(self): | ||
185 | return self._contains_interval(Interval(3)) | 180 | return self._contains_interval(Interval(3)) | ||
186 | 181 | ||||
187 | def is_major(self): | 182 | def is_major(self): | ||
188 | return self._contains_interval(Interval(4)) | 183 | return self._contains_interval(Interval(4)) | ||
189 | 184 | ||||
190 | def is_power_chord(self): | 185 | def is_power_chord(self): | ||
191 | return not self.is_minor() and not self.is_major() | 186 | return not self.is_minor() and not self.is_major() | ||
192 | 187 | ||||
193 | def __add__(self, other): | 188 | def __add__(self, other): | ||
194 | if isinstance(other, Tone): | 189 | if isinstance(other, Tone): | ||
195 | return Chord(self.main_tone, *self.secondary_tones, other) | 190 | return Chord(self.main_tone, *self.secondary_tones, other) | ||
196 | elif isinstance(other, Chord): | 191 | elif isinstance(other, Chord): | ||
197 | return Chord(self.main_tone, *self.secondary_tones, *other.tones) | 192 | return Chord(self.main_tone, *self.secondary_tones, *other.tones) | ||
198 | 193 | ||||
199 | def __sub__(self, tone_to_remove): | 194 | def __sub__(self, tone_to_remove): | ||
200 | if str(tone_to_remove) not in [str(tone) for tone in self.tones]: | 195 | if str(tone_to_remove) not in [str(tone) for tone in self.tones]: | ||
201 | raise TypeError(f'Cannot remove tone {str(tone_to_remove)} from chord {str(self)}') | 196 | raise TypeError(f'Cannot remove tone {str(tone_to_remove)} from chord {str(self)}') | ||
202 | 197 | ||||
203 | if len(self.tones) <= MIN_TONES_COUNT_IN_CHORD: | 198 | if len(self.tones) <= MIN_TONES_COUNT_IN_CHORD: | ||
204 | raise TypeError(TONES_COUNT_ERROR_MESSAGE) | 199 | raise TypeError(TONES_COUNT_ERROR_MESSAGE) | ||
205 | 200 | ||||
206 | if str(self.main_tone) == str(tone_to_remove): | 201 | if str(self.main_tone) == str(tone_to_remove): | ||
207 | return Chord(self.tones[1], *self.secondary_tones) | 202 | return Chord(self.tones[1], *self.secondary_tones) | ||
208 | 203 | ||||
209 | updated_tones = [tone for tone in self.secondary_tones if str(tone) != str(tone_to_remove)] | 204 | updated_tones = [tone for tone in self.secondary_tones if str(tone) != str(tone_to_remove)] | ||
210 | return Chord(self.main_tone, *updated_tones) | 205 | return Chord(self.main_tone, *updated_tones) | ||
211 | 206 | ||||
212 | def transposed(self, interval): | 207 | def transposed(self, interval): | ||
213 | if interval.is_direction_forward: | 208 | if interval.is_direction_forward: | ||
214 | shifted_secondary_tones = [tone + interval for tone in self.secondary_tones] | 209 | shifted_secondary_tones = [tone + interval for tone in self.secondary_tones] | ||
215 | return Chord(self.main_tone + interval, *shifted_secondary_tones) | 210 | return Chord(self.main_tone + interval, *shifted_secondary_tones) | ||
216 | 211 | ||||
217 | shifted_secondary_tones = [tone - interval for tone in self.secondary_tones] | 212 | shifted_secondary_tones = [tone - interval for tone in self.secondary_tones] | ||
218 | return Chord(self.main_tone - interval, *shifted_secondary_tones) | 213 | return Chord(self.main_tone - interval, *shifted_secondary_tones) |
Legends | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
|