Nowa wersja
|
Poprzednia wersja
|
pl:dydaktyka:jimp2:2017:labs:wyjatki [2017/02/25 12:56] 127.0.0.1 edycja zewnętrzna |
pl:dydaktyka:jimp2:2017:labs:wyjatki [2019/06/27 15:50] (aktualna) |
======Wyjątki====== | ======Wyjątki====== |
| |
<WRAP center round info 60%> | |
To laboratorium jest opcjonalne i zawiera informacje mające na celu poszerzenie wiedzy o języku C%%++%%. | |
| |
Z racji, że nie jest to "regularne" laboratorium, nie obowiązuje ono do kolokwium, a za rozwiązanie zadań nie będą przyznawane plusy/punkty. Oczywiście w przypadku jakichkolwiek pytań czy wątpliwości zapraszam do zadawania pytań / pisania maili / przychodzenia na konsultacje! Udzielę wszelkich wyjaśnień :-) | |
</WRAP> | |
| |
| |
=====Czym są wyjątki===== | =====Czym są wyjątki===== |
Wyjątki to inaczej błędy generowane podczas wykonywania programu - sytuacje wyjątkowe, anomalie. | Wyjątki to inaczej błędy generowane podczas wykonywania programu - sytuacje wyjątkowe, anomalie. |
c=a/b;</code> | c=a/b;</code> |
| |
Te konstrukcje wymagają jednak, aby sytuacja wyjątkowa była rozwiązana lokalnie: w miejscu w którym się pojawiła. Rozpatrywanie niektórych sytuacji wyjątkowych w miejscu ich wystąpienia nie jest naturalne. | Te konstrukcje wymagają jednak, aby sytuacja wyjątkowa była rozwiązana lokalnie: w miejscu w którym się pojawiła. Rozpatrywanie większości sytuacji wyjątkowych w miejscu ich wystąpienia nie jest wygodne ani wręcz możliwe (nie dysponujemy odpowiednim kontekstem w jaki sposób powinna zostać obsłużony błąd). |
| |
| <code cpp> |
| int bar(int v) { |
| if (v == 2) { |
| ... |
| return 0; //OK |
| } else { |
| //sugeruje blad funkcja spodziewala sie |
| //dwóch parametrów programu |
| return -1; |
| } |
| } |
| |
| inf foo(int args_number) { |
| int result = bar(args_number); |
| if (result < 0) { |
| //jeśli nastąpił błąd w funkcji bar |
| //propaguje wynik do wywołania |
| return result; |
| } else { |
| ... |
| } |
| } |
| |
| int main(int argc, char *argv[]) { |
| int result = foo(argc); |
| if (result < 0) { |
| //dopiero z punktu wiedzenia funkcji main wiadomo, |
| //że w tym wypadku należy poinformować użytkownika |
| //o błędzie |
| cerr << "nie poprawna ilość argumentów" << end; |
| return -1; |
| } |
| ... |
| return 0; |
| } |
| </code> |
| |
| Alternatywna metoda w postaci zwracania kodów błędu z funkcji jest w praktyce niewygodna do stosowania i łatwo o przoczenie instrukcji warunkowej sprawdzającej przypadek błędnego wykonania. |
| |
Mając daną metodę klasy //Matrix// odpowiedzialną za dodawanie macierzy i zwracającą obiekt klasy macierz będący wynikiem dodawania, w jaki sposób można przekazać informację o tym, że wymiary podanych macierzy są nieprawidłowe do kodu wywołującego tą metodę? Odpowiedzią na ten problem są wyjątki. | Mając daną metodę klasy //Matrix// odpowiedzialną za dodawanie macierzy i zwracającą obiekt klasy macierz będący wynikiem dodawania, w jaki sposób można przekazać informację o tym, że wymiary podanych macierzy są nieprawidłowe do kodu wywołującego tę metodę, gdzie już wiadomo jak należy obsłużyć tą wyjątkową sytuację a z pominięciem wszystkich pośrednich wywołań funkcji (takich jak foo powyżej)? Odpowiedzią na ten problem są wyjątki. |
| |
Mechanizm wyjątków pozwala na przerwanie działania programu w momencie pojawienia się sytuacji wyjątkowej i //wycofanie// się do fragmentu kodu który te sytuacje obsługuje: | Mechanizm wyjątków pozwala na przerwanie działania programu w momencie pojawienia się sytuacji wyjątkowej i //wycofanie// się do fragmentu kodu który tę sytuację wie jak obsłusłużyć (np. poprosić użytkownika o ponowne wprowadzenie danych, albo zamknięcie programu, albo ciche pominięcie problemu): |
| |
<code cpp> | <code cpp> |
| #include <stdexcept> |
#include <iostream> | #include <iostream> |
| |
using namespace std; | using namespace std; |
| |
// Klasa reprezentująca naszą sytuację wyjatkową | // Klasa reprezentująca naszą sytuację wyjątkową |
class WrongDimensionsException{ | class WrongDimensionsException : public invalid_argument { |
private: | |
Matrix m; | |
public: | public: |
WrongDimensionsException(Matrix _m):m(_m){} | WrongDimensionsException(const Matrix &m) : invalid_argument{"Wrong dimensions of matrix"}, m_{m}{} |
| public: |
void printMessage(){ | Matrix GetMatrix() const { |
cout << "Dimensions of matrix " << m << "are wrong" << endl; | return m_; |
} | } |
| private: |
| Matrix m_; |
}; | }; |
| |
class Matrix{ | class Matrix{ |
| public: |
| ... |
| Matrix add(const Matrix& m); |
private: | private: |
int rows, cols; | int rows, cols; |
double** tab; | double** tab; |
public: | |
... | |
// Ustalenie, że metoda add moze rzucić wyjątek typu | |
// WrongDimensionsException | |
Matrix add(const Matrix& m) throw (WrongDimensionsException); | |
}; | }; |
| |
Matrix Matrix::add(const Matrix& m) throw (WrongDimensionsException){ | Matrix Matrix::add(const Matrix& m) { |
// jeśli wymiary nie są prawidłowe, to zgłoś wyjątek | // jeśli wymiary nie są prawidłowe, to zgłoś wyjątek |
if( !validDimensions(*this,m) ) throw WrongDimensionsException(m); | if( !validDimensions(*this,m) ) { |
| throw WrongDimensionsException(m); |
| } |
| |
Matrix result; | Matrix result; |
| |
} | } |
int main(){ | |
| int static_main(){ |
Matrix m1("[1 2 ; 3 4]"); | Matrix m1("[1 2 ; 3 4]"); |
Matrix m2("[3 4 4; 5 6 4]"); | Matrix m2("[3 4 4; 5 6 4]"); |
| |
try{ | try { |
cout << m1.add(m2) << endl; | cout << m1.add(m2) << endl; |
}catch(WrongDimensionsExcepion w){ | } catch(const WrongDimensionsExcepion &w) { |
// jesli wyjątek zostanie zgłoszony program wejdze tutaj | // jesli wyjątek zostanie zgłoszony program wejdze tutaj |
// i wyświetli informacje | // i wyświetli informacje |
w.printMessage(); | cerr << w.what() << ": " << w.GetMatrix() << endl; |
} | } |
| } |
| |
| int user_main() { |
| while(true) { |
| try { |
| std::string m1_str; |
| std::string m2_str; |
| cin >> m1_str; |
| cin >> m2_str; |
| Matrix m1(m1_str); |
| Matrix m2(m2_str); |
| cout << "Result is: " << m1.add(m2) << endl; |
| } catch(const WrongDimensionsExcepion &w) { |
| // nawet jesli wyjątek zostanie zgłoszony zostanie tutaj wyświetlona |
| // informacja dla użytkownika |
| cerr << "Wrong dimensions of Matrices. Please enter matrices once again..." << endl; |
| } |
| } |
| } |
| |
| int main() { |
| static_main(); |
| user_main(); |
} | } |
</code> | </code> |
| |
{{.:exceptions.png |Zwijanie stosu}}<code cpp> | {{.:exceptions.png |Zwijanie stosu}}<code cpp> |
int foo() throw (exception) { | int foo() { |
string s("ala ma kota"); | string s("ala ma kota"); |
bar(); | bar(); |
} | } |
| |
int bar() throw (exception){ | int bar() { |
// pamięć nie | // pamięć nie |
// zostanie zwolniona | // zostanie zwolniona |
} | } |
| |
int fido() throw (exception){ | int fido() { |
Matrix m("[1 2 3; 3 4 5]"); | Matrix m("[1 2 3; 3 4 5]"); |
throw exception(); | throw runtime_error(__func__ + " error in " + __FILE__ + " at " + __LINE__); |
return 0; | return 0; |
} | } |
foo(); | foo(); |
| |
}catch(exception e){} | } catch(const exception &e){ |
| cerr << e.what() << endl; |
| } |
| |
cout << "Dalej" << endl; | cout << "Dalej" << endl; |
using namespace std; | using namespace std; |
| |
Matrix Matrix::div(const Matrix& m) throw (WrongDimensionException, DivisionByZero){ | Matrix Matrix::div(const Matrix& m) { |
// jeśli wymiary nie są prawidłowe, to zgłoś wyjątek | // jeśli wymiary nie są prawidłowe, to zgłoś wyjątek |
| |
} | } |
</code> | </code> |
| |
| |
=====Przechwycenie wyjątku===== | =====Przechwycenie wyjątku===== |
====try/catch==== | ====try/catch==== |
Oznacza on mniej więcej tyle: //jeśli pojawił się wyjątek, którego nie mają w parametrach wcześniejsze bloki catch ja się nim zajmę.// **Uwaga** Kolejność bloków catch ma znaczenie! Konstrukcja //catch(...)// pasuje do wszystkich wyjątków, dlatego powinna być umieszczana zawsze jako ostatnia. | Oznacza on mniej więcej tyle: //jeśli pojawił się wyjątek, którego nie mają w parametrach wcześniejsze bloki catch ja się nim zajmę.// **Uwaga** Kolejność bloków catch ma znaczenie! Konstrukcja //catch(...)// pasuje do wszystkich wyjątków, dlatego powinna być umieszczana zawsze jako ostatnia. |
| |
| |
| Jeśli dwa różne wyjątki mają wspólną klasę bazową można je przechwycić w pojedynczym bloku catch: |
| <code cpp> |
| int main() { |
| Matrix m1 ... |
| Matrix m2 ... |
| try { |
| m1.div(m2); |
| } catch (const invalid_argument &e) { |
| cerr << e.what() << endl; |
| } catch (...) { |
| //ten blok jest wstanie przechwycić każdy inny wyjątek |
| cerr << "Something went wrong" << endl; |
| } |
| } |
| </code> |
====Wyjątki nieoczekiwane==== | ====Wyjątki nieoczekiwane==== |
W przypadku kiedy wewnątrz funkcji pojawi się wyjątek, który nie znajduje się w specyfikacji wyjątków danej funkcji, wywoływana jest automatycznie funkcja //unexpected//, która z kolei wywołuje funkcje ustawioną przez //set_unexpected//. Domyślnie funkcją tą jest //terminate// - czyli zakończenie programu. | W przypadku kiedy wewnątrz funkcji pojawi się wyjątek, który nie znajduje się w specyfikacji wyjątków danej funkcji, wywoływana jest automatycznie funkcja //unexpected//, która z kolei wywołuje funkcje ustawioną przez //set_unexpected//. Domyślnie funkcją tą jest //terminate// - czyli zakończenie programu. |
Jeśli zdefiniowana przez programistę funkcja nie rzuca wyjątku określonego w specyfikacji funkcji (patrz [[#specyfikacja_wyjatkow|Specyfikacji wyjątków]], to program po jej wywołaniu i tak zostanie zakończony. | Jeśli zdefiniowana przez programistę funkcja nie rzuca wyjątku określonego w specyfikacji funkcji (patrz [[#specyfikacja_wyjatkow|Specyfikacji wyjątków]], to program po jej wywołaniu i tak zostanie zakończony. |
| |
=====Specyfikacja wyjątków===== | =====noexcept===== |
//Specyfikacja wyjątków// pozwala na zdefiniowanie listy wyjątków, które mogą być zgłoszone przez funkcję. Robi się to przy pomocy operatora //throw// umieszczonego po liście parametrów funkcji: | Specyfikacja noexcept pozwala na jawne zadeklarowanie metody jako nie wyrzucającej wyjątków, wtedy żadna pochodna metoda również nie może wyrzucać wyjątków, wszystkie pozozstałe motedy są traktowane jako potencjlanie wyrzucające wyjątki: |
| |
<code cpp> | <code cpp> |
public: | public: |
... | ... |
// Metoda może zgłosić ejden z dwóch wyjątków określonych | // Metoda potencjalnie wyrzuca wyjątki |
// jako parametry operatora throw. Zakładamy, ze klasy | Matrix div(const Matrix&); |
// WrongDimensionsExcpetion i DivisionByZero są zaimplementowane | // Metoda nie może wyrzucić wyjątku |
Matrix div(const Matrix&) throw (WrongDimensionsExcpetion,DivisionByZero); | virtual ~Matrix() noexcept; |
}; | }; |
| |
</code> | </code> |
| |
Umieszczenie pustej specyfikacji wyjątków (throw()) oznacza, ze funkcja nie powinna zgłaszać żadnych wyjątków. Jeśli jednak wewnątrz funkcji zostanie zgłoszony wyjątek, zostanie wywołana funkcja //unexpected//. Jeśli nie określi się żadnych wyjątków, to znaczy, że metoda może zgłaszać dowolne wyjątki. | |
| |
<code cpp> | |
int myfoo (int param) throw(); // nie można zgłaszać żądnych wyjątków | |
int mybar (int param); // wszystkie wyjątki dozwolone | |
</code> | |
| |
=====Konstruktory, destruktory i wyjątki===== | =====Konstruktory, destruktory i wyjątki===== |
| |
Wyjątki w konstruktorach wyrzuca się w analogiczny sposób jak w //zwykłych// metodach. Kiedy zostanie wyrzucony wyjątek w ciele konstruktora, mechanizm zwijający stos uruchomi destruktory wszystkich obiektów składowych danego obiektu. | Wyjątki w konstruktorach wyrzuca się w analogiczny sposób jak w //zwykłych// metodach. Kiedy zostanie wyrzucony wyjątek w ciele konstruktora, mechanizm zwijający stos uruchomi destruktory wszystkich obiektów składowych danego obiektu. |
| |
Problem pojawia się jednak kiedy w ciele konstruktora dynamicznie alokujemy pamięć. Nie zostanie ona w takim wypadku zwolniona. | W praktyce zawsze należy sprwdzić dziedzinę parametrów przekazanych w konstruktorze i pozowalać na utworzenie jedynie w pełni poprawnego obiektu, namtomiast w przeciwnym wypadku należy wyrzucić wyjątek. |
| |
| Problem pojawia się jednak kiedy w ciele konstruktora dynamicznie alokujemy pamięć. Nie zostanie ona w takim wypadku zwolniona. Chyba, że użyjemy smart pointery. |
| |
====Wyjątki w destruktorze==== | ====Wyjątki w destruktorze==== |
C++ gwarantuje co prawda, że w przypadku zaistnienia takiej sytuacji zostanie wywołana funkcja //terminate()// i program zostanie zamknięty, nie jest to jednak satysfakcjonujące rozwiązanie. | C++ gwarantuje co prawda, że w przypadku zaistnienia takiej sytuacji zostanie wywołana funkcja //terminate()// i program zostanie zamknięty, nie jest to jednak satysfakcjonujące rozwiązanie. |
| |
| Od wersji C++11 destuktory domyślnie są zadeklarowane jako noexcept. |
=====Dziedziczenie i wyjątki===== | =====Dziedziczenie i wyjątki===== |
(:!: do zrozumienia tej sekcji wymagana jest znajomość mechanizmu dziedziczenia w C%%++%% -> laboratorium [[.:dziedziczenie|Dziedziczenie i polimorfizm]]) | (:!: do zrozumienia tej sekcji wymagana jest znajomość mechanizmu dziedziczenia w C%%++%% -> laboratorium [[.:dziedziczenie|Dziedziczenie i polimorfizm]]) |
}; | }; |
| |
void drawBall() throw (BallException){ | void drawBall() { |
throw BallException(); | throw BallException(); |
} | } |
| |
Już podczas kompilacji otrzymujemy ostrzeżenie, że wyjątek //BallException// nigdy nie zostanie wychwycony. Dzieje się tak dlatego, że po napotkaniu pierwszego bloku //catch//, nastąpi automatyczne rzutowanie w górę i dopasowanie //BallException// do //CircleException//. | Już podczas kompilacji otrzymujemy ostrzeżenie, że wyjątek //BallException// nigdy nie zostanie wychwycony. Dzieje się tak dlatego, że po napotkaniu pierwszego bloku //catch//, nastąpi automatyczne rzutowanie w górę i dopasowanie //BallException// do //CircleException//. |
| |
| ===== Warto się zapoznać ===== |
| - [[http://www.exceptionsafecode.com/]] (filmiki z konferencji) |
| - [[http://en.cppreference.com/w/cpp/language/noexcept_spec]] |
| - [[http://www.gotw.ca/publications/mill22.htm]] |
| |
======Ćwiczenia====== | ======Ćwiczenia====== |
- Przetestuj przykład z sekcji [[#dziedziczenie_i_wyjatki|Dziedziczenie i wyjątki]]. Co zrobić, żeby wyjątek //BallException// był łapany poprawnie? | - [1 plus] Przetestuj przykład z sekcji [[#dziedziczenie_i_wyjatki|Dziedziczenie i wyjątki]]. Co zrobić, żeby wyjątek //BallException// był łapany poprawnie? |
- Napisz trzy metody, które będą posiadały wyspecyfkowane po jednym wyjątku jaki mogą wyrzucać. W każdej z metod wyrzuć wyjątek, ale inny od tego jaki został wyspecyfikowany. Za pomocą //set_unexpected// ustaw metodę, która będzie uruchamiana w takim wypadku. **Uwaga**: Program nie może się zakończyć w przypadku wywołania żadnej z 3 funkcji! Jak to osiągnąć pisząc tylko jedną funkcję //unexpected// i pamiętając, że program nie zostanie zatrzymany tylko wtedy kiedy funkcja //unexpected// wyrzuci wyjątek określony w specyfikacji funkcji, która wyrzuciła niepoprawny wyjątek? | - [3 plusy] Napisz klasę //PESEL//, która w konstruktorze przyjmuje ciąg znaków będących numerem PESEL. Klasa powinna posiadać metodę //validatePESEL(const char*)//, która sprawdza czy PESEL jest poprawny według algorytmu podanego na [[http://pl.wikipedia.org/wiki/PESEL|Wikipedii]]. Jeśli przekazany do konstruktora PESEL nie jest poprawny, program powinien wyrzucać wyjątek. Napisz funkcję //main// i przetestuj program. |
- Napisz klasę //PESEL//, która w konstruktorze przyjmuje ciąg znaków będących numerem PESEL. Klasa powinna posiadać metodę //validatePESEL(const char*)//, która sprawdza czy PESEL jest poprawny według algorytmu podanego na [[http://pl.wikipedia.org/wiki/PESEL|Wikipedii]]. Jeśli przekazany do konstruktora PESEL nie jest poprawny, program powinien wyrzucać wyjątek. Napisz funkcję //main// i przetestuj program. | - [3 plusy] Dla klasy Student dopisać metody walidujące argumenty przekazywane do klasy. W taki sposób by uniemożliwić storzenie niepoprawnego stanu obiektu. |
- Napisz program, który będzie służył do opóźniania, lub przyspieszania wyświetlania napisów do filmów w formacie MicroDVD ;-). Czas pojawienia się napisu oznaczany jest w tym formacie dwiema liczbami umieszczonymi pomiędzy nawiasami klamrowymi. Pierwsza liczba to numer klatki pojawienia się napisu, druga to numer klatki zniknięcia. Program powinien posiadać metodę //delay(const char* in, const char* out,int delay, int fps)// wykonującą opóźnienie (lub przyspieszenie) napisów o podaną ilość milisekund w zależności od tego jaki film ma //framerate//. \\ Program powinien przyjmować jako parametry cztery wartości: ścieżkę do pliku wejściowego, ścieżkę do pliku wyjściowego i liczbę milisekund i //framerate//. Metoda //delay// powinna wyrzucać wyjątek gdy w pliku pojawi się niepoprana sekwencja znaków. Np. zamiast {1234}{4565} pojawia się {sdfg}{33ff}. | * Student musi mieć co najmniej imię i nazwisko, każdy człon imienia musi być napisany z wielkiej litery pozostałe litery małe. Imię nie może zawierać cyfr i znaków specjalnych (amperad, dolar, procent, itp...) |
- Jeśli masz dostępną swoją klasę Matrix (laboratorium [[.:klasy2|Klasy i obiekty II]]), dopisz dla niej obsługę wyjątków. | * Wiek studenta musi mieścić się w zakresie 10 do 100 lat |
| * kierunek studiów musi zawierać się w zbiorze oferowanym akutalnie przez uczelnie XYZ: informatyka, ekonomia, matematyka, fizyka, filozofia |
| * w każdym wypadku, kiedy zostanie naruszony któryś z warunków należy zwrócić wyjątek dziedziczący po klasie invalid_argument, odpowiednio: |
| * InvalidNameSurname, InvalidNameCharacters, InvalidAge, InvalidProgram |
| * dopisać main z możliwością wczytywania danych o studencie i tworzącym studentów w pętli wstawiającym poprawnie utworzone obiekty do repozytorium. |
| |
| ======Zadanie domowe====== |
| Napisz program, który będzie służył do opóźniania, lub przyspieszania wyświetlania napisów do filmów w dwóch różnych formatach: MicroDVD i SubRip. Elementem centralnym interfejsu biblioteki jest abstrakcyjna klasa: **MovieSubtitles** z dwoma wirtualnymi metodami (przestrzeń nazw **moviesubs**): |
| * <code cpp>void ShiftAllSubtitlesBy(int offset_in_micro_seconds, int frame_per_second, std::istream *in, std::ostream *out)</code> - metoda nie powinna posiadać domyślnej implementacji (abstrakcyjna, czysto wirtualna). |
| * Destruktor (domyślna implementacja) |
| - **[1 punkt]** (lab8_micro_dvd_correct_cases_tests) Zaimplementuj klasę **MicroDvdSubtitles** implementującą metody z klasy bazowej **MovieSubtitles** i wspierającą format **MicroDvd**. Każda linia ma następujący format: //{INT_ON}{INT_OFF}SUBTITLE// gdzie nawiasy klamrowe wystepują w tej linii jak widać, natomiast INT_ON oznacza numer klatki filmu, w której pojawia się napis; INT_OFF numer klatki filmu w której znika napis; SUBTITLE oznacza napis do wyświetlenia. W tym formacie napis do wyświetlenia może zawierać dodatkowe informacje sterujące wyświetlaniem, np. | rozdzielający linie napisu, czy {y:b} wprowadzający pogrubienie. |
| - **[1 punkt]** (lab8_micro_dvd_error_cases_tests) Przesuwanie napisów powinno również obsługiwać przypadki, gdy podany plik (zawrtość strumienia wejściowego) zawiera niepoprawnie sformatowany tekst: |
| * próba przesunięcia klatek wstecz która skutkowałaby ujemnymi wartościami INT_ON i/lub INT_OFF powinna być zasygnalizowana przez wyjątkiem **NegativeFrameAfterShift** |
| * jeśli klatka ma zniknąć przed pojawieniem się (INT_ON >= INT_OFF) należy zasygnalizować to wyjątkiem **SubtitleEndBeforeStart** |
| * jeżeli jest inny błąd formatowania lini np. brak {INT_OFF}, brak nawiasu klamrowego, itp. należy zwrócić wyjątek **InvalidSubtitleLineFormat** |
| * jeżeli kolejne napisy pojawiają sie przed niż wcześniejszymi napisami powinine zostać zgłoszony wyjątek **OutOfOrderFrames** |
| * wszystkie te wyjątki powinny dziedziczyć po klasie **SubtitlesException**, która przyjmuje dwa argumenty w konstruktorze: numer linii, w której wystąpił błąd, treść tej linii (cała linia, nie tylko etykieta). A z kolei klasa **SubtitlesException** powinna dziedziczyć po klasie **std::invalid_argument** z biblioteki stdexcept |
| - **[1 punkt]** (lab8_sub_rip_correct_cases_tests) Zaimplementuj klasę **SubRipSubtitles** implementującą metody z klasy bazowej **MovieSubtitles** i wspierającą format **SubRip**. Każda napis ma następujący format: //INDEX NEW_LINE TIME_IN -> TIME_OFF NEW_LINE SUBTITLE1 NEW_LINE SUBTITLE2 NEW_LINE NEW_LINE//, gdzie INDEX - to numer klatki, NEW_LINE to znak nowej linii, TIME_IN i TIME_OFF to moment pojawienia się i zniknięcia klatki w formacie HH:MM:SS,mmm (godzina:minuta:sekundy,milisekundy). Napisy mogą być wielolinijkowe, nowa klatka pojawia się po pustej linii. |
| - **[1 punkt]** (lab8_sub_rip_error_cases_tests) poza wyjątkami wymienionymi w punkcie drugim należy zaimplementować też: |
| * numery klatek są nie pokolei **OutOfOrderFrames** |
| * jeśli klatka nie posiada secyfikacji kiedy miałaby się pojawić **MissingTimeSpecification** |
| - **[1 punkt]** (lab8_movie_subtitles_tests) sprawdzić czy wszystko działa ok. |