Домашни > Pitches love the D


Pitches love the D
Краен срок: 06.11.2024 18:00
Точки: 10

Сигурни сме, че в курс с ~70 човека има поне 4-5 от вас, които притежават китари, седящи тихичко в някой ъгъл или шкаф и бавно събиращи прах. Може би на тях е свирено, а може би са купени или получени като подарък, но така и не сте прогресирали с онзи плейлист с туториъли na JustinGuitar и все още единственото, което умеете, са онези 3 акорда, с които можете да изсвирите Wonderwall. Today is gonna be the day... Спирам. Преди да продължим с домашното, ето ви нещо за настроение: [![Crow - JinsanKim (2022)](/media/resources/homework3.jpg)](https://www.youtube.com/watch?v=grlvDLzvbL4&ab_channel=JinsanKim%EA%B9%80%EC%A7%84%EC%82%B0) Е, няма да ви караме да вадите китарата и да започвате пак да се учите, макар че ако се надъхате - ще сме доволни, че сме ви вдъхновили и с удоволствие бихме си поговорили с вас по темата. Вместо това, ще ви научим на малко... ### Музикална теория #### Тонове Основен градивен елемент на всяко едно музикално произведение и негова най-атомарна част са, you guessed it, тоновете. Тоновете са най-просто казано вибрацията на дадено тяло, които трептят с определена честота. Отвъд това няма да си говорим за физичните свойства на тоновете, макар че и там има какво интересно да се каже. В западната музика най-често използваме 12 такива предефинирани честоти, определени от различните височини _(i.e. скоростта на трептенето, честотата)_ на тоновете. Тоновете назоваваме и записваме с ноти, които си имат определени имена. Много от вас вероятно ще се досетят веднага за До-Ре-Ми-Фа-Сол-Ла-Си (за разлика от в училище, последното "До" няма да го броим два пъти), но това са само 8 ноти / тона, а не 12?! Преди визуализацията на въпросната концепция с клавиши на пиано, искаме да направим едно уточнение - за целите на домашното ще работим със западните имена на нотите, а не както по-горе. Това по-горе беше, за да ви припомним часовете по музика, но занапред ще искаме да работим с латинските букви, обозначаващи въпросните тонове: - **C** - До - **D** - Ре - **E** - Ми - **F** - Фа - **G** - Сол - **A** - Ла - **B** - Си Та, защо 12, а не 8?! Ако си представите как изглеждат клавишите на пианото, вероятно ще се сетите, че има бели клавиши и между някои от тях - по-малки, черни клавиши. Белите клавиши отговарят на 8-те тона, които изброихме по-горе. Черните са "междинни" тонове между някои от нотите. Тези тонове, между познатите ни ноти, отбелязваме с # или "диез" _(да, знаем, че има и алтернатива, но няма да я споменаваме, защото на тези от вас, които нямат грам понятие от тези концепции, ще им дойде в повече)_. Ето визуален пример за това как изглеждат всичките 12 тона / ноти спрямо клавишите на пиано: ![Хроматична скала](/media/resources/chromatic-scale.png) Тези 12 ноти дефинират една "октава". Причината в училище _(а и на горната картинка)_ да ни учеха да повтаряме "До" накрая е, че след като "приключат" тоновете от тази октава - все пак има по-високи и следващата октава отново работи със същите 12 имена на тоновете. Следователно след C, D, E, F, G, A, B... Имаме пак C... И после D, и така нататък. За целите на днешното домашно няма да взимаме предвид концепциите за октави, но е важно да възприемете тоновете като последователни и циклични, а не сякаш приключват със B. Ето ви картинка за по-лесно илюстиране: ![Хроматична скала в кръгче](/media/resources/chromatic-scale-circle.png) На този етап вероятно се чудите какво определя наличието на тези 12 тона и защо има "междинни" такива. Тук трябва да си поговорим за... #### Интервали Интервалите, най-просто казано, са разстоянията между височините на два тона. Между всеки два от дванайсетте тона има интервал от точно **един полутон**. С други думи, ако започнем от C, всяка стъпка по клавишите на пианото, включвайки и белите и черните клавиши, е през интервал от един полутон. C->C# - един полутон, C#->D - един полутон, E->F - един полутон и така нататък. Следователно ако скочим с два клавиша, получаваме интервал от 2 стъпки, 2 полутона или можем да го наречем "цял тон". Разстоянието, интервалът между C и D - два полутона / един тон, между C# и D# - същото. Съответно имаме интервали от по 3, 4, 5 и така до 12 скока разстояние, където вече се "връщаме" в оригиналния тон, от който сме започнали да броим. Всеки от тези интервали си има определено име, ние ще използваме имената на английски: - 0 - unison - 1 - minor 2nd - 2 - major 2nd - 3 - minor 3rd - 4 - major 3rd - 5 - perfect 4th - 6 - diminished 5th - 7 - perfect 5th - 8 - minor 6th - 9 - major 6th - 10 - minor 7th - 11 - major 7th На български има по-готини имена като "мажорна терца", "квинта", "септима" и прочие, но за домашното ще се придържаме към английските такива. Остана ни едно последно нещо, преди да можем да пристъпим към условието на домашното. #### Акорди Основата на акордите всъщност е доста проста - комбинацията от два или повече _(различни)_ тона образува акорд. С други думи ако изсвирим заедно C и G - вече имаме акорд (по-конкретно C5). Ако към този акорд добавим още един тон - получаваме нов акорд. Ако направим комбинация от C, E и G - получаваме акорд дефиниран от тези 3 тона (в този случай - C). Ще забележите, че и двата акорда са с имена, производни на нотата C. Тази нота наричаме "основен тон" или "root", а акордът назоваваме на базата на производния тон. Добре де, но в единия случай имаме C5, в другия C, а ако вземем да направим акорд от C, D# и G, ще получим още по-интересното Cm (C minor). Оттук идва и популярната фраза "Fingering A minor" (ако root ни беше A, разбира се). Защо? Тук идва и идеята за "качествата" на акорда. Не, не колко добре сме го изсвирили, това е друга тема. Няма да задълбаваме безкрайно в това, тъй като е обект на поне двусеместриален курс, част от бакалавърска степен в Музикалната академия, но това, което искаме да знаем, е, че "качествата" на акорда са директно последствие от взаимоотношенията между отделните тонове в акорда. Това, върху което ще се фокусираме, е наличието или липсата на minor или major 3rd, но за това - малко по-натам в условието. ### Най-накрая - Условието! #### `Tone` Напишете клас `Tone`, който да описва понятието за музикален тон. Искаме да може да се инстанцира по следния начин: ``` c_sharp = Tone("C#") # At least not Java ``` Искаме да имаме стрингова репрезентация на тоновете, работеща по следния начин: ``` print(str(c_sharp)) # "C#" ``` **Уговорка**: Както споменахме по-горе - за целите на домашното няма да се занимаваме с октавите на дадените тонове, приемаме, че всички тонове C са едно и също. #### `Interval` Напишете клас `Interval`, който да описва понятието за интервал. Искаме да може да се инстанцира по следния начин: ``` number_of_semitones = 3 # Където 3 е брой полутонове разстояние или "стъпки" според горните обяснения minor_third = Interval(number_of_semitones) ``` Искаме да имаме стрингова репрезентация на интервалите, работеща по следния начин: ``` print(str(minor_third)) # "minor 3rd" # С малки букви ``` **Уговорка**: Интервалите могат да приемат произволни естествени числа, като `Interval(13)` е същото като `Interval(1)` и неговата репрезентация би била `minor 2nd`. С други думи отново се абстрахираме от октавите. #### `Chord` Напишете клас `Chord`, който да описва понятието за акорд. Искаме да може да се инстанцира по следния начин: ``` c, d_sharp, g = Tone("C"), Tone("D#"), Tone("G") c_minor_chord = Chord(c, d_sharp, g) ``` **Имаме следните уговорки:** - Първият параметър при инстанциране винаги е основния тон (позиционен). - Останалите тонове са произволен брой и с произволна подредба. За подредбата повече информация малко по-надолу. - Повторенията на тоновете **не се отразяват**. С други думи дори да имате 10 пъти C - пази се единствено информация за тона C _(важно за долната подточка)_. - Приемаме опит за създаване на акорд от 1 тон за недефинирано поведение и искаме да се възбуди `TypeError` с текст `"Cannot have a chord made of only 1 unique tone"`. Още един пример: ``` c, another_c = Tone("C"), Tone("C") sissy_chord = Chord(c, another_c) Traceback (most recent call last): File ... ... TypeError: Cannot have a chord made of only 1 unique tone ``` Искаме да имаме стрингова репрезентация на акордите, работеща по следния начин: ``` print(str(c_minor_chord)) # "C-D#-G" # Описание на всички съдържащи се тонове, започващи от "root"-а, # подредени по редът си в хроматичната скала (C, C#, D, D#... B), разделени от тире # Както казахме по-горе - повторения няма c, another_c, f = Tone("C"), Tone("C"), Tone("F") csus4_chord = Chord(c, f, another_c) # Спокойно, не е важно какъв точно е този акорд print(str(csus4_chord)) # "C-F" f, c, d, a, g = Tone("F"), Tone("C"), Tone("D"), Tone("A"), Tone("G") f_sixth_ninth_chord = Chord(f, c, d, a, g) print(str(f_sixth_ninth_chord)) # "F-G-A-C-D" # Root-ът е водещ, оттам насетне всички са подредени спрямо редът си в скалата # релативно спрямо основния тон ``` За да не навлизаме в дълбините на "качествата" на акордите, ще се ограничим до следните 3 качества - "минорен", "мажорен" и "power" _(ако някой знае как е на български - да каже)_ акорди, определени от следните методи: ##### `Chord.is_minor` Ако акордът има в себе си тон, който заедно с основният (root) образува "minor 3rd" - той е минорен и функцията трябва да ни върне `True`. В противен случай - `False`: ``` c_minor_chord = Chord(Tone("C"), Tone("D#"), Tone("G")) print(c_minor_chord.is_minor()) # True # Тонът D# и root-ът C са на разстояние един от друг, образуващо интервал "minor 3rd" c_not_minor_chord = Chord(Tone("C"), Tone("D"), Tone("G")) print(c_not_minor_chord.is_minor()) # False # Няма тон, който заедно с root-а да образува "minor 3rd", така че приемаме, че акордът не е минорен ``` ##### `Chord.is_major` Ако акордът има в себе си тон, който заедно с основният (root) образува "major 3rd" - той е мажорен и функцията трябва да ни върне `True`. В противен случай - `False`: ``` c_major_chord = Chord(Tone("C"), Tone("E"), Tone("G")) print(c_major_chord.is_major()) # True # Тонът E и root-ът C са на разстояние един от друг, образуващо интервал "major 3rd" c_not_major_chord = Chord(Tone("C"), Tone("D"), Tone("G")) print(c_not_major_chord.is_major()) # False # Няма тон, който заедно с root-а да образува "major 3rd", така че приемаме, че акордът не е мажорен ``` ##### `Chord.is_power_chord` Ако акордът няма в себе си тон, който заедно с основният (root) образува "minor 3rd", нито пък "major 3rd" - той е power акорд и функцията трябва да ни върне `True`. В противен случай - `False`: ``` c_power_chord = Chord(Tone("C"), Tone("F"), Tone("G")) print(c_power_chord.is_power_chord()) # True # Нямаме нито "minor 3rd", нито "major 3rd" интервал спрямо основния тон # И преди някой, който е бил в дебрите на музикалната теория да ни направи забележка, # че въпросният акорд всъщност не е power chord, а суспендиран (или там както се води # на български) - искате ли да ви караме да отбелязвате и суспендираните акорди? :D c_not_power_chord = Chord(Tone("C"), Tone("E"), Tone("G")) print(c_not_power_chord.is_power_chord()) # False # Тонът E и root-ът C са на разстояние един от друг, образуващо интервал "major 3rd", следователно нямаме power chord ``` #### Операции Искаме да имаме възможност да извършваме следните операции с обектите от горните класове: ##### Събиране на тонове Искаме да можем да събираме обекти от клас `Tone` и резултат от операцията да бъде обект от клас `Chord`: ``` c, g = Tone("C"), Tone("G") result_chord = c + g # result_chord е обект от тип Chord print(result_chord) # "C-G" ``` ##### Изваждане на тонове Искаме да можем да изваждаме обекти от клас `Tone` и резултат от операцията да бъде обект от клас `Interval`: ``` c, g = Tone("C"), Tone("G") result_interval = g - c # result_interval е обект от тип Interval print(result_interval) # "perfect 5th" ``` ##### Събиране на тон с интервал Искаме да можем да събираме тонове с интервали и резултат от операцията да бъде нов тон с променена височина: ``` c = Tone("C") perfect_fifth = Interval(7) result_tone = c + perfect_fifth print(result_tone) # "G" c = Tone("C") result_tone = c + Interval(12) print(result_tone) # "C" # Тук забелязваме "цикличността" на 12-те тона - тонът е отново C, тъй като не се # ангажираме с концепцията за октави g = Tone("G") perfect_fifth = Interval(7) result_tone = g + perfect_fifth print(result_tone) # "D" ``` **Забележка:** Искаме това да работи **само** когато тонът е отляво на знакът за събиране. Когато тонът е отдясно, го приемаме за недефинирано поведение и искаме да се възбуди `TypeError` с текст `"Invalid operation"`. Семпло, но няма да ви вгорчаваме живота... Повече. ##### Изваждане на интервал от тон Искаме да можем да изваждаме интервал от тон и резултат от операцията да бъде нов тон с променена височина, но този път в обратна посока, т.е. "надолу": ``` c = Tone("C") perfect_fifth = Interval(7) result_tone = c - perfect_fifth print(result_tone) # "F" # Тук отново забелязваме "цикличността" на 12-те тона ``` **Забележка:** Искаме това да работи **само** когато тонът е отляво на знакът за изваждане. Когато тонът е отдясно, го приемаме за недефинирано поведение и искаме да се възбуди `TypeError` с текст `"Invalid operation"`. Ако искате може и друг... Просто ще ви фейлнат тестовете. ##### Събиране на интервали Искаме да можем да събираме обекти от клас `Interval` и резултат от операцията да бъде нов обект от клас `Interval`: ``` perfect_fifth = Interval(7) minor_third = Interval(3) result_interval = perfect_fifth + minor_third print(result_interval) # "minor 7th" ``` ##### Събиране на акорд с тон Искаме да можем да събираме акорди с тонове и резултат от операцията да бъде нов акорд: ``` c5_chord = Chord(Tone("C"), Tone("G")) result_chord = c5_chord + Tone("E") print(result_chord) # C-Е-G ``` ##### Изваждане на тон от акорд Искаме да можем да изваждаме тон от акорд. Ако акордът съдържа три или повече, тона ще се върне нов акорд, в противен случай ще го третираме като недефинирано поведение и искаме да се възбуди `TypeError` с текст `"Cannot have a chord made of only 1 unique tone"`. ``` c_major_chord = Chord(Tone("C"), Tone("E"), Tone("G")) result_chord = c_major_chord - Tone("E") print(result_chord) # C-G c_power_chord = Chord(Tone("C"), Tone("G")) result_chord = c_power_chord - Tone("G") Traceback (most recent call last): File ... ... TypeError: Cannot have a chord made of only 1 unique tone ``` Изваждането на несъществуващ тон също е недефинирано поведение, отново искаме `TypeError`, но този път искаме текстът да бъде `"Cannot remove tone <Tone> from chord <Chord>"`, където `<Tone>` и `<Chord>` са съотвените стрингови репрезентации на тона и акорда: ``` c_power_chord = Chord(Tone("C"), Tone("G")) result_chord = c_power_chord - Tone("E") Traceback (most recent call last): File ... ... TypeError: Cannot remove tone E from chord C-G ``` ##### Събиране на акорди Искаме да можем да събираме обекти от клас `Chord` и резултат от операцията да бъде нов обект от клас `Chord`: ``` c5_chord = Chord(Tone("C"), Tone("G")) this_other_chord = Chord(Tone("A"), Tone("B")) result_chord = c5_chord + this_other_chord print(result_chord) # "C-G-A-B" ``` #### И финално ##### `Chord.transposed` Умишлено оставихме този метод за накрая, тъй като горните операции описват поведението на тоновете когато ги "изместваме" нагоре или надолу, което е важно за финалната ни функция. Последен термин, обещавам... Искаме да можем да "транспонираме" даден акорд, т.е. да изместим всеки един от тоновете му с един и същ интервал. Интервалът в нашият случай ще бъде обект от тип `Interval`, а промяната на тоновете следва да може да се случва както нагоре, така и надолу, определено от знака пред интервала. Тъй като методът е transpose**d** - искаме като резултат да ни връща нов обект от тип `Chord`. Ето и пример: ``` print(str(c_minor_chord)) # "C-D#-G" d_minor_chord = c_minor_chord.transposed(Interval(2)) print(str(d_minor_chord)) # "D-F-A" a_sharp_minor_chord = d_minor_chord.transposed(-Interval(4)) print(str(a_sharp_minor_chord)) # "A#-C#-F" ``` ### Възбуждане на грешки?! Утре (31.10) ще говорим за това. И все пак, за хората, които много бързат, в Python грешки се хвърлят по следния начин: ``` raise TypeError("текст") ``` That's pretty much it, поне за целите на домашното. Утре - по-интересни неща по темата. ### За любознателните - Ако ви е интересно защо на английски нотите започват от C, а не от A, защо тоновете са 12, но разпределени шантаво и други интересни неща, можете да прочетете повече по темата [тук](https://www.reddit.com/r/musictheory/wiki/faq/history/alphabet/).
 1import unittest
 2
 3import solution
 4
 5
 6class TestSanity(unittest.TestCase):
 7    """Check if all of the classes are present."""
 8
 9    def test_tone(self):
10        self.assertIn('Tone', dir(solution), 'Убеди се, че класът "Tone" е наличен с точно това име.')
11        self.assertTrue(isinstance(solution.Tone, type), 'Убеди се, че "Tone" е клас.')
12
13    def test_interval(self):
14        self.assertIn('Interval', dir(solution), 'Убеди се, че класът "Interval" е наличен с точно това име.')
15        self.assertTrue(isinstance(solution.Interval, type), 'Убеди се, че "Interval" е клас.')
16
17    def test_chord(self):
18        self.assertIn('Chord', dir(solution), 'Убеди се, че класът "Chord" е наличен с точно това име.')
19        self.assertTrue(isinstance(solution.Chord, type), 'Убеди се, че "Chord" е клас.')
20
21
22if __name__ == '__main__':
23    unittest.main()
  1import unittest
  2
  3from solution import *
  4
  5
  6CANNOT_CHORD_THIS_CHORD = "Cannot have a chord made of only 1 unique tone"
  7CANNOT_REMOVE_TONE = "Cannot remove tone {0} from chord {1}"
  8INVALID_OPERATION = "Invalid operation"
  9
 10
 11class TestBasicToneFunctionality(unittest.TestCase):
 12    """Test the basic functionality of the Tone class (operations excluded)."""
 13    TONES = "CDEFGAB"
 14    SHARPS = (f"{tone}#" for tone in "CDFGA")
 15
 16    def test_tone_str(self):
 17        for tone in self.TONES:
 18            self.assertEqual(str(Tone(tone)), tone)
 19        for sharp_letter in self.SHARPS:
 20            self.assertEqual(str(Tone(sharp_letter)), sharp_letter)
 21
 22
 23class TestBasicIntervalFunctionality(unittest.TestCase):
 24    """Test the basic functionality of the Interval class (operations excluded)."""
 25    INTERVALS = (
 26        "unison",
 27        "minor 2nd",
 28        "major 2nd",
 29        "minor 3rd",
 30        "major 3rd",
 31        "perfect 4th",
 32        "diminished 5th",
 33        "perfect 5th",
 34        "minor 6th",
 35        "major 6th",
 36        "minor 7th",
 37        "major 7th"
 38    )
 39
 40    def test_interval_str(self):
 41        for index, interval in enumerate(self.INTERVALS):
 42            self.assertEqual(str(Interval(index)), interval)
 43
 44    def test_interval_overflow(self):
 45        major_7th = Interval(11 + 12**2)
 46        self.assertEqual(str(major_7th), "major 7th")
 47
 48    def test_interval_negative(self):
 49        major_7th = Interval(11)
 50        minor_2nd = -major_7th
 51        self.assertEqual(str(minor_2nd), "minor 2nd")
 52
 53
 54class TestBasicChordFunctionality(unittest.TestCase):
 55    """Test the basic functionality of the Chord class (operations excluded)."""
 56
 57    def test_chord_str(self):
 58        a_major_tones = (Tone(tone) for tone in ["A", "C#", "E"])
 59        a_major_chord = Chord(*a_major_tones)
 60        self.assertEqual(str(a_major_chord), "A-C#-E")
 61
 62    def test_chord_ordering(self):
 63        f_major_seventh_tones = (Tone(tone) for tone in "FCEA")
 64        f_sixth_ninth_chord = Chord(*f_major_seventh_tones)
 65        self.assertEqual(str(f_sixth_ninth_chord), "F-A-C-E")
 66
 67        f_sixth_ninth_tones = (Tone(tone) for tone in "FDAG")
 68        f_sixth_ninth_chord = Chord(*f_sixth_ninth_tones)
 69        self.assertEqual(str(f_sixth_ninth_chord), "F-G-A-D")
 70
 71    def test_chord_tone_repetition(self):
 72        a_minor_tones = (Tone(tone) for tone in "AAAAAAACCCCCCCCCCCCCCEEEEEEEEEEEEEEEEEE")
 73        a_minor_chord = Chord(*a_minor_tones)
 74        self.assertEqual(str(a_minor_chord), "A-C-E")
 75
 76    def test_chord_not_enough_tones(self):
 77        a_sharp = Tone("A#")
 78        with self.assertRaises(TypeError) as err:
 79            Chord(a_sharp)
 80        self.assertEqual(str(err.exception), CANNOT_CHORD_THIS_CHORD)
 81
 82        tones = (Tone(tone) for tone in ["A#", "A#", "A#"])
 83        with self.assertRaises(TypeError) as err:
 84            Chord(*tones)
 85        self.assertEqual(str(err.exception), CANNOT_CHORD_THIS_CHORD)
 86
 87    def test_is_minor(self):
 88        a_minor_tones = (Tone(tone) for tone in "AEC")
 89        a_minor_chord = Chord(*a_minor_tones)
 90        self.assertTrue(a_minor_chord.is_minor())
 91
 92        a_major_tones = (Tone(tone) for tone in ["A", "C#", "E"])
 93        a_major_chord = Chord(*a_major_tones)
 94        self.assertFalse(a_major_chord.is_minor())
 95
 96        a_kind_of_power_chord_tones = (Tone(tone) for tone in ["A", "B", "D", "E"])
 97        a_kind_of_power_chord = Chord(*a_kind_of_power_chord_tones)
 98        self.assertFalse(a_kind_of_power_chord.is_minor())
 99
100    def test_is_major(self):
101        a_minor_tones = (Tone(tone) for tone in "AEC")
102        a_minor_chord = Chord(*a_minor_tones)
103        self.assertFalse(a_minor_chord.is_major())
104
105        a_major_tones = (Tone(tone) for tone in ["A", "C#", "E"])
106        a_major_chord = Chord(*a_major_tones)
107        self.assertTrue(a_major_chord.is_major())
108
109        a_kind_of_power_chord_tones = (Tone(tone) for tone in ["A", "B", "D", "E"])
110        a_kind_of_power_chord = Chord(*a_kind_of_power_chord_tones)
111        self.assertFalse(a_kind_of_power_chord.is_major())
112
113    def test_is_power_chord(self):
114        a_minor_tones = (Tone(tone) for tone in "AEC")
115        a_minor_chord = Chord(*a_minor_tones)
116        self.assertFalse(a_minor_chord.is_power_chord())
117
118        a_major_tones = (Tone(tone) for tone in ["A", "C#", "E"])
119        a_major_chord = Chord(*a_major_tones)
120        self.assertFalse(a_major_chord.is_power_chord())
121
122        a_kind_of_power_chord_tones = (Tone(tone) for tone in ["A", "B", "D", "E"])
123        a_kind_of_power_chord = Chord(*a_kind_of_power_chord_tones)
124        self.assertTrue(a_kind_of_power_chord.is_power_chord())
125
126    def test_transposed(self):
127        d_major_tones = (Tone(tone) for tone in ["D", "F#", "A"])
128        d_major_chord = Chord(*d_major_tones)
129        result_chord = d_major_chord.transposed(Interval(2))
130        self.assertIsInstance(result_chord, Chord)
131        self.assertEqual(str(result_chord), "E-G#-B")
132
133    def test_transposed_negative(self):
134        e_minor_tones = (Tone(tone) for tone in "EGB")
135        e_minor_chord = Chord(*e_minor_tones)
136        result_chord = e_minor_chord.transposed(-Interval(2))
137        self.assertIsInstance(result_chord, Chord)
138        self.assertEqual(str(result_chord), "D-F-A")
139
140    def test_transposed_overflow(self):
141        d_major_tones = (Tone(tone) for tone in ["D", "F#", "A"])
142        d_major_chord = Chord(*d_major_tones)
143        result_chord = d_major_chord.transposed(Interval(8))
144        self.assertIsInstance(result_chord, Chord)
145        self.assertEqual(str(result_chord), "A#-D-F")
146
147    def test_transposed_negative_overflow(self):
148        e_minor_tones = (Tone(tone) for tone in "EGB")
149        e_minor_chord = Chord(*e_minor_tones)
150        result_chord = e_minor_chord.transposed(-Interval(8))
151        self.assertIsInstance(result_chord, Chord)
152        self.assertEqual(str(result_chord), "G#-B-D#")
153
154
155class TestOperations(unittest.TestCase):
156    """Test the operations between the different musical primitives."""
157
158    def test_tone_addition_different_tones(self):
159        g, f = Tone("F"), Tone("G")
160        result_chord = g + f
161        self.assertIsInstance(result_chord, Chord)
162        self.assertEqual(str(result_chord), "F-G")
163
164    def test_tone_addition_same_tone(self):
165        c, another_c = Tone("C"), Tone("C")
166        with self.assertRaises(TypeError) as err:
167            c + another_c
168        self.assertEqual(str(err.exception), CANNOT_CHORD_THIS_CHORD)
169
170    def test_tone_subtraction(self):
171        g, c = Tone("G"), Tone("C")
172        perfect_5th = g - c
173        self.assertEqual(str(perfect_5th), "perfect 5th")
174
175    def test_tone_subtraction_inverse(self):
176        c, g = Tone("C"), Tone("G")
177        perfect_4th = c - g
178        self.assertEqual(str(perfect_4th), "perfect 4th")
179
180    def test_tone_subtraction_same_tone(self):
181        c, another_c = Tone("C"), Tone("C")
182        unison = c - another_c
183        self.assertEqual(str(unison), "unison")
184
185    def test_add_interval_to_tone(self):
186        g = Tone("G")
187        g_sharp = g + Interval(1)
188        self.assertIsInstance(g_sharp, Tone)
189        self.assertEqual(str(g_sharp), "G#")
190
191    def test_add_interval_to_tone_same_tone(self):
192        g = Tone("G")
193        still_g = g + Interval(0)
194        self.assertIsInstance(still_g, Tone)
195        self.assertEqual(str(still_g), "G")
196
197    def test_add_interval_to_tone_overflow(self):
198        g = Tone("G")
199        a = g + Interval(2)
200        self.assertIsInstance(a, Tone)
201        self.assertEqual(str(a), "A")
202
203        a = g + Interval(2 + 12**2)
204        self.assertIsInstance(a, Tone)
205        self.assertEqual(str(a), "A")
206
207    def test_add_interval_to_tone_left_side_error(self):
208        g = Tone("G")
209        with self.assertRaises(TypeError) as err:
210            Interval(2) + g
211        self.assertEqual(str(err.exception), INVALID_OPERATION)
212
213    def test_subtract_interval_from_tone(self):
214        g = Tone("G")
215        f_sharp = g - Interval(1)
216        self.assertIsInstance(f_sharp, Tone)
217        self.assertEqual(str(f_sharp), "F#")
218
219    def test_subtract_interval_from_tone_same_tone(self):
220        g = Tone("G")
221        still_g = g - Interval(0)
222        self.assertIsInstance(still_g, Tone)
223        self.assertEqual(str(still_g), "G")
224
225    def test_subtract_interval_from_tone_oveflow(self):
226        c = Tone("C")
227        g = c - Interval(5)
228        self.assertIsInstance(g, Tone)
229        self.assertEqual(str(g), "G")
230
231    def test_subtract_interval_from_tone_left_side_error(self):
232        g = Tone("G")
233        with self.assertRaises(TypeError) as err:
234            Interval(2) - g
235        self.assertEqual(str(err.exception), INVALID_OPERATION)
236
237    def test_interval_addition(self):
238        minor_2nd, minor_3rd = Interval(1), Interval(3)
239        major_3rd = minor_2nd + minor_3rd
240        self.assertIsInstance(major_3rd, Interval)
241        self.assertEqual(str(major_3rd), "major 3rd")
242
243    def test_interval_addition_overflow(self):
244        perfect_4th, perfect_5th = Interval(5), Interval(7)
245        unison = perfect_4th + perfect_5th
246        self.assertIsInstance(unison, Interval)
247        self.assertEqual(str(unison), "unison")
248
249    def test_add_tone_to_chord(self):
250        f_major_tones = (Tone(tone) for tone in "FAC")
251        f_major_chord = Chord(*f_major_tones)
252        result_chord = f_major_chord + Tone("D")
253        self.assertIsInstance(result_chord, Chord)
254        self.assertEqual(str(result_chord), "F-A-C-D")
255
256    def test_add_tone_to_chord_order(self):
257        f_major_tones = (Tone(tone) for tone in "FAC")
258        f_major_chord = Chord(*f_major_tones)
259        result_chord = f_major_chord + Tone("G")
260        self.assertIsInstance(result_chord, Chord)
261        self.assertEqual(str(result_chord), "F-G-A-C")
262
263    def test_add_tone_to_chord_existing_tone(self):
264        f_minor_tones = (Tone(tone) for tone in ["F", "G#", "C"])
265        f_minor_chord = Chord(*f_minor_tones)
266        result_chord = f_minor_chord + Tone("F")
267        result_chord = result_chord + Tone("G#")
268        result_chord = result_chord + Tone("C")
269        self.assertIsInstance(result_chord, Chord)
270        self.assertEqual(str(result_chord), "F-G#-C")
271
272    def test_subtract_tone_from_chord(self):
273        f_minor_tones = (Tone(tone) for tone in ["F", "G#", "C"])
274        f_minor_chord = Chord(*f_minor_tones)
275        result_chord = f_minor_chord - Tone("C")
276        self.assertIsInstance(result_chord, Chord)
277        self.assertEqual(str(result_chord), "F-G#")
278
279    def test_subtract_tone_from_chord_error(self):
280        c5_chord_tones = Tone("C"), Tone("G")
281        c5_chord = Chord(*c5_chord_tones)
282        with self.assertRaises(TypeError) as err:
283            c5_chord - Tone("G")
284        self.assertEqual(str(err.exception), CANNOT_CHORD_THIS_CHORD)
285
286    def test_subtract_tone_from_chord_error(self):
287        c5_chord_tones = Tone("C"), Tone("G")
288        c5_chord = Chord(*c5_chord_tones)
289        a = Tone("A")
290        expected_message = CANNOT_REMOVE_TONE.format(str(a), str(c5_chord))
291        with self.assertRaises(TypeError) as err:
292            c5_chord - a
293        self.assertEqual(str(err.exception), expected_message)
294
295    def test_add_chords(self):
296        c5_chord = Chord(Tone("C"), Tone("G"))
297        this_other_chord = Chord(Tone("A"), Tone("B"))
298        result_chord = c5_chord + this_other_chord
299        self.assertEqual(str(result_chord), "C-G-A-B")
300
301    def test_add_chords_repeating_notes(self):
302        c5_chord = Chord(Tone("C"), Tone("G"))
303        inverted_c5_chord = Chord(Tone("G"), Tone("C"))
304        result_chord = c5_chord + inverted_c5_chord
305        self.assertEqual(str(result_chord), "C-G")
306
307
308if __name__ == "__main__":
309    unittest.main()
Дискусия
Костадин Русалов
06.11.2024 19:46

Друго си е решението да е 1 ред.
Георги Кунчев
06.11.2024 17:31

Привет! Поради големия брой решения, влезли в последните няколко часа, ще довършим проверката и ще публикуваме резултатите утре. Не се проверяват лесно 60 решения с по 150 реда (9000 реда код) :tired_face:
Георги Кунчев
04.11.2024 14:59

@Даниел_Стефанов, както Виктор каза, не сме говорили още за тестове, но тъй като пишеш, а ми направи впечатление - ще споделя нещо за твое удобство. Вместо да закоментираш тест, който не ти трябва за момента, можеш да го декорираш с декоратор, който `unittest` дава наготово: ``` @unittest.skip('No need to test this right now.') def test_something(self): self.assertTrue('something') ```
Виктор Бечев
04.11.2024 14:55

Супер е, че си написал тестове и тестовете ти реално тестват немалко неща. Проблемът, обаче, е, че са зависими от твоята собствена имплементация. Ето пример: ``` for index, tone in enumerate(c_minor_chord.tones): self.assertEqual(str(tone), str(expected_tones[index])) ``` Ако колега, на когото имплементацията му не разчита на `Chord.tones` - атрибутът се казва по различен начин или пък изобщо не използва такъв атрибут - тестът фейлва. Същото важи и за `Interval.length` и прочие. Освен това имаш дефинирани тестове и за поведение, което не изискваме от вас - например: ``` def test_add_with_invalid_second_param_bool(self): with self.assertRaises(TypeError) as context: self.c + True self.assertIn(INVALID_OPERATION_ERROR_MESSAGE, str(context.exception)) ``` Точката е поощрителна, защото още не сме говорили за тестове, а си се постарал, а вече по-натам - ще поощряваме само валидни такива. Но е важно за всички останали да знаят, че ако някои от тестовете ти не минават - **това не е непременно проблем с тяхното решение**.
Даниел Стефанов
04.11.2024 14:33

Още тестове: https://pastebin.com/cjMUqPpV фикснете импорта и сменете HOMEWORK_FILENAME във файла
Виктор Бечев
03.11.2024 22:07

След дискусия в едно от решенията, едно уточнение (новият текст в **bold**): **Забележка:** Искаме това да работи **само** когато тонът е отляво **на знакът за събиране**. Когато тонът е отдясно, го приемаме за недефинирано поведение и искаме да се възбуди `TypeError` с текст `"Invalid operation"`. Семпло, но няма да ви вгорчаваме живота... Повече. Говорим за отляво или отдясно спрямо операцията, а не някаква релативност спрямо скалата.
Виктор Бечев
03.11.2024 21:21

@Стефан_Шиваров - реално това ще работи без да правиш нищо. Не защото ще е "static", а защото това е разликата между функция и bound метод. `Chord.is_minor(chord)` ще предаде експлицитно `chord` като `self` на `is_minor(self)`, в случая с инстанцията - просто е имплицитно. П.П. Пробвай го. :)
Стефан Шиваров
03.11.2024 21:07

