diff --git a/core/include/Data/ArrayData.h b/core/include/Data/ArrayData.h index 0acf6a0..dfa62ea 100644 --- a/core/include/Data/ArrayData.h +++ b/core/include/Data/ArrayData.h @@ -27,6 +27,17 @@ public: m_Data[0].resize(nbColumns); } + /** + * Ctor for a unidimensional ArrayData + * @param data the data the ArrayData will hold + */ + template > + explicit ArrayData(QVector data) : m_Data{1, QVector{}} + { + QWriteLocker locker{&m_Lock}; + m_Data[0] = std::move(data); + } + /// Copy ctor explicit ArrayData(const ArrayData &other) { diff --git a/core/include/Data/IDataSeries.h b/core/include/Data/IDataSeries.h index 4fc0ff9..5dfa75a 100644 --- a/core/include/Data/IDataSeries.h +++ b/core/include/Data/IDataSeries.h @@ -16,6 +16,12 @@ struct Unit { { } + inline bool operator==(const Unit &other) const + { + return std::tie(m_Name, m_TimeUnit) == std::tie(other.m_Name, other.m_TimeUnit); + } + inline bool operator!=(const Unit &other) const { return !(*this == other); } + QString m_Name; ///< Unit name bool m_TimeUnit; ///< The unit is a unit of time }; diff --git a/core/include/Data/ScalarSeries.h b/core/include/Data/ScalarSeries.h index 03d69cc..bbc2168 100644 --- a/core/include/Data/ScalarSeries.h +++ b/core/include/Data/ScalarSeries.h @@ -17,6 +17,15 @@ public: explicit ScalarSeries(int size, const Unit &xAxisUnit, const Unit &valuesUnit); /** + * Ctor with two vectors. The vectors must have the same size, otherwise a ScalarSeries with no + * values will be created. + * @param xAxisData x-axis data + * @param valuesData values data + */ + explicit ScalarSeries(QVector xAxisData, QVector valuesData, + const Unit &xAxisUnit, const Unit &valuesUnit); + + /** * Sets data for a specific index. The index has to be valid to be effective * @param index the index to which the data will be set * @param x the x-axis data diff --git a/core/src/Data/ScalarSeries.cpp b/core/src/Data/ScalarSeries.cpp index 2673da9..b38d8b0 100644 --- a/core/src/Data/ScalarSeries.cpp +++ b/core/src/Data/ScalarSeries.cpp @@ -6,6 +6,13 @@ ScalarSeries::ScalarSeries(int size, const Unit &xAxisUnit, const Unit &valuesUn { } +ScalarSeries::ScalarSeries(QVector xAxisData, QVector valuesData, + const Unit &xAxisUnit, const Unit &valuesUnit) + : DataSeries{std::make_shared >(std::move(xAxisData)), xAxisUnit, + std::make_shared >(std::move(valuesData)), valuesUnit} +{ +} + void ScalarSeries::setData(int index, double x, double value) noexcept { xAxisData()->setData(index, x); diff --git a/plugins/amda/src/AmdaResultParser.cpp b/plugins/amda/src/AmdaResultParser.cpp index b929c7b..2438d51 100644 --- a/plugins/amda/src/AmdaResultParser.cpp +++ b/plugins/amda/src/AmdaResultParser.cpp @@ -4,6 +4,7 @@ #include #include +#include Q_LOGGING_CATEGORY(LOG_AmdaResultParser, "AmdaResultParser") @@ -12,59 +13,120 @@ namespace { /// Format for dates in result files const auto DATE_FORMAT = QStringLiteral("yyyy-MM-ddThh:mm:ss.zzz"); -/// @todo ALX +/// Separator between values in a result line +const auto RESULT_LINE_SEPARATOR = QRegularExpression{QStringLiteral("\\s+")}; + +/// Regex to find unit in a line. Examples of valid lines: +/// ... - Units : nT - ... +/// ... -Units:nT- ... +/// ... -Units: m²- ... +/// ... - Units : m/s - ... +const auto UNIT_REGEX = QRegularExpression{QStringLiteral("-\\s*Units\\s*:\\s*(.+?)\\s*-")}; + +/// Converts a string date to a double date +/// @return a double that represents the date in seconds, NaN if the string date can't be converted double doubleDate(const QString &stringDate) noexcept { auto dateTime = QDateTime::fromString(stringDate, DATE_FORMAT); - return dateTime.toMSecsSinceEpoch() / 1000.; + return dateTime.isValid() ? (dateTime.toMSecsSinceEpoch() / 1000.) + : std::numeric_limits::quiet_NaN(); } -} // namespace - -std::shared_ptr AmdaResultParser::readTxt(const QString &filePath) noexcept +/** + * Reads stream to retrieve x-axis unit + * @param stream the stream to read + * @return the unit that has been read in the stream, a default unit (time unit with no label) if an + * error occured during reading + */ +Unit readXAxisUnit(QTextStream &stream) { - QFile file{filePath}; + QString line{}; - if (!file.open(QFile::ReadOnly | QIODevice::Text)) { - qCCritical(LOG_AmdaResultParser()) - << QObject::tr("Can't retrieve AMDA data from file %1: %2") - .arg(filePath, file.errorString()); - return nullptr; + if (stream.readLineInto(&line)) { + auto match = UNIT_REGEX.match(line); + if (match.hasMatch()) { + return Unit{match.captured(1), true}; + } + else { + qCWarning(LOG_AmdaResultParser()) + << QObject::tr("Can't read unit: invalid line %1").arg(line); + } + } + else { + qCWarning(LOG_AmdaResultParser()) << QObject::tr("Can't read unit: end of file"); } + // Error cases + return Unit{{}, true}; +} + +/** + * Reads stream to retrieve results + * @param stream the stream to read + * @return the pair of vectors x-axis data/values data that has been read in the stream + */ +QPair, QVector > readResults(QTextStream &stream) +{ auto xData = QVector{}; auto valuesData = QVector{}; - QTextStream stream{&file}; - - // Ignore comment lines (3 lines) - stream.readLine(); - stream.readLine(); - stream.readLine(); - QString line{}; - auto lineRegex = QRegExp{QStringLiteral("\\s+")}; while (stream.readLineInto(&line)) { - auto lineData = line.split(lineRegex, QString::SkipEmptyParts); + auto lineData = line.split(RESULT_LINE_SEPARATOR, QString::SkipEmptyParts); if (lineData.size() == 2) { // X : the data is converted from date to double (in secs) - xData.push_back(doubleDate(lineData.at(0))); + auto x = doubleDate(lineData.at(0)); // Value - valuesData.push_back(lineData.at(1).toDouble()); + bool valueOk; + auto value = lineData.at(1).toDouble(&valueOk); + + // Adds result only if x and value are valid + if (!std::isnan(x) && !std::isnan(value) && valueOk) { + xData.push_back(x); + valuesData.push_back(value); + } + else { + qCWarning(LOG_AmdaResultParser()) + << QObject::tr( + "Can't retrieve results from line %1: x and/or value are invalid") + .arg(line); + } } else { - /// @todo ALX : log + qCWarning(LOG_AmdaResultParser()) + << QObject::tr("Can't retrieve results from line %1: invalid line").arg(line); } } - /// @todo ALX : handle units - auto scalarSeries = std::make_shared(xData.size(), Unit{"nT", true}, Unit{}); + return qMakePair(std::move(xData), std::move(valuesData)); +} + +} // namespace - const auto count = xData.size(); - for (auto i = 0; i < count; ++i) { - scalarSeries->setData(i, xData.at(i), valuesData.at(i)); +std::shared_ptr AmdaResultParser::readTxt(const QString &filePath) noexcept +{ + QFile file{filePath}; + + if (!file.open(QFile::ReadOnly | QIODevice::Text)) { + qCCritical(LOG_AmdaResultParser()) + << QObject::tr("Can't retrieve AMDA data from file %1: %2") + .arg(filePath, file.errorString()); + return nullptr; } - return scalarSeries; + QTextStream stream{&file}; + + // Ignore first two lines (comments lines) + stream.readLine(); + stream.readLine(); + + // Reads x-axis unit + auto xAxisUnit = readXAxisUnit(stream); + + // Reads results + auto results = readResults(stream); + + return std::make_shared(std::move(results.first), std::move(results.second), + xAxisUnit, Unit{}); } diff --git a/plugins/amda/tests-resources/TestAmdaResultParser/NaNValue.txt b/plugins/amda/tests-resources/TestAmdaResultParser/NaNValue.txt new file mode 100644 index 0000000..dab62ce --- /dev/null +++ b/plugins/amda/tests-resources/TestAmdaResultParser/NaNValue.txt @@ -0,0 +1,6 @@ +#Sampling Time : 60 +#Time Format : YYYY-MM-DDThh:mm:ss.mls +#imf(0) - Type : Local Parameter @ CDPP/AMDA - Name : bx_gse - Units : nT - Size : 1 - Frame : GSE - Mission : ACE - Instrument : MFI - Dataset : mfi_final-prelim +2013-09-23T09:00:30.000 NaN +2013-09-23T09:01:30.000 -2.71850 +2013-09-23T09:02:30.000 -2.52150 \ No newline at end of file diff --git a/plugins/amda/tests-resources/TestAmdaResultParser/NoUnit.txt b/plugins/amda/tests-resources/TestAmdaResultParser/NoUnit.txt new file mode 100644 index 0000000..648be9f --- /dev/null +++ b/plugins/amda/tests-resources/TestAmdaResultParser/NoUnit.txt @@ -0,0 +1,2 @@ +#Sampling Time : 60 +#Time Format : YYYY-MM-DDThh:mm:ss.mls \ No newline at end of file diff --git a/plugins/amda/tests-resources/TestAmdaResultParser/TooManyValues.txt b/plugins/amda/tests-resources/TestAmdaResultParser/TooManyValues.txt new file mode 100644 index 0000000..c58e9d4 --- /dev/null +++ b/plugins/amda/tests-resources/TestAmdaResultParser/TooManyValues.txt @@ -0,0 +1,6 @@ +#Sampling Time : 60 +#Time Format : YYYY-MM-DDThh:mm:ss.mls +#imf(0) - Type : Local Parameter @ CDPP/AMDA - Name : bx_gse - Units : nT - Size : 1 - Frame : GSE - Mission : ACE - Instrument : MFI - Dataset : mfi_final-prelim +2013-09-23T09:00:30.000 -2.83950 1.05141 3.01547 +2013-09-23T09:01:30.000 -2.71850 +2013-09-23T09:02:30.000 -2.52150 \ No newline at end of file diff --git a/plugins/amda/tests-resources/TestAmdaResultParser/ValidScalar1.txt b/plugins/amda/tests-resources/TestAmdaResultParser/ValidScalar1.txt new file mode 100644 index 0000000..41b585a --- /dev/null +++ b/plugins/amda/tests-resources/TestAmdaResultParser/ValidScalar1.txt @@ -0,0 +1,13 @@ +#Sampling Time : 60 +#Time Format : YYYY-MM-DDThh:mm:ss.mls +#imf(0) - Type : Local Parameter @ CDPP/AMDA - Name : bx_gse - Units : nT - Size : 1 - Frame : GSE - Mission : ACE - Instrument : MFI - Dataset : mfi_final-prelim +2013-09-23T09:00:30.000 -2.83950 +2013-09-23T09:01:30.000 -2.71850 +2013-09-23T09:02:30.000 -2.52150 +2013-09-23T09:03:30.000 -2.57633 +2013-09-23T09:04:30.000 -2.58050 +2013-09-23T09:05:30.000 -2.48325 +2013-09-23T09:06:30.000 -2.63025 +2013-09-23T09:07:30.000 -2.55800 +2013-09-23T09:08:30.000 -2.43250 +2013-09-23T09:09:30.000 -2.42200 \ No newline at end of file diff --git a/plugins/amda/tests-resources/TestAmdaResultParser/WrongDate.txt b/plugins/amda/tests-resources/TestAmdaResultParser/WrongDate.txt new file mode 100644 index 0000000..7dc5266 --- /dev/null +++ b/plugins/amda/tests-resources/TestAmdaResultParser/WrongDate.txt @@ -0,0 +1,6 @@ +#Sampling Time : 60 +#Time Format : YYYY-MM-DDThh:mm:ss.mls +#imf(0) - Type : Local Parameter @ CDPP/AMDA - Name : bx_gse - Units : nT - Size : 1 - Frame : GSE - Mission : ACE - Instrument : MFI - Dataset : mfi_final-prelim +23/09/2013 07:50:30 -2.83950 +2013-09-23T09:01:30.000 -2.71850 +2013-09-23T09:02:30.000 -2.52150 \ No newline at end of file diff --git a/plugins/amda/tests-resources/TestAmdaResultParser/WrongUnit.txt b/plugins/amda/tests-resources/TestAmdaResultParser/WrongUnit.txt new file mode 100644 index 0000000..5a62942 --- /dev/null +++ b/plugins/amda/tests-resources/TestAmdaResultParser/WrongUnit.txt @@ -0,0 +1,6 @@ +#Sampling Time : 60 +#Time Format : YYYY-MM-DDThh:mm:ss.mls +#Wrong unit comment +2013-09-23T09:00:30.000 -2.83950 +2013-09-23T09:01:30.000 -2.71850 +2013-09-23T09:02:30.000 -2.52150 \ No newline at end of file diff --git a/plugins/amda/tests-resources/TestAmdaResultParser/WrongValue.txt b/plugins/amda/tests-resources/TestAmdaResultParser/WrongValue.txt new file mode 100644 index 0000000..578a209 --- /dev/null +++ b/plugins/amda/tests-resources/TestAmdaResultParser/WrongValue.txt @@ -0,0 +1,6 @@ +#Sampling Time : 60 +#Time Format : YYYY-MM-DDThh:mm:ss.mls +#imf(0) - Type : Local Parameter @ CDPP/AMDA - Name : bx_gse - Units : nT - Size : 1 - Frame : GSE - Mission : ACE - Instrument : MFI - Dataset : mfi_final-prelim +2013-09-23T09:00:30.000 abc +2013-09-23T09:01:30.000 -2.71850 +2013-09-23T09:02:30.000 -2.52150 \ No newline at end of file diff --git a/plugins/amda/tests/TestAmdaResultParser.cpp b/plugins/amda/tests/TestAmdaResultParser.cpp new file mode 100644 index 0000000..2d381aa --- /dev/null +++ b/plugins/amda/tests/TestAmdaResultParser.cpp @@ -0,0 +1,179 @@ +#include "AmdaResultParser.h" + +#include + +#include +#include + +namespace { + +/// Path for the tests +const auto TESTS_RESOURCES_PATH + = QFileInfo{QString{AMDA_TESTS_RESOURCES_DIR}, "TestAmdaResultParser"}.absoluteFilePath(); + +QString inputFilePath(const QString &inputFileName) +{ + return QFileInfo{TESTS_RESOURCES_PATH, inputFileName}.absoluteFilePath(); +} + +struct ExpectedResults { + explicit ExpectedResults() = default; + + /// Ctor with QVector as x-axis data. Datetimes are converted to doubles + explicit ExpectedResults(Unit xAxisUnit, Unit valuesUnit, const QVector &xAxisData, + QVector valuesData) + : m_ParsingOK{true}, + m_XAxisUnit{xAxisUnit}, + m_ValuesUnit{valuesUnit}, + m_XAxisData{}, + m_ValuesData{std::move(valuesData)} + { + // Converts QVector to QVector + std::transform(xAxisData.cbegin(), xAxisData.cend(), std::back_inserter(m_XAxisData), + [](const auto &dateTime) { return dateTime.toMSecsSinceEpoch() / 1000.; }); + } + + /** + * Validates a DataSeries compared to the expected results + * @param results the DataSeries to validate + */ + void validate(std::shared_ptr results) + { + if (m_ParsingOK) { + auto scalarSeries = dynamic_cast(results.get()); + QVERIFY(scalarSeries != nullptr); + + // Checks units + QVERIFY(scalarSeries->xAxisUnit() == m_XAxisUnit); + QVERIFY(scalarSeries->valuesUnit() == m_ValuesUnit); + + // Checks values + QVERIFY(scalarSeries->xAxisData()->data() == m_XAxisData); + QVERIFY(scalarSeries->valuesData()->data() == m_ValuesData); + } + else { + QVERIFY(results == nullptr); + } + } + + // Parsing was successfully completed + bool m_ParsingOK{false}; + // Expected x-axis unit + Unit m_XAxisUnit{}; + // Expected values unit + Unit m_ValuesUnit{}; + // Expected x-axis data + QVector m_XAxisData{}; + // Expected values data + QVector m_ValuesData{}; +}; + +} // namespace + +Q_DECLARE_METATYPE(ExpectedResults) + +class TestAmdaResultParser : public QObject { + Q_OBJECT +private slots: + /// Input test data + /// @sa testTxtJson() + void testReadTxt_data(); + + /// Tests parsing of a TXT file + void testReadTxt(); +}; + +void TestAmdaResultParser::testReadTxt_data() +{ + // ////////////// // + // Test structure // + // ////////////// // + + // Name of TXT file to read + QTest::addColumn("inputFileName"); + // Expected results + QTest::addColumn("expectedResults"); + + // ////////// // + // Test cases // + // ////////// // + + auto dateTime = [](int year, int month, int day, int hours, int minutes, int seconds) { + return QDateTime{{year, month, day}, {hours, minutes, seconds}}; + }; + + // Valid file + QTest::newRow("Valid file") + << QStringLiteral("ValidScalar1.txt") + << ExpectedResults{ + Unit{QStringLiteral("nT"), true}, Unit{}, + QVector{dateTime(2013, 9, 23, 9, 0, 30), dateTime(2013, 9, 23, 9, 1, 30), + dateTime(2013, 9, 23, 9, 2, 30), dateTime(2013, 9, 23, 9, 3, 30), + dateTime(2013, 9, 23, 9, 4, 30), dateTime(2013, 9, 23, 9, 5, 30), + dateTime(2013, 9, 23, 9, 6, 30), dateTime(2013, 9, 23, 9, 7, 30), + dateTime(2013, 9, 23, 9, 8, 30), dateTime(2013, 9, 23, 9, 9, 30)}, + QVector{-2.83950, -2.71850, -2.52150, -2.57633, -2.58050, -2.48325, -2.63025, + -2.55800, -2.43250, -2.42200}}; + + // Valid files but with some invalid lines (wrong unit, wrong values, etc.) + QTest::newRow("No unit file") << QStringLiteral("NoUnit.txt") + << ExpectedResults{Unit{QStringLiteral(""), true}, Unit{}, + QVector{}, QVector{}}; + QTest::newRow("Wrong unit file") + << QStringLiteral("WrongUnit.txt") + << ExpectedResults{Unit{QStringLiteral(""), true}, Unit{}, + QVector{dateTime(2013, 9, 23, 9, 0, 30), + dateTime(2013, 9, 23, 9, 1, 30), + dateTime(2013, 9, 23, 9, 2, 30)}, + QVector{-2.83950, -2.71850, -2.52150}}; + + QTest::newRow("Wrong results file (date of first line is invalid") + << QStringLiteral("WrongDate.txt") + << ExpectedResults{ + Unit{QStringLiteral("nT"), true}, Unit{}, + QVector{dateTime(2013, 9, 23, 9, 1, 30), dateTime(2013, 9, 23, 9, 2, 30)}, + QVector{-2.71850, -2.52150}}; + + QTest::newRow("Wrong results file (too many values for first line") + << QStringLiteral("TooManyValues.txt") + << ExpectedResults{ + Unit{QStringLiteral("nT"), true}, Unit{}, + QVector{dateTime(2013, 9, 23, 9, 1, 30), dateTime(2013, 9, 23, 9, 2, 30)}, + QVector{-2.71850, -2.52150}}; + + QTest::newRow("Wrong results file (value of first line is invalid") + << QStringLiteral("WrongValue.txt") + << ExpectedResults{ + Unit{QStringLiteral("nT"), true}, Unit{}, + QVector{dateTime(2013, 9, 23, 9, 1, 30), dateTime(2013, 9, 23, 9, 2, 30)}, + QVector{-2.71850, -2.52150}}; + + QTest::newRow("Wrong results file (value of first line is NaN") + << QStringLiteral("NaNValue.txt") + << ExpectedResults{ + Unit{QStringLiteral("nT"), true}, Unit{}, + QVector{dateTime(2013, 9, 23, 9, 1, 30), dateTime(2013, 9, 23, 9, 2, 30)}, + QVector{-2.71850, -2.52150}}; + + // Invalid file + QTest::newRow("Invalid file (unexisting file)") + << QStringLiteral("UnexistingFile.txt") << ExpectedResults{}; +} + +void TestAmdaResultParser::testReadTxt() +{ + QFETCH(QString, inputFileName); + QFETCH(ExpectedResults, expectedResults); + + // Parses file + auto filePath = inputFilePath(inputFileName); + auto results = AmdaResultParser::readTxt(filePath); + + // ///////////////// // + // Validates results // + // ///////////////// // + expectedResults.validate(results); +} + +QTEST_MAIN(TestAmdaResultParser) +#include "TestAmdaResultParser.moc"