Skip to content

kristian3551/Personal_Calendar

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

62 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Personal calendar

Това е проект, даден за домашно в курса Обектно-ориентирано програмиране 1-ви курс 2-ри семестър 2022 г, спец. КН. Целта на проекта е да се създаде конзолно приложение, което да предоставя възможността потребителят да записва своите задачи за деня и да ги достъпва по удобен и лесен начин в конзолата. Не по-малко важна част от проекта е да се създаде добра и гъвкава структура, която би могла да се използва от други програмисти, които биха разработвали календар. Проектът е написан на C++. Линк към GitGub: https://github.com/kristian3551/Personal-calendar.

Цел и задачи за разработка

Основната цел на проекта е изграждане на добра структура на приложението, която може да служи като основа за бъдещо развитие. Приложението трябва да работи при всички сценарии на вход от потребителя и да има лесно достъпен интерфейс.

Разработени функционалности са:

  1. Запазване на час за среща в календара по дадени параметри за среща (дата, часове, коментар и име).
  2. Отмяна на среща по въведени параметри.
  3. Отпечатване на всички срещи за даден ден (ако има такива и датата е преди 31.03.1916 г., че не някой би искал да запазва в календара си среща за преди 106 години).
  4. Промяна на някой параметър на среща, ако тя съществува и след промяната е възможно да бъде записана (възможно е да има съвпадане на часови диапазон с друга среща на съответната дата).
  5. Търсене по даден низ - отпечатване на всички запазени срещи, които съдържат в името си или в коментара си подадения от потребителя низ.
  6. Създаване на файл по начална и крайна дата, в който има данни за броя на срещи за даден ден от седмицата. Пример:
    Monday: 5 events
    Tuesday: 4 events
    Wednesday: 4 events

    ... и т.н.
  7. По подадени начална и крайна дата и час time (часове, минути и секунди) приложението намира свободен час, в който може да запише среща с такава продължителност, колкото e въведеният time. Часовете, в които търси време за среща, са между 8:00 и 16:00 ч.
  8. Записването на всички данни в текстов файл, служещ като база данни. При всяко влизане в приложението се зарежда съответния файл със записани в строго дефинирания формат срещи. При всяко излизане от приложението заредените и евентуално модифицирани срещи се записват отново в същия файл. By default името на файла е database.dat, който се зарежда от директорията на компилирания файл, стартиращ приложението.

Съдържание

  1. Преглед на предметната област
  2. Проектиране
  3. Реализация, тестване
  4. Заключение

Преглед на предметната област

Календарът, както всички знаем, представлява таблица с дни и месеци за дадена година. Тъй като самото приложение е в конзолата, основният фокус са самите срещи и менажирането им в съответните класове, а не самият UI. За целта на проекта календарът представлява списък от дни с различни дати. Един ден представлява списък от часове, на които е запазена среща.
Откъм алгоритмична гледна точка проектът не използва особено тежки алгоритми, тъй като е писан в курса ООП. Търсенето е линейно и в някои случаи двоично, когато логиката позволява. В някои класове (например DaySchedule) се ползва подход, който аз наричам sortedInsert, при който на всяко добавяне на нова среща за деня масивът от срещи остава сортиран по начален час, което и дава възможност за логаритмично търсене по часовете.
Векторът като основна структура от данни
Векторът като структура от данни е в основата на изграждането на функционалността на някои класове (DaySchedule и Calendar), като е вътрешно имплементиран в тях за удобство и възможности за гъвкава промяна на функционалността.
Основен проблем на проекта е баланс между използвана памет и бързина на търсене и добавяне. Доброто разделяне на обектите т.е. стриктното следване на принципа на абстракция също е от голямо значение. Писането на по-голямо количество код, планирането и концептуалното изграждане на проекта са не по-малки проблеми от предходните.
Подход, който улеснява местенето на обекти като например в DaySchedule::addEvent(const Event&), е използването на двойни пойнтъри. Тогава единствено разместваме адреси, което е много по-евтина операция от създаването на цели копия на обекти.

Проектиране

Кодът е изграден според принципите на обектно-ориентираното програмиране, изучавани до момента - абстракция и капсулация. Логиката на приложението не налага нуждата да се ползва наследяване или полимофорфизъм, поне на този начален етап на разработка.

Aтомарни класове в архитектурата на проекта са:

  1. Time - реализирана е базова функционалност на клас, от който се изисква да пази часове, минути и секунди, както и да сравнява такива обекти.
  2. Date - пресмята вътрешно деня от седмицата, като за улеснение и бързина началната дата, която може да бъде заемана, е 31.03.1916 г. поради смяна на календара. Поддържа валидация за дата, независеща от създаване на конкретен обект (използва се в Engine класа)
  3. 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 са реализирани функции, отговарящи на изискваните по условие функционалности. Примери:

  1. void printDay(const Date&) const;
  2. void printEventsByString(const String&)const;
  3. 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().

Важни елементи на реализацията. Тестване

Time

сетъри: 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::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));

нямат значение, защото операторите за сравняване на дати не сравняват деня от седмицата.

Event

bool Event::doEventsIntersect(const Event& event) const: тъй като за началният час на среща е гарантирано, че е по-малък от крайния (логиката в Engine), е достатъчно да се провери дали крайният на първата среща е по-малък или равен на началния на втората и обратно: началният на първия е по-голям или равен на крайния на втория:
--startTime1------endTime1-startTime2------endTime2-

DaySchedule

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(): извиква 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

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, както и чрез голямо количество ръчни тестове на всеки клас.

Заключение

Проектът ми помогна да усвоя основните принципи на обектно-ориентираното програмиране на едно по-високо ниво. Той би могъл да е добра за изграждане на календар с много повече функционалности и възможности за потребителя.

Ресурси

About

Project for passing OOP course in FMI

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published