├── report.pdf ├── specs ├── id1674.pdf ├── README.md ├── Cargo.toml ├── imdb_well_formed - Copy.csv ├── imdb_well_formed - Copy - Copy.csv ├── Cargo.lock └── src │ └── main.rs ├── resources └── crewmate.png ├── TestQtProject_en_GB.ts ├── resources.qrc ├── addfilmwidget.h ├── main.cpp ├── README.md ├── mainwindow.h ├── .gitignore ├── filmlistmodel.h ├── CMakeLists.txt ├── report.md ├── filmfilterproxymodel.cpp ├── filmfilterproxymodel.h ├── filmlistmodel.cpp ├── mainwindow.ui ├── addfilmwidget.cpp ├── mainwindow.cpp ├── addfilmwidget.ui └── csv.h /report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiftoo/dsba-itop2023-hw/master/report.pdf -------------------------------------------------------------------------------- /specs/id1674.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiftoo/dsba-itop2023-hw/master/specs/id1674.pdf -------------------------------------------------------------------------------- /resources/crewmate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiftoo/dsba-itop2023-hw/master/resources/crewmate.png -------------------------------------------------------------------------------- /TestQtProject_en_GB.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | resources/crewmate.png 4 | 5 | 6 | -------------------------------------------------------------------------------- /specs/README.md: -------------------------------------------------------------------------------- 1 | # This is a rust script to sanitize the table from [Kaggle](https://www.kaggle.com/datasets/mazenramadan/imdb-most-popular-films-and-series) 2 | -------------------------------------------------------------------------------- /specs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "specs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | csv = "1.2.2" 10 | -------------------------------------------------------------------------------- /addfilmwidget.h: -------------------------------------------------------------------------------- 1 | #ifndef ADDFILMWIDGET_H 2 | #define ADDFILMWIDGET_H 3 | 4 | #include 5 | #include "filmlistmodel.h" 6 | 7 | namespace Ui { 8 | class AddFilmWidget; 9 | } 10 | 11 | class AddFilmWidget : public QDialog 12 | { 13 | Q_OBJECT 14 | 15 | public: 16 | FilmListModel::Film newFilm; 17 | 18 | explicit AddFilmWidget(const FilmListModel::Film* base = nullptr, QWidget *parent = nullptr); 19 | 20 | void updateAddButtonLockState(); 21 | 22 | bool checkDataValid() const; 23 | 24 | void checkDataAndClose(); 25 | 26 | ~AddFilmWidget(); 27 | 28 | private: 29 | Ui::AddFilmWidget *ui; 30 | 31 | private slots: 32 | void on_dial_sliderMoved(int position); 33 | }; 34 | 35 | #endif // ADDFILMWIDGET_H 36 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include "mainwindow.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | int main(int argc, char *argv[]) 9 | { 10 | QApplication a(argc, argv); 11 | 12 | QTranslator translator; 13 | const QStringList uiLanguages = QLocale::system().uiLanguages(); 14 | for (const QString &locale : uiLanguages) { 15 | const QString baseName = "TestQtProject_" + QLocale(locale).name(); 16 | if (translator.load(":/i18n/" + baseName)) { 17 | a.installTranslator(&translator); 18 | break; 19 | } 20 | } 21 | 22 | MainWindow w; 23 | w.setWindowTitle(DEFAULT_WINDOW_TITLE); 24 | w.show(); 25 | return a.exec(); 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qt 6.5.1 Year one project. 2 | 3 | it was fun speedrunning this from 0 to 100 in a few days. 4 | 5 | # Building 6 | 7 | - Install qt 6.5.1 8 | - Make sure Qt Creator is available 9 | - Clone this repo 10 | - Open `/CMakeLists.txt` with Qt Creator 11 | - Build 12 | - ??? 13 | - Profit 14 | 15 | # Building the rust thing 16 | 17 |
18 | 19 | - Download the [Kaggle dataset](https://www.kaggle.com/datasets/mazenramadan/imdb-most-popular-films-and-series) 20 | - Rename the downloaded file to 'imdb.csv' 21 | - Place it in `/specs` 22 | - [Install rust](https://www.rust-lang.org/tools/install) 23 | - Run `cargo build` in `/specs` 24 | - Salvage `imdb_well_formed.csv` 25 | 26 | # Running 27 | 28 | - Build the Qt app 29 | - Run it 30 | - Navigate to `File -> New...` 31 | - Save the new file in a directory 32 | - Proceed using the app 33 | 34 | Optionally, follow the steps in [Building the rust thing](#building-the-rust-thing), then run and navigate to `File -> Open...`, open `imdb_well_formed.csv` and proceed using the app. 35 | -------------------------------------------------------------------------------- /mainwindow.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifndef MAINWINDOW_H 3 | #define MAINWINDOW_H 4 | 5 | #include 6 | #include 7 | 8 | QT_BEGIN_NAMESPACE 9 | namespace Ui { class MainWindow; } 10 | QT_END_NAMESPACE 11 | 12 | #define DEFAULT_WINDOW_TITLE "Film viewer" 13 | 14 | class MainWindow : public QMainWindow 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | MainWindow(QWidget *parent = nullptr); 20 | ~MainWindow(); 21 | 22 | public: 23 | void addNewFilmEntry(); 24 | void editFilmEntry(int row); 25 | 26 | private slots: 27 | void createAndOpenFile(); 28 | void openFile(); 29 | void loadDataFromPath(QString& path); 30 | void saveFile(); 31 | void saveFileAs(); 32 | 33 | void on_lineEdit_textChanged(const QString &arg1); 34 | 35 | void on_listView_clicked(const QModelIndex &index); 36 | 37 | void on_addFilmButton_clicked(); 38 | 39 | void on_listView_customContextMenuRequested(const QPoint &pos); 40 | 41 | private: 42 | Ui::MainWindow *ui; 43 | 44 | QFile* fileHandle = nullptr; 45 | }; 46 | 47 | #endif // MAINWINDOW_H 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used to ignore files which are generated 2 | # ---------------------------------------------------------------------------- 3 | 4 | *~ 5 | *.autosave 6 | *.a 7 | *.core 8 | *.moc 9 | *.o 10 | *.obj 11 | *.orig 12 | *.rej 13 | *.so 14 | *.so.* 15 | *_pch.h.cpp 16 | *_resource.rc 17 | *.qm 18 | .#* 19 | *.*# 20 | core 21 | !core/ 22 | tags 23 | .DS_Store 24 | .directory 25 | *.debug 26 | Makefile* 27 | *.prl 28 | *.app 29 | moc_*.cpp 30 | ui_*.h 31 | qrc_*.cpp 32 | Thumbs.db 33 | *.res 34 | *.rc 35 | /.qmake.cache 36 | /.qmake.stash 37 | 38 | # qtcreator generated files 39 | *.pro.user* 40 | CMakeLists.txt.user* 41 | 42 | # xemacs temporary files 43 | *.flc 44 | 45 | # Vim temporary files 46 | .*.swp 47 | 48 | # Visual Studio generated files 49 | *.ib_pdb_index 50 | *.idb 51 | *.ilk 52 | *.pdb 53 | *.sln 54 | *.suo 55 | *.vcproj 56 | *vcproj.*.*.user 57 | *.ncb 58 | *.sdf 59 | *.opensdf 60 | *.vcxproj 61 | *vcxproj.* 62 | 63 | # MinGW generated files 64 | *.Debug 65 | *.Release 66 | 67 | # Python byte code 68 | *.pyc 69 | 70 | # Binaries 71 | # -------- 72 | *.dll 73 | *.exe 74 | 75 | /UnlockerPortable 76 | /specs/target -------------------------------------------------------------------------------- /specs/imdb_well_formed - Copy.csv: -------------------------------------------------------------------------------- 1 | name,date,rate,votes,genre,duration,type,certificate,nudity,violence 2 | "#Saraitda",2020,6.3,32672,"Action, Drama, Horro",98,film,TV-MA,none,moderate 3 | "'Allo 'Allo!",2021,8.3,24463,"Comedy, History, Wa",45,series,unknown,no rate,no rate 4 | "10",1979,6.1,17216,"Comedy, Romanc",122,film,R,severe,none 5 | "10 Cloverfield Lane",2016,7.2,306304,"Action, Drama, Horro",103,film,PG-13,none,moderate 6 | "10 Things I Hate About You",1999,7.3,315247,"Comedy, Drama, Romanc",97,film,PG-13,mild,mild 7 | "10 Years",2011,6.1,25016,"Comedy, Drama, Romanc",110,film,PG-13,mild,none 8 | "101 Dalmatians",1996,5.7,107992,"Adventure, Comedy, Crim",103,film,G,none,mild 9 | "12 Angry Men",1957,9,732301,"Crime, Dram",96,film,unknown,none,none 10 | "12 Mighty Orphans",2021,6.8,2576,"History, Spor",118,film,PG-13,mild,mild 11 | "12 Monkeys",2000,7.7,42680,"Adventure, Drama, Myster",42,series,TV-14,mild,moderate 12 | "12 Strong",2018,6.6,74061,"Action, Drama, Histor",130,film,R,none,severe 13 | "12 Years a Slave",2013,8.1,666564,"Biography, Drama, Histor",134,film,R,severe,severe 14 | "127 Hours",2010,7.5,361435,"Biography, Dram",94,film,R,mild,severe 15 | "13 Ghosts",1960,6.1,5929,"Horror, Myster",85,film,unknown,none,mild 16 | -------------------------------------------------------------------------------- /specs/imdb_well_formed - Copy - Copy.csv: -------------------------------------------------------------------------------- 1 | name,date,rate,votes,genre,duration,type,certificate,nudity,violence 2 | "#Saraitda",2020,6.3,32672,"Action, Drama, Horror",98,film,TV-MA,none,moderate 3 | "'Allo 'Allo!",2021,8.3,24463,"Comedy, History, War",45,series,unknown,no rate,no rate 4 | "(500) Days of Summer",2009,7.7,489549,"Comedy, Drama, Romance",95,film,PG-13,mild,mild 5 | "10",1979,6.1,17216,"Comedy, Romance",122,film,R,severe,none 6 | "10 Cloverfield Lane",2016,7.2,306304,"Action, Drama, Horror",103,film,PG-13,none,moderate 7 | "10 Things I Hate About You",1999,7.3,315247,"Comedy, Drama, Romance",97,film,PG-13,mild,mild 8 | "10 Years",2011,6.1,25016,"Comedy, Drama, Romance",110,film,PG-13,mild,none 9 | "101 Dalmatians",1996,5.7,107992,"Adventure, Comedy, Crime",103,film,G,none,mild 10 | "12 Angry Men",1957,9,732301,"Crime, Drama",96,film,unknown,none,none 11 | "12 Mighty Orphans",2021,6.8,2576,"History, Sport",118,film,PG-13,mild,mild 12 | "12 Monkeys",2000,7.7,42680,"Adventure, Drama, Mystery",42,series,TV-14,mild,moderate 13 | "12 Strong",2018,6.6,74061,"Action, Drama, History",130,film,R,none,severe 14 | "12 Years a Slave",2013,8.1,666564,"Biography, Drama, History",134,film,R,severe,severe 15 | "127 Hours",2010,7.5,361435,"Biography, Drama",94,film,R,mild,severe 16 | "13 Ghosts",1960,6.1,5929,"Horror, Mystery",85,film,unknown,none,mild 17 | -------------------------------------------------------------------------------- /filmlistmodel.h: -------------------------------------------------------------------------------- 1 | #ifndef FILMLISTMODEL_H 2 | #define FILMLISTMODEL_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define CSV_COLUMN_NAMES\ 10 | "name","date", "rate",\ 11 | "votes","genre","duration",\ 12 | "type","certificate","nudity",\ 13 | "violence" 14 | 15 | class FilmListModel : public QAbstractListModel 16 | { 17 | Q_OBJECT 18 | 19 | public: 20 | struct Film { 21 | QString name; 22 | int year; 23 | float rate; 24 | int votes; 25 | std::vector genres; 26 | int duration; 27 | QString type; 28 | QString certificate; 29 | QString nudity; 30 | QString violence; 31 | }; 32 | 33 | explicit FilmListModel(QObject *parent = nullptr); 34 | 35 | void loadFilms(const std::string&); 36 | 37 | Film& getFilmData(int row); 38 | 39 | int rowCount(const QModelIndex &parent = QModelIndex()) const override; 40 | 41 | void addFilm(Film film); 42 | void removeFilm(int row); 43 | void changeFilm(int row, FilmListModel::Film newValue); 44 | 45 | bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; 46 | 47 | QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; 48 | 49 | std::vector films; 50 | private: 51 | }; 52 | 53 | #endif // FILMLISTMODEL_H 54 | -------------------------------------------------------------------------------- /specs/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "csv" 7 | version = "1.2.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "626ae34994d3d8d668f4269922248239db4ae42d538b14c398b74a52208e8086" 10 | dependencies = [ 11 | "csv-core", 12 | "itoa", 13 | "ryu", 14 | "serde", 15 | ] 16 | 17 | [[package]] 18 | name = "csv-core" 19 | version = "0.1.10" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" 22 | dependencies = [ 23 | "memchr", 24 | ] 25 | 26 | [[package]] 27 | name = "itoa" 28 | version = "1.0.6" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 31 | 32 | [[package]] 33 | name = "memchr" 34 | version = "2.5.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 37 | 38 | [[package]] 39 | name = "ryu" 40 | version = "1.0.13" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 43 | 44 | [[package]] 45 | name = "serde" 46 | version = "1.0.164" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" 49 | 50 | [[package]] 51 | name = "specs" 52 | version = "0.1.0" 53 | dependencies = [ 54 | "csv", 55 | ] 56 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | project(TestQtProject VERSION 0.1 LANGUAGES CXX) 4 | 5 | set(CMAKE_AUTOUIC ON) 6 | set(CMAKE_AUTOMOC ON) 7 | set(CMAKE_AUTORCC ON) 8 | 9 | set(CMAKE_CXX_STANDARD 17) 10 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 11 | 12 | find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets LinguistTools) 13 | find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets LinguistTools) 14 | 15 | set(TS_FILES TestQtProject_en_GB.ts) 16 | 17 | set(PROJECT_SOURCES 18 | main.cpp 19 | mainwindow.cpp 20 | mainwindow.h 21 | mainwindow.ui 22 | csv.h 23 | resources.qrc 24 | filmlistmodel.cpp filmlistmodel.h 25 | filmfilterproxymodel.h filmfilterproxymodel.cpp 26 | addfilmwidget.h addfilmwidget.cpp addfilmwidget.ui 27 | ${TS_FILES} 28 | ) 29 | 30 | if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) 31 | qt_add_executable(TestQtProject 32 | MANUAL_FINALIZATION 33 | ${PROJECT_SOURCES} 34 | ) 35 | # Define target properties for Android with Qt 6 as: 36 | # set_property(TARGET TestQtProject APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR 37 | # ${CMAKE_CURRENT_SOURCE_DIR}/android) 38 | # For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation 39 | 40 | qt_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES}) 41 | else() 42 | if(ANDROID) 43 | add_library(TestQtProject SHARED 44 | ${PROJECT_SOURCES} 45 | ) 46 | # Define properties for Android with Qt 5 after find_package() calls as: 47 | # set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") 48 | else() 49 | add_executable(TestQtProject 50 | ${PROJECT_SOURCES} 51 | ) 52 | endif() 53 | 54 | qt5_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES}) 55 | endif() 56 | 57 | target_link_libraries(TestQtProject PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) 58 | 59 | set_target_properties(TestQtProject PROPERTIES 60 | MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com 61 | MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} 62 | MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} 63 | MACOSX_BUNDLE TRUE 64 | WIN32_EXECUTABLE TRUE 65 | ) 66 | 67 | install(TARGETS TestQtProject 68 | BUNDLE DESTINATION . 69 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 70 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 71 | ) 72 | 73 | if(QT_VERSION_MAJOR EQUAL 6) 74 | qt_finalize_executable(TestQtProject) 75 | endif() 76 | -------------------------------------------------------------------------------- /report.md: -------------------------------------------------------------------------------- 1 | Film viewer project report 2 | =========================== 3 | Name: Film viewer 4 | Author: Daniil Kovalenok 5 | Group number: 224-2 6 | Submission date: June 10, 2023 7 | 8 | # Problem statement 9 | There was a need for a GUI application to manage in some way a list of films. 10 | Functionality desired: 11 | - Filtering by name 12 | - Filtering by property 13 | - Adding, Removing, Editing an entry 14 | - Saving, Loading, Creating lists 15 | # Implementation details 16 | The app is a generic Qt MVC program with bits of procedural code sprinkled in because I couldn't be bothered to create methods every time I needed something. 17 | 18 | The core of film viewer consists of two models: 19 | - `FilmListModel`, which inherits from `QAbstractListModel`; 20 | - `FilmFilterProxyModel`, which inherits from `QSortFilterProxyModel`. 21 | The former is responsible for the management of film objects in-memory, their deletion, addition and editing. 22 | The latter proxies the first model and controls which films are displayed to the user. 23 | 24 | Qt provided most of functionality needed for this task; however, property filters had to be implemented from scratch, thus justifying the usage of a superclass in this case. 25 | 26 | Addition and editing of an entry both use the same Modal widget. 27 | 28 | The rest of the functionality contains no notable details. 29 | 30 | # Results and discussion 31 | This project was my first experience with Qt. Naturally, it was iterated on quite a lot. 32 | My first approach utilized a table which would provide similar user experience to a sheet engine (Excel, Google sheets, etc.) It, however, was was quickly scrapped due to the complexity of this approach and issues with signals I had been facing as a new Qt developer. 33 | 34 | The next design ended up being the final one. Discovering `QStringList` inspired me to go with it. The challenge I needed to overcome is the sorting and filtering of data, which both worked well until property filters were introduced. Afterwards, I discovered that overriding model methods require the inheriting class to call special functions and emit special signals. This was a big roadblock, which I solved by separating filtering and the management of data into two separate models (`FilmListModel` and `FilmFilterProxyModel`). Afterwards, I faced no extra issues. 35 | # Conclusion 36 | It's neat. 37 | Here's a list of obvious extra features that would enhance user experience 38 | 39 | - Batch deletion 40 | - Merging csv files 41 | - More than one filter in query per property 42 | - Selectable text in the panel on the right 43 | - Edit, delete hotkey 44 | - Configuration file to allow hardcoded values in the add dialog to be added/removed 45 | 46 | Thank you. 47 | rev1. 48 | -------------------------------------------------------------------------------- /filmfilterproxymodel.cpp: -------------------------------------------------------------------------------- 1 | #include "filmfilterproxymodel.h" 2 | #include 3 | #include "filmlistmodel.h" 4 | 5 | FilmFilterProxyModel::FilmFilterProxyModel(QObject *parent) 6 | : QSortFilterProxyModel(parent) 7 | { 8 | this->yearFilter.ordering = NumericFilter::Ordering::Default; 9 | this->rateFilter.ordering = NumericFilter::Ordering::Default; 10 | this->voteFilter.ordering = NumericFilter::Ordering::Default; 11 | 12 | this->sort(0, Qt::AscendingOrder); 13 | } 14 | 15 | bool FilmFilterProxyModel::shouldDisplayBasedOnFilter(const FilmListModel::Film& film) const { 16 | bool shouldShow = true; 17 | shouldShow &= this->yearFilter.apply(film.year); 18 | shouldShow &= this->rateFilter.apply(film.rate); 19 | shouldShow &= this->voteFilter.apply(film.votes); 20 | return shouldShow; 21 | } 22 | 23 | void setPtrToNewFilter(FilmFilterProxyModel::NumericFilter& filter, QString order, QString value) { 24 | bool success = false; 25 | filter.ordering = FilmFilterProxyModel::NumericFilter::orderingFromString(order); 26 | filter.value_b = value.toDouble(&success); 27 | } 28 | 29 | QString FilmFilterProxyModel::updateFiltersFromQuery(const QString& in) { 30 | // reset filters 31 | setPtrToNewFilter(this->yearFilter, "default", "0.0"); 32 | setPtrToNewFilter(this->rateFilter, "default", "0.0"); 33 | setPtrToNewFilter(this->voteFilter, "default", "0.0"); 34 | static QRegularExpression regex("([a-z]+):(<|>|<=|>=|)?([0-9.]+)"); 35 | auto matchesIter = regex.globalMatch(in); 36 | while(matchesIter.hasNext()) { 37 | auto match = matchesIter.next(); 38 | auto key = match.captured(1); 39 | auto order = match.captured(2); 40 | auto value = match.captured(3); 41 | qDebug() << "Matched:" << key << order << value; 42 | if(key == "year") { 43 | setPtrToNewFilter(this->yearFilter, order, value); 44 | qDebug() << "Update year filter:" << order << value; 45 | } 46 | if(key == "rate") { 47 | setPtrToNewFilter(this->rateFilter, order, value); 48 | qDebug() << "Update rate filter:" << order << value; 49 | } 50 | if(key == "votes") { 51 | setPtrToNewFilter(this->voteFilter, order, value); 52 | qDebug() << "Update votes filter:" << order << value; 53 | } 54 | } 55 | 56 | this->invalidateFilter(); 57 | 58 | QString clone = in; 59 | clone.remove(regex); 60 | return clone; 61 | } 62 | 63 | bool FilmFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { 64 | auto source = (FilmListModel*)this->sourceModel(); 65 | return this->shouldDisplayBasedOnFilter(source->films[sourceRow]) 66 | && QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); 67 | } 68 | -------------------------------------------------------------------------------- /filmfilterproxymodel.h: -------------------------------------------------------------------------------- 1 | #ifndef FILMFILTERPROXYMODEL_H 2 | #define FILMFILTERPROXYMODEL_H 3 | 4 | #include 5 | #include 6 | #include "filmlistmodel.h" 7 | 8 | class FilmFilterProxyModel : public QSortFilterProxyModel 9 | { 10 | Q_OBJECT 11 | 12 | public: 13 | struct NumericFilter; 14 | 15 | explicit FilmFilterProxyModel(QObject *parent = nullptr); 16 | 17 | bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; 18 | 19 | bool shouldDisplayBasedOnFilter(const FilmListModel::Film& film) const; 20 | 21 | void setYearFilter(FilmFilterProxyModel::NumericFilter filter); 22 | 23 | QString updateFiltersFromQuery(const QString& in); 24 | 25 | void updateQueryCache(const QString& query); 26 | 27 | // int rowCount(const QModelIndex &parent = QModelIndex()) const override; 28 | 29 | // QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; 30 | 31 | struct NumericFilter { 32 | enum Ordering { 33 | Default, 34 | Less, 35 | LessEqual, 36 | Greater, 37 | GreaterEqual, 38 | Equal 39 | }; 40 | Ordering ordering = Ordering::Default; 41 | double value_b = 0.0; 42 | 43 | template< 44 | typename T, 45 | typename = typename std::enable_if::value, T>::type 46 | > 47 | bool apply(T a) const { 48 | T b = (T)this->value_b; 49 | if(this->ordering == NumericFilter::Default) { 50 | return true; 51 | } 52 | if(this->ordering == NumericFilter::Equal) { 53 | return a == b; 54 | } 55 | if(this->ordering == NumericFilter::Greater) { 56 | return a > b; 57 | } 58 | if(this->ordering == NumericFilter::GreaterEqual) { 59 | return a >= b; 60 | } 61 | if(this->ordering == NumericFilter::Less) { 62 | return a < b; 63 | } 64 | if(this->ordering == NumericFilter::LessEqual) { 65 | return a <= b; 66 | } 67 | return false; 68 | } 69 | 70 | Ordering static orderingFromString(QString& str) { 71 | if(str == "<") { 72 | return NumericFilter::Less; 73 | } 74 | if(str == "<=") { 75 | return NumericFilter::LessEqual; 76 | } 77 | if(str == ">") { 78 | return NumericFilter::Greater; 79 | } 80 | if(str == ">=") { 81 | return NumericFilter::GreaterEqual; 82 | } 83 | if(str == "=" || str.isEmpty()) { 84 | return NumericFilter::Equal; 85 | } 86 | return NumericFilter::Default; 87 | } 88 | }; 89 | 90 | private: 91 | NumericFilter yearFilter; 92 | NumericFilter rateFilter; 93 | NumericFilter voteFilter; 94 | 95 | }; 96 | 97 | #endif // FILMFILTERPROXYMODEL_H 98 | -------------------------------------------------------------------------------- /specs/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashSet, panic::catch_unwind, hash::Hash}; 2 | 3 | fn main() { 4 | let file = std::fs::File::open("imdb.csv").unwrap(); 5 | let mut reader = csv::Reader::from_reader(file); 6 | 7 | #[derive(Debug)] 8 | struct Film { 9 | name: String, 10 | date: u32, // year 11 | rate: f32, // rating 12 | votes: u32, // voters 13 | genre: Vec, 14 | duration: u32, // minutes 15 | _type: String, 16 | certificate: String, 17 | nudity: String, 18 | violence: String, 19 | } 20 | 21 | let mut dedup = HashSet::new(); 22 | let mut films: Vec = vec![]; 23 | 24 | let mut read = 0; 25 | let mut read_not_dupe = 0; 26 | 27 | for film_entry in reader.records().filter_map(|x| x.ok()) { 28 | let vec_ptr = &mut films as *mut Vec; 29 | let set_ptr = &mut dedup as *mut HashSet; 30 | let r1 = &mut read as *mut i32; 31 | let r2 = &mut read_not_dupe as *mut i32; 32 | catch_unwind(|| { 33 | let mut film = Film { 34 | name: film_entry[0].to_string(), 35 | date: film_entry[1].parse::().unwrap(), 36 | rate: film_entry[2].parse::().unwrap_or(0.0), 37 | votes: film_entry[3].replace(',', "").parse::().unwrap_or(0), 38 | genre: film_entry[4].split(',').map(|x| x.trim().to_string()).collect(), 39 | duration: film_entry[5].trim().parse::().unwrap_or(0), 40 | _type: film_entry[6].to_string().to_lowercase(), 41 | certificate: film_entry[7].to_string().to_uppercase(), 42 | nudity: film_entry[9].to_string().to_lowercase(), 43 | violence: film_entry[10].to_string().to_lowercase(), 44 | }; 45 | 46 | unsafe { 47 | *r1 += 1; 48 | if !set_ptr.as_mut().unwrap().insert(film.name.clone()) { 49 | return; 50 | } 51 | *r2 += 1; 52 | } 53 | 54 | film.certificate = match film.certificate.as_str() { 55 | x 56 | @ ("TV-Y" | "TV-Y7" | "G" | "TV-G" | "PG" | "TV-PG" | "PG-13" | "TV-14" | "R" | "TV-MA" | "NC-17") => x.to_string(), 57 | _ => "unknown".to_string(), 58 | }; 59 | 60 | unsafe { 61 | vec_ptr.as_mut().unwrap().push(film); 62 | } 63 | }) 64 | .map_err(|_| { 65 | println!("Error: {:?}", film_entry); 66 | panic!(); 67 | }) 68 | .unwrap(); 69 | } 70 | 71 | let mut writer = csv::Writer::from_path("imdb_well_formed.csv").unwrap(); 72 | writer 73 | .write_record([ 74 | "name", 75 | "date", 76 | "rate", 77 | "votes", 78 | "genre", 79 | "duration", 80 | "type", 81 | "certificate", 82 | "nudity", 83 | "violence", 84 | ]) 85 | .unwrap(); 86 | 87 | println!("{}", films.iter().filter(|x| x.date > 2020).count()); 88 | println!("{:?}", films.iter().filter_map(|x| Some(x.nudity.clone())).collect::>()); 89 | println!("{:?}", films.iter().filter_map(|x| Some(x.genre.clone())).flatten().collect::>()); 90 | 91 | 92 | for film in films { 93 | writer 94 | .write_record([ 95 | film.name, 96 | film.date.to_string(), 97 | film.rate.to_string(), 98 | film.votes.to_string(), 99 | film.genre.join(","), 100 | film.duration.to_string(), 101 | film._type, 102 | film.certificate, 103 | film.nudity, 104 | film.violence, 105 | ]) 106 | .unwrap(); 107 | } 108 | 109 | println!("Transformed {}, saved {}", read, read_not_dupe); 110 | } 111 | -------------------------------------------------------------------------------- /filmlistmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "filmlistmodel.h" 2 | #include "csv.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | std::vector splitString(std::string& text, char delim) { 11 | std::string line; 12 | std::vector vec; 13 | std::stringstream ss(text); 14 | while(std::getline(ss, line, delim)) { 15 | vec.push_back(QString::fromStdString(line)); 16 | } 17 | return vec; 18 | } 19 | 20 | FilmListModel::FilmListModel(QObject *parent) 21 | : QAbstractListModel(parent) 22 | { 23 | } 24 | 25 | void FilmListModel::loadFilms(const std::string& path) { 26 | this->beginResetModel(); 27 | 28 | io::CSVReader<10> csv(path); 29 | csv.read_header(io::ignore_extra_column, CSV_COLUMN_NAMES); 30 | 31 | std::string name; 32 | int year; 33 | float rate; 34 | int votes; 35 | std::string genreList; 36 | int duration; 37 | std::string type; 38 | std::string certificate; 39 | std::string nudity; 40 | std::string violence; 41 | try { 42 | while(csv.read_row(name, year, rate, votes, genreList, duration, type, certificate, nudity, violence)) { 43 | QString nameQstr = QString::fromStdString(name); 44 | std::vector genres = splitString(genreList, ','); 45 | QString typeQstr = QString::fromStdString(type); 46 | QString certificateQstr = QString::fromStdString(certificate); 47 | QString nudityQstr = QString::fromStdString(nudity); 48 | QString violenceQstr = QString::fromStdString(violence); 49 | this->films.push_back(Film { 50 | nameQstr, 51 | year, 52 | rate, 53 | votes, 54 | genres, 55 | duration, 56 | typeQstr, 57 | certificateQstr, 58 | nudityQstr, 59 | violenceQstr, 60 | }); 61 | } 62 | } catch(std::exception& ex) { 63 | qDebug() << ex.what(); 64 | throw ex; 65 | } 66 | 67 | std::sort(films.begin(), films.end(), [](Film& a, Film& b) { 68 | return a.name < b.name; 69 | }); 70 | 71 | qDebug() << "Loaded .csv:" << films.size() << "films."; 72 | 73 | this->endResetModel(); 74 | } 75 | 76 | FilmListModel::Film& FilmListModel::getFilmData(int row) { 77 | return this->films[row]; 78 | } 79 | 80 | int FilmListModel::rowCount(const QModelIndex &parent) const 81 | { 82 | return films.size(); 83 | } 84 | 85 | QVariant FilmListModel::data(const QModelIndex &index, int role) const 86 | { 87 | if(index.isValid() && role == Qt::DisplayRole) 88 | { 89 | // qDebug() << "Data" << index.row() << this->films[index.row()].name << role; 90 | return this->films[index.row()].name; 91 | } 92 | 93 | return QVariant(); 94 | } 95 | 96 | void FilmListModel::addFilm(FilmListModel::Film film) { 97 | int c = this->rowCount(); 98 | beginInsertRows(QModelIndex(), c, c); 99 | this->films.push_back(film); 100 | endInsertRows(); 101 | } 102 | 103 | void FilmListModel::removeFilm(int row) { 104 | beginRemoveRows(QModelIndex(), row, row); 105 | this->films.erase(this->films.begin() + row); 106 | endRemoveRows(); 107 | } 108 | 109 | void FilmListModel::changeFilm(int row, FilmListModel::Film newValue) { 110 | this->films[row] = newValue; 111 | 112 | emit dataChanged(index(row), index(row)); 113 | } 114 | 115 | bool FilmListModel::insertRows(int row, int count, const QModelIndex &parent) { 116 | // beginInsertRows(parent, row, row + count); 117 | // endInsertRows(); 118 | return true; 119 | } 120 | -------------------------------------------------------------------------------- /mainwindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 583 10 | 330 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 21 | 0 22 | 23 | 24 | 25 | 26 | 0 27 | 28 | 29 | 30 | 31 | false 32 | 33 | 34 | Filter... 35 | 36 | 37 | 38 | 39 | 40 | 41 | false 42 | 43 | 44 | 45 | 10 46 | 10 47 | 48 | 49 | 50 | + 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | false 60 | 61 | 62 | Qt::CustomContextMenu 63 | 64 | 65 | QListView::Batched 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | false 75 | 76 | 77 | QFrame::Sunken 78 | 79 | 80 | Qt::ScrollBarAsNeeded 81 | 82 | 83 | QAbstractItemView::NoEditTriggers 84 | 85 | 86 | false 87 | 88 | 89 | false 90 | 91 | 92 | false 93 | 94 | 95 | QAbstractItemView::NoSelection 96 | 97 | 98 | false 99 | 100 | 101 | Qt::NoPen 102 | 103 | 104 | false 105 | 106 | 107 | 10 108 | 109 | 110 | false 111 | 112 | 113 | true 114 | 115 | 116 | false 117 | 118 | 119 | 20 120 | 121 | 122 | false 123 | 124 | 125 | true 126 | 127 | 128 | 129 | Name 130 | 131 | 132 | 133 | 134 | Date 135 | 136 | 137 | 138 | 139 | Rate 140 | 141 | 142 | 143 | 144 | Votes 145 | 146 | 147 | 148 | 149 | Genre 150 | 151 | 152 | 153 | 154 | Duration 155 | 156 | 157 | 158 | 159 | Type 160 | 161 | 162 | 163 | 164 | Certificate 165 | 166 | 167 | 168 | 169 | Nudity 170 | 171 | 172 | 173 | 174 | Violence 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | Value 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 0 195 | 0 196 | 583 197 | 21 198 | 199 | 200 | 201 | QMenu::item:disabled { 202 | color: grey; 203 | } 204 | 205 | 206 | 207 | File 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | Help 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | Certificates 233 | 234 | 235 | 236 | 237 | About 238 | 239 | 240 | 241 | 242 | true 243 | 244 | 245 | Open... 246 | 247 | 248 | Ctrl+O 249 | 250 | 251 | 252 | 253 | false 254 | 255 | 256 | Save 257 | 258 | 259 | Ctrl+S 260 | 261 | 262 | 263 | 264 | false 265 | 266 | 267 | Save As... 268 | 269 | 270 | Ctrl+Shift+S 271 | 272 | 273 | 274 | 275 | Exit 276 | 277 | 278 | 279 | 280 | New... 281 | 282 | 283 | 284 | 285 | Filters 286 | 287 | 288 | 289 | 290 | 291 | 292 | clickPushButton() 293 | 294 | 295 | -------------------------------------------------------------------------------- /addfilmwidget.cpp: -------------------------------------------------------------------------------- 1 | #include "addfilmwidget.h" 2 | #include "ui_addfilmwidget.h" 3 | #include "filmlistmodel.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | void addItemToListWidget(QListWidget* w, QString text) { 10 | QListWidgetItem* item = new QListWidgetItem(w); 11 | item->setCheckState(Qt::Unchecked); 12 | item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); 13 | item->setText(QCoreApplication::translate("AddFilmWidget", text.toStdString().c_str(), nullptr)); 14 | 15 | // __qlistwidgetitem->setCheckState(Qt::Unchecked); 16 | // __qlistwidgetitem->setFlags(Qt::ItemIsDragEnabled|Qt::ItemIsUserCheckable|Qt::ItemIsEnabled); 17 | // QListWidgetItem *__qlistwidgetitem1 = new QListWidgetItem(genreListWidget); 18 | // __qlistwidgetitem1->setCheckState(Qt::Unchecked); 19 | // QListWidgetItem *__qlistwidgetitem2 = new QListWidgetItem(genreListWidget); 20 | // __qlistwidgetitem2->setCheckState(Qt::Unchecked); 21 | // genreListWidget->setObjectName("genreListWidget"); 22 | // QSizePolicy sizePolicy2(QSizePolicy::Minimum, QSizePolicy::Expanding); 23 | // sizePolicy2.setHorizontalStretch(0); 24 | // sizePolicy2.setVerticalStretch(0); 25 | // sizePolicy2.setHeightForWidth(genreListWidget->sizePolicy().hasHeightForWidth()); 26 | // genreListWidget->setSizePolicy(sizePolicy2); 27 | // genreListWidget->setMaximumSize(QSize(1212412, 16777215)); 28 | // genreListWidget->setLayoutMode(QListView::SinglePass); 29 | // genreListWidget->setViewMode(QListView::ListMode); 30 | // genreListWidget->setSelectionRectVisible(true); 31 | 32 | } 33 | 34 | AddFilmWidget::AddFilmWidget(const FilmListModel::Film* base, QWidget *parent) : 35 | QDialog(parent), 36 | ui(new Ui::AddFilmWidget) 37 | { 38 | ui->setupUi(this); 39 | 40 | connect(ui->addButton, &QPushButton::clicked, this, &AddFilmWidget::checkDataAndClose); 41 | connect(ui->cancelButton, &QPushButton::clicked, this, &AddFilmWidget::reject); 42 | 43 | ui->genreListWidget->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); 44 | 45 | addItemToListWidget(ui->genreListWidget, "Talk-Show"); 46 | addItemToListWidget(ui->genreListWidget, "Thriller"); 47 | addItemToListWidget(ui->genreListWidget, "News"); 48 | addItemToListWidget(ui->genreListWidget, "Sport"); 49 | addItemToListWidget(ui->genreListWidget, "Short"); 50 | addItemToListWidget(ui->genreListWidget, "Comedy"); 51 | addItemToListWidget(ui->genreListWidget, "Fantasy"); 52 | addItemToListWidget(ui->genreListWidget, "Crime"); 53 | addItemToListWidget(ui->genreListWidget, "Animation"); 54 | addItemToListWidget(ui->genreListWidget, "Sci-Fi"); 55 | addItemToListWidget(ui->genreListWidget, "Reality-TV"); 56 | addItemToListWidget(ui->genreListWidget, "Biography"); 57 | addItemToListWidget(ui->genreListWidget, "Adventure"); 58 | addItemToListWidget(ui->genreListWidget, "Drama"); 59 | addItemToListWidget(ui->genreListWidget, "Romance"); 60 | addItemToListWidget(ui->genreListWidget, "Mystery"); 61 | addItemToListWidget(ui->genreListWidget, "Musical"); 62 | addItemToListWidget(ui->genreListWidget, "Action"); 63 | addItemToListWidget(ui->genreListWidget, "Family"); 64 | addItemToListWidget(ui->genreListWidget, "Western"); 65 | addItemToListWidget(ui->genreListWidget, "Music"); 66 | addItemToListWidget(ui->genreListWidget, "Documentary"); 67 | addItemToListWidget(ui->genreListWidget, "Game-Show"); 68 | addItemToListWidget(ui->genreListWidget, "Horror"); 69 | addItemToListWidget(ui->genreListWidget, "War"); 70 | addItemToListWidget(ui->genreListWidget, "Film-Noir"); 71 | addItemToListWidget(ui->genreListWidget, "History"); 72 | 73 | ui->yearEdit->setValidator(new QRegularExpressionValidator(QRegularExpression("[0-9]+"))); 74 | auto dv = new QDoubleValidator(0, 10, 1); 75 | dv->setNotation(QDoubleValidator::StandardNotation); 76 | ui->rateEdit->setValidator(dv); 77 | ui->votesEdit->setValidator(new QRegularExpressionValidator(QRegularExpression("[0-9]+"))); 78 | ui->durationEdit->setValidator(new QRegularExpressionValidator(QRegularExpression("[0-9]+"))); 79 | 80 | connect(ui->nameEdit, &QLineEdit::textEdited, this, &AddFilmWidget::updateAddButtonLockState); 81 | connect(ui->yearEdit, &QLineEdit::textEdited, this, &AddFilmWidget::updateAddButtonLockState); 82 | connect(ui->rateEdit, &QLineEdit::textEdited, this, &AddFilmWidget::updateAddButtonLockState); 83 | connect(ui->votesEdit, &QLineEdit::textEdited, this, &AddFilmWidget::updateAddButtonLockState); 84 | connect(ui->genreListWidget, &QListWidget::itemClicked, this, &AddFilmWidget::updateAddButtonLockState); 85 | connect(ui->durationEdit, &QLineEdit::textEdited, this, &AddFilmWidget::updateAddButtonLockState); 86 | connect(ui->typeSelect, &QComboBox::currentTextChanged, this, &AddFilmWidget::updateAddButtonLockState); 87 | connect(ui->certificateSelect, &QComboBox::currentTextChanged, this, &AddFilmWidget::updateAddButtonLockState); 88 | connect(ui->nuditySelect, &QComboBox::currentTextChanged, this, &AddFilmWidget::updateAddButtonLockState); 89 | connect(ui->violenceSelect, &QComboBox::currentTextChanged, this, &AddFilmWidget::updateAddButtonLockState); 90 | 91 | if(base) { 92 | this->newFilm = *base; 93 | this->updateAddButtonLockState(); 94 | 95 | qDebug() << "Use custom" << base; 96 | ui->nameEdit->setText(base->name); 97 | qDebug() << "1"; 98 | ui->yearEdit->setText(QString::number(base->year)); 99 | qDebug() << "1"; 100 | ui->rateEdit->setText(QString::number(base->rate)); 101 | qDebug() << "1"; 102 | ui->votesEdit->setText(QString::number(base->votes)); 103 | qDebug() << "1"; 104 | auto gl = ui->genreListWidget; 105 | for(int i = 0; i < gl->count(); i++) { 106 | auto item = gl->item(i); 107 | for(const QString& genreInBase : base->genres) { 108 | if(genreInBase == item->text()) { 109 | item->setCheckState(Qt::Checked); 110 | } 111 | } 112 | } 113 | qDebug() << "1"; 114 | ui->durationEdit->setText(QString::number(base->duration)); 115 | qDebug() << "1"; 116 | ui->typeSelect->setCurrentText(base->type); 117 | qDebug() << "1"; 118 | ui->certificateSelect->setCurrentText(base->certificate); 119 | qDebug() << "1"; 120 | ui->nuditySelect->setCurrentText(base->nudity); 121 | qDebug() << "1"; 122 | ui->violenceSelect->setCurrentText(base->nudity); 123 | qDebug() << "1"; 124 | qDebug() << "End custom" << base; 125 | } 126 | 127 | } 128 | 129 | void AddFilmWidget::updateAddButtonLockState() { 130 | this->ui->addButton->setEnabled(this->checkDataValid()); 131 | } 132 | 133 | void showBadFormatInfo() { 134 | QMessageBox msg; 135 | msg.setWindowTitle("Bad format"); 136 | msg.setIcon(QMessageBox::Icon::Critical); 137 | msg.setText("The following fields are incorrect:\ntodo"); 138 | msg.setStandardButtons(QMessageBox::Close); 139 | msg.setDefaultButton(QMessageBox::Close); 140 | msg.exec(); 141 | } 142 | 143 | bool AddFilmWidget::checkDataValid() const { 144 | bool b1 = false; 145 | bool b2 = false; 146 | bool b3 = false; 147 | bool b4 = false; 148 | auto name = ui->nameEdit->text(); 149 | auto year = ui->yearEdit->text().toInt(&b1); 150 | auto rate = ui->rateEdit->text().toDouble(&b2); 151 | b2 &= rate <= 10.0; 152 | auto votes = ui->votesEdit->text().toInt(&b3); 153 | auto genreList = ui->genreListWidget; 154 | auto duration = ui->durationEdit->text().toInt(&b4); 155 | auto type = ui->typeSelect->currentText(); 156 | auto certificate = ui->certificateSelect->currentText(); 157 | auto nudity = ui->nuditySelect->currentText(); 158 | auto violence = ui->violenceSelect->currentText(); 159 | 160 | int selectedGenres = 0; 161 | for(int i = 0; i < genreList->count(); i++) { 162 | auto item = genreList->item(i); 163 | if(item->checkState() == Qt::Checked) { 164 | selectedGenres += 1; 165 | } 166 | } 167 | 168 | return selectedGenres > 0 && b1 && b2 && b3 && b4 169 | && !name.isEmpty() && !type.isEmpty() 170 | && !certificate.isEmpty() && !nudity.isEmpty() 171 | && !violence.isEmpty(); 172 | } 173 | 174 | void AddFilmWidget::checkDataAndClose() { 175 | FilmListModel::Film film; 176 | 177 | auto name = ui->nameEdit->text(); 178 | auto year = ui->yearEdit->text().toInt(); 179 | auto rate = ui->rateEdit->text().toDouble(); 180 | auto votes = ui->votesEdit->text().toInt(); 181 | auto genreList = ui->genreListWidget; 182 | auto duration = ui->durationEdit->text().toInt(); 183 | auto type = ui->typeSelect->currentText(); 184 | auto certificate = ui->certificateSelect->currentText(); 185 | auto nudity = ui->nuditySelect->currentText(); 186 | auto violence = ui->violenceSelect->currentText(); 187 | 188 | for(int i = 0; i < genreList->count(); i++) { 189 | auto item = genreList->item(i); 190 | if(item->checkState() == Qt::Checked) { 191 | film.genres.push_back(item->text()); 192 | } 193 | } 194 | 195 | film.name = name; 196 | film.year = year; 197 | film.rate = rate; 198 | film.votes = votes; 199 | film.duration = duration; 200 | film.type = type; 201 | film.certificate = certificate; 202 | film.nudity = nudity; 203 | film.violence = violence; 204 | 205 | this->newFilm = film; 206 | 207 | this->accept(); 208 | } 209 | 210 | AddFilmWidget::~AddFilmWidget() 211 | { 212 | delete ui; 213 | } 214 | 215 | void AddFilmWidget::on_dial_sliderMoved(int position) 216 | { 217 | auto genreList = ui->genreListWidget; 218 | 219 | auto& rng = *QRandomGenerator::global(); 220 | ui->nameEdit->setText(QString::number(rng.generate64(), 16)); 221 | ui->yearEdit->setText(QString::number(rng.bounded(1900, 2023))); 222 | ui->rateEdit->setText(QString::number( std::floor(rng.bounded((double)10.0) * 10.0) / 10.0 )); 223 | ui->votesEdit->setText(QString::number(rng.bounded(100, 9000000))); 224 | 225 | int checked = 0; 226 | for(int i = 0; i < genreList->count(); i++) { 227 | auto item = genreList->item(i); 228 | if(rng.generateDouble() < 0.16) { 229 | checked += 1; 230 | item->setCheckState(Qt::Checked); 231 | } 232 | } 233 | if(checked == 0) { 234 | genreList->item(0)->setCheckState(Qt::Checked); 235 | } 236 | 237 | ui->durationEdit->setText(QString::number(rng.bounded(30, 210))); 238 | ui->typeSelect->setCurrentText(rng.generateDouble() < 0.5 239 | ? ui->typeSelect->itemText(0) 240 | : ui->typeSelect->itemText(1)); 241 | 242 | ui->certificateSelect->setCurrentText( 243 | ui->certificateSelect->itemText(rng.bounded(0, ui->certificateSelect->count())) 244 | ); 245 | ui->nuditySelect->setCurrentText( 246 | ui->nuditySelect->itemText(rng.bounded(0, ui->nuditySelect->count())) 247 | ); 248 | ui->violenceSelect->setCurrentText( 249 | ui->violenceSelect->itemText(rng.bounded(0, ui->violenceSelect->count())) 250 | ); 251 | 252 | } 253 | -------------------------------------------------------------------------------- /mainwindow.cpp: -------------------------------------------------------------------------------- 1 | #include "mainwindow.h" 2 | #include "./ui_mainwindow.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include "csv.h" 22 | #include "filmlistmodel.h" 23 | #include "filmfilterproxymodel.h" 24 | #include "addfilmwidget.h" 25 | 26 | void saveFilmsToFile(QFile* file, const std::vector& films); 27 | 28 | QString joinVector(const std::vector& vec, bool spaceAfterComma) { 29 | QString result; 30 | for (auto& str : vec) { 31 | result += str + (spaceAfterComma ? ", " : ","); 32 | } 33 | result.chop(spaceAfterComma ? 2 : 1); 34 | return result; 35 | } 36 | 37 | void showAboutDialog() { 38 | QMessageBox msg; 39 | msg.setWindowTitle("About"); 40 | msg.setIconPixmap(QPixmap(":/resources/crewmate.png")); 41 | msg.setText("Qt project application. No rights reserved. I hate it.\n\nUpd: I hate it less."); 42 | msg.setStandardButtons(QMessageBox::Close); 43 | msg.setDefaultButton(QMessageBox::Close); 44 | msg.exec(); 45 | } 46 | 47 | void showFiltersDialog() { 48 | QMessageBox msg; 49 | msg.setIcon(QMessageBox::Icon::Information); 50 | msg.setWindowTitle("Filters"); 51 | msg.setText("Filter: " 52 | ":[<|<=|>|>=|=]\n\n" 53 | "Example: year:2000 - show all films released in the year 2000.\n" 54 | "Note: Currently, only one filter per property is evaluated in a given query."); 55 | msg.setStandardButtons(QMessageBox::Close); 56 | msg.setDefaultButton(QMessageBox::Close); 57 | msg.exec(); 58 | } 59 | 60 | void showCertificateLegendDialog() { 61 | QMessageBox msg; 62 | msg.setWindowTitle("Certificate legend"); 63 | msg.setIcon(QMessageBox::Icon::Information); 64 | msg.setText("TV-Y: Designed to be appropriate for all children\n" 65 | "TV-Y7: Suitable for ages 7 and up\n" 66 | "G: Suitable for General Audiences\n" 67 | "TV-G: Suitable for General Audiences\n" 68 | "PG: Parental Guidance suggested\n" 69 | "TV-PG: Parental Guidance suggested\n" 70 | "PG-13: Parents strongly cautioned. May be Inappropriate for ages 12 and under.\n" 71 | "TV-14: Parents strongly cautioned. May not be suitable for ages 14 and under.\n" 72 | "R: Restricted. May be inappropriate for ages 17 and under.\n" 73 | "TV-MA: For Mature Audiences. May not be suitable for ages 17 and under.\n" 74 | "NC-17: Inappropriate for ages 17 and under\n"); 75 | msg.setStandardButtons(QMessageBox::Close); 76 | msg.setDefaultButton(QMessageBox::Close); 77 | msg.exec(); 78 | } 79 | 80 | void showErrorDialog(QString& msg) { 81 | QMessageBox dialog; 82 | dialog.setWindowTitle("Error"); 83 | dialog.setIcon(QMessageBox::Icon::Critical); 84 | dialog.setText("Error: " + msg); 85 | dialog.setStandardButtons(QMessageBox::Ok); 86 | dialog.setDefaultButton(QMessageBox::Ok); 87 | dialog.exec(); 88 | } 89 | 90 | void MainWindow::createAndOpenFile() { 91 | QString fileName = QFileDialog::getSaveFileName(this, "Create new file", "/", "*.csv"); 92 | if(!fileName.isEmpty()) { 93 | QFile newFile(fileName); 94 | try { 95 | newFile.open(QIODevice::WriteOnly | QIODevice::Text); 96 | 97 | std::vector emptyVector; 98 | saveFilmsToFile(&newFile, emptyVector); 99 | 100 | loadDataFromPath(fileName); 101 | } catch(std::exception ex) { 102 | auto msg = QString("Error writing to new file: ") + ex.what(); 103 | showErrorDialog(msg); 104 | } 105 | } 106 | } 107 | 108 | void MainWindow::loadDataFromPath(QString& fileName) { 109 | auto filmListModel = new FilmListModel; 110 | try { 111 | filmListModel->loadFilms(fileName.toStdString()); 112 | } catch(std::exception& ex) { 113 | auto msg = QString("Failed to read csv data: %0").arg(ex.what()); 114 | showErrorDialog(msg); 115 | return; 116 | } 117 | 118 | auto filterModel = new FilmFilterProxyModel; 119 | filterModel->setSourceModel(filmListModel); 120 | filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); 121 | 122 | auto oldModel = ui->listView->model(); 123 | delete oldModel; 124 | ui->listView->setModel(filterModel); 125 | ui->lineEdit->clear(); 126 | 127 | delete this->fileHandle; 128 | 129 | QFile* file = new QFile(fileName); 130 | file->open(QIODevice::ReadWrite | QIODevice::Text); 131 | this->fileHandle = file; 132 | 133 | ui->actionSave->setEnabled(true); 134 | ui->actionSave_as->setEnabled(true); 135 | ui->listView->setEnabled(true); 136 | ui->lineEdit->setEnabled(true); 137 | ui->addFilmButton->setEnabled(true); 138 | ui->filmInfoWidget->setEnabled(true); 139 | ui->filmInfoWidget->verticalHeader()->setVisible(true); 140 | 141 | QString newTitle = QString("%0 (%1)").arg(DEFAULT_WINDOW_TITLE).arg(fileName); 142 | this->setWindowTitle(newTitle); 143 | } 144 | 145 | void MainWindow::openFile() { 146 | try { 147 | QString fileName = QFileDialog::getOpenFileName(this, "Open file", "/", "*.csv"); 148 | qDebug() << fileName; 149 | 150 | if(fileName.isEmpty()) { 151 | return; 152 | } 153 | 154 | this->loadDataFromPath(fileName); 155 | 156 | } catch(std::exception& ex) { 157 | auto msg = QString::fromStdString(ex.what()); 158 | showErrorDialog(msg); 159 | } 160 | } 161 | 162 | void saveFilmsToFile(QFile* file, const std::vector& films) { 163 | if(file) { 164 | const char* columns[] = { CSV_COLUMN_NAMES }; 165 | int rowLength = sizeof(columns)/sizeof(*columns); 166 | 167 | file->seek(0); 168 | file->resize(0); 169 | QTextStream output(file); 170 | 171 | for(int i = 0; i < rowLength ; i++) { 172 | output << columns[i]; 173 | if(i + 1 != rowLength) { 174 | output << ","; 175 | } 176 | } 177 | output << "\n"; 178 | 179 | for(auto& film : films) { 180 | output << '"' << film.name << '"' << ',' 181 | << film.year << ',' 182 | << film.rate << ',' 183 | << film.votes << ',' 184 | << '"' << joinVector(film.genres, false) << '"' << ',' 185 | << film.duration << ',' 186 | << film.type << ',' 187 | << film.certificate << ',' 188 | << film.nudity << ',' 189 | << film.violence 190 | << "\n"; 191 | } 192 | 193 | output.flush(); 194 | 195 | qDebug() << "Saved file"; 196 | } else { 197 | qDebug("File pointer is null"); 198 | } 199 | } 200 | 201 | void MainWindow::saveFile() { 202 | qDebug() << "save"; 203 | auto proxyModel = (FilmFilterProxyModel*)this->ui->listView->model(); 204 | auto filmListModel = (FilmListModel*)proxyModel->sourceModel(); 205 | 206 | QFile* file = this->fileHandle; 207 | if(file) { 208 | saveFilmsToFile(file, filmListModel->films); 209 | } 210 | 211 | QString title = this->windowTitle(); 212 | if(title.startsWith('*')) { 213 | this->setWindowTitle(title.mid(1)); // remove asterisk 214 | } 215 | } 216 | 217 | void MainWindow::saveFileAs() { 218 | qDebug() << "save as"; 219 | auto proxyModel = (FilmFilterProxyModel*)this->ui->listView->model(); 220 | auto filmListModel = (FilmListModel*)proxyModel->sourceModel(); 221 | 222 | QString fileName = QFileDialog::getSaveFileName(this, "Save file", "/", "*.csv"); 223 | if(!fileName.isEmpty()) { 224 | QFile newFile(fileName); 225 | try { 226 | newFile.open(QIODevice::WriteOnly | QIODevice::Text); 227 | saveFilmsToFile(&newFile, filmListModel->films); 228 | } catch(std::exception ex) { 229 | auto msg = QString("Error opening file: ") + ex.what(); 230 | showErrorDialog(msg); 231 | } 232 | } 233 | 234 | QString title = this->windowTitle(); 235 | if(title.startsWith('*')) { 236 | this->setWindowTitle(title.mid(1)); // remove asterisk 237 | } 238 | } 239 | 240 | void MainWindow::addNewFilmEntry() { 241 | AddFilmWidget afw; 242 | int result = afw.exec(); 243 | qDebug() << "Add result:" << result << afw.newFilm.name; 244 | 245 | if(result == AddFilmWidget::Accepted) { 246 | auto proxyModel = (FilmFilterProxyModel*)this->ui->listView->model(); 247 | auto filmListModel = (FilmListModel*)proxyModel->sourceModel(); 248 | 249 | qDebug() << &afw.newFilm; 250 | filmListModel->addFilm(afw.newFilm); 251 | } 252 | 253 | QString title = this->windowTitle(); 254 | if(!title.startsWith('*')) { 255 | this->setWindowTitle(QString("*") + title); // add asterisk 256 | } 257 | } 258 | 259 | void MainWindow::editFilmEntry(int row) { 260 | auto proxyModel = (FilmFilterProxyModel*)this->ui->listView->model(); 261 | auto filmListModel = (FilmListModel*)proxyModel->sourceModel(); 262 | 263 | FilmListModel::Film film = filmListModel->getFilmData(row); 264 | AddFilmWidget afw(&film); 265 | afw.newFilm = film; 266 | 267 | int result = afw.exec(); 268 | if(result == AddFilmWidget::Accepted) { 269 | filmListModel->changeFilm(row, afw.newFilm); 270 | qDebug() << "Edited film" << row; 271 | } 272 | 273 | QString title = this->windowTitle(); 274 | if(!title.startsWith('*')) { 275 | this->setWindowTitle(QString("*") + title); // add asterisk 276 | } 277 | } 278 | 279 | MainWindow::MainWindow(QWidget *parent) 280 | : QMainWindow(parent) 281 | , ui(new Ui::MainWindow) 282 | { 283 | ui->setupUi(this); 284 | 285 | // Set up menu bar // 286 | 287 | connect(ui->actionNew, &QAction::triggered, this, &MainWindow::createAndOpenFile); 288 | connect(ui->actionOpen, &QAction::triggered, this, &MainWindow::openFile); 289 | connect(ui->actionSave, &QAction::triggered, this, &MainWindow::saveFile); 290 | connect(ui->actionSave_as, &QAction::triggered, this, &MainWindow::saveFileAs); 291 | connect(ui->actionExit, &QAction::triggered, this, [](){ std::exit(0); }); 292 | 293 | connect(ui->actionCertificates, &QAction::triggered, this, &showCertificateLegendDialog); 294 | connect(ui->actionFilters, &QAction::triggered, this, &showFiltersDialog); 295 | connect(ui->actionAbout, &QAction::triggered, this, &showAboutDialog); 296 | 297 | ui->actionSave->setEnabled(false); 298 | ui->actionSave_as->setEnabled(false); 299 | 300 | // Set up film list // 301 | 302 | ui->listView->setEditTriggers(QAbstractItemView::NoEditTriggers); 303 | 304 | // Set up info panel // 305 | 306 | ui->filmInfoWidget->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); 307 | } 308 | 309 | 310 | MainWindow::~MainWindow() 311 | { 312 | delete ui; 313 | } 314 | 315 | void updateFilmInfoWidget(QTableWidget* widget, FilmListModel::Film& film) { 316 | auto model = widget->model(); 317 | 318 | model->setData(model->index(0, 0), film.name, Qt::ToolTipRole); 319 | model->setData(model->index(0, 0), film.name, Qt::DisplayRole); 320 | model->setData(model->index(1, 0), film.year, Qt::ToolTipRole); 321 | model->setData(model->index(1, 0), film.year, Qt::DisplayRole); 322 | model->setData(model->index(2, 0), film.rate, Qt::ToolTipRole); 323 | model->setData(model->index(2, 0), film.rate, Qt::DisplayRole); 324 | model->setData(model->index(3, 0), film.votes, Qt::ToolTipRole); 325 | model->setData(model->index(3, 0), film.votes, Qt::DisplayRole); 326 | model->setData(model->index(4, 0), joinVector(film.genres, true), Qt::ToolTipRole); 327 | model->setData(model->index(4, 0), joinVector(film.genres, true), Qt::DisplayRole); 328 | model->setData(model->index(5, 0), QString("%0 minutes").arg(film.duration), Qt::ToolTipRole); 329 | model->setData(model->index(5, 0), QString("%0 minutes").arg(film.duration), Qt::DisplayRole); 330 | model->setData(model->index(6, 0), film.type, Qt::ToolTipRole); 331 | model->setData(model->index(6, 0), film.type, Qt::DisplayRole); 332 | model->setData(model->index(7, 0), film.certificate, Qt::ToolTipRole); 333 | model->setData(model->index(7, 0), film.certificate, Qt::DisplayRole); 334 | model->setData(model->index(8, 0), film.nudity, Qt::ToolTipRole); 335 | model->setData(model->index(8, 0), film.nudity, Qt::DisplayRole); 336 | model->setData(model->index(9, 0), film.violence, Qt::ToolTipRole); 337 | model->setData(model->index(9, 0), film.violence, Qt::DisplayRole); 338 | } 339 | 340 | void MainWindow::on_lineEdit_textChanged(const QString &text) 341 | { 342 | auto proxyModel = (FilmFilterProxyModel*)this->ui->listView->model(); 343 | if(!proxyModel) { 344 | return; 345 | } 346 | // auto filmListModel = (FilmListModel*)proxyModel->sourceModel(); 347 | 348 | QString queryNoFilters = proxyModel->updateFiltersFromQuery(text); 349 | 350 | qDebug() << "No filters:" << queryNoFilters; 351 | 352 | proxyModel->setFilterFixedString(queryNoFilters.trimmed()); 353 | } 354 | 355 | void MainWindow::on_listView_clicked(const QModelIndex &index) 356 | { 357 | auto proxyModel = (FilmFilterProxyModel*)this->ui->listView->model(); 358 | auto filmListModel = (FilmListModel*)proxyModel->sourceModel(); 359 | 360 | auto mappedIndex = proxyModel->mapToSource(index); 361 | 362 | if(!mappedIndex.isValid()) { 363 | return; 364 | } 365 | 366 | auto film = filmListModel->getFilmData(mappedIndex.row()); 367 | 368 | updateFilmInfoWidget(ui->filmInfoWidget, film); 369 | } 370 | 371 | void MainWindow::on_addFilmButton_clicked() 372 | { 373 | this->addNewFilmEntry(); 374 | } 375 | 376 | 377 | void MainWindow::on_listView_customContextMenuRequested(const QPoint &pos) 378 | { 379 | auto proxyModel = (FilmFilterProxyModel*)this->ui->listView->model(); 380 | auto filmListModel = (FilmListModel*)proxyModel->sourceModel(); 381 | 382 | QPoint globalPos = this->ui->listView->mapToGlobal(pos); 383 | auto idx = proxyModel->mapToSource(this->ui->listView->indexAt(pos)); 384 | 385 | QMenu cm(this->ui->listView); 386 | QAction* editAction = cm.addAction("Edit"); 387 | QAction* deleteAction = cm.addAction("Delete"); 388 | 389 | QAction* selected = cm.exec(globalPos); 390 | 391 | if(selected == editAction) { 392 | this->editFilmEntry(idx.row()); 393 | } 394 | if(selected == deleteAction) { 395 | qDebug() << idx.row(); 396 | filmListModel->removeFilm(idx.row()); 397 | 398 | QString title = this->windowTitle(); 399 | if(!title.startsWith('*')) { 400 | this->setWindowTitle(QString("*") + title); // add asterisk 401 | } 402 | } 403 | } 404 | 405 | -------------------------------------------------------------------------------- /addfilmwidget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | AddFilmWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 315 10 | 485 11 | 12 | 13 | 14 | Add film 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 0 23 | 24 | 25 | 26 | 27 | 28 | 80 29 | 16777215 30 | 31 | 32 | 33 | Name 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 0 42 | 0 43 | 44 | 45 | 46 | 47 | 48 | 49 | Avatar 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 0 59 | 60 | 61 | 62 | 63 | 64 | 80 65 | 16777215 66 | 67 | 68 | 69 | Year 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 0 78 | 0 79 | 80 | 81 | 82 | 2009 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 0 92 | 93 | 94 | 95 | 96 | 97 | 80 98 | 16777215 99 | 100 | 101 | 102 | Rate 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 0 111 | 0 112 | 113 | 114 | 115 | 7.8 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 0 125 | 126 | 127 | 128 | 129 | 130 | 80 131 | 16777215 132 | 133 | 134 | 135 | Votes 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 0 144 | 0 145 | 146 | 147 | 148 | 1000000 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | QLayout::SetDefaultConstraint 158 | 159 | 160 | 161 | 162 | 163 | 0 164 | 0 165 | 166 | 167 | 168 | 169 | 80 170 | 16777215 171 | 172 | 173 | 174 | Genre 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 0 183 | 0 184 | 185 | 186 | 187 | 188 | 1212412 189 | 16777215 190 | 191 | 192 | 193 | QListView::SinglePass 194 | 195 | 196 | QListView::ListMode 197 | 198 | 199 | true 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 11 209 | 210 | 211 | 212 | 213 | 214 | 80 215 | 16777215 216 | 217 | 218 | 219 | Duration 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 0 228 | 0 229 | 230 | 231 | 232 | 160 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 0 242 | 243 | 244 | 245 | 246 | 247 | 80 248 | 16777215 249 | 250 | 251 | 252 | Type 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 0 261 | 0 262 | 263 | 264 | 265 | 266 | Film 267 | 268 | 269 | 270 | 271 | Series 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 0 282 | 283 | 284 | 285 | 286 | 287 | 80 288 | 16777215 289 | 290 | 291 | 292 | Certificate 293 | 294 | 295 | 296 | 297 | 298 | 299 | false 300 | 301 | 302 | 303 | 304 | 305 | Certificate 306 | 307 | 308 | 309 | TV-Y 310 | 311 | 312 | 313 | 314 | TV-Y7 315 | 316 | 317 | 318 | 319 | G 320 | 321 | 322 | 323 | 324 | PG 325 | 326 | 327 | 328 | 329 | TV-PG 330 | 331 | 332 | 333 | 334 | PG-13 335 | 336 | 337 | 338 | 339 | TV-14 340 | 341 | 342 | 343 | 344 | TV-MA 345 | 346 | 347 | 348 | 349 | NC-17 350 | 351 | 352 | 353 | 354 | Unknown 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 0 365 | 366 | 367 | 368 | 369 | 370 | 80 371 | 16777215 372 | 373 | 374 | 375 | Nudity 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | None 384 | 385 | 386 | 387 | 388 | Mild 389 | 390 | 391 | 392 | 393 | Moderate 394 | 395 | 396 | 397 | 398 | Severe 399 | 400 | 401 | 402 | 403 | Unknown 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 0 414 | 415 | 416 | 417 | 418 | 419 | 0 420 | 0 421 | 422 | 423 | 424 | 425 | 80 426 | 16777215 427 | 428 | 429 | 430 | Violence 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | None 439 | 440 | 441 | 442 | 443 | Mild 444 | 445 | 446 | 447 | 448 | Moderate 449 | 450 | 451 | 452 | 453 | Severe 454 | 455 | 456 | 457 | 458 | Unknown 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 16 471 | 472 | 473 | 0 474 | 475 | 476 | 477 | 478 | QFrame::StyledPanel 479 | 480 | 481 | QFrame::Raised 482 | 483 | 484 | 485 | 486 | 487 | 488 | false 489 | 490 | 491 | Add 492 | 493 | 494 | 495 | 496 | 497 | 498 | Cancel 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 42 509 | 42 510 | 511 | 512 | 513 | 514 | 42 515 | 42 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | -------------------------------------------------------------------------------- /csv.h: -------------------------------------------------------------------------------- 1 | #pragma warning(disable : 4996) //_CRT_SECURE_NO_WARNINGS 2 | 3 | // Copyright: (2012-2015) Ben Strasser 4 | // License: BSD-3 5 | // 6 | // All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 11 | // 1. Redistributions of source code must retain the above copyright notice, 12 | // this list of conditions and the following disclaimer. 13 | // 14 | // 2. Redistributions in binary form must reproduce the above copyright notice, 15 | // this list of conditions and the following disclaimer in the documentation 16 | // and/or other materials provided with the distribution. 17 | // 18 | // 3. Neither the name of the copyright holder nor the names of its contributors 19 | // may be used to endorse or promote products derived from this software 20 | // without specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 25 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 26 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 27 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 28 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 29 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 30 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 31 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | // POSSIBILITY OF SUCH DAMAGE. 33 | 34 | #ifndef CSV_H 35 | #define CSV_H 36 | 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #ifndef CSV_IO_NO_THREAD 45 | #include 46 | #include 47 | #include 48 | #endif 49 | #include 50 | #include 51 | #include 52 | #include 53 | #include 54 | 55 | namespace io { 56 | //////////////////////////////////////////////////////////////////////////// 57 | // LineReader // 58 | //////////////////////////////////////////////////////////////////////////// 59 | 60 | namespace error { 61 | struct base : std::exception { 62 | virtual void format_error_message() const = 0; 63 | 64 | const char *what() const noexcept override { 65 | format_error_message(); 66 | return error_message_buffer; 67 | } 68 | 69 | mutable char error_message_buffer[2048]; 70 | }; 71 | 72 | // this only affects the file name in the error message 73 | const int max_file_name_length = 1024; 74 | 75 | struct with_file_name { 76 | with_file_name() { std::memset(file_name, 0, sizeof(file_name)); } 77 | 78 | void set_file_name(const char *file_name) { 79 | if (file_name != nullptr) { 80 | // This call to strncpy has parenthesis around it 81 | // to silence the GCC -Wstringop-truncation warning 82 | (strncpy(this->file_name, file_name, sizeof(this->file_name))); 83 | this->file_name[sizeof(this->file_name) - 1] = '\0'; 84 | } else { 85 | this->file_name[0] = '\0'; 86 | } 87 | } 88 | 89 | char file_name[max_file_name_length + 1]; 90 | }; 91 | 92 | struct with_file_line { 93 | with_file_line() { file_line = -1; } 94 | 95 | void set_file_line(int file_line) { this->file_line = file_line; } 96 | 97 | int file_line; 98 | }; 99 | 100 | struct with_errno { 101 | with_errno() { errno_value = 0; } 102 | 103 | void set_errno(int errno_value) { this->errno_value = errno_value; } 104 | 105 | int errno_value; 106 | }; 107 | 108 | struct can_not_open_file : base, with_file_name, with_errno { 109 | void format_error_message() const override { 110 | if (errno_value != 0) 111 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 112 | "Can not open file \"%s\" because \"%s\".", file_name, 113 | std::strerror(errno_value)); 114 | else 115 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 116 | "Can not open file \"%s\".", file_name); 117 | } 118 | }; 119 | 120 | struct line_length_limit_exceeded : base, with_file_name, with_file_line { 121 | void format_error_message() const override { 122 | std::snprintf( 123 | error_message_buffer, sizeof(error_message_buffer), 124 | "Line number %d in file \"%s\" exceeds the maximum length of 2^24-1.", 125 | file_line, file_name); 126 | } 127 | }; 128 | } // namespace error 129 | 130 | class ByteSourceBase { 131 | public: 132 | virtual int read(char *buffer, int size) = 0; 133 | virtual ~ByteSourceBase() {} 134 | }; 135 | 136 | namespace detail { 137 | 138 | class OwningStdIOByteSourceBase : public ByteSourceBase { 139 | public: 140 | explicit OwningStdIOByteSourceBase(FILE *file) : file(file) { 141 | // Tell the std library that we want to do the buffering ourself. 142 | std::setvbuf(file, 0, _IONBF, 0); 143 | } 144 | 145 | int read(char *buffer, int size) { return std::fread(buffer, 1, size, file); } 146 | 147 | ~OwningStdIOByteSourceBase() { std::fclose(file); } 148 | 149 | private: 150 | FILE *file; 151 | }; 152 | 153 | class NonOwningIStreamByteSource : public ByteSourceBase { 154 | public: 155 | explicit NonOwningIStreamByteSource(std::istream &in) : in(in) {} 156 | 157 | int read(char *buffer, int size) { 158 | in.read(buffer, size); 159 | return in.gcount(); 160 | } 161 | 162 | ~NonOwningIStreamByteSource() {} 163 | 164 | private: 165 | std::istream ∈ 166 | }; 167 | 168 | class NonOwningStringByteSource : public ByteSourceBase { 169 | public: 170 | NonOwningStringByteSource(const char *str, long long size) 171 | : str(str), remaining_byte_count(size) {} 172 | 173 | int read(char *buffer, int desired_byte_count) { 174 | int to_copy_byte_count = desired_byte_count; 175 | if (remaining_byte_count < to_copy_byte_count) 176 | to_copy_byte_count = remaining_byte_count; 177 | std::memcpy(buffer, str, to_copy_byte_count); 178 | remaining_byte_count -= to_copy_byte_count; 179 | str += to_copy_byte_count; 180 | return to_copy_byte_count; 181 | } 182 | 183 | ~NonOwningStringByteSource() {} 184 | 185 | private: 186 | const char *str; 187 | long long remaining_byte_count; 188 | }; 189 | 190 | #ifndef CSV_IO_NO_THREAD 191 | class AsynchronousReader { 192 | public: 193 | void init(std::unique_ptr arg_byte_source) { 194 | std::unique_lock guard(lock); 195 | byte_source = std::move(arg_byte_source); 196 | desired_byte_count = -1; 197 | termination_requested = false; 198 | worker = std::thread([&] { 199 | std::unique_lock guard(lock); 200 | try { 201 | for (;;) { 202 | read_requested_condition.wait(guard, [&] { 203 | return desired_byte_count != -1 || termination_requested; 204 | }); 205 | if (termination_requested) 206 | return; 207 | 208 | read_byte_count = byte_source->read(buffer, desired_byte_count); 209 | desired_byte_count = -1; 210 | if (read_byte_count == 0) 211 | break; 212 | read_finished_condition.notify_one(); 213 | } 214 | } catch (...) { 215 | read_error = std::current_exception(); 216 | } 217 | read_finished_condition.notify_one(); 218 | }); 219 | } 220 | 221 | bool is_valid() const { return byte_source != nullptr; } 222 | 223 | void start_read(char *arg_buffer, int arg_desired_byte_count) { 224 | std::unique_lock guard(lock); 225 | buffer = arg_buffer; 226 | desired_byte_count = arg_desired_byte_count; 227 | read_byte_count = -1; 228 | read_requested_condition.notify_one(); 229 | } 230 | 231 | int finish_read() { 232 | std::unique_lock guard(lock); 233 | read_finished_condition.wait( 234 | guard, [&] { return read_byte_count != -1 || read_error; }); 235 | if (read_error) 236 | std::rethrow_exception(read_error); 237 | else 238 | return read_byte_count; 239 | } 240 | 241 | ~AsynchronousReader() { 242 | if (byte_source != nullptr) { 243 | { 244 | std::unique_lock guard(lock); 245 | termination_requested = true; 246 | } 247 | read_requested_condition.notify_one(); 248 | worker.join(); 249 | } 250 | } 251 | 252 | private: 253 | std::unique_ptr byte_source; 254 | 255 | std::thread worker; 256 | 257 | bool termination_requested; 258 | std::exception_ptr read_error; 259 | char *buffer; 260 | int desired_byte_count; 261 | int read_byte_count; 262 | 263 | std::mutex lock; 264 | std::condition_variable read_finished_condition; 265 | std::condition_variable read_requested_condition; 266 | }; 267 | #endif 268 | 269 | class SynchronousReader { 270 | public: 271 | void init(std::unique_ptr arg_byte_source) { 272 | byte_source = std::move(arg_byte_source); 273 | } 274 | 275 | bool is_valid() const { return byte_source != nullptr; } 276 | 277 | void start_read(char *arg_buffer, int arg_desired_byte_count) { 278 | buffer = arg_buffer; 279 | desired_byte_count = arg_desired_byte_count; 280 | } 281 | 282 | int finish_read() { return byte_source->read(buffer, desired_byte_count); } 283 | 284 | private: 285 | std::unique_ptr byte_source; 286 | char *buffer; 287 | int desired_byte_count; 288 | }; 289 | } // namespace detail 290 | 291 | class LineReader { 292 | private: 293 | static const int block_len = 1 << 20; 294 | std::unique_ptr buffer; // must be constructed before (and thus 295 | // destructed after) the reader! 296 | #ifdef CSV_IO_NO_THREAD 297 | detail::SynchronousReader reader; 298 | #else 299 | detail::AsynchronousReader reader; 300 | #endif 301 | int data_begin; 302 | int data_end; 303 | 304 | char file_name[error::max_file_name_length + 1]; 305 | unsigned file_line; 306 | 307 | static std::unique_ptr open_file(const char *file_name) { 308 | // We open the file in binary mode as it makes no difference under *nix 309 | // and under Windows we handle \r\n newlines ourself. 310 | FILE *file = std::fopen(file_name, "rb"); 311 | if (file == 0) { 312 | int x = errno; // store errno as soon as possible, doing it after 313 | // constructor call can fail. 314 | error::can_not_open_file err; 315 | err.set_errno(x); 316 | err.set_file_name(file_name); 317 | throw err; 318 | } 319 | return std::unique_ptr( 320 | new detail::OwningStdIOByteSourceBase(file)); 321 | } 322 | 323 | void init(std::unique_ptr byte_source) { 324 | file_line = 0; 325 | 326 | buffer = std::unique_ptr(new char[3 * block_len]); 327 | data_begin = 0; 328 | data_end = byte_source->read(buffer.get(), 2 * block_len); 329 | 330 | // Ignore UTF-8 BOM 331 | if (data_end >= 3 && buffer[0] == '\xEF' && buffer[1] == '\xBB' && 332 | buffer[2] == '\xBF') 333 | data_begin = 3; 334 | 335 | if (data_end == 2 * block_len) { 336 | reader.init(std::move(byte_source)); 337 | reader.start_read(buffer.get() + 2 * block_len, block_len); 338 | } 339 | } 340 | 341 | public: 342 | LineReader() = delete; 343 | LineReader(const LineReader &) = delete; 344 | LineReader &operator=(const LineReader &) = delete; 345 | 346 | explicit LineReader(const char *file_name) { 347 | set_file_name(file_name); 348 | init(open_file(file_name)); 349 | } 350 | 351 | explicit LineReader(const std::string &file_name) { 352 | set_file_name(file_name.c_str()); 353 | init(open_file(file_name.c_str())); 354 | } 355 | 356 | LineReader(const char *file_name, 357 | std::unique_ptr byte_source) { 358 | set_file_name(file_name); 359 | init(std::move(byte_source)); 360 | } 361 | 362 | LineReader(const std::string &file_name, 363 | std::unique_ptr byte_source) { 364 | set_file_name(file_name.c_str()); 365 | init(std::move(byte_source)); 366 | } 367 | 368 | LineReader(const char *file_name, const char *data_begin, 369 | const char *data_end) { 370 | set_file_name(file_name); 371 | init(std::unique_ptr(new detail::NonOwningStringByteSource( 372 | data_begin, data_end - data_begin))); 373 | } 374 | 375 | LineReader(const std::string &file_name, const char *data_begin, 376 | const char *data_end) { 377 | set_file_name(file_name.c_str()); 378 | init(std::unique_ptr(new detail::NonOwningStringByteSource( 379 | data_begin, data_end - data_begin))); 380 | } 381 | 382 | LineReader(const char *file_name, FILE *file) { 383 | set_file_name(file_name); 384 | init(std::unique_ptr( 385 | new detail::OwningStdIOByteSourceBase(file))); 386 | } 387 | 388 | LineReader(const std::string &file_name, FILE *file) { 389 | set_file_name(file_name.c_str()); 390 | init(std::unique_ptr( 391 | new detail::OwningStdIOByteSourceBase(file))); 392 | } 393 | 394 | LineReader(const char *file_name, std::istream &in) { 395 | set_file_name(file_name); 396 | init(std::unique_ptr( 397 | new detail::NonOwningIStreamByteSource(in))); 398 | } 399 | 400 | LineReader(const std::string &file_name, std::istream &in) { 401 | set_file_name(file_name.c_str()); 402 | init(std::unique_ptr( 403 | new detail::NonOwningIStreamByteSource(in))); 404 | } 405 | 406 | void set_file_name(const std::string &file_name) { 407 | set_file_name(file_name.c_str()); 408 | } 409 | 410 | void set_file_name(const char *file_name) { 411 | if (file_name != nullptr) { 412 | strncpy(this->file_name, file_name, sizeof(this->file_name)); 413 | this->file_name[sizeof(this->file_name) - 1] = '\0'; 414 | } else { 415 | this->file_name[0] = '\0'; 416 | } 417 | } 418 | 419 | const char *get_truncated_file_name() const { return file_name; } 420 | 421 | void set_file_line(unsigned file_line) { this->file_line = file_line; } 422 | 423 | unsigned get_file_line() const { return file_line; } 424 | 425 | char *next_line() { 426 | if (data_begin == data_end) 427 | return nullptr; 428 | 429 | ++file_line; 430 | 431 | assert(data_begin < data_end); 432 | assert(data_end <= block_len * 2); 433 | 434 | if (data_begin >= block_len) { 435 | std::memcpy(buffer.get(), buffer.get() + block_len, block_len); 436 | data_begin -= block_len; 437 | data_end -= block_len; 438 | if (reader.is_valid()) { 439 | data_end += reader.finish_read(); 440 | std::memcpy(buffer.get() + block_len, buffer.get() + 2 * block_len, 441 | block_len); 442 | reader.start_read(buffer.get() + 2 * block_len, block_len); 443 | } 444 | } 445 | 446 | int line_end = data_begin; 447 | while (line_end != data_end && buffer[line_end] != '\n') { 448 | ++line_end; 449 | } 450 | 451 | if (line_end - data_begin + 1 > block_len) { 452 | error::line_length_limit_exceeded err; 453 | err.set_file_name(file_name); 454 | err.set_file_line(file_line); 455 | throw err; 456 | } 457 | 458 | if (line_end != data_end && buffer[line_end] == '\n') { 459 | buffer[line_end] = '\0'; 460 | } else { 461 | // some files are missing the newline at the end of the 462 | // last line 463 | ++data_end; 464 | buffer[line_end] = '\0'; 465 | } 466 | 467 | // handle windows \r\n-line breaks 468 | if (line_end != data_begin && buffer[line_end - 1] == '\r') 469 | buffer[line_end - 1] = '\0'; 470 | 471 | char *ret = buffer.get() + data_begin; 472 | data_begin = line_end + 1; 473 | return ret; 474 | } 475 | }; 476 | 477 | //////////////////////////////////////////////////////////////////////////// 478 | // CSV // 479 | //////////////////////////////////////////////////////////////////////////// 480 | 481 | namespace error { 482 | const int max_column_name_length = 63; 483 | struct with_column_name { 484 | with_column_name() { 485 | std::memset(column_name, 0, max_column_name_length + 1); 486 | } 487 | 488 | void set_column_name(const char *column_name) { 489 | if (column_name != nullptr) { 490 | std::strncpy(this->column_name, column_name, max_column_name_length); 491 | this->column_name[max_column_name_length] = '\0'; 492 | } else { 493 | this->column_name[0] = '\0'; 494 | } 495 | } 496 | 497 | char column_name[max_column_name_length + 1]; 498 | }; 499 | 500 | const int max_column_content_length = 63; 501 | 502 | struct with_column_content { 503 | with_column_content() { 504 | std::memset(column_content, 0, max_column_content_length + 1); 505 | } 506 | 507 | void set_column_content(const char *column_content) { 508 | if (column_content != nullptr) { 509 | std::strncpy(this->column_content, column_content, 510 | max_column_content_length); 511 | this->column_content[max_column_content_length] = '\0'; 512 | } else { 513 | this->column_content[0] = '\0'; 514 | } 515 | } 516 | 517 | char column_content[max_column_content_length + 1]; 518 | }; 519 | 520 | struct extra_column_in_header : base, with_file_name, with_column_name { 521 | void format_error_message() const override { 522 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 523 | R"(Extra column "%s" in header of file "%s".)", column_name, 524 | file_name); 525 | } 526 | }; 527 | 528 | struct missing_column_in_header : base, with_file_name, with_column_name { 529 | void format_error_message() const override { 530 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 531 | R"(Missing column "%s" in header of file "%s".)", column_name, 532 | file_name); 533 | } 534 | }; 535 | 536 | struct duplicated_column_in_header : base, with_file_name, with_column_name { 537 | void format_error_message() const override { 538 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 539 | R"(Duplicated column "%s" in header of file "%s".)", 540 | column_name, file_name); 541 | } 542 | }; 543 | 544 | struct header_missing : base, with_file_name { 545 | void format_error_message() const override { 546 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 547 | "Header missing in file \"%s\".", file_name); 548 | } 549 | }; 550 | 551 | struct too_few_columns : base, with_file_name, with_file_line { 552 | void format_error_message() const override { 553 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 554 | "Too few columns in line %d in file \"%s\".", file_line, 555 | file_name); 556 | } 557 | }; 558 | 559 | struct too_many_columns : base, with_file_name, with_file_line { 560 | void format_error_message() const override { 561 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 562 | "Too many columns in line %d in file \"%s\".", file_line, 563 | file_name); 564 | } 565 | }; 566 | 567 | struct escaped_string_not_closed : base, with_file_name, with_file_line { 568 | void format_error_message() const override { 569 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 570 | "Escaped string was not closed in line %d in file \"%s\".", 571 | file_line, file_name); 572 | } 573 | }; 574 | 575 | struct integer_must_be_positive : base, 576 | with_file_name, 577 | with_file_line, 578 | with_column_name, 579 | with_column_content { 580 | void format_error_message() const override { 581 | std::snprintf( 582 | error_message_buffer, sizeof(error_message_buffer), 583 | R"(The integer "%s" must be positive or 0 in column "%s" in file "%s" in line "%d".)", 584 | column_content, column_name, file_name, file_line); 585 | } 586 | }; 587 | 588 | struct no_digit : base, 589 | with_file_name, 590 | with_file_line, 591 | with_column_name, 592 | with_column_content { 593 | void format_error_message() const override { 594 | std::snprintf( 595 | error_message_buffer, sizeof(error_message_buffer), 596 | R"(The integer "%s" contains an invalid digit in column "%s" in file "%s" in line "%d".)", 597 | column_content, column_name, file_name, file_line); 598 | } 599 | }; 600 | 601 | struct integer_overflow : base, 602 | with_file_name, 603 | with_file_line, 604 | with_column_name, 605 | with_column_content { 606 | void format_error_message() const override { 607 | std::snprintf( 608 | error_message_buffer, sizeof(error_message_buffer), 609 | R"(The integer "%s" overflows in column "%s" in file "%s" in line "%d".)", 610 | column_content, column_name, file_name, file_line); 611 | } 612 | }; 613 | 614 | struct integer_underflow : base, 615 | with_file_name, 616 | with_file_line, 617 | with_column_name, 618 | with_column_content { 619 | void format_error_message() const override { 620 | std::snprintf( 621 | error_message_buffer, sizeof(error_message_buffer), 622 | R"(The integer "%s" underflows in column "%s" in file "%s" in line "%d".)", 623 | column_content, column_name, file_name, file_line); 624 | } 625 | }; 626 | 627 | struct invalid_single_character : base, 628 | with_file_name, 629 | with_file_line, 630 | with_column_name, 631 | with_column_content { 632 | void format_error_message() const override { 633 | std::snprintf( 634 | error_message_buffer, sizeof(error_message_buffer), 635 | R"(The content "%s" of column "%s" in file "%s" in line "%d" is not a single character.)", 636 | column_content, column_name, file_name, file_line); 637 | } 638 | }; 639 | } // namespace error 640 | 641 | using ignore_column = unsigned int; 642 | static const ignore_column ignore_no_column = 0; 643 | static const ignore_column ignore_extra_column = 1; 644 | static const ignore_column ignore_missing_column = 2; 645 | 646 | template struct trim_chars { 647 | private: 648 | constexpr static bool is_trim_char(char) { return false; } 649 | 650 | template 651 | constexpr static bool is_trim_char(char c, char trim_char, 652 | OtherTrimChars... other_trim_chars) { 653 | return c == trim_char || is_trim_char(c, other_trim_chars...); 654 | } 655 | 656 | public: 657 | static void trim(char *&str_begin, char *&str_end) { 658 | while (str_begin != str_end && is_trim_char(*str_begin, trim_char_list...)) 659 | ++str_begin; 660 | while (str_begin != str_end && 661 | is_trim_char(*(str_end - 1), trim_char_list...)) 662 | --str_end; 663 | *str_end = '\0'; 664 | } 665 | }; 666 | 667 | struct no_comment { 668 | static bool is_comment(const char *) { return false; } 669 | }; 670 | 671 | template struct single_line_comment { 672 | private: 673 | constexpr static bool is_comment_start_char(char) { return false; } 674 | 675 | template 676 | constexpr static bool 677 | is_comment_start_char(char c, char comment_start_char, 678 | OtherCommentStartChars... other_comment_start_chars) { 679 | return c == comment_start_char || 680 | is_comment_start_char(c, other_comment_start_chars...); 681 | } 682 | 683 | public: 684 | static bool is_comment(const char *line) { 685 | return is_comment_start_char(*line, comment_start_char_list...); 686 | } 687 | }; 688 | 689 | struct empty_line_comment { 690 | static bool is_comment(const char *line) { 691 | if (*line == '\0') 692 | return true; 693 | while (*line == ' ' || *line == '\t') { 694 | ++line; 695 | if (*line == 0) 696 | return true; 697 | } 698 | return false; 699 | } 700 | }; 701 | 702 | template 703 | struct single_and_empty_line_comment { 704 | static bool is_comment(const char *line) { 705 | return single_line_comment::is_comment(line) || 706 | empty_line_comment::is_comment(line); 707 | } 708 | }; 709 | 710 | template struct no_quote_escape { 711 | static const char *find_next_column_end(const char *col_begin) { 712 | while (*col_begin != sep && *col_begin != '\0') 713 | ++col_begin; 714 | return col_begin; 715 | } 716 | 717 | static void unescape(char *&, char *&) {} 718 | }; 719 | 720 | template struct double_quote_escape { 721 | static const char *find_next_column_end(const char *col_begin) { 722 | while (*col_begin != sep && *col_begin != '\0') 723 | if (*col_begin != quote) 724 | ++col_begin; 725 | else { 726 | do { 727 | ++col_begin; 728 | while (*col_begin != quote) { 729 | if (*col_begin == '\0') 730 | throw error::escaped_string_not_closed(); 731 | ++col_begin; 732 | } 733 | ++col_begin; 734 | } while (*col_begin == quote); 735 | } 736 | return col_begin; 737 | } 738 | 739 | static void unescape(char *&col_begin, char *&col_end) { 740 | if (col_end - col_begin >= 2) { 741 | if (*col_begin == quote && *(col_end - 1) == quote) { 742 | ++col_begin; 743 | --col_end; 744 | char *out = col_begin; 745 | for (char *in = col_begin; in != col_end; ++in) { 746 | if (*in == quote && (in + 1) != col_end && *(in + 1) == quote) { 747 | ++in; 748 | } 749 | *out = *in; 750 | ++out; 751 | } 752 | col_end = out; 753 | *col_end = '\0'; 754 | } 755 | } 756 | } 757 | }; 758 | 759 | struct throw_on_overflow { 760 | template static void on_overflow(T &) { 761 | throw error::integer_overflow(); 762 | } 763 | 764 | template static void on_underflow(T &) { 765 | throw error::integer_underflow(); 766 | } 767 | }; 768 | 769 | struct ignore_overflow { 770 | template static void on_overflow(T &) {} 771 | 772 | template static void on_underflow(T &) {} 773 | }; 774 | 775 | struct set_to_max_on_overflow { 776 | template static void on_overflow(T &x) { 777 | // using (std::numeric_limits::max) instead of 778 | // std::numeric_limits::max to make code including windows.h with its max 779 | // macro happy 780 | x = (std::numeric_limits::max)(); 781 | } 782 | 783 | template static void on_underflow(T &x) { 784 | x = (std::numeric_limits::min)(); 785 | } 786 | }; 787 | 788 | namespace detail { 789 | template 790 | void chop_next_column(char *&line, char *&col_begin, char *&col_end) { 791 | assert(line != nullptr); 792 | 793 | col_begin = line; 794 | // the col_begin + (... - col_begin) removes the constness 795 | col_end = 796 | col_begin + (quote_policy::find_next_column_end(col_begin) - col_begin); 797 | 798 | if (*col_end == '\0') { 799 | line = nullptr; 800 | } else { 801 | *col_end = '\0'; 802 | line = col_end + 1; 803 | } 804 | } 805 | 806 | template 807 | void parse_line(char *line, char **sorted_col, 808 | const std::vector &col_order) { 809 | for (int i : col_order) { 810 | if (line == nullptr) 811 | throw ::io::error::too_few_columns(); 812 | char *col_begin, *col_end; 813 | chop_next_column(line, col_begin, col_end); 814 | 815 | if (i != -1) { 816 | trim_policy::trim(col_begin, col_end); 817 | quote_policy::unescape(col_begin, col_end); 818 | 819 | sorted_col[i] = col_begin; 820 | } 821 | } 822 | if (line != nullptr) 823 | throw ::io::error::too_many_columns(); 824 | } 825 | 826 | template 827 | void parse_header_line(char *line, std::vector &col_order, 828 | const std::string *col_name, 829 | ignore_column ignore_policy) { 830 | col_order.clear(); 831 | 832 | bool found[column_count]; 833 | std::fill(found, found + column_count, false); 834 | while (line) { 835 | char *col_begin, *col_end; 836 | chop_next_column(line, col_begin, col_end); 837 | 838 | trim_policy::trim(col_begin, col_end); 839 | quote_policy::unescape(col_begin, col_end); 840 | 841 | for (unsigned i = 0; i < column_count; ++i) 842 | if (col_begin == col_name[i]) { 843 | if (found[i]) { 844 | error::duplicated_column_in_header err; 845 | err.set_column_name(col_begin); 846 | throw err; 847 | } 848 | found[i] = true; 849 | col_order.push_back(i); 850 | col_begin = 0; 851 | break; 852 | } 853 | if (col_begin) { 854 | if (ignore_policy & ::io::ignore_extra_column) 855 | col_order.push_back(-1); 856 | else { 857 | error::extra_column_in_header err; 858 | err.set_column_name(col_begin); 859 | throw err; 860 | } 861 | } 862 | } 863 | if (!(ignore_policy & ::io::ignore_missing_column)) { 864 | for (unsigned i = 0; i < column_count; ++i) { 865 | if (!found[i]) { 866 | error::missing_column_in_header err; 867 | err.set_column_name(col_name[i].c_str()); 868 | throw err; 869 | } 870 | } 871 | } 872 | } 873 | 874 | template void parse(char *col, char &x) { 875 | if (!*col) 876 | throw error::invalid_single_character(); 877 | x = *col; 878 | ++col; 879 | if (*col) 880 | throw error::invalid_single_character(); 881 | } 882 | 883 | template void parse(char *col, std::string &x) { 884 | x = col; 885 | } 886 | 887 | template void parse(char *col, const char *&x) { 888 | x = col; 889 | } 890 | 891 | template void parse(char *col, char *&x) { x = col; } 892 | 893 | template 894 | void parse_unsigned_integer(const char *col, T &x) { 895 | x = 0; 896 | while (*col != '\0') { 897 | if ('0' <= *col && *col <= '9') { 898 | T y = *col - '0'; 899 | if (x > ((std::numeric_limits::max)() - y) / 10) { 900 | overflow_policy::on_overflow(x); 901 | return; 902 | } 903 | x = 10 * x + y; 904 | } else 905 | throw error::no_digit(); 906 | ++col; 907 | } 908 | } 909 | 910 | template void parse(char *col, unsigned char &x) { 911 | parse_unsigned_integer(col, x); 912 | } 913 | template void parse(char *col, unsigned short &x) { 914 | parse_unsigned_integer(col, x); 915 | } 916 | template void parse(char *col, unsigned int &x) { 917 | parse_unsigned_integer(col, x); 918 | } 919 | template void parse(char *col, unsigned long &x) { 920 | parse_unsigned_integer(col, x); 921 | } 922 | template void parse(char *col, unsigned long long &x) { 923 | parse_unsigned_integer(col, x); 924 | } 925 | 926 | template 927 | void parse_signed_integer(const char *col, T &x) { 928 | if (*col == '-') { 929 | ++col; 930 | 931 | x = 0; 932 | while (*col != '\0') { 933 | if ('0' <= *col && *col <= '9') { 934 | T y = *col - '0'; 935 | if (x < ((std::numeric_limits::min)() + y) / 10) { 936 | overflow_policy::on_underflow(x); 937 | return; 938 | } 939 | x = 10 * x - y; 940 | } else 941 | throw error::no_digit(); 942 | ++col; 943 | } 944 | return; 945 | } else if (*col == '+') 946 | ++col; 947 | parse_unsigned_integer(col, x); 948 | } 949 | 950 | template void parse(char *col, signed char &x) { 951 | parse_signed_integer(col, x); 952 | } 953 | template void parse(char *col, signed short &x) { 954 | parse_signed_integer(col, x); 955 | } 956 | template void parse(char *col, signed int &x) { 957 | parse_signed_integer(col, x); 958 | } 959 | template void parse(char *col, signed long &x) { 960 | parse_signed_integer(col, x); 961 | } 962 | template void parse(char *col, signed long long &x) { 963 | parse_signed_integer(col, x); 964 | } 965 | 966 | template void parse_float(const char *col, T &x) { 967 | bool is_neg = false; 968 | if (*col == '-') { 969 | is_neg = true; 970 | ++col; 971 | } else if (*col == '+') 972 | ++col; 973 | 974 | x = 0; 975 | while ('0' <= *col && *col <= '9') { 976 | int y = *col - '0'; 977 | x *= 10; 978 | x += y; 979 | ++col; 980 | } 981 | 982 | if (*col == '.' || *col == ',') { 983 | ++col; 984 | T pos = 1; 985 | while ('0' <= *col && *col <= '9') { 986 | pos /= 10; 987 | int y = *col - '0'; 988 | ++col; 989 | x += y * pos; 990 | } 991 | } 992 | 993 | if (*col == 'e' || *col == 'E') { 994 | ++col; 995 | int e; 996 | 997 | parse_signed_integer(col, e); 998 | 999 | if (e != 0) { 1000 | T base; 1001 | if (e < 0) { 1002 | base = T(0.1); 1003 | e = -e; 1004 | } else { 1005 | base = T(10); 1006 | } 1007 | 1008 | while (e != 1) { 1009 | if ((e & 1) == 0) { 1010 | base = base * base; 1011 | e >>= 1; 1012 | } else { 1013 | x *= base; 1014 | --e; 1015 | } 1016 | } 1017 | x *= base; 1018 | } 1019 | } else { 1020 | if (*col != '\0') 1021 | throw error::no_digit(); 1022 | } 1023 | 1024 | if (is_neg) 1025 | x = -x; 1026 | } 1027 | 1028 | template void parse(char *col, float &x) { 1029 | parse_float(col, x); 1030 | } 1031 | template void parse(char *col, double &x) { 1032 | parse_float(col, x); 1033 | } 1034 | template void parse(char *col, long double &x) { 1035 | parse_float(col, x); 1036 | } 1037 | 1038 | template void parse(char *col, T &x) { 1039 | // Mute unused variable compiler warning 1040 | (void)col; 1041 | (void)x; 1042 | // GCC evaluates "false" when reading the template and 1043 | // "sizeof(T)!=sizeof(T)" only when instantiating it. This is why 1044 | // this strange construct is used. 1045 | static_assert(sizeof(T) != sizeof(T), 1046 | "Can not parse this type. Only builtin integrals, floats, " 1047 | "char, char*, const char* and std::string are supported"); 1048 | } 1049 | 1050 | } // namespace detail 1051 | 1052 | template , 1053 | class quote_policy = double_quote_escape<',', '"'>, 1054 | class overflow_policy = throw_on_overflow, 1055 | class comment_policy = no_comment> 1056 | class CSVReader { 1057 | private: 1058 | LineReader in; 1059 | 1060 | char *row[column_count]; 1061 | std::string column_names[column_count]; 1062 | 1063 | std::vector col_order; 1064 | 1065 | template 1066 | void set_column_names(std::string s, ColNames... cols) { 1067 | column_names[column_count - sizeof...(ColNames) - 1] = std::move(s); 1068 | set_column_names(std::forward(cols)...); 1069 | } 1070 | 1071 | void set_column_names() {} 1072 | 1073 | public: 1074 | CSVReader() = delete; 1075 | CSVReader(const CSVReader &) = delete; 1076 | CSVReader &operator=(const CSVReader &); 1077 | 1078 | template 1079 | explicit CSVReader(Args &&... args) : in(std::forward(args)...) { 1080 | std::fill(row, row + column_count, nullptr); 1081 | col_order.resize(column_count); 1082 | for (unsigned i = 0; i < column_count; ++i) 1083 | col_order[i] = i; 1084 | for (unsigned i = 1; i <= column_count; ++i) 1085 | column_names[i - 1] = "col" + std::to_string(i); 1086 | } 1087 | 1088 | char *next_line() { return in.next_line(); } 1089 | 1090 | template 1091 | void read_header(ignore_column ignore_policy, ColNames... cols) { 1092 | static_assert(sizeof...(ColNames) >= column_count, 1093 | "not enough column names specified"); 1094 | static_assert(sizeof...(ColNames) <= column_count, 1095 | "too many column names specified"); 1096 | try { 1097 | set_column_names(std::forward(cols)...); 1098 | 1099 | char *line; 1100 | do { 1101 | line = in.next_line(); 1102 | if (!line) 1103 | throw error::header_missing(); 1104 | } while (comment_policy::is_comment(line)); 1105 | 1106 | detail::parse_header_line( 1107 | line, col_order, column_names, ignore_policy); 1108 | } catch (error::with_file_name &err) { 1109 | err.set_file_name(in.get_truncated_file_name()); 1110 | throw; 1111 | } 1112 | } 1113 | 1114 | template void set_header(ColNames... cols) { 1115 | static_assert(sizeof...(ColNames) >= column_count, 1116 | "not enough column names specified"); 1117 | static_assert(sizeof...(ColNames) <= column_count, 1118 | "too many column names specified"); 1119 | set_column_names(std::forward(cols)...); 1120 | std::fill(row, row + column_count, nullptr); 1121 | col_order.resize(column_count); 1122 | for (unsigned i = 0; i < column_count; ++i) 1123 | col_order[i] = i; 1124 | } 1125 | 1126 | bool has_column(const std::string &name) const { 1127 | return col_order.end() != 1128 | std::find(col_order.begin(), col_order.end(), 1129 | std::find(std::begin(column_names), std::end(column_names), 1130 | name) - 1131 | std::begin(column_names)); 1132 | } 1133 | 1134 | void set_file_name(const std::string &file_name) { 1135 | in.set_file_name(file_name); 1136 | } 1137 | 1138 | void set_file_name(const char *file_name) { in.set_file_name(file_name); } 1139 | 1140 | const char *get_truncated_file_name() const { 1141 | return in.get_truncated_file_name(); 1142 | } 1143 | 1144 | void set_file_line(unsigned file_line) { in.set_file_line(file_line); } 1145 | 1146 | unsigned get_file_line() const { return in.get_file_line(); } 1147 | 1148 | private: 1149 | void parse_helper(std::size_t) {} 1150 | 1151 | template 1152 | void parse_helper(std::size_t r, T &t, ColType &... cols) { 1153 | if (row[r]) { 1154 | try { 1155 | try { 1156 | ::io::detail::parse(row[r], t); 1157 | } catch (error::with_column_content &err) { 1158 | err.set_column_content(row[r]); 1159 | throw; 1160 | } 1161 | } catch (error::with_column_name &err) { 1162 | err.set_column_name(column_names[r].c_str()); 1163 | throw; 1164 | } 1165 | } 1166 | parse_helper(r + 1, cols...); 1167 | } 1168 | 1169 | public: 1170 | template bool read_row(ColType &... cols) { 1171 | static_assert(sizeof...(ColType) >= column_count, 1172 | "not enough columns specified"); 1173 | static_assert(sizeof...(ColType) <= column_count, 1174 | "too many columns specified"); 1175 | try { 1176 | try { 1177 | 1178 | char *line; 1179 | do { 1180 | line = in.next_line(); 1181 | if (!line) 1182 | return false; 1183 | } while (comment_policy::is_comment(line)); 1184 | 1185 | detail::parse_line(line, row, col_order); 1186 | 1187 | parse_helper(0, cols...); 1188 | } catch (error::with_file_name &err) { 1189 | err.set_file_name(in.get_truncated_file_name()); 1190 | throw; 1191 | } 1192 | } catch (error::with_file_line &err) { 1193 | err.set_file_line(in.get_file_line()); 1194 | throw; 1195 | } 1196 | 1197 | return true; 1198 | } 1199 | }; 1200 | } // namespace io 1201 | #endif 1202 | --------------------------------------------------------------------------------