Това е проект, даден за домашно в курса Обектно-ориентирано програмиране 1-ви курс 2-ри семестър 2022 г, спец. КН. Целта на проекта е да се създаде конзолно приложение, което да предоставя възможността потребителят да записва своите задачи за деня и да ги достъпва по удобен и лесен начин в конзолата. Не по-малко важна част от проекта е да се създаде добра и гъвкава структура, която би могла да се използва от други програмисти, които биха разработвали календар. Проектът е написан на C++. Линк към GitGub: https://github.com/kristian3551/Personal-calendar.
Основната цел на проекта е изграждане на добра структура на приложението, която може да служи като основа за бъдещо развитие. Приложението трябва да работи при всички сценарии на вход от потребителя и да има лесно достъпен интерфейс.
Разработени функционалности са:
- Запазване на час за среща в календара по дадени параметри за среща (дата, часове, коментар и име).
- Отмяна на среща по въведени параметри.
- Отпечатване на всички срещи за даден ден (ако има такива и датата е преди 31.03.1916 г., че не някой би искал да запазва в календара си среща за преди 106 години).
- Промяна на някой параметър на среща, ако тя съществува и след промяната е възможно да бъде записана (възможно е да има съвпадане на часови диапазон с друга среща на съответната дата).
- Търсене по даден низ - отпечатване на всички запазени срещи, които съдържат в името си или в коментара си подадения от потребителя низ.
- Създаване на файл по начална и крайна дата, в който има данни за броя на срещи за даден ден от седмицата. Пример:
Monday: 5 events
Tuesday: 4 events
Wednesday: 4 events
... и т.н.- По подадени начална и крайна дата и час
time
(часове, минути и секунди) приложението намира свободен час, в който може да запише среща с такава продължителност, колкото e въведениятtime
. Часовете, в които търси време за среща, са между 8:00 и 16:00 ч.- Записването на всички данни в текстов файл, служещ като база данни. При всяко влизане в приложението се зарежда съответния файл със записани в строго дефинирания формат срещи. При всяко излизане от приложението заредените и евентуално модифицирани срещи се записват отново в същия файл. By default името на файла е database.dat, който се зарежда от директорията на компилирания файл, стартиращ приложението.
Календарът, както всички знаем, представлява таблица с дни и месеци за дадена година. Тъй като самото приложение е в конзолата, основният фокус са самите срещи и менажирането им в съответните класове, а не самият UI. За целта на проекта календарът представлява списък от дни с различни дати. Един ден представлява списък от часове, на които е запазена среща.
Откъм алгоритмична гледна точка проектът не използва особено тежки алгоритми, тъй като е писан в курса ООП. Търсенето е линейно и в някои случаи двоично, когато логиката позволява. В някои класове (например DaySchedule
) се ползва подход, който аз наричам sortedInsert, при който на всяко добавяне на нова среща за деня масивът от срещи остава сортиран по начален час, което и дава възможност за логаритмично търсене по часовете.
Векторът като основна структура от данни
Векторът като структура от данни е в основата на изграждането на функционалността на някои класове (DaySchedule
и Calendar
), като е вътрешно имплементиран в тях за удобство и възможности за гъвкава промяна на функционалността.
Основен проблем на проекта е баланс между използвана памет и бързина на търсене и добавяне. Доброто разделяне на обектите т.е. стриктното следване на принципа на абстракция също е от голямо значение. Писането на по-голямо количество код, планирането и концептуалното изграждане на проекта са не по-малки проблеми от предходните.
Подход, който улеснява местенето на обекти като например в DaySchedule::addEvent(const Event&)
, е използването на двойни пойнтъри. Тогава единствено разместваме адреси, което е много по-евтина операция от създаването на цели копия на обекти.
Кодът е изграден според принципите на обектно-ориентираното програмиране, изучавани до момента - абстракция и капсулация. Логиката на приложението не налага нуждата да се ползва наследяване или полимофорфизъм, поне на този начален етап на разработка.
Aтомарни класове в архитектурата на проекта са:
Time
- реализирана е базова функционалност на клас, от който се изисква да пази часове, минути и секунди, както и да сравнява такива обекти.Date
- пресмята вътрешно деня от седмицата, като за улеснение и бързина началната дата, която може да бъде заемана, е 31.03.1916 г. поради смяна на календара. Поддържа валидация за дата, независеща от създаване на конкретен обект (използва се вEngine
класа)String
- поддържа някои от най-важните стандартни функционалности на познатия ниString
клас. Реализирана е голяма четворка.
- Забележка: За всички класове по-горе е реализирана логика за сравняване, както и стандартни гетъри и сетъри.
Event
Следващият отдолу-нагоре в йерархията клас е Event. Задачата му е да пази данните за дадена среща (име, коментар, дата, начален час и краен час. Предполагам, че се подразбира от какъв тип данни са променливите), да проверява дали две срещи се засичат в часови диапазон, възможност за принтиране във файл и специално на конзолата (функцията print). Не е реализирана голяма четворка, тъй като не се заделя динамична памет, а където в член данните се налага да се ползва такава, е реализирана голяма четворка в класа на съответната член-данна.
DaySchedule
Класът DaySchedule
на практика представлява OrderedList от
срещи, който съдържа и допълнителна функционалност при добавяне на среща и изтриване.
Всеки ден в календара е един обект DaySchedule
.
Всеки DaySchedule
има дата, на който отговаря, масив от тип Events**, както и размер и капацитет (почти стандартен вектор) с реализирана голяма четворка. Добавянето на среща винаги запазва масива сортиран, което позволява двоично търсене с методите bool DaySchedule::find(const Event&) const;
и int DaySchedule::getEventIndex(const Event&) const;
. Реализиран метод е findFreeTimeForEvent(const Time& timeForEvent) const
, който връща час, в който може да се запази среща с продължителност timeForEvent без съвпадане на часове.
Класът има публичен конструктор DaySchedule()
с дифолтна дата, за да може да се създава масив от обекти с тип DaySchedule
и след това да се инициализира с оператор =, както и предполагам, че читателят се досеща, но трябва да го спомена. Все пак е смислово грешно.
Calendar
Класът, който фактически реализира функционалността на приложението, е класът Calendar
. Представлява OrderedList от обекти от тип DaySchedule
и логиката, свързана с добавяне на дни и търсене на ден по дата, са имплементирани подобно на класа DaySchedule
. В интерфейса на Calendar
са реализирани функции, отговарящи на изискваните по условие функционалности. Примери:
void printDay(const Date&) const;
void printEventsByString(const String&)const;
bool addEvent(const Event&);
Класът реализира голяма четворка заради заделянето на динамична памет. "Дните" се пазят в динамичната памет, достъпна чрез член-данната DaySchedule** days
. Двоен пойнтър улеснява местенето на обекти в масива значително поради същата причина, указана по-горе за класа DaySchedule
.
Четенето от файл и записването също е реализирано в класа Calendar
. За целите на проекта това е съвсем достатъчно, без да се прави отделен клас за писане и четене. Двете функции saveInFile()
и readFromFile()
се грижат за това.
Конструкторът на Calendar
единствено извиква функцията readFromFile()
, която търси файла database.dat
, за да прочете данните от него, и ако този файл не съществува или неговото отваряне е неуспешно, инизиализира масива от указатели дифолтно, задава дифолтните стойности на променливите size
и capacity
и програмата продължава по стандартен начин.
Engine
Този клас съдържа точно една публична функция run()
, която задейства конзолното приложение в main. Другите функции са помощни, където се случва валидацията на данните на потребителите, както и управлението на самия календар - единсвената член-данна на Engine
. Въвеждането на данните се случва във функциите initTime()
, initDate()
, initName()
и initComment()
. Там се throw-ват грешки, които се handle-ват във функциите, private за класа Engine
. Извикването на тези функции се извършва в Engine::run()
.
сетъри:
void setMinutes(unsigned minutes)
: инициализира минутите по модулно деление на 60 и ако minutes>=60, то увеличаваме часовете с minutes / 60.
void setSeconds(unsigned seconds)
: подобна логика на setMinutes
void setHours(unsigned hours)
: инициализира часовете като hours % 24, за да се гарантира, че входа е коректен
Time Time::operator +(const Time& time) const
: събира секундите, минутите и часовете посредством логиката в сетърите, които при надхвърляне на 60 или 24 от съответно секундите и минутите или часовете се грижат за правилното изчисление. Ако при събиране часовете надхвърлят 24, то часовете "превъртат" и се връщат на нула.
Date::Date()
: запазва датата 31-ви март 1916 г., за да не става объркване в разликата между стария стил и новия стил (Григорианския календар). Също така dayOfWeek
се сетва на 5, което се интерпретира като петък (ден нула е неделя), защото 31.03.1916 г. е петък.
Date::Date(unsigned day, unsigned month, unsigned year)
: ако подадената дата е по-ранна от 31.03.1916 г., сетва я на 31.03.1916 г. След това запазва данните и извиква setDayOfWeek().
Date::Date(unsigned day, unsigned month, unsigned year, unsigned dayOfWeek)
: цялата идея на този конструктор е да се създаде дата, без да се изчислява dayOfWeek, тъй като в самото изчисление в setDayOfWeek()
се налага да бъде създадена дата и се влиза в безкрайна косвена рекурсия. Това е private конструктор, тъй като се извиква само в тялото на класа Date
.
void Date::setDayOfWeek()
: започва да инкрементира датата 31.03.1916 г. с функцията Date::incrementDay()
, като в същото време инкрементира dayOfWeek и го дели модулно на 7.
static bool Date::isValidDate()
: валидира дата, използвайки private конструктора. Инициализираните директно във функцията dayOfWeek на ред 118:
return (Date(day, month, year, 5) >= Date(31, 3, 1916, 5));
нямат значение, защото операторите за сравняване на дати не сравняват деня от седмицата.
bool Event::doEventsIntersect(const Event& event) const
: тъй като за началният час на среща е гарантирано, че е по-малък от крайния (логиката в Engine
), е достатъчно да се провери дали крайният на първата среща е по-малък или равен на началния на втората и обратно: началният на първия е по-голям или равен на крайния на втория:
--startTime1------endTime1-startTime2------endTime2-
bool DaySchedule::addEvent(const Event& event)
: добавя event на съответния ден. Ако подадената като параметър на функцията среща е на друга дата, нищо не се добавя и връща false. Ако съществува среща, чийто часови диапазон се засича в часовия диапазон на event, срещата не се добавя и функцията връща false. Обхождането на срещите е линейно. Поддържа се стандартната логика на клас std::vector
за увеличаване на капацитета на масива посредством функцията DaySchedule::resize()
. След като е гарантирано, че има място в events, се проверява на кое място в масива трябва да бъде добавен event, като се гледат началният и крайният час в сравнението. Търсенето е линейно. След като е установен индексът на добавяне на event, всички следващи срещи в масива се изместват с един индекс "нагоре", events[index] се сетва, големината на events (променливата size) се инкрементира и се връща true за успешно добавяне.
bool DaySchedule::removeEvent(const Event& event)
: ако event не фигурира в events (няма среща на тази дата с този стартов час и краен час), то функцията връща false. Търсенето става с логаритмична сложност. При открит такъв event на индекс index всички срещи в масива след този индекс се изместват с един индекс "надолу", големината се намалява с едно и се връща true за успешно изтриване.
bool DaySchedule::find(const Event& event)
и int DaySchedule::getEventIndex(const Event& event) const
: getEventIndex() връща индекса на event или -1, ако такъв няма след извършването на след двоично търсене. Функцията find връща true, ако индексът на срещата е различен от -1, и false в противен случай. Използвана е една и съща функция, външна на класа, която имплементира binarySearch.
Time DaySchedule::findFreeTimeForEvent(const Time& time) const
: връща начален час на възможна среща в диапазона WORK_TIME_START и WORK_TIME_END с продължителност time. Търси в пролуките между срещите, в началото на възможния период или в края му (08:00 и 16:00 съответно). Ако възможност за среща с такава продължитеност в този ден няма, се връща Time(), което е 00:00:00 ч. Такъв час за срещи е невалиден, затова може да бъде връщан в такъв контекст.
Calendar::Calendar()
: извиква Calendar::readFromFile()
. Последната функция чете информация под точно определен формат от файл с фиксиран път const char* DATABASE_FILE_PATH = "database.dat"
. Пример за форматирането във файла database.dat:
2 // дни, в които има срещи (големината на *days*)
2 // брой срещи изобщо, записани във файла
6 Event1 // брой символи на име и самото име на среща
8 Comment1 // брой символи на коментара и самият него
10 5 2022 // дата (ден месец година)
12 0 0 // начален час
14 0 0 // краен час
6 Event2
8 Comment2
12 5 2022
10 0 0
12 0 0
...
- Форматът на запазване на среща във файл е зададен във функцията
ostream& Event::operator<<(ostream&, const Event&)
.
Ако файлът не е отворен, то days се инициализира като празен, задават се стойности на capacity и size и функцията приключва. В противен случай се четат броя дни от файла, брой срещи и се инициализират days, capacity и size. От файла се четат срещи на брой колкото е стойността на прочетените преди това брой срещи. Четенето е съвсем стандартно, както се чете вход в конзолата.
Функцията Calendar::getIndexByDate()
търси алгоритмично чрез двоично търсене в days, за което е отговорна функцията Calendar::addDay()
, която добавя обект от DaySchedule подобно на DaySchedule::addEvent()
. addDay бива използвана в почти всички функции в Calendar
.
За функциите Calendar::change{Property}(const Event&, {property})
са важни за срещата единствено date, startTime и endTime, тъй като сравнението на среща не зависи от name и comment. Всяка среща еднозначно се определя по датата, началния и крайния час в този проект. Друг вид сравнение не е нужен поне на този етап.
Engine.cpp съдържа функции за вход на данни като например функциите void initDate(Date& date)
и void initTime(Time& time)
. Функцията initDate
хвърля обект от клас String
, ако подадената дата е невалидна. Подобно поведение имат initTime
, initEvent
и initPartialEvent
. Последната функция е подобна на initEvent
, но въвежда само датата, крайния и началния час на среща, което се ползва във функцията Engine::changeEvent()
, извикваща функция от вида Calendar::change{Property}
. Грешките се хващат от функциите, които са private за класа Engine
, извиквани от switch оператора в Engine::run()
.
Engine::run()
: тази функция извиква private функциите на Engine
, с които управляваме календара. Променливата command може да приема всякакви стойности от тип int, където при нула запазваме информацията във файл, при стойности от {1, 2, ..., 7} задействаме функционалност от по-горе описаните задачи за проекта. При всяка останала стойност функцията се прекратява, информацията се запазва и програмата приключва.
Програмата е тествана посредством unit-testing библиотеката Doctest, както и чрез голямо количество ръчни тестове на всеки клас.
Проектът ми помогна да усвоя основните принципи на обектно-ориентираното програмиране на едно по-високо ниво. Той би могъл да е добра за изграждане на календар с много повече функционалности и възможности за потребителя.
- Идеята за имплементация на клас Date е вдъхновена от тази на Angel Dimitriev - https://github.com/Angeld55/Object-oriented_programming_FMI/blob/master/Sem.%2006/Event/Date.cpp