За методите Chord.is\_minor , Chord.is\_major и т.н. , очаква ли се да правим и static версия, тоест освен през инстанцията chord.is\_minor(), да могат да се викат като Chord.is\_minor(chord) ?
Никола Георгиев
03.11.2024 12:58

``` import unittest class TestMusicTheory(unittest.TestCase): def test_tone_initialization(self): c_sharp = Tone("C#") self.assertEqual(str(c_sharp), "C#") def test_interval_initialization(self): minor_third = Interval(3) self.assertEqual(str(minor_third), "minor 3rd") def test_chord_initialization(self): c_minor_chord = Chord(Tone("C"), Tone("D#"), Tone("G")) self.assertEqual(str(c_minor_chord), "C-D#-G") def test_chord_unique_tones(self): c = Tone("C") another_c = Tone("C") with self.assertRaises(TypeError) as context: Chord(c, another_c) self.assertEqual(str(context.exception), "Cannot have a chord made of only 1 unique tone") def test_csus4_chord(self): c = Tone("C") another_c = Tone("C") f = Tone("F") csus4_chord = Chord(c, f, another_c) self.assertEqual(str(csus4_chord), "C-F") def test_f_sixth_ninth_chord(self): f_sixth_ninth_chord = Chord(Tone("F"), Tone("C"), Tone("D"), Tone("A"), Tone("G")) self.assertEqual(str(f_sixth_ninth_chord), "F-G-A-C-D") def test_c_minor_chord_is_minor(self): c_minor_chord = Chord(Tone("C"), Tone("D#"), Tone("G")) self.assertTrue(c_minor_chord.is_minor()) def test_c_not_minor_chord(self): c_not_minor_chord = Chord(Tone("C"), Tone("D"), Tone("G")) self.assertFalse(c_not_minor_chord.is_minor()) def test_c_major_chord_is_major(self): c_major_chord = Chord(Tone("C"), Tone("E"), Tone("G")) self.assertTrue(c_major_chord.is_major()) def test_c_not_major_chord(self): c_not_major_chord = Chord(Tone("C"), Tone("D"), Tone("G")) self.assertFalse(c_not_major_chord.is_major()) def test_c_power_chord(self): c_power_chord = Chord(Tone("C"), Tone("F"), Tone("G")) self.assertTrue(c_power_chord.is_power_chord()) def test_c_not_power_chord(self): c_not_power_chord = Chord(Tone("C"), Tone("E"), Tone("G")) self.assertFalse(c_not_power_chord.is_power_chord()) def test_add_tones(self): c = Tone("C") g = Tone("G") result_chord = c + g self.assertEqual(str(result_chord), "C-G") def test_subtract_tones(self): c = Tone("C") g = Tone("G") result_interval = g - c self.assertEqual(str(result_interval), "perfect 5th") def test_add_interval_to_tone(self): c = Tone("C") perfect_fifth = Interval(7) result_tone = c + perfect_fifth self.assertEqual(str(result_tone), "G") def test_add_full_octave_to_tone(self): c = Tone("C") result_tone = c + Interval(12) self.assertEqual(str(result_tone), "C") def test_add_interval_to_tone_g(self): g = Tone("G") perfect_fifth = Interval(7) result_tone = g + perfect_fifth self.assertEqual(str(result_tone), "D") def test_subtract_interval_from_tone(self): c = Tone("C") perfect_fifth = Interval(7) result_tone = c - perfect_fifth self.assertEqual(str(result_tone), "F") def test_add_intervals(self): perfect_fifth = Interval(7) minor_third = Interval(3) result_interval = perfect_fifth + minor_third self.assertEqual(str(result_interval), "minor 7th") def test_add_chord_and_tone(self): c5_chord = Chord(Tone("C"), Tone("G")) result_chord = c5_chord + Tone("E") self.assertEqual(str(result_chord), "C-E-G") def test_subtract_from_major_chord(self): c_major_chord = Chord(Tone("C"), Tone("E"), Tone("G")) result_chord = c_major_chord - Tone("E") self.assertEqual(str(result_chord), "C-G") def test_c_power_chord_subtract_g(self): c_power_chord = Chord(Tone("C"), Tone("G")) with self.assertRaises(TypeError) as context: c_power_chord - Tone("G") self.assertEqual(str(context.exception), "Cannot have a chord made of only 1 unique tone") def test_c_power_chord_subtract_e(self): c_power_chord = Chord(Tone("C"), Tone("G")) with self.assertRaises(TypeError) as context: c_power_chord - Tone("E") self.assertEqual(str(context.exception), "Cannot remove tone E from chord C-G") def test_add_two_chords(self): c5_chord = Chord(Tone("C"), Tone("G")) this_other_chord = Chord(Tone("A"), Tone("B")) result_chord = c5_chord + this_other_chord self.assertEqual(str(result_chord), "C-G-A-B") def test_transpose_minor_chord(self): c_minor_chord = Chord(Tone("C"), Tone("D#"), Tone("G")) d_minor_chord = c_minor_chord.transposed(Interval(2)) self.assertEqual(str(d_minor_chord), "D-F-A") def test_transpose_a_sharp_minor_chord(self): d_minor_chord = Chord(Tone("D"), Tone("F"), Tone("A")) a_sharp_minor_chord = d_minor_chord.transposed(-Interval(4)) self.assertEqual(str(a_sharp_minor_chord), "A#-C#-F") if __name__ == '__main__': unittest.main() ```
Георги Кунчев
03.11.2024 09:33

@Дейвид_Барух, Павел зададе този въпрос и дадохме отговор. Следващият тон става главен.