1_TONES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
2_INTERVALS = {
3 0: "unison", 1: "minor 2nd", 2: "major 2nd", 3: "minor 3rd", 4: "major 3rd",
4 5: "perfect 4th", 6: "tritone", 7: "perfect 5th", 8: "minor 6th",
5 9: "major 6th", 10: "minor 7th", 11: "major 7th"
6 }
7
8
9class Tone:
10
11 def __init__(self, name):
12 if name not in _TONES:
13 raise ValueError("Invalid tone name")
14 self.name = name
15 self.index = _TONES.index(name)
16
17 def __str__(self):
18 return self.name
19
20 def __add__(self, other):
21 if isinstance(other, Tone):
22 return Chord(self, other)
23 elif isinstance(other, Interval):
24 new_index = (self.index + other.semitones) % 12
25 return Tone(_TONES[new_index])
26 raise TypeError("Invalid operation")
27
28 def __sub__(self, other):
29 if isinstance(other, Tone):
30 semitone_difference = (self.index - other.index) % 12
31 return Interval(semitone_difference)
32 elif isinstance(other, Interval):
33 new_index = (self.index - other.semitones) % 12
34 return Tone(_TONES[new_index])
35 raise TypeError("Invalid operation")
36
37
38class Interval:
39
40 def __init__(self, semitones):
41 self.semitones = semitones % 12
42 self.name = _INTERVALS.get(self.semitones)
43
44 def __str__(self):
45 return self.name
46
47 def __add__(self, other):
48 if isinstance(other, Interval):
49 return Interval(self.semitones + other.semitones)
50 raise TypeError("Invalid operation")
51 def __neg__(self):
52 return Interval(-self.semitones % 12)
53
54
55class Chord:
56
57 def __init__(self, root, *other_tones):
58 self.root = root
59 unique_tones = {root} | {tone for tone in other_tones}
60
61 if len(unique_tones) == 1:
62 raise TypeError("Cannot have a chord made of only 1 unique tone")
63 root_index = _TONES.index(root.name)
64 self.tones = sorted(unique_tones, key=lambda tone: (_TONES.index(tone.name) - root_index) % len(_TONES))
65
66 def __str__(self):
67 return "-".join(str(tone) for tone in self.tones)
68
69 def is_minor(self):
70 minor_third = Interval(3)
71 return any((tone - self.root).semitones == minor_third.semitones for tone in self.tones[1:])
72
73 def is_major(self):
74 major_third = Interval(4)
75 return any((tone - self.root).semitones == major_third.semitones for tone in self.tones[1:])
76
77 def is_power_chord(self):
78 return not self.is_minor() and not self.is_major()
79
80 def __add__(self, other):
81 if isinstance(other, Tone):
82 if other.name in [tone.name for tone in self.tones]:
83 return self
84 return Chord(self.root, *(self.tones + [other]))
85 elif isinstance(other, Chord):
86 new_tones = {tone.name: tone for tone in self.tones + other.tones}
87 return Chord(self.root, *new_tones.values())
88 raise TypeError("Invalid operation")
89
90 def __sub__(self, other):
91 if isinstance(other, Tone):
92 if other.name not in [tone.name for tone in self.tones]:
93 raise TypeError(f"Cannot remove tone {other} from chord {self}")
94 new_tones = [tone for tone in self.tones if tone.name != other.name]
95 if len(new_tones) < 2:
96 raise TypeError("Cannot have a chord made of only 1 unique tone")
97 return Chord(self.root, *new_tones)
98 raise TypeError("Invalid operation")
99
100 def transposed(self, interval):
101 if not isinstance(interval, Interval):
102 raise TypeError("Invalid transposition interval")
103 transposed_tones = [tone + interval for tone in self.tones]
104 return Chord(transposed_tones[0], *transposed_tones[1:])
F..F.........F..F..........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_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: 'A-A-A-A-A-A-A-C-C-C-C-C-C-C-C-C-C-C-C-C-C-E-E-E-E-E-E-E-E-E-E-E-E-E-E-E-E-E-E' != 'A-C-E'
- A-A-A-A-A-A-A-C-C-C-C-C-C-C-C-C-C-C-C-C-C-E-E-E-E-E-E-E-E-E-E-E-E-E-E-E-E-E-E
+ A-C-E
======================================================================
FAIL: test_interval_str (test.TestBasicIntervalFunctionality.test_interval_str)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 42, in test_interval_str
self.assertEqual(str(Interval(index)), interval)
AssertionError: 'tritone' != 'diminished 5th'
- tritone
+ diminished 5th
======================================================================
FAIL: test_add_chords_repeating_notes (test.TestOperations.test_add_chords_repeating_notes)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 305, in test_add_chords_repeating_notes
self.assertEqual(str(result_chord), "C-G")
AssertionError: 'C-C-G' != 'C-G'
- C-C-G
? --
+ C-G
======================================================================
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_addition_same_tone (test.TestOperations.test_tone_addition_same_tone)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 166, in test_tone_addition_same_tone
with self.assertRaises(TypeError) as err:
AssertionError: TypeError not raised
----------------------------------------------------------------------
Ran 37 tests in 0.003s
FAILED (failures=6)
Виктор Бечев
06.11.2024 16:45Коментарите по решението са предимно по отношение на дизайна и някои питонски идиоми, но всъщност решението е чисто, с добър стил и се чете лесно.
|
06.11.2024 16:13
06.11.2024 16:16
06.11.2024 16:15
06.11.2024 16:34
06.11.2024 16:17
06.11.2024 16:23
06.11.2024 16:19
06.11.2024 16:22
06.11.2024 16:26
06.11.2024 16:28