├── 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 |
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 |
--------------------------------------------------------------------------------