From 57074a05e1a0a9153785405c2946dc2ed98575d2 Mon Sep 17 00:00:00 2001 From: iuri dotta Date: Sat, 28 Feb 2026 10:35:39 -0300 Subject: [PATCH 01/11] Refactor code structure for improved readability and maintainability --- .gitignore | 3 + CMakeLists.txt | 17 + include/edfio/Config.hpp | 2 +- include/edfio/Utils.hpp | 2 +- include/edfio/core/Annotation.hpp | 2 +- include/edfio/core/DataFormat.hpp | 8 +- include/edfio/core/Device.hpp | 13 +- include/edfio/core/Record.hpp | 22 +- include/edfio/header/HeaderUtils.hpp | 16 +- .../edfio/processor/detail/ProcessorUtils.hpp | 132 +- .../processor/impl/ProcessorHeaderExam.ipp | 2 +- .../processor/impl/ProcessorHeaderGeneral.ipp | 22 +- .../impl/ProcessorHeaderGeneralFields.ipp | 92 +- .../processor/impl/ProcessorHeaderSignal.ipp | 2 +- .../impl/ProcessorHeaderSignalFields.ipp | 55 +- .../edfio/processor/impl/ProcessorSample.ipp | 2 +- .../processor/impl/ProcessorSampleRecord.ipp | 26 +- .../processor/impl/ProcessorTalRecord.ipp | 22 +- .../impl/ProcessorTimeStampRecord.ipp | 4 +- .../edfio/reader/impl/ReaderHeaderExam.ipp | 8 +- .../edfio/reader/impl/ReaderHeaderGeneral.ipp | 4 +- .../edfio/reader/impl/ReaderHeaderSignal.ipp | 4 +- include/edfio/sink/DataRecordSink.hpp | 8 +- include/edfio/sink/RecordSink.hpp | 93 +- include/edfio/sink/SignalRecordSink.hpp | 12 +- include/edfio/sink/Sink.hpp | 11 +- include/edfio/sink/detail/SinkUtils.hpp | 10 +- include/edfio/store/DatarecordStore.hpp | 12 +- include/edfio/store/RecordStore.hpp | 133 +- include/edfio/store/SignalSampleStore.hpp | 18 +- include/edfio/store/SignalrecordStore.hpp | 12 +- include/edfio/store/Store.hpp | 13 +- include/edfio/store/TalStore.hpp | 112 +- include/edfio/store/TimeStampStore.hpp | 12 +- include/edfio/store/detail/StoreUtils.hpp | 16 +- .../edfio/writer/impl/WriterHeaderExam.ipp | 8 +- .../edfio/writer/impl/WriterHeaderGeneral.ipp | 2 +- .../edfio/writer/impl/WriterHeaderSignals.ipp | 2 +- include/edfio/writer/impl/WriterRecord.ipp | 2 +- test_generator_2.edf | Bin 0 -> 2704528 bytes tests/CMakeLists.txt | 33 + tests/test_iterators.cpp | 170 + tests/test_processor_sample.cpp | 76 + tests/test_processor_utils.cpp | 93 + tests/test_reader.cpp | 41 + tests/test_record.cpp | 45 + tests/test_writer.cpp | 48 + third_party/doctest/doctest.h | 7106 +++++++++++++++++ 48 files changed, 8036 insertions(+), 512 deletions(-) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 test_generator_2.edf create mode 100644 tests/CMakeLists.txt create mode 100644 tests/test_iterators.cpp create mode 100644 tests/test_processor_sample.cpp create mode 100644 tests/test_processor_utils.cpp create mode 100644 tests/test_reader.cpp create mode 100644 tests/test_record.cpp create mode 100644 tests/test_writer.cpp create mode 100644 third_party/doctest/doctest.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e4a901 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +.cache/ +.claude/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..3fcbebd --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 4.0) +project(edfio VERSION 0.2.0 LANGUAGES CXX) + +# ---- Header-only library target ---- +add_library(edfio INTERFACE) +target_include_directories(edfio INTERFACE + $ + $ +) +target_compile_features(edfio INTERFACE cxx_std_23) + +# ---- Tests ---- +option(EDFIO_BUILD_TESTS "Build tests" ON) +if(EDFIO_BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/include/edfio/Config.hpp b/include/edfio/Config.hpp index da9b000..4f833f0 100644 --- a/include/edfio/Config.hpp +++ b/include/edfio/Config.hpp @@ -20,7 +20,7 @@ namespace edfio namespace config { - static constexpr ProcessorErrorCheck PROCESSOR_ERROR_CHECKING = ProcessorErrorCheck::Strict; + inline constexpr ProcessorErrorCheck PROCESSOR_ERROR_CHECKING = ProcessorErrorCheck::Strict; } } diff --git a/include/edfio/Utils.hpp b/include/edfio/Utils.hpp index c71cfd6..35ee419 100644 --- a/include/edfio/Utils.hpp +++ b/include/edfio/Utils.hpp @@ -16,7 +16,7 @@ namespace edfio namespace detail { - static const char* GetError(FileErrc err) + inline constexpr const char* GetError(FileErrc err) { if (err == FileErrc::FileDoesNotOpen) { diff --git a/include/edfio/core/Annotation.hpp b/include/edfio/core/Annotation.hpp index e588ed8..a73db30 100644 --- a/include/edfio/core/Annotation.hpp +++ b/include/edfio/core/Annotation.hpp @@ -24,7 +24,7 @@ namespace edfio struct TimeStamp { - long long m_dararecord = 0; + long long m_datarecord = 0; double m_start = 0; }; diff --git a/include/edfio/core/DataFormat.hpp b/include/edfio/core/DataFormat.hpp index 618d089..c83514a 100644 --- a/include/edfio/core/DataFormat.hpp +++ b/include/edfio/core/DataFormat.hpp @@ -23,23 +23,23 @@ namespace edfio Invalid }; - static const bool IsPlus(DataFormat format) + inline constexpr bool IsPlus(DataFormat format) { return format == DataFormat::EdfPlusC || format == DataFormat::EdfPlusD || format == DataFormat::BdfPlusC || format == DataFormat::BdfPlusD; } - static const bool IsEdf(DataFormat format) + inline constexpr bool IsEdf(DataFormat format) { return format == DataFormat::Edf || format == DataFormat::EdfPlusC || format == DataFormat::EdfPlusD; } - static const bool IsBdf(DataFormat format) + inline constexpr bool IsBdf(DataFormat format) { return format == DataFormat::Bdf || format == DataFormat::BdfPlusC || format == DataFormat::BdfPlusD; } - static const int GetSampleBytes(DataFormat format) + inline constexpr int GetSampleBytes(DataFormat format) { if (IsEdf(format)) return 2; diff --git a/include/edfio/core/Device.hpp b/include/edfio/core/Device.hpp index 5af49af..e99b59c 100644 --- a/include/edfio/core/Device.hpp +++ b/include/edfio/core/Device.hpp @@ -32,12 +32,13 @@ namespace edfio class iterator { public: - typedef typename Device::difference_type difference_type; - typedef typename Device::value_type value_type; - typedef typename Device::reference reference; - typedef typename Device::pointer pointer; - typedef IterCategory iterator_category; - typedef typename Device::stream_type stream_type; + using difference_type = typename Device::difference_type; + using value_type = typename Device::value_type; + using reference = typename Device::reference; + using pointer = typename Device::pointer; + using iterator_category = IterCategory; + using iterator_concept = IterCategory; + using stream_type = typename Device::stream_type; }; Device() = delete; diff --git a/include/edfio/core/Record.hpp b/include/edfio/core/Record.hpp index 8f87171..f362a3c 100644 --- a/include/edfio/core/Record.hpp +++ b/include/edfio/core/Record.hpp @@ -27,22 +27,19 @@ namespace edfio Record() = delete; Record(size_t recordSize) - : m_size(recordSize) - , m_value(recordSize, 0) {} + : m_value(recordSize, 0) {} Record(typename VectorType::const_iterator first, typename VectorType::const_iterator last) - : m_size(std::distance(first, last)) - , m_value(first, last) {} + : m_value(first, last) {} - Record(const Record &record) - : m_size(record.m_size) - , m_value(record.m_value) - { - } + Record(const Record&) = default; + Record(Record&&) = default; + Record& operator=(const Record&) = default; + Record& operator=(Record&&) = default; - const size_t Size() const + size_t Size() const { - return m_size; + return m_value.size(); } const VectorType& operator()() const { @@ -52,7 +49,7 @@ namespace edfio { return m_value; } - Record operator+(const Record& record) + Record operator+(const Record& record) const { Record tmp(Size() + record.Size()); std::copy(m_value.begin(), m_value.end(), tmp().begin()); @@ -61,7 +58,6 @@ namespace edfio } VectorType m_value; - const size_t m_size; }; template diff --git a/include/edfio/header/HeaderUtils.hpp b/include/edfio/header/HeaderUtils.hpp index 21d45ad..69a7734 100644 --- a/include/edfio/header/HeaderUtils.hpp +++ b/include/edfio/header/HeaderUtils.hpp @@ -22,7 +22,7 @@ namespace edfio namespace detail { - static HeaderGeneral CreateHeaderGeneral( + inline HeaderGeneral CreateHeaderGeneral( DataFormat version, std::string patient, std::string recording, @@ -59,10 +59,10 @@ namespace edfio } header.m_detail.m_recordSize *= GetSampleBytes(version); - return std::move(header); + return header; } - static HeaderGeneral CreateHeaderGeneralPlus( + inline HeaderGeneral CreateHeaderGeneralPlus( DataFormat version, std::string patientCode, std::string gender, @@ -86,10 +86,10 @@ namespace edfio const std::vector& signals ) { - auto header = std::move(CreateHeaderGeneral( + auto header = CreateHeaderGeneral( version, "", "", startDateD, startDateM, startDateY, startTimeH, startTimeM, startTimeS, headerSize, reserved, datarecordsFile, datarecordDuration, signals - )); + ); header.m_detail.m_patientCode = patientCode; header.m_detail.m_gender = gender; @@ -101,10 +101,10 @@ namespace edfio header.m_detail.m_equipment = equipment; header.m_detail.m_recordingAdditional = recordingAdditional; - return std::move(header); + return header; } - static HeaderSignal CreateHeaderSignal( + inline HeaderSignal CreateHeaderSignal( std::string label, int samplesInDataRecord, double physicalMin, @@ -137,7 +137,7 @@ namespace edfio signal.m_detail.m_offset = signal.m_physicalMin - signal.m_detail.m_scaling * signal.m_digitalMin; signal.m_detail.m_isAnnotation = annotation; - return std::move(signal); + return signal; } } diff --git a/include/edfio/processor/detail/ProcessorUtils.hpp b/include/edfio/processor/detail/ProcessorUtils.hpp index 41039d9..dfb907a 100644 --- a/include/edfio/processor/detail/ProcessorUtils.hpp +++ b/include/edfio/processor/detail/ProcessorUtils.hpp @@ -13,8 +13,11 @@ #include #include +#include +#include #include -#include +#include +#include namespace edfio { @@ -23,41 +26,33 @@ namespace edfio { template - static bool CheckFormatErrors(const typename std::enable_if>::type &str) + inline bool CheckFormatErrors(const std::basic_string &str) { - for (auto& c : str) - { - if (!std::isprint(c)) + if constexpr (Check == ProcessorErrorCheck::Permissive) { + return false; + } else { + for (auto c : str) { - return true; + if (!std::isprint(static_cast(c))) + return true; } + return false; } - return false; - } - - template - static bool CheckFormatErrors(const typename std::enable_if>::type &str) - { - return false; } template - static bool CheckFormatErrors(const typename std::enable_if>::type &str) + inline bool CheckFormatErrors(const std::vector &str) { - for (auto& c : str) - { - if (!std::isprint(c)) + if constexpr (Check == ProcessorErrorCheck::Permissive) { + return false; + } else { + for (auto c : str) { - return true; + if (!std::isprint(static_cast(c))) + return true; } + return false; } - return false; - } - - template - static bool CheckFormatErrors(const typename std::enable_if>::type &str) - { - return false; } } @@ -65,26 +60,28 @@ namespace edfio namespace detail { - static const char ADDITIONAL_SEPARATOR = '|'; + inline constexpr char ADDITIONAL_SEPARATOR = '|'; + inline constexpr std::array MONTHS = { + "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" + }; template - static bool CheckFormatErrors(const std::basic_string &str) + inline bool CheckFormatErrors(const std::basic_string &str) { return impl::CheckFormatErrors(str); } template - static bool CheckFormatErrors(const std::vector &str) + inline bool CheckFormatErrors(const std::vector &str) { return impl::CheckFormatErrors(str); } - static int GetMonthFromString(const std::string &str) + inline int GetMonthFromString(std::string_view str) { - static const std::vector months = { "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; - for (size_t idx = 0; idx < months.size(); idx++) + for (size_t idx = 0; idx < MONTHS.size(); idx++) { - if (str == months[idx]) + if (str == MONTHS[idx]) { return idx + 1; } @@ -92,21 +89,39 @@ namespace edfio return 0; } - static std::string GetStringFromMonth(size_t idx) + inline std::string GetStringFromMonth(size_t idx) { - idx--; - static const std::vector months = { "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; - if (idx >= 0 && idx < months.size()) - return months[idx]; + if (idx >= 1 && idx <= 12) + return std::string{ MONTHS[idx - 1] }; return "JAN"; } - static std::string ReduceString(const std::string &value) + inline std::string ReduceString(std::string_view value) { - return std::regex_replace(value, std::regex("^ +| +$|( ) +"), "$1"); + // Trim leading spaces + auto start = value.find_first_not_of(' '); + if (start == std::string::npos) return ""; + // Trim trailing spaces + auto end = value.find_last_not_of(' '); + // Collapse interior runs of spaces to single space + std::string result; + result.reserve(end - start + 1); + bool prev_space = false; + for (size_t i = start; i <= end; ++i) { + if (value[i] == ' ') { + if (!prev_space) { + result += ' '; + prev_space = true; + } + } else { + result += value[i]; + prev_space = false; + } + } + return result; } - static std::string GetFormatName(DataFormat format) + inline std::string_view GetFormatName(DataFormat format) { if (format == DataFormat::Edf) return "EDF"; @@ -123,8 +138,43 @@ namespace edfio return ""; } + inline int ParseInt(std::string_view sv, const char* error_msg) + { + while (!sv.empty() && sv.front() == ' ') sv.remove_prefix(1); + while (!sv.empty() && sv.back() == ' ') sv.remove_suffix(1); + int value{}; + auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), value); + if (ec != std::errc{}) + throw std::invalid_argument(error_msg); + return value; + } + + inline long long ParseLongLong(std::string_view sv, const char* error_msg) + { + while (!sv.empty() && sv.front() == ' ') sv.remove_prefix(1); + while (!sv.empty() && sv.back() == ' ') sv.remove_suffix(1); + long long value{}; + auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), value); + if (ec != std::errc{}) + throw std::invalid_argument(error_msg); + return value; + } + + inline double ParseDouble(std::string_view sv, const char* error_msg) + { + while (!sv.empty() && sv.front() == ' ') sv.remove_prefix(1); + while (!sv.empty() && sv.back() == ' ') sv.remove_suffix(1); + // std::from_chars accepts '-' but not '+' by standard; strip leading '+' + if (!sv.empty() && sv.front() == '+') sv.remove_prefix(1); + double value{}; + auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), value); + if (ec != std::errc{}) + throw std::invalid_argument(error_msg); + return value; + } + template - std::string to_string_decimal(const T& t) + inline std::string to_string_decimal(const T& t) { std::string str{ std::to_string(t) }; std::replace(str.begin(), str.end(), ',', '.'); diff --git a/include/edfio/processor/impl/ProcessorHeaderExam.ipp b/include/edfio/processor/impl/ProcessorHeaderExam.ipp index 278db7b..0843cc5 100644 --- a/include/edfio/processor/impl/ProcessorHeaderExam.ipp +++ b/include/edfio/processor/impl/ProcessorHeaderExam.ipp @@ -42,7 +42,7 @@ namespace edfio } header.m_detail.m_recordSize = recordsize; - return std::move(HeaderExam{ std::move(header), std::move(signals) }); + return HeaderExam{ std::move(header), std::move(signals) }; } } diff --git a/include/edfio/processor/impl/ProcessorHeaderGeneral.ipp b/include/edfio/processor/impl/ProcessorHeaderGeneral.ipp index 18c1d01..5e4360a 100644 --- a/include/edfio/processor/impl/ProcessorHeaderGeneral.ipp +++ b/include/edfio/processor/impl/ProcessorHeaderGeneral.ipp @@ -14,7 +14,7 @@ #include "../detail/ProcessorUtils.hpp" #include -#include +#include namespace edfio { @@ -50,22 +50,14 @@ namespace edfio int month = std::get<1>(in.m_startDate); int year = std::get<2>(in.m_startDate); year -= year > 1999 ? 2000 : 1900; - std::ostringstream oss; - oss << std::setw(2) << std::setfill('0') << day << "."; - oss << std::setw(2) << std::setfill('0') << month << "."; - oss << std::setw(2) << std::setfill('0') << year; - out.m_startDate(oss.str()); + out.m_startDate(std::format("{:02d}.{:02d}.{:02d}", day, month, year)); } // Start Time { int hour = std::get<0>(in.m_startTime); int minute = std::get<1>(in.m_startTime); int second = std::get<2>(in.m_startTime); - std::ostringstream oss; - oss << std::setw(2) << std::setfill('0') << hour << "."; - oss << std::setw(2) << std::setfill('0') << minute << "."; - oss << std::setw(2) << std::setfill('0') << second; - out.m_startTime(oss.str()); + out.m_startTime(std::format("{:02d}.{:02d}.{:02d}", hour, minute, second)); } // Header Size { @@ -79,7 +71,7 @@ namespace edfio } else if (IsPlus(in.m_version)) { - out.m_reserved(detail::GetFormatName(in.m_version)); + out.m_reserved(std::string(detail::GetFormatName(in.m_version))); } } // Datarecords in File @@ -130,12 +122,10 @@ namespace edfio // The text 'Startdate' fields.push_back("Startdate"); // The startdate itself in dd-MMM-yyyy format using the English 3-character abbreviations of the month in capitals: dd-MMM-yyyy (MMM = 'JAN' | 'FEV' | ...) - std::ostringstream oss; int day = std::get<0>(in.m_startDate); int month = std::get<1>(in.m_startDate); int year = std::get<2>(in.m_startDate); - oss << std::setw(2) << std::setfill('0') << (day ? day : 1) << "-" << detail::GetStringFromMonth(month) << "-" << (year ? year : 1984); - fields.push_back(oss.str()); + fields.push_back(std::format("{:02d}-{}-{}", day ? day : 1, detail::GetStringFromMonth(month), year ? year : 1984)); // The hospital administration code of the investigation, i.e. EEG number or PSG number. fields.push_back(in.m_detail.m_admincode.empty() ? "X" : in.m_detail.m_admincode); // A code specifying the responsible investigator or technician. @@ -159,6 +149,6 @@ namespace edfio out.m_recording(recording); } } - return std::move(out); + return out; } } diff --git a/include/edfio/processor/impl/ProcessorHeaderGeneralFields.ipp b/include/edfio/processor/impl/ProcessorHeaderGeneralFields.ipp index d64f865..98a0062 100644 --- a/include/edfio/processor/impl/ProcessorHeaderGeneralFields.ipp +++ b/include/edfio/processor/impl/ProcessorHeaderGeneralFields.ipp @@ -14,7 +14,6 @@ #include "../detail/ProcessorUtils.hpp" #include -#include namespace edfio { @@ -76,24 +75,19 @@ namespace edfio { throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); } - try { - int day = std::stoi(startdate); - int month = std::stoi(startdate.substr(3)); - int year = std::stoi(startdate.substr(6)); - - if (day < 1 || day > 31 || month < 1 || month > 12) + int day{}, month{}, year{}; + auto [p1, e1] = std::from_chars(startdate.data(), startdate.data() + 2, day); + auto [p2, e2] = std::from_chars(startdate.data() + 3, startdate.data() + 5, month); + auto [p3, e3] = std::from_chars(startdate.data() + 6, startdate.data() + 8, year); + if (e1 != std::errc{} || e2 != std::errc{} || e3 != std::errc{} + || day < 1 || day > 31 || month < 1 || month > 12) { throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); } year += year > 84 ? 1900 : 2000; - out.m_startDate = std::make_tuple(day, month, year); } - catch (...) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } } // Start Time { @@ -105,33 +99,22 @@ namespace edfio { throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); } - try { - int hour = std::stoi(starttime); - int minute = std::stoi(starttime.substr(3)); - int second = std::stoi(starttime.substr(6)); - - if (hour > 23 || minute > 59 || second > 59) + int hour{}, minute{}, second{}; + auto [p1, e1] = std::from_chars(starttime.data(), starttime.data() + 2, hour); + auto [p2, e2] = std::from_chars(starttime.data() + 3, starttime.data() + 5, minute); + auto [p3, e3] = std::from_chars(starttime.data() + 6, starttime.data() + 8, second); + if (e1 != std::errc{} || e2 != std::errc{} || e3 != std::errc{} + || hour > 23 || minute > 59 || second > 59) { throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); } out.m_startTime = std::make_tuple(hour, minute, second); } - catch (...) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } } // Header Size { - try - { - out.m_headerSize = std::stoi(in.m_headerSize()); - } - catch (...) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } + out.m_headerSize = detail::ParseInt(in.m_headerSize(), detail::GetError(FileErrc::FileContainsFormatErrors)); } // Reserved { @@ -161,14 +144,7 @@ namespace edfio } // Datarecords in File { - try - { - out.m_datarecordsFile = std::stoll(in.m_datarecordsFile()); - } - catch (...) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } + out.m_datarecordsFile = detail::ParseLongLong(in.m_datarecordsFile(), detail::GetError(FileErrc::FileContainsFormatErrors)); if (out.m_datarecordsFile < 0) { throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); @@ -176,16 +152,8 @@ namespace edfio } // Datarecord Duration { - double duration = -1; - try - { - duration = std::stod(in.m_datarecordDuration()); - out.m_datarecordDuration = duration; - } - catch (...) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } + double duration = detail::ParseDouble(in.m_datarecordDuration(), detail::GetError(FileErrc::FileContainsFormatErrors)); + out.m_datarecordDuration = duration; if (duration < 0) { throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); @@ -194,15 +162,7 @@ namespace edfio } // Number of signals { - int signals = 0; - try - { - signals = std::stoi(in.m_totalSignals()); - } - catch (...) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } + int signals = detail::ParseInt(in.m_totalSignals(), detail::GetError(FileErrc::FileContainsFormatErrors)); if (signals <= 0 || (signals * 256 + 256) != out.m_headerSize) { throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); @@ -299,21 +259,17 @@ namespace edfio case 1: // The startdate itself in dd-MMM-yyyy format using the English 3-character abbreviations of the month in capitals: dd-MMM-yyyy (MMM = 'JAN' | 'FEV' | ...) if (str.size() == 11 && str[2] == '-' && str[6] == '-') { - try { - int day = std::stoi(str.substr(0, 2)); - int year = std::stoi(str.substr(7, 4)); - int month = detail::GetMonthFromString(str.substr(3, 3)); - if (month == 0) + int day{}, year{}; + auto [p1, e1] = std::from_chars(str.data(), str.data() + 2, day); + auto [p2, e2] = std::from_chars(str.data() + 7, str.data() + 11, year); + int month = detail::GetMonthFromString(std::string_view{str}.substr(3, 3)); + if (e1 != std::errc{} || e2 != std::errc{} || month == 0) { - throw std::invalid_argument("Invalid Month"); + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); } out.m_startDate = std::make_tuple(day, month, year); } - catch (...) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } } else { @@ -355,7 +311,7 @@ namespace edfio out.m_detail.m_equipment = detail::ReduceString(out.m_detail.m_equipment); out.m_detail.m_recordingAdditional = detail::ReduceString(out.m_detail.m_recordingAdditional); - return std::move(out); + return out; } } diff --git a/include/edfio/processor/impl/ProcessorHeaderSignal.ipp b/include/edfio/processor/impl/ProcessorHeaderSignal.ipp index ef70f5a..aa9efbf 100644 --- a/include/edfio/processor/impl/ProcessorHeaderSignal.ipp +++ b/include/edfio/processor/impl/ProcessorHeaderSignal.ipp @@ -95,7 +95,7 @@ namespace edfio } } - return std::move(out); + return out; } } diff --git a/include/edfio/processor/impl/ProcessorHeaderSignalFields.ipp b/include/edfio/processor/impl/ProcessorHeaderSignalFields.ipp index 4aabdab..703693d 100644 --- a/include/edfio/processor/impl/ProcessorHeaderSignalFields.ipp +++ b/include/edfio/processor/impl/ProcessorHeaderSignalFields.ipp @@ -110,15 +110,7 @@ namespace edfio { auto &signal = signals[idx]; auto& physMin = in[idx].m_physicalMin(); - - try - { - signal.m_physicalMin = std::stod(physMin); - } - catch (...) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } + signal.m_physicalMin = detail::ParseDouble(physMin, detail::GetError(FileErrc::FileContainsFormatErrors)); } } // Physical Maxima @@ -127,15 +119,7 @@ namespace edfio { auto &signal = signals[idx]; auto& physMax = in[idx].m_physicalMax(); - - try - { - signal.m_physicalMax = std::stod(physMax); - } - catch (...) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } + signal.m_physicalMax = detail::ParseDouble(physMax, detail::GetError(FileErrc::FileContainsFormatErrors)); } } // Digital Minima @@ -144,16 +128,7 @@ namespace edfio { auto &signal = signals[idx]; auto& digMin = in[idx].m_digitalMin(); - - int n = 0; - try - { - n = std::stoi(digMin); - } - catch (...) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } + int n = detail::ParseInt(digMin, detail::GetError(FileErrc::FileContainsFormatErrors)); if (signal.m_detail.m_isAnnotation) { @@ -195,16 +170,7 @@ namespace edfio { auto &signal = signals[idx]; auto& digMax = in[idx].m_digitalMax(); - - int n = 0; - try - { - n = std::stoi(digMax); - } - catch (...) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } + int n = detail::ParseInt(digMax, detail::GetError(FileErrc::FileContainsFormatErrors)); if (signal.m_detail.m_isAnnotation) { @@ -268,16 +234,7 @@ namespace edfio { auto &signal = signals[idx]; auto& nrSamples = in[idx].m_samplesInDataRecord(); - - int n = 0; - try - { - n = std::stoi(nrSamples); - } - catch (...) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } + int n = detail::ParseInt(nrSamples, detail::GetError(FileErrc::FileContainsFormatErrors)); if (n < 1) { @@ -323,7 +280,7 @@ namespace edfio signal.m_reserved = detail::ReduceString(signal.m_reserved); } - return std::move(signals); + return signals; } } diff --git a/include/edfio/processor/impl/ProcessorSample.ipp b/include/edfio/processor/impl/ProcessorSample.ipp index 07a9417..9222be0 100644 --- a/include/edfio/processor/impl/ProcessorSample.ipp +++ b/include/edfio/processor/impl/ProcessorSample.ipp @@ -30,7 +30,7 @@ namespace edfio *it++ = tmp; } - return std::move(record); + return record; } } diff --git a/include/edfio/processor/impl/ProcessorSampleRecord.ipp b/include/edfio/processor/impl/ProcessorSampleRecord.ipp index e1a9a31..33f130c 100644 --- a/include/edfio/processor/impl/ProcessorSampleRecord.ipp +++ b/include/edfio/processor/impl/ProcessorSampleRecord.ipp @@ -13,23 +13,31 @@ namespace edfio { template - inline typename ProcessorSampleRecord::ProcType ProcessorSampleRecord::operator()(Record record) + inline typename ProcessorSampleRecord::ProcType + ProcessorSampleRecord::operator()(Record record) { DigiType sample = 0; - size_t idx = 0; - for (unsigned char r : record()) + auto const& bytes = record(); + std::size_t const nbytes = bytes.size(); + + // Assemble bytes (big-endian order as written by ProcessorSample) + for (std::size_t i = 0; i < nbytes; ++i) { - if (r < 0 && idx++) - sample = -1; sample <<= 8; - sample |= r; - idx++; + sample |= static_cast(bytes[i]); } - if (std::is_same::value) + // Sign-extend: if high bit of the MSB is set, the value is negative + if (nbytes > 0 && nbytes < sizeof(DigiType)) + { + unsigned int sign_bit = 1u << (nbytes * 8 - 1); + if (sample & sign_bit) + sample |= ~((1 << (nbytes * 8)) - 1); + } + + if constexpr (std::is_same_v) return sample; return impl::ConvertSample(m_offset, m_scaling, sample); } } - diff --git a/include/edfio/processor/impl/ProcessorTalRecord.ipp b/include/edfio/processor/impl/ProcessorTalRecord.ipp index 6690461..0b844fd 100644 --- a/include/edfio/processor/impl/ProcessorTalRecord.ipp +++ b/include/edfio/processor/impl/ProcessorTalRecord.ipp @@ -45,14 +45,7 @@ namespace edfio if (*it == detail::DURATION_DIV || *it == detail::ANNOTATION_DIV) { std::string tmp(first, it); - try - { - start = std::stod(tmp); - } - catch (...) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsInvalidAnnotations)); - } + start = detail::ParseDouble(tmp, detail::GetError(FileErrc::FileContainsInvalidAnnotations)); onset = false; first = it; @@ -63,14 +56,7 @@ namespace edfio if (*first == detail::DURATION_DIV) { std::string tmp(first + 1, it); - try - { - duration = std::stod(tmp); - } - catch (...) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsInvalidAnnotations)); - } + duration = detail::ParseDouble(tmp, detail::GetError(FileErrc::FileContainsInvalidAnnotations)); first = it; } else if (*first == detail::ANNOTATION_DIV) @@ -84,7 +70,7 @@ namespace edfio annot.m_start = start; annot.m_duration = duration; annot.m_annotation = tmp; - annot.m_dararecord = datarecord; + annot.m_datarecord = datarecord; out.emplace_back(std::move(annot)); } first = it; @@ -93,7 +79,7 @@ namespace edfio } } - return std::move(out); + return out; } } diff --git a/include/edfio/processor/impl/ProcessorTimeStampRecord.ipp b/include/edfio/processor/impl/ProcessorTimeStampRecord.ipp index b6d89b8..42390f6 100644 --- a/include/edfio/processor/impl/ProcessorTimeStampRecord.ipp +++ b/include/edfio/processor/impl/ProcessorTimeStampRecord.ipp @@ -21,7 +21,7 @@ namespace edfio inline TimeStamp edfio::ProcessorTimeStampRecord::operator()(Record record, long long datarecord) { TimeStamp timestamp; - timestamp.m_dararecord = datarecord; + timestamp.m_datarecord = datarecord; auto& value = record(); // TimeStamp MUST start with '+' or '-' @@ -53,7 +53,7 @@ namespace edfio timestamp.m_start = start; } } - return std::move(timestamp); + return timestamp; } } diff --git a/include/edfio/reader/impl/ReaderHeaderExam.ipp b/include/edfio/reader/impl/ReaderHeaderExam.ipp index b49f7c5..2e8e29d 100644 --- a/include/edfio/reader/impl/ReaderHeaderExam.ipp +++ b/include/edfio/reader/impl/ReaderHeaderExam.ipp @@ -30,18 +30,18 @@ namespace edfio auto generalFields = readerGeneral(stream); // Process general fields ProcessorHeaderGeneralFields procGeneralFields; - auto general = std::move(procGeneralFields(std::move(generalFields))); + auto general = procGeneralFields(std::move(generalFields)); // Read signal fields ReaderHeaderSignal readerSignals(general.m_totalSignals); auto signalFields = readerSignals(stream); // Process signal fields ProcessorHeaderSignalFields procSignalFields(general.m_version, general.m_datarecordDuration); - auto signals = std::move(procSignalFields(std::move(signalFields))); + auto signals = procSignalFields(std::move(signalFields)); // Process header exam ProcessorHeaderExam procHeader; - auto header = std::move(procHeader(std::move(general), std::move(signals))); + auto header = procHeader(std::move(general), std::move(signals)); // File size { @@ -59,7 +59,7 @@ namespace edfio } } - return std::move(header); + return header; } } diff --git a/include/edfio/reader/impl/ReaderHeaderGeneral.ipp b/include/edfio/reader/impl/ReaderHeaderGeneral.ipp index 166f115..a27558d 100644 --- a/include/edfio/reader/impl/ReaderHeaderGeneral.ipp +++ b/include/edfio/reader/impl/ReaderHeaderGeneral.ipp @@ -40,11 +40,11 @@ namespace edfio stream >> hdr.m_datarecordDuration; stream >> hdr.m_totalSignals; } - catch (std::exception e) + catch (const std::exception&) { throw std::invalid_argument(detail::GetError(FileErrc::FileReadError)); } - return std::move(hdr); + return hdr; } } diff --git a/include/edfio/reader/impl/ReaderHeaderSignal.ipp b/include/edfio/reader/impl/ReaderHeaderSignal.ipp index 9e5a86f..768e84b 100644 --- a/include/edfio/reader/impl/ReaderHeaderSignal.ipp +++ b/include/edfio/reader/impl/ReaderHeaderSignal.ipp @@ -52,11 +52,11 @@ namespace edfio for (auto &s : signals) stream >> s.m_reserved; } - catch (std::exception e) + catch (const std::exception&) { throw std::invalid_argument(detail::GetError(FileErrc::FileReadError)); } - return std::move(signals); + return signals; } } diff --git a/include/edfio/sink/DataRecordSink.hpp b/include/edfio/sink/DataRecordSink.hpp index b630618..7b0224e 100644 --- a/include/edfio/sink/DataRecordSink.hpp +++ b/include/edfio/sink/DataRecordSink.hpp @@ -18,10 +18,10 @@ namespace edfio { public: - typedef RecordSink::iterator iterator; - typedef iterator const const_iterator; - typedef std::reverse_iterator reverse_iterator; - typedef std::reverse_iterator const_reverse_iterator; + using iterator = RecordSink::iterator; + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; DataRecordSink() = delete; diff --git a/include/edfio/sink/RecordSink.hpp b/include/edfio/sink/RecordSink.hpp index 5b4c049..052e584 100644 --- a/include/edfio/sink/RecordSink.hpp +++ b/include/edfio/sink/RecordSink.hpp @@ -14,24 +14,33 @@ #include #include +#include +#include namespace edfio { class RecordSink : public Sink, Record*, Record&, std::ofstream, std::output_iterator_tag> { + using base_sink = Sink, Record*, Record&, std::ofstream, std::output_iterator_tag>; public: + using typename base_sink::stream_type; + using typename base_sink::value_type; + using typename base_sink::pointer; + using typename base_sink::reference; + using typename base_sink::difference_type; + using typename base_sink::size_type; - class iterator : public sink_type::iterator + class iterator : public base_sink::iterator { - size_type m_offset = -1; // Default is end + std::optional m_offset; // nullopt = end RecordSink *m_context = nullptr; public: // Construction iterator() = default; - iterator(RecordSink *context, size_type offset = -1) + iterator(RecordSink *context, std::optional offset = std::nullopt) : m_offset(offset) , m_context(context) { @@ -48,44 +57,26 @@ namespace edfio { if (!m_context) throw std::invalid_argument("Invalid context"); - m_context->save(m_offset, std::move(value)); + if (!m_offset) + throw std::length_error("Cannot assign to end iterator"); + m_context->save(*m_offset, std::move(value)); return *this; } - // Equality + // Equality (!= auto-generated) bool operator==(const iterator &it) const { return (m_offset == it.m_offset && m_context == it.m_context); } - bool operator!=(const iterator &it) const - { - return !(*this == it); - } - // Relation - bool operator<(const iterator &it) const - { - if (m_context != it.m_context) - throw std::invalid_argument("Iterators incompatible"); - return (m_offset < it.m_offset); - } - bool operator>(const iterator &it) const - { - if (m_context != it.m_context) - throw std::invalid_argument("Iterators incompatible"); - return (m_offset > it.m_offset); - } - bool operator<=(const iterator &it) const - { - if (m_context != it.m_context) - throw std::invalid_argument("Iterators incompatible"); - return (m_offset <= it.m_offset); - } - bool operator>=(const iterator &it) const + // Three-way comparison (<, >, <=, >= auto-generated) + std::strong_ordering operator<=>(const iterator &it) const { if (m_context != it.m_context) throw std::invalid_argument("Iterators incompatible"); - return (m_offset >= it.m_offset); + auto lhs = m_offset.value_or(m_context->size()); + auto rhs = it.m_offset.value_or(it.m_context->size()); + return lhs <=> rhs; } // Pre-increment @@ -93,10 +84,10 @@ namespace edfio { if (!m_context) throw std::invalid_argument("Invalid context"); - if (m_offset == -1) + if (!m_offset) throw std::length_error("Iterator not incrementable"); - if (++m_offset >= m_context->size()) - m_offset = -1; + if (++(*m_offset) >= m_context->size()) + m_offset = std::nullopt; return *this; } // Post-increment @@ -113,12 +104,12 @@ namespace edfio { if (!m_context) throw std::invalid_argument("Invalid context"); - if (m_offset == 0) + if (m_offset && *m_offset == 0) throw std::length_error("Iterator not decrementable"); - if (m_offset == -1) + if (!m_offset) m_offset = m_context->size() - 1; else - m_offset--; + (*m_offset)--; return *this; } // Post-decrement @@ -135,14 +126,14 @@ namespace edfio { if (!m_context) throw std::invalid_argument("Invalid context"); - if (m_offset == -1) + if (!m_offset) throw std::length_error("Iterator not incrementable"); - if (m_offset + off > m_context->size()) + if (*m_offset + off > m_context->size()) throw std::length_error("Iterator + offset out of range"); - if (m_offset + off == m_context->size()) - m_offset = -1; + if (*m_offset + off == m_context->size()) + m_offset = std::nullopt; else - m_offset += off; + *m_offset += off; return *this; } // Addition @@ -159,14 +150,10 @@ namespace edfio { if (!m_context) throw std::invalid_argument("Invalid context"); - if (m_offset == 0) - throw std::length_error("Iterator not decrementable"); - if (m_offset != -1 && m_offset < off) + auto pos = m_offset.value_or(m_context->size()); + if (pos < off) throw std::length_error("Iterator - offset out of range"); - if (m_offset == -1) - m_offset = m_context->size() - off; - else - m_offset -= off; + m_offset = pos - off; return *this; } // Subtraction @@ -184,7 +171,9 @@ namespace edfio throw std::invalid_argument("Invalid context"); if (m_context != it.m_context) throw std::invalid_argument("Iterators incompatible"); - return difference_type(it.m_offset - m_offset); + auto lhs = static_cast(m_offset.value_or(m_context->size())); + auto rhs = static_cast(it.m_offset.value_or(it.m_context->size())); + return lhs - rhs; } // Dereference @@ -204,9 +193,9 @@ namespace edfio } }; - typedef iterator const const_iterator; - typedef std::reverse_iterator reverse_iterator; - typedef std::reverse_iterator const_reverse_iterator; + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; RecordSink() = delete; diff --git a/include/edfio/sink/SignalRecordSink.hpp b/include/edfio/sink/SignalRecordSink.hpp index 9a7cd90..24f097b 100644 --- a/include/edfio/sink/SignalRecordSink.hpp +++ b/include/edfio/sink/SignalRecordSink.hpp @@ -18,10 +18,10 @@ namespace edfio { public: - typedef RecordSink::iterator iterator; - typedef iterator const const_iterator; - typedef std::reverse_iterator reverse_iterator; - typedef std::reverse_iterator const_reverse_iterator; + using iterator = RecordSink::iterator; + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; SignalRecordSink() = delete; @@ -44,7 +44,7 @@ namespace edfio throw std::invalid_argument("Invalid header"); size_type datarecords = (sz - m_headerOffset) / m_datarecordSize; - size_type offset = (sz - m_headerOffset) % m_datarecordSize;; + size_type offset = (sz - m_headerOffset) % m_datarecordSize; m_sinkSize = datarecords; if (offset >= m_signalOffset) @@ -58,7 +58,7 @@ namespace edfio { m_stream.seekp(0, std::ios::end); size_type sz = m_stream.tellp(); - size_type offset = (sz - m_headerOffset) % m_datarecordSize;; + size_type offset = (sz - m_headerOffset) % m_datarecordSize; if (offset < m_signalOffset) throw std::invalid_argument("Invalid signal order"); m_sinkSize++; diff --git a/include/edfio/sink/Sink.hpp b/include/edfio/sink/Sink.hpp index 18ae717..eae8cfb 100644 --- a/include/edfio/sink/Sink.hpp +++ b/include/edfio/sink/Sink.hpp @@ -20,8 +20,15 @@ namespace edfio class Sink : public Device { public: - typedef Sink sink_type; - typedef device_type::iterator iterator; + using device_type = Device; + using sink_type = Sink; + using typename device_type::stream_type; + using typename device_type::value_type; + using typename device_type::pointer; + using typename device_type::reference; + using typename device_type::difference_type; + using typename device_type::size_type; + using iterator = typename device_type::iterator; Sink() = delete; diff --git a/include/edfio/sink/detail/SinkUtils.hpp b/include/edfio/sink/detail/SinkUtils.hpp index fcbba58..547de09 100644 --- a/include/edfio/sink/detail/SinkUtils.hpp +++ b/include/edfio/sink/detail/SinkUtils.hpp @@ -14,8 +14,6 @@ #include "../../header/HeaderGeneral.hpp" #include "../../header/HeaderSignal.hpp" -#include - namespace edfio { @@ -23,23 +21,23 @@ namespace edfio { template - static DataRecordSink CreateDataRecordSink(Stream &stream, const HeaderGeneral &general) + inline DataRecordSink CreateDataRecordSink(Stream &stream, const HeaderGeneral &general) { DataRecordSink::size_type recordSize = general.m_detail.m_recordSize; DataRecordSink::size_type sinkSize = general.m_datarecordsFile; std::streamoff headerSize = general.m_headerSize; - return std::move(DataRecordSink{ stream, recordSize, sinkSize, headerSize }); + return DataRecordSink{ stream, recordSize, sinkSize, headerSize }; } template - static SignalRecordSink CreateSignalRecordSink(Stream &stream, const HeaderGeneral &general, const HeaderSignal &signal) + inline SignalRecordSink CreateSignalRecordSink(Stream &stream, const HeaderGeneral &general, const HeaderSignal &signal) { SignalRecordSink::size_type recordSize = general.m_detail.m_recordSize; SignalRecordSink::size_type sinkSize = general.m_datarecordsFile; std::streamoff headerSize = general.m_headerSize; SignalRecordSink::size_type signalSize = signal.m_samplesInDataRecord * GetSampleBytes(general.m_version); std::streamoff signalOff = signal.m_detail.m_signalOffset; - return std::move(SignalRecordSink{ stream, signalSize, sinkSize, headerSize, recordSize, signalOff }); + return SignalRecordSink{ stream, signalSize, sinkSize, headerSize, recordSize, signalOff }; } } diff --git a/include/edfio/store/DatarecordStore.hpp b/include/edfio/store/DatarecordStore.hpp index 47fa58b..1de3a6c 100644 --- a/include/edfio/store/DatarecordStore.hpp +++ b/include/edfio/store/DatarecordStore.hpp @@ -18,10 +18,10 @@ namespace edfio { public: - typedef RecordStore::iterator iterator; - typedef iterator const const_iterator; - typedef std::reverse_iterator reverse_iterator; //optional - typedef std::reverse_iterator const_reverse_iterator; //optional + using iterator = RecordStore::iterator; + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; DataRecordStore() = delete; @@ -32,9 +32,9 @@ namespace edfio protected: - void load(size_type off) override + void load(size_type off) const override { - if (off < 0 || off >= size()) + if (off >= size()) { throw std::out_of_range("Iterator not dereferenceable"); } diff --git a/include/edfio/store/RecordStore.hpp b/include/edfio/store/RecordStore.hpp index 8a74370..9e2e492 100644 --- a/include/edfio/store/RecordStore.hpp +++ b/include/edfio/store/RecordStore.hpp @@ -15,77 +15,52 @@ #include #include #include +#include namespace edfio { class RecordStore : public Store, Record const*, Record const&, std::ifstream, std::random_access_iterator_tag> { + using base_store = Store, Record const*, Record const&, std::ifstream, std::random_access_iterator_tag>; public: + using typename base_store::stream_type; + using typename base_store::value_type; + using typename base_store::pointer; + using typename base_store::reference; + using typename base_store::difference_type; + using typename base_store::size_type; - class iterator : public store_type::iterator + class iterator : public base_store::iterator { size_type m_offset = 0; // Relative to total of Stores - RecordStore *m_context = nullptr; + const RecordStore *m_context = nullptr; public: // Construction iterator() = default; - iterator(RecordStore *context, size_type offset = 0) + iterator(const RecordStore *context, size_type offset = 0) : m_offset(offset) , m_context(context) { } - iterator(const iterator &it) - : m_offset(it.m_offset) - , m_context(it.m_context) - { - } - - // Assignment - iterator& operator=(const iterator &it) - { - m_offset = it.m_offset; - m_context = it.m_context; - return *this; - } + iterator(const iterator &it) = default; + iterator& operator=(const iterator &it) = default; - // Equality + // Equality (!= auto-generated) bool operator==(const iterator &it) const { return (m_offset == it.m_offset && m_context == it.m_context); } - bool operator!=(const iterator &it) const - { - return !(*this == it); - } - // Relation - bool operator<(const iterator &it) const - { - if (m_context != it.m_context) - throw std::invalid_argument("Iterators incompatible"); - return (m_offset < it.m_offset); - } - bool operator>(const iterator &it) const - { - if (m_context != it.m_context) - throw std::invalid_argument("Iterators incompatible"); - return (m_offset > it.m_offset); - } - bool operator<=(const iterator &it) const + // Three-way comparison (<, >, <=, >= auto-generated) + std::strong_ordering operator<=>(const iterator &it) const { if (m_context != it.m_context) throw std::invalid_argument("Iterators incompatible"); - return (m_offset <= it.m_offset); - } - bool operator>=(const iterator &it) const - { - if (m_context != it.m_context) - throw std::invalid_argument("Iterators incompatible"); - return (m_offset >= it.m_offset); + return m_offset <=> it.m_offset; } // Pre-increment @@ -127,41 +102,45 @@ namespace edfio return tmp; } // Compound addition assignment - iterator& operator+=(size_type off) + iterator& operator+=(difference_type n) { if (!m_context) throw std::invalid_argument("Invalid context"); - if (m_context->size() <= 0 || m_offset + off > m_context->size()) + if (n < 0) return *this -= static_cast(-n); + auto off = static_cast(n); + if (m_offset + off > m_context->size()) throw std::length_error("Iterator + offset out of range"); m_offset += off; return *this; } // Addition - iterator operator+(size_type off) const + iterator operator+(difference_type n) const { if (!m_context) throw std::invalid_argument("Invalid context"); iterator tmp = *this; - tmp += off; + tmp += n; return tmp; } // Compound subtraction assignment - iterator& operator-=(size_type off) + iterator& operator-=(difference_type n) { if (!m_context) throw std::invalid_argument("Invalid context"); - if (m_context->size() <= 0 || m_offset < off) + if (n < 0) return *this += static_cast(-n); + auto off = static_cast(n); + if (m_offset < off) throw std::length_error("Iterator - offset out of range"); m_offset -= off; return *this; } // Subtraction - iterator operator-(size_type off) const + iterator operator-(difference_type n) const { if (!m_context) throw std::invalid_argument("Invalid context"); iterator tmp = *this; - tmp -= off; + tmp -= n; return tmp; } difference_type operator-(iterator it) const @@ -170,7 +149,7 @@ namespace edfio throw std::invalid_argument("Invalid context"); if (m_context != it.m_context) throw std::invalid_argument("Iterators incompatible"); - return difference_type(it.m_offset - m_offset); + return difference_type(m_offset - it.m_offset); } // Dereference @@ -188,19 +167,25 @@ namespace edfio } // Subscripting - reference operator[](size_type off) const + reference operator[](difference_type n) const { if (!m_context) throw std::invalid_argument("Invalid context"); iterator tmp = *this; - tmp += off; + tmp += n; return *tmp; } + + // n + it (required for random_access_iterator) + friend iterator operator+(difference_type n, const iterator& it) + { + return it + n; + } }; - typedef iterator const const_iterator; - typedef std::reverse_iterator reverse_iterator; - typedef std::reverse_iterator const_reverse_iterator; + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; RecordStore() = delete; @@ -213,50 +198,34 @@ namespace edfio { } - iterator begin() + iterator begin() const { return iterator(this); } - const_iterator begin() const - { - return const_iterator(const_cast(this)); - } const_iterator cbegin() const { - return const_iterator(const_cast(this)); + return begin(); } - iterator end() + iterator end() const { return iterator(this, size()); } - const_iterator end() const - { - return const_iterator(const_cast(this), size()); - } const_iterator cend() const { - return const_iterator(const_cast(this), size()); + return end(); } - reverse_iterator rbegin() + reverse_iterator rbegin() const { return reverse_iterator(end()); } - const_reverse_iterator rbegin() const - { - return const_reverse_iterator(end()); - } const_reverse_iterator crbegin() const { return const_reverse_iterator(cend()); } - reverse_iterator rend() + reverse_iterator rend() const { return reverse_iterator(begin()); } - const_reverse_iterator rend() const - { - return const_reverse_iterator(begin()); - } const_reverse_iterator crend() const { return const_reverse_iterator(cbegin()); @@ -269,24 +238,24 @@ namespace edfio } protected: - virtual reference getR(size_type off) + virtual reference getR(size_type off) const { load(off); return m_value; } - virtual pointer getP(size_type off) + virtual pointer getP(size_type off) const { load(off); return &m_value; } - virtual void load(size_type off) = 0; + virtual void load(size_type off) const = 0; size_type m_recordSize; size_type m_storeSize; std::streamoff m_headerOffset; - value_type m_value; + mutable value_type m_value; }; } diff --git a/include/edfio/store/SignalSampleStore.hpp b/include/edfio/store/SignalSampleStore.hpp index afabe80..415968d 100644 --- a/include/edfio/store/SignalSampleStore.hpp +++ b/include/edfio/store/SignalSampleStore.hpp @@ -18,10 +18,10 @@ namespace edfio { public: - typedef RecordStore::iterator iterator; - typedef iterator const const_iterator; - typedef std::reverse_iterator reverse_iterator; //optional - typedef std::reverse_iterator const_reverse_iterator; //optional + using iterator = RecordStore::iterator; + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; SignalSampleStore() = delete; @@ -39,9 +39,9 @@ namespace edfio protected: - void load(size_type off) override + void load(size_type off) const override { - if (off < 0 || off >= size()) + if (off >= size()) { throw std::out_of_range("Iterator not dereferenceable"); } @@ -60,7 +60,7 @@ namespace edfio std::copy(first, first + m_value.Size(), m_value().begin()); } - void readStream(long long newPos) + void readStream(long long newPos) const { if (!m_stream.good()) m_stream.clear(); @@ -77,8 +77,8 @@ namespace edfio size_type m_signalrecordSize; std::streamoff m_signalOffset; // Samples buffer to decrease read requests - value_type m_buffer; - std::streamoff m_bufferPos; + mutable value_type m_buffer; + mutable std::streamoff m_bufferPos; }; } diff --git a/include/edfio/store/SignalrecordStore.hpp b/include/edfio/store/SignalrecordStore.hpp index 042bd28..73d1c62 100644 --- a/include/edfio/store/SignalrecordStore.hpp +++ b/include/edfio/store/SignalrecordStore.hpp @@ -18,10 +18,10 @@ namespace edfio { public: - typedef RecordStore::iterator iterator; - typedef iterator const const_iterator; - typedef std::reverse_iterator reverse_iterator; //optional - typedef std::reverse_iterator const_reverse_iterator; //optional + using iterator = RecordStore::iterator; + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; SignalRecordStore() = delete; @@ -35,9 +35,9 @@ namespace edfio protected: - void load(size_type off) override + void load(size_type off) const override { - if (off < 0 || off >= size()) + if (off >= size()) { throw std::out_of_range("Iterator not dereferenceable"); } diff --git a/include/edfio/store/Store.hpp b/include/edfio/store/Store.hpp index 08eda9b..4943164 100644 --- a/include/edfio/store/Store.hpp +++ b/include/edfio/store/Store.hpp @@ -20,9 +20,16 @@ namespace edfio class Store : public Device { public: - typedef Store store_type; - typedef device_type::iterator iterator; - typedef iterator const const_iterator; + using device_type = Device; + using store_type = Store; + using typename device_type::stream_type; + using typename device_type::value_type; + using typename device_type::pointer; + using typename device_type::reference; + using typename device_type::difference_type; + using typename device_type::size_type; + using iterator = typename device_type::iterator; + using const_iterator = iterator; Store() = delete; diff --git a/include/edfio/store/TalStore.hpp b/include/edfio/store/TalStore.hpp index b6c9a5a..7e2fe21 100644 --- a/include/edfio/store/TalStore.hpp +++ b/include/edfio/store/TalStore.hpp @@ -12,7 +12,6 @@ #include "Store.hpp" #include "../core/Record.hpp" -#include #include namespace edfio @@ -24,22 +23,29 @@ namespace edfio // and dereferences a TAL class TalStore : public Store::VectorType, Record::VectorType const*, Record::VectorType const&, const Record, std::bidirectional_iterator_tag> { + using base_store = Store::VectorType, Record::VectorType const*, Record::VectorType const&, const Record, std::bidirectional_iterator_tag>; public: + using typename base_store::stream_type; + using typename base_store::value_type; + using typename base_store::pointer; + using typename base_store::reference; + using typename base_store::difference_type; + using typename base_store::size_type; - class iterator : public store_type::iterator + class iterator : public base_store::iterator { friend class TalStore; size_type m_offset = 0; // Absolute position in current Store - TalStore *m_context = nullptr; + const TalStore *m_context = nullptr; public: // Construction iterator() = default; protected: - iterator(TalStore *context, size_type off) - : m_context(context) - , m_offset(off) + iterator(const TalStore *context, size_type off) + : m_offset(off) + , m_context(context) { if (m_offset == 0) ++*this; @@ -47,8 +53,8 @@ namespace edfio public: iterator(const iterator &it) - : m_context(it.m_context) - , m_offset(it.m_offset) + : m_offset(it.m_offset) + , m_context(it.m_context) { } @@ -60,15 +66,11 @@ namespace edfio return *this; } - // Equality + // Equality (!= auto-generated) bool operator==(const iterator &it) const { return (m_offset == it.m_offset && m_context == it.m_context); } - bool operator!=(const iterator &it) const - { - return !(*this == it); - } // Pre-increment iterator& operator++() @@ -122,9 +124,9 @@ namespace edfio } }; - typedef iterator const const_iterator; - typedef std::reverse_iterator reverse_iterator; - typedef std::reverse_iterator const_reverse_iterator; + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; TalStore() = delete; @@ -133,67 +135,51 @@ namespace edfio { } - iterator begin() + iterator begin() const { return iterator(this, 0); } - const_iterator begin() const - { - return const_iterator(const_cast(this), 0); - } const_iterator cbegin() const { - return const_iterator(const_cast(this), 0); + return begin(); } - iterator end() + iterator end() const { return iterator(this, m_stream.Size()); } - const_iterator end() const - { - return const_iterator(const_cast(this), m_stream.Size()); - } const_iterator cend() const { - return const_iterator(const_cast(this), m_stream.Size()); + return end(); } - reverse_iterator rbegin() + reverse_iterator rbegin() const { return reverse_iterator(end()); } - const_reverse_iterator rbegin() const - { - return const_reverse_iterator(end()); - } const_reverse_iterator crbegin() const { return const_reverse_iterator(cend()); } - reverse_iterator rend() + reverse_iterator rend() const { return reverse_iterator(begin()); } - const_reverse_iterator rend() const - { - return const_reverse_iterator(begin()); - } const_reverse_iterator crend() const { return const_reverse_iterator(cbegin()); } protected: - reference getR() + reference getR() const { return m_value; } - pointer getP() + pointer getP() const { return &m_value; } - size_type next(size_type off) + size_type next(size_type off) const { if (off >= m_stream().size()) throw std::length_error("Iterator not incrementable"); @@ -225,39 +211,31 @@ namespace edfio return off; } - size_type prev(size_type off) + size_type prev(size_type off) const { - if (off <= 0) + if (off == 0) throw std::length_error("Iterator not decrementable"); - if (off > 0) - { - auto first = m_stream().rend() + off; - auto last = m_stream().rend(); + auto const& data = m_stream(); - while (*first == 0 && first != last) - { - first++; - off--; - } + // Walk backward from position (off - 1), skipping zeros + size_type pos = off; + while (pos > 0 && data[pos - 1] == 0) + --pos; - if (first != last) - { - size_type offOld = off; - for (auto it = first; *it != 0 && it != last; it++) - { - off--; - } - if (offOld != off) - { - m_value.assign(first + offOld, first + off); - } - } - } - return off; + if (pos == 0) + throw std::length_error("Iterator not decrementable"); + + // Find the start of the previous non-zero TAL + size_type end_of_tal = pos; + while (pos > 0 && data[pos - 1] != 0) + --pos; + + m_value.assign(data.begin() + pos, data.begin() + end_of_tal); + return pos; } - value_type m_value; + mutable value_type m_value; }; } diff --git a/include/edfio/store/TimeStampStore.hpp b/include/edfio/store/TimeStampStore.hpp index fae7a71..fbd765b 100644 --- a/include/edfio/store/TimeStampStore.hpp +++ b/include/edfio/store/TimeStampStore.hpp @@ -18,10 +18,10 @@ namespace edfio { public: - typedef RecordStore::iterator iterator; - typedef iterator const const_iterator; - typedef std::reverse_iterator reverse_iterator; //optional - typedef std::reverse_iterator const_reverse_iterator; //optional + using iterator = RecordStore::iterator; + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; TimeStampStore() = delete; @@ -35,9 +35,9 @@ namespace edfio protected: - void load(size_type off) override + void load(size_type off) const override { - if (off < 0 || off >= size()) + if (off >= size()) { throw std::out_of_range("Iterator not dereferenceable"); } diff --git a/include/edfio/store/detail/StoreUtils.hpp b/include/edfio/store/detail/StoreUtils.hpp index 2569541..6a00592 100644 --- a/include/edfio/store/detail/StoreUtils.hpp +++ b/include/edfio/store/detail/StoreUtils.hpp @@ -25,27 +25,27 @@ namespace edfio { template - static DataRecordStore CreateDataRecordStore(Stream &stream, const HeaderGeneral &general) + inline DataRecordStore CreateDataRecordStore(Stream &stream, const HeaderGeneral &general) { DataRecordStore::size_type recordSize = general.m_detail.m_recordSize; DataRecordStore::size_type storeSize = general.m_datarecordsFile; std::streamoff headerSize = general.m_headerSize; - return std::move(DataRecordStore{ stream, recordSize, storeSize, headerSize }); + return DataRecordStore{ stream, recordSize, storeSize, headerSize }; } template - static SignalRecordStore CreateSignalRecordStore(Stream &stream, const HeaderGeneral &general, const HeaderSignal &signal) + inline SignalRecordStore CreateSignalRecordStore(Stream &stream, const HeaderGeneral &general, const HeaderSignal &signal) { SignalRecordStore::size_type recordSize = general.m_detail.m_recordSize; SignalRecordStore::size_type storeSize = general.m_datarecordsFile; std::streamoff headerSize = general.m_headerSize; SignalRecordStore::size_type signalSize = signal.m_samplesInDataRecord * GetSampleBytes(general.m_version); std::streamoff signalOff = signal.m_detail.m_signalOffset; - return std::move(SignalRecordStore{ stream, signalSize, storeSize, headerSize, recordSize, signalOff }); + return SignalRecordStore{ stream, signalSize, storeSize, headerSize, recordSize, signalOff }; } template - static SignalSampleStore CreateSignalSampleStore(Stream &stream, const HeaderGeneral &general, const HeaderSignal &signal) + inline SignalSampleStore CreateSignalSampleStore(Stream &stream, const HeaderGeneral &general, const HeaderSignal &signal) { SignalSampleStore::size_type recordSize = general.m_detail.m_recordSize; SignalSampleStore::size_type storeSize = general.m_datarecordsFile * signal.m_samplesInDataRecord; @@ -53,18 +53,18 @@ namespace edfio SignalSampleStore::size_type sampleSize = GetSampleBytes(general.m_version); SignalSampleStore::size_type signalSize = signal.m_samplesInDataRecord; std::streamoff signalOff = signal.m_detail.m_signalOffset; - return std::move(SignalSampleStore{ stream, sampleSize, storeSize, headerSize, recordSize, signalSize, signalOff }); + return SignalSampleStore{ stream, sampleSize, storeSize, headerSize, recordSize, signalSize, signalOff }; } template - static TimeStampStore CreateTimeStampStore(Stream &stream, const HeaderGeneral &general, const HeaderSignal &signal) + inline TimeStampStore CreateTimeStampStore(Stream &stream, const HeaderGeneral &general, const HeaderSignal &signal) { TimeStampStore::size_type recordSize = general.m_detail.m_recordSize; TimeStampStore::size_type storeSize = general.m_datarecordsFile; std::streamoff headerSize = general.m_headerSize; TimeStampStore::size_type signalSize = signal.m_samplesInDataRecord * GetSampleBytes(general.m_version); std::streamoff signalOff = signal.m_detail.m_signalOffset; - return std::move(TimeStampStore{ stream, signalSize, storeSize, headerSize, recordSize, signalOff }); + return TimeStampStore{ stream, signalSize, storeSize, headerSize, recordSize, signalOff }; } } diff --git a/include/edfio/writer/impl/WriterHeaderExam.ipp b/include/edfio/writer/impl/WriterHeaderExam.ipp index 62031cc..5c5c41b 100644 --- a/include/edfio/writer/impl/WriterHeaderExam.ipp +++ b/include/edfio/writer/impl/WriterHeaderExam.ipp @@ -25,16 +25,16 @@ namespace edfio inline void WriterHeaderExam::operator ()(Stream &stream, HeaderExam &input) { // Process header general - auto general = std::move(ProcessorHeaderGeneral{}(input.m_general)); + auto general = ProcessorHeaderGeneral{}(input.m_general); // Process signal fields - auto signals = std::move(ProcessorHeaderSignal{}(input.m_signals)); + auto signals = ProcessorHeaderSignal{}(input.m_signals); // Write general - WriterHeaderGeneral{}(stream, std::move(general)); + WriterHeaderGeneral{}(stream, general); // Write signals - WriterHeaderSignals{}(stream, std::move(signals)); + WriterHeaderSignals{}(stream, signals); } } diff --git a/include/edfio/writer/impl/WriterHeaderGeneral.ipp b/include/edfio/writer/impl/WriterHeaderGeneral.ipp index 4e1bfa3..e6cf556 100644 --- a/include/edfio/writer/impl/WriterHeaderGeneral.ipp +++ b/include/edfio/writer/impl/WriterHeaderGeneral.ipp @@ -41,7 +41,7 @@ namespace edfio stream << hdr.m_datarecordDuration; stream << hdr.m_totalSignals; } - catch (std::exception e) + catch (const std::exception&) { throw std::invalid_argument(detail::GetError(FileErrc::FileWriteError)); } diff --git a/include/edfio/writer/impl/WriterHeaderSignals.ipp b/include/edfio/writer/impl/WriterHeaderSignals.ipp index 425e448..f60ef5b 100644 --- a/include/edfio/writer/impl/WriterHeaderSignals.ipp +++ b/include/edfio/writer/impl/WriterHeaderSignals.ipp @@ -49,7 +49,7 @@ namespace edfio for (auto &s : signals) stream << s.m_reserved; } - catch (std::exception e) + catch (const std::exception&) { throw std::invalid_argument(detail::GetError(FileErrc::FileWriteError)); } diff --git a/include/edfio/writer/impl/WriterRecord.ipp b/include/edfio/writer/impl/WriterRecord.ipp index 7bf425a..f7d6ffd 100644 --- a/include/edfio/writer/impl/WriterRecord.ipp +++ b/include/edfio/writer/impl/WriterRecord.ipp @@ -26,7 +26,7 @@ namespace edfio { stream << record; } - catch (std::exception e) + catch (const std::exception&) { throw std::invalid_argument(detail::GetError(FileErrc::FileWriteError)); } diff --git a/test_generator_2.edf b/test_generator_2.edf new file mode 100644 index 0000000000000000000000000000000000000000..68a678767ab8fdae3c3caa606572f30970396f67 GIT binary patch literal 2704528 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

6#3I+Y$l*hg*26 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⪚Kl!te<#)3!Oms5?x5$bvUadOH|oCRj3>q`7lu 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 VN@bMFD3wtvb51H#{`OOu{{@4eo +#include +#include +#include + +using namespace edfio; + +// Phase 4: Verify ranges compliance at compile time +static_assert(std::random_access_iterator); +static_assert(std::random_access_iterator); +static_assert(std::bidirectional_iterator); +static_assert(std::ranges::random_access_range); +static_assert(std::ranges::random_access_range); +static_assert(std::ranges::bidirectional_range); + +TEST_CASE("RecordStore iterator subtraction and arithmetic (multi-record file)") { + std::ifstream stream("test_generator_2.edf", std::ios::binary); + REQUIRE(stream.is_open()); + ReaderHeaderExam reader; + auto header = reader(stream); + auto store = detail::CreateDataRecordStore(stream, header.m_general); + REQUIRE(store.size() == 600); + + auto a = store.begin(); + auto b = store.begin() + 100; + auto c = store.end(); + + CHECK((b - a) == 100); + CHECK((a - b) == -100); + CHECK((c - a) == 600); + CHECK((a - c) == -600); +} + +TEST_CASE("RecordStore iterator n + it and negative offsets") { + std::ifstream stream("test_generator_2.edf", std::ios::binary); + REQUIRE(stream.is_open()); + ReaderHeaderExam reader; + auto header = reader(stream); + auto store = detail::CreateDataRecordStore(stream, header.m_general); + REQUIRE(store.size() >= 10); + + // n + it form + auto it = store.begin(); + auto it2 = 5 + it; + CHECK((it2 - store.begin()) == 5); + + // Negative offset via += + auto it3 = store.begin() + 10; + it3 += -3; + CHECK((it3 - store.begin()) == 7); + + // Negative offset via -= + auto it4 = store.begin() + 10; + it4 -= -2; // subtracting negative = adding + CHECK((it4 - store.begin()) == 12); +} + +TEST_CASE("DataRecordStore iteration works") { + std::ifstream stream("test_generator_2.edf", std::ios::binary); + REQUIRE(stream.is_open()); + ReaderHeaderExam reader; + auto header = reader(stream); + auto store = detail::CreateDataRecordStore(stream, header.m_general); + + CHECK(store.size() == static_cast(header.m_general.m_datarecordsFile)); + CHECK(store.size() == 600); + + // Iterate through first 10 records + size_t count = 0; + for (auto it = store.begin(); it != store.begin() + 10; ++it) { + auto& rec = *it; + CHECK(rec.Size() > 0); + ++count; + } + CHECK(count == 10); +} + +TEST_CASE("RecordStore iterator comparisons via spaceship") { + std::ifstream stream("test_generator_2.edf", std::ios::binary); + REQUIRE(stream.is_open()); + ReaderHeaderExam reader; + auto header = reader(stream); + auto store = detail::CreateDataRecordStore(stream, header.m_general); + REQUIRE(store.size() >= 10); + + auto a = store.begin(); + auto b = store.begin() + 5; + auto c = store.end(); + CHECK(a < b); + CHECK(b < c); + CHECK(b > a); + CHECK(c > b); + CHECK(a <= a); + CHECK(a <= b); + CHECK(c >= b); + CHECK(a == a); + CHECK(a != b); +} + +TEST_CASE("RecordStore iterator subscript operator") { + std::ifstream stream("test_generator_2.edf", std::ios::binary); + REQUIRE(stream.is_open()); + ReaderHeaderExam reader; + auto header = reader(stream); + auto store = detail::CreateDataRecordStore(stream, header.m_general); + REQUIRE(store.size() >= 3); + + auto it = store.begin(); + auto& rec0 = it[0]; + auto& rec2 = it[2]; + CHECK(rec0.Size() > 0); + CHECK(rec2.Size() > 0); +} + +TEST_CASE("SignalRecordStore iteration works") { + std::ifstream stream("test_generator_2.edf", std::ios::binary); + REQUIRE(stream.is_open()); + ReaderHeaderExam reader; + auto header = reader(stream); + REQUIRE(header.m_signals.size() > 0); + + auto store = detail::CreateSignalRecordStore(stream, header.m_general, header.m_signals[0]); + CHECK(store.size() == static_cast(header.m_general.m_datarecordsFile)); + + // Iterate first 5 records + size_t count = 0; + for (auto it = store.begin(); it != store.begin() + 5; ++it) { + auto& rec = *it; + CHECK(rec.Size() > 0); + ++count; + } + CHECK(count == 5); +} + +TEST_CASE("DataRecordStore satisfies ranges::random_access_range") { + std::ifstream stream("test_generator_2.edf", std::ios::binary); + REQUIRE(stream.is_open()); + ReaderHeaderExam reader; + auto header = reader(stream); + auto store = detail::CreateDataRecordStore(stream, header.m_general); + + // Use std::ranges algorithms + CHECK(std::ranges::distance(store.begin(), store.end()) == 600); + + // Range-based for loop (proves range concept works) + size_t count = 0; + for ([[maybe_unused]] auto& rec : store) { + ++count; + if (count >= 3) break; + } + CHECK(count == 3); +} + +TEST_CASE("Const iteration works without const_cast issues") { + std::ifstream stream("test_generator_2.edf", std::ios::binary); + REQUIRE(stream.is_open()); + ReaderHeaderExam reader; + auto header = reader(stream); + auto store = detail::CreateDataRecordStore(stream, header.m_general); + + // Call const begin/end + const auto& cstore = store; + auto it = cstore.begin(); + auto end = cstore.end(); + CHECK(it != end); + CHECK((end - it) == 600); + auto& rec = *it; + CHECK(rec.Size() > 0); +} diff --git a/tests/test_processor_sample.cpp b/tests/test_processor_sample.cpp new file mode 100644 index 0000000..7f2430c --- /dev/null +++ b/tests/test_processor_sample.cpp @@ -0,0 +1,76 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include + +using namespace edfio; + +TEST_CASE("ProcessorSampleRecord sign-extends negative 2-byte samples") { + // -100 as signed 16-bit = 0xFF9C + Record rec(2); + rec()[0] = static_cast(0xFF); + rec()[1] = static_cast(0x9C); + ProcessorSampleRecord proc(0.0, 1.0); + auto result = proc(rec); + CHECK(result == -100); +} + +TEST_CASE("ProcessorSampleRecord positive 2-byte samples") { + Record rec(2); + rec()[0] = static_cast(0x00); + rec()[1] = static_cast(0x64); + ProcessorSampleRecord proc(0.0, 1.0); + auto result = proc(rec); + CHECK(result == 100); +} + +TEST_CASE("ProcessorSampleRecord 3-byte BDF negative sample") { + // -1000 as signed 24-bit = 0xFFFC18 + Record rec(3); + rec()[0] = static_cast(0xFF); + rec()[1] = static_cast(0xFC); + rec()[2] = static_cast(0x18); + ProcessorSampleRecord proc(0.0, 1.0); + auto result = proc(rec); + CHECK(result == -1000); +} + +TEST_CASE("ProcessorSampleRecord 3-byte BDF positive sample") { + // 1000 as signed 24-bit = 0x0003E8 + Record rec(3); + rec()[0] = static_cast(0x00); + rec()[1] = static_cast(0x03); + rec()[2] = static_cast(0xE8); + ProcessorSampleRecord proc(0.0, 1.0); + auto result = proc(rec); + CHECK(result == 1000); +} + +TEST_CASE("ProcessorSampleRecord 2-byte max positive") { + // 32767 = 0x7FFF + Record rec(2); + rec()[0] = static_cast(0x7F); + rec()[1] = static_cast(0xFF); + ProcessorSampleRecord proc(0.0, 1.0); + auto result = proc(rec); + CHECK(result == 32767); +} + +TEST_CASE("ProcessorSampleRecord 2-byte min negative") { + // -32768 = 0x8000 + Record rec(2); + rec()[0] = static_cast(0x80); + rec()[1] = static_cast(0x00); + ProcessorSampleRecord proc(0.0, 1.0); + auto result = proc(rec); + CHECK(result == -32768); +} + +TEST_CASE("ProcessorSampleRecord 2-byte minus one") { + // -1 = 0xFFFF + Record rec(2); + rec()[0] = static_cast(0xFF); + rec()[1] = static_cast(0xFF); + ProcessorSampleRecord proc(0.0, 1.0); + auto result = proc(rec); + CHECK(result == -1); +} diff --git a/tests/test_processor_utils.cpp b/tests/test_processor_utils.cpp new file mode 100644 index 0000000..040f839 --- /dev/null +++ b/tests/test_processor_utils.cpp @@ -0,0 +1,93 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include + +TEST_CASE("ReduceString trims and collapses spaces") { + using edfio::detail::ReduceString; + CHECK(ReduceString(" hello world ") == "hello world"); + CHECK(ReduceString(" ") == ""); + CHECK(ReduceString("no_change") == "no_change"); + CHECK(ReduceString(" a b c ") == "a b c"); + CHECK(ReduceString("") == ""); + CHECK(ReduceString("single") == "single"); + CHECK(ReduceString(" leading") == "leading"); + CHECK(ReduceString("trailing ") == "trailing"); +} + +TEST_CASE("GetStringFromMonth handles boundaries") { + CHECK(edfio::detail::GetStringFromMonth(0) == "JAN"); // was UB, now returns default + CHECK(edfio::detail::GetStringFromMonth(1) == "JAN"); + CHECK(edfio::detail::GetStringFromMonth(6) == "JUN"); + CHECK(edfio::detail::GetStringFromMonth(12) == "DEC"); + CHECK(edfio::detail::GetStringFromMonth(13) == "JAN"); // out of range, returns default +} + +TEST_CASE("GetMonthFromString returns correct values") { + CHECK(edfio::detail::GetMonthFromString("JAN") == 1); + CHECK(edfio::detail::GetMonthFromString("DEC") == 12); + CHECK(edfio::detail::GetMonthFromString("XXX") == 0); +} + +TEST_CASE("GetFormatName returns correct strings") { + CHECK(edfio::detail::GetFormatName(edfio::DataFormat::Edf) == "EDF"); + CHECK(edfio::detail::GetFormatName(edfio::DataFormat::BdfPlusD) == "BDF+D"); + CHECK(edfio::detail::GetFormatName(edfio::DataFormat::Invalid) == ""); +} + +TEST_CASE("DataFormat constexpr functions") { + static_assert(edfio::IsEdf(edfio::DataFormat::Edf)); + static_assert(!edfio::IsBdf(edfio::DataFormat::Edf)); + static_assert(edfio::IsBdf(edfio::DataFormat::Bdf)); + static_assert(edfio::IsPlus(edfio::DataFormat::EdfPlusC)); + static_assert(!edfio::IsPlus(edfio::DataFormat::Edf)); + static_assert(edfio::GetSampleBytes(edfio::DataFormat::Edf) == 2); + static_assert(edfio::GetSampleBytes(edfio::DataFormat::Bdf) == 3); + CHECK(true); +} + +TEST_CASE("GetMonthFromString accepts string_view") { + using namespace std::string_view_literals; + CHECK(edfio::detail::GetMonthFromString("FEB"sv) == 2); + CHECK(edfio::detail::GetMonthFromString("NOV"sv) == 11); +} + +TEST_CASE("GetFormatName returns string_view") { + auto name = edfio::detail::GetFormatName(edfio::DataFormat::EdfPlusC); + static_assert(std::is_same_v); + CHECK(name == "EDF+C"); +} + +TEST_CASE("ParseInt parses integers and trims spaces") { + using edfio::detail::ParseInt; + CHECK(ParseInt("42", "err") == 42); + CHECK(ParseInt(" 42 ", "err") == 42); + CHECK(ParseInt("-7", "err") == -7); + CHECK(ParseInt("0", "err") == 0); + CHECK_THROWS_AS(ParseInt("", "bad"), std::invalid_argument); + CHECK_THROWS_AS(ParseInt("abc", "bad"), std::invalid_argument); + CHECK_THROWS_AS(ParseInt(" ", "bad"), std::invalid_argument); +} + +TEST_CASE("ParseLongLong parses large integers") { + using edfio::detail::ParseLongLong; + CHECK(ParseLongLong("600", "err") == 600LL); + CHECK(ParseLongLong(" 12345678901 ", "err") == 12345678901LL); + CHECK_THROWS_AS(ParseLongLong("xyz", "bad"), std::invalid_argument); +} + +TEST_CASE("ParseDouble parses floating-point numbers") { + using edfio::detail::ParseDouble; + CHECK(ParseDouble("3.14", "err") == doctest::Approx(3.14)); + CHECK(ParseDouble(" 1.0 ", "err") == doctest::Approx(1.0)); + CHECK(ParseDouble("-2.5", "err") == doctest::Approx(-2.5)); + CHECK(ParseDouble("+0.001", "err") == doctest::Approx(0.001)); + CHECK_THROWS_AS(ParseDouble("abc", "bad"), std::invalid_argument); +} + +TEST_CASE("GetError returns non-null for all error codes") { + CHECK(edfio::detail::GetError(edfio::FileErrc::FileDoesNotOpen) != nullptr); + CHECK(edfio::detail::GetError(edfio::FileErrc::FileNotOpened) != nullptr); + CHECK(edfio::detail::GetError(edfio::FileErrc::FileReadError) != nullptr); + CHECK(edfio::detail::GetError(edfio::FileErrc::FileContainsFormatErrors) != nullptr); + CHECK(edfio::detail::GetError(edfio::FileErrc::FileWriteError) != nullptr); +} diff --git a/tests/test_reader.cpp b/tests/test_reader.cpp new file mode 100644 index 0000000..ff29571 --- /dev/null +++ b/tests/test_reader.cpp @@ -0,0 +1,41 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include +#include + +using namespace edfio; + +TEST_CASE("Read Calib5.edf header successfully") { + std::ifstream stream("Calib5.edf", std::ios::binary); + REQUIRE(stream.is_open()); + ReaderHeaderExam reader; + auto header = reader(stream); + + CHECK(header.m_general.m_totalSignals > 0); + CHECK(header.m_general.m_datarecordsFile > 0); + CHECK(header.m_general.m_datarecordDuration > 0); + CHECK(header.m_general.m_headerSize > 0); + CHECK(header.m_signals.size() == static_cast(header.m_general.m_totalSignals)); +} + +TEST_CASE("Calib5.edf is plain EDF format") { + std::ifstream stream("Calib5.edf", std::ios::binary); + REQUIRE(stream.is_open()); + ReaderHeaderExam reader; + auto header = reader(stream); + + CHECK(IsEdf(header.m_general.m_version)); + CHECK_FALSE(IsBdf(header.m_general.m_version)); +} + +TEST_CASE("Calib5.edf signal headers are valid") { + std::ifstream stream("Calib5.edf", std::ios::binary); + REQUIRE(stream.is_open()); + ReaderHeaderExam reader; + auto header = reader(stream); + + for (auto const& sig : header.m_signals) { + CHECK(sig.m_samplesInDataRecord > 0); + CHECK(sig.m_digitalMax > sig.m_digitalMin); + } +} diff --git a/tests/test_record.cpp b/tests/test_record.cpp new file mode 100644 index 0000000..ebf2ca0 --- /dev/null +++ b/tests/test_record.cpp @@ -0,0 +1,45 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include + +TEST_CASE("Record is assignable") { + edfio::Record a(10); + a()[0] = 'X'; + edfio::Record b(10); + b = a; // Must compile and work + CHECK(b()[0] == 'X'); + CHECK(b.Size() == 10); +} + +TEST_CASE("Record move-assignment works") { + edfio::Record a(5); + a()[0] = 42; + edfio::Record b(5); + b = std::move(a); + CHECK(b()[0] == 42); +} + +TEST_CASE("Record concatenation") { + edfio::Record a(3); + edfio::Record b(2); + a()[0] = 1; a()[1] = 2; a()[2] = 3; + b()[0] = 4; b()[1] = 5; + auto c = a + b; + CHECK(c.Size() == 5); + CHECK(c()[3] == 4); +} + +TEST_CASE("Record Size derives from vector") { + edfio::Record r(7); + CHECK(r.Size() == 7); + CHECK(r.Size() == r().size()); +} + +TEST_CASE("Record copy construction") { + edfio::Record a(3); + a()[0] = 'A'; a()[1] = 'B'; a()[2] = 'C'; + edfio::Record b(a); + CHECK(b.Size() == 3); + CHECK(b()[0] == 'A'); + CHECK(b()[2] == 'C'); +} diff --git a/tests/test_writer.cpp b/tests/test_writer.cpp new file mode 100644 index 0000000..ea6c814 --- /dev/null +++ b/tests/test_writer.cpp @@ -0,0 +1,48 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include +#include +#include + +using namespace edfio; + +TEST_CASE("Write and read back header round-trip") { + // Read original + std::ifstream instream("Calib5.edf", std::ios::binary); + REQUIRE(instream.is_open()); + ReaderHeaderExam reader; + auto header = reader(instream); + instream.close(); + + // Write to temp file + const char* tmpfile = "test_roundtrip.edf"; + { + std::ofstream outstream(tmpfile, std::ios::binary); + REQUIRE(outstream.is_open()); + WriterHeaderExam writer; + writer(outstream, header); + + // Also write data records + std::ifstream instream2("Calib5.edf", std::ios::binary); + auto store = detail::CreateDataRecordStore(instream2, header.m_general); + auto sink = detail::CreateDataRecordSink(outstream, header.m_general); + auto sink_it = sink.begin(); + for (auto it = store.begin(); it != store.end(); ++it) { + *sink_it = *it; + ++sink_it; + } + } + + // Read back + std::ifstream checkstream(tmpfile, std::ios::binary); + REQUIRE(checkstream.is_open()); + auto header2 = reader(checkstream); + + CHECK(header2.m_general.m_totalSignals == header.m_general.m_totalSignals); + CHECK(header2.m_general.m_datarecordsFile == header.m_general.m_datarecordsFile); + CHECK(header2.m_general.m_datarecordDuration == doctest::Approx(header.m_general.m_datarecordDuration)); + CHECK(header2.m_signals.size() == header.m_signals.size()); + + checkstream.close(); + std::remove(tmpfile); +} diff --git a/third_party/doctest/doctest.h b/third_party/doctest/doctest.h new file mode 100644 index 0000000..5c754cd --- /dev/null +++ b/third_party/doctest/doctest.h @@ -0,0 +1,7106 @@ +// ====================================================================== lgtm [cpp/missing-header-guard] +// == DO NOT MODIFY THIS FILE BY HAND - IT IS AUTO GENERATED BY CMAKE! == +// ====================================================================== +// +// doctest.h - the lightest feature-rich C++ single-header testing framework for unit tests and TDD +// +// Copyright (c) 2016-2023 Viktor Kirilov +// +// Distributed under the MIT Software License +// See accompanying file LICENSE.txt or copy at +// https://opensource.org/licenses/MIT +// +// The documentation can be found at the library's page: +// https://github.com/doctest/doctest/blob/master/doc/markdown/readme.md +// +// ================================================================================================= +// ================================================================================================= +// ================================================================================================= +// +// The library is heavily influenced by Catch - https://github.com/catchorg/Catch2 +// which uses the Boost Software License - Version 1.0 +// see here - https://github.com/catchorg/Catch2/blob/master/LICENSE.txt +// +// The concept of subcases (sections in Catch) and expression decomposition are from there. +// Some parts of the code are taken directly: +// - stringification - the detection of "ostream& operator<<(ostream&, const T&)" and StringMaker<> +// - the Approx() helper class for floating point comparison +// - colors in the console +// - breaking into a debugger +// - signal / SEH handling +// - timer +// - XmlWriter class - thanks to Phil Nash for allowing the direct reuse (AKA copy/paste) +// +// The expression decomposing templates are taken from lest - https://github.com/martinmoene/lest +// which uses the Boost Software License - Version 1.0 +// see here - https://github.com/martinmoene/lest/blob/master/LICENSE.txt +// +// ================================================================================================= +// ================================================================================================= +// ================================================================================================= + +#ifndef DOCTEST_LIBRARY_INCLUDED +#define DOCTEST_LIBRARY_INCLUDED + +// ================================================================================================= +// == VERSION ====================================================================================== +// ================================================================================================= + +#define DOCTEST_VERSION_MAJOR 2 +#define DOCTEST_VERSION_MINOR 4 +#define DOCTEST_VERSION_PATCH 11 + +// util we need here +#define DOCTEST_TOSTR_IMPL(x) #x +#define DOCTEST_TOSTR(x) DOCTEST_TOSTR_IMPL(x) + +#define DOCTEST_VERSION_STR \ + DOCTEST_TOSTR(DOCTEST_VERSION_MAJOR) "." \ + DOCTEST_TOSTR(DOCTEST_VERSION_MINOR) "." \ + DOCTEST_TOSTR(DOCTEST_VERSION_PATCH) + +#define DOCTEST_VERSION \ + (DOCTEST_VERSION_MAJOR * 10000 + DOCTEST_VERSION_MINOR * 100 + DOCTEST_VERSION_PATCH) + +// ================================================================================================= +// == COMPILER VERSION ============================================================================= +// ================================================================================================= + +// ideas for the version stuff are taken from here: https://github.com/cxxstuff/cxx_detect + +#ifdef _MSC_VER +#define DOCTEST_CPLUSPLUS _MSVC_LANG +#else +#define DOCTEST_CPLUSPLUS __cplusplus +#endif + +#define DOCTEST_COMPILER(MAJOR, MINOR, PATCH) ((MAJOR)*10000000 + (MINOR)*100000 + (PATCH)) + +// GCC/Clang and GCC/MSVC are mutually exclusive, but Clang/MSVC are not because of clang-cl... +#if defined(_MSC_VER) && defined(_MSC_FULL_VER) +#if _MSC_VER == _MSC_FULL_VER / 10000 +#define DOCTEST_MSVC DOCTEST_COMPILER(_MSC_VER / 100, _MSC_VER % 100, _MSC_FULL_VER % 10000) +#else // MSVC +#define DOCTEST_MSVC \ + DOCTEST_COMPILER(_MSC_VER / 100, (_MSC_FULL_VER / 100000) % 100, _MSC_FULL_VER % 100000) +#endif // MSVC +#endif // MSVC +#if defined(__clang__) && defined(__clang_minor__) && defined(__clang_patchlevel__) +#define DOCTEST_CLANG DOCTEST_COMPILER(__clang_major__, __clang_minor__, __clang_patchlevel__) +#elif defined(__GNUC__) && defined(__GNUC_MINOR__) && defined(__GNUC_PATCHLEVEL__) && \ + !defined(__INTEL_COMPILER) +#define DOCTEST_GCC DOCTEST_COMPILER(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#endif // GCC +#if defined(__INTEL_COMPILER) +#define DOCTEST_ICC DOCTEST_COMPILER(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, 0) +#endif // ICC + +#ifndef DOCTEST_MSVC +#define DOCTEST_MSVC 0 +#endif // DOCTEST_MSVC +#ifndef DOCTEST_CLANG +#define DOCTEST_CLANG 0 +#endif // DOCTEST_CLANG +#ifndef DOCTEST_GCC +#define DOCTEST_GCC 0 +#endif // DOCTEST_GCC +#ifndef DOCTEST_ICC +#define DOCTEST_ICC 0 +#endif // DOCTEST_ICC + +// ================================================================================================= +// == COMPILER WARNINGS HELPERS ==================================================================== +// ================================================================================================= + +#if DOCTEST_CLANG && !DOCTEST_ICC +#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x) +#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH _Pragma("clang diagnostic push") +#define DOCTEST_CLANG_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(clang diagnostic ignored w) +#define DOCTEST_CLANG_SUPPRESS_WARNING_POP _Pragma("clang diagnostic pop") +#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w) \ + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH DOCTEST_CLANG_SUPPRESS_WARNING(w) +#else // DOCTEST_CLANG +#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +#define DOCTEST_CLANG_SUPPRESS_WARNING(w) +#define DOCTEST_CLANG_SUPPRESS_WARNING_POP +#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_CLANG + +#if DOCTEST_GCC +#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x) +#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH _Pragma("GCC diagnostic push") +#define DOCTEST_GCC_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(GCC diagnostic ignored w) +#define DOCTEST_GCC_SUPPRESS_WARNING_POP _Pragma("GCC diagnostic pop") +#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w) \ + DOCTEST_GCC_SUPPRESS_WARNING_PUSH DOCTEST_GCC_SUPPRESS_WARNING(w) +#else // DOCTEST_GCC +#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH +#define DOCTEST_GCC_SUPPRESS_WARNING(w) +#define DOCTEST_GCC_SUPPRESS_WARNING_POP +#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_GCC + +#if DOCTEST_MSVC +#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH __pragma(warning(push)) +#define DOCTEST_MSVC_SUPPRESS_WARNING(w) __pragma(warning(disable : w)) +#define DOCTEST_MSVC_SUPPRESS_WARNING_POP __pragma(warning(pop)) +#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w) \ + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH DOCTEST_MSVC_SUPPRESS_WARNING(w) +#else // DOCTEST_MSVC +#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +#define DOCTEST_MSVC_SUPPRESS_WARNING(w) +#define DOCTEST_MSVC_SUPPRESS_WARNING_POP +#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w) +#endif // DOCTEST_MSVC + +// ================================================================================================= +// == COMPILER WARNINGS ============================================================================ +// ================================================================================================= + +// both the header and the implementation suppress all of these, +// so it only makes sense to aggregate them like so +#define DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH \ + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wunknown-pragmas") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wweak-vtables") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wpadded") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-prototypes") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat") \ + DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat-pedantic") \ + \ + DOCTEST_GCC_SUPPRESS_WARNING_PUSH \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wunknown-pragmas") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wpragmas") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Weffc++") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-overflow") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-aliasing") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-declarations") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wuseless-cast") \ + DOCTEST_GCC_SUPPRESS_WARNING("-Wnoexcept") \ + \ + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH \ + /* these 4 also disabled globally via cmake: */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4514) /* unreferenced inline function has been removed */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4571) /* SEH related */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4710) /* function not inlined */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4711) /* function selected for inline expansion*/ \ + /* common ones */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4616) /* invalid compiler warning */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4619) /* invalid compiler warning */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4996) /* The compiler encountered a deprecated declaration */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4706) /* assignment within conditional expression */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4512) /* 'class' : assignment operator could not be generated */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4127) /* conditional expression is constant */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4820) /* padding */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4625) /* copy constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4626) /* assignment operator was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5027) /* move assignment operator implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5026) /* move constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4640) /* construction of local static object not thread-safe */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5045) /* Spectre mitigation for memory load */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5264) /* 'variable-name': 'const' variable is not used */ \ + /* static analysis */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26439) /* Function may not throw. Declare it 'noexcept' */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26495) /* Always initialize a member variable */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26451) /* Arithmetic overflow ... */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26444) /* Avoid unnamed objects with custom ctor and dtor... */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(26812) /* Prefer 'enum class' over 'enum' */ + +#define DOCTEST_SUPPRESS_COMMON_WARNINGS_POP \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP \ + DOCTEST_GCC_SUPPRESS_WARNING_POP \ + DOCTEST_MSVC_SUPPRESS_WARNING_POP + +DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH + +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wnon-virtual-dtor") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wdeprecated") + +DOCTEST_GCC_SUPPRESS_WARNING_PUSH +DOCTEST_GCC_SUPPRESS_WARNING("-Wctor-dtor-privacy") +DOCTEST_GCC_SUPPRESS_WARNING("-Wnon-virtual-dtor") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-promo") + +DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +DOCTEST_MSVC_SUPPRESS_WARNING(4623) // default constructor was implicitly defined as deleted + +#define DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN \ + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH \ + DOCTEST_MSVC_SUPPRESS_WARNING(4548) /* before comma no effect; expected side - effect */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4265) /* virtual functions, but destructor is not virtual */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4986) /* exception specification does not match previous */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4350) /* 'member1' called instead of 'member2' */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4668) /* not defined as a preprocessor macro */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4365) /* signed/unsigned mismatch */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4774) /* format string not a string literal */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4820) /* padding */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4625) /* copy constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4626) /* assignment operator was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5027) /* move assignment operator implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5026) /* move constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4623) /* default constructor was implicitly deleted */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5039) /* pointer to pot. throwing function passed to extern C */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5045) /* Spectre mitigation for memory load */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5105) /* macro producing 'defined' has undefined behavior */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(4738) /* storing float result in memory, loss of performance */ \ + DOCTEST_MSVC_SUPPRESS_WARNING(5262) /* implicit fall-through */ + +#define DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END DOCTEST_MSVC_SUPPRESS_WARNING_POP + +// ================================================================================================= +// == FEATURE DETECTION ============================================================================ +// ================================================================================================= + +// general compiler feature support table: https://en.cppreference.com/w/cpp/compiler_support +// MSVC C++11 feature support table: https://msdn.microsoft.com/en-us/library/hh567368.aspx +// GCC C++11 feature support table: https://gcc.gnu.org/projects/cxx-status.html +// MSVC version table: +// https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#Internal_version_numbering +// MSVC++ 14.3 (17) _MSC_VER == 1930 (Visual Studio 2022) +// MSVC++ 14.2 (16) _MSC_VER == 1920 (Visual Studio 2019) +// MSVC++ 14.1 (15) _MSC_VER == 1910 (Visual Studio 2017) +// MSVC++ 14.0 _MSC_VER == 1900 (Visual Studio 2015) +// MSVC++ 12.0 _MSC_VER == 1800 (Visual Studio 2013) +// MSVC++ 11.0 _MSC_VER == 1700 (Visual Studio 2012) +// MSVC++ 10.0 _MSC_VER == 1600 (Visual Studio 2010) +// MSVC++ 9.0 _MSC_VER == 1500 (Visual Studio 2008) +// MSVC++ 8.0 _MSC_VER == 1400 (Visual Studio 2005) + +// Universal Windows Platform support +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) +#define DOCTEST_CONFIG_NO_WINDOWS_SEH +#endif // WINAPI_FAMILY +#if DOCTEST_MSVC && !defined(DOCTEST_CONFIG_WINDOWS_SEH) +#define DOCTEST_CONFIG_WINDOWS_SEH +#endif // MSVC +#if defined(DOCTEST_CONFIG_NO_WINDOWS_SEH) && defined(DOCTEST_CONFIG_WINDOWS_SEH) +#undef DOCTEST_CONFIG_WINDOWS_SEH +#endif // DOCTEST_CONFIG_NO_WINDOWS_SEH + +#if !defined(_WIN32) && !defined(__QNX__) && !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && \ + !defined(__EMSCRIPTEN__) && !defined(__wasi__) +#define DOCTEST_CONFIG_POSIX_SIGNALS +#endif // _WIN32 +#if defined(DOCTEST_CONFIG_NO_POSIX_SIGNALS) && defined(DOCTEST_CONFIG_POSIX_SIGNALS) +#undef DOCTEST_CONFIG_POSIX_SIGNALS +#endif // DOCTEST_CONFIG_NO_POSIX_SIGNALS + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +#if !defined(__cpp_exceptions) && !defined(__EXCEPTIONS) && !defined(_CPPUNWIND) \ + || defined(__wasi__) +#define DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // no exceptions +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS +#define DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#if defined(DOCTEST_CONFIG_NO_EXCEPTIONS) && !defined(DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS) +#define DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS && !DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS + +#ifdef __wasi__ +#define DOCTEST_CONFIG_NO_MULTITHREADING +#endif + +#if defined(DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN) && !defined(DOCTEST_CONFIG_IMPLEMENT) +#define DOCTEST_CONFIG_IMPLEMENT +#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + +#if defined(_WIN32) || defined(__CYGWIN__) +#if DOCTEST_MSVC +#define DOCTEST_SYMBOL_EXPORT __declspec(dllexport) +#define DOCTEST_SYMBOL_IMPORT __declspec(dllimport) +#else // MSVC +#define DOCTEST_SYMBOL_EXPORT __attribute__((dllexport)) +#define DOCTEST_SYMBOL_IMPORT __attribute__((dllimport)) +#endif // MSVC +#else // _WIN32 +#define DOCTEST_SYMBOL_EXPORT __attribute__((visibility("default"))) +#define DOCTEST_SYMBOL_IMPORT +#endif // _WIN32 + +#ifdef DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL +#ifdef DOCTEST_CONFIG_IMPLEMENT +#define DOCTEST_INTERFACE DOCTEST_SYMBOL_EXPORT +#else // DOCTEST_CONFIG_IMPLEMENT +#define DOCTEST_INTERFACE DOCTEST_SYMBOL_IMPORT +#endif // DOCTEST_CONFIG_IMPLEMENT +#else // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL +#define DOCTEST_INTERFACE +#endif // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL + +// needed for extern template instantiations +// see https://github.com/fmtlib/fmt/issues/2228 +#if DOCTEST_MSVC +#define DOCTEST_INTERFACE_DECL +#define DOCTEST_INTERFACE_DEF DOCTEST_INTERFACE +#else // DOCTEST_MSVC +#define DOCTEST_INTERFACE_DECL DOCTEST_INTERFACE +#define DOCTEST_INTERFACE_DEF +#endif // DOCTEST_MSVC + +#define DOCTEST_EMPTY + +#if DOCTEST_MSVC +#define DOCTEST_NOINLINE __declspec(noinline) +#define DOCTEST_UNUSED +#define DOCTEST_ALIGNMENT(x) +#elif DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 5, 0) +#define DOCTEST_NOINLINE +#define DOCTEST_UNUSED +#define DOCTEST_ALIGNMENT(x) +#else +#define DOCTEST_NOINLINE __attribute__((noinline)) +#define DOCTEST_UNUSED __attribute__((unused)) +#define DOCTEST_ALIGNMENT(x) __attribute__((aligned(x))) +#endif + +#ifdef DOCTEST_CONFIG_NO_CONTRADICTING_INLINE +#define DOCTEST_INLINE_NOINLINE inline +#else +#define DOCTEST_INLINE_NOINLINE inline DOCTEST_NOINLINE +#endif + +#ifndef DOCTEST_NORETURN +#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_NORETURN +#else // DOCTEST_MSVC +#define DOCTEST_NORETURN [[noreturn]] +#endif // DOCTEST_MSVC +#endif // DOCTEST_NORETURN + +#ifndef DOCTEST_NOEXCEPT +#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_NOEXCEPT +#else // DOCTEST_MSVC +#define DOCTEST_NOEXCEPT noexcept +#endif // DOCTEST_MSVC +#endif // DOCTEST_NOEXCEPT + +#ifndef DOCTEST_CONSTEXPR +#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_CONSTEXPR const +#define DOCTEST_CONSTEXPR_FUNC inline +#else // DOCTEST_MSVC +#define DOCTEST_CONSTEXPR constexpr +#define DOCTEST_CONSTEXPR_FUNC constexpr +#endif // DOCTEST_MSVC +#endif // DOCTEST_CONSTEXPR + +#ifndef DOCTEST_NO_SANITIZE_INTEGER +#if DOCTEST_CLANG >= DOCTEST_COMPILER(3, 7, 0) +#define DOCTEST_NO_SANITIZE_INTEGER __attribute__((no_sanitize("integer"))) +#else +#define DOCTEST_NO_SANITIZE_INTEGER +#endif +#endif // DOCTEST_NO_SANITIZE_INTEGER + +// ================================================================================================= +// == FEATURE DETECTION END ======================================================================== +// ================================================================================================= + +#define DOCTEST_DECLARE_INTERFACE(name) \ + virtual ~name(); \ + name() = default; \ + name(const name&) = delete; \ + name(name&&) = delete; \ + name& operator=(const name&) = delete; \ + name& operator=(name&&) = delete; + +#define DOCTEST_DEFINE_INTERFACE(name) \ + name::~name() = default; + +// internal macros for string concatenation and anonymous variable name generation +#define DOCTEST_CAT_IMPL(s1, s2) s1##s2 +#define DOCTEST_CAT(s1, s2) DOCTEST_CAT_IMPL(s1, s2) +#ifdef __COUNTER__ // not standard and may be missing for some compilers +#define DOCTEST_ANONYMOUS(x) DOCTEST_CAT(x, __COUNTER__) +#else // __COUNTER__ +#define DOCTEST_ANONYMOUS(x) DOCTEST_CAT(x, __LINE__) +#endif // __COUNTER__ + +#ifndef DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE +#define DOCTEST_REF_WRAP(x) x& +#else // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE +#define DOCTEST_REF_WRAP(x) x +#endif // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE + +// not using __APPLE__ because... this is how Catch does it +#ifdef __MAC_OS_X_VERSION_MIN_REQUIRED +#define DOCTEST_PLATFORM_MAC +#elif defined(__IPHONE_OS_VERSION_MIN_REQUIRED) +#define DOCTEST_PLATFORM_IPHONE +#elif defined(_WIN32) +#define DOCTEST_PLATFORM_WINDOWS +#elif defined(__wasi__) +#define DOCTEST_PLATFORM_WASI +#else // DOCTEST_PLATFORM +#define DOCTEST_PLATFORM_LINUX +#endif // DOCTEST_PLATFORM + +namespace doctest { namespace detail { + static DOCTEST_CONSTEXPR int consume(const int*, int) noexcept { return 0; } +}} + +#define DOCTEST_GLOBAL_NO_WARNINGS(var, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wglobal-constructors") \ + static const int var = doctest::detail::consume(&var, __VA_ARGS__); \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#ifndef DOCTEST_BREAK_INTO_DEBUGGER +// should probably take a look at https://github.com/scottt/debugbreak +#ifdef DOCTEST_PLATFORM_LINUX +#if defined(__GNUC__) && (defined(__i386) || defined(__x86_64)) +// Break at the location of the failing check if possible +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("int $3\n" : :) // NOLINT(hicpp-no-assembler) +#else +#include +#define DOCTEST_BREAK_INTO_DEBUGGER() raise(SIGTRAP) +#endif +#elif defined(DOCTEST_PLATFORM_MAC) +#if defined(__x86_64) || defined(__x86_64__) || defined(__amd64__) || defined(__i386) +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("int $3\n" : :) // NOLINT(hicpp-no-assembler) +#elif defined(__ppc__) || defined(__ppc64__) +// https://www.cocoawithlove.com/2008/03/break-into-debugger.html +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("li r0, 20\nsc\nnop\nli r0, 37\nli r4, 2\nsc\nnop\n": : : "memory","r0","r3","r4") // NOLINT(hicpp-no-assembler) +#else +#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("brk #0"); // NOLINT(hicpp-no-assembler) +#endif +#elif DOCTEST_MSVC +#define DOCTEST_BREAK_INTO_DEBUGGER() __debugbreak() +#elif defined(__MINGW32__) +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wredundant-decls") +extern "C" __declspec(dllimport) void __stdcall DebugBreak(); +DOCTEST_GCC_SUPPRESS_WARNING_POP +#define DOCTEST_BREAK_INTO_DEBUGGER() ::DebugBreak() +#else // linux +#define DOCTEST_BREAK_INTO_DEBUGGER() (static_cast(0)) +#endif // linux +#endif // DOCTEST_BREAK_INTO_DEBUGGER + +// this is kept here for backwards compatibility since the config option was changed +#ifdef DOCTEST_CONFIG_USE_IOSFWD +#ifndef DOCTEST_CONFIG_USE_STD_HEADERS +#define DOCTEST_CONFIG_USE_STD_HEADERS +#endif +#endif // DOCTEST_CONFIG_USE_IOSFWD + +// for clang - always include ciso646 (which drags some std stuff) because +// we want to check if we are using libc++ with the _LIBCPP_VERSION macro in +// which case we don't want to forward declare stuff from std - for reference: +// https://github.com/doctest/doctest/issues/126 +// https://github.com/doctest/doctest/issues/356 +#if DOCTEST_CLANG +#include +#endif // clang + +#ifdef _LIBCPP_VERSION +#ifndef DOCTEST_CONFIG_USE_STD_HEADERS +#define DOCTEST_CONFIG_USE_STD_HEADERS +#endif +#endif // _LIBCPP_VERSION + +#ifdef DOCTEST_CONFIG_USE_STD_HEADERS +#ifndef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#define DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN +#include +#include +#include +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END +#else // DOCTEST_CONFIG_USE_STD_HEADERS + +// Forward declaring 'X' in namespace std is not permitted by the C++ Standard. +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4643) + +namespace std { // NOLINT(cert-dcl58-cpp) +typedef decltype(nullptr) nullptr_t; // NOLINT(modernize-use-using) +typedef decltype(sizeof(void*)) size_t; // NOLINT(modernize-use-using) +template +struct char_traits; +template <> +struct char_traits; +template +class basic_ostream; // NOLINT(fuchsia-virtual-inheritance) +typedef basic_ostream> ostream; // NOLINT(modernize-use-using) +template +// NOLINTNEXTLINE +basic_ostream& operator<<(basic_ostream&, const char*); +template +class basic_istream; +typedef basic_istream> istream; // NOLINT(modernize-use-using) +template +class tuple; +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 +template +class allocator; +template +class basic_string; +using string = basic_string, allocator>; +#endif // VS 2019 +} // namespace std + +DOCTEST_MSVC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_USE_STD_HEADERS + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#include +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + +namespace doctest { + +using std::size_t; + +DOCTEST_INTERFACE extern bool is_running_in_test; + +#ifndef DOCTEST_CONFIG_STRING_SIZE_TYPE +#define DOCTEST_CONFIG_STRING_SIZE_TYPE unsigned +#endif + +// A 24 byte string class (can be as small as 17 for x64 and 13 for x86) that can hold strings with length +// of up to 23 chars on the stack before going on the heap - the last byte of the buffer is used for: +// - "is small" bit - the highest bit - if "0" then it is small - otherwise its "1" (128) +// - if small - capacity left before going on the heap - using the lowest 5 bits +// - if small - 2 bits are left unused - the second and third highest ones +// - if small - acts as a null terminator if strlen() is 23 (24 including the null terminator) +// and the "is small" bit remains "0" ("as well as the capacity left") so its OK +// Idea taken from this lecture about the string implementation of facebook/folly - fbstring +// https://www.youtube.com/watch?v=kPR8h4-qZdk +// TODO: +// - optimizations - like not deleting memory unnecessarily in operator= and etc. +// - resize/reserve/clear +// - replace +// - back/front +// - iterator stuff +// - find & friends +// - push_back/pop_back +// - assign/insert/erase +// - relational operators as free functions - taking const char* as one of the params +class DOCTEST_INTERFACE String +{ +public: + using size_type = DOCTEST_CONFIG_STRING_SIZE_TYPE; + +private: + static DOCTEST_CONSTEXPR size_type len = 24; //!OCLINT avoid private static members + static DOCTEST_CONSTEXPR size_type last = len - 1; //!OCLINT avoid private static members + + struct view // len should be more than sizeof(view) - because of the final byte for flags + { + char* ptr; + size_type size; + size_type capacity; + }; + + union + { + char buf[len]; // NOLINT(*-avoid-c-arrays) + view data; + }; + + char* allocate(size_type sz); + + bool isOnStack() const noexcept { return (buf[last] & 128) == 0; } + void setOnHeap() noexcept; + void setLast(size_type in = last) noexcept; + void setSize(size_type sz) noexcept; + + void copy(const String& other); + +public: + static DOCTEST_CONSTEXPR size_type npos = static_cast(-1); + + String() noexcept; + ~String(); + + // cppcheck-suppress noExplicitConstructor + String(const char* in); + String(const char* in, size_type in_size); + + String(std::istream& in, size_type in_size); + + String(const String& other); + String& operator=(const String& other); + + String& operator+=(const String& other); + + String(String&& other) noexcept; + String& operator=(String&& other) noexcept; + + char operator[](size_type i) const; + char& operator[](size_type i); + + // the only functions I'm willing to leave in the interface - available for inlining + const char* c_str() const { return const_cast(this)->c_str(); } // NOLINT + char* c_str() { + if (isOnStack()) { + return reinterpret_cast(buf); + } + return data.ptr; + } + + size_type size() const; + size_type capacity() const; + + String substr(size_type pos, size_type cnt = npos) &&; + String substr(size_type pos, size_type cnt = npos) const &; + + size_type find(char ch, size_type pos = 0) const; + size_type rfind(char ch, size_type pos = npos) const; + + int compare(const char* other, bool no_case = false) const; + int compare(const String& other, bool no_case = false) const; + +friend DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& s, const String& in); +}; + +DOCTEST_INTERFACE String operator+(const String& lhs, const String& rhs); + +DOCTEST_INTERFACE bool operator==(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator!=(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator<(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator>(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator<=(const String& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator>=(const String& lhs, const String& rhs); + +class DOCTEST_INTERFACE Contains { +public: + explicit Contains(const String& string); + + bool checkWith(const String& other) const; + + String string; +}; + +DOCTEST_INTERFACE String toString(const Contains& in); + +DOCTEST_INTERFACE bool operator==(const String& lhs, const Contains& rhs); +DOCTEST_INTERFACE bool operator==(const Contains& lhs, const String& rhs); +DOCTEST_INTERFACE bool operator!=(const String& lhs, const Contains& rhs); +DOCTEST_INTERFACE bool operator!=(const Contains& lhs, const String& rhs); + +namespace Color { + enum Enum + { + None = 0, + White, + Red, + Green, + Blue, + Cyan, + Yellow, + Grey, + + Bright = 0x10, + + BrightRed = Bright | Red, + BrightGreen = Bright | Green, + LightGrey = Bright | Grey, + BrightWhite = Bright | White + }; + + DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& s, Color::Enum code); +} // namespace Color + +namespace assertType { + enum Enum + { + // macro traits + + is_warn = 1, + is_check = 2 * is_warn, + is_require = 2 * is_check, + + is_normal = 2 * is_require, + is_throws = 2 * is_normal, + is_throws_as = 2 * is_throws, + is_throws_with = 2 * is_throws_as, + is_nothrow = 2 * is_throws_with, + + is_false = 2 * is_nothrow, + is_unary = 2 * is_false, // not checked anywhere - used just to distinguish the types + + is_eq = 2 * is_unary, + is_ne = 2 * is_eq, + + is_lt = 2 * is_ne, + is_gt = 2 * is_lt, + + is_ge = 2 * is_gt, + is_le = 2 * is_ge, + + // macro types + + DT_WARN = is_normal | is_warn, + DT_CHECK = is_normal | is_check, + DT_REQUIRE = is_normal | is_require, + + DT_WARN_FALSE = is_normal | is_false | is_warn, + DT_CHECK_FALSE = is_normal | is_false | is_check, + DT_REQUIRE_FALSE = is_normal | is_false | is_require, + + DT_WARN_THROWS = is_throws | is_warn, + DT_CHECK_THROWS = is_throws | is_check, + DT_REQUIRE_THROWS = is_throws | is_require, + + DT_WARN_THROWS_AS = is_throws_as | is_warn, + DT_CHECK_THROWS_AS = is_throws_as | is_check, + DT_REQUIRE_THROWS_AS = is_throws_as | is_require, + + DT_WARN_THROWS_WITH = is_throws_with | is_warn, + DT_CHECK_THROWS_WITH = is_throws_with | is_check, + DT_REQUIRE_THROWS_WITH = is_throws_with | is_require, + + DT_WARN_THROWS_WITH_AS = is_throws_with | is_throws_as | is_warn, + DT_CHECK_THROWS_WITH_AS = is_throws_with | is_throws_as | is_check, + DT_REQUIRE_THROWS_WITH_AS = is_throws_with | is_throws_as | is_require, + + DT_WARN_NOTHROW = is_nothrow | is_warn, + DT_CHECK_NOTHROW = is_nothrow | is_check, + DT_REQUIRE_NOTHROW = is_nothrow | is_require, + + DT_WARN_EQ = is_normal | is_eq | is_warn, + DT_CHECK_EQ = is_normal | is_eq | is_check, + DT_REQUIRE_EQ = is_normal | is_eq | is_require, + + DT_WARN_NE = is_normal | is_ne | is_warn, + DT_CHECK_NE = is_normal | is_ne | is_check, + DT_REQUIRE_NE = is_normal | is_ne | is_require, + + DT_WARN_GT = is_normal | is_gt | is_warn, + DT_CHECK_GT = is_normal | is_gt | is_check, + DT_REQUIRE_GT = is_normal | is_gt | is_require, + + DT_WARN_LT = is_normal | is_lt | is_warn, + DT_CHECK_LT = is_normal | is_lt | is_check, + DT_REQUIRE_LT = is_normal | is_lt | is_require, + + DT_WARN_GE = is_normal | is_ge | is_warn, + DT_CHECK_GE = is_normal | is_ge | is_check, + DT_REQUIRE_GE = is_normal | is_ge | is_require, + + DT_WARN_LE = is_normal | is_le | is_warn, + DT_CHECK_LE = is_normal | is_le | is_check, + DT_REQUIRE_LE = is_normal | is_le | is_require, + + DT_WARN_UNARY = is_normal | is_unary | is_warn, + DT_CHECK_UNARY = is_normal | is_unary | is_check, + DT_REQUIRE_UNARY = is_normal | is_unary | is_require, + + DT_WARN_UNARY_FALSE = is_normal | is_false | is_unary | is_warn, + DT_CHECK_UNARY_FALSE = is_normal | is_false | is_unary | is_check, + DT_REQUIRE_UNARY_FALSE = is_normal | is_false | is_unary | is_require, + }; +} // namespace assertType + +DOCTEST_INTERFACE const char* assertString(assertType::Enum at); +DOCTEST_INTERFACE const char* failureString(assertType::Enum at); +DOCTEST_INTERFACE const char* skipPathFromFilename(const char* file); + +struct DOCTEST_INTERFACE TestCaseData +{ + String m_file; // the file in which the test was registered (using String - see #350) + unsigned m_line; // the line where the test was registered + const char* m_name; // name of the test case + const char* m_test_suite; // the test suite in which the test was added + const char* m_description; + bool m_skip; + bool m_no_breaks; + bool m_no_output; + bool m_may_fail; + bool m_should_fail; + int m_expected_failures; + double m_timeout; +}; + +struct DOCTEST_INTERFACE AssertData +{ + // common - for all asserts + const TestCaseData* m_test_case; + assertType::Enum m_at; + const char* m_file; + int m_line; + const char* m_expr; + bool m_failed; + + // exception-related - for all asserts + bool m_threw; + String m_exception; + + // for normal asserts + String m_decomp; + + // for specific exception-related asserts + bool m_threw_as; + const char* m_exception_type; + + class DOCTEST_INTERFACE StringContains { + private: + Contains content; + bool isContains; + + public: + StringContains(const String& str) : content(str), isContains(false) { } + StringContains(Contains cntn) : content(static_cast(cntn)), isContains(true) { } + + bool check(const String& str) { return isContains ? (content == str) : (content.string == str); } + + operator const String&() const { return content.string; } + + const char* c_str() const { return content.string.c_str(); } + } m_exception_string; + + AssertData(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type, const StringContains& exception_string); +}; + +struct DOCTEST_INTERFACE MessageData +{ + String m_string; + const char* m_file; + int m_line; + assertType::Enum m_severity; +}; + +struct DOCTEST_INTERFACE SubcaseSignature +{ + String m_name; + const char* m_file; + int m_line; + + bool operator==(const SubcaseSignature& other) const; + bool operator<(const SubcaseSignature& other) const; +}; + +struct DOCTEST_INTERFACE IContextScope +{ + DOCTEST_DECLARE_INTERFACE(IContextScope) + virtual void stringify(std::ostream*) const = 0; +}; + +namespace detail { + struct DOCTEST_INTERFACE TestCase; +} // namespace detail + +struct ContextOptions //!OCLINT too many fields +{ + std::ostream* cout = nullptr; // stdout stream + String binary_name; // the test binary name + + const detail::TestCase* currentTest = nullptr; + + // == parameters from the command line + String out; // output filename + String order_by; // how tests should be ordered + unsigned rand_seed; // the seed for rand ordering + + unsigned first; // the first (matching) test to be executed + unsigned last; // the last (matching) test to be executed + + int abort_after; // stop tests after this many failed assertions + int subcase_filter_levels; // apply the subcase filters for the first N levels + + bool success; // include successful assertions in output + bool case_sensitive; // if filtering should be case sensitive + bool exit; // if the program should be exited after the tests are ran/whatever + bool duration; // print the time duration of each test case + bool minimal; // minimal console output (only test failures) + bool quiet; // no console output + bool no_throw; // to skip exceptions-related assertion macros + bool no_exitcode; // if the framework should return 0 as the exitcode + bool no_run; // to not run the tests at all (can be done with an "*" exclude) + bool no_intro; // to not print the intro of the framework + bool no_version; // to not print the version of the framework + bool no_colors; // if output to the console should be colorized + bool force_colors; // forces the use of colors even when a tty cannot be detected + bool no_breaks; // to not break into the debugger + bool no_skip; // don't skip test cases which are marked to be skipped + bool gnu_file_line; // if line numbers should be surrounded with :x: and not (x): + bool no_path_in_filenames; // if the path to files should be removed from the output + bool no_line_numbers; // if source code line numbers should be omitted from the output + bool no_debug_output; // no output in the debug console when a debugger is attached + bool no_skipped_summary; // don't print "skipped" in the summary !!! UNDOCUMENTED !!! + bool no_time_in_output; // omit any time/timestamps from output !!! UNDOCUMENTED !!! + + bool help; // to print the help + bool version; // to print the version + bool count; // if only the count of matching tests is to be retrieved + bool list_test_cases; // to list all tests matching the filters + bool list_test_suites; // to list all suites matching the filters + bool list_reporters; // lists all registered reporters +}; + +namespace detail { + namespace types { +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + using namespace std; +#else + template + struct enable_if { }; + + template + struct enable_if { using type = T; }; + + struct true_type { static DOCTEST_CONSTEXPR bool value = true; }; + struct false_type { static DOCTEST_CONSTEXPR bool value = false; }; + + template struct remove_reference { using type = T; }; + template struct remove_reference { using type = T; }; + template struct remove_reference { using type = T; }; + + template struct is_rvalue_reference : false_type { }; + template struct is_rvalue_reference : true_type { }; + + template struct remove_const { using type = T; }; + template struct remove_const { using type = T; }; + + // Compiler intrinsics + template struct is_enum { static DOCTEST_CONSTEXPR bool value = __is_enum(T); }; + template struct underlying_type { using type = __underlying_type(T); }; + + template struct is_pointer : false_type { }; + template struct is_pointer : true_type { }; + + template struct is_array : false_type { }; + // NOLINTNEXTLINE(*-avoid-c-arrays) + template struct is_array : true_type { }; +#endif + } + + // + template + T&& declval(); + + template + DOCTEST_CONSTEXPR_FUNC T&& forward(typename types::remove_reference::type& t) DOCTEST_NOEXCEPT { + return static_cast(t); + } + + template + DOCTEST_CONSTEXPR_FUNC T&& forward(typename types::remove_reference::type&& t) DOCTEST_NOEXCEPT { + return static_cast(t); + } + + template + struct deferred_false : types::false_type { }; + +// MSVS 2015 :( +#if !DOCTEST_CLANG && defined(_MSC_VER) && _MSC_VER <= 1900 + template + struct has_global_insertion_operator : types::false_type { }; + + template + struct has_global_insertion_operator(), declval()), void())> : types::true_type { }; + + template + struct has_insertion_operator { static DOCTEST_CONSTEXPR bool value = has_global_insertion_operator::value; }; + + template + struct insert_hack; + + template + struct insert_hack { + static void insert(std::ostream& os, const T& t) { ::operator<<(os, t); } + }; + + template + struct insert_hack { + static void insert(std::ostream& os, const T& t) { operator<<(os, t); } + }; + + template + using insert_hack_t = insert_hack::value>; +#else + template + struct has_insertion_operator : types::false_type { }; +#endif + + template + struct has_insertion_operator(), declval()), void())> : types::true_type { }; + + template + struct should_stringify_as_underlying_type { + static DOCTEST_CONSTEXPR bool value = detail::types::is_enum::value && !doctest::detail::has_insertion_operator::value; + }; + + DOCTEST_INTERFACE std::ostream* tlssPush(); + DOCTEST_INTERFACE String tlssPop(); + + template + struct StringMakerBase { + template + static String convert(const DOCTEST_REF_WRAP(T)) { +#ifdef DOCTEST_CONFIG_REQUIRE_STRINGIFICATION_FOR_ALL_USED_TYPES + static_assert(deferred_false::value, "No stringification detected for type T. See string conversion manual"); +#endif + return "{?}"; + } + }; + + template + struct filldata; + + template + void filloss(std::ostream* stream, const T& in) { + filldata::fill(stream, in); + } + + template + void filloss(std::ostream* stream, const T (&in)[N]) { // NOLINT(*-avoid-c-arrays) + // T[N], T(&)[N], T(&&)[N] have same behaviour. + // Hence remove reference. + filloss::type>(stream, in); + } + + template + String toStream(const T& in) { + std::ostream* stream = tlssPush(); + filloss(stream, in); + return tlssPop(); + } + + template <> + struct StringMakerBase { + template + static String convert(const DOCTEST_REF_WRAP(T) in) { + return toStream(in); + } + }; +} // namespace detail + +template +struct StringMaker : public detail::StringMakerBase< + detail::has_insertion_operator::value || detail::types::is_pointer::value || detail::types::is_array::value> +{}; + +#ifndef DOCTEST_STRINGIFY +#ifdef DOCTEST_CONFIG_DOUBLE_STRINGIFY +#define DOCTEST_STRINGIFY(...) toString(toString(__VA_ARGS__)) +#else +#define DOCTEST_STRINGIFY(...) toString(__VA_ARGS__) +#endif +#endif + +template +String toString() { +#if DOCTEST_CLANG == 0 && DOCTEST_GCC == 0 && DOCTEST_ICC == 0 + String ret = __FUNCSIG__; // class doctest::String __cdecl doctest::toString(void) + String::size_type beginPos = ret.find('<'); + return ret.substr(beginPos + 1, ret.size() - beginPos - static_cast(sizeof(">(void)"))); +#else + String ret = __PRETTY_FUNCTION__; // doctest::String toString() [with T = TYPE] + String::size_type begin = ret.find('=') + 2; + return ret.substr(begin, ret.size() - begin - 1); +#endif +} + +template ::value, bool>::type = true> +String toString(const DOCTEST_REF_WRAP(T) value) { + return StringMaker::convert(value); +} + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +DOCTEST_INTERFACE String toString(const char* in); +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 +DOCTEST_INTERFACE String toString(const std::string& in); +#endif // VS 2019 + +DOCTEST_INTERFACE String toString(String in); + +DOCTEST_INTERFACE String toString(std::nullptr_t); + +DOCTEST_INTERFACE String toString(bool in); + +DOCTEST_INTERFACE String toString(float in); +DOCTEST_INTERFACE String toString(double in); +DOCTEST_INTERFACE String toString(double long in); + +DOCTEST_INTERFACE String toString(char in); +DOCTEST_INTERFACE String toString(char signed in); +DOCTEST_INTERFACE String toString(char unsigned in); +DOCTEST_INTERFACE String toString(short in); +DOCTEST_INTERFACE String toString(short unsigned in); +DOCTEST_INTERFACE String toString(signed in); +DOCTEST_INTERFACE String toString(unsigned in); +DOCTEST_INTERFACE String toString(long in); +DOCTEST_INTERFACE String toString(long unsigned in); +DOCTEST_INTERFACE String toString(long long in); +DOCTEST_INTERFACE String toString(long long unsigned in); + +template ::value, bool>::type = true> +String toString(const DOCTEST_REF_WRAP(T) value) { + using UT = typename detail::types::underlying_type::type; + return (DOCTEST_STRINGIFY(static_cast(value))); +} + +namespace detail { + template + struct filldata + { + static void fill(std::ostream* stream, const T& in) { +#if defined(_MSC_VER) && _MSC_VER <= 1900 + insert_hack_t::insert(*stream, in); +#else + operator<<(*stream, in); +#endif + } + }; + +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4866) +// NOLINTBEGIN(*-avoid-c-arrays) + template + struct filldata { + static void fill(std::ostream* stream, const T(&in)[N]) { + *stream << "["; + for (size_t i = 0; i < N; i++) { + if (i != 0) { *stream << ", "; } + *stream << (DOCTEST_STRINGIFY(in[i])); + } + *stream << "]"; + } + }; +// NOLINTEND(*-avoid-c-arrays) +DOCTEST_MSVC_SUPPRESS_WARNING_POP + + // Specialized since we don't want the terminating null byte! +// NOLINTBEGIN(*-avoid-c-arrays) + template + struct filldata { + static void fill(std::ostream* stream, const char (&in)[N]) { + *stream << String(in, in[N - 1] ? N : N - 1); + } // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + }; +// NOLINTEND(*-avoid-c-arrays) + + template <> + struct filldata { + static void fill(std::ostream* stream, const void* in); + }; + + template + struct filldata { +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4180) + static void fill(std::ostream* stream, const T* in) { +DOCTEST_MSVC_SUPPRESS_WARNING_POP +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wmicrosoft-cast") + filldata::fill(stream, +#if DOCTEST_GCC == 0 || DOCTEST_GCC >= DOCTEST_COMPILER(4, 9, 0) + reinterpret_cast(in) +#else + *reinterpret_cast(&in) +#endif + ); +DOCTEST_CLANG_SUPPRESS_WARNING_POP + } + }; +} + +struct DOCTEST_INTERFACE Approx +{ + Approx(double value); + + Approx operator()(double value) const; + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + explicit Approx(const T& value, + typename detail::types::enable_if::value>::type* = + static_cast(nullptr)) { + *this = static_cast(value); + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + Approx& epsilon(double newEpsilon); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + typename std::enable_if::value, Approx&>::type epsilon( + const T& newEpsilon) { + m_epsilon = static_cast(newEpsilon); + return *this; + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + Approx& scale(double newScale); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + template + typename std::enable_if::value, Approx&>::type scale( + const T& newScale) { + m_scale = static_cast(newScale); + return *this; + } +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + // clang-format off + DOCTEST_INTERFACE friend bool operator==(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator==(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator!=(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator!=(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator<=(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator<=(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator>=(double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator>=(const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator< (double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator< (const Approx & lhs, double rhs); + DOCTEST_INTERFACE friend bool operator> (double lhs, const Approx & rhs); + DOCTEST_INTERFACE friend bool operator> (const Approx & lhs, double rhs); + +#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS +#define DOCTEST_APPROX_PREFIX \ + template friend typename std::enable_if::value, bool>::type + + DOCTEST_APPROX_PREFIX operator==(const T& lhs, const Approx& rhs) { return operator==(static_cast(lhs), rhs); } + DOCTEST_APPROX_PREFIX operator==(const Approx& lhs, const T& rhs) { return operator==(rhs, lhs); } + DOCTEST_APPROX_PREFIX operator!=(const T& lhs, const Approx& rhs) { return !operator==(lhs, rhs); } + DOCTEST_APPROX_PREFIX operator!=(const Approx& lhs, const T& rhs) { return !operator==(rhs, lhs); } + DOCTEST_APPROX_PREFIX operator<=(const T& lhs, const Approx& rhs) { return static_cast(lhs) < rhs.m_value || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator<=(const Approx& lhs, const T& rhs) { return lhs.m_value < static_cast(rhs) || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator>=(const T& lhs, const Approx& rhs) { return static_cast(lhs) > rhs.m_value || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator>=(const Approx& lhs, const T& rhs) { return lhs.m_value > static_cast(rhs) || lhs == rhs; } + DOCTEST_APPROX_PREFIX operator< (const T& lhs, const Approx& rhs) { return static_cast(lhs) < rhs.m_value && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator< (const Approx& lhs, const T& rhs) { return lhs.m_value < static_cast(rhs) && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator> (const T& lhs, const Approx& rhs) { return static_cast(lhs) > rhs.m_value && lhs != rhs; } + DOCTEST_APPROX_PREFIX operator> (const Approx& lhs, const T& rhs) { return lhs.m_value > static_cast(rhs) && lhs != rhs; } +#undef DOCTEST_APPROX_PREFIX +#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS + + // clang-format on + + double m_epsilon; + double m_scale; + double m_value; +}; + +DOCTEST_INTERFACE String toString(const Approx& in); + +DOCTEST_INTERFACE const ContextOptions* getContextOptions(); + +template +struct DOCTEST_INTERFACE_DECL IsNaN +{ + F value; bool flipped; + IsNaN(F f, bool flip = false) : value(f), flipped(flip) { } + IsNaN operator!() const { return { value, !flipped }; } + operator bool() const; +}; +#ifndef __MINGW32__ +extern template struct DOCTEST_INTERFACE_DECL IsNaN; +extern template struct DOCTEST_INTERFACE_DECL IsNaN; +extern template struct DOCTEST_INTERFACE_DECL IsNaN; +#endif +DOCTEST_INTERFACE String toString(IsNaN in); +DOCTEST_INTERFACE String toString(IsNaN in); +DOCTEST_INTERFACE String toString(IsNaN in); + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace detail { + // clang-format off +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + template struct decay_array { using type = T; }; + template struct decay_array { using type = T*; }; + template struct decay_array { using type = T*; }; + + template struct not_char_pointer { static DOCTEST_CONSTEXPR int value = 1; }; + template<> struct not_char_pointer { static DOCTEST_CONSTEXPR int value = 0; }; + template<> struct not_char_pointer { static DOCTEST_CONSTEXPR int value = 0; }; + + template struct can_use_op : public not_char_pointer::type> {}; +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + // clang-format on + + struct DOCTEST_INTERFACE TestFailureException + { + }; + + DOCTEST_INTERFACE bool checkIfShouldThrow(assertType::Enum at); + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + DOCTEST_NORETURN +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + DOCTEST_INTERFACE void throwException(); + + struct DOCTEST_INTERFACE Subcase + { + SubcaseSignature m_signature; + bool m_entered = false; + + Subcase(const String& name, const char* file, int line); + Subcase(const Subcase&) = delete; + Subcase(Subcase&&) = delete; + Subcase& operator=(const Subcase&) = delete; + Subcase& operator=(Subcase&&) = delete; + ~Subcase(); + + operator bool() const; + + private: + bool checkFilters(); + }; + + template + String stringifyBinaryExpr(const DOCTEST_REF_WRAP(L) lhs, const char* op, + const DOCTEST_REF_WRAP(R) rhs) { + return (DOCTEST_STRINGIFY(lhs)) + op + (DOCTEST_STRINGIFY(rhs)); + } + +#if DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 6, 0) +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-comparison") +#endif + +// This will check if there is any way it could find a operator like member or friend and uses it. +// If not it doesn't find the operator or if the operator at global scope is defined after +// this template, the template won't be instantiated due to SFINAE. Once the template is not +// instantiated it can look for global operator using normal conversions. +#ifdef __NVCC__ +#define SFINAE_OP(ret,op) ret +#else +#define SFINAE_OP(ret,op) decltype((void)(doctest::detail::declval() op doctest::detail::declval()),ret{}) +#endif + +#define DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(op, op_str, op_macro) \ + template \ + DOCTEST_NOINLINE SFINAE_OP(Result,op) operator op(R&& rhs) { \ + bool res = op_macro(doctest::detail::forward(lhs), doctest::detail::forward(rhs)); \ + if(m_at & assertType::is_false) \ + res = !res; \ + if(!res || doctest::getContextOptions()->success) \ + return Result(res, stringifyBinaryExpr(lhs, op_str, rhs)); \ + return Result(res); \ + } + + // more checks could be added - like in Catch: + // https://github.com/catchorg/Catch2/pull/1480/files + // https://github.com/catchorg/Catch2/pull/1481/files +#define DOCTEST_FORBIT_EXPRESSION(rt, op) \ + template \ + rt& operator op(const R&) { \ + static_assert(deferred_false::value, \ + "Expression Too Complex Please Rewrite As Binary Comparison!"); \ + return *this; \ + } + + struct DOCTEST_INTERFACE Result // NOLINT(*-member-init) + { + bool m_passed; + String m_decomp; + + Result() = default; // TODO: Why do we need this? (To remove NOLINT) + Result(bool passed, const String& decomposition = String()); + + // forbidding some expressions based on this table: https://en.cppreference.com/w/cpp/language/operator_precedence + DOCTEST_FORBIT_EXPRESSION(Result, &) + DOCTEST_FORBIT_EXPRESSION(Result, ^) + DOCTEST_FORBIT_EXPRESSION(Result, |) + DOCTEST_FORBIT_EXPRESSION(Result, &&) + DOCTEST_FORBIT_EXPRESSION(Result, ||) + DOCTEST_FORBIT_EXPRESSION(Result, ==) + DOCTEST_FORBIT_EXPRESSION(Result, !=) + DOCTEST_FORBIT_EXPRESSION(Result, <) + DOCTEST_FORBIT_EXPRESSION(Result, >) + DOCTEST_FORBIT_EXPRESSION(Result, <=) + DOCTEST_FORBIT_EXPRESSION(Result, >=) + DOCTEST_FORBIT_EXPRESSION(Result, =) + DOCTEST_FORBIT_EXPRESSION(Result, +=) + DOCTEST_FORBIT_EXPRESSION(Result, -=) + DOCTEST_FORBIT_EXPRESSION(Result, *=) + DOCTEST_FORBIT_EXPRESSION(Result, /=) + DOCTEST_FORBIT_EXPRESSION(Result, %=) + DOCTEST_FORBIT_EXPRESSION(Result, <<=) + DOCTEST_FORBIT_EXPRESSION(Result, >>=) + DOCTEST_FORBIT_EXPRESSION(Result, &=) + DOCTEST_FORBIT_EXPRESSION(Result, ^=) + DOCTEST_FORBIT_EXPRESSION(Result, |=) + }; + +#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + + DOCTEST_CLANG_SUPPRESS_WARNING_PUSH + DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion") + DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-compare") + //DOCTEST_CLANG_SUPPRESS_WARNING("-Wdouble-promotion") + //DOCTEST_CLANG_SUPPRESS_WARNING("-Wconversion") + //DOCTEST_CLANG_SUPPRESS_WARNING("-Wfloat-equal") + + DOCTEST_GCC_SUPPRESS_WARNING_PUSH + DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion") + DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-compare") + //DOCTEST_GCC_SUPPRESS_WARNING("-Wdouble-promotion") + //DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion") + //DOCTEST_GCC_SUPPRESS_WARNING("-Wfloat-equal") + + DOCTEST_MSVC_SUPPRESS_WARNING_PUSH + // https://stackoverflow.com/questions/39479163 what's the difference between 4018 and 4389 + DOCTEST_MSVC_SUPPRESS_WARNING(4388) // signed/unsigned mismatch + DOCTEST_MSVC_SUPPRESS_WARNING(4389) // 'operator' : signed/unsigned mismatch + DOCTEST_MSVC_SUPPRESS_WARNING(4018) // 'expression' : signed/unsigned mismatch + //DOCTEST_MSVC_SUPPRESS_WARNING(4805) // 'operation' : unsafe mix of type 'type' and type 'type' in operation + +#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + + // clang-format off +#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_COMPARISON_RETURN_TYPE bool +#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_COMPARISON_RETURN_TYPE typename types::enable_if::value || can_use_op::value, bool>::type + inline bool eq(const char* lhs, const char* rhs) { return String(lhs) == String(rhs); } + inline bool ne(const char* lhs, const char* rhs) { return String(lhs) != String(rhs); } + inline bool lt(const char* lhs, const char* rhs) { return String(lhs) < String(rhs); } + inline bool gt(const char* lhs, const char* rhs) { return String(lhs) > String(rhs); } + inline bool le(const char* lhs, const char* rhs) { return String(lhs) <= String(rhs); } + inline bool ge(const char* lhs, const char* rhs) { return String(lhs) >= String(rhs); } +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + // clang-format on + +#define DOCTEST_RELATIONAL_OP(name, op) \ + template \ + DOCTEST_COMPARISON_RETURN_TYPE name(const DOCTEST_REF_WRAP(L) lhs, \ + const DOCTEST_REF_WRAP(R) rhs) { \ + return lhs op rhs; \ + } + + DOCTEST_RELATIONAL_OP(eq, ==) + DOCTEST_RELATIONAL_OP(ne, !=) + DOCTEST_RELATIONAL_OP(lt, <) + DOCTEST_RELATIONAL_OP(gt, >) + DOCTEST_RELATIONAL_OP(le, <=) + DOCTEST_RELATIONAL_OP(ge, >=) + +#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_CMP_EQ(l, r) l == r +#define DOCTEST_CMP_NE(l, r) l != r +#define DOCTEST_CMP_GT(l, r) l > r +#define DOCTEST_CMP_LT(l, r) l < r +#define DOCTEST_CMP_GE(l, r) l >= r +#define DOCTEST_CMP_LE(l, r) l <= r +#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +#define DOCTEST_CMP_EQ(l, r) eq(l, r) +#define DOCTEST_CMP_NE(l, r) ne(l, r) +#define DOCTEST_CMP_GT(l, r) gt(l, r) +#define DOCTEST_CMP_LT(l, r) lt(l, r) +#define DOCTEST_CMP_GE(l, r) ge(l, r) +#define DOCTEST_CMP_LE(l, r) le(l, r) +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + + template + // cppcheck-suppress copyCtorAndEqOperator + struct Expression_lhs + { + L lhs; + assertType::Enum m_at; + + explicit Expression_lhs(L&& in, assertType::Enum at) + : lhs(static_cast(in)) + , m_at(at) {} + + DOCTEST_NOINLINE operator Result() { +// this is needed only for MSVC 2015 +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4800) // 'int': forcing value to bool + bool res = static_cast(lhs); +DOCTEST_MSVC_SUPPRESS_WARNING_POP + if(m_at & assertType::is_false) { //!OCLINT bitwise operator in conditional + res = !res; + } + + if(!res || getContextOptions()->success) { + return { res, (DOCTEST_STRINGIFY(lhs)) }; + } + return { res }; + } + + /* This is required for user-defined conversions from Expression_lhs to L */ + operator L() const { return lhs; } + + // clang-format off + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(==, " == ", DOCTEST_CMP_EQ) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(!=, " != ", DOCTEST_CMP_NE) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>, " > ", DOCTEST_CMP_GT) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<, " < ", DOCTEST_CMP_LT) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>=, " >= ", DOCTEST_CMP_GE) //!OCLINT bitwise operator in conditional + DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<=, " <= ", DOCTEST_CMP_LE) //!OCLINT bitwise operator in conditional + // clang-format on + + // forbidding some expressions based on this table: https://en.cppreference.com/w/cpp/language/operator_precedence + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &&) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ||) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, =) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, +=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, -=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, *=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, /=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, %=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^=) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |=) + // these 2 are unfortunate because they should be allowed - they have higher precedence over the comparisons, but the + // ExpressionDecomposer class uses the left shift operator to capture the left operand of the binary expression... + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<) + DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>) + }; + +#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + + DOCTEST_CLANG_SUPPRESS_WARNING_POP + DOCTEST_MSVC_SUPPRESS_WARNING_POP + DOCTEST_GCC_SUPPRESS_WARNING_POP + +#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION + +#if DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 6, 0) +DOCTEST_CLANG_SUPPRESS_WARNING_POP +#endif + + struct DOCTEST_INTERFACE ExpressionDecomposer + { + assertType::Enum m_at; + + ExpressionDecomposer(assertType::Enum at); + + // The right operator for capturing expressions is "<=" instead of "<<" (based on the operator precedence table) + // but then there will be warnings from GCC about "-Wparentheses" and since "_Pragma()" is problematic this will stay for now... + // https://github.com/catchorg/Catch2/issues/870 + // https://github.com/catchorg/Catch2/issues/565 + template + Expression_lhs operator<<(L&& operand) { + return Expression_lhs(static_cast(operand), m_at); + } + + template ::value,void >::type* = nullptr> + Expression_lhs operator<<(const L &operand) { + return Expression_lhs(operand, m_at); + } + }; + + struct DOCTEST_INTERFACE TestSuite + { + const char* m_test_suite = nullptr; + const char* m_description = nullptr; + bool m_skip = false; + bool m_no_breaks = false; + bool m_no_output = false; + bool m_may_fail = false; + bool m_should_fail = false; + int m_expected_failures = 0; + double m_timeout = 0; + + TestSuite& operator*(const char* in); + + template + TestSuite& operator*(const T& in) { + in.fill(*this); + return *this; + } + }; + + using funcType = void (*)(); + + struct DOCTEST_INTERFACE TestCase : public TestCaseData + { + funcType m_test; // a function pointer to the test case + + String m_type; // for templated test cases - gets appended to the real name + int m_template_id; // an ID used to distinguish between the different versions of a templated test case + String m_full_name; // contains the name (only for templated test cases!) + the template type + + TestCase(funcType test, const char* file, unsigned line, const TestSuite& test_suite, + const String& type = String(), int template_id = -1); + + TestCase(const TestCase& other); + TestCase(TestCase&&) = delete; + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function + TestCase& operator=(const TestCase& other); + DOCTEST_MSVC_SUPPRESS_WARNING_POP + + TestCase& operator=(TestCase&&) = delete; + + TestCase& operator*(const char* in); + + template + TestCase& operator*(const T& in) { + in.fill(*this); + return *this; + } + + bool operator<(const TestCase& other) const; + + ~TestCase() = default; + }; + + // forward declarations of functions used by the macros + DOCTEST_INTERFACE int regTest(const TestCase& tc); + DOCTEST_INTERFACE int setTestSuite(const TestSuite& ts); + DOCTEST_INTERFACE bool isDebuggerActive(); + + template + int instantiationHelper(const T&) { return 0; } + + namespace binaryAssertComparison { + enum Enum + { + eq = 0, + ne, + gt, + lt, + ge, + le + }; + } // namespace binaryAssertComparison + + // clang-format off + template struct RelationalComparator { bool operator()(const DOCTEST_REF_WRAP(L), const DOCTEST_REF_WRAP(R) ) const { return false; } }; + +#define DOCTEST_BINARY_RELATIONAL_OP(n, op) \ + template struct RelationalComparator { bool operator()(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) const { return op(lhs, rhs); } }; + // clang-format on + + DOCTEST_BINARY_RELATIONAL_OP(0, doctest::detail::eq) + DOCTEST_BINARY_RELATIONAL_OP(1, doctest::detail::ne) + DOCTEST_BINARY_RELATIONAL_OP(2, doctest::detail::gt) + DOCTEST_BINARY_RELATIONAL_OP(3, doctest::detail::lt) + DOCTEST_BINARY_RELATIONAL_OP(4, doctest::detail::ge) + DOCTEST_BINARY_RELATIONAL_OP(5, doctest::detail::le) + + struct DOCTEST_INTERFACE ResultBuilder : public AssertData + { + ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type = "", const String& exception_string = ""); + + ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type, const Contains& exception_string); + + void setResult(const Result& res); + + template + DOCTEST_NOINLINE bool binary_assert(const DOCTEST_REF_WRAP(L) lhs, + const DOCTEST_REF_WRAP(R) rhs) { + m_failed = !RelationalComparator()(lhs, rhs); + if (m_failed || getContextOptions()->success) { + m_decomp = stringifyBinaryExpr(lhs, ", ", rhs); + } + return !m_failed; + } + + template + DOCTEST_NOINLINE bool unary_assert(const DOCTEST_REF_WRAP(L) val) { + m_failed = !val; + + if (m_at & assertType::is_false) { //!OCLINT bitwise operator in conditional + m_failed = !m_failed; + } + + if (m_failed || getContextOptions()->success) { + m_decomp = (DOCTEST_STRINGIFY(val)); + } + + return !m_failed; + } + + void translateException(); + + bool log(); + void react() const; + }; + + namespace assertAction { + enum Enum + { + nothing = 0, + dbgbreak = 1, + shouldthrow = 2 + }; + } // namespace assertAction + + DOCTEST_INTERFACE void failed_out_of_a_testing_context(const AssertData& ad); + + DOCTEST_INTERFACE bool decomp_assert(assertType::Enum at, const char* file, int line, + const char* expr, const Result& result); + +#define DOCTEST_ASSERT_OUT_OF_TESTS(decomp) \ + do { \ + if(!is_running_in_test) { \ + if(failed) { \ + ResultBuilder rb(at, file, line, expr); \ + rb.m_failed = failed; \ + rb.m_decomp = decomp; \ + failed_out_of_a_testing_context(rb); \ + if(isDebuggerActive() && !getContextOptions()->no_breaks) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + if(checkIfShouldThrow(at)) \ + throwException(); \ + } \ + return !failed; \ + } \ + } while(false) + +#define DOCTEST_ASSERT_IN_TESTS(decomp) \ + ResultBuilder rb(at, file, line, expr); \ + rb.m_failed = failed; \ + if(rb.m_failed || getContextOptions()->success) \ + rb.m_decomp = decomp; \ + if(rb.log()) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + if(rb.m_failed && checkIfShouldThrow(at)) \ + throwException() + + template + DOCTEST_NOINLINE bool binary_assert(assertType::Enum at, const char* file, int line, + const char* expr, const DOCTEST_REF_WRAP(L) lhs, + const DOCTEST_REF_WRAP(R) rhs) { + bool failed = !RelationalComparator()(lhs, rhs); + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS(stringifyBinaryExpr(lhs, ", ", rhs)); + DOCTEST_ASSERT_IN_TESTS(stringifyBinaryExpr(lhs, ", ", rhs)); + return !failed; + } + + template + DOCTEST_NOINLINE bool unary_assert(assertType::Enum at, const char* file, int line, + const char* expr, const DOCTEST_REF_WRAP(L) val) { + bool failed = !val; + + if(at & assertType::is_false) //!OCLINT bitwise operator in conditional + failed = !failed; + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS((DOCTEST_STRINGIFY(val))); + DOCTEST_ASSERT_IN_TESTS((DOCTEST_STRINGIFY(val))); + return !failed; + } + + struct DOCTEST_INTERFACE IExceptionTranslator + { + DOCTEST_DECLARE_INTERFACE(IExceptionTranslator) + virtual bool translate(String&) const = 0; + }; + + template + class ExceptionTranslator : public IExceptionTranslator //!OCLINT destructor of virtual class + { + public: + explicit ExceptionTranslator(String (*translateFunction)(T)) + : m_translateFunction(translateFunction) {} + + bool translate(String& res) const override { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + try { + throw; // lgtm [cpp/rethrow-no-exception] + // cppcheck-suppress catchExceptionByValue + } catch(const T& ex) { + res = m_translateFunction(ex); //!OCLINT parameter reassignment + return true; + } catch(...) {} //!OCLINT - empty catch statement +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + static_cast(res); // to silence -Wunused-parameter + return false; + } + + private: + String (*m_translateFunction)(T); + }; + + DOCTEST_INTERFACE void registerExceptionTranslatorImpl(const IExceptionTranslator* et); + + // ContextScope base class used to allow implementing methods of ContextScope + // that don't depend on the template parameter in doctest.cpp. + struct DOCTEST_INTERFACE ContextScopeBase : public IContextScope { + ContextScopeBase(const ContextScopeBase&) = delete; + + ContextScopeBase& operator=(const ContextScopeBase&) = delete; + ContextScopeBase& operator=(ContextScopeBase&&) = delete; + + ~ContextScopeBase() override = default; + + protected: + ContextScopeBase(); + ContextScopeBase(ContextScopeBase&& other) noexcept; + + void destroy(); + bool need_to_destroy{true}; + }; + + template class ContextScope : public ContextScopeBase + { + L lambda_; + + public: + explicit ContextScope(const L &lambda) : lambda_(lambda) {} + explicit ContextScope(L&& lambda) : lambda_(static_cast(lambda)) { } + + ContextScope(const ContextScope&) = delete; + ContextScope(ContextScope&&) noexcept = default; + + ContextScope& operator=(const ContextScope&) = delete; + ContextScope& operator=(ContextScope&&) = delete; + + void stringify(std::ostream* s) const override { lambda_(s); } + + ~ContextScope() override { + if (need_to_destroy) { + destroy(); + } + } + }; + + struct DOCTEST_INTERFACE MessageBuilder : public MessageData + { + std::ostream* m_stream; + bool logged = false; + + MessageBuilder(const char* file, int line, assertType::Enum severity); + + MessageBuilder(const MessageBuilder&) = delete; + MessageBuilder(MessageBuilder&&) = delete; + + MessageBuilder& operator=(const MessageBuilder&) = delete; + MessageBuilder& operator=(MessageBuilder&&) = delete; + + ~MessageBuilder(); + + // the preferred way of chaining parameters for stringification +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4866) + template + MessageBuilder& operator,(const T& in) { + *m_stream << (DOCTEST_STRINGIFY(in)); + return *this; + } +DOCTEST_MSVC_SUPPRESS_WARNING_POP + + // kept here just for backwards-compatibility - the comma operator should be preferred now + template + MessageBuilder& operator<<(const T& in) { return this->operator,(in); } + + // the `,` operator has the lowest operator precedence - if `<<` is used by the user then + // the `,` operator will be called last which is not what we want and thus the `*` operator + // is used first (has higher operator precedence compared to `<<`) so that we guarantee that + // an operator of the MessageBuilder class is called first before the rest of the parameters + template + MessageBuilder& operator*(const T& in) { return this->operator,(in); } + + bool log(); + void react(); + }; + + template + ContextScope MakeContextScope(const L &lambda) { + return ContextScope(lambda); + } +} // namespace detail + +#define DOCTEST_DEFINE_DECORATOR(name, type, def) \ + struct name \ + { \ + type data; \ + name(type in = def) \ + : data(in) {} \ + void fill(detail::TestCase& state) const { state.DOCTEST_CAT(m_, name) = data; } \ + void fill(detail::TestSuite& state) const { state.DOCTEST_CAT(m_, name) = data; } \ + } + +DOCTEST_DEFINE_DECORATOR(test_suite, const char*, ""); +DOCTEST_DEFINE_DECORATOR(description, const char*, ""); +DOCTEST_DEFINE_DECORATOR(skip, bool, true); +DOCTEST_DEFINE_DECORATOR(no_breaks, bool, true); +DOCTEST_DEFINE_DECORATOR(no_output, bool, true); +DOCTEST_DEFINE_DECORATOR(timeout, double, 0); +DOCTEST_DEFINE_DECORATOR(may_fail, bool, true); +DOCTEST_DEFINE_DECORATOR(should_fail, bool, true); +DOCTEST_DEFINE_DECORATOR(expected_failures, int, 0); + +template +int registerExceptionTranslator(String (*translateFunction)(T)) { + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors") + static detail::ExceptionTranslator exceptionTranslator(translateFunction); + DOCTEST_CLANG_SUPPRESS_WARNING_POP + detail::registerExceptionTranslatorImpl(&exceptionTranslator); + return 0; +} + +} // namespace doctest + +// in a separate namespace outside of doctest because the DOCTEST_TEST_SUITE macro +// introduces an anonymous namespace in which getCurrentTestSuite gets overridden +namespace doctest_detail_test_suite_ns { +DOCTEST_INTERFACE doctest::detail::TestSuite& getCurrentTestSuite(); +} // namespace doctest_detail_test_suite_ns + +namespace doctest { +#else // DOCTEST_CONFIG_DISABLE +template +int registerExceptionTranslator(String (*)(T)) { + return 0; +} +#endif // DOCTEST_CONFIG_DISABLE + +namespace detail { + using assert_handler = void (*)(const AssertData&); + struct ContextState; +} // namespace detail + +class DOCTEST_INTERFACE Context +{ + detail::ContextState* p; + + void parseArgs(int argc, const char* const* argv, bool withDefaults = false); + +public: + explicit Context(int argc = 0, const char* const* argv = nullptr); + + Context(const Context&) = delete; + Context(Context&&) = delete; + + Context& operator=(const Context&) = delete; + Context& operator=(Context&&) = delete; + + ~Context(); // NOLINT(performance-trivially-destructible) + + void applyCommandLine(int argc, const char* const* argv); + + void addFilter(const char* filter, const char* value); + void clearFilters(); + void setOption(const char* option, bool value); + void setOption(const char* option, int value); + void setOption(const char* option, const char* value); + + bool shouldExit(); + + void setAsDefaultForAssertsOutOfTestCases(); + + void setAssertHandler(detail::assert_handler ah); + + void setCout(std::ostream* out); + + int run(); +}; + +namespace TestCaseFailureReason { + enum Enum + { + None = 0, + AssertFailure = 1, // an assertion has failed in the test case + Exception = 2, // test case threw an exception + Crash = 4, // a crash... + TooManyFailedAsserts = 8, // the abort-after option + Timeout = 16, // see the timeout decorator + ShouldHaveFailedButDidnt = 32, // see the should_fail decorator + ShouldHaveFailedAndDid = 64, // see the should_fail decorator + DidntFailExactlyNumTimes = 128, // see the expected_failures decorator + FailedExactlyNumTimes = 256, // see the expected_failures decorator + CouldHaveFailedAndDid = 512 // see the may_fail decorator + }; +} // namespace TestCaseFailureReason + +struct DOCTEST_INTERFACE CurrentTestCaseStats +{ + int numAssertsCurrentTest; + int numAssertsFailedCurrentTest; + double seconds; + int failure_flags; // use TestCaseFailureReason::Enum + bool testCaseSuccess; +}; + +struct DOCTEST_INTERFACE TestCaseException +{ + String error_string; + bool is_crash; +}; + +struct DOCTEST_INTERFACE TestRunStats +{ + unsigned numTestCases; + unsigned numTestCasesPassingFilters; + unsigned numTestSuitesPassingFilters; + unsigned numTestCasesFailed; + int numAsserts; + int numAssertsFailed; +}; + +struct QueryData +{ + const TestRunStats* run_stats = nullptr; + const TestCaseData** data = nullptr; + unsigned num_data = 0; +}; + +struct DOCTEST_INTERFACE IReporter +{ + // The constructor has to accept "const ContextOptions&" as a single argument + // which has most of the options for the run + a pointer to the stdout stream + // Reporter(const ContextOptions& in) + + // called when a query should be reported (listing test cases, printing the version, etc.) + virtual void report_query(const QueryData&) = 0; + + // called when the whole test run starts + virtual void test_run_start() = 0; + // called when the whole test run ends (caching a pointer to the input doesn't make sense here) + virtual void test_run_end(const TestRunStats&) = 0; + + // called when a test case is started (safe to cache a pointer to the input) + virtual void test_case_start(const TestCaseData&) = 0; + // called when a test case is reentered because of unfinished subcases (safe to cache a pointer to the input) + virtual void test_case_reenter(const TestCaseData&) = 0; + // called when a test case has ended + virtual void test_case_end(const CurrentTestCaseStats&) = 0; + + // called when an exception is thrown from the test case (or it crashes) + virtual void test_case_exception(const TestCaseException&) = 0; + + // called whenever a subcase is entered (don't cache pointers to the input) + virtual void subcase_start(const SubcaseSignature&) = 0; + // called whenever a subcase is exited (don't cache pointers to the input) + virtual void subcase_end() = 0; + + // called for each assert (don't cache pointers to the input) + virtual void log_assert(const AssertData&) = 0; + // called for each message (don't cache pointers to the input) + virtual void log_message(const MessageData&) = 0; + + // called when a test case is skipped either because it doesn't pass the filters, has a skip decorator + // or isn't in the execution range (between first and last) (safe to cache a pointer to the input) + virtual void test_case_skipped(const TestCaseData&) = 0; + + DOCTEST_DECLARE_INTERFACE(IReporter) + + // can obtain all currently active contexts and stringify them if one wishes to do so + static int get_num_active_contexts(); + static const IContextScope* const* get_active_contexts(); + + // can iterate through contexts which have been stringified automatically in their destructors when an exception has been thrown + static int get_num_stringified_contexts(); + static const String* get_stringified_contexts(); +}; + +namespace detail { + using reporterCreatorFunc = IReporter* (*)(const ContextOptions&); + + DOCTEST_INTERFACE void registerReporterImpl(const char* name, int prio, reporterCreatorFunc c, bool isReporter); + + template + IReporter* reporterCreator(const ContextOptions& o) { + return new Reporter(o); + } +} // namespace detail + +template +int registerReporter(const char* name, int priority, bool isReporter) { + detail::registerReporterImpl(name, priority, detail::reporterCreator, isReporter); + return 0; +} +} // namespace doctest + +#ifdef DOCTEST_CONFIG_ASSERTS_RETURN_VALUES +#define DOCTEST_FUNC_EMPTY [] { return false; }() +#else +#define DOCTEST_FUNC_EMPTY (void)0 +#endif + +// if registering is not disabled +#ifndef DOCTEST_CONFIG_DISABLE + +#ifdef DOCTEST_CONFIG_ASSERTS_RETURN_VALUES +#define DOCTEST_FUNC_SCOPE_BEGIN [&] +#define DOCTEST_FUNC_SCOPE_END () +#define DOCTEST_FUNC_SCOPE_RET(v) return v +#else +#define DOCTEST_FUNC_SCOPE_BEGIN do +#define DOCTEST_FUNC_SCOPE_END while(false) +#define DOCTEST_FUNC_SCOPE_RET(v) (void)0 +#endif + +// common code in asserts - for convenience +#define DOCTEST_ASSERT_LOG_REACT_RETURN(b) \ + if(b.log()) DOCTEST_BREAK_INTO_DEBUGGER(); \ + b.react(); \ + DOCTEST_FUNC_SCOPE_RET(!b.m_failed) + +#ifdef DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#define DOCTEST_WRAP_IN_TRY(x) x; +#else // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS +#define DOCTEST_WRAP_IN_TRY(x) \ + try { \ + x; \ + } catch(...) { DOCTEST_RB.translateException(); } +#endif // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS + +#ifdef DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS +#define DOCTEST_CAST_TO_VOID(...) \ + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wuseless-cast") \ + static_cast(__VA_ARGS__); \ + DOCTEST_GCC_SUPPRESS_WARNING_POP +#else // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS +#define DOCTEST_CAST_TO_VOID(...) __VA_ARGS__; +#endif // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS + +// registers the test by initializing a dummy var with a function +#define DOCTEST_REGISTER_FUNCTION(global_prefix, f, decorators) \ + global_prefix DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), /* NOLINT */ \ + doctest::detail::regTest( \ + doctest::detail::TestCase( \ + f, __FILE__, __LINE__, \ + doctest_detail_test_suite_ns::getCurrentTestSuite()) * \ + decorators)) + +#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, decorators) \ + namespace { /* NOLINT */ \ + struct der : public base \ + { \ + void f(); \ + }; \ + static DOCTEST_INLINE_NOINLINE void func() { \ + der v; \ + v.f(); \ + } \ + DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, func, decorators) \ + } \ + DOCTEST_INLINE_NOINLINE void der::f() // NOLINT(misc-definitions-in-headers) + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, decorators) \ + static void f(); \ + DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, f, decorators) \ + static void f() + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS(f, proxy, decorators) \ + static doctest::detail::funcType proxy() { return f; } \ + DOCTEST_REGISTER_FUNCTION(inline, proxy(), decorators) \ + static void f() + +// for registering tests +#define DOCTEST_TEST_CASE(decorators) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), decorators) + +// for registering tests in classes - requires C++17 for inline variables! +#if DOCTEST_CPLUSPLUS >= 201703L +#define DOCTEST_TEST_CASE_CLASS(decorators) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_PROXY_), \ + decorators) +#else // DOCTEST_TEST_CASE_CLASS +#define DOCTEST_TEST_CASE_CLASS(...) \ + TEST_CASES_CAN_BE_REGISTERED_IN_CLASSES_ONLY_IN_CPP17_MODE_OR_WITH_VS_2017_OR_NEWER +#endif // DOCTEST_TEST_CASE_CLASS + +// for registering tests with a fixture +#define DOCTEST_TEST_CASE_FIXTURE(c, decorators) \ + DOCTEST_IMPLEMENT_FIXTURE(DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), c, \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), decorators) + +// for converting types to strings without the header and demangling +#define DOCTEST_TYPE_TO_STRING_AS(str, ...) \ + namespace doctest { \ + template <> \ + inline String toString<__VA_ARGS__>() { \ + return str; \ + } \ + } \ + static_assert(true, "") + +#define DOCTEST_TYPE_TO_STRING(...) DOCTEST_TYPE_TO_STRING_AS(#__VA_ARGS__, __VA_ARGS__) + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, iter, func) \ + template \ + static void func(); \ + namespace { /* NOLINT */ \ + template \ + struct iter; \ + template \ + struct iter> \ + { \ + iter(const char* file, unsigned line, int index) { \ + doctest::detail::regTest(doctest::detail::TestCase(func, file, line, \ + doctest_detail_test_suite_ns::getCurrentTestSuite(), \ + doctest::toString(), \ + int(line) * 1000 + index) \ + * dec); \ + iter>(file, line, index + 1); \ + } \ + }; \ + template <> \ + struct iter> \ + { \ + iter(const char*, unsigned, int) {} \ + }; \ + } \ + template \ + static void func() + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(dec, T, id) \ + DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(id, ITERATOR), \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)) + +#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, anon, ...) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_CAT(anon, DUMMY), /* NOLINT(cert-err58-cpp, fuchsia-statically-constructed-objects) */ \ + doctest::detail::instantiationHelper( \ + DOCTEST_CAT(id, ITERATOR)<__VA_ARGS__>(__FILE__, __LINE__, 0))) + +#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), std::tuple<__VA_ARGS__>) \ + static_assert(true, "") + +#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), __VA_ARGS__) \ + static_assert(true, "") + +#define DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, anon, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(anon, ITERATOR), anon); \ + DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(anon, anon, std::tuple<__VA_ARGS__>) \ + template \ + static void anon() + +#define DOCTEST_TEST_CASE_TEMPLATE(dec, T, ...) \ + DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), __VA_ARGS__) + +// for subcases +#define DOCTEST_SUBCASE(name) \ + if(const doctest::detail::Subcase & DOCTEST_ANONYMOUS(DOCTEST_ANON_SUBCASE_) DOCTEST_UNUSED = \ + doctest::detail::Subcase(name, __FILE__, __LINE__)) + +// for grouping tests in test suites by using code blocks +#define DOCTEST_TEST_SUITE_IMPL(decorators, ns_name) \ + namespace ns_name { namespace doctest_detail_test_suite_ns { \ + static DOCTEST_NOINLINE doctest::detail::TestSuite& getCurrentTestSuite() noexcept { \ + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4640) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors") \ + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wmissing-field-initializers") \ + static doctest::detail::TestSuite data{}; \ + static bool inited = false; \ + DOCTEST_MSVC_SUPPRESS_WARNING_POP \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP \ + DOCTEST_GCC_SUPPRESS_WARNING_POP \ + if(!inited) { \ + data* decorators; \ + inited = true; \ + } \ + return data; \ + } \ + } \ + } \ + namespace ns_name + +#define DOCTEST_TEST_SUITE(decorators) \ + DOCTEST_TEST_SUITE_IMPL(decorators, DOCTEST_ANONYMOUS(DOCTEST_ANON_SUITE_)) + +// for starting a testsuite block +#define DOCTEST_TEST_SUITE_BEGIN(decorators) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), /* NOLINT(cert-err58-cpp) */ \ + doctest::detail::setTestSuite(doctest::detail::TestSuite() * decorators)) \ + static_assert(true, "") + +// for ending a testsuite block +#define DOCTEST_TEST_SUITE_END \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), /* NOLINT(cert-err58-cpp) */ \ + doctest::detail::setTestSuite(doctest::detail::TestSuite() * "")) \ + using DOCTEST_ANONYMOUS(DOCTEST_ANON_FOR_SEMICOLON_) = int + +// for registering exception translators +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(translatorName, signature) \ + inline doctest::String translatorName(signature); \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_), /* NOLINT(cert-err58-cpp) */ \ + doctest::registerExceptionTranslator(translatorName)) \ + doctest::String translatorName(signature) + +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \ + DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_), \ + signature) + +// for registering reporters +#define DOCTEST_REGISTER_REPORTER(name, priority, reporter) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_REPORTER_), /* NOLINT(cert-err58-cpp) */ \ + doctest::registerReporter(name, priority, true)) \ + static_assert(true, "") + +// for registering listeners +#define DOCTEST_REGISTER_LISTENER(name, priority, reporter) \ + DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_REPORTER_), /* NOLINT(cert-err58-cpp) */ \ + doctest::registerReporter(name, priority, false)) \ + static_assert(true, "") + +// clang-format off +// for logging - disabling formatting because it's important to have these on 2 separate lines - see PR #557 +#define DOCTEST_INFO(...) \ + DOCTEST_INFO_IMPL(DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_), \ + DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_OTHER_), \ + __VA_ARGS__) +// clang-format on + +#define DOCTEST_INFO_IMPL(mb_name, s_name, ...) \ + auto DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_) = doctest::detail::MakeContextScope( \ + [&](std::ostream* s_name) { \ + doctest::detail::MessageBuilder mb_name(__FILE__, __LINE__, doctest::assertType::is_warn); \ + mb_name.m_stream = s_name; \ + mb_name * __VA_ARGS__; \ + }) + +#define DOCTEST_CAPTURE(x) DOCTEST_INFO(#x " := ", x) + +#define DOCTEST_ADD_AT_IMPL(type, file, line, mb, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + doctest::detail::MessageBuilder mb(file, line, doctest::assertType::type); \ + mb * __VA_ARGS__; \ + if(mb.log()) \ + DOCTEST_BREAK_INTO_DEBUGGER(); \ + mb.react(); \ + } DOCTEST_FUNC_SCOPE_END + +// clang-format off +#define DOCTEST_ADD_MESSAGE_AT(file, line, ...) DOCTEST_ADD_AT_IMPL(is_warn, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) +#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, ...) DOCTEST_ADD_AT_IMPL(is_check, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) +#define DOCTEST_ADD_FAIL_AT(file, line, ...) DOCTEST_ADD_AT_IMPL(is_require, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__) +// clang-format on + +#define DOCTEST_MESSAGE(...) DOCTEST_ADD_MESSAGE_AT(__FILE__, __LINE__, __VA_ARGS__) +#define DOCTEST_FAIL_CHECK(...) DOCTEST_ADD_FAIL_CHECK_AT(__FILE__, __LINE__, __VA_ARGS__) +#define DOCTEST_FAIL(...) DOCTEST_ADD_FAIL_AT(__FILE__, __LINE__, __VA_ARGS__) + +#define DOCTEST_TO_LVALUE(...) __VA_ARGS__ // Not removed to keep backwards compatibility. + +#ifndef DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_ASSERT_IMPLEMENT_2(assert_type, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \ + /* NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) */ \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY(DOCTEST_RB.setResult( \ + doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) \ + << __VA_ARGS__)) /* NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) */ \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB) \ + DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + DOCTEST_ASSERT_IMPLEMENT_2(assert_type, __VA_ARGS__); \ + } DOCTEST_FUNC_SCOPE_END // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + +#define DOCTEST_BINARY_ASSERT(assert_type, comp, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY( \ + DOCTEST_RB.binary_assert( \ + __VA_ARGS__)) \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } DOCTEST_FUNC_SCOPE_END + +#define DOCTEST_UNARY_ASSERT(assert_type, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + DOCTEST_WRAP_IN_TRY(DOCTEST_RB.unary_assert(__VA_ARGS__)) \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } DOCTEST_FUNC_SCOPE_END + +#else // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +// necessary for _MESSAGE +#define DOCTEST_ASSERT_IMPLEMENT_2 DOCTEST_ASSERT_IMPLEMENT_1 + +#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \ + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \ + doctest::detail::decomp_assert( \ + doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, \ + doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) \ + << __VA_ARGS__) DOCTEST_CLANG_SUPPRESS_WARNING_POP + +#define DOCTEST_BINARY_ASSERT(assert_type, comparison, ...) \ + doctest::detail::binary_assert( \ + doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, __VA_ARGS__) + +#define DOCTEST_UNARY_ASSERT(assert_type, ...) \ + doctest::detail::unary_assert(doctest::assertType::assert_type, __FILE__, __LINE__, \ + #__VA_ARGS__, __VA_ARGS__) + +#endif // DOCTEST_CONFIG_SUPER_FAST_ASSERTS + +#define DOCTEST_WARN(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN, __VA_ARGS__) +#define DOCTEST_CHECK(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK, __VA_ARGS__) +#define DOCTEST_REQUIRE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE, __VA_ARGS__) +#define DOCTEST_WARN_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN_FALSE, __VA_ARGS__) +#define DOCTEST_CHECK_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK_FALSE, __VA_ARGS__) +#define DOCTEST_REQUIRE_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE_FALSE, __VA_ARGS__) + +// clang-format off +#define DOCTEST_WARN_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN_FALSE, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK_FALSE, cond); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE_FALSE, cond); } DOCTEST_FUNC_SCOPE_END +// clang-format on + +#define DOCTEST_WARN_EQ(...) DOCTEST_BINARY_ASSERT(DT_WARN_EQ, eq, __VA_ARGS__) +#define DOCTEST_CHECK_EQ(...) DOCTEST_BINARY_ASSERT(DT_CHECK_EQ, eq, __VA_ARGS__) +#define DOCTEST_REQUIRE_EQ(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_EQ, eq, __VA_ARGS__) +#define DOCTEST_WARN_NE(...) DOCTEST_BINARY_ASSERT(DT_WARN_NE, ne, __VA_ARGS__) +#define DOCTEST_CHECK_NE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_NE, ne, __VA_ARGS__) +#define DOCTEST_REQUIRE_NE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_NE, ne, __VA_ARGS__) +#define DOCTEST_WARN_GT(...) DOCTEST_BINARY_ASSERT(DT_WARN_GT, gt, __VA_ARGS__) +#define DOCTEST_CHECK_GT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GT, gt, __VA_ARGS__) +#define DOCTEST_REQUIRE_GT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GT, gt, __VA_ARGS__) +#define DOCTEST_WARN_LT(...) DOCTEST_BINARY_ASSERT(DT_WARN_LT, lt, __VA_ARGS__) +#define DOCTEST_CHECK_LT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LT, lt, __VA_ARGS__) +#define DOCTEST_REQUIRE_LT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LT, lt, __VA_ARGS__) +#define DOCTEST_WARN_GE(...) DOCTEST_BINARY_ASSERT(DT_WARN_GE, ge, __VA_ARGS__) +#define DOCTEST_CHECK_GE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GE, ge, __VA_ARGS__) +#define DOCTEST_REQUIRE_GE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GE, ge, __VA_ARGS__) +#define DOCTEST_WARN_LE(...) DOCTEST_BINARY_ASSERT(DT_WARN_LE, le, __VA_ARGS__) +#define DOCTEST_CHECK_LE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LE, le, __VA_ARGS__) +#define DOCTEST_REQUIRE_LE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LE, le, __VA_ARGS__) + +#define DOCTEST_WARN_UNARY(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY, __VA_ARGS__) +#define DOCTEST_CHECK_UNARY(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY, __VA_ARGS__) +#define DOCTEST_REQUIRE_UNARY(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY, __VA_ARGS__) +#define DOCTEST_WARN_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY_FALSE, __VA_ARGS__) +#define DOCTEST_CHECK_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY_FALSE, __VA_ARGS__) +#define DOCTEST_REQUIRE_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY_FALSE, __VA_ARGS__) + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + +#define DOCTEST_ASSERT_THROWS_AS(expr, assert_type, message, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + if(!doctest::getContextOptions()->no_throw) { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #expr, #__VA_ARGS__, message); \ + try { \ + DOCTEST_CAST_TO_VOID(expr) \ + } catch(const typename doctest::detail::types::remove_const< \ + typename doctest::detail::types::remove_reference<__VA_ARGS__>::type>::type&) {\ + DOCTEST_RB.translateException(); \ + DOCTEST_RB.m_threw_as = true; \ + } catch(...) { DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } else { /* NOLINT(*-else-after-return) */ \ + DOCTEST_FUNC_SCOPE_RET(false); \ + } \ + } DOCTEST_FUNC_SCOPE_END + +#define DOCTEST_ASSERT_THROWS_WITH(expr, expr_str, assert_type, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + if(!doctest::getContextOptions()->no_throw) { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, expr_str, "", __VA_ARGS__); \ + try { \ + DOCTEST_CAST_TO_VOID(expr) \ + } catch(...) { DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } else { /* NOLINT(*-else-after-return) */ \ + DOCTEST_FUNC_SCOPE_RET(false); \ + } \ + } DOCTEST_FUNC_SCOPE_END + +#define DOCTEST_ASSERT_NOTHROW(assert_type, ...) \ + DOCTEST_FUNC_SCOPE_BEGIN { \ + doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \ + __LINE__, #__VA_ARGS__); \ + try { \ + DOCTEST_CAST_TO_VOID(__VA_ARGS__) \ + } catch(...) { DOCTEST_RB.translateException(); } \ + DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \ + } DOCTEST_FUNC_SCOPE_END + +// clang-format off +#define DOCTEST_WARN_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_WARN_THROWS, "") +#define DOCTEST_CHECK_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_CHECK_THROWS, "") +#define DOCTEST_REQUIRE_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_REQUIRE_THROWS, "") + +#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_AS, "", __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_AS, "", __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_AS, "", __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_WARN_THROWS_WITH, __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_CHECK_THROWS_WITH, __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_REQUIRE_THROWS_WITH, __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_WITH_AS, message, __VA_ARGS__) +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_WITH_AS, message, __VA_ARGS__) +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_WITH_AS, message, __VA_ARGS__) + +#define DOCTEST_WARN_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_WARN_NOTHROW, __VA_ARGS__) +#define DOCTEST_CHECK_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_CHECK_NOTHROW, __VA_ARGS__) +#define DOCTEST_REQUIRE_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_REQUIRE_NOTHROW, __VA_ARGS__) + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_AS(expr, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_AS(expr, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_AS(expr, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_WITH(expr, with); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_WITH(expr, with); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_WITH(expr, with); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_WITH_AS(expr, with, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ex); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_NOTHROW(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_NOTHROW(expr); } DOCTEST_FUNC_SCOPE_END +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_NOTHROW(expr); } DOCTEST_FUNC_SCOPE_END +// clang-format on + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +// ================================================================================================= +// == WHAT FOLLOWS IS VERSIONS OF THE MACROS THAT DO NOT DO ANY REGISTERING! == +// == THIS CAN BE ENABLED BY DEFINING DOCTEST_CONFIG_DISABLE GLOBALLY! == +// ================================================================================================= +#else // DOCTEST_CONFIG_DISABLE + +#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, name) \ + namespace /* NOLINT */ { \ + template \ + struct der : public base \ + { void f(); }; \ + } \ + template \ + inline void der::f() + +#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, name) \ + template \ + static inline void f() + +// for registering tests +#define DOCTEST_TEST_CASE(name) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) + +// for registering tests in classes +#define DOCTEST_TEST_CASE_CLASS(name) \ + DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) + +// for registering tests with a fixture +#define DOCTEST_TEST_CASE_FIXTURE(x, name) \ + DOCTEST_IMPLEMENT_FIXTURE(DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), x, \ + DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name) + +// for converting types to strings without the header and demangling +#define DOCTEST_TYPE_TO_STRING_AS(str, ...) static_assert(true, "") +#define DOCTEST_TYPE_TO_STRING(...) static_assert(true, "") + +// for typed tests +#define DOCTEST_TEST_CASE_TEMPLATE(name, type, ...) \ + template \ + inline void DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)() + +#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(name, type, id) \ + template \ + inline void DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)() + +#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) static_assert(true, "") +#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) static_assert(true, "") + +// for subcases +#define DOCTEST_SUBCASE(name) + +// for a testsuite block +#define DOCTEST_TEST_SUITE(name) namespace // NOLINT + +// for starting a testsuite block +#define DOCTEST_TEST_SUITE_BEGIN(name) static_assert(true, "") + +// for ending a testsuite block +#define DOCTEST_TEST_SUITE_END using DOCTEST_ANONYMOUS(DOCTEST_ANON_FOR_SEMICOLON_) = int + +#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \ + template \ + static inline doctest::String DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_)(signature) + +#define DOCTEST_REGISTER_REPORTER(name, priority, reporter) +#define DOCTEST_REGISTER_LISTENER(name, priority, reporter) + +#define DOCTEST_INFO(...) (static_cast(0)) +#define DOCTEST_CAPTURE(x) (static_cast(0)) +#define DOCTEST_ADD_MESSAGE_AT(file, line, ...) (static_cast(0)) +#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, ...) (static_cast(0)) +#define DOCTEST_ADD_FAIL_AT(file, line, ...) (static_cast(0)) +#define DOCTEST_MESSAGE(...) (static_cast(0)) +#define DOCTEST_FAIL_CHECK(...) (static_cast(0)) +#define DOCTEST_FAIL(...) (static_cast(0)) + +#if defined(DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED) \ + && defined(DOCTEST_CONFIG_ASSERTS_RETURN_VALUES) + +#define DOCTEST_WARN(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_CHECK(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_REQUIRE(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_WARN_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_CHECK_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_FALSE(...) [&] { return !(__VA_ARGS__); }() + +#define DOCTEST_WARN_MESSAGE(cond, ...) [&] { return cond; }() +#define DOCTEST_CHECK_MESSAGE(cond, ...) [&] { return cond; }() +#define DOCTEST_REQUIRE_MESSAGE(cond, ...) [&] { return cond; }() +#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }() + +namespace doctest { +namespace detail { +#define DOCTEST_RELATIONAL_OP(name, op) \ + template \ + bool name(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) { return lhs op rhs; } + + DOCTEST_RELATIONAL_OP(eq, ==) + DOCTEST_RELATIONAL_OP(ne, !=) + DOCTEST_RELATIONAL_OP(lt, <) + DOCTEST_RELATIONAL_OP(gt, >) + DOCTEST_RELATIONAL_OP(le, <=) + DOCTEST_RELATIONAL_OP(ge, >=) +} // namespace detail +} // namespace doctest + +#define DOCTEST_WARN_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() +#define DOCTEST_CHECK_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }() +#define DOCTEST_WARN_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() +#define DOCTEST_CHECK_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }() +#define DOCTEST_WARN_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() +#define DOCTEST_CHECK_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }() +#define DOCTEST_WARN_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() +#define DOCTEST_CHECK_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }() +#define DOCTEST_WARN_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() +#define DOCTEST_CHECK_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }() +#define DOCTEST_WARN_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() +#define DOCTEST_CHECK_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }() +#define DOCTEST_WARN_UNARY(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_CHECK_UNARY(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_REQUIRE_UNARY(...) [&] { return __VA_ARGS__; }() +#define DOCTEST_WARN_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_CHECK_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() +#define DOCTEST_REQUIRE_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }() + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + +#define DOCTEST_WARN_THROWS_WITH(expr, with, ...) [] { static_assert(false, "Exception translation is not available when doctest is disabled."); return false; }() +#define DOCTEST_CHECK_THROWS_WITH(expr, with, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_REQUIRE_THROWS_WITH(expr, with, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,) + +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,) +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,) + +#define DOCTEST_WARN_THROWS(...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_CHECK_THROWS(...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_REQUIRE_THROWS(...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_WARN_THROWS_AS(expr, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_CHECK_THROWS_AS(expr, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_WARN_NOTHROW(...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +#define DOCTEST_CHECK_NOTHROW(...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +#define DOCTEST_REQUIRE_NOTHROW(...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }() +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }() +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }() + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +#else // DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED + +#define DOCTEST_WARN(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_FALSE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_FALSE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_FALSE(...) DOCTEST_FUNC_EMPTY + +#define DOCTEST_WARN_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY + +#define DOCTEST_WARN_EQ(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_EQ(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_EQ(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_NE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_NE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_NE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_GT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_GT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_GT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_LT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_LT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_LT(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_GE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_GE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_GE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_LE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_LE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_LE(...) DOCTEST_FUNC_EMPTY + +#define DOCTEST_WARN_UNARY(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_UNARY(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_UNARY(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_UNARY_FALSE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_UNARY_FALSE(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_UNARY_FALSE(...) DOCTEST_FUNC_EMPTY + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + +#define DOCTEST_WARN_THROWS(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_NOTHROW(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_NOTHROW(...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_NOTHROW(...) DOCTEST_FUNC_EMPTY + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +#endif // DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED + +#endif // DOCTEST_CONFIG_DISABLE + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS + +#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#define DOCTEST_EXCEPTION_EMPTY_FUNC DOCTEST_FUNC_EMPTY +#else // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS +#define DOCTEST_EXCEPTION_EMPTY_FUNC [] { static_assert(false, "Exceptions are disabled! " \ + "Use DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS if you want to compile with exceptions disabled."); return false; }() + +#undef DOCTEST_REQUIRE +#undef DOCTEST_REQUIRE_FALSE +#undef DOCTEST_REQUIRE_MESSAGE +#undef DOCTEST_REQUIRE_FALSE_MESSAGE +#undef DOCTEST_REQUIRE_EQ +#undef DOCTEST_REQUIRE_NE +#undef DOCTEST_REQUIRE_GT +#undef DOCTEST_REQUIRE_LT +#undef DOCTEST_REQUIRE_GE +#undef DOCTEST_REQUIRE_LE +#undef DOCTEST_REQUIRE_UNARY +#undef DOCTEST_REQUIRE_UNARY_FALSE + +#define DOCTEST_REQUIRE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_FALSE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_MESSAGE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_FALSE_MESSAGE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_EQ DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_NE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_GT DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_LT DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_GE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_LE DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_UNARY DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_UNARY_FALSE DOCTEST_EXCEPTION_EMPTY_FUNC + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS + +#define DOCTEST_WARN_THROWS(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_NOTHROW(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_NOTHROW(...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_NOTHROW(...) DOCTEST_EXCEPTION_EMPTY_FUNC + +#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC +#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC + +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + +// clang-format off +// KEPT FOR BACKWARDS COMPATIBILITY - FORWARDING TO THE RIGHT MACROS +#define DOCTEST_FAST_WARN_EQ DOCTEST_WARN_EQ +#define DOCTEST_FAST_CHECK_EQ DOCTEST_CHECK_EQ +#define DOCTEST_FAST_REQUIRE_EQ DOCTEST_REQUIRE_EQ +#define DOCTEST_FAST_WARN_NE DOCTEST_WARN_NE +#define DOCTEST_FAST_CHECK_NE DOCTEST_CHECK_NE +#define DOCTEST_FAST_REQUIRE_NE DOCTEST_REQUIRE_NE +#define DOCTEST_FAST_WARN_GT DOCTEST_WARN_GT +#define DOCTEST_FAST_CHECK_GT DOCTEST_CHECK_GT +#define DOCTEST_FAST_REQUIRE_GT DOCTEST_REQUIRE_GT +#define DOCTEST_FAST_WARN_LT DOCTEST_WARN_LT +#define DOCTEST_FAST_CHECK_LT DOCTEST_CHECK_LT +#define DOCTEST_FAST_REQUIRE_LT DOCTEST_REQUIRE_LT +#define DOCTEST_FAST_WARN_GE DOCTEST_WARN_GE +#define DOCTEST_FAST_CHECK_GE DOCTEST_CHECK_GE +#define DOCTEST_FAST_REQUIRE_GE DOCTEST_REQUIRE_GE +#define DOCTEST_FAST_WARN_LE DOCTEST_WARN_LE +#define DOCTEST_FAST_CHECK_LE DOCTEST_CHECK_LE +#define DOCTEST_FAST_REQUIRE_LE DOCTEST_REQUIRE_LE + +#define DOCTEST_FAST_WARN_UNARY DOCTEST_WARN_UNARY +#define DOCTEST_FAST_CHECK_UNARY DOCTEST_CHECK_UNARY +#define DOCTEST_FAST_REQUIRE_UNARY DOCTEST_REQUIRE_UNARY +#define DOCTEST_FAST_WARN_UNARY_FALSE DOCTEST_WARN_UNARY_FALSE +#define DOCTEST_FAST_CHECK_UNARY_FALSE DOCTEST_CHECK_UNARY_FALSE +#define DOCTEST_FAST_REQUIRE_UNARY_FALSE DOCTEST_REQUIRE_UNARY_FALSE + +#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id,__VA_ARGS__) +// clang-format on + +// BDD style macros +// clang-format off +#define DOCTEST_SCENARIO(name) DOCTEST_TEST_CASE(" Scenario: " name) +#define DOCTEST_SCENARIO_CLASS(name) DOCTEST_TEST_CASE_CLASS(" Scenario: " name) +#define DOCTEST_SCENARIO_TEMPLATE(name, T, ...) DOCTEST_TEST_CASE_TEMPLATE(" Scenario: " name, T, __VA_ARGS__) +#define DOCTEST_SCENARIO_TEMPLATE_DEFINE(name, T, id) DOCTEST_TEST_CASE_TEMPLATE_DEFINE(" Scenario: " name, T, id) + +#define DOCTEST_GIVEN(name) DOCTEST_SUBCASE(" Given: " name) +#define DOCTEST_WHEN(name) DOCTEST_SUBCASE(" When: " name) +#define DOCTEST_AND_WHEN(name) DOCTEST_SUBCASE("And when: " name) +#define DOCTEST_THEN(name) DOCTEST_SUBCASE(" Then: " name) +#define DOCTEST_AND_THEN(name) DOCTEST_SUBCASE(" And: " name) +// clang-format on + +// == SHORT VERSIONS OF THE MACROS +#ifndef DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES + +#define TEST_CASE(name) DOCTEST_TEST_CASE(name) +#define TEST_CASE_CLASS(name) DOCTEST_TEST_CASE_CLASS(name) +#define TEST_CASE_FIXTURE(x, name) DOCTEST_TEST_CASE_FIXTURE(x, name) +#define TYPE_TO_STRING_AS(str, ...) DOCTEST_TYPE_TO_STRING_AS(str, __VA_ARGS__) +#define TYPE_TO_STRING(...) DOCTEST_TYPE_TO_STRING(__VA_ARGS__) +#define TEST_CASE_TEMPLATE(name, T, ...) DOCTEST_TEST_CASE_TEMPLATE(name, T, __VA_ARGS__) +#define TEST_CASE_TEMPLATE_DEFINE(name, T, id) DOCTEST_TEST_CASE_TEMPLATE_DEFINE(name, T, id) +#define TEST_CASE_TEMPLATE_INVOKE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, __VA_ARGS__) +#define TEST_CASE_TEMPLATE_APPLY(id, ...) DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, __VA_ARGS__) +#define SUBCASE(name) DOCTEST_SUBCASE(name) +#define TEST_SUITE(decorators) DOCTEST_TEST_SUITE(decorators) +#define TEST_SUITE_BEGIN(name) DOCTEST_TEST_SUITE_BEGIN(name) +#define TEST_SUITE_END DOCTEST_TEST_SUITE_END +#define REGISTER_EXCEPTION_TRANSLATOR(signature) DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) +#define REGISTER_REPORTER(name, priority, reporter) DOCTEST_REGISTER_REPORTER(name, priority, reporter) +#define REGISTER_LISTENER(name, priority, reporter) DOCTEST_REGISTER_LISTENER(name, priority, reporter) +#define INFO(...) DOCTEST_INFO(__VA_ARGS__) +#define CAPTURE(x) DOCTEST_CAPTURE(x) +#define ADD_MESSAGE_AT(file, line, ...) DOCTEST_ADD_MESSAGE_AT(file, line, __VA_ARGS__) +#define ADD_FAIL_CHECK_AT(file, line, ...) DOCTEST_ADD_FAIL_CHECK_AT(file, line, __VA_ARGS__) +#define ADD_FAIL_AT(file, line, ...) DOCTEST_ADD_FAIL_AT(file, line, __VA_ARGS__) +#define MESSAGE(...) DOCTEST_MESSAGE(__VA_ARGS__) +#define FAIL_CHECK(...) DOCTEST_FAIL_CHECK(__VA_ARGS__) +#define FAIL(...) DOCTEST_FAIL(__VA_ARGS__) +#define TO_LVALUE(...) DOCTEST_TO_LVALUE(__VA_ARGS__) + +#define WARN(...) DOCTEST_WARN(__VA_ARGS__) +#define WARN_FALSE(...) DOCTEST_WARN_FALSE(__VA_ARGS__) +#define WARN_THROWS(...) DOCTEST_WARN_THROWS(__VA_ARGS__) +#define WARN_THROWS_AS(expr, ...) DOCTEST_WARN_THROWS_AS(expr, __VA_ARGS__) +#define WARN_THROWS_WITH(expr, ...) DOCTEST_WARN_THROWS_WITH(expr, __VA_ARGS__) +#define WARN_THROWS_WITH_AS(expr, with, ...) DOCTEST_WARN_THROWS_WITH_AS(expr, with, __VA_ARGS__) +#define WARN_NOTHROW(...) DOCTEST_WARN_NOTHROW(__VA_ARGS__) +#define CHECK(...) DOCTEST_CHECK(__VA_ARGS__) +#define CHECK_FALSE(...) DOCTEST_CHECK_FALSE(__VA_ARGS__) +#define CHECK_THROWS(...) DOCTEST_CHECK_THROWS(__VA_ARGS__) +#define CHECK_THROWS_AS(expr, ...) DOCTEST_CHECK_THROWS_AS(expr, __VA_ARGS__) +#define CHECK_THROWS_WITH(expr, ...) DOCTEST_CHECK_THROWS_WITH(expr, __VA_ARGS__) +#define CHECK_THROWS_WITH_AS(expr, with, ...) DOCTEST_CHECK_THROWS_WITH_AS(expr, with, __VA_ARGS__) +#define CHECK_NOTHROW(...) DOCTEST_CHECK_NOTHROW(__VA_ARGS__) +#define REQUIRE(...) DOCTEST_REQUIRE(__VA_ARGS__) +#define REQUIRE_FALSE(...) DOCTEST_REQUIRE_FALSE(__VA_ARGS__) +#define REQUIRE_THROWS(...) DOCTEST_REQUIRE_THROWS(__VA_ARGS__) +#define REQUIRE_THROWS_AS(expr, ...) DOCTEST_REQUIRE_THROWS_AS(expr, __VA_ARGS__) +#define REQUIRE_THROWS_WITH(expr, ...) DOCTEST_REQUIRE_THROWS_WITH(expr, __VA_ARGS__) +#define REQUIRE_THROWS_WITH_AS(expr, with, ...) DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, __VA_ARGS__) +#define REQUIRE_NOTHROW(...) DOCTEST_REQUIRE_NOTHROW(__VA_ARGS__) + +#define WARN_MESSAGE(cond, ...) DOCTEST_WARN_MESSAGE(cond, __VA_ARGS__) +#define WARN_FALSE_MESSAGE(cond, ...) DOCTEST_WARN_FALSE_MESSAGE(cond, __VA_ARGS__) +#define WARN_THROWS_MESSAGE(expr, ...) DOCTEST_WARN_THROWS_MESSAGE(expr, __VA_ARGS__) +#define WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) +#define WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) +#define WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) +#define WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_WARN_NOTHROW_MESSAGE(expr, __VA_ARGS__) +#define CHECK_MESSAGE(cond, ...) DOCTEST_CHECK_MESSAGE(cond, __VA_ARGS__) +#define CHECK_FALSE_MESSAGE(cond, ...) DOCTEST_CHECK_FALSE_MESSAGE(cond, __VA_ARGS__) +#define CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_CHECK_THROWS_MESSAGE(expr, __VA_ARGS__) +#define CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) +#define CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) +#define CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) +#define CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_CHECK_NOTHROW_MESSAGE(expr, __VA_ARGS__) +#define REQUIRE_MESSAGE(cond, ...) DOCTEST_REQUIRE_MESSAGE(cond, __VA_ARGS__) +#define REQUIRE_FALSE_MESSAGE(cond, ...) DOCTEST_REQUIRE_FALSE_MESSAGE(cond, __VA_ARGS__) +#define REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_REQUIRE_THROWS_MESSAGE(expr, __VA_ARGS__) +#define REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__) +#define REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__) +#define REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__) +#define REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, __VA_ARGS__) + +#define SCENARIO(name) DOCTEST_SCENARIO(name) +#define SCENARIO_CLASS(name) DOCTEST_SCENARIO_CLASS(name) +#define SCENARIO_TEMPLATE(name, T, ...) DOCTEST_SCENARIO_TEMPLATE(name, T, __VA_ARGS__) +#define SCENARIO_TEMPLATE_DEFINE(name, T, id) DOCTEST_SCENARIO_TEMPLATE_DEFINE(name, T, id) +#define GIVEN(name) DOCTEST_GIVEN(name) +#define WHEN(name) DOCTEST_WHEN(name) +#define AND_WHEN(name) DOCTEST_AND_WHEN(name) +#define THEN(name) DOCTEST_THEN(name) +#define AND_THEN(name) DOCTEST_AND_THEN(name) + +#define WARN_EQ(...) DOCTEST_WARN_EQ(__VA_ARGS__) +#define CHECK_EQ(...) DOCTEST_CHECK_EQ(__VA_ARGS__) +#define REQUIRE_EQ(...) DOCTEST_REQUIRE_EQ(__VA_ARGS__) +#define WARN_NE(...) DOCTEST_WARN_NE(__VA_ARGS__) +#define CHECK_NE(...) DOCTEST_CHECK_NE(__VA_ARGS__) +#define REQUIRE_NE(...) DOCTEST_REQUIRE_NE(__VA_ARGS__) +#define WARN_GT(...) DOCTEST_WARN_GT(__VA_ARGS__) +#define CHECK_GT(...) DOCTEST_CHECK_GT(__VA_ARGS__) +#define REQUIRE_GT(...) DOCTEST_REQUIRE_GT(__VA_ARGS__) +#define WARN_LT(...) DOCTEST_WARN_LT(__VA_ARGS__) +#define CHECK_LT(...) DOCTEST_CHECK_LT(__VA_ARGS__) +#define REQUIRE_LT(...) DOCTEST_REQUIRE_LT(__VA_ARGS__) +#define WARN_GE(...) DOCTEST_WARN_GE(__VA_ARGS__) +#define CHECK_GE(...) DOCTEST_CHECK_GE(__VA_ARGS__) +#define REQUIRE_GE(...) DOCTEST_REQUIRE_GE(__VA_ARGS__) +#define WARN_LE(...) DOCTEST_WARN_LE(__VA_ARGS__) +#define CHECK_LE(...) DOCTEST_CHECK_LE(__VA_ARGS__) +#define REQUIRE_LE(...) DOCTEST_REQUIRE_LE(__VA_ARGS__) +#define WARN_UNARY(...) DOCTEST_WARN_UNARY(__VA_ARGS__) +#define CHECK_UNARY(...) DOCTEST_CHECK_UNARY(__VA_ARGS__) +#define REQUIRE_UNARY(...) DOCTEST_REQUIRE_UNARY(__VA_ARGS__) +#define WARN_UNARY_FALSE(...) DOCTEST_WARN_UNARY_FALSE(__VA_ARGS__) +#define CHECK_UNARY_FALSE(...) DOCTEST_CHECK_UNARY_FALSE(__VA_ARGS__) +#define REQUIRE_UNARY_FALSE(...) DOCTEST_REQUIRE_UNARY_FALSE(__VA_ARGS__) + +// KEPT FOR BACKWARDS COMPATIBILITY +#define FAST_WARN_EQ(...) DOCTEST_FAST_WARN_EQ(__VA_ARGS__) +#define FAST_CHECK_EQ(...) DOCTEST_FAST_CHECK_EQ(__VA_ARGS__) +#define FAST_REQUIRE_EQ(...) DOCTEST_FAST_REQUIRE_EQ(__VA_ARGS__) +#define FAST_WARN_NE(...) DOCTEST_FAST_WARN_NE(__VA_ARGS__) +#define FAST_CHECK_NE(...) DOCTEST_FAST_CHECK_NE(__VA_ARGS__) +#define FAST_REQUIRE_NE(...) DOCTEST_FAST_REQUIRE_NE(__VA_ARGS__) +#define FAST_WARN_GT(...) DOCTEST_FAST_WARN_GT(__VA_ARGS__) +#define FAST_CHECK_GT(...) DOCTEST_FAST_CHECK_GT(__VA_ARGS__) +#define FAST_REQUIRE_GT(...) DOCTEST_FAST_REQUIRE_GT(__VA_ARGS__) +#define FAST_WARN_LT(...) DOCTEST_FAST_WARN_LT(__VA_ARGS__) +#define FAST_CHECK_LT(...) DOCTEST_FAST_CHECK_LT(__VA_ARGS__) +#define FAST_REQUIRE_LT(...) DOCTEST_FAST_REQUIRE_LT(__VA_ARGS__) +#define FAST_WARN_GE(...) DOCTEST_FAST_WARN_GE(__VA_ARGS__) +#define FAST_CHECK_GE(...) DOCTEST_FAST_CHECK_GE(__VA_ARGS__) +#define FAST_REQUIRE_GE(...) DOCTEST_FAST_REQUIRE_GE(__VA_ARGS__) +#define FAST_WARN_LE(...) DOCTEST_FAST_WARN_LE(__VA_ARGS__) +#define FAST_CHECK_LE(...) DOCTEST_FAST_CHECK_LE(__VA_ARGS__) +#define FAST_REQUIRE_LE(...) DOCTEST_FAST_REQUIRE_LE(__VA_ARGS__) + +#define FAST_WARN_UNARY(...) DOCTEST_FAST_WARN_UNARY(__VA_ARGS__) +#define FAST_CHECK_UNARY(...) DOCTEST_FAST_CHECK_UNARY(__VA_ARGS__) +#define FAST_REQUIRE_UNARY(...) DOCTEST_FAST_REQUIRE_UNARY(__VA_ARGS__) +#define FAST_WARN_UNARY_FALSE(...) DOCTEST_FAST_WARN_UNARY_FALSE(__VA_ARGS__) +#define FAST_CHECK_UNARY_FALSE(...) DOCTEST_FAST_CHECK_UNARY_FALSE(__VA_ARGS__) +#define FAST_REQUIRE_UNARY_FALSE(...) DOCTEST_FAST_REQUIRE_UNARY_FALSE(__VA_ARGS__) + +#define TEST_CASE_TEMPLATE_INSTANTIATE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE(id, __VA_ARGS__) + +#endif // DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES + +#ifndef DOCTEST_CONFIG_DISABLE + +// this is here to clear the 'current test suite' for the current translation unit - at the top +DOCTEST_TEST_SUITE_END(); + +#endif // DOCTEST_CONFIG_DISABLE + +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_MSVC_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +DOCTEST_SUPPRESS_COMMON_WARNINGS_POP + +#endif // DOCTEST_LIBRARY_INCLUDED + +#ifndef DOCTEST_SINGLE_HEADER +#define DOCTEST_SINGLE_HEADER +#endif // DOCTEST_SINGLE_HEADER + +#if defined(DOCTEST_CONFIG_IMPLEMENT) || !defined(DOCTEST_SINGLE_HEADER) + +#ifndef DOCTEST_SINGLE_HEADER +#include "doctest_fwd.h" +#endif // DOCTEST_SINGLE_HEADER + +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-macros") + +#ifndef DOCTEST_LIBRARY_IMPLEMENTATION +#define DOCTEST_LIBRARY_IMPLEMENTATION + +DOCTEST_CLANG_SUPPRESS_WARNING_POP + +DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH + +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wglobal-constructors") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wshorten-64-to-32") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-variable-declarations") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch-enum") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wcovered-switch-default") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-noreturn") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wdisabled-macro-expansion") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-braces") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-field-initializers") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-member-function") +DOCTEST_CLANG_SUPPRESS_WARNING("-Wnonportable-system-include-path") + +DOCTEST_GCC_SUPPRESS_WARNING_PUSH +DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-field-initializers") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-braces") +DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch") +DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-enum") +DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-default") +DOCTEST_GCC_SUPPRESS_WARNING("-Wunsafe-loop-optimizations") +DOCTEST_GCC_SUPPRESS_WARNING("-Wold-style-cast") +DOCTEST_GCC_SUPPRESS_WARNING("-Wunused-function") +DOCTEST_GCC_SUPPRESS_WARNING("-Wmultiple-inheritance") +DOCTEST_GCC_SUPPRESS_WARNING("-Wsuggest-attribute") + +DOCTEST_MSVC_SUPPRESS_WARNING_PUSH +DOCTEST_MSVC_SUPPRESS_WARNING(4267) // 'var' : conversion from 'x' to 'y', possible loss of data +DOCTEST_MSVC_SUPPRESS_WARNING(4530) // C++ exception handler used, but unwind semantics not enabled +DOCTEST_MSVC_SUPPRESS_WARNING(4577) // 'noexcept' used with no exception handling mode specified +DOCTEST_MSVC_SUPPRESS_WARNING(4774) // format string expected in argument is not a string literal +DOCTEST_MSVC_SUPPRESS_WARNING(4365) // conversion from 'int' to 'unsigned', signed/unsigned mismatch +DOCTEST_MSVC_SUPPRESS_WARNING(5039) // pointer to potentially throwing function passed to extern C +DOCTEST_MSVC_SUPPRESS_WARNING(4800) // forcing value to bool 'true' or 'false' (performance warning) +DOCTEST_MSVC_SUPPRESS_WARNING(5245) // unreferenced function with internal linkage has been removed + +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN + +// required includes - will go only in one translation unit! +#include +#include +#include +// borland (Embarcadero) compiler requires math.h and not cmath - https://github.com/doctest/doctest/pull/37 +#ifdef __BORLANDC__ +#include +#endif // __BORLANDC__ +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM +#include +#endif // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM +#include +#include +#include +#ifndef DOCTEST_CONFIG_NO_MULTITHREADING +#include +#include +#define DOCTEST_DECLARE_MUTEX(name) std::mutex name; +#define DOCTEST_DECLARE_STATIC_MUTEX(name) static DOCTEST_DECLARE_MUTEX(name) +#define DOCTEST_LOCK_MUTEX(name) std::lock_guard DOCTEST_ANONYMOUS(DOCTEST_ANON_LOCK_)(name); +#else // DOCTEST_CONFIG_NO_MULTITHREADING +#define DOCTEST_DECLARE_MUTEX(name) +#define DOCTEST_DECLARE_STATIC_MUTEX(name) +#define DOCTEST_LOCK_MUTEX(name) +#endif // DOCTEST_CONFIG_NO_MULTITHREADING +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef DOCTEST_PLATFORM_MAC +#include +#include +#include +#endif // DOCTEST_PLATFORM_MAC + +#ifdef DOCTEST_PLATFORM_WINDOWS + +// defines for a leaner windows.h +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#define DOCTEST_UNDEF_WIN32_LEAN_AND_MEAN +#endif // WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#define DOCTEST_UNDEF_NOMINMAX +#endif // NOMINMAX + +// not sure what AfxWin.h is for - here I do what Catch does +#ifdef __AFXDLL +#include +#else +#include +#endif +#include + +#else // DOCTEST_PLATFORM_WINDOWS + +#include +#include + +#endif // DOCTEST_PLATFORM_WINDOWS + +// this is a fix for https://github.com/doctest/doctest/issues/348 +// https://mail.gnome.org/archives/xml/2012-January/msg00000.html +#if !defined(HAVE_UNISTD_H) && !defined(STDOUT_FILENO) +#define STDOUT_FILENO fileno(stdout) +#endif // HAVE_UNISTD_H + +DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END + +// counts the number of elements in a C array +#define DOCTEST_COUNTOF(x) (sizeof(x) / sizeof(x[0])) + +#ifdef DOCTEST_CONFIG_DISABLE +#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_disabled +#else // DOCTEST_CONFIG_DISABLE +#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_not_disabled +#endif // DOCTEST_CONFIG_DISABLE + +#ifndef DOCTEST_CONFIG_OPTIONS_PREFIX +#define DOCTEST_CONFIG_OPTIONS_PREFIX "dt-" +#endif + +#ifndef DOCTEST_THREAD_LOCAL +#if defined(DOCTEST_CONFIG_NO_MULTITHREADING) || DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0)) +#define DOCTEST_THREAD_LOCAL +#else // DOCTEST_MSVC +#define DOCTEST_THREAD_LOCAL thread_local +#endif // DOCTEST_MSVC +#endif // DOCTEST_THREAD_LOCAL + +#ifndef DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES +#define DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES 32 +#endif + +#ifndef DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE +#define DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE 64 +#endif + +#ifdef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS +#define DOCTEST_OPTIONS_PREFIX_DISPLAY DOCTEST_CONFIG_OPTIONS_PREFIX +#else +#define DOCTEST_OPTIONS_PREFIX_DISPLAY "" +#endif + +#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP) +#define DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS +#endif + +#ifndef DOCTEST_CDECL +#define DOCTEST_CDECL __cdecl +#endif + +namespace doctest { + +bool is_running_in_test = false; + +namespace { + using namespace detail; + + template + DOCTEST_NORETURN void throw_exception(Ex const& e) { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + throw e; +#else // DOCTEST_CONFIG_NO_EXCEPTIONS +#ifdef DOCTEST_CONFIG_HANDLE_EXCEPTION + DOCTEST_CONFIG_HANDLE_EXCEPTION(e); +#else // DOCTEST_CONFIG_HANDLE_EXCEPTION +#ifndef DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + std::cerr << "doctest will terminate because it needed to throw an exception.\n" + << "The message was: " << e.what() << '\n'; +#endif // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM +#endif // DOCTEST_CONFIG_HANDLE_EXCEPTION + std::terminate(); +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + } + +#ifndef DOCTEST_INTERNAL_ERROR +#define DOCTEST_INTERNAL_ERROR(msg) \ + throw_exception(std::logic_error( \ + __FILE__ ":" DOCTEST_TOSTR(__LINE__) ": Internal doctest error: " msg)) +#endif // DOCTEST_INTERNAL_ERROR + + // case insensitive strcmp + int stricmp(const char* a, const char* b) { + for(;; a++, b++) { + const int d = tolower(*a) - tolower(*b); + if(d != 0 || !*a) + return d; + } + } + + struct Endianness + { + enum Arch + { + Big, + Little + }; + + static Arch which() { + int x = 1; + // casting any data pointer to char* is allowed + auto ptr = reinterpret_cast(&x); + if(*ptr) + return Little; + return Big; + } + }; +} // namespace + +namespace detail { + DOCTEST_THREAD_LOCAL class + { + std::vector stack; + std::stringstream ss; + + public: + std::ostream* push() { + stack.push_back(ss.tellp()); + return &ss; + } + + String pop() { + if (stack.empty()) + DOCTEST_INTERNAL_ERROR("TLSS was empty when trying to pop!"); + + std::streampos pos = stack.back(); + stack.pop_back(); + unsigned sz = static_cast(ss.tellp() - pos); + ss.rdbuf()->pubseekpos(pos, std::ios::in | std::ios::out); + return String(ss, sz); + } + } g_oss; + + std::ostream* tlssPush() { + return g_oss.push(); + } + + String tlssPop() { + return g_oss.pop(); + } + +#ifndef DOCTEST_CONFIG_DISABLE + +namespace timer_large_integer +{ + +#if defined(DOCTEST_PLATFORM_WINDOWS) + using type = ULONGLONG; +#else // DOCTEST_PLATFORM_WINDOWS + using type = std::uint64_t; +#endif // DOCTEST_PLATFORM_WINDOWS +} + +using ticks_t = timer_large_integer::type; + +#ifdef DOCTEST_CONFIG_GETCURRENTTICKS + ticks_t getCurrentTicks() { return DOCTEST_CONFIG_GETCURRENTTICKS(); } +#elif defined(DOCTEST_PLATFORM_WINDOWS) + ticks_t getCurrentTicks() { + static LARGE_INTEGER hz = { {0} }, hzo = { {0} }; + if(!hz.QuadPart) { + QueryPerformanceFrequency(&hz); + QueryPerformanceCounter(&hzo); + } + LARGE_INTEGER t; + QueryPerformanceCounter(&t); + return ((t.QuadPart - hzo.QuadPart) * LONGLONG(1000000)) / hz.QuadPart; + } +#else // DOCTEST_PLATFORM_WINDOWS + ticks_t getCurrentTicks() { + timeval t; + gettimeofday(&t, nullptr); + return static_cast(t.tv_sec) * 1000000 + static_cast(t.tv_usec); + } +#endif // DOCTEST_PLATFORM_WINDOWS + + struct Timer + { + void start() { m_ticks = getCurrentTicks(); } + unsigned int getElapsedMicroseconds() const { + return static_cast(getCurrentTicks() - m_ticks); + } + //unsigned int getElapsedMilliseconds() const { + // return static_cast(getElapsedMicroseconds() / 1000); + //} + double getElapsedSeconds() const { return static_cast(getCurrentTicks() - m_ticks) / 1000000.0; } + + private: + ticks_t m_ticks = 0; + }; + +#ifdef DOCTEST_CONFIG_NO_MULTITHREADING + template + using Atomic = T; +#else // DOCTEST_CONFIG_NO_MULTITHREADING + template + using Atomic = std::atomic; +#endif // DOCTEST_CONFIG_NO_MULTITHREADING + +#if defined(DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS) || defined(DOCTEST_CONFIG_NO_MULTITHREADING) + template + using MultiLaneAtomic = Atomic; +#else // DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS + // Provides a multilane implementation of an atomic variable that supports add, sub, load, + // store. Instead of using a single atomic variable, this splits up into multiple ones, + // each sitting on a separate cache line. The goal is to provide a speedup when most + // operations are modifying. It achieves this with two properties: + // + // * Multiple atomics are used, so chance of congestion from the same atomic is reduced. + // * Each atomic sits on a separate cache line, so false sharing is reduced. + // + // The disadvantage is that there is a small overhead due to the use of TLS, and load/store + // is slower because all atomics have to be accessed. + template + class MultiLaneAtomic + { + struct CacheLineAlignedAtomic + { + Atomic atomic{}; + char padding[DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE - sizeof(Atomic)]; + }; + CacheLineAlignedAtomic m_atomics[DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES]; + + static_assert(sizeof(CacheLineAlignedAtomic) == DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE, + "guarantee one atomic takes exactly one cache line"); + + public: + T operator++() DOCTEST_NOEXCEPT { return fetch_add(1) + 1; } + + T operator++(int) DOCTEST_NOEXCEPT { return fetch_add(1); } + + T fetch_add(T arg, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { + return myAtomic().fetch_add(arg, order); + } + + T fetch_sub(T arg, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { + return myAtomic().fetch_sub(arg, order); + } + + operator T() const DOCTEST_NOEXCEPT { return load(); } + + T load(std::memory_order order = std::memory_order_seq_cst) const DOCTEST_NOEXCEPT { + auto result = T(); + for(auto const& c : m_atomics) { + result += c.atomic.load(order); + } + return result; + } + + T operator=(T desired) DOCTEST_NOEXCEPT { // lgtm [cpp/assignment-does-not-return-this] + store(desired); + return desired; + } + + void store(T desired, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT { + // first value becomes desired", all others become 0. + for(auto& c : m_atomics) { + c.atomic.store(desired, order); + desired = {}; + } + } + + private: + // Each thread has a different atomic that it operates on. If more than NumLanes threads + // use this, some will use the same atomic. So performance will degrade a bit, but still + // everything will work. + // + // The logic here is a bit tricky. The call should be as fast as possible, so that there + // is minimal to no overhead in determining the correct atomic for the current thread. + // + // 1. A global static counter laneCounter counts continuously up. + // 2. Each successive thread will use modulo operation of that counter so it gets an atomic + // assigned in a round-robin fashion. + // 3. This tlsLaneIdx is stored in the thread local data, so it is directly available with + // little overhead. + Atomic& myAtomic() DOCTEST_NOEXCEPT { + static Atomic laneCounter; + DOCTEST_THREAD_LOCAL size_t tlsLaneIdx = + laneCounter++ % DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES; + + return m_atomics[tlsLaneIdx].atomic; + } + }; +#endif // DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS + + // this holds both parameters from the command line and runtime data for tests + struct ContextState : ContextOptions, TestRunStats, CurrentTestCaseStats + { + MultiLaneAtomic numAssertsCurrentTest_atomic; + MultiLaneAtomic numAssertsFailedCurrentTest_atomic; + + std::vector> filters = decltype(filters)(9); // 9 different filters + + std::vector reporters_currently_used; + + assert_handler ah = nullptr; + + Timer timer; + + std::vector stringifiedContexts; // logging from INFO() due to an exception + + // stuff for subcases + bool reachedLeaf; + std::vector subcaseStack; + std::vector nextSubcaseStack; + std::unordered_set fullyTraversedSubcases; + size_t currentSubcaseDepth; + Atomic shouldLogCurrentException; + + void resetRunData() { + numTestCases = 0; + numTestCasesPassingFilters = 0; + numTestSuitesPassingFilters = 0; + numTestCasesFailed = 0; + numAsserts = 0; + numAssertsFailed = 0; + numAssertsCurrentTest = 0; + numAssertsFailedCurrentTest = 0; + } + + void finalizeTestCaseData() { + seconds = timer.getElapsedSeconds(); + + // update the non-atomic counters + numAsserts += numAssertsCurrentTest_atomic; + numAssertsFailed += numAssertsFailedCurrentTest_atomic; + numAssertsCurrentTest = numAssertsCurrentTest_atomic; + numAssertsFailedCurrentTest = numAssertsFailedCurrentTest_atomic; + + if(numAssertsFailedCurrentTest) + failure_flags |= TestCaseFailureReason::AssertFailure; + + if(Approx(currentTest->m_timeout).epsilon(DBL_EPSILON) != 0 && + Approx(seconds).epsilon(DBL_EPSILON) > currentTest->m_timeout) + failure_flags |= TestCaseFailureReason::Timeout; + + if(currentTest->m_should_fail) { + if(failure_flags) { + failure_flags |= TestCaseFailureReason::ShouldHaveFailedAndDid; + } else { + failure_flags |= TestCaseFailureReason::ShouldHaveFailedButDidnt; + } + } else if(failure_flags && currentTest->m_may_fail) { + failure_flags |= TestCaseFailureReason::CouldHaveFailedAndDid; + } else if(currentTest->m_expected_failures > 0) { + if(numAssertsFailedCurrentTest == currentTest->m_expected_failures) { + failure_flags |= TestCaseFailureReason::FailedExactlyNumTimes; + } else { + failure_flags |= TestCaseFailureReason::DidntFailExactlyNumTimes; + } + } + + bool ok_to_fail = (TestCaseFailureReason::ShouldHaveFailedAndDid & failure_flags) || + (TestCaseFailureReason::CouldHaveFailedAndDid & failure_flags) || + (TestCaseFailureReason::FailedExactlyNumTimes & failure_flags); + + // if any subcase has failed - the whole test case has failed + testCaseSuccess = !(failure_flags && !ok_to_fail); + if(!testCaseSuccess) + numTestCasesFailed++; + } + }; + + ContextState* g_cs = nullptr; + + // used to avoid locks for the debug output + // TODO: figure out if this is indeed necessary/correct - seems like either there still + // could be a race or that there wouldn't be a race even if using the context directly + DOCTEST_THREAD_LOCAL bool g_no_colors; + +#endif // DOCTEST_CONFIG_DISABLE +} // namespace detail + +char* String::allocate(size_type sz) { + if (sz <= last) { + buf[sz] = '\0'; + setLast(last - sz); + return buf; + } else { + setOnHeap(); + data.size = sz; + data.capacity = data.size + 1; + data.ptr = new char[data.capacity]; + data.ptr[sz] = '\0'; + return data.ptr; + } +} + +void String::setOnHeap() noexcept { *reinterpret_cast(&buf[last]) = 128; } +void String::setLast(size_type in) noexcept { buf[last] = char(in); } +void String::setSize(size_type sz) noexcept { + if (isOnStack()) { buf[sz] = '\0'; setLast(last - sz); } + else { data.ptr[sz] = '\0'; data.size = sz; } +} + +void String::copy(const String& other) { + if(other.isOnStack()) { + memcpy(buf, other.buf, len); + } else { + memcpy(allocate(other.data.size), other.data.ptr, other.data.size); + } +} + +String::String() noexcept { + buf[0] = '\0'; + setLast(); +} + +String::~String() { + if(!isOnStack()) + delete[] data.ptr; +} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) + +String::String(const char* in) + : String(in, strlen(in)) {} + +String::String(const char* in, size_type in_size) { + memcpy(allocate(in_size), in, in_size); +} + +String::String(std::istream& in, size_type in_size) { + in.read(allocate(in_size), in_size); +} + +String::String(const String& other) { copy(other); } + +String& String::operator=(const String& other) { + if(this != &other) { + if(!isOnStack()) + delete[] data.ptr; + + copy(other); + } + + return *this; +} + +String& String::operator+=(const String& other) { + const size_type my_old_size = size(); + const size_type other_size = other.size(); + const size_type total_size = my_old_size + other_size; + if(isOnStack()) { + if(total_size < len) { + // append to the current stack space + memcpy(buf + my_old_size, other.c_str(), other_size + 1); + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) + setLast(last - total_size); + } else { + // alloc new chunk + char* temp = new char[total_size + 1]; + // copy current data to new location before writing in the union + memcpy(temp, buf, my_old_size); // skip the +1 ('\0') for speed + // update data in union + setOnHeap(); + data.size = total_size; + data.capacity = data.size + 1; + data.ptr = temp; + // transfer the rest of the data + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } + } else { + if(data.capacity > total_size) { + // append to the current heap block + data.size = total_size; + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } else { + // resize + data.capacity *= 2; + if(data.capacity <= total_size) + data.capacity = total_size + 1; + // alloc new chunk + char* temp = new char[data.capacity]; + // copy current data to new location before releasing it + memcpy(temp, data.ptr, my_old_size); // skip the +1 ('\0') for speed + // release old chunk + delete[] data.ptr; + // update the rest of the union members + data.size = total_size; + data.ptr = temp; + // transfer the rest of the data + memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1); + } + } + + return *this; +} + +String::String(String&& other) noexcept { + memcpy(buf, other.buf, len); + other.buf[0] = '\0'; + other.setLast(); +} + +String& String::operator=(String&& other) noexcept { + if(this != &other) { + if(!isOnStack()) + delete[] data.ptr; + memcpy(buf, other.buf, len); + other.buf[0] = '\0'; + other.setLast(); + } + return *this; +} + +char String::operator[](size_type i) const { + return const_cast(this)->operator[](i); +} + +char& String::operator[](size_type i) { + if(isOnStack()) + return reinterpret_cast(buf)[i]; + return data.ptr[i]; +} + +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wmaybe-uninitialized") +String::size_type String::size() const { + if(isOnStack()) + return last - (size_type(buf[last]) & 31); // using "last" would work only if "len" is 32 + return data.size; +} +DOCTEST_GCC_SUPPRESS_WARNING_POP + +String::size_type String::capacity() const { + if(isOnStack()) + return len; + return data.capacity; +} + +String String::substr(size_type pos, size_type cnt) && { + cnt = std::min(cnt, size() - 1 - pos); + char* cptr = c_str(); + memmove(cptr, cptr + pos, cnt); + setSize(cnt); + return std::move(*this); +} + +String String::substr(size_type pos, size_type cnt) const & { + cnt = std::min(cnt, size() - 1 - pos); + return String{ c_str() + pos, cnt }; +} + +String::size_type String::find(char ch, size_type pos) const { + const char* begin = c_str(); + const char* end = begin + size(); + const char* it = begin + pos; + for (; it < end && *it != ch; it++); + if (it < end) { return static_cast(it - begin); } + else { return npos; } +} + +String::size_type String::rfind(char ch, size_type pos) const { + const char* begin = c_str(); + const char* it = begin + std::min(pos, size() - 1); + for (; it >= begin && *it != ch; it--); + if (it >= begin) { return static_cast(it - begin); } + else { return npos; } +} + +int String::compare(const char* other, bool no_case) const { + if(no_case) + return doctest::stricmp(c_str(), other); + return std::strcmp(c_str(), other); +} + +int String::compare(const String& other, bool no_case) const { + return compare(other.c_str(), no_case); +} + +String operator+(const String& lhs, const String& rhs) { return String(lhs) += rhs; } + +bool operator==(const String& lhs, const String& rhs) { return lhs.compare(rhs) == 0; } +bool operator!=(const String& lhs, const String& rhs) { return lhs.compare(rhs) != 0; } +bool operator< (const String& lhs, const String& rhs) { return lhs.compare(rhs) < 0; } +bool operator> (const String& lhs, const String& rhs) { return lhs.compare(rhs) > 0; } +bool operator<=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) < 0 : true; } +bool operator>=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) > 0 : true; } + +std::ostream& operator<<(std::ostream& s, const String& in) { return s << in.c_str(); } + +Contains::Contains(const String& str) : string(str) { } + +bool Contains::checkWith(const String& other) const { + return strstr(other.c_str(), string.c_str()) != nullptr; +} + +String toString(const Contains& in) { + return "Contains( " + in.string + " )"; +} + +bool operator==(const String& lhs, const Contains& rhs) { return rhs.checkWith(lhs); } +bool operator==(const Contains& lhs, const String& rhs) { return lhs.checkWith(rhs); } +bool operator!=(const String& lhs, const Contains& rhs) { return !rhs.checkWith(lhs); } +bool operator!=(const Contains& lhs, const String& rhs) { return !lhs.checkWith(rhs); } + +namespace { + void color_to_stream(std::ostream&, Color::Enum) DOCTEST_BRANCH_ON_DISABLED({}, ;) +} // namespace + +namespace Color { + std::ostream& operator<<(std::ostream& s, Color::Enum code) { + color_to_stream(s, code); + return s; + } +} // namespace Color + +// clang-format off +const char* assertString(assertType::Enum at) { + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4061) // enum 'x' in switch of enum 'y' is not explicitly handled + #define DOCTEST_GENERATE_ASSERT_TYPE_CASE(assert_type) case assertType::DT_ ## assert_type: return #assert_type + #define DOCTEST_GENERATE_ASSERT_TYPE_CASES(assert_type) \ + DOCTEST_GENERATE_ASSERT_TYPE_CASE(WARN_ ## assert_type); \ + DOCTEST_GENERATE_ASSERT_TYPE_CASE(CHECK_ ## assert_type); \ + DOCTEST_GENERATE_ASSERT_TYPE_CASE(REQUIRE_ ## assert_type) + switch(at) { + DOCTEST_GENERATE_ASSERT_TYPE_CASE(WARN); + DOCTEST_GENERATE_ASSERT_TYPE_CASE(CHECK); + DOCTEST_GENERATE_ASSERT_TYPE_CASE(REQUIRE); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(FALSE); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS_AS); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS_WITH); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS_WITH_AS); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(NOTHROW); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(EQ); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(NE); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(GT); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(LT); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(GE); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(LE); + + DOCTEST_GENERATE_ASSERT_TYPE_CASES(UNARY); + DOCTEST_GENERATE_ASSERT_TYPE_CASES(UNARY_FALSE); + + default: DOCTEST_INTERNAL_ERROR("Tried stringifying invalid assert type!"); + } + DOCTEST_MSVC_SUPPRESS_WARNING_POP +} +// clang-format on + +const char* failureString(assertType::Enum at) { + if(at & assertType::is_warn) //!OCLINT bitwise operator in conditional + return "WARNING"; + if(at & assertType::is_check) //!OCLINT bitwise operator in conditional + return "ERROR"; + if(at & assertType::is_require) //!OCLINT bitwise operator in conditional + return "FATAL ERROR"; + return ""; +} + +DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference") +DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference") +// depending on the current options this will remove the path of filenames +const char* skipPathFromFilename(const char* file) { +#ifndef DOCTEST_CONFIG_DISABLE + if(getContextOptions()->no_path_in_filenames) { + auto back = std::strrchr(file, '\\'); + auto forward = std::strrchr(file, '/'); + if(back || forward) { + if(back > forward) + forward = back; + return forward + 1; + } + } +#endif // DOCTEST_CONFIG_DISABLE + return file; +} +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +bool SubcaseSignature::operator==(const SubcaseSignature& other) const { + return m_line == other.m_line + && std::strcmp(m_file, other.m_file) == 0 + && m_name == other.m_name; +} + +bool SubcaseSignature::operator<(const SubcaseSignature& other) const { + if(m_line != other.m_line) + return m_line < other.m_line; + if(std::strcmp(m_file, other.m_file) != 0) + return std::strcmp(m_file, other.m_file) < 0; + return m_name.compare(other.m_name) < 0; +} + +DOCTEST_DEFINE_INTERFACE(IContextScope) + +namespace detail { + void filldata::fill(std::ostream* stream, const void* in) { + if (in) { *stream << in; } + else { *stream << "nullptr"; } + } + + template + String toStreamLit(T t) { + std::ostream* os = tlssPush(); + os->operator<<(t); + return tlssPop(); + } +} + +#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING +String toString(const char* in) { return String("\"") + (in ? in : "{null string}") + "\""; } +#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING + +#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0) +// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183 +String toString(const std::string& in) { return in.c_str(); } +#endif // VS 2019 + +String toString(String in) { return in; } + +String toString(std::nullptr_t) { return "nullptr"; } + +String toString(bool in) { return in ? "true" : "false"; } + +String toString(float in) { return toStreamLit(in); } +String toString(double in) { return toStreamLit(in); } +String toString(double long in) { return toStreamLit(in); } + +String toString(char in) { return toStreamLit(static_cast(in)); } +String toString(char signed in) { return toStreamLit(static_cast(in)); } +String toString(char unsigned in) { return toStreamLit(static_cast(in)); } +String toString(short in) { return toStreamLit(in); } +String toString(short unsigned in) { return toStreamLit(in); } +String toString(signed in) { return toStreamLit(in); } +String toString(unsigned in) { return toStreamLit(in); } +String toString(long in) { return toStreamLit(in); } +String toString(long unsigned in) { return toStreamLit(in); } +String toString(long long in) { return toStreamLit(in); } +String toString(long long unsigned in) { return toStreamLit(in); } + +Approx::Approx(double value) + : m_epsilon(static_cast(std::numeric_limits::epsilon()) * 100) + , m_scale(1.0) + , m_value(value) {} + +Approx Approx::operator()(double value) const { + Approx approx(value); + approx.epsilon(m_epsilon); + approx.scale(m_scale); + return approx; +} + +Approx& Approx::epsilon(double newEpsilon) { + m_epsilon = newEpsilon; + return *this; +} +Approx& Approx::scale(double newScale) { + m_scale = newScale; + return *this; +} + +bool operator==(double lhs, const Approx& rhs) { + // Thanks to Richard Harris for his help refining this formula + return std::fabs(lhs - rhs.m_value) < + rhs.m_epsilon * (rhs.m_scale + std::max(std::fabs(lhs), std::fabs(rhs.m_value))); +} +bool operator==(const Approx& lhs, double rhs) { return operator==(rhs, lhs); } +bool operator!=(double lhs, const Approx& rhs) { return !operator==(lhs, rhs); } +bool operator!=(const Approx& lhs, double rhs) { return !operator==(rhs, lhs); } +bool operator<=(double lhs, const Approx& rhs) { return lhs < rhs.m_value || lhs == rhs; } +bool operator<=(const Approx& lhs, double rhs) { return lhs.m_value < rhs || lhs == rhs; } +bool operator>=(double lhs, const Approx& rhs) { return lhs > rhs.m_value || lhs == rhs; } +bool operator>=(const Approx& lhs, double rhs) { return lhs.m_value > rhs || lhs == rhs; } +bool operator<(double lhs, const Approx& rhs) { return lhs < rhs.m_value && lhs != rhs; } +bool operator<(const Approx& lhs, double rhs) { return lhs.m_value < rhs && lhs != rhs; } +bool operator>(double lhs, const Approx& rhs) { return lhs > rhs.m_value && lhs != rhs; } +bool operator>(const Approx& lhs, double rhs) { return lhs.m_value > rhs && lhs != rhs; } + +String toString(const Approx& in) { + return "Approx( " + doctest::toString(in.m_value) + " )"; +} +const ContextOptions* getContextOptions() { return DOCTEST_BRANCH_ON_DISABLED(nullptr, g_cs); } + +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4738) +template +IsNaN::operator bool() const { + return std::isnan(value) ^ flipped; +} +DOCTEST_MSVC_SUPPRESS_WARNING_POP +template struct DOCTEST_INTERFACE_DEF IsNaN; +template struct DOCTEST_INTERFACE_DEF IsNaN; +template struct DOCTEST_INTERFACE_DEF IsNaN; +template +String toString(IsNaN in) { return String(in.flipped ? "! " : "") + "IsNaN( " + doctest::toString(in.value) + " )"; } +String toString(IsNaN in) { return toString(in); } +String toString(IsNaN in) { return toString(in); } +String toString(IsNaN in) { return toString(in); } + +} // namespace doctest + +#ifdef DOCTEST_CONFIG_DISABLE +namespace doctest { +Context::Context(int, const char* const*) {} +Context::~Context() = default; +void Context::applyCommandLine(int, const char* const*) {} +void Context::addFilter(const char*, const char*) {} +void Context::clearFilters() {} +void Context::setOption(const char*, bool) {} +void Context::setOption(const char*, int) {} +void Context::setOption(const char*, const char*) {} +bool Context::shouldExit() { return false; } +void Context::setAsDefaultForAssertsOutOfTestCases() {} +void Context::setAssertHandler(detail::assert_handler) {} +void Context::setCout(std::ostream*) {} +int Context::run() { return 0; } + +int IReporter::get_num_active_contexts() { return 0; } +const IContextScope* const* IReporter::get_active_contexts() { return nullptr; } +int IReporter::get_num_stringified_contexts() { return 0; } +const String* IReporter::get_stringified_contexts() { return nullptr; } + +int registerReporter(const char*, int, IReporter*) { return 0; } + +} // namespace doctest +#else // DOCTEST_CONFIG_DISABLE + +#if !defined(DOCTEST_CONFIG_COLORS_NONE) +#if !defined(DOCTEST_CONFIG_COLORS_WINDOWS) && !defined(DOCTEST_CONFIG_COLORS_ANSI) +#ifdef DOCTEST_PLATFORM_WINDOWS +#define DOCTEST_CONFIG_COLORS_WINDOWS +#else // linux +#define DOCTEST_CONFIG_COLORS_ANSI +#endif // platform +#endif // DOCTEST_CONFIG_COLORS_WINDOWS && DOCTEST_CONFIG_COLORS_ANSI +#endif // DOCTEST_CONFIG_COLORS_NONE + +namespace doctest_detail_test_suite_ns { +// holds the current test suite +doctest::detail::TestSuite& getCurrentTestSuite() { + static doctest::detail::TestSuite data{}; + return data; +} +} // namespace doctest_detail_test_suite_ns + +namespace doctest { +namespace { + // the int (priority) is part of the key for automatic sorting - sadly one can register a + // reporter with a duplicate name and a different priority but hopefully that won't happen often :| + using reporterMap = std::map, reporterCreatorFunc>; + + reporterMap& getReporters() { + static reporterMap data; + return data; + } + reporterMap& getListeners() { + static reporterMap data; + return data; + } +} // namespace +namespace detail { +#define DOCTEST_ITERATE_THROUGH_REPORTERS(function, ...) \ + for(auto& curr_rep : g_cs->reporters_currently_used) \ + curr_rep->function(__VA_ARGS__) + + bool checkIfShouldThrow(assertType::Enum at) { + if(at & assertType::is_require) //!OCLINT bitwise operator in conditional + return true; + + if((at & assertType::is_check) //!OCLINT bitwise operator in conditional + && getContextOptions()->abort_after > 0 && + (g_cs->numAssertsFailed + g_cs->numAssertsFailedCurrentTest_atomic) >= + getContextOptions()->abort_after) + return true; + + return false; + } + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + DOCTEST_NORETURN void throwException() { + g_cs->shouldLogCurrentException = false; + throw TestFailureException(); // NOLINT(hicpp-exception-baseclass) + } +#else // DOCTEST_CONFIG_NO_EXCEPTIONS + void throwException() {} +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +} // namespace detail + +namespace { + using namespace detail; + // matching of a string against a wildcard mask (case sensitivity configurable) taken from + // https://www.codeproject.com/Articles/1088/Wildcard-string-compare-globbing + int wildcmp(const char* str, const char* wild, bool caseSensitive) { + const char* cp = str; + const char* mp = wild; + + while((*str) && (*wild != '*')) { + if((caseSensitive ? (*wild != *str) : (tolower(*wild) != tolower(*str))) && + (*wild != '?')) { + return 0; + } + wild++; + str++; + } + + while(*str) { + if(*wild == '*') { + if(!*++wild) { + return 1; + } + mp = wild; + cp = str + 1; + } else if((caseSensitive ? (*wild == *str) : (tolower(*wild) == tolower(*str))) || + (*wild == '?')) { + wild++; + str++; + } else { + wild = mp; //!OCLINT parameter reassignment + str = cp++; //!OCLINT parameter reassignment + } + } + + while(*wild == '*') { + wild++; + } + return !*wild; + } + + // checks if the name matches any of the filters (and can be configured what to do when empty) + bool matchesAny(const char* name, const std::vector& filters, bool matchEmpty, + bool caseSensitive) { + if (filters.empty() && matchEmpty) + return true; + for (auto& curr : filters) + if (wildcmp(name, curr.c_str(), caseSensitive)) + return true; + return false; + } + + DOCTEST_NO_SANITIZE_INTEGER + unsigned long long hash(unsigned long long a, unsigned long long b) { + return (a << 5) + b; + } + + // C string hash function (djb2) - taken from http://www.cse.yorku.ca/~oz/hash.html + DOCTEST_NO_SANITIZE_INTEGER + unsigned long long hash(const char* str) { + unsigned long long hash = 5381; + char c; + while ((c = *str++)) + hash = ((hash << 5) + hash) + c; // hash * 33 + c + return hash; + } + + unsigned long long hash(const SubcaseSignature& sig) { + return hash(hash(hash(sig.m_file), hash(sig.m_name.c_str())), sig.m_line); + } + + unsigned long long hash(const std::vector& sigs, size_t count) { + unsigned long long running = 0; + auto end = sigs.begin() + count; + for (auto it = sigs.begin(); it != end; it++) { + running = hash(running, hash(*it)); + } + return running; + } + + unsigned long long hash(const std::vector& sigs) { + unsigned long long running = 0; + for (const SubcaseSignature& sig : sigs) { + running = hash(running, hash(sig)); + } + return running; + } +} // namespace +namespace detail { + bool Subcase::checkFilters() { + if (g_cs->subcaseStack.size() < size_t(g_cs->subcase_filter_levels)) { + if (!matchesAny(m_signature.m_name.c_str(), g_cs->filters[6], true, g_cs->case_sensitive)) + return true; + if (matchesAny(m_signature.m_name.c_str(), g_cs->filters[7], false, g_cs->case_sensitive)) + return true; + } + return false; + } + + Subcase::Subcase(const String& name, const char* file, int line) + : m_signature({name, file, line}) { + if (!g_cs->reachedLeaf) { + if (g_cs->nextSubcaseStack.size() <= g_cs->subcaseStack.size() + || g_cs->nextSubcaseStack[g_cs->subcaseStack.size()] == m_signature) { + // Going down. + if (checkFilters()) { return; } + + g_cs->subcaseStack.push_back(m_signature); + g_cs->currentSubcaseDepth++; + m_entered = true; + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_start, m_signature); + } + } else { + if (g_cs->subcaseStack[g_cs->currentSubcaseDepth] == m_signature) { + // This subcase is reentered via control flow. + g_cs->currentSubcaseDepth++; + m_entered = true; + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_start, m_signature); + } else if (g_cs->nextSubcaseStack.size() <= g_cs->currentSubcaseDepth + && g_cs->fullyTraversedSubcases.find(hash(hash(g_cs->subcaseStack, g_cs->currentSubcaseDepth), hash(m_signature))) + == g_cs->fullyTraversedSubcases.end()) { + if (checkFilters()) { return; } + // This subcase is part of the one to be executed next. + g_cs->nextSubcaseStack.clear(); + g_cs->nextSubcaseStack.insert(g_cs->nextSubcaseStack.end(), + g_cs->subcaseStack.begin(), g_cs->subcaseStack.begin() + g_cs->currentSubcaseDepth); + g_cs->nextSubcaseStack.push_back(m_signature); + } + } + } + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4996) // std::uncaught_exception is deprecated in C++17 + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + + Subcase::~Subcase() { + if (m_entered) { + g_cs->currentSubcaseDepth--; + + if (!g_cs->reachedLeaf) { + // Leaf. + g_cs->fullyTraversedSubcases.insert(hash(g_cs->subcaseStack)); + g_cs->nextSubcaseStack.clear(); + g_cs->reachedLeaf = true; + } else if (g_cs->nextSubcaseStack.empty()) { + // All children are finished. + g_cs->fullyTraversedSubcases.insert(hash(g_cs->subcaseStack)); + } + +#if defined(__cpp_lib_uncaught_exceptions) && __cpp_lib_uncaught_exceptions >= 201411L && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200) + if(std::uncaught_exceptions() > 0 +#else + if(std::uncaught_exception() +#endif + && g_cs->shouldLogCurrentException) { + DOCTEST_ITERATE_THROUGH_REPORTERS( + test_case_exception, {"exception thrown in subcase - will translate later " + "when the whole test case has been exited (cannot " + "translate while there is an active exception)", + false}); + g_cs->shouldLogCurrentException = false; + } + + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY); + } + } + + DOCTEST_CLANG_SUPPRESS_WARNING_POP + DOCTEST_GCC_SUPPRESS_WARNING_POP + DOCTEST_MSVC_SUPPRESS_WARNING_POP + + Subcase::operator bool() const { return m_entered; } + + Result::Result(bool passed, const String& decomposition) + : m_passed(passed) + , m_decomp(decomposition) {} + + ExpressionDecomposer::ExpressionDecomposer(assertType::Enum at) + : m_at(at) {} + + TestSuite& TestSuite::operator*(const char* in) { + m_test_suite = in; + return *this; + } + + TestCase::TestCase(funcType test, const char* file, unsigned line, const TestSuite& test_suite, + const String& type, int template_id) { + m_file = file; + m_line = line; + m_name = nullptr; // will be later overridden in operator* + m_test_suite = test_suite.m_test_suite; + m_description = test_suite.m_description; + m_skip = test_suite.m_skip; + m_no_breaks = test_suite.m_no_breaks; + m_no_output = test_suite.m_no_output; + m_may_fail = test_suite.m_may_fail; + m_should_fail = test_suite.m_should_fail; + m_expected_failures = test_suite.m_expected_failures; + m_timeout = test_suite.m_timeout; + + m_test = test; + m_type = type; + m_template_id = template_id; + } + + TestCase::TestCase(const TestCase& other) + : TestCaseData() { + *this = other; + } + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function + TestCase& TestCase::operator=(const TestCase& other) { + TestCaseData::operator=(other); + m_test = other.m_test; + m_type = other.m_type; + m_template_id = other.m_template_id; + m_full_name = other.m_full_name; + + if(m_template_id != -1) + m_name = m_full_name.c_str(); + return *this; + } + DOCTEST_MSVC_SUPPRESS_WARNING_POP + + TestCase& TestCase::operator*(const char* in) { + m_name = in; + // make a new name with an appended type for templated test case + if(m_template_id != -1) { + m_full_name = String(m_name) + "<" + m_type + ">"; + // redirect the name to point to the newly constructed full name + m_name = m_full_name.c_str(); + } + return *this; + } + + bool TestCase::operator<(const TestCase& other) const { + // this will be used only to differentiate between test cases - not relevant for sorting + if(m_line != other.m_line) + return m_line < other.m_line; + const int name_cmp = strcmp(m_name, other.m_name); + if(name_cmp != 0) + return name_cmp < 0; + const int file_cmp = m_file.compare(other.m_file); + if(file_cmp != 0) + return file_cmp < 0; + return m_template_id < other.m_template_id; + } + + // all the registered tests + std::set& getRegisteredTests() { + static std::set data; + return data; + } +} // namespace detail +namespace { + using namespace detail; + // for sorting tests by file/line + bool fileOrderComparator(const TestCase* lhs, const TestCase* rhs) { + // this is needed because MSVC gives different case for drive letters + // for __FILE__ when evaluated in a header and a source file + const int res = lhs->m_file.compare(rhs->m_file, bool(DOCTEST_MSVC)); + if(res != 0) + return res < 0; + if(lhs->m_line != rhs->m_line) + return lhs->m_line < rhs->m_line; + return lhs->m_template_id < rhs->m_template_id; + } + + // for sorting tests by suite/file/line + bool suiteOrderComparator(const TestCase* lhs, const TestCase* rhs) { + const int res = std::strcmp(lhs->m_test_suite, rhs->m_test_suite); + if(res != 0) + return res < 0; + return fileOrderComparator(lhs, rhs); + } + + // for sorting tests by name/suite/file/line + bool nameOrderComparator(const TestCase* lhs, const TestCase* rhs) { + const int res = std::strcmp(lhs->m_name, rhs->m_name); + if(res != 0) + return res < 0; + return suiteOrderComparator(lhs, rhs); + } + + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + void color_to_stream(std::ostream& s, Color::Enum code) { + static_cast(s); // for DOCTEST_CONFIG_COLORS_NONE or DOCTEST_CONFIG_COLORS_WINDOWS + static_cast(code); // for DOCTEST_CONFIG_COLORS_NONE +#ifdef DOCTEST_CONFIG_COLORS_ANSI + if(g_no_colors || + (isatty(STDOUT_FILENO) == false && getContextOptions()->force_colors == false)) + return; + + auto col = ""; + // clang-format off + switch(code) { //!OCLINT missing break in switch statement / unnecessary default statement in covered switch statement + case Color::Red: col = "[0;31m"; break; + case Color::Green: col = "[0;32m"; break; + case Color::Blue: col = "[0;34m"; break; + case Color::Cyan: col = "[0;36m"; break; + case Color::Yellow: col = "[0;33m"; break; + case Color::Grey: col = "[1;30m"; break; + case Color::LightGrey: col = "[0;37m"; break; + case Color::BrightRed: col = "[1;31m"; break; + case Color::BrightGreen: col = "[1;32m"; break; + case Color::BrightWhite: col = "[1;37m"; break; + case Color::Bright: // invalid + case Color::None: + case Color::White: + default: col = "[0m"; + } + // clang-format on + s << "\033" << col; +#endif // DOCTEST_CONFIG_COLORS_ANSI + +#ifdef DOCTEST_CONFIG_COLORS_WINDOWS + if(g_no_colors || + (_isatty(_fileno(stdout)) == false && getContextOptions()->force_colors == false)) + return; + + static struct ConsoleHelper { + HANDLE stdoutHandle; + WORD origFgAttrs; + WORD origBgAttrs; + + ConsoleHelper() { + stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE); + CONSOLE_SCREEN_BUFFER_INFO csbiInfo; + GetConsoleScreenBufferInfo(stdoutHandle, &csbiInfo); + origFgAttrs = csbiInfo.wAttributes & ~(BACKGROUND_GREEN | BACKGROUND_RED | + BACKGROUND_BLUE | BACKGROUND_INTENSITY); + origBgAttrs = csbiInfo.wAttributes & ~(FOREGROUND_GREEN | FOREGROUND_RED | + FOREGROUND_BLUE | FOREGROUND_INTENSITY); + } + } ch; + +#define DOCTEST_SET_ATTR(x) SetConsoleTextAttribute(ch.stdoutHandle, x | ch.origBgAttrs) + + // clang-format off + switch (code) { + case Color::White: DOCTEST_SET_ATTR(FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break; + case Color::Red: DOCTEST_SET_ATTR(FOREGROUND_RED); break; + case Color::Green: DOCTEST_SET_ATTR(FOREGROUND_GREEN); break; + case Color::Blue: DOCTEST_SET_ATTR(FOREGROUND_BLUE); break; + case Color::Cyan: DOCTEST_SET_ATTR(FOREGROUND_BLUE | FOREGROUND_GREEN); break; + case Color::Yellow: DOCTEST_SET_ATTR(FOREGROUND_RED | FOREGROUND_GREEN); break; + case Color::Grey: DOCTEST_SET_ATTR(0); break; + case Color::LightGrey: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY); break; + case Color::BrightRed: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_RED); break; + case Color::BrightGreen: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN); break; + case Color::BrightWhite: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break; + case Color::None: + case Color::Bright: // invalid + default: DOCTEST_SET_ATTR(ch.origFgAttrs); + } + // clang-format on +#endif // DOCTEST_CONFIG_COLORS_WINDOWS + } + DOCTEST_CLANG_SUPPRESS_WARNING_POP + + std::vector& getExceptionTranslators() { + static std::vector data; + return data; + } + + String translateActiveException() { +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + String res; + auto& translators = getExceptionTranslators(); + for(auto& curr : translators) + if(curr->translate(res)) + return res; + // clang-format off + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wcatch-value") + try { + throw; + } catch(std::exception& ex) { + return ex.what(); + } catch(std::string& msg) { + return msg.c_str(); + } catch(const char* msg) { + return msg; + } catch(...) { + return "unknown exception"; + } + DOCTEST_GCC_SUPPRESS_WARNING_POP +// clang-format on +#else // DOCTEST_CONFIG_NO_EXCEPTIONS + return ""; +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + } +} // namespace + +namespace detail { + // used by the macros for registering tests + int regTest(const TestCase& tc) { + getRegisteredTests().insert(tc); + return 0; + } + + // sets the current test suite + int setTestSuite(const TestSuite& ts) { + doctest_detail_test_suite_ns::getCurrentTestSuite() = ts; + return 0; + } + +#ifdef DOCTEST_IS_DEBUGGER_ACTIVE + bool isDebuggerActive() { return DOCTEST_IS_DEBUGGER_ACTIVE(); } +#else // DOCTEST_IS_DEBUGGER_ACTIVE +#ifdef DOCTEST_PLATFORM_LINUX + class ErrnoGuard { + public: + ErrnoGuard() : m_oldErrno(errno) {} + ~ErrnoGuard() { errno = m_oldErrno; } + private: + int m_oldErrno; + }; + // See the comments in Catch2 for the reasoning behind this implementation: + // https://github.com/catchorg/Catch2/blob/v2.13.1/include/internal/catch_debugger.cpp#L79-L102 + bool isDebuggerActive() { + ErrnoGuard guard; + std::ifstream in("/proc/self/status"); + for(std::string line; std::getline(in, line);) { + static const int PREFIX_LEN = 11; + if(line.compare(0, PREFIX_LEN, "TracerPid:\t") == 0) { + return line.length() > PREFIX_LEN && line[PREFIX_LEN] != '0'; + } + } + return false; + } +#elif defined(DOCTEST_PLATFORM_MAC) + // The following function is taken directly from the following technical note: + // https://developer.apple.com/library/archive/qa/qa1361/_index.html + // Returns true if the current process is being debugged (either + // running under the debugger or has a debugger attached post facto). + bool isDebuggerActive() { + int mib[4]; + kinfo_proc info; + size_t size; + // Initialize the flags so that, if sysctl fails for some bizarre + // reason, we get a predictable result. + info.kp_proc.p_flag = 0; + // Initialize mib, which tells sysctl the info we want, in this case + // we're looking for information about a specific process ID. + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = getpid(); + // Call sysctl. + size = sizeof(info); + if(sysctl(mib, DOCTEST_COUNTOF(mib), &info, &size, 0, 0) != 0) { + std::cerr << "\nCall to sysctl failed - unable to determine if debugger is active **\n"; + return false; + } + // We're being debugged if the P_TRACED flag is set. + return ((info.kp_proc.p_flag & P_TRACED) != 0); + } +#elif DOCTEST_MSVC || defined(__MINGW32__) || defined(__MINGW64__) + bool isDebuggerActive() { return ::IsDebuggerPresent() != 0; } +#else + bool isDebuggerActive() { return false; } +#endif // Platform +#endif // DOCTEST_IS_DEBUGGER_ACTIVE + + void registerExceptionTranslatorImpl(const IExceptionTranslator* et) { + if(std::find(getExceptionTranslators().begin(), getExceptionTranslators().end(), et) == + getExceptionTranslators().end()) + getExceptionTranslators().push_back(et); + } + + DOCTEST_THREAD_LOCAL std::vector g_infoContexts; // for logging with INFO() + + ContextScopeBase::ContextScopeBase() { + g_infoContexts.push_back(this); + } + + ContextScopeBase::ContextScopeBase(ContextScopeBase&& other) noexcept { + if (other.need_to_destroy) { + other.destroy(); + } + other.need_to_destroy = false; + g_infoContexts.push_back(this); + } + + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4996) // std::uncaught_exception is deprecated in C++17 + DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations") + + // destroy cannot be inlined into the destructor because that would mean calling stringify after + // ContextScope has been destroyed (base class destructors run after derived class destructors). + // Instead, ContextScope calls this method directly from its destructor. + void ContextScopeBase::destroy() { +#if defined(__cpp_lib_uncaught_exceptions) && __cpp_lib_uncaught_exceptions >= 201411L && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200) + if(std::uncaught_exceptions() > 0) { +#else + if(std::uncaught_exception()) { +#endif + std::ostringstream s; + this->stringify(&s); + g_cs->stringifiedContexts.push_back(s.str().c_str()); + } + g_infoContexts.pop_back(); + } + + DOCTEST_CLANG_SUPPRESS_WARNING_POP + DOCTEST_GCC_SUPPRESS_WARNING_POP + DOCTEST_MSVC_SUPPRESS_WARNING_POP +} // namespace detail +namespace { + using namespace detail; + +#if !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && !defined(DOCTEST_CONFIG_WINDOWS_SEH) + struct FatalConditionHandler + { + static void reset() {} + static void allocateAltStackMem() {} + static void freeAltStackMem() {} + }; +#else // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + + void reportFatal(const std::string&); + +#ifdef DOCTEST_PLATFORM_WINDOWS + + struct SignalDefs + { + DWORD id; + const char* name; + }; + // There is no 1-1 mapping between signals and windows exceptions. + // Windows can easily distinguish between SO and SigSegV, + // but SigInt, SigTerm, etc are handled differently. + SignalDefs signalDefs[] = { + {static_cast(EXCEPTION_ILLEGAL_INSTRUCTION), + "SIGILL - Illegal instruction signal"}, + {static_cast(EXCEPTION_STACK_OVERFLOW), "SIGSEGV - Stack overflow"}, + {static_cast(EXCEPTION_ACCESS_VIOLATION), + "SIGSEGV - Segmentation violation signal"}, + {static_cast(EXCEPTION_INT_DIVIDE_BY_ZERO), "Divide by zero error"}, + }; + + struct FatalConditionHandler + { + static LONG CALLBACK handleException(PEXCEPTION_POINTERS ExceptionInfo) { + // Multiple threads may enter this filter/handler at once. We want the error message to be printed on the + // console just once no matter how many threads have crashed. + DOCTEST_DECLARE_STATIC_MUTEX(mutex) + static bool execute = true; + { + DOCTEST_LOCK_MUTEX(mutex) + if(execute) { + bool reported = false; + for(size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + if(ExceptionInfo->ExceptionRecord->ExceptionCode == signalDefs[i].id) { + reportFatal(signalDefs[i].name); + reported = true; + break; + } + } + if(reported == false) + reportFatal("Unhandled SEH exception caught"); + if(isDebuggerActive() && !g_cs->no_breaks) + DOCTEST_BREAK_INTO_DEBUGGER(); + } + execute = false; + } + std::exit(EXIT_FAILURE); + } + + static void allocateAltStackMem() {} + static void freeAltStackMem() {} + + FatalConditionHandler() { + isSet = true; + // 32k seems enough for doctest to handle stack overflow, + // but the value was found experimentally, so there is no strong guarantee + guaranteeSize = 32 * 1024; + // Register an unhandled exception filter + previousTop = SetUnhandledExceptionFilter(handleException); + // Pass in guarantee size to be filled + SetThreadStackGuarantee(&guaranteeSize); + + // On Windows uncaught exceptions from another thread, exceptions from + // destructors, or calls to std::terminate are not a SEH exception + + // The terminal handler gets called when: + // - std::terminate is called FROM THE TEST RUNNER THREAD + // - an exception is thrown from a destructor FROM THE TEST RUNNER THREAD + original_terminate_handler = std::get_terminate(); + std::set_terminate([]() DOCTEST_NOEXCEPT { + reportFatal("Terminate handler called"); + if(isDebuggerActive() && !g_cs->no_breaks) + DOCTEST_BREAK_INTO_DEBUGGER(); + std::exit(EXIT_FAILURE); // explicitly exit - otherwise the SIGABRT handler may be called as well + }); + + // SIGABRT is raised when: + // - std::terminate is called FROM A DIFFERENT THREAD + // - an exception is thrown from a destructor FROM A DIFFERENT THREAD + // - an uncaught exception is thrown FROM A DIFFERENT THREAD + prev_sigabrt_handler = std::signal(SIGABRT, [](int signal) DOCTEST_NOEXCEPT { + if(signal == SIGABRT) { + reportFatal("SIGABRT - Abort (abnormal termination) signal"); + if(isDebuggerActive() && !g_cs->no_breaks) + DOCTEST_BREAK_INTO_DEBUGGER(); + std::exit(EXIT_FAILURE); + } + }); + + // The following settings are taken from google test, and more + // specifically from UnitTest::Run() inside of gtest.cc + + // the user does not want to see pop-up dialogs about crashes + prev_error_mode_1 = SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOALIGNMENTFAULTEXCEPT | + SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX); + // This forces the abort message to go to stderr in all circumstances. + prev_error_mode_2 = _set_error_mode(_OUT_TO_STDERR); + // In the debug version, Visual Studio pops up a separate dialog + // offering a choice to debug the aborted program - we want to disable that. + prev_abort_behavior = _set_abort_behavior(0x0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + // In debug mode, the Windows CRT can crash with an assertion over invalid + // input (e.g. passing an invalid file descriptor). The default handling + // for these assertions is to pop up a dialog and wait for user input. + // Instead ask the CRT to dump such assertions to stderr non-interactively. + prev_report_mode = _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG); + prev_report_file = _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR); + } + + static void reset() { + if(isSet) { + // Unregister handler and restore the old guarantee + SetUnhandledExceptionFilter(previousTop); + SetThreadStackGuarantee(&guaranteeSize); + std::set_terminate(original_terminate_handler); + std::signal(SIGABRT, prev_sigabrt_handler); + SetErrorMode(prev_error_mode_1); + _set_error_mode(prev_error_mode_2); + _set_abort_behavior(prev_abort_behavior, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + static_cast(_CrtSetReportMode(_CRT_ASSERT, prev_report_mode)); + static_cast(_CrtSetReportFile(_CRT_ASSERT, prev_report_file)); + isSet = false; + } + } + + ~FatalConditionHandler() { reset(); } + + private: + static UINT prev_error_mode_1; + static int prev_error_mode_2; + static unsigned int prev_abort_behavior; + static int prev_report_mode; + static _HFILE prev_report_file; + static void (DOCTEST_CDECL *prev_sigabrt_handler)(int); + static std::terminate_handler original_terminate_handler; + static bool isSet; + static ULONG guaranteeSize; + static LPTOP_LEVEL_EXCEPTION_FILTER previousTop; + }; + + UINT FatalConditionHandler::prev_error_mode_1; + int FatalConditionHandler::prev_error_mode_2; + unsigned int FatalConditionHandler::prev_abort_behavior; + int FatalConditionHandler::prev_report_mode; + _HFILE FatalConditionHandler::prev_report_file; + void (DOCTEST_CDECL *FatalConditionHandler::prev_sigabrt_handler)(int); + std::terminate_handler FatalConditionHandler::original_terminate_handler; + bool FatalConditionHandler::isSet = false; + ULONG FatalConditionHandler::guaranteeSize = 0; + LPTOP_LEVEL_EXCEPTION_FILTER FatalConditionHandler::previousTop = nullptr; + +#else // DOCTEST_PLATFORM_WINDOWS + + struct SignalDefs + { + int id; + const char* name; + }; + SignalDefs signalDefs[] = {{SIGINT, "SIGINT - Terminal interrupt signal"}, + {SIGILL, "SIGILL - Illegal instruction signal"}, + {SIGFPE, "SIGFPE - Floating point error signal"}, + {SIGSEGV, "SIGSEGV - Segmentation violation signal"}, + {SIGTERM, "SIGTERM - Termination request signal"}, + {SIGABRT, "SIGABRT - Abort (abnormal termination) signal"}}; + + struct FatalConditionHandler + { + static bool isSet; + static struct sigaction oldSigActions[DOCTEST_COUNTOF(signalDefs)]; + static stack_t oldSigStack; + static size_t altStackSize; + static char* altStackMem; + + static void handleSignal(int sig) { + const char* name = ""; + for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + SignalDefs& def = signalDefs[i]; + if(sig == def.id) { + name = def.name; + break; + } + } + reset(); + reportFatal(name); + raise(sig); + } + + static void allocateAltStackMem() { + altStackMem = new char[altStackSize]; + } + + static void freeAltStackMem() { + delete[] altStackMem; + } + + FatalConditionHandler() { + isSet = true; + stack_t sigStack; + sigStack.ss_sp = altStackMem; + sigStack.ss_size = altStackSize; + sigStack.ss_flags = 0; + sigaltstack(&sigStack, &oldSigStack); + struct sigaction sa = {}; + sa.sa_handler = handleSignal; + sa.sa_flags = SA_ONSTACK; + for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + sigaction(signalDefs[i].id, &sa, &oldSigActions[i]); + } + } + + ~FatalConditionHandler() { reset(); } + static void reset() { + if(isSet) { + // Set signals back to previous values -- hopefully nobody overwrote them in the meantime + for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) { + sigaction(signalDefs[i].id, &oldSigActions[i], nullptr); + } + // Return the old stack + sigaltstack(&oldSigStack, nullptr); + isSet = false; + } + } + }; + + bool FatalConditionHandler::isSet = false; + struct sigaction FatalConditionHandler::oldSigActions[DOCTEST_COUNTOF(signalDefs)] = {}; + stack_t FatalConditionHandler::oldSigStack = {}; + size_t FatalConditionHandler::altStackSize = 4 * SIGSTKSZ; + char* FatalConditionHandler::altStackMem = nullptr; + +#endif // DOCTEST_PLATFORM_WINDOWS +#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH + +} // namespace + +namespace { + using namespace detail; + +#ifdef DOCTEST_PLATFORM_WINDOWS +#define DOCTEST_OUTPUT_DEBUG_STRING(text) ::OutputDebugStringA(text) +#else + // TODO: integration with XCode and other IDEs +#define DOCTEST_OUTPUT_DEBUG_STRING(text) +#endif // Platform + + void addAssert(assertType::Enum at) { + if((at & assertType::is_warn) == 0) //!OCLINT bitwise operator in conditional + g_cs->numAssertsCurrentTest_atomic++; + } + + void addFailedAssert(assertType::Enum at) { + if((at & assertType::is_warn) == 0) //!OCLINT bitwise operator in conditional + g_cs->numAssertsFailedCurrentTest_atomic++; + } + +#if defined(DOCTEST_CONFIG_POSIX_SIGNALS) || defined(DOCTEST_CONFIG_WINDOWS_SEH) + void reportFatal(const std::string& message) { + g_cs->failure_flags |= TestCaseFailureReason::Crash; + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception, {message.c_str(), true}); + + while (g_cs->subcaseStack.size()) { + g_cs->subcaseStack.pop_back(); + DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY); + } + + g_cs->finalizeTestCaseData(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs); + } +#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH +} // namespace + +AssertData::AssertData(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type, const StringContains& exception_string) + : m_test_case(g_cs->currentTest), m_at(at), m_file(file), m_line(line), m_expr(expr), + m_failed(true), m_threw(false), m_threw_as(false), m_exception_type(exception_type), + m_exception_string(exception_string) { +#if DOCTEST_MSVC + if (m_expr[0] == ' ') // this happens when variadic macros are disabled under MSVC + ++m_expr; +#endif // MSVC +} + +namespace detail { + ResultBuilder::ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type, const String& exception_string) + : AssertData(at, file, line, expr, exception_type, exception_string) { } + + ResultBuilder::ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr, + const char* exception_type, const Contains& exception_string) + : AssertData(at, file, line, expr, exception_type, exception_string) { } + + void ResultBuilder::setResult(const Result& res) { + m_decomp = res.m_decomp; + m_failed = !res.m_passed; + } + + void ResultBuilder::translateException() { + m_threw = true; + m_exception = translateActiveException(); + } + + bool ResultBuilder::log() { + if(m_at & assertType::is_throws) { //!OCLINT bitwise operator in conditional + m_failed = !m_threw; + } else if((m_at & assertType::is_throws_as) && (m_at & assertType::is_throws_with)) { //!OCLINT + m_failed = !m_threw_as || !m_exception_string.check(m_exception); + } else if(m_at & assertType::is_throws_as) { //!OCLINT bitwise operator in conditional + m_failed = !m_threw_as; + } else if(m_at & assertType::is_throws_with) { //!OCLINT bitwise operator in conditional + m_failed = !m_exception_string.check(m_exception); + } else if(m_at & assertType::is_nothrow) { //!OCLINT bitwise operator in conditional + m_failed = m_threw; + } + + if(m_exception.size()) + m_exception = "\"" + m_exception + "\""; + + if(is_running_in_test) { + addAssert(m_at); + DOCTEST_ITERATE_THROUGH_REPORTERS(log_assert, *this); + + if(m_failed) + addFailedAssert(m_at); + } else if(m_failed) { + failed_out_of_a_testing_context(*this); + } + + return m_failed && isDebuggerActive() && !getContextOptions()->no_breaks && + (g_cs->currentTest == nullptr || !g_cs->currentTest->m_no_breaks); // break into debugger + } + + void ResultBuilder::react() const { + if(m_failed && checkIfShouldThrow(m_at)) + throwException(); + } + + void failed_out_of_a_testing_context(const AssertData& ad) { + if(g_cs->ah) + g_cs->ah(ad); + else + std::abort(); + } + + bool decomp_assert(assertType::Enum at, const char* file, int line, const char* expr, + const Result& result) { + bool failed = !result.m_passed; + + // ################################################################################### + // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT + // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED + // ################################################################################### + DOCTEST_ASSERT_OUT_OF_TESTS(result.m_decomp); + DOCTEST_ASSERT_IN_TESTS(result.m_decomp); + return !failed; + } + + MessageBuilder::MessageBuilder(const char* file, int line, assertType::Enum severity) { + m_stream = tlssPush(); + m_file = file; + m_line = line; + m_severity = severity; + } + + MessageBuilder::~MessageBuilder() { + if (!logged) + tlssPop(); + } + + DOCTEST_DEFINE_INTERFACE(IExceptionTranslator) + + bool MessageBuilder::log() { + if (!logged) { + m_string = tlssPop(); + logged = true; + } + + DOCTEST_ITERATE_THROUGH_REPORTERS(log_message, *this); + + const bool isWarn = m_severity & assertType::is_warn; + + // warn is just a message in this context so we don't treat it as an assert + if(!isWarn) { + addAssert(m_severity); + addFailedAssert(m_severity); + } + + return isDebuggerActive() && !getContextOptions()->no_breaks && !isWarn && + (g_cs->currentTest == nullptr || !g_cs->currentTest->m_no_breaks); // break into debugger + } + + void MessageBuilder::react() { + if(m_severity & assertType::is_require) //!OCLINT bitwise operator in conditional + throwException(); + } +} // namespace detail +namespace { + using namespace detail; + + // clang-format off + +// ================================================================================================= +// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.h/cpp +// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched. +// ================================================================================================= + + class XmlEncode { + public: + enum ForWhat { ForTextNodes, ForAttributes }; + + XmlEncode( std::string const& str, ForWhat forWhat = ForTextNodes ); + + void encodeTo( std::ostream& os ) const; + + friend std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ); + + private: + std::string m_str; + ForWhat m_forWhat; + }; + + class XmlWriter { + public: + + class ScopedElement { + public: + ScopedElement( XmlWriter* writer ); + + ScopedElement( ScopedElement&& other ) DOCTEST_NOEXCEPT; + ScopedElement& operator=( ScopedElement&& other ) DOCTEST_NOEXCEPT; + + ~ScopedElement(); + + ScopedElement& writeText( std::string const& text, bool indent = true ); + + template + ScopedElement& writeAttribute( std::string const& name, T const& attribute ) { + m_writer->writeAttribute( name, attribute ); + return *this; + } + + private: + mutable XmlWriter* m_writer = nullptr; + }; + +#ifndef DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + XmlWriter( std::ostream& os = std::cout ); +#else // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + XmlWriter( std::ostream& os ); +#endif // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + ~XmlWriter(); + + XmlWriter( XmlWriter const& ) = delete; + XmlWriter& operator=( XmlWriter const& ) = delete; + + XmlWriter& startElement( std::string const& name ); + + ScopedElement scopedElement( std::string const& name ); + + XmlWriter& endElement(); + + XmlWriter& writeAttribute( std::string const& name, std::string const& attribute ); + + XmlWriter& writeAttribute( std::string const& name, const char* attribute ); + + XmlWriter& writeAttribute( std::string const& name, bool attribute ); + + template + XmlWriter& writeAttribute( std::string const& name, T const& attribute ) { + std::stringstream rss; + rss << attribute; + return writeAttribute( name, rss.str() ); + } + + XmlWriter& writeText( std::string const& text, bool indent = true ); + + //XmlWriter& writeComment( std::string const& text ); + + //void writeStylesheetRef( std::string const& url ); + + //XmlWriter& writeBlankLine(); + + void ensureTagClosed(); + + void writeDeclaration(); + + private: + + void newlineIfNecessary(); + + bool m_tagIsOpen = false; + bool m_needsNewline = false; + std::vector m_tags; + std::string m_indent; + std::ostream& m_os; + }; + +// ================================================================================================= +// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.h/cpp +// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched. +// ================================================================================================= + +using uchar = unsigned char; + +namespace { + + size_t trailingBytes(unsigned char c) { + if ((c & 0xE0) == 0xC0) { + return 2; + } + if ((c & 0xF0) == 0xE0) { + return 3; + } + if ((c & 0xF8) == 0xF0) { + return 4; + } + DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered"); + } + + uint32_t headerValue(unsigned char c) { + if ((c & 0xE0) == 0xC0) { + return c & 0x1F; + } + if ((c & 0xF0) == 0xE0) { + return c & 0x0F; + } + if ((c & 0xF8) == 0xF0) { + return c & 0x07; + } + DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered"); + } + + void hexEscapeChar(std::ostream& os, unsigned char c) { + std::ios_base::fmtflags f(os.flags()); + os << "\\x" + << std::uppercase << std::hex << std::setfill('0') << std::setw(2) + << static_cast(c); + os.flags(f); + } + +} // anonymous namespace + + XmlEncode::XmlEncode( std::string const& str, ForWhat forWhat ) + : m_str( str ), + m_forWhat( forWhat ) + {} + + void XmlEncode::encodeTo( std::ostream& os ) const { + // Apostrophe escaping not necessary if we always use " to write attributes + // (see: https://www.w3.org/TR/xml/#syntax) + + for( std::size_t idx = 0; idx < m_str.size(); ++ idx ) { + uchar c = m_str[idx]; + switch (c) { + case '<': os << "<"; break; + case '&': os << "&"; break; + + case '>': + // See: https://www.w3.org/TR/xml/#syntax + if (idx > 2 && m_str[idx - 1] == ']' && m_str[idx - 2] == ']') + os << ">"; + else + os << c; + break; + + case '\"': + if (m_forWhat == ForAttributes) + os << """; + else + os << c; + break; + + default: + // Check for control characters and invalid utf-8 + + // Escape control characters in standard ascii + // see https://stackoverflow.com/questions/404107/why-are-control-characters-illegal-in-xml-1-0 + if (c < 0x09 || (c > 0x0D && c < 0x20) || c == 0x7F) { + hexEscapeChar(os, c); + break; + } + + // Plain ASCII: Write it to stream + if (c < 0x7F) { + os << c; + break; + } + + // UTF-8 territory + // Check if the encoding is valid and if it is not, hex escape bytes. + // Important: We do not check the exact decoded values for validity, only the encoding format + // First check that this bytes is a valid lead byte: + // This means that it is not encoded as 1111 1XXX + // Or as 10XX XXXX + if (c < 0xC0 || + c >= 0xF8) { + hexEscapeChar(os, c); + break; + } + + auto encBytes = trailingBytes(c); + // Are there enough bytes left to avoid accessing out-of-bounds memory? + if (idx + encBytes - 1 >= m_str.size()) { + hexEscapeChar(os, c); + break; + } + // The header is valid, check data + // The next encBytes bytes must together be a valid utf-8 + // This means: bitpattern 10XX XXXX and the extracted value is sane (ish) + bool valid = true; + uint32_t value = headerValue(c); + for (std::size_t n = 1; n < encBytes; ++n) { + uchar nc = m_str[idx + n]; + valid &= ((nc & 0xC0) == 0x80); + value = (value << 6) | (nc & 0x3F); + } + + if ( + // Wrong bit pattern of following bytes + (!valid) || + // Overlong encodings + (value < 0x80) || + ( value < 0x800 && encBytes > 2) || // removed "0x80 <= value &&" because redundant + (0x800 < value && value < 0x10000 && encBytes > 3) || + // Encoded value out of range + (value >= 0x110000) + ) { + hexEscapeChar(os, c); + break; + } + + // If we got here, this is in fact a valid(ish) utf-8 sequence + for (std::size_t n = 0; n < encBytes; ++n) { + os << m_str[idx + n]; + } + idx += encBytes - 1; + break; + } + } + } + + std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ) { + xmlEncode.encodeTo( os ); + return os; + } + + XmlWriter::ScopedElement::ScopedElement( XmlWriter* writer ) + : m_writer( writer ) + {} + + XmlWriter::ScopedElement::ScopedElement( ScopedElement&& other ) DOCTEST_NOEXCEPT + : m_writer( other.m_writer ){ + other.m_writer = nullptr; + } + XmlWriter::ScopedElement& XmlWriter::ScopedElement::operator=( ScopedElement&& other ) DOCTEST_NOEXCEPT { + if ( m_writer ) { + m_writer->endElement(); + } + m_writer = other.m_writer; + other.m_writer = nullptr; + return *this; + } + + + XmlWriter::ScopedElement::~ScopedElement() { + if( m_writer ) + m_writer->endElement(); + } + + XmlWriter::ScopedElement& XmlWriter::ScopedElement::writeText( std::string const& text, bool indent ) { + m_writer->writeText( text, indent ); + return *this; + } + + XmlWriter::XmlWriter( std::ostream& os ) : m_os( os ) + { + // writeDeclaration(); // called explicitly by the reporters that use the writer class - see issue #627 + } + + XmlWriter::~XmlWriter() { + while( !m_tags.empty() ) + endElement(); + } + + XmlWriter& XmlWriter::startElement( std::string const& name ) { + ensureTagClosed(); + newlineIfNecessary(); + m_os << m_indent << '<' << name; + m_tags.push_back( name ); + m_indent += " "; + m_tagIsOpen = true; + return *this; + } + + XmlWriter::ScopedElement XmlWriter::scopedElement( std::string const& name ) { + ScopedElement scoped( this ); + startElement( name ); + return scoped; + } + + XmlWriter& XmlWriter::endElement() { + newlineIfNecessary(); + m_indent = m_indent.substr( 0, m_indent.size()-2 ); + if( m_tagIsOpen ) { + m_os << "/>"; + m_tagIsOpen = false; + } + else { + m_os << m_indent << ""; + } + m_os << std::endl; + m_tags.pop_back(); + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, std::string const& attribute ) { + if( !name.empty() && !attribute.empty() ) + m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, const char* attribute ) { + if( !name.empty() && attribute && attribute[0] != '\0' ) + m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeAttribute( std::string const& name, bool attribute ) { + m_os << ' ' << name << "=\"" << ( attribute ? "true" : "false" ) << '"'; + return *this; + } + + XmlWriter& XmlWriter::writeText( std::string const& text, bool indent ) { + if( !text.empty() ){ + bool tagWasOpen = m_tagIsOpen; + ensureTagClosed(); + if( tagWasOpen && indent ) + m_os << m_indent; + m_os << XmlEncode( text ); + m_needsNewline = true; + } + return *this; + } + + //XmlWriter& XmlWriter::writeComment( std::string const& text ) { + // ensureTagClosed(); + // m_os << m_indent << ""; + // m_needsNewline = true; + // return *this; + //} + + //void XmlWriter::writeStylesheetRef( std::string const& url ) { + // m_os << "\n"; + //} + + //XmlWriter& XmlWriter::writeBlankLine() { + // ensureTagClosed(); + // m_os << '\n'; + // return *this; + //} + + void XmlWriter::ensureTagClosed() { + if( m_tagIsOpen ) { + m_os << ">" << std::endl; + m_tagIsOpen = false; + } + } + + void XmlWriter::writeDeclaration() { + m_os << "\n"; + } + + void XmlWriter::newlineIfNecessary() { + if( m_needsNewline ) { + m_os << std::endl; + m_needsNewline = false; + } + } + +// ================================================================================================= +// End of copy-pasted code from Catch +// ================================================================================================= + + // clang-format on + + struct XmlReporter : public IReporter + { + XmlWriter xml; + DOCTEST_DECLARE_MUTEX(mutex) + + // caching pointers/references to objects of these types - safe to do + const ContextOptions& opt; + const TestCaseData* tc = nullptr; + + XmlReporter(const ContextOptions& co) + : xml(*co.cout) + , opt(co) {} + + void log_contexts() { + int num_contexts = get_num_active_contexts(); + if(num_contexts) { + auto contexts = get_active_contexts(); + std::stringstream ss; + for(int i = 0; i < num_contexts; ++i) { + contexts[i]->stringify(&ss); + xml.scopedElement("Info").writeText(ss.str()); + ss.str(""); + } + } + } + + unsigned line(unsigned l) const { return opt.no_line_numbers ? 0 : l; } + + void test_case_start_impl(const TestCaseData& in) { + bool open_ts_tag = false; + if(tc != nullptr) { // we have already opened a test suite + if(std::strcmp(tc->m_test_suite, in.m_test_suite) != 0) { + xml.endElement(); + open_ts_tag = true; + } + } + else { + open_ts_tag = true; // first test case ==> first test suite + } + + if(open_ts_tag) { + xml.startElement("TestSuite"); + xml.writeAttribute("name", in.m_test_suite); + } + + tc = ∈ + xml.startElement("TestCase") + .writeAttribute("name", in.m_name) + .writeAttribute("filename", skipPathFromFilename(in.m_file.c_str())) + .writeAttribute("line", line(in.m_line)) + .writeAttribute("description", in.m_description); + + if(Approx(in.m_timeout) != 0) + xml.writeAttribute("timeout", in.m_timeout); + if(in.m_may_fail) + xml.writeAttribute("may_fail", true); + if(in.m_should_fail) + xml.writeAttribute("should_fail", true); + } + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData& in) override { + test_run_start(); + if(opt.list_reporters) { + for(auto& curr : getListeners()) + xml.scopedElement("Listener") + .writeAttribute("priority", curr.first.first) + .writeAttribute("name", curr.first.second); + for(auto& curr : getReporters()) + xml.scopedElement("Reporter") + .writeAttribute("priority", curr.first.first) + .writeAttribute("name", curr.first.second); + } else if(opt.count || opt.list_test_cases) { + for(unsigned i = 0; i < in.num_data; ++i) { + xml.scopedElement("TestCase").writeAttribute("name", in.data[i]->m_name) + .writeAttribute("testsuite", in.data[i]->m_test_suite) + .writeAttribute("filename", skipPathFromFilename(in.data[i]->m_file.c_str())) + .writeAttribute("line", line(in.data[i]->m_line)) + .writeAttribute("skipped", in.data[i]->m_skip); + } + xml.scopedElement("OverallResultsTestCases") + .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters); + } else if(opt.list_test_suites) { + for(unsigned i = 0; i < in.num_data; ++i) + xml.scopedElement("TestSuite").writeAttribute("name", in.data[i]->m_test_suite); + xml.scopedElement("OverallResultsTestCases") + .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters); + xml.scopedElement("OverallResultsTestSuites") + .writeAttribute("unskipped", in.run_stats->numTestSuitesPassingFilters); + } + xml.endElement(); + } + + void test_run_start() override { + xml.writeDeclaration(); + + // remove .exe extension - mainly to have the same output on UNIX and Windows + std::string binary_name = skipPathFromFilename(opt.binary_name.c_str()); +#ifdef DOCTEST_PLATFORM_WINDOWS + if(binary_name.rfind(".exe") != std::string::npos) + binary_name = binary_name.substr(0, binary_name.length() - 4); +#endif // DOCTEST_PLATFORM_WINDOWS + + xml.startElement("doctest").writeAttribute("binary", binary_name); + if(opt.no_version == false) + xml.writeAttribute("version", DOCTEST_VERSION_STR); + + // only the consequential ones (TODO: filters) + xml.scopedElement("Options") + .writeAttribute("order_by", opt.order_by.c_str()) + .writeAttribute("rand_seed", opt.rand_seed) + .writeAttribute("first", opt.first) + .writeAttribute("last", opt.last) + .writeAttribute("abort_after", opt.abort_after) + .writeAttribute("subcase_filter_levels", opt.subcase_filter_levels) + .writeAttribute("case_sensitive", opt.case_sensitive) + .writeAttribute("no_throw", opt.no_throw) + .writeAttribute("no_skip", opt.no_skip); + } + + void test_run_end(const TestRunStats& p) override { + if(tc) // the TestSuite tag - only if there has been at least 1 test case + xml.endElement(); + + xml.scopedElement("OverallResultsAsserts") + .writeAttribute("successes", p.numAsserts - p.numAssertsFailed) + .writeAttribute("failures", p.numAssertsFailed); + + xml.startElement("OverallResultsTestCases") + .writeAttribute("successes", + p.numTestCasesPassingFilters - p.numTestCasesFailed) + .writeAttribute("failures", p.numTestCasesFailed); + if(opt.no_skipped_summary == false) + xml.writeAttribute("skipped", p.numTestCases - p.numTestCasesPassingFilters); + xml.endElement(); + + xml.endElement(); + } + + void test_case_start(const TestCaseData& in) override { + test_case_start_impl(in); + xml.ensureTagClosed(); + } + + void test_case_reenter(const TestCaseData&) override {} + + void test_case_end(const CurrentTestCaseStats& st) override { + xml.startElement("OverallResultsAsserts") + .writeAttribute("successes", + st.numAssertsCurrentTest - st.numAssertsFailedCurrentTest) + .writeAttribute("failures", st.numAssertsFailedCurrentTest) + .writeAttribute("test_case_success", st.testCaseSuccess); + if(opt.duration) + xml.writeAttribute("duration", st.seconds); + if(tc->m_expected_failures) + xml.writeAttribute("expected_failures", tc->m_expected_failures); + xml.endElement(); + + xml.endElement(); + } + + void test_case_exception(const TestCaseException& e) override { + DOCTEST_LOCK_MUTEX(mutex) + + xml.scopedElement("Exception") + .writeAttribute("crash", e.is_crash) + .writeText(e.error_string.c_str()); + } + + void subcase_start(const SubcaseSignature& in) override { + xml.startElement("SubCase") + .writeAttribute("name", in.m_name) + .writeAttribute("filename", skipPathFromFilename(in.m_file)) + .writeAttribute("line", line(in.m_line)); + xml.ensureTagClosed(); + } + + void subcase_end() override { xml.endElement(); } + + void log_assert(const AssertData& rb) override { + if(!rb.m_failed && !opt.success) + return; + + DOCTEST_LOCK_MUTEX(mutex) + + xml.startElement("Expression") + .writeAttribute("success", !rb.m_failed) + .writeAttribute("type", assertString(rb.m_at)) + .writeAttribute("filename", skipPathFromFilename(rb.m_file)) + .writeAttribute("line", line(rb.m_line)); + + xml.scopedElement("Original").writeText(rb.m_expr); + + if(rb.m_threw) + xml.scopedElement("Exception").writeText(rb.m_exception.c_str()); + + if(rb.m_at & assertType::is_throws_as) + xml.scopedElement("ExpectedException").writeText(rb.m_exception_type); + if(rb.m_at & assertType::is_throws_with) + xml.scopedElement("ExpectedExceptionString").writeText(rb.m_exception_string.c_str()); + if((rb.m_at & assertType::is_normal) && !rb.m_threw) + xml.scopedElement("Expanded").writeText(rb.m_decomp.c_str()); + + log_contexts(); + + xml.endElement(); + } + + void log_message(const MessageData& mb) override { + DOCTEST_LOCK_MUTEX(mutex) + + xml.startElement("Message") + .writeAttribute("type", failureString(mb.m_severity)) + .writeAttribute("filename", skipPathFromFilename(mb.m_file)) + .writeAttribute("line", line(mb.m_line)); + + xml.scopedElement("Text").writeText(mb.m_string.c_str()); + + log_contexts(); + + xml.endElement(); + } + + void test_case_skipped(const TestCaseData& in) override { + if(opt.no_skipped_summary == false) { + test_case_start_impl(in); + xml.writeAttribute("skipped", "true"); + xml.endElement(); + } + } + }; + + DOCTEST_REGISTER_REPORTER("xml", 0, XmlReporter); + + void fulltext_log_assert_to_stream(std::ostream& s, const AssertData& rb) { + if((rb.m_at & (assertType::is_throws_as | assertType::is_throws_with)) == + 0) //!OCLINT bitwise operator in conditional + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << " ) " + << Color::None; + + if(rb.m_at & assertType::is_throws) { //!OCLINT bitwise operator in conditional + s << (rb.m_threw ? "threw as expected!" : "did NOT throw at all!") << "\n"; + } else if((rb.m_at & assertType::is_throws_as) && + (rb.m_at & assertType::is_throws_with)) { //!OCLINT + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \"" + << rb.m_exception_string.c_str() + << "\", " << rb.m_exception_type << " ) " << Color::None; + if(rb.m_threw) { + if(!rb.m_failed) { + s << "threw as expected!\n"; + } else { + s << "threw a DIFFERENT exception! (contents: " << rb.m_exception << ")\n"; + } + } else { + s << "did NOT throw at all!\n"; + } + } else if(rb.m_at & + assertType::is_throws_as) { //!OCLINT bitwise operator in conditional + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", " + << rb.m_exception_type << " ) " << Color::None + << (rb.m_threw ? (rb.m_threw_as ? "threw as expected!" : + "threw a DIFFERENT exception: ") : + "did NOT throw at all!") + << Color::Cyan << rb.m_exception << "\n"; + } else if(rb.m_at & + assertType::is_throws_with) { //!OCLINT bitwise operator in conditional + s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \"" + << rb.m_exception_string.c_str() + << "\" ) " << Color::None + << (rb.m_threw ? (!rb.m_failed ? "threw as expected!" : + "threw a DIFFERENT exception: ") : + "did NOT throw at all!") + << Color::Cyan << rb.m_exception << "\n"; + } else if(rb.m_at & assertType::is_nothrow) { //!OCLINT bitwise operator in conditional + s << (rb.m_threw ? "THREW exception: " : "didn't throw!") << Color::Cyan + << rb.m_exception << "\n"; + } else { + s << (rb.m_threw ? "THREW exception: " : + (!rb.m_failed ? "is correct!\n" : "is NOT correct!\n")); + if(rb.m_threw) + s << rb.m_exception << "\n"; + else + s << " values: " << assertString(rb.m_at) << "( " << rb.m_decomp << " )\n"; + } + } + + // TODO: + // - log_message() + // - respond to queries + // - honor remaining options + // - more attributes in tags + struct JUnitReporter : public IReporter + { + XmlWriter xml; + DOCTEST_DECLARE_MUTEX(mutex) + Timer timer; + std::vector deepestSubcaseStackNames; + + struct JUnitTestCaseData + { + static std::string getCurrentTimestamp() { + // Beware, this is not reentrant because of backward compatibility issues + // Also, UTC only, again because of backward compatibility (%z is C++11) + time_t rawtime; + std::time(&rawtime); + auto const timeStampSize = sizeof("2017-01-16T17:06:45Z"); + + std::tm timeInfo; +#ifdef DOCTEST_PLATFORM_WINDOWS + gmtime_s(&timeInfo, &rawtime); +#else // DOCTEST_PLATFORM_WINDOWS + gmtime_r(&rawtime, &timeInfo); +#endif // DOCTEST_PLATFORM_WINDOWS + + char timeStamp[timeStampSize]; + const char* const fmt = "%Y-%m-%dT%H:%M:%SZ"; + + std::strftime(timeStamp, timeStampSize, fmt, &timeInfo); + return std::string(timeStamp); + } + + struct JUnitTestMessage + { + JUnitTestMessage(const std::string& _message, const std::string& _type, const std::string& _details) + : message(_message), type(_type), details(_details) {} + + JUnitTestMessage(const std::string& _message, const std::string& _details) + : message(_message), type(), details(_details) {} + + std::string message, type, details; + }; + + struct JUnitTestCase + { + JUnitTestCase(const std::string& _classname, const std::string& _name) + : classname(_classname), name(_name), time(0), failures() {} + + std::string classname, name; + double time; + std::vector failures, errors; + }; + + void add(const std::string& classname, const std::string& name) { + testcases.emplace_back(classname, name); + } + + void appendSubcaseNamesToLastTestcase(std::vector nameStack) { + for(auto& curr: nameStack) + if(curr.size()) + testcases.back().name += std::string("/") + curr.c_str(); + } + + void addTime(double time) { + if(time < 1e-4) + time = 0; + testcases.back().time = time; + totalSeconds += time; + } + + void addFailure(const std::string& message, const std::string& type, const std::string& details) { + testcases.back().failures.emplace_back(message, type, details); + ++totalFailures; + } + + void addError(const std::string& message, const std::string& details) { + testcases.back().errors.emplace_back(message, details); + ++totalErrors; + } + + std::vector testcases; + double totalSeconds = 0; + int totalErrors = 0, totalFailures = 0; + }; + + JUnitTestCaseData testCaseData; + + // caching pointers/references to objects of these types - safe to do + const ContextOptions& opt; + const TestCaseData* tc = nullptr; + + JUnitReporter(const ContextOptions& co) + : xml(*co.cout) + , opt(co) {} + + unsigned line(unsigned l) const { return opt.no_line_numbers ? 0 : l; } + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData&) override { + xml.writeDeclaration(); + } + + void test_run_start() override { + xml.writeDeclaration(); + } + + void test_run_end(const TestRunStats& p) override { + // remove .exe extension - mainly to have the same output on UNIX and Windows + std::string binary_name = skipPathFromFilename(opt.binary_name.c_str()); +#ifdef DOCTEST_PLATFORM_WINDOWS + if(binary_name.rfind(".exe") != std::string::npos) + binary_name = binary_name.substr(0, binary_name.length() - 4); +#endif // DOCTEST_PLATFORM_WINDOWS + xml.startElement("testsuites"); + xml.startElement("testsuite").writeAttribute("name", binary_name) + .writeAttribute("errors", testCaseData.totalErrors) + .writeAttribute("failures", testCaseData.totalFailures) + .writeAttribute("tests", p.numAsserts); + if(opt.no_time_in_output == false) { + xml.writeAttribute("time", testCaseData.totalSeconds); + xml.writeAttribute("timestamp", JUnitTestCaseData::getCurrentTimestamp()); + } + if(opt.no_version == false) + xml.writeAttribute("doctest_version", DOCTEST_VERSION_STR); + + for(const auto& testCase : testCaseData.testcases) { + xml.startElement("testcase") + .writeAttribute("classname", testCase.classname) + .writeAttribute("name", testCase.name); + if(opt.no_time_in_output == false) + xml.writeAttribute("time", testCase.time); + // This is not ideal, but it should be enough to mimic gtest's junit output. + xml.writeAttribute("status", "run"); + + for(const auto& failure : testCase.failures) { + xml.scopedElement("failure") + .writeAttribute("message", failure.message) + .writeAttribute("type", failure.type) + .writeText(failure.details, false); + } + + for(const auto& error : testCase.errors) { + xml.scopedElement("error") + .writeAttribute("message", error.message) + .writeText(error.details); + } + + xml.endElement(); + } + xml.endElement(); + xml.endElement(); + } + + void test_case_start(const TestCaseData& in) override { + testCaseData.add(skipPathFromFilename(in.m_file.c_str()), in.m_name); + timer.start(); + } + + void test_case_reenter(const TestCaseData& in) override { + testCaseData.addTime(timer.getElapsedSeconds()); + testCaseData.appendSubcaseNamesToLastTestcase(deepestSubcaseStackNames); + deepestSubcaseStackNames.clear(); + + timer.start(); + testCaseData.add(skipPathFromFilename(in.m_file.c_str()), in.m_name); + } + + void test_case_end(const CurrentTestCaseStats&) override { + testCaseData.addTime(timer.getElapsedSeconds()); + testCaseData.appendSubcaseNamesToLastTestcase(deepestSubcaseStackNames); + deepestSubcaseStackNames.clear(); + } + + void test_case_exception(const TestCaseException& e) override { + DOCTEST_LOCK_MUTEX(mutex) + testCaseData.addError("exception", e.error_string.c_str()); + } + + void subcase_start(const SubcaseSignature& in) override { + deepestSubcaseStackNames.push_back(in.m_name); + } + + void subcase_end() override {} + + void log_assert(const AssertData& rb) override { + if(!rb.m_failed) // report only failures & ignore the `success` option + return; + + DOCTEST_LOCK_MUTEX(mutex) + + std::ostringstream os; + os << skipPathFromFilename(rb.m_file) << (opt.gnu_file_line ? ":" : "(") + << line(rb.m_line) << (opt.gnu_file_line ? ":" : "):") << std::endl; + + fulltext_log_assert_to_stream(os, rb); + log_contexts(os); + testCaseData.addFailure(rb.m_decomp.c_str(), assertString(rb.m_at), os.str()); + } + + void log_message(const MessageData& mb) override { + if(mb.m_severity & assertType::is_warn) // report only failures + return; + + DOCTEST_LOCK_MUTEX(mutex) + + std::ostringstream os; + os << skipPathFromFilename(mb.m_file) << (opt.gnu_file_line ? ":" : "(") + << line(mb.m_line) << (opt.gnu_file_line ? ":" : "):") << std::endl; + + os << mb.m_string.c_str() << "\n"; + log_contexts(os); + + testCaseData.addFailure(mb.m_string.c_str(), + mb.m_severity & assertType::is_check ? "FAIL_CHECK" : "FAIL", os.str()); + } + + void test_case_skipped(const TestCaseData&) override {} + + void log_contexts(std::ostringstream& s) { + int num_contexts = get_num_active_contexts(); + if(num_contexts) { + auto contexts = get_active_contexts(); + + s << " logged: "; + for(int i = 0; i < num_contexts; ++i) { + s << (i == 0 ? "" : " "); + contexts[i]->stringify(&s); + s << std::endl; + } + } + } + }; + + DOCTEST_REGISTER_REPORTER("junit", 0, JUnitReporter); + + struct Whitespace + { + int nrSpaces; + explicit Whitespace(int nr) + : nrSpaces(nr) {} + }; + + std::ostream& operator<<(std::ostream& out, const Whitespace& ws) { + if(ws.nrSpaces != 0) + out << std::setw(ws.nrSpaces) << ' '; + return out; + } + + struct ConsoleReporter : public IReporter + { + std::ostream& s; + bool hasLoggedCurrentTestStart; + std::vector subcasesStack; + size_t currentSubcaseLevel; + DOCTEST_DECLARE_MUTEX(mutex) + + // caching pointers/references to objects of these types - safe to do + const ContextOptions& opt; + const TestCaseData* tc; + + ConsoleReporter(const ContextOptions& co) + : s(*co.cout) + , opt(co) {} + + ConsoleReporter(const ContextOptions& co, std::ostream& ostr) + : s(ostr) + , opt(co) {} + + // ========================================================================================= + // WHAT FOLLOWS ARE HELPERS USED BY THE OVERRIDES OF THE VIRTUAL METHODS OF THE INTERFACE + // ========================================================================================= + + void separator_to_stream() { + s << Color::Yellow + << "===============================================================================" + "\n"; + } + + const char* getSuccessOrFailString(bool success, assertType::Enum at, + const char* success_str) { + if(success) + return success_str; + return failureString(at); + } + + Color::Enum getSuccessOrFailColor(bool success, assertType::Enum at) { + return success ? Color::BrightGreen : + (at & assertType::is_warn) ? Color::Yellow : Color::Red; + } + + void successOrFailColoredStringToStream(bool success, assertType::Enum at, + const char* success_str = "SUCCESS") { + s << getSuccessOrFailColor(success, at) + << getSuccessOrFailString(success, at, success_str) << ": "; + } + + void log_contexts() { + int num_contexts = get_num_active_contexts(); + if(num_contexts) { + auto contexts = get_active_contexts(); + + s << Color::None << " logged: "; + for(int i = 0; i < num_contexts; ++i) { + s << (i == 0 ? "" : " "); + contexts[i]->stringify(&s); + s << "\n"; + } + } + + s << "\n"; + } + + // this was requested to be made virtual so users could override it + virtual void file_line_to_stream(const char* file, int line, + const char* tail = "") { + s << Color::LightGrey << skipPathFromFilename(file) << (opt.gnu_file_line ? ":" : "(") + << (opt.no_line_numbers ? 0 : line) // 0 or the real num depending on the option + << (opt.gnu_file_line ? ":" : "):") << tail; + } + + void logTestStart() { + if(hasLoggedCurrentTestStart) + return; + + separator_to_stream(); + file_line_to_stream(tc->m_file.c_str(), tc->m_line, "\n"); + if(tc->m_description) + s << Color::Yellow << "DESCRIPTION: " << Color::None << tc->m_description << "\n"; + if(tc->m_test_suite && tc->m_test_suite[0] != '\0') + s << Color::Yellow << "TEST SUITE: " << Color::None << tc->m_test_suite << "\n"; + if(strncmp(tc->m_name, " Scenario:", 11) != 0) + s << Color::Yellow << "TEST CASE: "; + s << Color::None << tc->m_name << "\n"; + + for(size_t i = 0; i < currentSubcaseLevel; ++i) { + if(subcasesStack[i].m_name[0] != '\0') + s << " " << subcasesStack[i].m_name << "\n"; + } + + if(currentSubcaseLevel != subcasesStack.size()) { + s << Color::Yellow << "\nDEEPEST SUBCASE STACK REACHED (DIFFERENT FROM THE CURRENT ONE):\n" << Color::None; + for(size_t i = 0; i < subcasesStack.size(); ++i) { + if(subcasesStack[i].m_name[0] != '\0') + s << " " << subcasesStack[i].m_name << "\n"; + } + } + + s << "\n"; + + hasLoggedCurrentTestStart = true; + } + + void printVersion() { + if(opt.no_version == false) + s << Color::Cyan << "[doctest] " << Color::None << "doctest version is \"" + << DOCTEST_VERSION_STR << "\"\n"; + } + + void printIntro() { + if(opt.no_intro == false) { + printVersion(); + s << Color::Cyan << "[doctest] " << Color::None + << "run with \"--" DOCTEST_OPTIONS_PREFIX_DISPLAY "help\" for options\n"; + } + } + + void printHelp() { + int sizePrefixDisplay = static_cast(strlen(DOCTEST_OPTIONS_PREFIX_DISPLAY)); + printVersion(); + // clang-format off + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "boolean values: \"1/on/yes/true\" or \"0/off/no/false\"\n"; + s << Color::Cyan << "[doctest] " << Color::None; + s << "filter values: \"str1,str2,str3\" (comma separated strings)\n"; + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "filters use wildcards for matching strings\n"; + s << Color::Cyan << "[doctest] " << Color::None; + s << "something passes a filter if any of the strings in a filter matches\n"; +#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "ALL FLAGS, OPTIONS AND FILTERS ALSO AVAILABLE WITH A \"" DOCTEST_CONFIG_OPTIONS_PREFIX "\" PREFIX!!!\n"; +#endif + s << Color::Cyan << "[doctest]\n" << Color::None; + s << Color::Cyan << "[doctest] " << Color::None; + s << "Query flags - the program quits after them. Available:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "?, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "help, -" DOCTEST_OPTIONS_PREFIX_DISPLAY "h " + << Whitespace(sizePrefixDisplay*0) << "prints this message\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "v, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "version " + << Whitespace(sizePrefixDisplay*1) << "prints the version\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "c, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "count " + << Whitespace(sizePrefixDisplay*1) << "prints the number of matching tests\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ltc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-cases " + << Whitespace(sizePrefixDisplay*1) << "lists all matching tests by name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-suites " + << Whitespace(sizePrefixDisplay*1) << "lists all matching test suites\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-reporters " + << Whitespace(sizePrefixDisplay*1) << "lists all registered reporters\n\n"; + // ================================================================================== << 79 + s << Color::Cyan << "[doctest] " << Color::None; + s << "The available / options/filters are:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case= " + << Whitespace(sizePrefixDisplay*1) << "filters tests by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file= " + << Whitespace(sizePrefixDisplay*1) << "filters tests by their file\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sfe, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their file\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite= " + << Whitespace(sizePrefixDisplay*1) << "filters tests by their test suite\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tse, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their test suite\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase= " + << Whitespace(sizePrefixDisplay*1) << "filters subcases by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-exclude= " + << Whitespace(sizePrefixDisplay*1) << "filters OUT subcases by their name\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "r, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "reporters= " + << Whitespace(sizePrefixDisplay*1) << "reporters to use (console is default)\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "o, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "out= " + << Whitespace(sizePrefixDisplay*1) << "output filename\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ob, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "order-by= " + << Whitespace(sizePrefixDisplay*1) << "how the tests should be ordered\n"; + s << Whitespace(sizePrefixDisplay*3) << " - [file/suite/name/rand/none]\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "rs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "rand-seed= " + << Whitespace(sizePrefixDisplay*1) << "seed for random ordering\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "f, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "first= " + << Whitespace(sizePrefixDisplay*1) << "the first test passing the filters to\n"; + s << Whitespace(sizePrefixDisplay*3) << " execute - for range-based execution\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "l, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "last= " + << Whitespace(sizePrefixDisplay*1) << "the last test passing the filters to\n"; + s << Whitespace(sizePrefixDisplay*3) << " execute - for range-based execution\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "aa, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "abort-after= " + << Whitespace(sizePrefixDisplay*1) << "stop after failed assertions\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "scfl,--" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-filter-levels= " + << Whitespace(sizePrefixDisplay*1) << "apply filters for the first levels\n"; + s << Color::Cyan << "\n[doctest] " << Color::None; + s << "Bool options - can be used like flags and true is assumed. Available:\n\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "s, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "success= " + << Whitespace(sizePrefixDisplay*1) << "include successful assertions in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "cs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "case-sensitive= " + << Whitespace(sizePrefixDisplay*1) << "filters being treated as case sensitive\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "e, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "exit= " + << Whitespace(sizePrefixDisplay*1) << "exits after the tests finish\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "d, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "duration= " + << Whitespace(sizePrefixDisplay*1) << "prints the time duration of each test\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "m, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "minimal= " + << Whitespace(sizePrefixDisplay*1) << "minimal console output (only failures)\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "q, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "quiet= " + << Whitespace(sizePrefixDisplay*1) << "no console output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nt, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-throw= " + << Whitespace(sizePrefixDisplay*1) << "skips exceptions-related assert checks\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ne, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-exitcode= " + << Whitespace(sizePrefixDisplay*1) << "returns (or exits) always with success\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-run= " + << Whitespace(sizePrefixDisplay*1) << "skips all runtime doctest operations\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ni, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-intro= " + << Whitespace(sizePrefixDisplay*1) << "omit the framework intro in the output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nv, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-version= " + << Whitespace(sizePrefixDisplay*1) << "omit the framework version in the output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-colors= " + << Whitespace(sizePrefixDisplay*1) << "disables colors in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "fc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "force-colors= " + << Whitespace(sizePrefixDisplay*1) << "use colors even when not in a tty\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nb, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-breaks= " + << Whitespace(sizePrefixDisplay*1) << "disables breakpoints in debuggers\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ns, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-skip= " + << Whitespace(sizePrefixDisplay*1) << "don't skip test cases marked as skip\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "gfl, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "gnu-file-line= " + << Whitespace(sizePrefixDisplay*1) << ":n: vs (n): for line numbers in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "npf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-path-filenames= " + << Whitespace(sizePrefixDisplay*1) << "only filenames and no paths in output\n"; + s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nln, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-line-numbers= " + << Whitespace(sizePrefixDisplay*1) << "0 instead of real line numbers in output\n"; + // ================================================================================== << 79 + // clang-format on + + s << Color::Cyan << "\n[doctest] " << Color::None; + s << "for more information visit the project documentation\n\n"; + } + + void printRegisteredReporters() { + printVersion(); + auto printReporters = [this] (const reporterMap& reporters, const char* type) { + if(reporters.size()) { + s << Color::Cyan << "[doctest] " << Color::None << "listing all registered " << type << "\n"; + for(auto& curr : reporters) + s << "priority: " << std::setw(5) << curr.first.first + << " name: " << curr.first.second << "\n"; + } + }; + printReporters(getListeners(), "listeners"); + printReporters(getReporters(), "reporters"); + } + + // ========================================================================================= + // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE + // ========================================================================================= + + void report_query(const QueryData& in) override { + if(opt.version) { + printVersion(); + } else if(opt.help) { + printHelp(); + } else if(opt.list_reporters) { + printRegisteredReporters(); + } else if(opt.count || opt.list_test_cases) { + if(opt.list_test_cases) { + s << Color::Cyan << "[doctest] " << Color::None + << "listing all test case names\n"; + separator_to_stream(); + } + + for(unsigned i = 0; i < in.num_data; ++i) + s << Color::None << in.data[i]->m_name << "\n"; + + separator_to_stream(); + + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " + << g_cs->numTestCasesPassingFilters << "\n"; + + } else if(opt.list_test_suites) { + s << Color::Cyan << "[doctest] " << Color::None << "listing all test suites\n"; + separator_to_stream(); + + for(unsigned i = 0; i < in.num_data; ++i) + s << Color::None << in.data[i]->m_test_suite << "\n"; + + separator_to_stream(); + + s << Color::Cyan << "[doctest] " << Color::None + << "unskipped test cases passing the current filters: " + << g_cs->numTestCasesPassingFilters << "\n"; + s << Color::Cyan << "[doctest] " << Color::None + << "test suites with unskipped test cases passing the current filters: " + << g_cs->numTestSuitesPassingFilters << "\n"; + } + } + + void test_run_start() override { + if(!opt.minimal) + printIntro(); + } + + void test_run_end(const TestRunStats& p) override { + if(opt.minimal && p.numTestCasesFailed == 0) + return; + + separator_to_stream(); + s << std::dec; + + auto totwidth = int(std::ceil(log10(static_cast(std::max(p.numTestCasesPassingFilters, static_cast(p.numAsserts))) + 1))); + auto passwidth = int(std::ceil(log10(static_cast(std::max(p.numTestCasesPassingFilters - p.numTestCasesFailed, static_cast(p.numAsserts - p.numAssertsFailed))) + 1))); + auto failwidth = int(std::ceil(log10(static_cast(std::max(p.numTestCasesFailed, static_cast(p.numAssertsFailed))) + 1))); + const bool anythingFailed = p.numTestCasesFailed > 0 || p.numAssertsFailed > 0; + s << Color::Cyan << "[doctest] " << Color::None << "test cases: " << std::setw(totwidth) + << p.numTestCasesPassingFilters << " | " + << ((p.numTestCasesPassingFilters == 0 || anythingFailed) ? Color::None : + Color::Green) + << std::setw(passwidth) << p.numTestCasesPassingFilters - p.numTestCasesFailed << " passed" + << Color::None << " | " << (p.numTestCasesFailed > 0 ? Color::Red : Color::None) + << std::setw(failwidth) << p.numTestCasesFailed << " failed" << Color::None << " |"; + if(opt.no_skipped_summary == false) { + const int numSkipped = p.numTestCases - p.numTestCasesPassingFilters; + s << " " << (numSkipped == 0 ? Color::None : Color::Yellow) << numSkipped + << " skipped" << Color::None; + } + s << "\n"; + s << Color::Cyan << "[doctest] " << Color::None << "assertions: " << std::setw(totwidth) + << p.numAsserts << " | " + << ((p.numAsserts == 0 || anythingFailed) ? Color::None : Color::Green) + << std::setw(passwidth) << (p.numAsserts - p.numAssertsFailed) << " passed" << Color::None + << " | " << (p.numAssertsFailed > 0 ? Color::Red : Color::None) << std::setw(failwidth) + << p.numAssertsFailed << " failed" << Color::None << " |\n"; + s << Color::Cyan << "[doctest] " << Color::None + << "Status: " << (p.numTestCasesFailed > 0 ? Color::Red : Color::Green) + << ((p.numTestCasesFailed > 0) ? "FAILURE!" : "SUCCESS!") << Color::None << std::endl; + } + + void test_case_start(const TestCaseData& in) override { + hasLoggedCurrentTestStart = false; + tc = ∈ + subcasesStack.clear(); + currentSubcaseLevel = 0; + } + + void test_case_reenter(const TestCaseData&) override { + subcasesStack.clear(); + } + + void test_case_end(const CurrentTestCaseStats& st) override { + if(tc->m_no_output) + return; + + // log the preamble of the test case only if there is something + // else to print - something other than that an assert has failed + if(opt.duration || + (st.failure_flags && st.failure_flags != static_cast(TestCaseFailureReason::AssertFailure))) + logTestStart(); + + if(opt.duration) + s << Color::None << std::setprecision(6) << std::fixed << st.seconds + << " s: " << tc->m_name << "\n"; + + if(st.failure_flags & TestCaseFailureReason::Timeout) + s << Color::Red << "Test case exceeded time limit of " << std::setprecision(6) + << std::fixed << tc->m_timeout << "!\n"; + + if(st.failure_flags & TestCaseFailureReason::ShouldHaveFailedButDidnt) { + s << Color::Red << "Should have failed but didn't! Marking it as failed!\n"; + } else if(st.failure_flags & TestCaseFailureReason::ShouldHaveFailedAndDid) { + s << Color::Yellow << "Failed as expected so marking it as not failed\n"; + } else if(st.failure_flags & TestCaseFailureReason::CouldHaveFailedAndDid) { + s << Color::Yellow << "Allowed to fail so marking it as not failed\n"; + } else if(st.failure_flags & TestCaseFailureReason::DidntFailExactlyNumTimes) { + s << Color::Red << "Didn't fail exactly " << tc->m_expected_failures + << " times so marking it as failed!\n"; + } else if(st.failure_flags & TestCaseFailureReason::FailedExactlyNumTimes) { + s << Color::Yellow << "Failed exactly " << tc->m_expected_failures + << " times as expected so marking it as not failed!\n"; + } + if(st.failure_flags & TestCaseFailureReason::TooManyFailedAsserts) { + s << Color::Red << "Aborting - too many failed asserts!\n"; + } + s << Color::None; // lgtm [cpp/useless-expression] + } + + void test_case_exception(const TestCaseException& e) override { + DOCTEST_LOCK_MUTEX(mutex) + if(tc->m_no_output) + return; + + logTestStart(); + + file_line_to_stream(tc->m_file.c_str(), tc->m_line, " "); + successOrFailColoredStringToStream(false, e.is_crash ? assertType::is_require : + assertType::is_check); + s << Color::Red << (e.is_crash ? "test case CRASHED: " : "test case THREW exception: ") + << Color::Cyan << e.error_string << "\n"; + + int num_stringified_contexts = get_num_stringified_contexts(); + if(num_stringified_contexts) { + auto stringified_contexts = get_stringified_contexts(); + s << Color::None << " logged: "; + for(int i = num_stringified_contexts; i > 0; --i) { + s << (i == num_stringified_contexts ? "" : " ") + << stringified_contexts[i - 1] << "\n"; + } + } + s << "\n" << Color::None; + } + + void subcase_start(const SubcaseSignature& subc) override { + subcasesStack.push_back(subc); + ++currentSubcaseLevel; + hasLoggedCurrentTestStart = false; + } + + void subcase_end() override { + --currentSubcaseLevel; + hasLoggedCurrentTestStart = false; + } + + void log_assert(const AssertData& rb) override { + if((!rb.m_failed && !opt.success) || tc->m_no_output) + return; + + DOCTEST_LOCK_MUTEX(mutex) + + logTestStart(); + + file_line_to_stream(rb.m_file, rb.m_line, " "); + successOrFailColoredStringToStream(!rb.m_failed, rb.m_at); + + fulltext_log_assert_to_stream(s, rb); + + log_contexts(); + } + + void log_message(const MessageData& mb) override { + if(tc->m_no_output) + return; + + DOCTEST_LOCK_MUTEX(mutex) + + logTestStart(); + + file_line_to_stream(mb.m_file, mb.m_line, " "); + s << getSuccessOrFailColor(false, mb.m_severity) + << getSuccessOrFailString(mb.m_severity & assertType::is_warn, mb.m_severity, + "MESSAGE") << ": "; + s << Color::None << mb.m_string << "\n"; + log_contexts(); + } + + void test_case_skipped(const TestCaseData&) override {} + }; + + DOCTEST_REGISTER_REPORTER("console", 0, ConsoleReporter); + +#ifdef DOCTEST_PLATFORM_WINDOWS + struct DebugOutputWindowReporter : public ConsoleReporter + { + DOCTEST_THREAD_LOCAL static std::ostringstream oss; + + DebugOutputWindowReporter(const ContextOptions& co) + : ConsoleReporter(co, oss) {} + +#define DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(func, type, arg) \ + void func(type arg) override { \ + bool with_col = g_no_colors; \ + g_no_colors = false; \ + ConsoleReporter::func(arg); \ + if(oss.tellp() != std::streampos{}) { \ + DOCTEST_OUTPUT_DEBUG_STRING(oss.str().c_str()); \ + oss.str(""); \ + } \ + g_no_colors = with_col; \ + } + + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_start, DOCTEST_EMPTY, DOCTEST_EMPTY) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_end, const TestRunStats&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_start, const TestCaseData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_reenter, const TestCaseData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_end, const CurrentTestCaseStats&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_exception, const TestCaseException&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_start, const SubcaseSignature&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_end, DOCTEST_EMPTY, DOCTEST_EMPTY) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_assert, const AssertData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_message, const MessageData&, in) + DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_skipped, const TestCaseData&, in) + }; + + DOCTEST_THREAD_LOCAL std::ostringstream DebugOutputWindowReporter::oss; +#endif // DOCTEST_PLATFORM_WINDOWS + + // the implementation of parseOption() + bool parseOptionImpl(int argc, const char* const* argv, const char* pattern, String* value) { + // going from the end to the beginning and stopping on the first occurrence from the end + for(int i = argc; i > 0; --i) { + auto index = i - 1; + auto temp = std::strstr(argv[index], pattern); + if(temp && (value || strlen(temp) == strlen(pattern))) { //!OCLINT prefer early exits and continue + // eliminate matches in which the chars before the option are not '-' + bool noBadCharsFound = true; + auto curr = argv[index]; + while(curr != temp) { + if(*curr++ != '-') { + noBadCharsFound = false; + break; + } + } + if(noBadCharsFound && argv[index][0] == '-') { + if(value) { + // parsing the value of an option + temp += strlen(pattern); + const unsigned len = strlen(temp); + if(len) { + *value = temp; + return true; + } + } else { + // just a flag - no value + return true; + } + } + } + } + return false; + } + + // parses an option and returns the string after the '=' character + bool parseOption(int argc, const char* const* argv, const char* pattern, String* value = nullptr, + const String& defaultVal = String()) { + if(value) + *value = defaultVal; +#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + // offset (normally 3 for "dt-") to skip prefix + if(parseOptionImpl(argc, argv, pattern + strlen(DOCTEST_CONFIG_OPTIONS_PREFIX), value)) + return true; +#endif // DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS + return parseOptionImpl(argc, argv, pattern, value); + } + + // locates a flag on the command line + bool parseFlag(int argc, const char* const* argv, const char* pattern) { + return parseOption(argc, argv, pattern); + } + + // parses a comma separated list of words after a pattern in one of the arguments in argv + bool parseCommaSepArgs(int argc, const char* const* argv, const char* pattern, + std::vector& res) { + String filtersString; + if(parseOption(argc, argv, pattern, &filtersString)) { + // tokenize with "," as a separator, unless escaped with backslash + std::ostringstream s; + auto flush = [&s, &res]() { + auto string = s.str(); + if(string.size() > 0) { + res.push_back(string.c_str()); + } + s.str(""); + }; + + bool seenBackslash = false; + const char* current = filtersString.c_str(); + const char* end = current + strlen(current); + while(current != end) { + char character = *current++; + if(seenBackslash) { + seenBackslash = false; + if(character == ',' || character == '\\') { + s.put(character); + continue; + } + s.put('\\'); + } + if(character == '\\') { + seenBackslash = true; + } else if(character == ',') { + flush(); + } else { + s.put(character); + } + } + + if(seenBackslash) { + s.put('\\'); + } + flush(); + return true; + } + return false; + } + + enum optionType + { + option_bool, + option_int + }; + + // parses an int/bool option from the command line + bool parseIntOption(int argc, const char* const* argv, const char* pattern, optionType type, + int& res) { + String parsedValue; + if(!parseOption(argc, argv, pattern, &parsedValue)) + return false; + + if(type) { + // integer + // TODO: change this to use std::stoi or something else! currently it uses undefined behavior - assumes '0' on failed parse... + int theInt = std::atoi(parsedValue.c_str()); + if (theInt != 0) { + res = theInt; //!OCLINT parameter reassignment + return true; + } + } else { + // boolean + const char positive[][5] = { "1", "true", "on", "yes" }; // 5 - strlen("true") + 1 + const char negative[][6] = { "0", "false", "off", "no" }; // 6 - strlen("false") + 1 + + // if the value matches any of the positive/negative possibilities + for (unsigned i = 0; i < 4; i++) { + if (parsedValue.compare(positive[i], true) == 0) { + res = 1; //!OCLINT parameter reassignment + return true; + } + if (parsedValue.compare(negative[i], true) == 0) { + res = 0; //!OCLINT parameter reassignment + return true; + } + } + } + return false; + } +} // namespace + +Context::Context(int argc, const char* const* argv) + : p(new detail::ContextState) { + parseArgs(argc, argv, true); + if(argc) + p->binary_name = argv[0]; +} + +Context::~Context() { + if(g_cs == p) + g_cs = nullptr; + delete p; +} + +void Context::applyCommandLine(int argc, const char* const* argv) { + parseArgs(argc, argv); + if(argc) + p->binary_name = argv[0]; +} + +// parses args +void Context::parseArgs(int argc, const char* const* argv, bool withDefaults) { + using namespace detail; + + // clang-format off + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file=", p->filters[0]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sf=", p->filters[0]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file-exclude=",p->filters[1]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sfe=", p->filters[1]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite=", p->filters[2]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ts=", p->filters[2]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite-exclude=", p->filters[3]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tse=", p->filters[3]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case=", p->filters[4]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tc=", p->filters[4]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case-exclude=", p->filters[5]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tce=", p->filters[5]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase=", p->filters[6]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sc=", p->filters[6]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase-exclude=", p->filters[7]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sce=", p->filters[7]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "reporters=", p->filters[8]); + parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "r=", p->filters[8]); + // clang-format on + + int intRes = 0; + String strRes; + +#define DOCTEST_PARSE_AS_BOOL_OR_FLAG(name, sname, var, default) \ + if(parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_bool, intRes) || \ + parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_bool, intRes)) \ + p->var = static_cast(intRes); \ + else if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name) || \ + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname)) \ + p->var = true; \ + else if(withDefaults) \ + p->var = default + +#define DOCTEST_PARSE_INT_OPTION(name, sname, var, default) \ + if(parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_int, intRes) || \ + parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_int, intRes)) \ + p->var = intRes; \ + else if(withDefaults) \ + p->var = default + +#define DOCTEST_PARSE_STR_OPTION(name, sname, var, default) \ + if(parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", &strRes, default) || \ + parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", &strRes, default) || \ + withDefaults) \ + p->var = strRes + + // clang-format off + DOCTEST_PARSE_STR_OPTION("out", "o", out, ""); + DOCTEST_PARSE_STR_OPTION("order-by", "ob", order_by, "file"); + DOCTEST_PARSE_INT_OPTION("rand-seed", "rs", rand_seed, 0); + + DOCTEST_PARSE_INT_OPTION("first", "f", first, 0); + DOCTEST_PARSE_INT_OPTION("last", "l", last, UINT_MAX); + + DOCTEST_PARSE_INT_OPTION("abort-after", "aa", abort_after, 0); + DOCTEST_PARSE_INT_OPTION("subcase-filter-levels", "scfl", subcase_filter_levels, INT_MAX); + + DOCTEST_PARSE_AS_BOOL_OR_FLAG("success", "s", success, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("case-sensitive", "cs", case_sensitive, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("exit", "e", exit, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("duration", "d", duration, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("minimal", "m", minimal, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("quiet", "q", quiet, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-throw", "nt", no_throw, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-exitcode", "ne", no_exitcode, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-run", "nr", no_run, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-intro", "ni", no_intro, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-version", "nv", no_version, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-colors", "nc", no_colors, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("force-colors", "fc", force_colors, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-breaks", "nb", no_breaks, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skip", "ns", no_skip, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("gnu-file-line", "gfl", gnu_file_line, !bool(DOCTEST_MSVC)); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-path-filenames", "npf", no_path_in_filenames, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-line-numbers", "nln", no_line_numbers, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-debug-output", "ndo", no_debug_output, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skipped-summary", "nss", no_skipped_summary, false); + DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-time-in-output", "ntio", no_time_in_output, false); + // clang-format on + + if(withDefaults) { + p->help = false; + p->version = false; + p->count = false; + p->list_test_cases = false; + p->list_test_suites = false; + p->list_reporters = false; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "help") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "h") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "?")) { + p->help = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "version") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "v")) { + p->version = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "count") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "c")) { + p->count = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-cases") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ltc")) { + p->list_test_cases = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-suites") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lts")) { + p->list_test_suites = true; + p->exit = true; + } + if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-reporters") || + parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lr")) { + p->list_reporters = true; + p->exit = true; + } +} + +// allows the user to add procedurally to the filters from the command line +void Context::addFilter(const char* filter, const char* value) { setOption(filter, value); } + +// allows the user to clear all filters from the command line +void Context::clearFilters() { + for(auto& curr : p->filters) + curr.clear(); +} + +// allows the user to override procedurally the bool options from the command line +void Context::setOption(const char* option, bool value) { + setOption(option, value ? "true" : "false"); +} + +// allows the user to override procedurally the int options from the command line +void Context::setOption(const char* option, int value) { + setOption(option, toString(value).c_str()); +} + +// allows the user to override procedurally the string options from the command line +void Context::setOption(const char* option, const char* value) { + auto argv = String("-") + option + "=" + value; + auto lvalue = argv.c_str(); + parseArgs(1, &lvalue); +} + +// users should query this in their main() and exit the program if true +bool Context::shouldExit() { return p->exit; } + +void Context::setAsDefaultForAssertsOutOfTestCases() { g_cs = p; } + +void Context::setAssertHandler(detail::assert_handler ah) { p->ah = ah; } + +void Context::setCout(std::ostream* out) { p->cout = out; } + +static class DiscardOStream : public std::ostream +{ +private: + class : public std::streambuf + { + private: + // allowing some buffering decreases the amount of calls to overflow + char buf[1024]; + + protected: + std::streamsize xsputn(const char_type*, std::streamsize count) override { return count; } + + int_type overflow(int_type ch) override { + setp(std::begin(buf), std::end(buf)); + return traits_type::not_eof(ch); + } + } discardBuf; + +public: + DiscardOStream() + : std::ostream(&discardBuf) {} +} discardOut; + +// the main function that does all the filtering and test running +int Context::run() { + using namespace detail; + + // save the old context state in case such was setup - for using asserts out of a testing context + auto old_cs = g_cs; + // this is the current contest + g_cs = p; + is_running_in_test = true; + + g_no_colors = p->no_colors; + p->resetRunData(); + + std::fstream fstr; + if(p->cout == nullptr) { + if(p->quiet) { + p->cout = &discardOut; + } else if(p->out.size()) { + // to a file if specified + fstr.open(p->out.c_str(), std::fstream::out); + p->cout = &fstr; + } else { +#ifndef DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + // stdout by default + p->cout = &std::cout; +#else // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + return EXIT_FAILURE; +#endif // DOCTEST_CONFIG_NO_INCLUDE_IOSTREAM + } + } + + FatalConditionHandler::allocateAltStackMem(); + + auto cleanup_and_return = [&]() { + FatalConditionHandler::freeAltStackMem(); + + if(fstr.is_open()) + fstr.close(); + + // restore context + g_cs = old_cs; + is_running_in_test = false; + + // we have to free the reporters which were allocated when the run started + for(auto& curr : p->reporters_currently_used) + delete curr; + p->reporters_currently_used.clear(); + + if(p->numTestCasesFailed && !p->no_exitcode) + return EXIT_FAILURE; + return EXIT_SUCCESS; + }; + + // setup default reporter if none is given through the command line + if(p->filters[8].empty()) + p->filters[8].push_back("console"); + + // check to see if any of the registered reporters has been selected + for(auto& curr : getReporters()) { + if(matchesAny(curr.first.second.c_str(), p->filters[8], false, p->case_sensitive)) + p->reporters_currently_used.push_back(curr.second(*g_cs)); + } + + // TODO: check if there is nothing in reporters_currently_used + + // prepend all listeners + for(auto& curr : getListeners()) + p->reporters_currently_used.insert(p->reporters_currently_used.begin(), curr.second(*g_cs)); + +#ifdef DOCTEST_PLATFORM_WINDOWS + if(isDebuggerActive() && p->no_debug_output == false) + p->reporters_currently_used.push_back(new DebugOutputWindowReporter(*g_cs)); +#endif // DOCTEST_PLATFORM_WINDOWS + + // handle version, help and no_run + if(p->no_run || p->version || p->help || p->list_reporters) { + DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, QueryData()); + + return cleanup_and_return(); + } + + std::vector testArray; + for(auto& curr : getRegisteredTests()) + testArray.push_back(&curr); + p->numTestCases = testArray.size(); + + // sort the collected records + if(!testArray.empty()) { + if(p->order_by.compare("file", true) == 0) { + std::sort(testArray.begin(), testArray.end(), fileOrderComparator); + } else if(p->order_by.compare("suite", true) == 0) { + std::sort(testArray.begin(), testArray.end(), suiteOrderComparator); + } else if(p->order_by.compare("name", true) == 0) { + std::sort(testArray.begin(), testArray.end(), nameOrderComparator); + } else if(p->order_by.compare("rand", true) == 0) { + std::srand(p->rand_seed); + + // random_shuffle implementation + const auto first = &testArray[0]; + for(size_t i = testArray.size() - 1; i > 0; --i) { + int idxToSwap = std::rand() % (i + 1); + + const auto temp = first[i]; + + first[i] = first[idxToSwap]; + first[idxToSwap] = temp; + } + } else if(p->order_by.compare("none", true) == 0) { + // means no sorting - beneficial for death tests which call into the executable + // with a specific test case in mind - we don't want to slow down the startup times + } + } + + std::set testSuitesPassingFilt; + + bool query_mode = p->count || p->list_test_cases || p->list_test_suites; + std::vector queryResults; + + if(!query_mode) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_start, DOCTEST_EMPTY); + + // invoke the registered functions if they match the filter criteria (or just count them) + for(auto& curr : testArray) { + const auto& tc = *curr; + + bool skip_me = false; + if(tc.m_skip && !p->no_skip) + skip_me = true; + + if(!matchesAny(tc.m_file.c_str(), p->filters[0], true, p->case_sensitive)) + skip_me = true; + if(matchesAny(tc.m_file.c_str(), p->filters[1], false, p->case_sensitive)) + skip_me = true; + if(!matchesAny(tc.m_test_suite, p->filters[2], true, p->case_sensitive)) + skip_me = true; + if(matchesAny(tc.m_test_suite, p->filters[3], false, p->case_sensitive)) + skip_me = true; + if(!matchesAny(tc.m_name, p->filters[4], true, p->case_sensitive)) + skip_me = true; + if(matchesAny(tc.m_name, p->filters[5], false, p->case_sensitive)) + skip_me = true; + + if(!skip_me) + p->numTestCasesPassingFilters++; + + // skip the test if it is not in the execution range + if((p->last < p->numTestCasesPassingFilters && p->first <= p->last) || + (p->first > p->numTestCasesPassingFilters)) + skip_me = true; + + if(skip_me) { + if(!query_mode) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_skipped, tc); + continue; + } + + // do not execute the test if we are to only count the number of filter passing tests + if(p->count) + continue; + + // print the name of the test and don't execute it + if(p->list_test_cases) { + queryResults.push_back(&tc); + continue; + } + + // print the name of the test suite if not done already and don't execute it + if(p->list_test_suites) { + if((testSuitesPassingFilt.count(tc.m_test_suite) == 0) && tc.m_test_suite[0] != '\0') { + queryResults.push_back(&tc); + testSuitesPassingFilt.insert(tc.m_test_suite); + p->numTestSuitesPassingFilters++; + } + continue; + } + + // execute the test if it passes all the filtering + { + p->currentTest = &tc; + + p->failure_flags = TestCaseFailureReason::None; + p->seconds = 0; + + // reset atomic counters + p->numAssertsFailedCurrentTest_atomic = 0; + p->numAssertsCurrentTest_atomic = 0; + + p->fullyTraversedSubcases.clear(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_start, tc); + + p->timer.start(); + + bool run_test = true; + + do { + // reset some of the fields for subcases (except for the set of fully passed ones) + p->reachedLeaf = false; + // May not be empty if previous subcase exited via exception. + p->subcaseStack.clear(); + p->currentSubcaseDepth = 0; + + p->shouldLogCurrentException = true; + + // reset stuff for logging with INFO() + p->stringifiedContexts.clear(); + +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + try { +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS +// MSVC 2015 diagnoses fatalConditionHandler as unused (because reset() is a static method) +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4101) // unreferenced local variable + FatalConditionHandler fatalConditionHandler; // Handle signals + // execute the test + tc.m_test(); + fatalConditionHandler.reset(); +DOCTEST_MSVC_SUPPRESS_WARNING_POP +#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS + } catch(const TestFailureException&) { + p->failure_flags |= TestCaseFailureReason::AssertFailure; + } catch(...) { + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception, + {translateActiveException(), false}); + p->failure_flags |= TestCaseFailureReason::Exception; + } +#endif // DOCTEST_CONFIG_NO_EXCEPTIONS + + // exit this loop if enough assertions have failed - even if there are more subcases + if(p->abort_after > 0 && + p->numAssertsFailed + p->numAssertsFailedCurrentTest_atomic >= p->abort_after) { + run_test = false; + p->failure_flags |= TestCaseFailureReason::TooManyFailedAsserts; + } + + if(!p->nextSubcaseStack.empty() && run_test) + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_reenter, tc); + if(p->nextSubcaseStack.empty()) + run_test = false; + } while(run_test); + + p->finalizeTestCaseData(); + + DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs); + + p->currentTest = nullptr; + + // stop executing tests if enough assertions have failed + if(p->abort_after > 0 && p->numAssertsFailed >= p->abort_after) + break; + } + } + + if(!query_mode) { + DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs); + } else { + QueryData qdata; + qdata.run_stats = g_cs; + qdata.data = queryResults.data(); + qdata.num_data = unsigned(queryResults.size()); + DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, qdata); + } + + return cleanup_and_return(); +} + +DOCTEST_DEFINE_INTERFACE(IReporter) + +int IReporter::get_num_active_contexts() { return detail::g_infoContexts.size(); } +const IContextScope* const* IReporter::get_active_contexts() { + return get_num_active_contexts() ? &detail::g_infoContexts[0] : nullptr; +} + +int IReporter::get_num_stringified_contexts() { return detail::g_cs->stringifiedContexts.size(); } +const String* IReporter::get_stringified_contexts() { + return get_num_stringified_contexts() ? &detail::g_cs->stringifiedContexts[0] : nullptr; +} + +namespace detail { + void registerReporterImpl(const char* name, int priority, reporterCreatorFunc c, bool isReporter) { + if(isReporter) + getReporters().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c)); + else + getListeners().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c)); + } +} // namespace detail + +} // namespace doctest + +#endif // DOCTEST_CONFIG_DISABLE + +#ifdef DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4007) // 'function' : must be 'attribute' - see issue #182 +int main(int argc, char** argv) { return doctest::Context(argc, argv).run(); } +DOCTEST_MSVC_SUPPRESS_WARNING_POP +#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN + +DOCTEST_CLANG_SUPPRESS_WARNING_POP +DOCTEST_MSVC_SUPPRESS_WARNING_POP +DOCTEST_GCC_SUPPRESS_WARNING_POP + +DOCTEST_SUPPRESS_COMMON_WARNINGS_POP + +#endif // DOCTEST_LIBRARY_IMPLEMENTATION +#endif // DOCTEST_CONFIG_IMPLEMENT + +#ifdef DOCTEST_UNDEF_WIN32_LEAN_AND_MEAN +#undef WIN32_LEAN_AND_MEAN +#undef DOCTEST_UNDEF_WIN32_LEAN_AND_MEAN +#endif // DOCTEST_UNDEF_WIN32_LEAN_AND_MEAN + +#ifdef DOCTEST_UNDEF_NOMINMAX +#undef NOMINMAX +#undef DOCTEST_UNDEF_NOMINMAX +#endif // DOCTEST_UNDEF_NOMINMAX From 9e464bd6964fa3bbaeaf4cbc2ec486b8aae9dcf9 Mon Sep 17 00:00:00 2001 From: iuri dotta Date: Sun, 1 Mar 2026 00:16:57 -0300 Subject: [PATCH 02/11] Remove unnecessary includes from multiple header files for cleaner code --- include/edfio/processor/impl/ProcessorTimeStamp.ipp | 2 -- include/edfio/reader/impl/ReaderHeaderExam.ipp | 1 - include/edfio/reader/impl/ReaderHeaderGeneral.ipp | 1 - include/edfio/reader/impl/ReaderHeaderSignal.ipp | 1 - include/edfio/sink/RecordSink.hpp | 1 - include/edfio/store/RecordStore.hpp | 1 - include/edfio/writer/impl/WriterHeaderExam.ipp | 2 -- include/edfio/writer/impl/WriterHeaderGeneral.ipp | 2 -- include/edfio/writer/impl/WriterHeaderSignals.ipp | 1 - 9 files changed, 12 deletions(-) diff --git a/include/edfio/processor/impl/ProcessorTimeStamp.ipp b/include/edfio/processor/impl/ProcessorTimeStamp.ipp index b0893cb..6487b44 100644 --- a/include/edfio/processor/impl/ProcessorTimeStamp.ipp +++ b/include/edfio/processor/impl/ProcessorTimeStamp.ipp @@ -14,8 +14,6 @@ #include "../../core/Annotation.hpp" #include "../detail/ProcessorUtils.hpp" -#include - namespace edfio { diff --git a/include/edfio/reader/impl/ReaderHeaderExam.ipp b/include/edfio/reader/impl/ReaderHeaderExam.ipp index 2e8e29d..c4b75fd 100644 --- a/include/edfio/reader/impl/ReaderHeaderExam.ipp +++ b/include/edfio/reader/impl/ReaderHeaderExam.ipp @@ -18,7 +18,6 @@ #include "../../processor/ProcessorHeaderExam.hpp" #include -#include namespace edfio { diff --git a/include/edfio/reader/impl/ReaderHeaderGeneral.ipp b/include/edfio/reader/impl/ReaderHeaderGeneral.ipp index a27558d..5d47a7a 100644 --- a/include/edfio/reader/impl/ReaderHeaderGeneral.ipp +++ b/include/edfio/reader/impl/ReaderHeaderGeneral.ipp @@ -13,7 +13,6 @@ #include "../../header/HeaderGeneral.hpp" #include -#include namespace edfio { diff --git a/include/edfio/reader/impl/ReaderHeaderSignal.ipp b/include/edfio/reader/impl/ReaderHeaderSignal.ipp index 768e84b..91b3189 100644 --- a/include/edfio/reader/impl/ReaderHeaderSignal.ipp +++ b/include/edfio/reader/impl/ReaderHeaderSignal.ipp @@ -15,7 +15,6 @@ #include #include -#include namespace edfio { diff --git a/include/edfio/sink/RecordSink.hpp b/include/edfio/sink/RecordSink.hpp index 052e584..3863d5a 100644 --- a/include/edfio/sink/RecordSink.hpp +++ b/include/edfio/sink/RecordSink.hpp @@ -12,7 +12,6 @@ #include "Sink.hpp" #include "../core/Record.hpp" -#include #include #include #include diff --git a/include/edfio/store/RecordStore.hpp b/include/edfio/store/RecordStore.hpp index 9e2e492..13d0673 100644 --- a/include/edfio/store/RecordStore.hpp +++ b/include/edfio/store/RecordStore.hpp @@ -12,7 +12,6 @@ #include "Store.hpp" #include "../core/Record.hpp" -#include #include #include #include diff --git a/include/edfio/writer/impl/WriterHeaderExam.ipp b/include/edfio/writer/impl/WriterHeaderExam.ipp index 5c5c41b..de3f215 100644 --- a/include/edfio/writer/impl/WriterHeaderExam.ipp +++ b/include/edfio/writer/impl/WriterHeaderExam.ipp @@ -16,8 +16,6 @@ #include "../../processor/ProcessorHeaderGeneral.hpp" #include "../../processor/ProcessorHeaderSignal.hpp" -#include -#include namespace edfio { diff --git a/include/edfio/writer/impl/WriterHeaderGeneral.ipp b/include/edfio/writer/impl/WriterHeaderGeneral.ipp index e6cf556..a97ef45 100644 --- a/include/edfio/writer/impl/WriterHeaderGeneral.ipp +++ b/include/edfio/writer/impl/WriterHeaderGeneral.ipp @@ -12,9 +12,7 @@ #include "../../Utils.hpp" #include "../../header/HeaderGeneral.hpp" -#include #include -#include namespace edfio { diff --git a/include/edfio/writer/impl/WriterHeaderSignals.ipp b/include/edfio/writer/impl/WriterHeaderSignals.ipp index f60ef5b..334e243 100644 --- a/include/edfio/writer/impl/WriterHeaderSignals.ipp +++ b/include/edfio/writer/impl/WriterHeaderSignals.ipp @@ -14,7 +14,6 @@ #include #include -#include namespace edfio { From 3198fbe406419624b1fd9055dc59ba2221c4d2d7 Mon Sep 17 00:00:00 2001 From: iuri dotta Date: Sun, 1 Mar 2026 00:30:35 -0300 Subject: [PATCH 03/11] Refactor: Remove deprecated processor implementations and streamline header reading/writing - Deleted ProcessorHeaderSignalFields, ProcessorSample, ProcessorSampleRecord, ProcessorTalRecord, ProcessorTimeStamp, and ProcessorTimeStampRecord implementations to clean up the codebase. - Updated ReaderHeaderExam, ReaderHeaderGeneral, and ReaderHeaderSignal to directly implement reading logic without separate implementation files. - Enhanced WriterHeaderExam, WriterHeaderGeneral, and WriterHeaderSignals to include error handling and direct writing logic. - Improved overall structure and readability of header processing by consolidating related functionalities. --- .../edfio/processor/ProcessorAnnotation.hpp | 55 ++- .../edfio/processor/ProcessorHeaderExam.hpp | 34 +- .../processor/ProcessorHeaderGeneral.hpp | 140 +++++++- .../ProcessorHeaderGeneralFields.hpp | 303 ++++++++++++++++- .../edfio/processor/ProcessorHeaderSignal.hpp | 88 ++++- .../processor/ProcessorHeaderSignalFields.hpp | 271 ++++++++++++++- include/edfio/processor/ProcessorSample.hpp | 23 +- .../edfio/processor/ProcessorSampleRecord.hpp | 30 +- .../edfio/processor/ProcessorTalRecord.hpp | 68 +++- .../edfio/processor/ProcessorTimeStamp.hpp | 34 +- .../processor/ProcessorTimeStampRecord.hpp | 45 ++- .../processor/impl/ProcessorAnnotation.ipp | 69 ---- .../processor/impl/ProcessorHeaderExam.ipp | 48 --- .../processor/impl/ProcessorHeaderGeneral.ipp | 154 --------- .../impl/ProcessorHeaderGeneralFields.ipp | 317 ------------------ .../processor/impl/ProcessorHeaderSignal.ipp | 101 ------ .../impl/ProcessorHeaderSignalFields.ipp | 286 ---------------- .../edfio/processor/impl/ProcessorSample.ipp | 37 -- .../processor/impl/ProcessorSampleRecord.ipp | 43 --- .../processor/impl/ProcessorTalRecord.ipp | 85 ----- .../processor/impl/ProcessorTimeStamp.ipp | 48 --- .../impl/ProcessorTimeStampRecord.ipp | 59 ---- include/edfio/reader/ReaderHeaderExam.hpp | 49 ++- include/edfio/reader/ReaderHeaderGeneral.hpp | 34 +- include/edfio/reader/ReaderHeaderSignal.hpp | 45 ++- .../edfio/reader/impl/ReaderHeaderExam.ipp | 64 ---- .../edfio/reader/impl/ReaderHeaderGeneral.ipp | 49 --- .../edfio/reader/impl/ReaderHeaderSignal.ipp | 61 ---- include/edfio/writer/WriterHeaderExam.hpp | 24 +- include/edfio/writer/WriterHeaderGeneral.hpp | 34 +- include/edfio/writer/WriterHeaderSignals.hpp | 40 ++- include/edfio/writer/WriterRecord.hpp | 22 +- .../edfio/writer/impl/WriterHeaderExam.ipp | 38 --- .../edfio/writer/impl/WriterHeaderGeneral.ipp | 48 --- .../edfio/writer/impl/WriterHeaderSignals.ipp | 57 ---- include/edfio/writer/impl/WriterRecord.ipp | 35 -- 36 files changed, 1291 insertions(+), 1647 deletions(-) delete mode 100644 include/edfio/processor/impl/ProcessorAnnotation.ipp delete mode 100644 include/edfio/processor/impl/ProcessorHeaderExam.ipp delete mode 100644 include/edfio/processor/impl/ProcessorHeaderGeneral.ipp delete mode 100644 include/edfio/processor/impl/ProcessorHeaderGeneralFields.ipp delete mode 100644 include/edfio/processor/impl/ProcessorHeaderSignal.ipp delete mode 100644 include/edfio/processor/impl/ProcessorHeaderSignalFields.ipp delete mode 100644 include/edfio/processor/impl/ProcessorSample.ipp delete mode 100644 include/edfio/processor/impl/ProcessorSampleRecord.ipp delete mode 100644 include/edfio/processor/impl/ProcessorTalRecord.ipp delete mode 100644 include/edfio/processor/impl/ProcessorTimeStamp.ipp delete mode 100644 include/edfio/processor/impl/ProcessorTimeStampRecord.ipp delete mode 100644 include/edfio/reader/impl/ReaderHeaderExam.ipp delete mode 100644 include/edfio/reader/impl/ReaderHeaderGeneral.ipp delete mode 100644 include/edfio/reader/impl/ReaderHeaderSignal.ipp delete mode 100644 include/edfio/writer/impl/WriterHeaderExam.ipp delete mode 100644 include/edfio/writer/impl/WriterHeaderGeneral.ipp delete mode 100644 include/edfio/writer/impl/WriterHeaderSignals.ipp delete mode 100644 include/edfio/writer/impl/WriterRecord.ipp diff --git a/include/edfio/processor/ProcessorAnnotation.hpp b/include/edfio/processor/ProcessorAnnotation.hpp index 4b72101..62554c8 100644 --- a/include/edfio/processor/ProcessorAnnotation.hpp +++ b/include/edfio/processor/ProcessorAnnotation.hpp @@ -9,8 +9,10 @@ #pragma once -#include "../core/Record.hpp" +#include "../Utils.hpp" #include "../core/Annotation.hpp" +#include "../core/Record.hpp" +#include "detail/ProcessorUtils.hpp" namespace edfio { @@ -20,6 +22,53 @@ namespace edfio Record operator ()(Annotation annotation); }; -} + inline Record ProcessorAnnotation::operator()(Annotation annotation) + { + if (annotation.m_annotation.empty()) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileWriteInvalidAnnotations)); + } + + std::string timestamp = (annotation.m_start >= 0 ? "+" : "") + detail::to_string_decimal(annotation.m_start); + std::string duration; + if (annotation.m_duration > 0) + duration = detail::to_string_decimal(annotation.m_duration); + + size_t size = timestamp.size(); // timestamp + if (!duration.empty()) + { + size++; // 21 div + size += duration.size(); // duration + } + + size++; // 20 div + size += annotation.m_annotation.size(); // annotation + size += 2; // 20 div and 0 -#include "impl/ProcessorAnnotation.ipp" + Record record(size); + auto it = record().begin(); + + // timestamp + std::move(timestamp.begin(), timestamp.end(), it); + it += timestamp.size(); + + if (!duration.empty()) + { + *it++ = 21; // 21 div + // duration + std::move(duration.begin(), duration.end(), it); + it += duration.size(); + } + + *it++ = 20; // 20 div + // annotation + std::move(annotation.m_annotation.begin(), annotation.m_annotation.end(), it); + it += annotation.m_annotation.size(); + + *it++ = 20; // 20 div + *it++ = 0; // 0 end + + return record; + } + +} diff --git a/include/edfio/processor/ProcessorHeaderExam.hpp b/include/edfio/processor/ProcessorHeaderExam.hpp index e4aa4a1..a0cde5f 100644 --- a/include/edfio/processor/ProcessorHeaderExam.hpp +++ b/include/edfio/processor/ProcessorHeaderExam.hpp @@ -9,6 +9,8 @@ #pragma once +#include "../Utils.hpp" +#include "../core/DataFormat.hpp" #include "../header/HeaderExam.hpp" namespace edfio @@ -19,6 +21,34 @@ namespace edfio HeaderExam operator ()(HeaderGeneral header, std::vector signals); }; -} + inline HeaderExam edfio::ProcessorHeaderExam::operator()(HeaderGeneral header, std::vector signals) + { + // Record size + size_t recordsize = 0; + for (auto &signal : signals) + { + recordsize += signal.m_samplesInDataRecord; + } + + if (IsBdf(header.m_version)) + { + recordsize *= 3; + if (recordsize > 0xF00000) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + } + else + { + recordsize *= 2; + if (recordsize > 0xA00000) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + } + header.m_detail.m_recordSize = recordsize; -#include "impl/ProcessorHeaderExam.ipp" + return HeaderExam{ std::move(header), std::move(signals) }; + } + +} diff --git a/include/edfio/processor/ProcessorHeaderGeneral.hpp b/include/edfio/processor/ProcessorHeaderGeneral.hpp index 9b723c5..2f5e78f 100644 --- a/include/edfio/processor/ProcessorHeaderGeneral.hpp +++ b/include/edfio/processor/ProcessorHeaderGeneral.hpp @@ -9,7 +9,13 @@ #pragma once +#include "../Defs.hpp" +#include "../core/DataFormat.hpp" #include "../header/HeaderGeneral.hpp" +#include "detail/ProcessorUtils.hpp" + +#include +#include namespace edfio { @@ -19,6 +25,136 @@ namespace edfio HeaderGeneralFields operator ()(HeaderGeneral in); }; -} + inline HeaderGeneralFields ProcessorHeaderGeneral::operator()(HeaderGeneral in) + { + HeaderGeneralFields out; -#include "impl/ProcessorHeaderGeneral.ipp" + // Version + { + if (IsEdf(in.m_version)) + { + out.m_version("0 "); + } + else if (IsBdf(in.m_version)) + { + std::string value = "BIOSEMI"; + value.insert(value.begin(), -1); + out.m_version(value); + } + } + // Patient Name + { + out.m_patient(in.m_patient); + } + // Recording + { + out.m_recording(in.m_recording); + } + // Start Date + { + int day = std::get<0>(in.m_startDate); + int month = std::get<1>(in.m_startDate); + int year = std::get<2>(in.m_startDate); + year -= year > 1999 ? 2000 : 1900; + out.m_startDate(std::format("{:02d}.{:02d}.{:02d}", day, month, year)); + } + // Start Time + { + int hour = std::get<0>(in.m_startTime); + int minute = std::get<1>(in.m_startTime); + int second = std::get<2>(in.m_startTime); + out.m_startTime(std::format("{:02d}.{:02d}.{:02d}", hour, minute, second)); + } + // Header Size + { + out.m_headerSize(std::to_string(in.m_headerSize)); + } + // Reserved + { + if (!in.m_reserved.empty()) + { + out.m_reserved(in.m_reserved); + } + else if (IsPlus(in.m_version)) + { + out.m_reserved(std::string(detail::GetFormatName(in.m_version))); + } + } + // Datarecords in File + { + out.m_datarecordsFile(std::to_string(in.m_datarecordsFile)); + } + // Datarecord Duration + { + out.m_datarecordDuration(detail::to_string_decimal(in.m_datarecordDuration)); + } + // Number of signals + { + out.m_totalSignals(std::to_string(in.m_totalSignals)); + } + // Plus Fields + if (IsPlus(in.m_version)) + { + // Patient Name + { + std::vector fields; + // Code + fields.push_back(in.m_detail.m_patientCode.empty() ? "X" : in.m_detail.m_patientCode); + // Sex + fields.push_back(in.m_detail.m_gender.empty() ? "X" : in.m_detail.m_gender); + // Birthdate in dd-MMM-yyyy format using the English 3-character abbreviations of the month in capitals. 02-AUG-1951 is OK, while 2-AUG-1951 is not. + fields.push_back(in.m_detail.m_birthdate.empty() ? "X" : in.m_detail.m_birthdate); + // The patients name. + fields.push_back(in.m_detail.m_patientName.empty() ? "X" : in.m_detail.m_patientName); + // Additional information. + std::string info; + std::istringstream f(in.m_detail.m_patientAdditional); + while (std::getline(f, info, detail::ADDITIONAL_SEPARATOR)) + { + fields.push_back(info); + } + std::string patient = ""; + for (auto& field : fields) + { + std::replace(field.begin(), field.end(), ' ', '_'); // replace all ' ' to '_' + patient += field + " "; + } + patient.pop_back(); // remove last " " + out.m_patient(patient); + } + // Recording + { + std::vector fields; + // The text 'Startdate' + fields.push_back("Startdate"); + // The startdate itself in dd-MMM-yyyy format using the English 3-character abbreviations of the month in capitals: dd-MMM-yyyy (MMM = 'JAN' | 'FEV' | ...) + int day = std::get<0>(in.m_startDate); + int month = std::get<1>(in.m_startDate); + int year = std::get<2>(in.m_startDate); + fields.push_back(std::format("{:02d}-{}-{}", day ? day : 1, detail::GetStringFromMonth(month), year ? year : 1984)); + // The hospital administration code of the investigation, i.e. EEG number or PSG number. + fields.push_back(in.m_detail.m_admincode.empty() ? "X" : in.m_detail.m_admincode); + // A code specifying the responsible investigator or technician. + fields.push_back(in.m_detail.m_technician.empty() ? "X" : in.m_detail.m_technician); + // A code specifying the used equipment. + fields.push_back(in.m_detail.m_equipment.empty() ? "X" : in.m_detail.m_equipment); + // Additional information. + std::string info; + std::istringstream f(in.m_detail.m_recordingAdditional); + while (std::getline(f, info, detail::ADDITIONAL_SEPARATOR)) + { + fields.push_back(info); + } + std::string recording = ""; + for (auto& field : fields) + { + std::replace(field.begin(), field.end(), ' ', '_'); // replace all '_' to ' ' + recording += field + " "; + } + recording.pop_back(); // remove last " " + out.m_recording(recording); + } + } + return out; + } +} diff --git a/include/edfio/processor/ProcessorHeaderGeneralFields.hpp b/include/edfio/processor/ProcessorHeaderGeneralFields.hpp index 7728faf..5f738de 100644 --- a/include/edfio/processor/ProcessorHeaderGeneralFields.hpp +++ b/include/edfio/processor/ProcessorHeaderGeneralFields.hpp @@ -9,7 +9,12 @@ #pragma once +#include "../Utils.hpp" +#include "../core/DataFormat.hpp" #include "../header/HeaderGeneral.hpp" +#include "detail/ProcessorUtils.hpp" + +#include namespace edfio { @@ -19,6 +24,300 @@ namespace edfio HeaderGeneral operator ()(HeaderGeneralFields in); }; -} + inline HeaderGeneral ProcessorHeaderGeneralFields::operator()(HeaderGeneralFields in) + { + HeaderGeneral out; + + if (/*detail::CheckFormatErrors(in.m_version()) + ||*/ detail::CheckFormatErrors(in.m_patient()) + || detail::CheckFormatErrors(in.m_recording()) + || detail::CheckFormatErrors(in.m_startDate()) + || detail::CheckFormatErrors(in.m_startTime()) + || detail::CheckFormatErrors(in.m_headerSize()) + || detail::CheckFormatErrors(in.m_reserved()) + || detail::CheckFormatErrors(in.m_datarecordsFile()) + || detail::CheckFormatErrors(in.m_datarecordDuration()) + || detail::CheckFormatErrors(in.m_totalSignals())) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + + + // Version + { + auto &version = in.m_version(); + if (version.front() == -1) // BDF-file + { + if (version.substr(1) != "BIOSEMI") + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + out.m_version = DataFormat::Bdf; + } + else // EDF-file + { + if (version != "0 ") + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + out.m_version = DataFormat::Edf; + } + } + // Patient Name + { + out.m_patient = in.m_patient(); + } + // Recording + { + out.m_recording = in.m_recording(); + } + // Start Date + { + auto& startdate = in.m_startDate(); + if ((startdate[2] != '.') || (startdate[5] != '.') + || !std::isdigit(startdate[0]) || !std::isdigit(startdate[1]) + || !std::isdigit(startdate[3]) || !std::isdigit(startdate[4]) + || !std::isdigit(startdate[6]) || !std::isdigit(startdate[7])) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + { + int day{}, month{}, year{}; + auto [p1, e1] = std::from_chars(startdate.data(), startdate.data() + 2, day); + auto [p2, e2] = std::from_chars(startdate.data() + 3, startdate.data() + 5, month); + auto [p3, e3] = std::from_chars(startdate.data() + 6, startdate.data() + 8, year); + if (e1 != std::errc{} || e2 != std::errc{} || e3 != std::errc{} + || day < 1 || day > 31 || month < 1 || month > 12) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + year += year > 84 ? 1900 : 2000; + out.m_startDate = std::make_tuple(day, month, year); + } + } + // Start Time + { + auto& starttime = in.m_startTime(); + if ((starttime[2] != '.') || (starttime[5] != '.') + || !std::isdigit(starttime[0]) || !std::isdigit(starttime[1]) + || !std::isdigit(starttime[3]) || !std::isdigit(starttime[4]) + || !std::isdigit(starttime[6]) || !std::isdigit(starttime[7])) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + { + int hour{}, minute{}, second{}; + auto [p1, e1] = std::from_chars(starttime.data(), starttime.data() + 2, hour); + auto [p2, e2] = std::from_chars(starttime.data() + 3, starttime.data() + 5, minute); + auto [p3, e3] = std::from_chars(starttime.data() + 6, starttime.data() + 8, second); + if (e1 != std::errc{} || e2 != std::errc{} || e3 != std::errc{} + || hour > 23 || minute > 59 || second > 59) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + out.m_startTime = std::make_tuple(hour, minute, second); + } + } + // Header Size + { + out.m_headerSize = detail::ParseInt(in.m_headerSize(), detail::GetError(FileErrc::FileContainsFormatErrors)); + } + // Reserved + { + auto &reserved = in.m_reserved(); + if (IsEdf(out.m_version)) + { + if (reserved.find("EDF+C") != std::string::npos) + { + out.m_version = DataFormat::EdfPlusC; + } + else if (reserved.find("EDF+D") != std::string::npos) + { + out.m_version = DataFormat::EdfPlusD; + } + } + else if (IsBdf(out.m_version)) + { + if (reserved.find("BDF+C") != std::string::npos) + { + out.m_version = DataFormat::BdfPlusC; + } + else if (reserved.find("BDF+D") != std::string::npos) + { + out.m_version = DataFormat::BdfPlusD; + } + } + } + // Datarecords in File + { + out.m_datarecordsFile = detail::ParseLongLong(in.m_datarecordsFile(), detail::GetError(FileErrc::FileContainsFormatErrors)); + if (out.m_datarecordsFile < 0) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + } + // Datarecord Duration + { + double duration = detail::ParseDouble(in.m_datarecordDuration(), detail::GetError(FileErrc::FileContainsFormatErrors)); + out.m_datarecordDuration = duration; + if (duration < 0) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + out.m_detail.m_fileDuration = out.m_datarecordDuration * out.m_datarecordsFile; + } + // Number of signals + { + int signals = detail::ParseInt(in.m_totalSignals(), detail::GetError(FileErrc::FileContainsFormatErrors)); + if (signals <= 0 || (signals * 256 + 256) != out.m_headerSize) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + out.m_totalSignals = signals; + } + // Plus Fields + if (IsPlus(out.m_version)) + { + // Patient Name + { + auto &details = out.m_detail; + std::vector fields; + std::string field; + std::istringstream f(out.m_patient); + while (std::getline(f, field, ' ')) + { + fields.push_back(field); + } + if (fields.size() < 4) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + + details.m_patientAdditional.clear(); -#include "impl/ProcessorHeaderGeneralFields.ipp" + for (size_t i = 0; i < fields.size(); i++) + { + auto& str = fields[i]; + std::replace(str.begin(), str.end(), '_', ' '); // replace all '_' to ' ' + switch (i) + { + case 0: // The code by which the patient is known in the hospital administration. + details.m_patientCode = str; + break; + case 1: // Sex + if (str == "F" || str == "M" || str == "X") + { + details.m_gender = str; + } + else + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + break; + case 2: // Birthdate in dd-MMM-yyyy format using the English 3-character abbreviations of the month in capitals. 02-AUG-1951 is OK, while 2-AUG-1951 is not. + details.m_birthdate = str; + break; + case 3: // The patients name. + details.m_patientName = str; + break; + default: // Additional information. + if (!details.m_patientAdditional.empty()) + { + details.m_patientAdditional += detail::ADDITIONAL_SEPARATOR; + } + details.m_patientAdditional.append(str); + break; + } + } + } + // Recording + { + auto &details = out.m_detail; + std::vector fields; + std::string field; + std::istringstream f(out.m_recording); + while (std::getline(f, field, ' ')) + { + fields.push_back(field); + } + + if (fields.size() < 5) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + + details.m_recordingAdditional.clear(); + + for (size_t i = 0; i < fields.size(); i++) + { + auto& str = fields[i]; + std::replace(str.begin(), str.end(), '_', ' '); // replace all '_' to ' ' + if (str != "X") + { + switch (i) + { + case 0: // The text 'Startdate'. + if (str != "Startdate") + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + break; + case 1: // The startdate itself in dd-MMM-yyyy format using the English 3-character abbreviations of the month in capitals: dd-MMM-yyyy (MMM = 'JAN' | 'FEV' | ...) + if (str.size() == 11 && str[2] == '-' && str[6] == '-') + { + { + int day{}, year{}; + auto [p1, e1] = std::from_chars(str.data(), str.data() + 2, day); + auto [p2, e2] = std::from_chars(str.data() + 7, str.data() + 11, year); + int month = detail::GetMonthFromString(std::string_view{str}.substr(3, 3)); + if (e1 != std::errc{} || e2 != std::errc{} || month == 0) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + out.m_startDate = std::make_tuple(day, month, year); + } + } + else + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + break; + case 2: // The hospital administration code of the investigation, i.e. EEG number or PSG number. + details.m_admincode = str; + break; + case 3: // A code specifying the responsible investigator or technician. + details.m_technician = str; + break; + case 4: // A code specifying the used equipment. + details.m_equipment = str; + break; + default: // Additional information. + if (!details.m_recordingAdditional.empty()) + { + details.m_recordingAdditional += detail::ADDITIONAL_SEPARATOR; + } + details.m_recordingAdditional.append(str); + break; + } + } + } + } + } + + out.m_patient = detail::ReduceString(out.m_patient); + out.m_recording = detail::ReduceString(out.m_recording); + out.m_reserved = detail::ReduceString(out.m_reserved); + out.m_detail.m_patientCode = detail::ReduceString(out.m_detail.m_patientCode); + out.m_detail.m_gender = detail::ReduceString(out.m_detail.m_gender); + out.m_detail.m_birthdate = detail::ReduceString(out.m_detail.m_birthdate); + out.m_detail.m_patientName = detail::ReduceString(out.m_detail.m_patientName); + out.m_detail.m_patientAdditional = detail::ReduceString(out.m_detail.m_patientAdditional); + out.m_detail.m_admincode = detail::ReduceString(out.m_detail.m_admincode); + out.m_detail.m_technician = detail::ReduceString(out.m_detail.m_technician); + out.m_detail.m_equipment = detail::ReduceString(out.m_detail.m_equipment); + out.m_detail.m_recordingAdditional = detail::ReduceString(out.m_detail.m_recordingAdditional); + + return out; + } + +} diff --git a/include/edfio/processor/ProcessorHeaderSignal.hpp b/include/edfio/processor/ProcessorHeaderSignal.hpp index b6cf110..61e4d96 100644 --- a/include/edfio/processor/ProcessorHeaderSignal.hpp +++ b/include/edfio/processor/ProcessorHeaderSignal.hpp @@ -9,16 +9,90 @@ #pragma once +#include "../Utils.hpp" +#include "../core/DataFormat.hpp" #include "../header/HeaderSignal.hpp" +#include "detail/ProcessorUtils.hpp" -namespace edfio -{ +#include +#include - struct ProcessorHeaderSignal - { - std::vector operator ()(std::vector in); - }; +namespace edfio { +struct ProcessorHeaderSignal { + std::vector operator()(std::vector in); +}; + +inline std::vector +ProcessorHeaderSignal::operator()(std::vector in) { + std::vector out(in.size()); + auto &signals = in; + + // Labels + { + for (size_t idx = 0; idx < signals.size(); idx++) { + out[idx].m_label(signals[idx].m_label); + } + } + // Transducers Types + { + for (size_t idx = 0; idx < signals.size(); idx++) { + out[idx].m_transducer(signals[idx].m_transducer); + } + } + // Physical Dimensions + { + for (size_t idx = 0; idx < signals.size(); idx++) { + out[idx].m_physDimension(signals[idx].m_physDimension); + } + } + // Physical Minima + { + for (size_t idx = 0; idx < signals.size(); idx++) { + out[idx].m_physicalMin( + detail::to_string_decimal(signals[idx].m_physicalMin)); + } + } + // Physical Maxima + { + for (size_t idx = 0; idx < signals.size(); idx++) { + out[idx].m_physicalMax( + detail::to_string_decimal(signals[idx].m_physicalMax)); + } + } + // Digital Minima + { + for (size_t idx = 0; idx < signals.size(); idx++) { + out[idx].m_digitalMin(std::to_string(signals[idx].m_digitalMin)); + } + } + // Digital Maxima + { + for (size_t idx = 0; idx < signals.size(); idx++) { + out[idx].m_digitalMax(std::to_string(signals[idx].m_digitalMax)); + } + } + // Prefilter + { + for (size_t idx = 0; idx < signals.size(); idx++) { + out[idx].m_prefilter(signals[idx].m_prefilter); + } + } + // Samples in each datarecord + { + for (size_t idx = 0; idx < signals.size(); idx++) { + out[idx].m_samplesInDataRecord( + std::to_string(signals[idx].m_samplesInDataRecord)); + } + } + // Reserved + { + for (size_t idx = 0; idx < signals.size(); idx++) { + out[idx].m_reserved(signals[idx].m_reserved); + } + } + + return out; } -#include "impl/ProcessorHeaderSignal.ipp" +} // namespace edfio diff --git a/include/edfio/processor/ProcessorHeaderSignalFields.hpp b/include/edfio/processor/ProcessorHeaderSignalFields.hpp index 5bb6859..3b6495a 100644 --- a/include/edfio/processor/ProcessorHeaderSignalFields.hpp +++ b/include/edfio/processor/ProcessorHeaderSignalFields.hpp @@ -9,8 +9,10 @@ #pragma once +#include "../Utils.hpp" #include "../core/DataFormat.hpp" #include "../header/HeaderSignal.hpp" +#include "detail/ProcessorUtils.hpp" namespace edfio { @@ -27,6 +29,271 @@ namespace edfio double m_datarecordDuration; }; -} + inline std::vector ProcessorHeaderSignalFields::operator()(std::vector in) + { + std::vector signals(in.size()); + + for (auto& sigFields : in) + { + if (detail::CheckFormatErrors(sigFields.m_label()) + || detail::CheckFormatErrors(sigFields.m_transducer()) + || detail::CheckFormatErrors(sigFields.m_physDimension()) + || detail::CheckFormatErrors(sigFields.m_physicalMin()) + || detail::CheckFormatErrors(sigFields.m_physicalMax()) + || detail::CheckFormatErrors(sigFields.m_digitalMin()) + || detail::CheckFormatErrors(sigFields.m_digitalMax()) + || detail::CheckFormatErrors(sigFields.m_prefilter()) + || detail::CheckFormatErrors(sigFields.m_samplesInDataRecord()) + || detail::CheckFormatErrors(sigFields.m_reserved())) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + + } + + // Labels + { + size_t totalAnnotationChannels = 0; + for (size_t idx = 0; idx < signals.size(); idx++) + { + auto &signal = signals[idx]; + auto& label = in[idx].m_label(); + if (IsPlus(m_version)) + { + if (label.find("Annotation") != std::string::npos) + { + totalAnnotationChannels++; + signal.m_detail.m_isAnnotation = true; + } + else + { + signal.m_detail.m_isAnnotation = false; + } + } + else + { + signal.m_detail.m_isAnnotation = false; + } + signal.m_label = label; + } + if (IsPlus(m_version) && totalAnnotationChannels == 0) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + // TODO: check if this is true + /*if (m_datarecordDuration < 1) + { + if (signals.size() != totalAnnotationChannels || !IsPlus(m_version)) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + }*/ + } + // Transducers Types + { + for (size_t idx = 0; idx < signals.size(); idx++) + { + auto &signal = signals[idx]; + auto& transducer = in[idx].m_transducer(); + + signal.m_transducer = transducer; + + if (signal.m_detail.m_isAnnotation) + { + if (transducer.find_first_not_of(' ') != std::string::npos) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + } + } + } + // Physical Dimensions + { + for (size_t idx = 0; idx < signals.size(); idx++) + { + auto &signal = signals[idx]; + auto& physDimension = in[idx].m_physDimension(); + + signal.m_physDimension = physDimension; + } + } + // Physical Minima + { + for (size_t idx = 0; idx < signals.size(); idx++) + { + auto &signal = signals[idx]; + auto& physMin = in[idx].m_physicalMin(); + signal.m_physicalMin = detail::ParseDouble(physMin, detail::GetError(FileErrc::FileContainsFormatErrors)); + } + } + // Physical Maxima + { + for (size_t idx = 0; idx < signals.size(); idx++) + { + auto &signal = signals[idx]; + auto& physMax = in[idx].m_physicalMax(); + signal.m_physicalMax = detail::ParseDouble(physMax, detail::GetError(FileErrc::FileContainsFormatErrors)); + } + } + // Digital Minima + { + for (size_t idx = 0; idx < signals.size(); idx++) + { + auto &signal = signals[idx]; + auto& digMin = in[idx].m_digitalMin(); + int n = detail::ParseInt(digMin, detail::GetError(FileErrc::FileContainsFormatErrors)); + + if (signal.m_detail.m_isAnnotation) + { + if (IsEdf(m_version) && IsPlus(m_version)) + { + if (n != -32768) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + } + else if (IsBdf(m_version) && IsPlus(m_version)) + { + if (n != -8388608) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + } + } + else if (IsEdf(m_version)) + { + if ((n > 32767) || (n < -32768)) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + } + else if (IsBdf(m_version)) + { + if ((n > 8388607) || (n < -8388608)) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + } + signal.m_digitalMin = n; + } + } + // Digital Maxima + { + for (size_t idx = 0; idx < signals.size(); idx++) + { + auto &signal = signals[idx]; + auto& digMax = in[idx].m_digitalMax(); + int n = detail::ParseInt(digMax, detail::GetError(FileErrc::FileContainsFormatErrors)); + + if (signal.m_detail.m_isAnnotation) + { + if (IsEdf(m_version) && IsPlus(m_version)) + { + if (n != 32767) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + } + else if (IsBdf(m_version) && IsPlus(m_version)) + { + if (n != 8388607) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + } + } + else if (IsEdf(m_version)) + { + if ((n > 32767) || (n < -32768)) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + } + else if (IsBdf(m_version)) + { + if ((n > 8388607) || (n < -8388608)) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + } + signal.m_digitalMax = n; + if (signal.m_digitalMax < (signal.m_digitalMin + 1)) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + } + } + // Prefilter + { + for (size_t idx = 0; idx < signals.size(); idx++) + { + auto &signal = signals[idx]; + auto& prefilter = in[idx].m_prefilter(); -#include "impl/ProcessorHeaderSignalFields.ipp" + signal.m_prefilter = prefilter; + + if (signal.m_detail.m_isAnnotation) + { + if (prefilter.find_first_not_of(' ') != std::string::npos) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + } + } + } + // Samples in each datarecord + { + for (size_t idx = 0; idx < signals.size(); idx++) + { + auto &signal = signals[idx]; + auto& nrSamples = in[idx].m_samplesInDataRecord(); + int n = detail::ParseInt(nrSamples, detail::GetError(FileErrc::FileContainsFormatErrors)); + + if (n < 1) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + signal.m_samplesInDataRecord = n; + } + } + // Reserved + { + for (size_t idx = 0; idx < signals.size(); idx++) + { + auto &signal = signals[idx]; + auto& reserved = in[idx].m_reserved(); + signal.m_reserved = reserved; + } + } + // Details + { + size_t n = 0; + for (size_t idx = 0; idx < signals.size(); idx++) + { + auto &signal = signals[idx]; + + signal.m_detail.m_signalOffset = n; + if (IsBdf(m_version)) + n += signal.m_samplesInDataRecord * 3; + else if (IsEdf(m_version)) + n += signal.m_samplesInDataRecord * 2; + + signal.m_detail.m_scaling = (signal.m_physicalMax - signal.m_physicalMin) / (signal.m_digitalMax - signal.m_digitalMin); + signal.m_detail.m_offset = signal.m_physicalMin - signal.m_detail.m_scaling * signal.m_digitalMin; + } + } + + + for (auto &signal : signals) + { + signal.m_label = detail::ReduceString(signal.m_label); + signal.m_transducer = detail::ReduceString(signal.m_transducer); + signal.m_physDimension = detail::ReduceString(signal.m_physDimension); + signal.m_prefilter = detail::ReduceString(signal.m_prefilter); + signal.m_reserved = detail::ReduceString(signal.m_reserved); + } + + return signals; + } + +} diff --git a/include/edfio/processor/ProcessorSample.hpp b/include/edfio/processor/ProcessorSample.hpp index 46c34ef..2eff135 100644 --- a/include/edfio/processor/ProcessorSample.hpp +++ b/include/edfio/processor/ProcessorSample.hpp @@ -37,6 +37,25 @@ namespace edfio size_t m_sampleSize; }; -} + template + inline Record ProcessorSample::operator()(ProcType sample) + { + DigiType value = 0; + if (std::is_same::value) + value = static_cast(sample); + else + value = impl::ConvertSample(m_offset, m_scaling, sample); + + Record record(m_sampleSize); + auto it = record().begin(); + + for (int count = m_sampleSize; count > 0; count--) + { + unsigned char tmp = (value >> (count - 1) * 8); + *it++ = tmp; + } -#include "impl/ProcessorSample.ipp" + return record; + } + +} diff --git a/include/edfio/processor/ProcessorSampleRecord.hpp b/include/edfio/processor/ProcessorSampleRecord.hpp index d45b7b5..7de74f6 100644 --- a/include/edfio/processor/ProcessorSampleRecord.hpp +++ b/include/edfio/processor/ProcessorSampleRecord.hpp @@ -35,6 +35,32 @@ namespace edfio const double m_scaling; }; -} + template + inline typename ProcessorSampleRecord::ProcType + ProcessorSampleRecord::operator()(Record record) + { + DigiType sample = 0; + auto const& bytes = record(); + std::size_t const nbytes = bytes.size(); + + // Assemble bytes (big-endian order as written by ProcessorSample) + for (std::size_t i = 0; i < nbytes; ++i) + { + sample <<= 8; + sample |= static_cast(bytes[i]); + } -#include "impl/ProcessorSampleRecord.ipp" + // Sign-extend: if high bit of the MSB is set, the value is negative + if (nbytes > 0 && nbytes < sizeof(DigiType)) + { + unsigned int sign_bit = 1u << (nbytes * 8 - 1); + if (sample & sign_bit) + sample |= ~((1 << (nbytes * 8)) - 1); + } + + if constexpr (std::is_same_v) + return sample; + return impl::ConvertSample(m_offset, m_scaling, sample); + } + +} diff --git a/include/edfio/processor/ProcessorTalRecord.hpp b/include/edfio/processor/ProcessorTalRecord.hpp index dda7682..37511e5 100644 --- a/include/edfio/processor/ProcessorTalRecord.hpp +++ b/include/edfio/processor/ProcessorTalRecord.hpp @@ -9,7 +9,9 @@ #pragma once +#include "../Utils.hpp" #include "../core/Annotation.hpp" +#include "../core/Record.hpp" #include @@ -21,6 +23,68 @@ namespace edfio std::vector operator ()(std::vector record, long long datarecord); }; -} + inline std::vector ProcessorTalRecord::operator()(std::vector record, long long datarecord) + { + std::vector out; + + // Boundaries + auto first = record.begin(); + auto last = record.end(); + + // TAL MUST start with '+' or '-' + if (*first != '+' && *first != '-') + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsInvalidAnnotations)); + } + + double start = 0; + double duration = 0; + bool onset = true; + for (auto it = first; it != last; it++) + { + if (std::distance(first, it) > 0) + { + // First field is for TAL onset + if (onset) + { + if (*it == detail::DURATION_DIV || *it == detail::ANNOTATION_DIV) + { + std::string tmp(first, it); + start = detail::ParseDouble(tmp, detail::GetError(FileErrc::FileContainsInvalidAnnotations)); + onset = false; -#include "impl/ProcessorTalRecord.ipp" + first = it; + } + } + else if (*it == detail::ANNOTATION_DIV) + { + if (*first == detail::DURATION_DIV) + { + std::string tmp(first + 1, it); + duration = detail::ParseDouble(tmp, detail::GetError(FileErrc::FileContainsInvalidAnnotations)); + first = it; + } + else if (*first == detail::ANNOTATION_DIV) + { + std::string tmp(first + 1, it); + + // Check if this TAL is in fact just a timestamp, we don't want this + if (!tmp.empty()) + { + Annotation annot; + annot.m_start = start; + annot.m_duration = duration; + annot.m_annotation = tmp; + annot.m_datarecord = datarecord; + out.emplace_back(std::move(annot)); + } + first = it; + } + } + } + + } + return out; + } + +} diff --git a/include/edfio/processor/ProcessorTimeStamp.hpp b/include/edfio/processor/ProcessorTimeStamp.hpp index afcc63f..f1a6dfb 100644 --- a/include/edfio/processor/ProcessorTimeStamp.hpp +++ b/include/edfio/processor/ProcessorTimeStamp.hpp @@ -9,8 +9,10 @@ #pragma once -#include "../core/Record.hpp" +#include "../Utils.hpp" #include "../core/Annotation.hpp" +#include "../core/Record.hpp" +#include "detail/ProcessorUtils.hpp" namespace edfio { @@ -20,6 +22,32 @@ namespace edfio Record operator ()(TimeStamp timestamp); }; -} + inline Record ProcessorTimeStamp::operator()(TimeStamp timestamp) + { + std::string ts = detail::to_string_decimal(timestamp.m_start); + + if (ts.empty()) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileWriteInvalidAnnotations)); + } -#include "impl/ProcessorTimeStamp.ipp" + size_t plusSignal = 0; + if (timestamp.m_start >= 0) + plusSignal = 1; + + Record record(plusSignal + ts.size() + 3); // 20 20 0 + auto it = record().begin(); + + // timestamp + if (plusSignal) + *it++ = '+'; + std::move(ts.begin(), ts.end(), it); + it += ts.size(); + *it++ = 20; // 20 div + *it++ = 20; // 20 div + *it++ = 0; // 0 end + + return record; + } + +} diff --git a/include/edfio/processor/ProcessorTimeStampRecord.hpp b/include/edfio/processor/ProcessorTimeStampRecord.hpp index c71434c..d8ce9e0 100644 --- a/include/edfio/processor/ProcessorTimeStampRecord.hpp +++ b/include/edfio/processor/ProcessorTimeStampRecord.hpp @@ -10,8 +10,11 @@ #pragma once #include "../Defs.hpp" -#include "../core/Record.hpp" +#include "../Utils.hpp" #include "../core/Annotation.hpp" +#include "../core/Record.hpp" + +#include namespace edfio { @@ -21,6 +24,42 @@ namespace edfio TimeStamp operator ()(Record record, long long datarecord); }; -} + inline TimeStamp edfio::ProcessorTimeStampRecord::operator()(Record record, long long datarecord) + { + TimeStamp timestamp; + timestamp.m_datarecord = datarecord; + auto& value = record(); -#include "impl/ProcessorTimeStampRecord.ipp" + // TimeStamp MUST start with '+' or '-' + if (value.front() != '+' && value.front() != '-') + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsInvalidAnnotations)); + } + + // Make sure it's a valid timestamp + static const std::vector comp = { detail::ANNOTATION_END , detail::ANNOTATION_DIV }; + auto result = std::find_first_of(value.begin(), value.end(), comp.begin(), comp.end()); + + if (result == value.end()) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsInvalidAnnotations)); + } + else + { + *result = 0; + char* end; + double start = std::strtod(value.data(), &end); + // On error + if (end == value.data()) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsInvalidAnnotations)); + } + else + { + timestamp.m_start = start; + } + } + return timestamp; + } + +} diff --git a/include/edfio/processor/impl/ProcessorAnnotation.ipp b/include/edfio/processor/impl/ProcessorAnnotation.ipp deleted file mode 100644 index 182d714..0000000 --- a/include/edfio/processor/impl/ProcessorAnnotation.ipp +++ /dev/null @@ -1,69 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../../Utils.hpp" -#include "../../core/Record.hpp" -#include "../../core/Annotation.hpp" -#include "../detail/ProcessorUtils.hpp" - -namespace edfio -{ - - inline Record ProcessorAnnotation::operator()(Annotation annotation) - { - if (annotation.m_annotation.empty()) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileWriteInvalidAnnotations)); - } - - std::string timestamp = (annotation.m_start >= 0 ? "+" : "") + detail::to_string_decimal(annotation.m_start); - std::string duration; - if (annotation.m_duration > 0) - duration = detail::to_string_decimal(annotation.m_duration); - - size_t size = timestamp.size(); // timestamp - if (!duration.empty()) - { - size++; // 21 div - size += duration.size(); // duration - } - - size++; // 20 div - size += annotation.m_annotation.size(); // annotation - size += 2; // 20 div and 0 - - Record record(size); - auto it = record().begin(); - - // timestamp - std::move(timestamp.begin(), timestamp.end(), it); - it += timestamp.size(); - - if (!duration.empty()) - { - *it++ = 21; // 21 div - // duration - std::move(duration.begin(), duration.end(), it); - it += duration.size(); - } - - *it++ = 20; // 20 div - // annotation - std::move(annotation.m_annotation.begin(), annotation.m_annotation.end(), it); - it += annotation.m_annotation.size(); - - *it++ = 20; // 20 div - *it++ = 0; // 0 end - - return record; - } - -} diff --git a/include/edfio/processor/impl/ProcessorHeaderExam.ipp b/include/edfio/processor/impl/ProcessorHeaderExam.ipp deleted file mode 100644 index 0843cc5..0000000 --- a/include/edfio/processor/impl/ProcessorHeaderExam.ipp +++ /dev/null @@ -1,48 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../../Utils.hpp" -#include "../../core/DataFormat.hpp" - -namespace edfio -{ - - inline HeaderExam edfio::ProcessorHeaderExam::operator()(HeaderGeneral header, std::vector signals) - { - // Record size - size_t recordsize = 0; - for (auto &signal : signals) - { - recordsize += signal.m_samplesInDataRecord; - } - - if (IsBdf(header.m_version)) - { - recordsize *= 3; - if (recordsize > 0xF00000) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - else - { - recordsize *= 2; - if (recordsize > 0xA00000) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - header.m_detail.m_recordSize = recordsize; - - return HeaderExam{ std::move(header), std::move(signals) }; - } - -} diff --git a/include/edfio/processor/impl/ProcessorHeaderGeneral.ipp b/include/edfio/processor/impl/ProcessorHeaderGeneral.ipp deleted file mode 100644 index 5e4360a..0000000 --- a/include/edfio/processor/impl/ProcessorHeaderGeneral.ipp +++ /dev/null @@ -1,154 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../../Defs.hpp" -#include "../../core/DataFormat.hpp" -#include "../detail/ProcessorUtils.hpp" - -#include -#include - -namespace edfio -{ - - inline HeaderGeneralFields ProcessorHeaderGeneral::operator()(HeaderGeneral in) - { - HeaderGeneralFields out; - - // Version - { - if (IsEdf(in.m_version)) - { - out.m_version("0 "); - } - else if (IsBdf(in.m_version)) - { - std::string value = "BIOSEMI"; - value.insert(value.begin(), -1); - out.m_version(value); - } - } - // Patient Name - { - out.m_patient(in.m_patient); - } - // Recording - { - out.m_recording(in.m_recording); - } - // Start Date - { - int day = std::get<0>(in.m_startDate); - int month = std::get<1>(in.m_startDate); - int year = std::get<2>(in.m_startDate); - year -= year > 1999 ? 2000 : 1900; - out.m_startDate(std::format("{:02d}.{:02d}.{:02d}", day, month, year)); - } - // Start Time - { - int hour = std::get<0>(in.m_startTime); - int minute = std::get<1>(in.m_startTime); - int second = std::get<2>(in.m_startTime); - out.m_startTime(std::format("{:02d}.{:02d}.{:02d}", hour, minute, second)); - } - // Header Size - { - out.m_headerSize(std::to_string(in.m_headerSize)); - } - // Reserved - { - if (!in.m_reserved.empty()) - { - out.m_reserved(in.m_reserved); - } - else if (IsPlus(in.m_version)) - { - out.m_reserved(std::string(detail::GetFormatName(in.m_version))); - } - } - // Datarecords in File - { - out.m_datarecordsFile(std::to_string(in.m_datarecordsFile)); - } - // Datarecord Duration - { - out.m_datarecordDuration(detail::to_string_decimal(in.m_datarecordDuration)); - } - // Number of signals - { - out.m_totalSignals(std::to_string(in.m_totalSignals)); - } - // Plus Fields - if (IsPlus(in.m_version)) - { - // Patient Name - { - std::vector fields; - // Code - fields.push_back(in.m_detail.m_patientCode.empty() ? "X" : in.m_detail.m_patientCode); - // Sex - fields.push_back(in.m_detail.m_gender.empty() ? "X" : in.m_detail.m_gender); - // Birthdate in dd-MMM-yyyy format using the English 3-character abbreviations of the month in capitals. 02-AUG-1951 is OK, while 2-AUG-1951 is not. - fields.push_back(in.m_detail.m_birthdate.empty() ? "X" : in.m_detail.m_birthdate); - // The patients name. - fields.push_back(in.m_detail.m_patientName.empty() ? "X" : in.m_detail.m_patientName); - // Additional information. - std::string info; - std::istringstream f(in.m_detail.m_patientAdditional); - while (std::getline(f, info, detail::ADDITIONAL_SEPARATOR)) - { - fields.push_back(info); - } - std::string patient = ""; - for (auto& field : fields) - { - std::replace(field.begin(), field.end(), ' ', '_'); // replace all ' ' to '_' - patient += field + " "; - } - patient.pop_back(); // remove last " " - out.m_patient(patient); - } - // Recording - { - std::vector fields; - // The text 'Startdate' - fields.push_back("Startdate"); - // The startdate itself in dd-MMM-yyyy format using the English 3-character abbreviations of the month in capitals: dd-MMM-yyyy (MMM = 'JAN' | 'FEV' | ...) - int day = std::get<0>(in.m_startDate); - int month = std::get<1>(in.m_startDate); - int year = std::get<2>(in.m_startDate); - fields.push_back(std::format("{:02d}-{}-{}", day ? day : 1, detail::GetStringFromMonth(month), year ? year : 1984)); - // The hospital administration code of the investigation, i.e. EEG number or PSG number. - fields.push_back(in.m_detail.m_admincode.empty() ? "X" : in.m_detail.m_admincode); - // A code specifying the responsible investigator or technician. - fields.push_back(in.m_detail.m_technician.empty() ? "X" : in.m_detail.m_technician); - // A code specifying the used equipment. - fields.push_back(in.m_detail.m_equipment.empty() ? "X" : in.m_detail.m_equipment); - // Additional information. - std::string info; - std::istringstream f(in.m_detail.m_recordingAdditional); - while (std::getline(f, info, detail::ADDITIONAL_SEPARATOR)) - { - fields.push_back(info); - } - std::string recording = ""; - for (auto& field : fields) - { - std::replace(field.begin(), field.end(), ' ', '_'); // replace all '_' to ' ' - recording += field + " "; - } - recording.pop_back(); // remove last " " - out.m_recording(recording); - } - } - return out; - } -} diff --git a/include/edfio/processor/impl/ProcessorHeaderGeneralFields.ipp b/include/edfio/processor/impl/ProcessorHeaderGeneralFields.ipp deleted file mode 100644 index 98a0062..0000000 --- a/include/edfio/processor/impl/ProcessorHeaderGeneralFields.ipp +++ /dev/null @@ -1,317 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../../Utils.hpp" -#include "../../core/DataFormat.hpp" -#include "../detail/ProcessorUtils.hpp" - -#include - -namespace edfio -{ - - inline HeaderGeneral ProcessorHeaderGeneralFields::operator()(HeaderGeneralFields in) - { - HeaderGeneral out; - - if (/*detail::CheckFormatErrors(in.m_version()) - ||*/ detail::CheckFormatErrors(in.m_patient()) - || detail::CheckFormatErrors(in.m_recording()) - || detail::CheckFormatErrors(in.m_startDate()) - || detail::CheckFormatErrors(in.m_startTime()) - || detail::CheckFormatErrors(in.m_headerSize()) - || detail::CheckFormatErrors(in.m_reserved()) - || detail::CheckFormatErrors(in.m_datarecordsFile()) - || detail::CheckFormatErrors(in.m_datarecordDuration()) - || detail::CheckFormatErrors(in.m_totalSignals())) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - - - // Version - { - auto &version = in.m_version(); - if (version.front() == -1) // BDF-file - { - if (version.substr(1) != "BIOSEMI") - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - out.m_version = DataFormat::Bdf; - } - else // EDF-file - { - if (version != "0 ") - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - out.m_version = DataFormat::Edf; - } - } - // Patient Name - { - out.m_patient = in.m_patient(); - } - // Recording - { - out.m_recording = in.m_recording(); - } - // Start Date - { - auto& startdate = in.m_startDate(); - if ((startdate[2] != '.') || (startdate[5] != '.') - || !std::isdigit(startdate[0]) || !std::isdigit(startdate[1]) - || !std::isdigit(startdate[3]) || !std::isdigit(startdate[4]) - || !std::isdigit(startdate[6]) || !std::isdigit(startdate[7])) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - { - int day{}, month{}, year{}; - auto [p1, e1] = std::from_chars(startdate.data(), startdate.data() + 2, day); - auto [p2, e2] = std::from_chars(startdate.data() + 3, startdate.data() + 5, month); - auto [p3, e3] = std::from_chars(startdate.data() + 6, startdate.data() + 8, year); - if (e1 != std::errc{} || e2 != std::errc{} || e3 != std::errc{} - || day < 1 || day > 31 || month < 1 || month > 12) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - year += year > 84 ? 1900 : 2000; - out.m_startDate = std::make_tuple(day, month, year); - } - } - // Start Time - { - auto& starttime = in.m_startTime(); - if ((starttime[2] != '.') || (starttime[5] != '.') - || !std::isdigit(starttime[0]) || !std::isdigit(starttime[1]) - || !std::isdigit(starttime[3]) || !std::isdigit(starttime[4]) - || !std::isdigit(starttime[6]) || !std::isdigit(starttime[7])) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - { - int hour{}, minute{}, second{}; - auto [p1, e1] = std::from_chars(starttime.data(), starttime.data() + 2, hour); - auto [p2, e2] = std::from_chars(starttime.data() + 3, starttime.data() + 5, minute); - auto [p3, e3] = std::from_chars(starttime.data() + 6, starttime.data() + 8, second); - if (e1 != std::errc{} || e2 != std::errc{} || e3 != std::errc{} - || hour > 23 || minute > 59 || second > 59) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - out.m_startTime = std::make_tuple(hour, minute, second); - } - } - // Header Size - { - out.m_headerSize = detail::ParseInt(in.m_headerSize(), detail::GetError(FileErrc::FileContainsFormatErrors)); - } - // Reserved - { - auto &reserved = in.m_reserved(); - if (IsEdf(out.m_version)) - { - if (reserved.find("EDF+C") != std::string::npos) - { - out.m_version = DataFormat::EdfPlusC; - } - else if (reserved.find("EDF+D") != std::string::npos) - { - out.m_version = DataFormat::EdfPlusD; - } - } - else if (IsBdf(out.m_version)) - { - if (reserved.find("BDF+C") != std::string::npos) - { - out.m_version = DataFormat::BdfPlusC; - } - else if (reserved.find("BDF+D") != std::string::npos) - { - out.m_version = DataFormat::BdfPlusD; - } - } - } - // Datarecords in File - { - out.m_datarecordsFile = detail::ParseLongLong(in.m_datarecordsFile(), detail::GetError(FileErrc::FileContainsFormatErrors)); - if (out.m_datarecordsFile < 0) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - // Datarecord Duration - { - double duration = detail::ParseDouble(in.m_datarecordDuration(), detail::GetError(FileErrc::FileContainsFormatErrors)); - out.m_datarecordDuration = duration; - if (duration < 0) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - out.m_detail.m_fileDuration = out.m_datarecordDuration * out.m_datarecordsFile; - } - // Number of signals - { - int signals = detail::ParseInt(in.m_totalSignals(), detail::GetError(FileErrc::FileContainsFormatErrors)); - if (signals <= 0 || (signals * 256 + 256) != out.m_headerSize) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - out.m_totalSignals = signals; - } - // Plus Fields - if (IsPlus(out.m_version)) - { - // Patient Name - { - auto &details = out.m_detail; - std::vector fields; - std::string field; - std::istringstream f(out.m_patient); - while (std::getline(f, field, ' ')) - { - fields.push_back(field); - } - if (fields.size() < 4) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - - details.m_patientAdditional.clear(); - - for (size_t i = 0; i < fields.size(); i++) - { - auto& str = fields[i]; - std::replace(str.begin(), str.end(), '_', ' '); // replace all '_' to ' ' - switch (i) - { - case 0: // The code by which the patient is known in the hospital administration. - details.m_patientCode = str; - break; - case 1: // Sex - if (str == "F" || str == "M" || str == "X") - { - details.m_gender = str; - } - else - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - break; - case 2: // Birthdate in dd-MMM-yyyy format using the English 3-character abbreviations of the month in capitals. 02-AUG-1951 is OK, while 2-AUG-1951 is not. - details.m_birthdate = str; - break; - case 3: // The patients name. - details.m_patientName = str; - break; - default: // Additional information. - if (!details.m_patientAdditional.empty()) - { - details.m_patientAdditional += detail::ADDITIONAL_SEPARATOR; - } - details.m_patientAdditional.append(str); - break; - } - } - } - // Recording - { - auto &details = out.m_detail; - std::vector fields; - std::string field; - std::istringstream f(out.m_recording); - while (std::getline(f, field, ' ')) - { - fields.push_back(field); - } - - if (fields.size() < 5) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - - details.m_recordingAdditional.clear(); - - for (size_t i = 0; i < fields.size(); i++) - { - auto& str = fields[i]; - std::replace(str.begin(), str.end(), '_', ' '); // replace all '_' to ' ' - if (str != "X") - { - switch (i) - { - case 0: // The text 'Startdate'. - if (str != "Startdate") - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - break; - case 1: // The startdate itself in dd-MMM-yyyy format using the English 3-character abbreviations of the month in capitals: dd-MMM-yyyy (MMM = 'JAN' | 'FEV' | ...) - if (str.size() == 11 && str[2] == '-' && str[6] == '-') - { - { - int day{}, year{}; - auto [p1, e1] = std::from_chars(str.data(), str.data() + 2, day); - auto [p2, e2] = std::from_chars(str.data() + 7, str.data() + 11, year); - int month = detail::GetMonthFromString(std::string_view{str}.substr(3, 3)); - if (e1 != std::errc{} || e2 != std::errc{} || month == 0) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - out.m_startDate = std::make_tuple(day, month, year); - } - } - else - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - break; - case 2: // The hospital administration code of the investigation, i.e. EEG number or PSG number. - details.m_admincode = str; - break; - case 3: // A code specifying the responsible investigator or technician. - details.m_technician = str; - break; - case 4: // A code specifying the used equipment. - details.m_equipment = str; - break; - default: // Additional information. - if (!details.m_recordingAdditional.empty()) - { - details.m_recordingAdditional += detail::ADDITIONAL_SEPARATOR; - } - details.m_recordingAdditional.append(str); - break; - } - } - } - } - } - - out.m_patient = detail::ReduceString(out.m_patient); - out.m_recording = detail::ReduceString(out.m_recording); - out.m_reserved = detail::ReduceString(out.m_reserved); - out.m_detail.m_patientCode = detail::ReduceString(out.m_detail.m_patientCode); - out.m_detail.m_gender = detail::ReduceString(out.m_detail.m_gender); - out.m_detail.m_birthdate = detail::ReduceString(out.m_detail.m_birthdate); - out.m_detail.m_patientName = detail::ReduceString(out.m_detail.m_patientName); - out.m_detail.m_patientAdditional = detail::ReduceString(out.m_detail.m_patientAdditional); - out.m_detail.m_admincode = detail::ReduceString(out.m_detail.m_admincode); - out.m_detail.m_technician = detail::ReduceString(out.m_detail.m_technician); - out.m_detail.m_equipment = detail::ReduceString(out.m_detail.m_equipment); - out.m_detail.m_recordingAdditional = detail::ReduceString(out.m_detail.m_recordingAdditional); - - return out; - } - -} diff --git a/include/edfio/processor/impl/ProcessorHeaderSignal.ipp b/include/edfio/processor/impl/ProcessorHeaderSignal.ipp deleted file mode 100644 index aa9efbf..0000000 --- a/include/edfio/processor/impl/ProcessorHeaderSignal.ipp +++ /dev/null @@ -1,101 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../../Utils.hpp" -#include "../../core/DataFormat.hpp" -#include "../detail/ProcessorUtils.hpp" - -#include -#include - -namespace edfio -{ - - inline std::vector ProcessorHeaderSignal::operator()(std::vector in) - { - std::vector out(in.size()); - auto &signals = in; - - // Labels - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - out[idx].m_label(signals[idx].m_label); - } - } - // Transducers Types - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - out[idx].m_transducer(signals[idx].m_transducer); - } - } - // Physical Dimensions - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - out[idx].m_physDimension(signals[idx].m_physDimension); - } - } - // Physical Minima - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - out[idx].m_physicalMin(detail::to_string_decimal(signals[idx].m_physicalMin)); - } - } - // Physical Maxima - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - out[idx].m_physicalMax(detail::to_string_decimal(signals[idx].m_physicalMax)); - } - } - // Digital Minima - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - out[idx].m_digitalMin(std::to_string(signals[idx].m_digitalMin)); - } - } - // Digital Maxima - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - out[idx].m_digitalMax(std::to_string(signals[idx].m_digitalMax)); - } - } - // Prefilter - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - out[idx].m_prefilter(signals[idx].m_prefilter); - } - } - // Samples in each datarecord - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - out[idx].m_samplesInDataRecord(std::to_string(signals[idx].m_samplesInDataRecord)); - } - } - // Reserved - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - out[idx].m_reserved(signals[idx].m_reserved); - } - } - - return out; - } - -} diff --git a/include/edfio/processor/impl/ProcessorHeaderSignalFields.ipp b/include/edfio/processor/impl/ProcessorHeaderSignalFields.ipp deleted file mode 100644 index 703693d..0000000 --- a/include/edfio/processor/impl/ProcessorHeaderSignalFields.ipp +++ /dev/null @@ -1,286 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../../Utils.hpp" -#include "../../core/DataFormat.hpp" -#include "../detail/ProcessorUtils.hpp" - -namespace edfio -{ - - inline std::vector ProcessorHeaderSignalFields::operator()(std::vector in) - { - std::vector signals(in.size()); - - for (auto& sigFields : in) - { - if (detail::CheckFormatErrors(sigFields.m_label()) - || detail::CheckFormatErrors(sigFields.m_transducer()) - || detail::CheckFormatErrors(sigFields.m_physDimension()) - || detail::CheckFormatErrors(sigFields.m_physicalMin()) - || detail::CheckFormatErrors(sigFields.m_physicalMax()) - || detail::CheckFormatErrors(sigFields.m_digitalMin()) - || detail::CheckFormatErrors(sigFields.m_digitalMax()) - || detail::CheckFormatErrors(sigFields.m_prefilter()) - || detail::CheckFormatErrors(sigFields.m_samplesInDataRecord()) - || detail::CheckFormatErrors(sigFields.m_reserved())) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - - } - - // Labels - { - size_t totalAnnotationChannels = 0; - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& label = in[idx].m_label(); - if (IsPlus(m_version)) - { - if (label.find("Annotation") != std::string::npos) - { - totalAnnotationChannels++; - signal.m_detail.m_isAnnotation = true; - } - else - { - signal.m_detail.m_isAnnotation = false; - } - } - else - { - signal.m_detail.m_isAnnotation = false; - } - signal.m_label = label; - } - if (IsPlus(m_version) && totalAnnotationChannels == 0) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - // TODO: check if this is true - /*if (m_datarecordDuration < 1) - { - if (signals.size() != totalAnnotationChannels || !IsPlus(m_version)) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - }*/ - } - // Transducers Types - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& transducer = in[idx].m_transducer(); - - signal.m_transducer = transducer; - - if (signal.m_detail.m_isAnnotation) - { - if (transducer.find_first_not_of(' ') != std::string::npos) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - } - } - // Physical Dimensions - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& physDimension = in[idx].m_physDimension(); - - signal.m_physDimension = physDimension; - } - } - // Physical Minima - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& physMin = in[idx].m_physicalMin(); - signal.m_physicalMin = detail::ParseDouble(physMin, detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - // Physical Maxima - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& physMax = in[idx].m_physicalMax(); - signal.m_physicalMax = detail::ParseDouble(physMax, detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - // Digital Minima - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& digMin = in[idx].m_digitalMin(); - int n = detail::ParseInt(digMin, detail::GetError(FileErrc::FileContainsFormatErrors)); - - if (signal.m_detail.m_isAnnotation) - { - if (IsEdf(m_version) && IsPlus(m_version)) - { - if (n != -32768) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - else if (IsBdf(m_version) && IsPlus(m_version)) - { - if (n != -8388608) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - } - else if (IsEdf(m_version)) - { - if ((n > 32767) || (n < -32768)) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - else if (IsBdf(m_version)) - { - if ((n > 8388607) || (n < -8388608)) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - signal.m_digitalMin = n; - } - } - // Digital Maxima - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& digMax = in[idx].m_digitalMax(); - int n = detail::ParseInt(digMax, detail::GetError(FileErrc::FileContainsFormatErrors)); - - if (signal.m_detail.m_isAnnotation) - { - if (IsEdf(m_version) && IsPlus(m_version)) - { - if (n != 32767) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - else if (IsBdf(m_version) && IsPlus(m_version)) - { - if (n != 8388607) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - } - else if (IsEdf(m_version)) - { - if ((n > 32767) || (n < -32768)) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - else if (IsBdf(m_version)) - { - if ((n > 8388607) || (n < -8388608)) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - signal.m_digitalMax = n; - if (signal.m_digitalMax < (signal.m_digitalMin + 1)) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - } - // Prefilter - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& prefilter = in[idx].m_prefilter(); - - signal.m_prefilter = prefilter; - - if (signal.m_detail.m_isAnnotation) - { - if (prefilter.find_first_not_of(' ') != std::string::npos) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - } - } - // Samples in each datarecord - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& nrSamples = in[idx].m_samplesInDataRecord(); - int n = detail::ParseInt(nrSamples, detail::GetError(FileErrc::FileContainsFormatErrors)); - - if (n < 1) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - signal.m_samplesInDataRecord = n; - } - } - // Reserved - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& reserved = in[idx].m_reserved(); - signal.m_reserved = reserved; - } - } - // Details - { - size_t n = 0; - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - - signal.m_detail.m_signalOffset = n; - if (IsBdf(m_version)) - n += signal.m_samplesInDataRecord * 3; - else if (IsEdf(m_version)) - n += signal.m_samplesInDataRecord * 2; - - signal.m_detail.m_scaling = (signal.m_physicalMax - signal.m_physicalMin) / (signal.m_digitalMax - signal.m_digitalMin); - signal.m_detail.m_offset = signal.m_physicalMin - signal.m_detail.m_scaling * signal.m_digitalMin; - } - } - - - for (auto &signal : signals) - { - signal.m_label = detail::ReduceString(signal.m_label); - signal.m_transducer = detail::ReduceString(signal.m_transducer); - signal.m_physDimension = detail::ReduceString(signal.m_physDimension); - signal.m_prefilter = detail::ReduceString(signal.m_prefilter); - signal.m_reserved = detail::ReduceString(signal.m_reserved); - } - - return signals; - } - -} diff --git a/include/edfio/processor/impl/ProcessorSample.ipp b/include/edfio/processor/impl/ProcessorSample.ipp deleted file mode 100644 index 9222be0..0000000 --- a/include/edfio/processor/impl/ProcessorSample.ipp +++ /dev/null @@ -1,37 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -namespace edfio -{ - - template - inline Record ProcessorSample::operator()(ProcType sample) - { - DigiType value = 0; - if (std::is_same::value) - value = static_cast(sample); - else - value = impl::ConvertSample(m_offset, m_scaling, sample); - - Record record(m_sampleSize); - auto it = record().begin(); - - for (int count = m_sampleSize; count > 0; count--) - { - unsigned char tmp = (value >> (count - 1) * 8); - *it++ = tmp; - } - - return record; - } - -} - diff --git a/include/edfio/processor/impl/ProcessorSampleRecord.ipp b/include/edfio/processor/impl/ProcessorSampleRecord.ipp deleted file mode 100644 index 33f130c..0000000 --- a/include/edfio/processor/impl/ProcessorSampleRecord.ipp +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -namespace edfio -{ - - template - inline typename ProcessorSampleRecord::ProcType - ProcessorSampleRecord::operator()(Record record) - { - DigiType sample = 0; - auto const& bytes = record(); - std::size_t const nbytes = bytes.size(); - - // Assemble bytes (big-endian order as written by ProcessorSample) - for (std::size_t i = 0; i < nbytes; ++i) - { - sample <<= 8; - sample |= static_cast(bytes[i]); - } - - // Sign-extend: if high bit of the MSB is set, the value is negative - if (nbytes > 0 && nbytes < sizeof(DigiType)) - { - unsigned int sign_bit = 1u << (nbytes * 8 - 1); - if (sample & sign_bit) - sample |= ~((1 << (nbytes * 8)) - 1); - } - - if constexpr (std::is_same_v) - return sample; - return impl::ConvertSample(m_offset, m_scaling, sample); - } - -} diff --git a/include/edfio/processor/impl/ProcessorTalRecord.ipp b/include/edfio/processor/impl/ProcessorTalRecord.ipp deleted file mode 100644 index 0b844fd..0000000 --- a/include/edfio/processor/impl/ProcessorTalRecord.ipp +++ /dev/null @@ -1,85 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../../Utils.hpp" -#include "../../core/Record.hpp" -#include "../../core/Annotation.hpp" - -#include - -namespace edfio -{ - - inline std::vector ProcessorTalRecord::operator()(std::vector record, long long datarecord) - { - std::vector out; - - // Boundaries - auto first = record.begin(); - auto last = record.end(); - - // TAL MUST start with '+' or '-' - if (*first != '+' && *first != '-') - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsInvalidAnnotations)); - } - - double start = 0; - double duration = 0; - bool onset = true; - for (auto it = first; it != last; it++) - { - if (std::distance(first, it) > 0) - { - // First field is for TAL onset - if (onset) - { - if (*it == detail::DURATION_DIV || *it == detail::ANNOTATION_DIV) - { - std::string tmp(first, it); - start = detail::ParseDouble(tmp, detail::GetError(FileErrc::FileContainsInvalidAnnotations)); - onset = false; - - first = it; - } - } - else if (*it == detail::ANNOTATION_DIV) - { - if (*first == detail::DURATION_DIV) - { - std::string tmp(first + 1, it); - duration = detail::ParseDouble(tmp, detail::GetError(FileErrc::FileContainsInvalidAnnotations)); - first = it; - } - else if (*first == detail::ANNOTATION_DIV) - { - std::string tmp(first + 1, it); - - // Check if this TAL is in fact just a timestamp, we don't want this - if (!tmp.empty()) - { - Annotation annot; - annot.m_start = start; - annot.m_duration = duration; - annot.m_annotation = tmp; - annot.m_datarecord = datarecord; - out.emplace_back(std::move(annot)); - } - first = it; - } - } - } - - } - return out; - } - -} diff --git a/include/edfio/processor/impl/ProcessorTimeStamp.ipp b/include/edfio/processor/impl/ProcessorTimeStamp.ipp deleted file mode 100644 index 6487b44..0000000 --- a/include/edfio/processor/impl/ProcessorTimeStamp.ipp +++ /dev/null @@ -1,48 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../../Utils.hpp" -#include "../../core/Record.hpp" -#include "../../core/Annotation.hpp" -#include "../detail/ProcessorUtils.hpp" - -namespace edfio -{ - - inline Record ProcessorTimeStamp::operator()(TimeStamp timestamp) - { - std::string ts = detail::to_string_decimal(timestamp.m_start); - - if (ts.empty()) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileWriteInvalidAnnotations)); - } - - size_t plusSignal = 0; - if (timestamp.m_start >= 0) - plusSignal = 1; - - Record record(plusSignal + ts.size() + 3); // 20 20 0 - auto it = record().begin(); - - // timestamp - if (plusSignal) - *it++ = '+'; - std::move(ts.begin(), ts.end(), it); - it += ts.size(); - *it++ = 20; // 20 div - *it++ = 20; // 20 div - *it++ = 0; // 0 end - - return record; - } - -} diff --git a/include/edfio/processor/impl/ProcessorTimeStampRecord.ipp b/include/edfio/processor/impl/ProcessorTimeStampRecord.ipp deleted file mode 100644 index 42390f6..0000000 --- a/include/edfio/processor/impl/ProcessorTimeStampRecord.ipp +++ /dev/null @@ -1,59 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../../Utils.hpp" -#include "../../core/Record.hpp" -#include "../../core/Annotation.hpp" - -#include - -namespace edfio -{ - - inline TimeStamp edfio::ProcessorTimeStampRecord::operator()(Record record, long long datarecord) - { - TimeStamp timestamp; - timestamp.m_datarecord = datarecord; - auto& value = record(); - - // TimeStamp MUST start with '+' or '-' - if (value.front() != '+' && value.front() != '-') - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsInvalidAnnotations)); - } - - // Make sure it's a valid timestamp - static const std::vector comp = { detail::ANNOTATION_END , detail::ANNOTATION_DIV }; - auto result = std::find_first_of(value.begin(), value.end(), comp.begin(), comp.end()); - - if (result == value.end()) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsInvalidAnnotations)); - } - else - { - *result = 0; - char* end; - double start = std::strtod(value.data(), &end); - // On error - if (end == value.data()) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsInvalidAnnotations)); - } - else - { - timestamp.m_start = start; - } - } - return timestamp; - } - -} diff --git a/include/edfio/reader/ReaderHeaderExam.hpp b/include/edfio/reader/ReaderHeaderExam.hpp index 306eda8..5d8a412 100644 --- a/include/edfio/reader/ReaderHeaderExam.hpp +++ b/include/edfio/reader/ReaderHeaderExam.hpp @@ -9,8 +9,16 @@ #pragma once +#include "../Utils.hpp" #include "../core/StreamIO.hpp" #include "../header/HeaderExam.hpp" +#include "../processor/ProcessorHeaderExam.hpp" +#include "../processor/ProcessorHeaderGeneralFields.hpp" +#include "../processor/ProcessorHeaderSignalFields.hpp" +#include "ReaderHeaderGeneral.hpp" +#include "ReaderHeaderSignal.hpp" + +#include namespace edfio { @@ -20,6 +28,43 @@ namespace edfio HeaderExam operator ()(Stream &stream); }; -} + inline HeaderExam ReaderHeaderExam::operator ()(Stream &stream) + { + // Read general fields + ReaderHeaderGeneral readerGeneral; + auto generalFields = readerGeneral(stream); + // Process general fields + ProcessorHeaderGeneralFields procGeneralFields; + auto general = procGeneralFields(std::move(generalFields)); + + // Read signal fields + ReaderHeaderSignal readerSignals(general.m_totalSignals); + auto signalFields = readerSignals(stream); + // Process signal fields + ProcessorHeaderSignalFields procSignalFields(general.m_version, general.m_datarecordDuration); + auto signals = procSignalFields(std::move(signalFields)); + + // Process header exam + ProcessorHeaderExam procHeader; + auto header = procHeader(std::move(general), std::move(signals)); -#include "impl/ReaderHeaderExam.ipp" + // File size + { + // get current position + auto position = stream.tellg(); + // get length of file + stream.seekg(0, stream.end); + long long length = stream.tellg(); + // send back to previous position + stream.seekg(position, stream.beg); + + if (length != (header.m_general.m_detail.m_recordSize * header.m_general.m_datarecordsFile + header.m_general.m_headerSize)) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); + } + } + + return header; + } + +} diff --git a/include/edfio/reader/ReaderHeaderGeneral.hpp b/include/edfio/reader/ReaderHeaderGeneral.hpp index 2dc1a6f..5d63ce3 100644 --- a/include/edfio/reader/ReaderHeaderGeneral.hpp +++ b/include/edfio/reader/ReaderHeaderGeneral.hpp @@ -9,9 +9,12 @@ #pragma once +#include "../Utils.hpp" #include "../core/StreamIO.hpp" #include "../header/HeaderGeneral.hpp" +#include + namespace edfio { @@ -20,6 +23,33 @@ namespace edfio HeaderGeneralFields operator ()(Stream &stream); }; -} + inline HeaderGeneralFields ReaderHeaderGeneral::operator ()(Stream &stream) + { + HeaderGeneralFields hdr; + if (!stream || !stream.is_open()) + throw std::invalid_argument(detail::GetError(FileErrc::FileNotOpened)); -#include "impl/ReaderHeaderGeneral.ipp" + stream.clear(); + stream.seekg(0, std::ios::beg); + + try + { + stream >> hdr.m_version; + stream >> hdr.m_patient; + stream >> hdr.m_recording; + stream >> hdr.m_startDate; + stream >> hdr.m_startTime; + stream >> hdr.m_headerSize; + stream >> hdr.m_reserved; + stream >> hdr.m_datarecordsFile; + stream >> hdr.m_datarecordDuration; + stream >> hdr.m_totalSignals; + } + catch (const std::exception&) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileReadError)); + } + return hdr; + } + +} diff --git a/include/edfio/reader/ReaderHeaderSignal.hpp b/include/edfio/reader/ReaderHeaderSignal.hpp index 0054a8c..b16f5e1 100644 --- a/include/edfio/reader/ReaderHeaderSignal.hpp +++ b/include/edfio/reader/ReaderHeaderSignal.hpp @@ -9,10 +9,12 @@ #pragma once +#include "../Utils.hpp" +#include "../core/StreamIO.hpp" #include "../header/HeaderGeneral.hpp" #include "../header/HeaderSignal.hpp" -#include "../core/StreamIO.hpp" +#include #include namespace edfio @@ -27,6 +29,43 @@ namespace edfio size_t m_totalSignals = 0; }; -} + inline std::vector ReaderHeaderSignal::operator ()(Stream &stream) + { + std::vector signals(m_totalSignals); + if (!stream || !stream.is_open()) + throw std::invalid_argument(detail::GetError(FileErrc::FileNotOpened)); -#include "impl/ReaderHeaderSignal.ipp" + stream.clear(); + stream.seekg(256, std::ios::beg); + + try + { + for (auto &s : signals) + stream >> s.m_label; + for (auto &s : signals) + stream >> s.m_transducer; + for (auto &s : signals) + stream >> s.m_physDimension; + for (auto &s : signals) + stream >> s.m_physicalMin; + for (auto &s : signals) + stream >> s.m_physicalMax; + for (auto &s : signals) + stream >> s.m_digitalMin; + for (auto &s : signals) + stream >> s.m_digitalMax; + for (auto &s : signals) + stream >> s.m_prefilter; + for (auto &s : signals) + stream >> s.m_samplesInDataRecord; + for (auto &s : signals) + stream >> s.m_reserved; + } + catch (const std::exception&) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileReadError)); + } + return signals; + } + +} diff --git a/include/edfio/reader/impl/ReaderHeaderExam.ipp b/include/edfio/reader/impl/ReaderHeaderExam.ipp deleted file mode 100644 index c4b75fd..0000000 --- a/include/edfio/reader/impl/ReaderHeaderExam.ipp +++ /dev/null @@ -1,64 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../../Utils.hpp" -#include "../../header/HeaderExam.hpp" -#include "../ReaderHeaderGeneral.hpp" -#include "../ReaderHeaderSignal.hpp" -#include "../../processor/ProcessorHeaderGeneralFields.hpp" -#include "../../processor/ProcessorHeaderSignalFields.hpp" -#include "../../processor/ProcessorHeaderExam.hpp" - -#include - -namespace edfio -{ - - inline HeaderExam ReaderHeaderExam::operator ()(Stream &stream) - { - // Read general fields - ReaderHeaderGeneral readerGeneral; - auto generalFields = readerGeneral(stream); - // Process general fields - ProcessorHeaderGeneralFields procGeneralFields; - auto general = procGeneralFields(std::move(generalFields)); - - // Read signal fields - ReaderHeaderSignal readerSignals(general.m_totalSignals); - auto signalFields = readerSignals(stream); - // Process signal fields - ProcessorHeaderSignalFields procSignalFields(general.m_version, general.m_datarecordDuration); - auto signals = procSignalFields(std::move(signalFields)); - - // Process header exam - ProcessorHeaderExam procHeader; - auto header = procHeader(std::move(general), std::move(signals)); - - // File size - { - // get current position - auto position = stream.tellg(); - // get length of file - stream.seekg(0, stream.end); - long long length = stream.tellg(); - // send back to previous position - stream.seekg(position, stream.beg); - - if (length != (header.m_general.m_detail.m_recordSize * header.m_general.m_datarecordsFile + header.m_general.m_headerSize)) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - - return header; - } - -} diff --git a/include/edfio/reader/impl/ReaderHeaderGeneral.ipp b/include/edfio/reader/impl/ReaderHeaderGeneral.ipp deleted file mode 100644 index 5d47a7a..0000000 --- a/include/edfio/reader/impl/ReaderHeaderGeneral.ipp +++ /dev/null @@ -1,49 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../../Utils.hpp" -#include "../../header/HeaderGeneral.hpp" - -#include - -namespace edfio -{ - - inline HeaderGeneralFields ReaderHeaderGeneral::operator ()(Stream &stream) - { - HeaderGeneralFields hdr; - if (!stream || !stream.is_open()) - throw std::invalid_argument(detail::GetError(FileErrc::FileNotOpened)); - - stream.clear(); - stream.seekg(0, std::ios::beg); - - try - { - stream >> hdr.m_version; - stream >> hdr.m_patient; - stream >> hdr.m_recording; - stream >> hdr.m_startDate; - stream >> hdr.m_startTime; - stream >> hdr.m_headerSize; - stream >> hdr.m_reserved; - stream >> hdr.m_datarecordsFile; - stream >> hdr.m_datarecordDuration; - stream >> hdr.m_totalSignals; - } - catch (const std::exception&) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileReadError)); - } - return hdr; - } - -} diff --git a/include/edfio/reader/impl/ReaderHeaderSignal.ipp b/include/edfio/reader/impl/ReaderHeaderSignal.ipp deleted file mode 100644 index 91b3189..0000000 --- a/include/edfio/reader/impl/ReaderHeaderSignal.ipp +++ /dev/null @@ -1,61 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../../Utils.hpp" -#include "../../header/HeaderGeneral.hpp" -#include "../../header/HeaderSignal.hpp" - -#include -#include - -namespace edfio -{ - - inline std::vector ReaderHeaderSignal::operator ()(Stream &stream) - { - std::vector signals(m_totalSignals); - if (!stream || !stream.is_open()) - throw std::invalid_argument(detail::GetError(FileErrc::FileNotOpened)); - - stream.clear(); - stream.seekg(256, std::ios::beg); - - try - { - for (auto &s : signals) - stream >> s.m_label; - for (auto &s : signals) - stream >> s.m_transducer; - for (auto &s : signals) - stream >> s.m_physDimension; - for (auto &s : signals) - stream >> s.m_physicalMin; - for (auto &s : signals) - stream >> s.m_physicalMax; - for (auto &s : signals) - stream >> s.m_digitalMin; - for (auto &s : signals) - stream >> s.m_digitalMax; - for (auto &s : signals) - stream >> s.m_prefilter; - for (auto &s : signals) - stream >> s.m_samplesInDataRecord; - for (auto &s : signals) - stream >> s.m_reserved; - } - catch (const std::exception&) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileReadError)); - } - return signals; - } - -} diff --git a/include/edfio/writer/WriterHeaderExam.hpp b/include/edfio/writer/WriterHeaderExam.hpp index 9741af6..96b62da 100644 --- a/include/edfio/writer/WriterHeaderExam.hpp +++ b/include/edfio/writer/WriterHeaderExam.hpp @@ -9,8 +9,13 @@ #pragma once -#include "../header/HeaderExam.hpp" #include "../core/StreamIO.hpp" +#include "../header/HeaderExam.hpp" +#include "../processor/ProcessorHeaderGeneral.hpp" +#include "../processor/ProcessorHeaderSignal.hpp" +#include "../Utils.hpp" +#include "WriterHeaderGeneral.hpp" +#include "WriterHeaderSignals.hpp" #include @@ -22,6 +27,19 @@ namespace edfio void operator ()(Stream &stream, HeaderExam &input); }; -} + inline void WriterHeaderExam::operator ()(Stream &stream, HeaderExam &input) + { + // Process header general + auto general = ProcessorHeaderGeneral{}(input.m_general); -#include "impl/WriterHeaderExam.ipp" + // Process signal fields + auto signals = ProcessorHeaderSignal{}(input.m_signals); + + // Write general + WriterHeaderGeneral{}(stream, general); + + // Write signals + WriterHeaderSignals{}(stream, signals); + } + +} diff --git a/include/edfio/writer/WriterHeaderGeneral.hpp b/include/edfio/writer/WriterHeaderGeneral.hpp index 3fe0dc3..6e2490e 100644 --- a/include/edfio/writer/WriterHeaderGeneral.hpp +++ b/include/edfio/writer/WriterHeaderGeneral.hpp @@ -9,9 +9,11 @@ #pragma once -#include "../header/HeaderGeneral.hpp" #include "../core/StreamIO.hpp" +#include "../header/HeaderGeneral.hpp" +#include "../Utils.hpp" +#include #include namespace edfio @@ -22,6 +24,32 @@ namespace edfio void operator ()(Stream &stream, HeaderGeneralFields &input); }; -} + inline void WriterHeaderGeneral::operator()(Stream & stream, HeaderGeneralFields & input) + { + auto &hdr = input; + if (!stream || !stream.is_open()) + throw std::invalid_argument(detail::GetError(FileErrc::FileNotOpened)); -#include "impl/WriterHeaderGeneral.ipp" + stream.clear(); + stream.seekp(0, std::ios::beg); + + try + { + stream << hdr.m_version; + stream << hdr.m_patient; + stream << hdr.m_recording; + stream << hdr.m_startDate; + stream << hdr.m_startTime; + stream << hdr.m_headerSize; + stream << hdr.m_reserved; + stream << hdr.m_datarecordsFile; + stream << hdr.m_datarecordDuration; + stream << hdr.m_totalSignals; + } + catch (const std::exception&) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileWriteError)); + } + } + +} diff --git a/include/edfio/writer/WriterHeaderSignals.hpp b/include/edfio/writer/WriterHeaderSignals.hpp index 9550deb..eb8b001 100644 --- a/include/edfio/writer/WriterHeaderSignals.hpp +++ b/include/edfio/writer/WriterHeaderSignals.hpp @@ -11,7 +11,9 @@ #include "../core/StreamIO.hpp" #include "../header/HeaderSignal.hpp" +#include "../Utils.hpp" +#include #include namespace edfio @@ -22,6 +24,40 @@ namespace edfio void operator ()(Stream &stream, std::vector &signals); }; -} + inline void WriterHeaderSignals::operator()(Stream &stream, std::vector &signals) + { + if (!stream || !stream.is_open()) + throw std::invalid_argument(detail::GetError(FileErrc::FileNotOpened)); + try + { + stream.clear(); + stream.seekp(256, std::ios::beg); -#include "impl/WriterHeaderSignals.ipp" + for (auto &s : signals) + stream << s.m_label; + for (auto &s : signals) + stream << s.m_transducer; + for (auto &s : signals) + stream << s.m_physDimension; + for (auto &s : signals) + stream << s.m_physicalMin; + for (auto &s : signals) + stream << s.m_physicalMax; + for (auto &s : signals) + stream << s.m_digitalMin; + for (auto &s : signals) + stream << s.m_digitalMax; + for (auto &s : signals) + stream << s.m_prefilter; + for (auto &s : signals) + stream << s.m_samplesInDataRecord; + for (auto &s : signals) + stream << s.m_reserved; + } + catch (const std::exception&) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileWriteError)); + } + } + +} diff --git a/include/edfio/writer/WriterRecord.hpp b/include/edfio/writer/WriterRecord.hpp index e685380..cf41184 100644 --- a/include/edfio/writer/WriterRecord.hpp +++ b/include/edfio/writer/WriterRecord.hpp @@ -9,8 +9,11 @@ #pragma once -#include "../core/StreamIO.hpp" #include "../core/Record.hpp" +#include "../core/StreamIO.hpp" +#include "../Utils.hpp" + +#include namespace edfio { @@ -20,6 +23,19 @@ namespace edfio void operator ()(Stream &stream, Record &record); }; -} + inline void WriterRecord::operator()(Stream &stream, Record &record) + { + if (!stream || !stream.is_open()) + throw std::invalid_argument(detail::GetError(FileErrc::FileNotOpened)); + + try + { + stream << record; + } + catch (const std::exception&) + { + throw std::invalid_argument(detail::GetError(FileErrc::FileWriteError)); + } + } -#include "impl/WriterRecord.ipp" +} diff --git a/include/edfio/writer/impl/WriterHeaderExam.ipp b/include/edfio/writer/impl/WriterHeaderExam.ipp deleted file mode 100644 index de3f215..0000000 --- a/include/edfio/writer/impl/WriterHeaderExam.ipp +++ /dev/null @@ -1,38 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../../Utils.hpp" -#include "../../header/HeaderExam.hpp" -#include "../WriterHeaderGeneral.hpp" -#include "../WriterHeaderSignals.hpp" -#include "../../processor/ProcessorHeaderGeneral.hpp" -#include "../../processor/ProcessorHeaderSignal.hpp" - - -namespace edfio -{ - - inline void WriterHeaderExam::operator ()(Stream &stream, HeaderExam &input) - { - // Process header general - auto general = ProcessorHeaderGeneral{}(input.m_general); - - // Process signal fields - auto signals = ProcessorHeaderSignal{}(input.m_signals); - - // Write general - WriterHeaderGeneral{}(stream, general); - - // Write signals - WriterHeaderSignals{}(stream, signals); - } - -} diff --git a/include/edfio/writer/impl/WriterHeaderGeneral.ipp b/include/edfio/writer/impl/WriterHeaderGeneral.ipp deleted file mode 100644 index a97ef45..0000000 --- a/include/edfio/writer/impl/WriterHeaderGeneral.ipp +++ /dev/null @@ -1,48 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../../Utils.hpp" -#include "../../header/HeaderGeneral.hpp" - -#include - -namespace edfio -{ - - inline void WriterHeaderGeneral::operator()(Stream & stream, HeaderGeneralFields & input) - { - auto &hdr = input; - if (!stream || !stream.is_open()) - throw std::invalid_argument(detail::GetError(FileErrc::FileNotOpened)); - - stream.clear(); - stream.seekp(0, std::ios::beg); - - try - { - stream << hdr.m_version; - stream << hdr.m_patient; - stream << hdr.m_recording; - stream << hdr.m_startDate; - stream << hdr.m_startTime; - stream << hdr.m_headerSize; - stream << hdr.m_reserved; - stream << hdr.m_datarecordsFile; - stream << hdr.m_datarecordDuration; - stream << hdr.m_totalSignals; - } - catch (const std::exception&) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileWriteError)); - } - } - -} diff --git a/include/edfio/writer/impl/WriterHeaderSignals.ipp b/include/edfio/writer/impl/WriterHeaderSignals.ipp deleted file mode 100644 index 334e243..0000000 --- a/include/edfio/writer/impl/WriterHeaderSignals.ipp +++ /dev/null @@ -1,57 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../../Utils.hpp" -#include "../../header/HeaderSignal.hpp" - -#include -#include - -namespace edfio -{ - - inline void WriterHeaderSignals::operator()(Stream &stream, std::vector &signals) - { - if (!stream || !stream.is_open()) - throw std::invalid_argument(detail::GetError(FileErrc::FileNotOpened)); - try - { - stream.clear(); - stream.seekp(256, std::ios::beg); - - for (auto &s : signals) - stream << s.m_label; - for (auto &s : signals) - stream << s.m_transducer; - for (auto &s : signals) - stream << s.m_physDimension; - for (auto &s : signals) - stream << s.m_physicalMin; - for (auto &s : signals) - stream << s.m_physicalMax; - for (auto &s : signals) - stream << s.m_digitalMin; - for (auto &s : signals) - stream << s.m_digitalMax; - for (auto &s : signals) - stream << s.m_prefilter; - for (auto &s : signals) - stream << s.m_samplesInDataRecord; - for (auto &s : signals) - stream << s.m_reserved; - } - catch (const std::exception&) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileWriteError)); - } - } - -} diff --git a/include/edfio/writer/impl/WriterRecord.ipp b/include/edfio/writer/impl/WriterRecord.ipp deleted file mode 100644 index f7d6ffd..0000000 --- a/include/edfio/writer/impl/WriterRecord.ipp +++ /dev/null @@ -1,35 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../../Utils.hpp" -#include "../../core/Record.hpp" - -#include - -namespace edfio -{ - - inline void WriterRecord::operator()(Stream &stream, Record &record) - { - if (!stream || !stream.is_open()) - throw std::invalid_argument(detail::GetError(FileErrc::FileNotOpened)); - - try - { - stream << record; - } - catch (const std::exception&) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileWriteError)); - } - } - -} From 6cf9ced365659835082312705eb98f0e097c94f9 Mon Sep 17 00:00:00 2001 From: iuri dotta Date: Sun, 1 Mar 2026 09:39:43 -0300 Subject: [PATCH 04/11] Add error handling and utility functions for EDFIO - Introduced Errors.hpp to define FileErrc enum for file-related errors with descriptive messages. - Added ProcessorUtils.hpp containing utility functions for format checking, string manipulation, and parsing for various data types. - Implemented functions to handle month conversion and string reduction, enhancing data processing capabilities. --- PLAN.md | 1430 +++++++++++++++++ include/edfio/Config.hpp | 2 +- include/edfio/Defs.hpp | 26 - include/edfio/EdfIO.hpp | 15 +- include/edfio/Errors.hpp | 45 + include/edfio/Utils.hpp | 54 - include/edfio/core/Annotation.hpp | 36 +- include/edfio/core/DataFormat.hpp | 72 +- include/edfio/core/Device.hpp | 80 +- include/edfio/core/Field.hpp | 81 +- include/edfio/core/Record.hpp | 99 +- include/edfio/core/SampleType.hpp | 66 +- include/edfio/core/StreamIO.hpp | 23 +- include/edfio/header/HeaderExam.hpp | 14 +- include/edfio/header/HeaderGeneral.hpp | 93 +- include/edfio/header/HeaderSignal.hpp | 87 +- include/edfio/header/HeaderUtils.hpp | 220 ++- .../edfio/processor/ProcessorAnnotation.hpp | 90 +- .../edfio/processor/ProcessorHeaderExam.hpp | 66 +- .../processor/ProcessorHeaderGeneral.hpp | 280 ++-- .../ProcessorHeaderGeneralFields.hpp | 581 ++++--- .../edfio/processor/ProcessorHeaderSignal.hpp | 10 +- .../processor/ProcessorHeaderSignalFields.hpp | 495 +++--- include/edfio/processor/ProcessorSample.hpp | 84 +- .../edfio/processor/ProcessorSampleRecord.hpp | 78 +- .../edfio/processor/ProcessorTalRecord.hpp | 119 +- .../edfio/processor/ProcessorTimeStamp.hpp | 57 +- .../processor/ProcessorTimeStampRecord.hpp | 87 +- include/edfio/processor/ProcessorUtils.hpp | 173 ++ .../edfio/processor/detail/ProcessorUtils.hpp | 192 --- include/edfio/reader/ReaderHeaderExam.hpp | 86 +- include/edfio/reader/ReaderHeaderGeneral.hpp | 66 +- include/edfio/reader/ReaderHeaderSignal.hpp | 91 +- include/edfio/sink/DataRecordSink.hpp | 94 +- include/edfio/sink/RecordSink.hpp | 464 +++--- include/edfio/sink/SignalRecordSink.hpp | 108 +- include/edfio/sink/Sink.hpp | 44 +- include/edfio/sink/detail/SinkUtils.hpp | 64 +- include/edfio/store/DatarecordStore.hpp | 77 +- include/edfio/store/RecordStore.hpp | 444 +++-- include/edfio/store/SignalSampleStore.hpp | 134 +- include/edfio/store/SignalrecordStore.hpp | 88 +- include/edfio/store/Store.hpp | 46 +- include/edfio/store/TalStore.hpp | 407 +++-- include/edfio/store/TimeStampStore.hpp | 87 +- include/edfio/store/detail/StoreUtils.hpp | 101 +- include/edfio/writer/WriterHeaderExam.hpp | 33 +- include/edfio/writer/WriterHeaderGeneral.hpp | 65 +- include/edfio/writer/WriterHeaderSignals.hpp | 81 +- include/edfio/writer/WriterRecord.hpp | 41 - tests/test_iterators.cpp | 24 +- tests/test_processor_utils.cpp | 10 +- tests/test_reader.cpp | 9 +- tests/test_writer.cpp | 8 +- 54 files changed, 4139 insertions(+), 3288 deletions(-) create mode 100644 PLAN.md delete mode 100644 include/edfio/Defs.hpp create mode 100644 include/edfio/Errors.hpp delete mode 100644 include/edfio/Utils.hpp create mode 100644 include/edfio/processor/ProcessorUtils.hpp delete mode 100644 include/edfio/processor/detail/ProcessorUtils.hpp delete mode 100644 include/edfio/writer/WriterRecord.hpp diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..84905ef --- /dev/null +++ b/PLAN.md @@ -0,0 +1,1430 @@ +# EdfIO C++23 Refactoring -- Implementation Plan + +Target: **clang++ 21.1.8**, **CMake 4.0.2**, **C++23**, **Windows 11** +Architecture invariant: header-only library; "iterators to navigate files like arrays" pattern preserved. + +--- + +## Phase 0: Infrastructure + +**Goal:** Establish CMake build, integrate doctest via FetchContent, create the test harness, verify everything compiles before touching a single line of library code. + +### 0.1 Create `CMakeLists.txt` (root) + +**New file:** `C:/dev/code/edfio/CMakeLists.txt` + +```cmake +cmake_minimum_required(VERSION 4.0) +project(edfio VERSION 0.2.0 LANGUAGES CXX) + +# ---- Header-only library target ---- +add_library(edfio INTERFACE) +target_include_directories(edfio INTERFACE + $ + $ +) +target_compile_features(edfio INTERFACE cxx_std_23) + +# ---- Tests ---- +option(EDFIO_BUILD_TESTS "Build tests" ON) +if(EDFIO_BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() +``` + +### 0.2 Create `tests/CMakeLists.txt` + +**New file:** `C:/dev/code/edfio/tests/CMakeLists.txt` + +```cmake +include(FetchContent) +FetchContent_Declare( + doctest + GIT_REPOSITORY https://github.com/doctest/doctest.git + GIT_TAG v2.4.11 +) +FetchContent_MakeAvailable(doctest) + +# Convenience function: one call per test source file +function(edfio_add_test name) + add_executable(${name} ${name}.cpp) + target_link_libraries(${name} PRIVATE edfio doctest::doctest) + target_compile_options(${name} PRIVATE + -Wall -Wextra -Wpedantic -Wno-c++98-compat -fsanitize=address,undefined + ) + target_link_options(${name} PRIVATE -fsanitize=address,undefined) + add_test(NAME ${name} COMMAND ${name}) +endfunction() + +# Copy test fixture +configure_file( + ${CMAKE_SOURCE_DIR}/Calib5.edf + ${CMAKE_CURRENT_BINARY_DIR}/Calib5.edf + COPYONLY +) + +# --- Test executables (added incrementally per phase) --- +edfio_add_test(test_record) +edfio_add_test(test_reader) +edfio_add_test(test_processor_sample) +edfio_add_test(test_iterators) +edfio_add_test(test_writer) +edfio_add_test(test_processor_utils) +``` + +### 0.3 Skeleton test files + +Create one `.cpp` per test target with a minimal doctest `main()` and a single `CHECK(true)`. These will be populated in later phases. + +**New files:** +- `C:/dev/code/edfio/tests/test_record.cpp` +- `C:/dev/code/edfio/tests/test_reader.cpp` +- `C:/dev/code/edfio/tests/test_processor_sample.cpp` +- `C:/dev/code/edfio/tests/test_iterators.cpp` +- `C:/dev/code/edfio/tests/test_writer.cpp` +- `C:/dev/code/edfio/tests/test_processor_utils.cpp` + +Each file follows this skeleton: + +```cpp +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include + +TEST_CASE("placeholder") { + CHECK(true); +} +``` + +### 0.4 Build verification + +``` +cmake -B build -G Ninja -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_STANDARD=23 +cmake --build build +ctest --test-dir build --output-on-failure +``` + +**Dependencies:** None. + +--- + +## Phase 1: Bug Fixes + +**Goal:** Fix every critical and high-severity bug identified during analysis, with regression tests proving each fix. Zero refactoring -- minimal, surgical patches only. + +### 1.1 CRITICAL -- `ProcessorSampleRecord` data corruption + +**File:** `C:/dev/code/edfio/include/edfio/processor/impl/ProcessorSampleRecord.ipp` + +**Bug A -- `unsigned char < 0` always false:** +The loop variable `r` is `unsigned char`. The test `r < 0` is always false, so sign-extension for negative samples (BDF 3-byte, EDF 2-byte) never triggers. `sample = -1` is dead code. + +**Bug B -- double `idx++`:** +`idx++` appears in the condition `if (r < 0 && idx++)` AND again at `idx++` at bottom of loop body. If the condition were ever true, `idx` would be incremented twice per iteration. + +**Current code (lines 16-32):** +```cpp +template +inline typename ProcessorSampleRecord::ProcType +ProcessorSampleRecord::operator()(Record record) +{ + DigiType sample = 0; + size_t idx = 0; + for (unsigned char r : record()) + { + if (r < 0 && idx++) // BUG: always false + sample = -1; + sample <<= 8; + sample |= r; + idx++; + } + + if (std::is_same::value) + return sample; + return impl::ConvertSample(m_offset, m_scaling, sample); +} +``` + +**Fixed code:** +The correct algorithm reads bytes little-endian (LSB first -- per EDF/BDF spec), assembles them, then sign-extends the result. The current code reads big-endian which is also wrong for EDF, but is consistent with how the rest of the codebase uses it (ProcessorSample writes big-endian too). So we preserve the byte order but fix sign extension: + +```cpp +template +inline typename ProcessorSampleRecord::ProcType +ProcessorSampleRecord::operator()(Record record) +{ + DigiType sample = 0; + auto const& bytes = record(); + std::size_t const nbytes = bytes.size(); + + // Assemble bytes (big-endian order as written by ProcessorSample) + for (std::size_t i = 0; i < nbytes; ++i) + { + sample <<= 8; + sample |= static_cast(bytes[i]); + } + + // Sign-extend: if high bit of the MSB is set, the value is negative + if (nbytes > 0 && nbytes < sizeof(DigiType)) + { + unsigned int sign_bit = 1u << (nbytes * 8 - 1); + if (sample & sign_bit) + sample |= ~((1 << (nbytes * 8)) - 1); + } + + if constexpr (std::is_same_v) + return sample; + return impl::ConvertSample(m_offset, m_scaling, sample); +} +``` + +**Test (in `test_processor_sample.cpp`):** +```cpp +TEST_CASE("ProcessorSampleRecord sign-extends negative 2-byte samples") { + // -100 as signed 16-bit = 0xFF9C + Record rec(2); + rec()[0] = static_cast(0xFF); + rec()[1] = static_cast(0x9C); + ProcessorSampleRecord proc(0.0, 1.0); + auto result = proc(rec); + CHECK(result == -100); +} + +TEST_CASE("ProcessorSampleRecord positive 2-byte samples") { + Record rec(2); + rec()[0] = static_cast(0x00); + rec()[1] = static_cast(0x64); + ProcessorSampleRecord proc(0.0, 1.0); + auto result = proc(rec); + CHECK(result == 100); +} + +TEST_CASE("ProcessorSampleRecord 3-byte BDF negative sample") { + // -1000 as signed 24-bit = 0xFFFC18 + Record rec(3); + rec()[0] = static_cast(0xFF); + rec()[1] = static_cast(0xFC); + rec()[2] = static_cast(0x18); + ProcessorSampleRecord proc(0.0, 1.0); + auto result = proc(rec); + CHECK(result == -1000); +} +``` + +--- + +### 1.2 CRITICAL -- `TalStore::prev()` out-of-bounds UB + +**File:** `C:/dev/code/edfio/include/edfio/store/TalStore.hpp` + +**Bug:** Line 235: `auto first = m_stream().rend() + off;` +`rend()` already points past-the-reverse-end. Adding a positive `off` goes further out of bounds -- undefined behavior. + +The intent is to iterate backward from position `off`. To go backwards from byte offset `off` in the underlying vector, we need `rbegin() + (size - off)`. + +**Current code (lines 228-258):** +```cpp +size_type prev(size_type off) +{ + if (off <= 0) + throw std::length_error("Iterator not decrementable"); + + if (off > 0) + { + auto first = m_stream().rend() + off; // BUG + auto last = m_stream().rend(); + // ... +``` + +**Fixed code:** +```cpp +size_type prev(size_type off) +{ + if (off == 0) + throw std::length_error("Iterator not decrementable"); + + auto const& data = m_stream(); + auto const sz = data.size(); + + // Walk backward from position (off - 1), skipping zeros + size_type pos = off; + while (pos > 0 && data[pos - 1] == 0) + --pos; + + if (pos == 0) + throw std::length_error("Iterator not decrementable"); + + // Find the start of the previous non-zero TAL + size_type end_of_tal = pos; + while (pos > 0 && data[pos - 1] != 0) + --pos; + + m_value.assign(data.begin() + pos, data.begin() + end_of_tal); + return pos; +} +``` + +**Test (in `test_iterators.cpp`):** Requires constructing a TalStore from a crafted SignalRecordStore, which is complex. Instead, test with a round-trip: iterate forward collecting TALs, then iterate backward and verify they appear in reverse order. Requires `Calib5.edf` to be an EDF+ file with annotations. If `Calib5.edf` is plain EDF (no annotations), write a unit test that creates a vector with known TAL structure and tests the `prev()` logic via a mock. A more practical approach: + +```cpp +TEST_CASE("TalStore reverse iteration does not crash") { + // This is an integration-level test using a real file. + // If Calib5.edf has no annotations, this test is skipped. + std::ifstream stream("Calib5.edf", std::ios::binary); + REQUIRE(stream.is_open()); + edfio::ReaderHeaderExam reader; + auto header = reader(stream); + // If no annotation signals, skip + bool has_annot = false; + for (auto const& sig : header.m_signals) { + if (sig.m_detail.m_isAnnotation) { has_annot = true; break; } + } + if (!has_annot) { MESSAGE("No annotations in Calib5.edf, skipping"); return; } + // ... full test using TalStore iterators +} +``` + +--- + +### 1.3 CRITICAL -- `RecordStore::iterator::operator-` inverted + +**File:** `C:/dev/code/edfio/include/edfio/store/RecordStore.hpp` + +**Bug (line 173):** +```cpp +difference_type operator-(iterator it) const +{ + return difference_type(it.m_offset - m_offset); // inverted! +} +``` + +For `a - b`, the standard requires `a.offset - b.offset`, but this returns `b.offset - a.offset`. + +**Fix:** +```cpp +difference_type operator-(iterator it) const +{ + if (!m_context) + throw std::invalid_argument("Invalid context"); + if (m_context != it.m_context) + throw std::invalid_argument("Iterators incompatible"); + return difference_type(m_offset - it.m_offset); +} +``` + +**Same bug in `RecordSink::iterator::operator-` (line 187):** + +**File:** `C:/dev/code/edfio/include/edfio/sink/RecordSink.hpp` + +```cpp +// Current (line 187): +return difference_type(it.m_offset - m_offset); +// Fixed: +return difference_type(m_offset - it.m_offset); +``` + +**Test (in `test_iterators.cpp`):** +```cpp +TEST_CASE("RecordStore iterator subtraction returns (a - b) not (b - a)") { + std::ifstream stream("Calib5.edf", std::ios::binary); + REQUIRE(stream.is_open()); + edfio::ReaderHeaderExam reader; + auto header = reader(stream); + auto store = edfio::detail::CreateDataRecordStore(stream, header.m_general); + auto a = store.begin() + 2; + auto b = store.begin(); + CHECK((a - b) == 2); + CHECK((b - a) == -2); +} +``` + +--- + +### 1.4 CRITICAL -- `Record` has `const size_t m_size` breaking assignment + +**File:** `C:/dev/code/edfio/include/edfio/core/Record.hpp` + +**Bug (line 64):** +```cpp +const size_t m_size; +``` + +`const` data member makes the implicit copy/move assignment operators deleted. The class has a copy constructor but relies on implicit assignment in many places (e.g., `m_value = ...` in Store::load paths, where `Record` is assigned into `m_value`). The `const` is doubly redundant because `m_size` should track `m_value.size()`. + +**Fix:** Remove `const` from `m_size`, add a proper assignment operator, or better yet, derive `m_size` from `m_value.size()` and remove the redundant member: + +```cpp +template +struct Record +{ + using ValueType = ValT; + using VectorType = std::vector; + + Record() = delete; + + Record(size_t recordSize) + : m_value(recordSize, 0) {} + + Record(typename VectorType::const_iterator first, + typename VectorType::const_iterator last) + : m_value(first, last) {} + + Record(const Record&) = default; + Record(Record&&) = default; + Record& operator=(const Record&) = default; + Record& operator=(Record&&) = default; + + size_t Size() const { return m_value.size(); } + const VectorType& operator()() const { return m_value; } + VectorType& operator()() { return m_value; } + + Record operator+(const Record& record) const + { + Record tmp(Size() + record.Size()); + std::copy(m_value.begin(), m_value.end(), tmp().begin()); + std::copy(record().begin(), record().end(), tmp().begin() + Size()); + return tmp; + } + + VectorType m_value; +}; +``` + +Note: `m_size` is removed entirely. `Size()` now returns `m_value.size()`. The stream operators (`operator>>`, `operator<<`) already call `record.resize(r.Size(), 0)` which will become a no-op now that the vector is already the right size. + +**Impact:** The `operator>>` and `operator<<` templates at bottom of Record.hpp call `record.resize(r.Size(), 0)`. After the fix, `r.Size()` returns `m_value.size()`, so `resize` is a no-op, which is correct. + +**Test (in `test_record.cpp`):** +```cpp +TEST_CASE("Record is assignable") { + edfio::Record a(10); + a()[0] = 'X'; + edfio::Record b(10); + b = a; // Must compile and work + CHECK(b()[0] == 'X'); + CHECK(b.Size() == 10); +} + +TEST_CASE("Record move-assignment works") { + edfio::Record a(5); + a()[0] = 42; + edfio::Record b(5); + b = std::move(a); + CHECK(b()[0] == 42); +} + +TEST_CASE("Record concatenation") { + edfio::Record a(3); + edfio::Record b(2); + a()[0] = 1; a()[1] = 2; a()[2] = 3; + b()[0] = 4; b()[1] = 5; + auto c = a + b; + CHECK(c.Size() == 5); + CHECK(c()[3] == 4); +} +``` + +--- + +### 1.5 HIGH -- `const_cast` in 12 places to fake const-correctness + +**Files:** +- `C:/dev/code/edfio/include/edfio/store/RecordStore.hpp` (lines 222, 226, 234, 238) +- `C:/dev/code/edfio/include/edfio/store/TalStore.hpp` (lines 142, 146, 154, 158) +- `C:/dev/code/edfio/include/edfio/sink/RecordSink.hpp` (lines 227, 231, 239, 243) + +**Bug:** The `const` begin/end/cbegin/cend methods cast away `const` on `this` to construct iterators. This is technically UB if the object is actually const, and defeats the purpose of const-correctness. + +**Root cause:** The iterator stores a raw pointer to the non-const Store/Sink. The `const_iterator` is just `typedef iterator const`, which means `const iterator` -- a const copy of a mutable iterator, NOT a true const_iterator. + +**Fix (Phase 1 -- minimal, safe):** Make `const_iterator` a proper alias and have iterators store a `const` pointer when needed. However, the minimal fix is to acknowledge the library is not used in truly const contexts and mark this for Phase 4 (iterator overhaul with deducing this). For Phase 1, we fix the UB by making the `const` overloads non-const (or by making the iterator's pointer member mutable). The pragmatic fix: + +Replace `typedef iterator const const_iterator;` with a true `const_iterator` class. But that is a large change. Phase 1 minimal: change to `using const_iterator = iterator;` (drop the `const`), remove the `const_cast`, and accept that `begin() const` just returns a mutable iterator. This is no worse than the current const_cast UB. + +Actually, the cleanest Phase 1 fix: store `m_context` as `const`-compatible. The load/getR/getP methods need mutable access. Use `mutable` on the cache fields (`m_value`) in RecordStore and TalStore. Then the iterator can hold a `const Store*` and call const methods that mutate only mutable cache fields. + +This is substantial but safe. **Defer to Phase 4** (iterator overhaul with deducing this) -- mark const_cast sites with `// FIXME(Phase4): const_cast removed by deducing-this iterator redesign`. + +For Phase 1, add the `// FIXME` comments but do NOT change behavior. The const_cast pattern is technically UB only if the underlying object is truly const, which never happens in this codebase. + +--- + +### 1.6 HIGH -- Iterators don't satisfy `std::random_access_iterator` concept + +**Files:** +- `C:/dev/code/edfio/include/edfio/core/Device.hpp` (iterator base class) +- `C:/dev/code/edfio/include/edfio/store/RecordStore.hpp` +- `C:/dev/code/edfio/include/edfio/sink/RecordSink.hpp` + +**Issues:** +1. No `iterator_concept` type alias (required for C++20+ iterator concepts). +2. `operator-` returns `b - a` instead of `a - b` (fixed in 1.3). +3. No `n + it` form (only `it + n`). +4. `size_type` is `unsigned long long` but `difference_type` is `long long` -- `operator+` / `operator-` take `size_type` (unsigned) but should take `difference_type` (signed) per the standard. + +**Fix (Phase 1 -- minimal):** + +In `Device.hpp` iterator, add: +```cpp +using iterator_concept = IterCategory; +``` + +In `RecordStore::iterator`, add friend free function: +```cpp +friend iterator operator+(size_type off, const iterator& it) +{ + return it + off; +} +``` + +Change `operator+=(size_type off)` to `operator+=(difference_type off)` (and correspondingly for `operator-=`, `operator+`, `operator-` scalar overloads). This is a **Phase 4** task due to cascading changes. Phase 1 adds only the `iterator_concept` alias and the `n + it` overload. + +**Defer signed/unsigned parameter changes to Phase 4.** + +--- + +### 1.7 HIGH -- `catch(std::exception e)` by value in 5 .ipp files + +**Files (5 locations):** +1. `C:/dev/code/edfio/include/edfio/reader/impl/ReaderHeaderGeneral.ipp` line 43 +2. `C:/dev/code/edfio/include/edfio/reader/impl/ReaderHeaderSignal.ipp` line 55 +3. `C:/dev/code/edfio/include/edfio/writer/impl/WriterHeaderGeneral.ipp` line 44 +4. `C:/dev/code/edfio/include/edfio/writer/impl/WriterHeaderSignals.ipp` line 53 +5. `C:/dev/code/edfio/include/edfio/writer/impl/WriterRecord.ipp` line 29 + +**Bug:** Catching by value slices derived exception types. + +**Fix:** Change all to `catch (const std::exception& e)`: + +```cpp +// Before: +catch (std::exception e) +// After: +catch (const std::exception&) +``` + +(The `e` variable is unused in all cases -- the handlers immediately throw a new exception.) + +**Test:** No specific test needed; this is a correctness fix verified by compiler warnings with `-Wcatch-value`. + +--- + +### 1.8 HIGH -- `off < 0` on unsigned in load() methods + +**Files:** +1. `C:/dev/code/edfio/include/edfio/store/DatarecordStore.hpp` line 37 +2. `C:/dev/code/edfio/include/edfio/include/edfio/store/SignalrecordStore.hpp` line 40 +3. `C:/dev/code/edfio/include/edfio/store/SignalSampleStore.hpp` line 43 +4. `C:/dev/code/edfio/include/edfio/store/TimeStampStore.hpp` line 40 + +**Bug:** `size_type` is `unsigned long long`. The check `off < 0` is always false. + +**Fix:** Remove the dead `off < 0` check. The `off >= size()` check is sufficient because `off` is unsigned and comes from iterator arithmetic that already bounds-checks. + +```cpp +// Before: +if (off < 0 || off >= size()) +// After: +if (off >= size()) +``` + +--- + +### Phase 1 summary of file changes + +| File | Changes | +|------|---------| +| `processor/impl/ProcessorSampleRecord.ipp` | Rewrite sample assembly + sign extension | +| `store/TalStore.hpp` | Rewrite `prev()` method | +| `store/RecordStore.hpp` | Fix `operator-` sign, add FIXME for const_cast | +| `sink/RecordSink.hpp` | Fix `operator-` sign, add FIXME for const_cast | +| `core/Record.hpp` | Remove `const` from `m_size`, or remove `m_size` entirely | +| `core/Device.hpp` | Add `iterator_concept` alias | +| `reader/impl/ReaderHeaderGeneral.ipp` | `catch(std::exception e)` -> `catch(const std::exception&)` | +| `reader/impl/ReaderHeaderSignal.ipp` | Same | +| `writer/impl/WriterHeaderGeneral.ipp` | Same | +| `writer/impl/WriterHeaderSignals.ipp` | Same | +| `writer/impl/WriterRecord.ipp` | Same | +| `store/DatarecordStore.hpp` | Remove `off < 0` | +| `store/SignalrecordStore.hpp` | Remove `off < 0` | +| `store/SignalSampleStore.hpp` | Remove `off < 0` | +| `store/TimeStampStore.hpp` | Remove `off < 0` | + +**Dependencies:** Phase 0 must be complete (CMake builds, tests compile). + +--- + +## Phase 2: Low-Risk Modernization + +**Goal:** Apply safe, mechanical transformations that improve code quality without altering semantics. Every change is individually small and independently testable by compilation + existing Phase 1 tests. + +### 2.1 Remove `return std::move(local)` -- NRVO prevention (21 instances) + +**Files and lines:** +1. `header/HeaderUtils.hpp` line 62: `return std::move(header);` in `CreateHeaderGeneral` +2. `header/HeaderUtils.hpp` line 89: `return std::move(CreateHeaderGeneral(...));` -- both the outer move and the nested move +3. `header/HeaderUtils.hpp` line 104: `return std::move(header);` in `CreateHeaderGeneralPlus` +4. `header/HeaderUtils.hpp` line 140: `return std::move(signal);` in `CreateHeaderSignal` +5. `store/detail/StoreUtils.hpp` line 33: `return std::move(DataRecordStore{...});` +6. `store/detail/StoreUtils.hpp` line 44: `return std::move(SignalRecordStore{...});` +7. `store/detail/StoreUtils.hpp` line 56: `return std::move(SignalSampleStore{...});` +8. `store/detail/StoreUtils.hpp` line 67: `return std::move(TimeStampStore{...});` +9. `sink/detail/SinkUtils.hpp` line 31: `return std::move(DataRecordSink{...});` +10. `sink/detail/SinkUtils.hpp` line 42: `return std::move(SignalRecordSink{...});` +11. `reader/impl/ReaderHeaderGeneral.ipp` line 47: `return std::move(hdr);` +12. `reader/impl/ReaderHeaderSignal.ipp` line 59: `return std::move(signals);` +13. `reader/impl/ReaderHeaderExam.ipp` lines 33, 40, 44, 62: Four `std::move` wrapping return values +14. `writer/impl/WriterHeaderExam.ipp` lines 28, 31: Two `std::move` on temporaries passed to functions (these are actually fine since they are function arguments, not return values -- leave alone) +15. `processor/impl/ProcessorHeaderExam.ipp` line 45: `return std::move(HeaderExam{...});` +16. `processor/impl/ProcessorHeaderGeneral.ipp` line 162: `return std::move(out);` +17. `processor/impl/ProcessorHeaderSignalFields.ipp` line 326: `return std::move(signals);` +18. `processor/impl/ProcessorHeaderSignal.ipp` line 98: `return std::move(out);` +19. `processor/impl/ProcessorTalRecord.ipp` line 96: `return std::move(out);` +20. `processor/impl/ProcessorTimeStampRecord.ipp` line 56: `return std::move(timestamp);` +21. `processor/impl/ProcessorSample.ipp` line 33: `return std::move(record);` + +**Fix:** For every `return std::move(x);` where `x` is a local variable or temporary, change to `return x;`. + +Example: +```cpp +// Before: +return std::move(header); +// After: +return header; +``` + +For `auto header = std::move(CreateHeaderGeneral(...));` change to `auto header = CreateHeaderGeneral(...);`. + +For `auto general = std::move(procGeneralFields(std::move(generalFields)));` in ReaderHeaderExam.ipp -- the inner `std::move(generalFields)` is correct (moving into a by-value parameter), but the outer `std::move()` around the return value assignment is fine to keep (assigning to a local, not returning). Actually, `auto general = std::move(procGeneralFields(...))` prevents copy elision from the function return. Change to `auto general = procGeneralFields(std::move(generalFields));`. + +--- + +### 2.2 Replace `typedef` with `using` (all files) + +**Scope:** Every file using `typedef`. This is a mechanical find-and-replace. + +```cpp +// Before: +typedef iterator const const_iterator; +typedef std::reverse_iterator reverse_iterator; +typedef std::reverse_iterator const_reverse_iterator; +typedef Store<...> store_type; +typedef device_type::iterator iterator; + +// After: +using const_iterator = iterator; // (also fixing the const issue noted in 1.5) +using reverse_iterator = std::reverse_iterator; +using const_reverse_iterator = std::reverse_iterator; +using store_type = Store<...>; +using iterator = typename device_type::iterator; +``` + +**Files:** +- `core/Device.hpp` (6 typedefs) +- `store/Store.hpp` (3 typedefs) +- `sink/Sink.hpp` (2 typedefs) +- `store/RecordStore.hpp` (3 typedefs) +- `store/TalStore.hpp` (4 typedefs) +- `sink/RecordSink.hpp` (3 typedefs) +- `store/DatarecordStore.hpp` (4 typedefs) +- `store/SignalrecordStore.hpp` (4 typedefs) +- `store/SignalSampleStore.hpp` (4 typedefs) +- `store/TimeStampStore.hpp` (4 typedefs) +- `sink/DataRecordSink.hpp` (4 typedefs) +- `sink/SignalRecordSink.hpp` (4 typedefs) + +--- + +### 2.3 Replace `static` functions in headers with `inline` + +**Files with `static` functions that should be `inline`:** +- `core/DataFormat.hpp`: `IsPlus`, `IsEdf`, `IsBdf`, `GetSampleBytes` (4 functions) +- `Utils.hpp`: `GetError` (1 function) +- `processor/detail/ProcessorUtils.hpp`: all `static` functions in `impl::` and `detail::` namespaces (9 functions) +- `header/HeaderUtils.hpp`: `CreateHeaderGeneral`, `CreateHeaderGeneralPlus`, `CreateHeaderSignal` (3 functions) +- `store/detail/StoreUtils.hpp`: 4 template functions (already have `static` but templates are implicitly inline, so just remove `static`) +- `sink/detail/SinkUtils.hpp`: 2 template functions (same) + +**Fix pattern:** +```cpp +// Before: +static const bool IsPlus(DataFormat format) { ... } +// After: +inline constexpr bool IsPlus(DataFormat format) { ... } +``` + +For `GetError` in Utils.hpp: +```cpp +// Before: +static const char* GetError(FileErrc err) +// After: +inline constexpr const char* GetError(FileErrc err) +``` + +For non-template `static` functions in ProcessorUtils.hpp (e.g., `ReduceString`, `GetFormatName`, `GetMonthFromString`, `GetStringFromMonth`, `to_string_decimal`): +```cpp +// Before: +static std::string ReduceString(const std::string &value) +// After: +inline std::string ReduceString(const std::string &value) +``` + +For `ADDITIONAL_SEPARATOR`: +```cpp +// Before: +static const char ADDITIONAL_SEPARATOR = '|'; +// After: +inline constexpr char ADDITIONAL_SEPARATOR = '|'; +``` + +For `Config.hpp`: +```cpp +// Before: +static constexpr ProcessorErrorCheck PROCESSOR_ERROR_CHECKING = ... +// After: +inline constexpr ProcessorErrorCheck PROCESSOR_ERROR_CHECKING = ... +``` + +--- + +### 2.4 Fix `std::regex` performance in `ReduceString` + +**File:** `C:/dev/code/edfio/include/edfio/processor/detail/ProcessorUtils.hpp` line 104-107 + +**Bug:** `std::regex("^ +| +$|( ) +")` is constructed on every call. std::regex construction is extremely expensive (up to 10ms per call). This function is called 12 times per header parse (see ProcessorHeaderGeneralFields.ipp lines 345-356 and ProcessorHeaderSignalFields.ipp lines 317-323). + +**Fix:** Replace with a simple string trim + collapse function: +```cpp +inline std::string ReduceString(const std::string& value) +{ + // Trim leading spaces + auto start = value.find_first_not_of(' '); + if (start == std::string::npos) return ""; + // Trim trailing spaces + auto end = value.find_last_not_of(' '); + // Collapse interior runs of spaces to single space + std::string result; + result.reserve(end - start + 1); + bool prev_space = false; + for (size_t i = start; i <= end; ++i) { + if (value[i] == ' ') { + if (!prev_space) { + result += ' '; + prev_space = true; + } + } else { + result += value[i]; + prev_space = false; + } + } + return result; +} +``` + +**Test (in `test_processor_utils.cpp`):** +```cpp +TEST_CASE("ReduceString trims and collapses spaces") { + using edfio::detail::ReduceString; + CHECK(ReduceString(" hello world ") == "hello world"); + CHECK(ReduceString(" ") == ""); + CHECK(ReduceString("no_change") == "no_change"); + CHECK(ReduceString(" a b c ") == "a b c"); + CHECK(ReduceString("") == ""); +} +``` + +--- + +### 2.5 Fix typo `m_dararecord` -> `m_datarecord` + +**Files:** +- `C:/dev/code/edfio/include/edfio/core/Annotation.hpp` line 27: `long long m_dararecord = 0;` +- `C:/dev/code/edfio/include/edfio/processor/impl/ProcessorTimeStampRecord.ipp` line 24: `timestamp.m_dararecord = datarecord;` +- `C:/dev/code/edfio/include/edfio/processor/impl/ProcessorTalRecord.ipp` line 87: `annot.m_dararecord = datarecord;` + +**Fix:** Rename to `m_datarecord` in all three locations. + +--- + +### 2.6 Fix double semicolons + +**File:** `C:/dev/code/edfio/include/edfio/sink/SignalRecordSink.hpp` +- Line 47: `size_type offset = (sz - m_headerOffset) % m_datarecordSize;;` +- Line 61: `size_type offset = (sz - m_headerOffset) % m_datarecordSize;;` + +**Fix:** Remove the duplicate semicolons. + +--- + +### 2.7 Make `DataFormat` functions `constexpr` + +**File:** `C:/dev/code/edfio/include/edfio/core/DataFormat.hpp` + +Make `IsPlus`, `IsEdf`, `IsBdf`, `GetSampleBytes` all `inline constexpr`. (Overlaps with 2.3.) + +--- + +### 2.8 Make `GetError` `constexpr` + +**File:** `C:/dev/code/edfio/include/edfio/Utils.hpp` + +Change `GetError` to `inline constexpr`. It only uses `if/else` chains returning string literals, which is valid constexpr in C++23. + +--- + +### 2.9 Fix `GetStringFromMonth` unsigned underflow + +**File:** `C:/dev/code/edfio/include/edfio/processor/detail/ProcessorUtils.hpp` lines 95-102 + +**Bug:** Parameter is `size_t idx`. Line 97: `idx--`. If `idx == 0`, this wraps to `SIZE_MAX`. The subsequent `idx >= 0` check (line 99) is always true because `idx` is unsigned. + +**Fix:** +```cpp +inline std::string GetStringFromMonth(size_t idx) +{ + static const std::vector months = { + "JAN", "FEB", "MAR", "APR", "MAY", "JUN", + "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" + }; + if (idx >= 1 && idx <= 12) + return months[idx - 1]; + return "JAN"; +} +``` + +**Test (in `test_processor_utils.cpp`):** +```cpp +TEST_CASE("GetStringFromMonth handles boundaries") { + CHECK(edfio::detail::GetStringFromMonth(0) == "JAN"); // was UB + CHECK(edfio::detail::GetStringFromMonth(1) == "JAN"); + CHECK(edfio::detail::GetStringFromMonth(12) == "DEC"); + CHECK(edfio::detail::GetStringFromMonth(13) == "JAN"); +} +``` + +--- + +### 2.10 Remove `` include + +**File:** `C:/dev/code/edfio/include/edfio/processor/detail/ProcessorUtils.hpp` + +After replacing `ReduceString` in 2.4, remove `#include `. This significantly improves compilation time. + +--- + +### Phase 2 summary + +All changes are mechanical. No architectural changes. Tests from Phase 1 serve as regression. Additional tests in `test_processor_utils.cpp` for ReduceString and GetStringFromMonth. + +**Dependencies:** Phase 1 complete. All Phase 1 tests passing. + +--- + +## Phase 3: Core C++23 Refactoring + +**Goal:** Rewrite internal implementations using C++23 features for clarity and safety. Public API signatures may change. Every file touched gets updated tests. + +### 3.1 `if constexpr` replaces SFINAE in ProcessorUtils + +**File:** `C:/dev/code/edfio/include/edfio/processor/detail/ProcessorUtils.hpp` + +**Current:** 4 overloads of `CheckFormatErrors` using `std::enable_if`: + +```cpp +template +static bool CheckFormatErrors(const typename std::enable_if>::type &str) { ... } +// ... 3 more overloads +``` + +**Replace with:** +```cpp +namespace impl { + template + inline bool CheckFormatErrors(const Container& data) + { + if constexpr (Check == ProcessorErrorCheck::Permissive) { + return false; + } else { + for (auto c : data) { + if (!std::isprint(static_cast(c))) + return true; + } + return false; + } + } +} + +namespace detail { + template + inline bool CheckFormatErrors(const Container& data) + { + return impl::CheckFormatErrors(data); + } +} +``` + +This replaces 4 overloads with 2 function templates. + +**Also** replace `std::is_same::value` in ProcessorSampleRecord.ipp and ProcessorSample.ipp with `if constexpr (std::is_same_v<...>)` (already done in Phase 1 fix for ProcessorSampleRecord). + +--- + +### 3.2 Spaceship operator (`<=>`) for iterators + +**Files:** +- `C:/dev/code/edfio/include/edfio/store/RecordStore.hpp` +- `C:/dev/code/edfio/include/edfio/store/TalStore.hpp` +- `C:/dev/code/edfio/include/edfio/sink/RecordSink.hpp` + +**Current:** Each iterator class has 6 comparison operators (`==`, `!=`, `<`, `>`, `<=`, `>=`), totalling ~120 lines across 3 files. + +**Replace with:** +```cpp +// In RecordStore::iterator: +bool operator==(const iterator& it) const +{ + return m_offset == it.m_offset && m_context == it.m_context; +} + +std::strong_ordering operator<=>(const iterator& it) const +{ + if (m_context != it.m_context) + throw std::invalid_argument("Iterators incompatible"); + return m_offset <=> it.m_offset; +} +``` + +This replaces 6 operators with 2, and `!=`, `<`, `>`, `<=`, `>=` are all synthesized by the compiler. Saves ~80 lines total. + +**Include `` in the relevant files.** + +--- + +### 3.3 `std::format` replaces `ostringstream` formatting + +**File:** `C:/dev/code/edfio/include/edfio/processor/impl/ProcessorHeaderGeneral.ipp` + +**Current (lines 53-57):** +```cpp +std::ostringstream oss; +oss << std::setw(2) << std::setfill('0') << day << "."; +oss << std::setw(2) << std::setfill('0') << month << "."; +oss << std::setw(2) << std::setfill('0') << year; +out.m_startDate(oss.str()); +``` + +**Replace with:** +```cpp +out.m_startDate(std::format("{:02d}.{:02d}.{:02d}", day, month, year)); +``` + +Same for start time (lines 64-68): +```cpp +out.m_startTime(std::format("{:02d}.{:02d}.{:02d}", hour, minute, second)); +``` + +And in the "Plus" Recording field (line 137): +```cpp +auto dateStr = std::format("{:02d}-{}-{}", day ? day : 1, + detail::GetStringFromMonth(month), + year ? year : 1984); +fields.push_back(dateStr); +``` + +Remove `#include ` and `#include `, add `#include `. + +--- + +### 3.4 `std::from_chars` replaces `stoi` / `stod` + try/catch + +**Files:** +- `processor/impl/ProcessorHeaderGeneralFields.ipp` (6 uses of stoi/stoll/stod) +- `processor/impl/ProcessorHeaderSignalFields.ipp` (5 uses of stoi/stod) +- `processor/impl/ProcessorTalRecord.ipp` (2 uses of stod) + +**Pattern:** +```cpp +// Before: +try { + int day = std::stoi(startdate); +} catch (...) { + throw std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); +} + +// After: +int day = 0; +auto [ptr, ec] = std::from_chars(startdate.data(), startdate.data() + startdate.size(), day); +if (ec != std::errc{}) + throw std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); +``` + +**Note:** `std::from_chars` for `double` has full support in C++23 / clang 21. Include ``. + +--- + +### 3.5 `std::string_view` for read-only string parameters + +**Files:** +- `processor/detail/ProcessorUtils.hpp`: `ReduceString(const std::string&)` -> `ReduceString(std::string_view)` +- `processor/detail/ProcessorUtils.hpp`: `GetMonthFromString(const std::string&)` -> `GetMonthFromString(std::string_view)` +- `processor/detail/ProcessorUtils.hpp`: `GetFormatName(DataFormat)` already returns string (keep as is) + +**Example:** +```cpp +inline int GetMonthFromString(std::string_view str) { + static constexpr std::array months = { + "JAN"sv, "FEB"sv, "MAR"sv, "APR"sv, "MAY"sv, "JUN"sv, + "JUL"sv, "AUG"sv, "SEP"sv, "OCT"sv, "NOV"sv, "DEC"sv + }; + for (size_t i = 0; i < months.size(); ++i) { + if (str == months[i]) return static_cast(i + 1); + } + return 0; +} +``` + +--- + +### 3.6 `std::expected` for error handling (optional / future API) + +**Scope:** This is a significant API change. Add `std::expected` returning overloads alongside the existing throw-based ones. Do NOT remove the throwing versions (that would break users). + +**Files:** +- `reader/ReaderHeaderExam.hpp` / `.ipp` +- `reader/ReaderHeaderGeneral.hpp` / `.ipp` +- `reader/ReaderHeaderSignal.hpp` / `.ipp` + +**Example new API:** +```cpp +struct ReaderHeaderExam : Reader { + // Existing (throws): + HeaderExam operator()(Stream& stream); + // New (returns expected): + std::expected try_read(Stream& stream) noexcept; +}; +``` + +**Implementation:** Wrap the existing call in a try/catch that maps exceptions to `std::unexpected(FileErrc::...)`. + +This is a **non-breaking additive change**. Existing code continues to work. + +**Defer implementation details to Phase 4** if time-constrained. The key Phase 3 deliverable is defining the `try_read` API shape. + +--- + +### 3.7 `std::optional` replaces `-1` sentinel in RecordSink + +**File:** `C:/dev/code/edfio/include/edfio/sink/RecordSink.hpp` + +**Bug:** `size_type m_offset = -1;` where `size_type` is `unsigned long long`. This means `m_offset` is `ULLONG_MAX` when used as "end" sentinel. Multiple comparisons like `m_offset == -1` rely on implicit conversion. + +**Fix:** Use `std::optional`: +```cpp +std::optional m_offset; // nullopt = end +``` + +Update all checks: +```cpp +// Before: +if (m_offset == -1) +// After: +if (!m_offset) +``` + +And the save call: +```cpp +// Before: +m_context->save(m_offset, std::move(value)); +// After: +m_context->save(m_offset.value_or(size_type(-1)), std::move(value)); +``` + +This change cascades into `DataRecordSink.hpp` and `SignalRecordSink.hpp` where `save()` checks `off == -1`. + +--- + +### Phase 3 summary + +| File | C++23 Feature | +|------|---------------| +| `processor/detail/ProcessorUtils.hpp` | `if constexpr`, `string_view`, `constexpr` arrays | +| `store/RecordStore.hpp` | `<=>` spaceship | +| `store/TalStore.hpp` | `<=>` spaceship | +| `sink/RecordSink.hpp` | `<=>` spaceship, `std::optional` | +| `processor/impl/ProcessorHeaderGeneral.ipp` | `std::format` | +| `processor/impl/ProcessorHeaderGeneralFields.ipp` | `std::from_chars` | +| `processor/impl/ProcessorHeaderSignalFields.ipp` | `std::from_chars` | +| `processor/impl/ProcessorTalRecord.ipp` | `std::from_chars` | +| `reader/*.hpp` | `std::expected` API addition | + +**Dependencies:** Phase 2 complete. All Phase 1+2 tests passing. + +--- + +## Phase 4: Architecture -- Ranges, Iterator Overhaul, Coroutines + +**Goal:** Make Store/Sink types satisfy the standard ranges concepts. Redesign iterators using C++23 deducing this. Optionally add a coroutine generator for TalStore. + +### 4.1 Deducing this eliminates const_cast (12 sites) + +**Files:** +- `store/RecordStore.hpp` +- `store/TalStore.hpp` +- `sink/RecordSink.hpp` + +**Concept:** C++23 deducing this allows a single function template to handle both const and non-const overloads: + +```cpp +// Before (6 functions per class: begin, begin const, cbegin const, end, ...): +iterator begin() { return iterator(this); } +const_iterator begin() const { return const_iterator(const_cast(this)); } + +// After (deducing this): +auto begin(this auto& self) +{ + return iterator(&self); +} +auto cbegin(this const auto& self) +{ + return const_iterator(&self); +} +``` + +For this to work, the iterator class must be templated on pointer constness, or the Store's cached `m_value` must be `mutable`. The cleanest approach: + +1. Make `m_value` (and `m_bufferPos`, `m_buffer`) `mutable` in RecordStore, TalStore, and derived classes. This is semantically correct: the cache is an implementation detail, and dereferencing a const iterator from a const Store should still work (the file read is a side effect hidden behind the cache). + +2. Make `getR()` / `getP()` / `load()` const (now possible because members are mutable). + +3. Store a `const RecordStore*` (or `const TalStore*`) in the iterator. + +4. Replace all 12 const_cast sites with direct `this`. + +5. Use deducing this for `begin`/`end`: + +```cpp +auto begin(this auto& self) -> iterator { return iterator(&self, 0); } +auto end(this auto& self) -> iterator { return iterator(&self, self.size()); } +auto cbegin(this const auto& self) -> iterator { return iterator(&self, 0); } +auto cend(this const auto& self) -> iterator { return iterator(&self, self.size()); } +``` + +This removes 12 const_cast uses and ~36 redundant function definitions. + +--- + +### 4.2 Ranges compliance for RecordStore + +**Goal:** `RecordStore` (and derived types) should model `std::ranges::random_access_range` and `std::ranges::sized_range`. + +**Requirements:** +1. `begin()` and `end()` must return iterators satisfying `std::random_access_iterator` +2. `size()` must return a value convertible to `std::ranges::range_size_t` +3. Iterator `difference_type` must be signed +4. Iterator `operator-(it, it)` must return `difference_type` +5. `operator+(n, it)` must be provided (free function) +6. All arithmetic operators must use `difference_type`, not `size_type` + +**Changes to `Device.hpp`:** +```cpp +template +class Device +{ +public: + using stream_type = Stream; + using device_type = Device; + using value_type = Value; + using pointer = Pointer; + using reference = Reference; + using difference_type = std::ptrdiff_t; // was long long + using size_type = std::size_t; // was unsigned long long + + struct iterator + { + using iterator_concept = IterCategory; + using difference_type = Device::difference_type; + using value_type = Device::value_type; + using reference = Device::reference; + using pointer = Device::pointer; + }; + // ... +}; +``` + +**Changes to `RecordStore::iterator`:** +- Change all `size_type` parameters in operators to `difference_type` +- Add `friend iterator operator+(difference_type n, const iterator& it) { return it + n; }` +- Verify `std::random_access_iterator` at compile time: + +```cpp +static_assert(std::random_access_iterator); +``` + +**Changes to `RecordStore` itself:** +- Add `begin()` / `end()` returning proper iterators (done in 4.1) +- Verify: `static_assert(std::ranges::random_access_range);` + +**Test (in `test_iterators.cpp`):** +```cpp +TEST_CASE("DataRecordStore models random_access_range") { + std::ifstream stream("Calib5.edf", std::ios::binary); + REQUIRE(stream.is_open()); + edfio::ReaderHeaderExam reader; + auto header = reader(stream); + auto store = edfio::detail::CreateDataRecordStore(stream, header.m_general); + + // Use ranges algorithms + auto count = std::ranges::distance(store); + CHECK(count == header.m_general.m_datarecordsFile); + + // Random access + auto it = std::ranges::begin(store); + auto rec = *(it + 0); + CHECK(rec.Size() > 0); +} +``` + +--- + +### 4.3 TalStore as a `std::ranges::view` with optional coroutine generator + +**Current:** TalStore is a bidirectional iterator over TAL strings embedded in annotation signals. The `next()`/`prev()` methods manually parse through a byte vector. + +**Option A -- Coroutine Generator:** +```cpp +#include + +std::generator> tals(const std::vector& data) +{ + size_t pos = 0; + while (pos < data.size()) + { + // Skip zeros + while (pos < data.size() && data[pos] == 0) + ++pos; + if (pos >= data.size()) + break; + // Find end of TAL + size_t start = pos; + while (pos < data.size() && data[pos] != 0) + ++pos; + co_yield std::vector(data.begin() + start, data.begin() + pos); + } +} +``` + +This is a forward-only range. TalStore currently claims bidirectional, but the `prev()` implementation is broken (fixed in Phase 1). If bidirectional is not actually needed by users, replace with the generator. If bidirectional is needed, keep the fixed `prev()` and add the generator as a convenience view. + +**Option B -- Ranges-based lazy view (no coroutine):** +Use `std::views::split` or a custom view to split the byte vector on null bytes and filter empties: + +```cpp +auto tals_view(const std::vector& data) +{ + return data + | std::views::split('\0') + | std::views::filter([](auto&& sub) { return !std::ranges::empty(sub); }) + | std::views::transform([](auto&& sub) { + return std::vector(std::ranges::begin(sub), std::ranges::end(sub)); + }); +} +``` + +**Recommendation:** Implement Option B as the primary API (composable, no overhead). Provide Option A as an alternative `tals_generator()` free function for users who prefer coroutine style. + +--- + +### 4.4 `std::span` for non-owning buffer views + +**Where applicable:** +- `ProcessorSampleRecord::operator()` takes `Record` by value. Change to `std::span`: +```cpp +ProcType operator()(std::span bytes); +``` +- Similarly for `ProcessorTalRecord::operator()` which takes `std::vector` by value. + +**Impact:** These are processor function objects used internally. The Store dereferences to `Record const&`, so passing a span is trivial: `proc(std::span(record()))`. + +--- + +### 4.5 Concepts for Device template hierarchy + +**Current:** The `Device` base class takes 5 template parameters: `Value, Pointer, Reference, Stream, IterCategory`. This is the CRTP/inheritance chain: `Device -> Store -> RecordStore -> DataRecordStore`. + +**Replace with concepts:** +```cpp +template +concept Streamable = requires(T& t) { + { t.good() } -> std::convertible_to; + { t.clear() }; +}; + +template +concept ReadableStream = Streamable && requires(T& t, std::streamoff off) { + { t.seekg(off, std::ios::beg) }; + { t.tellg() } -> std::convertible_to; +}; + +template +concept WritableStream = Streamable && requires(T& t, std::streamoff off) { + { t.seekp(off, std::ios::beg) }; + { t.tellp() } -> std::convertible_to; +}; +``` + +Then `Device` becomes: +```cpp +template +class Device { + // Value, difference_type, size_type derived automatically + // No Pointer/Reference/IterCategory template params needed +}; +``` + +**This is a significant redesign.** The existing 5-param template works and is stable. Recommend implementing this as an optional modernization, not a hard requirement. If implemented, all Store and Sink classes need updating. + +--- + +### Phase 4 summary + +| Task | Risk | Lines Changed | Benefit | +|------|------|--------------|---------| +| 4.1 Deducing this | Medium | ~120 | Eliminates const_cast UB, -36 function defs | +| 4.2 Ranges compliance | Medium | ~100 | Standard algorithm compatibility | +| 4.3 TalStore view/generator | Low | ~60 | Cleaner TAL parsing | +| 4.4 std::span | Low | ~20 | Zero-copy buffer passing | +| 4.5 Concepts for Device | High | ~200 | Cleaner template hierarchy | + +**Dependencies:** Phase 3 complete. All prior tests passing. + +**Recommendation:** Implement 4.1 and 4.2 first (highest value). 4.3 and 4.4 are independent and can be done in parallel. 4.5 is optional. + +--- + +## Appendix A: Complete file inventory + +All paths relative to `C:/dev/code/edfio/include/edfio/`. + +| # | File | Phase 1 | Phase 2 | Phase 3 | Phase 4 | +|---|------|---------|---------|---------|---------| +| 1 | `Config.hpp` | | 2.3 `inline` | | | +| 2 | `Defs.hpp` | | | | | +| 3 | `Utils.hpp` | | 2.3, 2.8 | | | +| 4 | `EdfIO.hpp` | | | | | +| 5 | `core/Annotation.hpp` | | 2.5 typo | | | +| 6 | `core/DataFormat.hpp` | | 2.3, 2.7 | | | +| 7 | `core/Device.hpp` | 1.6 | 2.2 | | 4.2, 4.5 | +| 8 | `core/Field.hpp` | | | | | +| 9 | `core/Record.hpp` | 1.4 | | | | +| 10 | `core/SampleType.hpp` | | | | | +| 11 | `core/StreamIO.hpp` | | | | | +| 12 | `header/HeaderExam.hpp` | | | | | +| 13 | `header/HeaderGeneral.hpp` | | | | | +| 14 | `header/HeaderSignal.hpp` | | | | | +| 15 | `header/HeaderUtils.hpp` | | 2.1 | | | +| 16 | `processor/detail/ProcessorUtils.hpp` | | 2.1, 2.3, 2.4, 2.9, 2.10 | 3.1, 3.5 | | +| 17 | `processor/ProcessorAnnotation.hpp` | | | | | +| 18 | `processor/impl/ProcessorAnnotation.ipp` | | | | | +| 19 | `processor/ProcessorHeaderExam.hpp` | | | | | +| 20 | `processor/impl/ProcessorHeaderExam.ipp` | | 2.1 | | | +| 21 | `processor/ProcessorHeaderGeneral.hpp` | | | | | +| 22 | `processor/impl/ProcessorHeaderGeneral.ipp` | | 2.1 | 3.3 | | +| 23 | `processor/ProcessorHeaderGeneralFields.hpp` | | | | | +| 24 | `processor/impl/ProcessorHeaderGeneralFields.ipp` | | 2.1 | 3.4 | | +| 25 | `processor/ProcessorHeaderSignal.hpp` | | | | | +| 26 | `processor/impl/ProcessorHeaderSignal.ipp` | | 2.1 | | | +| 27 | `processor/ProcessorHeaderSignalFields.hpp` | | | | | +| 28 | `processor/impl/ProcessorHeaderSignalFields.ipp` | | | 3.4 | | +| 29 | `processor/ProcessorSample.hpp` | | | | 4.4 | +| 30 | `processor/impl/ProcessorSample.ipp` | | 2.1 | | 4.4 | +| 31 | `processor/ProcessorSampleRecord.hpp` | | | | 4.4 | +| 32 | `processor/impl/ProcessorSampleRecord.ipp` | 1.1 | | | 4.4 | +| 33 | `processor/ProcessorTalRecord.hpp` | | | | 4.4 | +| 34 | `processor/impl/ProcessorTalRecord.ipp` | | 2.1, 2.5 | 3.4 | 4.4 | +| 35 | `processor/ProcessorTimeStamp.hpp` | | | | | +| 36 | `processor/impl/ProcessorTimeStamp.ipp` | | | | | +| 37 | `processor/ProcessorTimeStampRecord.hpp` | | | | | +| 38 | `processor/impl/ProcessorTimeStampRecord.ipp` | | 2.1, 2.5 | | | +| 39 | `reader/ReaderHeaderExam.hpp` | | | 3.6 | | +| 40 | `reader/impl/ReaderHeaderExam.ipp` | | 2.1 | | | +| 41 | `reader/ReaderHeaderGeneral.hpp` | | | | | +| 42 | `reader/impl/ReaderHeaderGeneral.ipp` | 1.7 | 2.1 | | | +| 43 | `reader/ReaderHeaderSignal.hpp` | | | | | +| 44 | `reader/impl/ReaderHeaderSignal.ipp` | 1.7 | 2.1 | | | +| 45 | `sink/DataRecordSink.hpp` | | 2.2 | 3.7 | 4.1 | +| 46 | `sink/RecordSink.hpp` | 1.3 | 2.2 | 3.2, 3.7 | 4.1 | +| 47 | `sink/SignalRecordSink.hpp` | | 2.2, 2.6 | 3.7 | 4.1 | +| 48 | `sink/Sink.hpp` | | 2.2 | | | +| 49 | `sink/detail/SinkUtils.hpp` | | 2.1 | | | +| 50 | `store/DatarecordStore.hpp` | 1.8 | 2.2 | | 4.1 | +| 51 | `store/RecordStore.hpp` | 1.3 | 2.2 | 3.2 | 4.1, 4.2 | +| 52 | `store/SignalrecordStore.hpp` | 1.8 | 2.2 | | 4.1 | +| 53 | `store/SignalSampleStore.hpp` | 1.8 | 2.2 | | 4.1 | +| 54 | `store/Store.hpp` | | 2.2 | | | +| 55 | `store/TalStore.hpp` | 1.2 | 2.2 | 3.2 | 4.1, 4.3 | +| 56 | `store/TimeStampStore.hpp` | 1.8 | 2.2 | | 4.1 | +| 57 | `store/detail/StoreUtils.hpp` | | 2.1 | | | +| 58 | `writer/WriterHeaderExam.hpp` | | | | | +| 59 | `writer/impl/WriterHeaderExam.ipp` | | 2.1 | | | +| 60 | `writer/WriterHeaderGeneral.hpp` | | | | | +| 61 | `writer/impl/WriterHeaderGeneral.ipp` | 1.7 | | | | +| 62 | `writer/WriterHeaderSignals.hpp` | | | | | +| 63 | `writer/impl/WriterHeaderSignals.ipp` | 1.7 | | | | +| 64 | `writer/WriterRecord.hpp` | | | | | +| 65 | `writer/impl/WriterRecord.ipp` | 1.7 | | | | + +--- + +## Appendix B: Test matrix + +| Test File | Covers | Phase | +|-----------|--------|-------| +| `test_record.cpp` | Record assign/move/concat, Size() | 1 | +| `test_reader.cpp` | Header reading from Calib5.edf, round-trip | 1, 2 | +| `test_processor_sample.cpp` | ProcessorSampleRecord sign extension, round-trip with ProcessorSample | 1 | +| `test_iterators.cpp` | RecordStore operator-, DataRecordStore iteration, ranges compliance, TalStore prev | 1, 4 | +| `test_writer.cpp` | Header write round-trip, Record write | 1, 2 | +| `test_processor_utils.cpp` | ReduceString, GetStringFromMonth, CheckFormatErrors, from_chars parsing | 2, 3 | + +--- + +## Appendix C: Build command reference + +```bash +# Configure +cmake -B build -G Ninja \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DCMAKE_CXX_STANDARD=23 \ + -DCMAKE_CXX_FLAGS="-Wall -Wextra -Wpedantic" + +# Build +cmake --build build + +# Test +ctest --test-dir build --output-on-failure + +# Sanitizer build (for CI) +cmake -B build-san -G Ninja \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DCMAKE_CXX_STANDARD=23 \ + -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -Wall -Wextra" +cmake --build build-san +ctest --test-dir build-san --output-on-failure +``` + +--- + +## Execution order and time estimates + +| Phase | Estimated Effort | Blocking | Deliverable | +|-------|-----------------|----------|-------------| +| Phase 0 | 1 hour | None | CMake builds, doctest runs, skeleton tests pass | +| Phase 1 | 4 hours | Phase 0 | All critical/high bugs fixed, regression tests green | +| Phase 2 | 3 hours | Phase 1 | Modernized style, no behavioral changes, all tests green | +| Phase 3 | 6 hours | Phase 2 | C++23 features integrated, new tests for from_chars/format/spaceship | +| Phase 4 | 8 hours | Phase 3 | Ranges-compliant iterators, deducing this, optional coroutine | + +**Total estimated effort: ~22 hours** + +Each phase produces a standalone, shippable commit. No phase depends on a later phase. If work is stopped after any phase, the codebase is strictly improved. diff --git a/include/edfio/Config.hpp b/include/edfio/Config.hpp index 4f833f0..5704b7f 100644 --- a/include/edfio/Config.hpp +++ b/include/edfio/Config.hpp @@ -18,7 +18,7 @@ namespace edfio Permissive }; - namespace config + namespace detail { inline constexpr ProcessorErrorCheck PROCESSOR_ERROR_CHECKING = ProcessorErrorCheck::Strict; } diff --git a/include/edfio/Defs.hpp b/include/edfio/Defs.hpp deleted file mode 100644 index 3a161ac..0000000 --- a/include/edfio/Defs.hpp +++ /dev/null @@ -1,26 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -namespace edfio -{ - - enum class FileErrc - { - FileDoesNotOpen, - FileNotOpened, - FileReadError, - FileContainsFormatErrors, - FileContainsInvalidAnnotations, - FileWriteError, - FileWriteInvalidAnnotations - }; - -} diff --git a/include/edfio/EdfIO.hpp b/include/edfio/EdfIO.hpp index de96612..2c74353 100644 --- a/include/edfio/EdfIO.hpp +++ b/include/edfio/EdfIO.hpp @@ -11,28 +11,33 @@ // Header #include "header/HeaderExam.hpp" + // Reader #include "reader/ReaderHeaderExam.hpp" + // Writer #include "writer/WriterHeaderExam.hpp" + // Store #include "store/DataRecordStore.hpp" #include "store/SignalRecordStore.hpp" #include "store/SignalSampleStore.hpp" -#include "store/detail/StoreUtils.hpp" #include "store/TalStore.hpp" #include "store/TimeStampStore.hpp" +#include "store/detail/StoreUtils.hpp" + // Sink #include "sink/DataRecordSink.hpp" #include "sink/SignalRecordSink.hpp" #include "sink/detail/SinkUtils.hpp" + // Processor -#include "processor/ProcessorSampleRecord.hpp" +#include "processor/ProcessorAnnotation.hpp" #include "processor/ProcessorSample.hpp" -#include "processor/ProcessorTimeStampRecord.hpp" -#include "processor/ProcessorTimeStamp.hpp" +#include "processor/ProcessorSampleRecord.hpp" #include "processor/ProcessorTalRecord.hpp" -#include "processor/ProcessorAnnotation.hpp" +#include "processor/ProcessorTimeStamp.hpp" +#include "processor/ProcessorTimeStampRecord.hpp" // STL #include diff --git a/include/edfio/Errors.hpp b/include/edfio/Errors.hpp new file mode 100644 index 0000000..1afc213 --- /dev/null +++ b/include/edfio/Errors.hpp @@ -0,0 +1,45 @@ +// +// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. +// +// Official repository: https://github.com/idotta/edfio +// + +#pragma once + +namespace edfio { + +enum class FileErrc { + FileDoesNotOpen, + FileNotOpened, + FileReadError, + FileContainsFormatErrors, + FileContainsInvalidAnnotations, + FileWriteError, + FileWriteInvalidAnnotations +}; + +inline constexpr const char *GetError(FileErrc err) { + switch (err) { + case FileErrc::FileDoesNotOpen: + return "Error: file does not open"; + case FileErrc::FileNotOpened: + return "Error: file not opened"; + case FileErrc::FileReadError: + return "Error: can't read file"; + case FileErrc::FileContainsFormatErrors: + return "Error: file contains format errors"; + case FileErrc::FileContainsInvalidAnnotations: + return "Error: file contains invalid annotations"; + case FileErrc::FileWriteError: + return "Error: can't write on file"; + case FileErrc::FileWriteInvalidAnnotations: + return "Error: writing invalid annotations"; + default: + return "Unspecified error"; + } +} + +} // namespace edfio diff --git a/include/edfio/Utils.hpp b/include/edfio/Utils.hpp deleted file mode 100644 index 35ee419..0000000 --- a/include/edfio/Utils.hpp +++ /dev/null @@ -1,54 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "Defs.hpp" - -namespace edfio -{ - - namespace detail - { - inline constexpr const char* GetError(FileErrc err) - { - if (err == FileErrc::FileDoesNotOpen) - { - return "Error: file does not open"; - } - else if (err == FileErrc::FileNotOpened) - { - return "Error: file not opened"; - } - else if (err == FileErrc::FileReadError) - { - return "Error: can't read file"; - } - else if (err == FileErrc::FileContainsFormatErrors) - { - return "Error: file contains format errors"; - } - else if (err == FileErrc::FileContainsInvalidAnnotations) - { - return "Error: file contains invalid annotations"; - } - else if (err == FileErrc::FileWriteError) - { - return "Error: can't write on file"; - } - else if (err == FileErrc::FileWriteInvalidAnnotations) - { - return "Error: writing invalid annotations"; - - } - return "Unspecified error"; - } - } - -} diff --git a/include/edfio/core/Annotation.hpp b/include/edfio/core/Annotation.hpp index a73db30..b9db3a9 100644 --- a/include/edfio/core/Annotation.hpp +++ b/include/edfio/core/Annotation.hpp @@ -11,27 +11,23 @@ #include -namespace edfio -{ +namespace edfio { - namespace detail - { - // Each TAL starts with a time stamp Onset21Duration20 - static const char DURATION_DIV = 21; - static const char ANNOTATION_DIV = 20; - static const char ANNOTATION_END = 0; - } +namespace detail { +// Each TAL starts with a time stamp Onset21Duration20 +static const char DURATION_DIV = 21; +static const char ANNOTATION_DIV = 20; +static const char ANNOTATION_END = 0; +} // namespace detail - struct TimeStamp - { - long long m_datarecord = 0; - double m_start = 0; - }; +struct TimeStamp { + long long m_datarecord = 0; + double m_start = 0; +}; - struct Annotation : TimeStamp - { - double m_duration = 0; - std::string m_annotation; - }; +struct Annotation : TimeStamp { + double m_duration = 0; + std::string m_annotation; +}; -} +} // namespace edfio diff --git a/include/edfio/core/DataFormat.hpp b/include/edfio/core/DataFormat.hpp index c83514a..dfd292d 100644 --- a/include/edfio/core/DataFormat.hpp +++ b/include/edfio/core/DataFormat.hpp @@ -9,43 +9,39 @@ #pragma once -namespace edfio -{ - - enum class DataFormat - { - Edf, - EdfPlusC, - EdfPlusD, - Bdf, - BdfPlusC, - BdfPlusD, - Invalid - }; - - inline constexpr bool IsPlus(DataFormat format) - { - return format == DataFormat::EdfPlusC || format == DataFormat::EdfPlusD - || format == DataFormat::BdfPlusC || format == DataFormat::BdfPlusD; - } - - inline constexpr bool IsEdf(DataFormat format) - { - return format == DataFormat::Edf || format == DataFormat::EdfPlusC || format == DataFormat::EdfPlusD; - } - - inline constexpr bool IsBdf(DataFormat format) - { - return format == DataFormat::Bdf || format == DataFormat::BdfPlusC || format == DataFormat::BdfPlusD; - } - - inline constexpr int GetSampleBytes(DataFormat format) - { - if (IsEdf(format)) - return 2; - else if (IsBdf(format)) - return 3; - return -1; - } +namespace edfio { + +enum class DataFormat { + Edf, + EdfPlusC, + EdfPlusD, + Bdf, + BdfPlusC, + BdfPlusD, + Invalid +}; + +inline constexpr bool IsPlus(DataFormat format) { + return format == DataFormat::EdfPlusC || format == DataFormat::EdfPlusD || + format == DataFormat::BdfPlusC || format == DataFormat::BdfPlusD; +} + +inline constexpr bool IsEdf(DataFormat format) { + return format == DataFormat::Edf || format == DataFormat::EdfPlusC || + format == DataFormat::EdfPlusD; +} +inline constexpr bool IsBdf(DataFormat format) { + return format == DataFormat::Bdf || format == DataFormat::BdfPlusC || + format == DataFormat::BdfPlusD; } + +inline constexpr int GetSampleBytes(DataFormat format) { + if (IsEdf(format)) + return 2; + else if (IsBdf(format)) + return 3; + return -1; +} + +} // namespace edfio diff --git a/include/edfio/core/Device.hpp b/include/edfio/core/Device.hpp index e99b59c..0b9e9d0 100644 --- a/include/edfio/core/Device.hpp +++ b/include/edfio/core/Device.hpp @@ -11,46 +11,40 @@ #include -namespace edfio -{ - - // A class created in order to have an easier way to access streams - // of specific data through their respective iterators. - template - class Device - { - public: - typedef Stream stream_type; - typedef Device device_type; - - typedef Value value_type; - typedef Pointer pointer; - typedef Reference reference; - typedef long long difference_type; - typedef unsigned long long size_type; - - class iterator - { - public: - using difference_type = typename Device::difference_type; - using value_type = typename Device::value_type; - using reference = typename Device::reference; - using pointer = typename Device::pointer; - using iterator_category = IterCategory; - using iterator_concept = IterCategory; - using stream_type = typename Device::stream_type; - }; - - Device() = delete; - - Device(stream_type &stream) - : m_stream(stream) - { - } - - protected: - - stream_type &m_stream; - }; - -} +namespace edfio { + +// A class created in order to have an easier way to access streams +// of specific data through their respective iterators. +template +class Device { +public: + typedef Stream stream_type; + typedef Device device_type; + + typedef Value value_type; + typedef Pointer pointer; + typedef Reference reference; + typedef long long difference_type; + typedef unsigned long long size_type; + + class iterator { + public: + using difference_type = typename Device::difference_type; + using value_type = typename Device::value_type; + using reference = typename Device::reference; + using pointer = typename Device::pointer; + using iterator_category = IterCategory; + using iterator_concept = IterCategory; + using stream_type = typename Device::stream_type; + }; + + Device() = delete; + + Device(stream_type &stream) : m_stream(stream) {} + +protected: + stream_type &m_stream; +}; + +} // namespace edfio diff --git a/include/edfio/core/Field.hpp b/include/edfio/core/Field.hpp index 988f6b4..c9569cb 100644 --- a/include/edfio/core/Field.hpp +++ b/include/edfio/core/Field.hpp @@ -12,53 +12,38 @@ #include #include -namespace edfio -{ - - // Fields have fixed predetermined sizes in the exam file - // They are used to read the headers - template - struct Field - { - using ValueType = CharT; - - constexpr size_t Size() const - { - return Sz; - } - const std::basic_string& operator()() const - { - return m_value; - } - std::basic_string& operator()() - { - return m_value; - } - void operator()(const std::basic_string& value) - { - m_value = value; - m_value.resize(Size(), ' '); - } - - std::basic_string m_value; - }; - - template - std::ostream& operator << (std::ostream &os, Field &f) - { - auto &value = f(); - value.resize(Sz, ' '); - os.write(value.data(), Sz); - return os; - } - - template - std::istream& operator >> (std::istream &is, Field &f) - { - auto &value = f(); - value.resize(f.Size(), ' '); - is.read(&value[0], f.Size()); - return is; - } +namespace edfio { + +// Fields have fixed predetermined sizes in the exam file +// They are used to read the headers +template struct Field { + using ValueType = CharT; + + constexpr size_t Size() const { return Sz; } + const std::basic_string &operator()() const { return m_value; } + std::basic_string &operator()() { return m_value; } + void operator()(const std::basic_string &value) { + m_value = value; + m_value.resize(Size(), ' '); + } + + std::basic_string m_value; +}; + +template +std::ostream &operator<<(std::ostream &os, Field &f) { + auto &value = f(); + value.resize(Sz, ' '); + os.write(value.data(), Sz); + return os; +} +template +std::istream &operator>>(std::istream &is, Field &f) { + auto &value = f(); + value.resize(f.Size(), ' '); + is.read(&value[0], f.Size()); + return is; } + +} // namespace edfio diff --git a/include/edfio/core/Record.hpp b/include/edfio/core/Record.hpp index f362a3c..a4e6d7e 100644 --- a/include/edfio/core/Record.hpp +++ b/include/edfio/core/Record.hpp @@ -12,70 +12,55 @@ #include #include -namespace edfio -{ +namespace edfio { - // Records have fixed sizes that can vary according to the signal - // They can be used for either single signal record or data record IO - // It is important that data records always have one size in the same file - template - struct Record - { - using ValueType = ValT; - using VectorType = std::vector; +// Records have fixed sizes that can vary according to the signal +// They can be used for either single signal record or data record IO +// It is important that data records always have one size in the same file +template struct Record { + using ValueType = ValT; + using VectorType = std::vector; - Record() = delete; + Record() = delete; - Record(size_t recordSize) - : m_value(recordSize, 0) {} + Record(size_t recordSize) : m_value(recordSize, 0) {} - Record(typename VectorType::const_iterator first, typename VectorType::const_iterator last) - : m_value(first, last) {} + Record(typename VectorType::const_iterator first, + typename VectorType::const_iterator last) + : m_value(first, last) {} - Record(const Record&) = default; - Record(Record&&) = default; - Record& operator=(const Record&) = default; - Record& operator=(Record&&) = default; + Record(const Record &) = default; + Record(Record &&) = default; + Record &operator=(const Record &) = default; + Record &operator=(Record &&) = default; - size_t Size() const - { - return m_value.size(); - } - const VectorType& operator()() const - { - return m_value; - } - VectorType& operator()() - { - return m_value; - } - Record operator+(const Record& record) const - { - Record tmp(Size() + record.Size()); - std::copy(m_value.begin(), m_value.end(), tmp().begin()); - std::copy(record().begin(), record().end(), tmp().begin() + Size()); - return tmp; - } + size_t Size() const { return m_value.size(); } + const VectorType &operator()() const { return m_value; } + VectorType &operator()() { return m_value; } + Record operator+(const Record &record) const { + Record tmp(Size() + record.Size()); + std::copy(m_value.begin(), m_value.end(), tmp().begin()); + std::copy(record().begin(), record().end(), tmp().begin() + Size()); + return tmp; + } - VectorType m_value; - }; + VectorType m_value; +}; - template - std::ostream& operator << (std::ostream &os, Record &r) - { - auto &record = r(); - record.resize(r.Size(), 0); - os.write(record.data(), r.Size() * sizeof(ValT)); - return os; - } - - template - std::istream& operator >> (std::istream &is, Record &r) - { - auto &record = r(); - record.resize(r.Size(), 0); - is.read(&record[0], r.Size() * sizeof(ValT)); - return is; - } +template +std::ostream &operator<<(std::ostream &os, Record &r) { + auto &record = r(); + record.resize(r.Size(), 0); + os.write(record.data(), r.Size() * sizeof(ValT)); + return os; +} +template +std::istream &operator>>(std::istream &is, Record &r) { + auto &record = r(); + record.resize(r.Size(), 0); + is.read(&record[0], r.Size() * sizeof(ValT)); + return is; } + +} // namespace edfio diff --git a/include/edfio/core/SampleType.hpp b/include/edfio/core/SampleType.hpp index 17fb5de..5b8e808 100644 --- a/include/edfio/core/SampleType.hpp +++ b/include/edfio/core/SampleType.hpp @@ -13,45 +13,31 @@ #include "../header/HeaderGeneral.hpp" #include "../header/HeaderSignal.hpp" -namespace edfio -{ - - enum class SampleType - { - Physical, - Digital - }; - - namespace impl - { - - template - struct Sample - { - }; - - template <> - struct Sample - { - using type = double; - }; - - template <> - struct Sample - { - using type = int; - }; - - inline Sample::type ConvertSample(double offset, double scaling, Sample::type sample) - { - return static_cast::type>((sample - offset) / scaling); - } - - inline Sample::type ConvertSample(double offset, double scaling, Sample::type sample) - { - return scaling * static_cast::type>(sample) + offset; - } - - } +namespace edfio { +enum class SampleType { Physical, Digital }; + +template struct Sample {}; + +template <> struct Sample { + using type = double; +}; + +template <> struct Sample { + using type = int; +}; + +inline Sample::type +ConvertSample(double offset, double scaling, + Sample::type sample) { + return static_cast::type>((sample - offset) / + scaling); +} + +inline Sample::type +ConvertSample(double offset, double scaling, + Sample::type sample) { + return scaling * static_cast::type>(sample) + + offset; } +} // namespace edfio diff --git a/include/edfio/core/StreamIO.hpp b/include/edfio/core/StreamIO.hpp index 6630b84..b8606f8 100644 --- a/include/edfio/core/StreamIO.hpp +++ b/include/edfio/core/StreamIO.hpp @@ -11,20 +11,17 @@ #include -namespace edfio -{ +namespace edfio { - template - struct StreamIO - { - using Stream = StreamT; - using ValueType = CharT; - }; +template struct StreamIO { + using Stream = StreamT; + using ValueType = CharT; +}; - template - using Reader = StreamIO , CharT>; +template +using Reader = StreamIO, CharT>; - template - using Writer = StreamIO , CharT>; +template +using Writer = StreamIO, CharT>; -} +} // namespace edfio diff --git a/include/edfio/header/HeaderExam.hpp b/include/edfio/header/HeaderExam.hpp index 3dc1e7e..5b21adb 100644 --- a/include/edfio/header/HeaderExam.hpp +++ b/include/edfio/header/HeaderExam.hpp @@ -14,13 +14,11 @@ #include -namespace edfio -{ +namespace edfio { - struct HeaderExam - { - HeaderGeneral m_general; - std::vector m_signals; - }; +struct HeaderExam { + HeaderGeneral m_general; + std::vector m_signals; +}; -} +} // namespace edfio diff --git a/include/edfio/header/HeaderGeneral.hpp b/include/edfio/header/HeaderGeneral.hpp index 101515b..55f6a19 100644 --- a/include/edfio/header/HeaderGeneral.hpp +++ b/include/edfio/header/HeaderGeneral.hpp @@ -15,57 +15,52 @@ #include #include -namespace edfio -{ +namespace edfio { - struct HeaderGeneralFields - { - Field<8> m_version; - Field<80> m_patient; - Field<80> m_recording; - Field<8> m_startDate; - Field<8> m_startTime; - Field<8> m_headerSize; - Field<44> m_reserved; - Field<8> m_datarecordsFile; - Field<8> m_datarecordDuration; - Field<4> m_totalSignals; - }; +struct HeaderGeneralFields { + Field<8> m_version; + Field<80> m_patient; + Field<80> m_recording; + Field<8> m_startDate; + Field<8> m_startTime; + Field<8> m_headerSize; + Field<44> m_reserved; + Field<8> m_datarecordsFile; + Field<8> m_datarecordDuration; + Field<4> m_totalSignals; +}; - namespace detail - { - struct HeaderGeneralDetail - { - unsigned int m_recordSize = 0; - double m_fileDuration = 0; +namespace detail { +struct HeaderGeneralDetail { + unsigned int m_recordSize = 0; + double m_fileDuration = 0; - std::string m_patientCode; - std::string m_gender; - std::string m_birthdate; - std::string m_patientName; - std::string m_patientAdditional; - std::string m_admincode; - std::string m_technician; - std::string m_equipment; - std::string m_recordingAdditional; - }; - } + std::string m_patientCode; + std::string m_gender; + std::string m_birthdate; + std::string m_patientName; + std::string m_patientAdditional; + std::string m_admincode; + std::string m_technician; + std::string m_equipment; + std::string m_recordingAdditional; +}; +} // namespace detail - struct HeaderGeneral - { - // Field values - DataFormat m_version = DataFormat::Invalid; - std::string m_patient; - std::string m_recording; - std::tuple m_startDate; - std::tuple m_startTime; - int m_headerSize = 0; - std::string m_reserved; - long long m_datarecordsFile = 0; - double m_datarecordDuration = 0; - int m_totalSignals = 0; - // Extra values - detail::HeaderGeneralDetail m_detail; - }; +struct HeaderGeneral { + // Field values + DataFormat m_version = DataFormat::Invalid; + std::string m_patient; + std::string m_recording; + std::tuple m_startDate; + std::tuple m_startTime; + int m_headerSize = 0; + std::string m_reserved; + long long m_datarecordsFile = 0; + double m_datarecordDuration = 0; + int m_totalSignals = 0; + // Extra values + detail::HeaderGeneralDetail m_detail; +}; -} +} // namespace edfio diff --git a/include/edfio/header/HeaderSignal.hpp b/include/edfio/header/HeaderSignal.hpp index 99bd21b..56da1c7 100644 --- a/include/edfio/header/HeaderSignal.hpp +++ b/include/edfio/header/HeaderSignal.hpp @@ -13,49 +13,44 @@ #include -namespace edfio -{ - - struct HeaderSignalFields - { - Field<16> m_label; - Field<80> m_transducer; - Field<8> m_physDimension; - Field<8> m_physicalMin; - Field<8> m_physicalMax; - Field<8> m_digitalMin; - Field<8> m_digitalMax; - Field<80> m_prefilter; - Field<8> m_samplesInDataRecord; - Field<32> m_reserved; - }; - - namespace detail - { - struct HeaderSignalDetail - { - long m_signalOffset = 0; - double m_scaling = 0; - double m_offset = 0; - bool m_isAnnotation = false; - }; - } - - struct HeaderSignal - { - // Field values - std::string m_label; - std::string m_transducer; - std::string m_physDimension; - double m_physicalMin = 0; - double m_physicalMax = 0; - int m_digitalMin = 0; - int m_digitalMax = 0; - std::string m_prefilter; - int m_samplesInDataRecord = 0; - std::string m_reserved; - // Extra values - detail::HeaderSignalDetail m_detail; - }; - -} +namespace edfio { + +struct HeaderSignalFields { + Field<16> m_label; + Field<80> m_transducer; + Field<8> m_physDimension; + Field<8> m_physicalMin; + Field<8> m_physicalMax; + Field<8> m_digitalMin; + Field<8> m_digitalMax; + Field<80> m_prefilter; + Field<8> m_samplesInDataRecord; + Field<32> m_reserved; +}; + +namespace detail { +struct HeaderSignalDetail { + long m_signalOffset = 0; + double m_scaling = 0; + double m_offset = 0; + bool m_isAnnotation = false; +}; +} // namespace detail + +struct HeaderSignal { + // Field values + std::string m_label; + std::string m_transducer; + std::string m_physDimension; + double m_physicalMin = 0; + double m_physicalMax = 0; + int m_digitalMin = 0; + int m_digitalMax = 0; + std::string m_prefilter; + int m_samplesInDataRecord = 0; + std::string m_reserved; + // Extra values + detail::HeaderSignalDetail m_detail; +}; + +} // namespace edfio diff --git a/include/edfio/header/HeaderUtils.hpp b/include/edfio/header/HeaderUtils.hpp index 69a7734..a519e1a 100644 --- a/include/edfio/header/HeaderUtils.hpp +++ b/include/edfio/header/HeaderUtils.hpp @@ -9,137 +9,103 @@ #pragma once -#include "../Defs.hpp" +#include "../Errors.hpp" #include "HeaderGeneral.hpp" #include "HeaderSignal.hpp" -#include #include +#include -namespace edfio -{ - - namespace detail - { - - inline HeaderGeneral CreateHeaderGeneral( - DataFormat version, - std::string patient, - std::string recording, - int startDateD, - int startDateM, - int startDateY, - int startTimeH, - int startTimeM, - int startTimeS, - int headerSize, - std::string reserved, - long long datarecordsFile, - double datarecordDuration, - const std::vector& signals - ) - { - HeaderGeneral header; - header.m_version = version; - header.m_patient = patient; - header.m_recording = recording; - header.m_startDate = std::make_tuple(startDateD, startDateM, startDateY); - header.m_startTime = std::make_tuple(startTimeH, startTimeM, startTimeS); - header.m_headerSize = headerSize; - header.m_reserved = reserved; - header.m_datarecordsFile = datarecordsFile; - header.m_datarecordDuration = datarecordDuration; - header.m_totalSignals = signals.size(); - - // Record size - header.m_detail.m_recordSize = 0; - for (auto &signal : signals) - { - header.m_detail.m_recordSize += signal.m_samplesInDataRecord; - } - header.m_detail.m_recordSize *= GetSampleBytes(version); - - return header; - } - - inline HeaderGeneral CreateHeaderGeneralPlus( - DataFormat version, - std::string patientCode, - std::string gender, - std::string birthdate, - std::string patientName, - std::string patientAdditional, - std::string admincode, - std::string technician, - std::string equipment, - std::string recordingAdditional, - int startDateD, - int startDateM, - int startDateY, - int startTimeH, - int startTimeM, - int startTimeS, - int headerSize, - std::string reserved, - long long datarecordsFile, - double datarecordDuration, - const std::vector& signals - ) - { - auto header = CreateHeaderGeneral( - version, "", "", startDateD, startDateM, startDateY, startTimeH, startTimeM, startTimeS, - headerSize, reserved, datarecordsFile, datarecordDuration, signals - ); - - header.m_detail.m_patientCode = patientCode; - header.m_detail.m_gender = gender; - header.m_detail.m_birthdate = birthdate; - header.m_detail.m_patientName = patientName; - header.m_detail.m_patientAdditional = patientAdditional; - header.m_detail.m_admincode = admincode; - header.m_detail.m_technician = technician; - header.m_detail.m_equipment = equipment; - header.m_detail.m_recordingAdditional = recordingAdditional; - - return header; - } - - inline HeaderSignal CreateHeaderSignal( - std::string label, - int samplesInDataRecord, - double physicalMin, - double physicalMax, - int digitalMin, - int digitalMax, - long signalOffset = 0, - bool annotation = false, - std::string transducer = "", - std::string physDimension = "", - std::string prefilter = "", - std::string reserved = "" - ) - { - HeaderSignal signal; - - signal.m_label = label; - signal.m_transducer = transducer; - signal.m_physDimension = physDimension; - signal.m_physicalMin = physicalMin; - signal.m_physicalMax = physicalMax; - signal.m_digitalMin = digitalMin; - signal.m_digitalMax = digitalMax; - signal.m_prefilter = prefilter; - signal.m_samplesInDataRecord = samplesInDataRecord; - signal.m_reserved = reserved; - - signal.m_detail.m_signalOffset = signalOffset; - signal.m_detail.m_scaling = (signal.m_physicalMax - signal.m_physicalMin) / (signal.m_digitalMax - signal.m_digitalMin); - signal.m_detail.m_offset = signal.m_physicalMin - signal.m_detail.m_scaling * signal.m_digitalMin; - signal.m_detail.m_isAnnotation = annotation; - - return signal; - } - - } +namespace edfio { + +namespace detail { + +inline HeaderGeneral +CreateHeaderGeneral(DataFormat version, std::string patient, + std::string recording, int startDateD, int startDateM, + int startDateY, int startTimeH, int startTimeM, + int startTimeS, int headerSize, std::string reserved, + long long datarecordsFile, double datarecordDuration, + const std::vector &signals) { + HeaderGeneral header; + header.m_version = version; + header.m_patient = patient; + header.m_recording = recording; + header.m_startDate = std::make_tuple(startDateD, startDateM, startDateY); + header.m_startTime = std::make_tuple(startTimeH, startTimeM, startTimeS); + header.m_headerSize = headerSize; + header.m_reserved = reserved; + header.m_datarecordsFile = datarecordsFile; + header.m_datarecordDuration = datarecordDuration; + header.m_totalSignals = signals.size(); + + // Record size + header.m_detail.m_recordSize = 0; + for (auto &signal : signals) { + header.m_detail.m_recordSize += signal.m_samplesInDataRecord; + } + header.m_detail.m_recordSize *= GetSampleBytes(version); + + return header; +} +inline HeaderGeneral CreateHeaderGeneralPlus( + DataFormat version, std::string patientCode, std::string gender, + std::string birthdate, std::string patientName, + std::string patientAdditional, std::string admincode, + std::string technician, std::string equipment, + std::string recordingAdditional, int startDateD, int startDateM, + int startDateY, int startTimeH, int startTimeM, int startTimeS, + int headerSize, std::string reserved, long long datarecordsFile, + double datarecordDuration, const std::vector &signals) { + auto header = CreateHeaderGeneral( + version, "", "", startDateD, startDateM, startDateY, startTimeH, + startTimeM, startTimeS, headerSize, reserved, datarecordsFile, + datarecordDuration, signals); + + header.m_detail.m_patientCode = patientCode; + header.m_detail.m_gender = gender; + header.m_detail.m_birthdate = birthdate; + header.m_detail.m_patientName = patientName; + header.m_detail.m_patientAdditional = patientAdditional; + header.m_detail.m_admincode = admincode; + header.m_detail.m_technician = technician; + header.m_detail.m_equipment = equipment; + header.m_detail.m_recordingAdditional = recordingAdditional; + + return header; } + +inline HeaderSignal +CreateHeaderSignal(std::string label, int samplesInDataRecord, + double physicalMin, double physicalMax, int digitalMin, + int digitalMax, long signalOffset = 0, + bool annotation = false, std::string transducer = "", + std::string physDimension = "", std::string prefilter = "", + std::string reserved = "") { + HeaderSignal signal; + + signal.m_label = label; + signal.m_transducer = transducer; + signal.m_physDimension = physDimension; + signal.m_physicalMin = physicalMin; + signal.m_physicalMax = physicalMax; + signal.m_digitalMin = digitalMin; + signal.m_digitalMax = digitalMax; + signal.m_prefilter = prefilter; + signal.m_samplesInDataRecord = samplesInDataRecord; + signal.m_reserved = reserved; + + signal.m_detail.m_signalOffset = signalOffset; + signal.m_detail.m_scaling = (signal.m_physicalMax - signal.m_physicalMin) / + (signal.m_digitalMax - signal.m_digitalMin); + signal.m_detail.m_offset = + signal.m_physicalMin - signal.m_detail.m_scaling * signal.m_digitalMin; + signal.m_detail.m_isAnnotation = annotation; + + return signal; +} + +} // namespace detail + +} // namespace edfio diff --git a/include/edfio/processor/ProcessorAnnotation.hpp b/include/edfio/processor/ProcessorAnnotation.hpp index 62554c8..c88f035 100644 --- a/include/edfio/processor/ProcessorAnnotation.hpp +++ b/include/edfio/processor/ProcessorAnnotation.hpp @@ -9,66 +9,58 @@ #pragma once -#include "../Utils.hpp" +#include "../Errors.hpp" #include "../core/Annotation.hpp" #include "../core/Record.hpp" -#include "detail/ProcessorUtils.hpp" +#include "ProcessorUtils.hpp" -namespace edfio -{ +namespace edfio { - struct ProcessorAnnotation - { - Record operator ()(Annotation annotation); - }; +inline Record ProcessAnnotation(Annotation annotation) { + if (annotation.m_annotation.empty()) { + throw std::invalid_argument( + GetError(FileErrc::FileWriteInvalidAnnotations)); + } - inline Record ProcessorAnnotation::operator()(Annotation annotation) - { - if (annotation.m_annotation.empty()) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileWriteInvalidAnnotations)); - } + std::string timestamp = (annotation.m_start >= 0 ? "+" : "") + + detail::to_string_decimal(annotation.m_start); + std::string duration; + if (annotation.m_duration > 0) + duration = detail::to_string_decimal(annotation.m_duration); - std::string timestamp = (annotation.m_start >= 0 ? "+" : "") + detail::to_string_decimal(annotation.m_start); - std::string duration; - if (annotation.m_duration > 0) - duration = detail::to_string_decimal(annotation.m_duration); + size_t size = timestamp.size(); // timestamp + if (!duration.empty()) { + size++; // 21 div + size += duration.size(); // duration + } - size_t size = timestamp.size(); // timestamp - if (!duration.empty()) - { - size++; // 21 div - size += duration.size(); // duration - } + size++; // 20 div + size += annotation.m_annotation.size(); // annotation + size += 2; // 20 div and 0 - size++; // 20 div - size += annotation.m_annotation.size(); // annotation - size += 2; // 20 div and 0 + Record record(size); + auto it = record().begin(); - Record record(size); - auto it = record().begin(); + // timestamp + std::move(timestamp.begin(), timestamp.end(), it); + it += timestamp.size(); - // timestamp - std::move(timestamp.begin(), timestamp.end(), it); - it += timestamp.size(); + if (!duration.empty()) { + *it++ = 21; // 21 div + // duration + std::move(duration.begin(), duration.end(), it); + it += duration.size(); + } - if (!duration.empty()) - { - *it++ = 21; // 21 div - // duration - std::move(duration.begin(), duration.end(), it); - it += duration.size(); - } + *it++ = 20; // 20 div + // annotation + std::move(annotation.m_annotation.begin(), annotation.m_annotation.end(), it); + it += annotation.m_annotation.size(); - *it++ = 20; // 20 div - // annotation - std::move(annotation.m_annotation.begin(), annotation.m_annotation.end(), it); - it += annotation.m_annotation.size(); - - *it++ = 20; // 20 div - *it++ = 0; // 0 end - - return record; - } + *it++ = 20; // 20 div + *it++ = 0; // 0 end + return record; } + +} // namespace edfio diff --git a/include/edfio/processor/ProcessorHeaderExam.hpp b/include/edfio/processor/ProcessorHeaderExam.hpp index a0cde5f..e96849c 100644 --- a/include/edfio/processor/ProcessorHeaderExam.hpp +++ b/include/edfio/processor/ProcessorHeaderExam.hpp @@ -9,46 +9,34 @@ #pragma once -#include "../Utils.hpp" +#include "../Errors.hpp" #include "../core/DataFormat.hpp" #include "../header/HeaderExam.hpp" -namespace edfio -{ - - struct ProcessorHeaderExam - { - HeaderExam operator ()(HeaderGeneral header, std::vector signals); - }; - - inline HeaderExam edfio::ProcessorHeaderExam::operator()(HeaderGeneral header, std::vector signals) - { - // Record size - size_t recordsize = 0; - for (auto &signal : signals) - { - recordsize += signal.m_samplesInDataRecord; - } - - if (IsBdf(header.m_version)) - { - recordsize *= 3; - if (recordsize > 0xF00000) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - else - { - recordsize *= 2; - if (recordsize > 0xA00000) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - header.m_detail.m_recordSize = recordsize; - - return HeaderExam{ std::move(header), std::move(signals) }; - } - +namespace edfio { + +inline HeaderExam ProcessHeaderExam(HeaderGeneral header, + std::vector signals) { + // Record size + size_t recordsize = 0; + for (auto &signal : signals) { + recordsize += signal.m_samplesInDataRecord; + } + + if (IsBdf(header.m_version)) { + recordsize *= 3; + if (recordsize > 0xF00000) { + throw std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); + } + } else { + recordsize *= 2; + if (recordsize > 0xA00000) { + throw std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); + } + } + header.m_detail.m_recordSize = recordsize; + + return HeaderExam{std::move(header), std::move(signals)}; } + +} // namespace edfio diff --git a/include/edfio/processor/ProcessorHeaderGeneral.hpp b/include/edfio/processor/ProcessorHeaderGeneral.hpp index 2f5e78f..e6494ad 100644 --- a/include/edfio/processor/ProcessorHeaderGeneral.hpp +++ b/include/edfio/processor/ProcessorHeaderGeneral.hpp @@ -9,152 +9,152 @@ #pragma once -#include "../Defs.hpp" +#include "../Errors.hpp" #include "../core/DataFormat.hpp" #include "../header/HeaderGeneral.hpp" -#include "detail/ProcessorUtils.hpp" +#include "ProcessorUtils.hpp" #include #include -namespace edfio -{ +namespace edfio { - struct ProcessorHeaderGeneral - { - HeaderGeneralFields operator ()(HeaderGeneral in); - }; +inline HeaderGeneralFields ProcessHeaderGeneral(HeaderGeneral in) { + HeaderGeneralFields out; - inline HeaderGeneralFields ProcessorHeaderGeneral::operator()(HeaderGeneral in) - { - HeaderGeneralFields out; - - // Version - { - if (IsEdf(in.m_version)) - { - out.m_version("0 "); - } - else if (IsBdf(in.m_version)) - { - std::string value = "BIOSEMI"; - value.insert(value.begin(), -1); - out.m_version(value); - } - } - // Patient Name - { - out.m_patient(in.m_patient); - } - // Recording - { - out.m_recording(in.m_recording); - } - // Start Date - { - int day = std::get<0>(in.m_startDate); - int month = std::get<1>(in.m_startDate); - int year = std::get<2>(in.m_startDate); - year -= year > 1999 ? 2000 : 1900; - out.m_startDate(std::format("{:02d}.{:02d}.{:02d}", day, month, year)); - } - // Start Time - { - int hour = std::get<0>(in.m_startTime); - int minute = std::get<1>(in.m_startTime); - int second = std::get<2>(in.m_startTime); - out.m_startTime(std::format("{:02d}.{:02d}.{:02d}", hour, minute, second)); - } - // Header Size - { - out.m_headerSize(std::to_string(in.m_headerSize)); - } - // Reserved - { - if (!in.m_reserved.empty()) - { - out.m_reserved(in.m_reserved); - } - else if (IsPlus(in.m_version)) - { - out.m_reserved(std::string(detail::GetFormatName(in.m_version))); - } - } - // Datarecords in File - { - out.m_datarecordsFile(std::to_string(in.m_datarecordsFile)); - } - // Datarecord Duration - { - out.m_datarecordDuration(detail::to_string_decimal(in.m_datarecordDuration)); - } - // Number of signals - { - out.m_totalSignals(std::to_string(in.m_totalSignals)); - } - // Plus Fields - if (IsPlus(in.m_version)) - { - // Patient Name - { - std::vector fields; - // Code - fields.push_back(in.m_detail.m_patientCode.empty() ? "X" : in.m_detail.m_patientCode); - // Sex - fields.push_back(in.m_detail.m_gender.empty() ? "X" : in.m_detail.m_gender); - // Birthdate in dd-MMM-yyyy format using the English 3-character abbreviations of the month in capitals. 02-AUG-1951 is OK, while 2-AUG-1951 is not. - fields.push_back(in.m_detail.m_birthdate.empty() ? "X" : in.m_detail.m_birthdate); - // The patients name. - fields.push_back(in.m_detail.m_patientName.empty() ? "X" : in.m_detail.m_patientName); - // Additional information. - std::string info; - std::istringstream f(in.m_detail.m_patientAdditional); - while (std::getline(f, info, detail::ADDITIONAL_SEPARATOR)) - { - fields.push_back(info); - } - std::string patient = ""; - for (auto& field : fields) - { - std::replace(field.begin(), field.end(), ' ', '_'); // replace all ' ' to '_' - patient += field + " "; - } - patient.pop_back(); // remove last " " - out.m_patient(patient); - } - // Recording - { - std::vector fields; - // The text 'Startdate' - fields.push_back("Startdate"); - // The startdate itself in dd-MMM-yyyy format using the English 3-character abbreviations of the month in capitals: dd-MMM-yyyy (MMM = 'JAN' | 'FEV' | ...) - int day = std::get<0>(in.m_startDate); - int month = std::get<1>(in.m_startDate); - int year = std::get<2>(in.m_startDate); - fields.push_back(std::format("{:02d}-{}-{}", day ? day : 1, detail::GetStringFromMonth(month), year ? year : 1984)); - // The hospital administration code of the investigation, i.e. EEG number or PSG number. - fields.push_back(in.m_detail.m_admincode.empty() ? "X" : in.m_detail.m_admincode); - // A code specifying the responsible investigator or technician. - fields.push_back(in.m_detail.m_technician.empty() ? "X" : in.m_detail.m_technician); - // A code specifying the used equipment. - fields.push_back(in.m_detail.m_equipment.empty() ? "X" : in.m_detail.m_equipment); - // Additional information. - std::string info; - std::istringstream f(in.m_detail.m_recordingAdditional); - while (std::getline(f, info, detail::ADDITIONAL_SEPARATOR)) - { - fields.push_back(info); - } - std::string recording = ""; - for (auto& field : fields) - { - std::replace(field.begin(), field.end(), ' ', '_'); // replace all '_' to ' ' - recording += field + " "; - } - recording.pop_back(); // remove last " " - out.m_recording(recording); - } - } - return out; - } + // Version + { + if (IsEdf(in.m_version)) { + out.m_version("0 "); + } else if (IsBdf(in.m_version)) { + std::string value = "BIOSEMI"; + value.insert(value.begin(), -1); + out.m_version(value); + } + } + // Patient Name + { + out.m_patient(in.m_patient); + } + // Recording + { + out.m_recording(in.m_recording); + } + // Start Date + { + int day = std::get<0>(in.m_startDate); + int month = std::get<1>(in.m_startDate); + int year = std::get<2>(in.m_startDate); + year -= year > 1999 ? 2000 : 1900; + out.m_startDate(std::format("{:02d}.{:02d}.{:02d}", day, month, year)); + } + // Start Time + { + int hour = std::get<0>(in.m_startTime); + int minute = std::get<1>(in.m_startTime); + int second = std::get<2>(in.m_startTime); + out.m_startTime(std::format("{:02d}.{:02d}.{:02d}", hour, minute, second)); + } + // Header Size + { + out.m_headerSize(std::to_string(in.m_headerSize)); + } + // Reserved + { + if (!in.m_reserved.empty()) { + out.m_reserved(in.m_reserved); + } else if (IsPlus(in.m_version)) { + out.m_reserved(std::string(detail::GetFormatName(in.m_version))); + } + } + // Datarecords in File + { + out.m_datarecordsFile(std::to_string(in.m_datarecordsFile)); + } + // Datarecord Duration + { + out.m_datarecordDuration( + detail::to_string_decimal(in.m_datarecordDuration)); + } + // Number of signals + { + out.m_totalSignals(std::to_string(in.m_totalSignals)); + } + // Plus Fields + if (IsPlus(in.m_version)) { + // Patient Name + { + std::vector fields; + // Code + fields.push_back( + in.m_detail.m_patientCode.empty() ? "X" : in.m_detail.m_patientCode); + // Sex + fields.push_back(in.m_detail.m_gender.empty() ? "X" + : in.m_detail.m_gender); + // Birthdate in dd-MMM-yyyy format using the English 3-character + // abbreviations of the month in capitals. 02-AUG-1951 is OK, while + // 2-AUG-1951 is not. + fields.push_back( + in.m_detail.m_birthdate.empty() ? "X" : in.m_detail.m_birthdate); + // The patients name. + fields.push_back( + in.m_detail.m_patientName.empty() ? "X" : in.m_detail.m_patientName); + // Additional information. + std::string info; + std::istringstream f(in.m_detail.m_patientAdditional); + while (std::getline(f, info, detail::ADDITIONAL_SEPARATOR)) { + fields.push_back(info); + } + std::string patient = ""; + for (auto &field : fields) { + std::replace(field.begin(), field.end(), ' ', + '_'); // replace all ' ' to '_' + patient += field + " "; + } + patient.pop_back(); // remove last " " + out.m_patient(patient); + } + // Recording + { + std::vector fields; + // The text 'Startdate' + fields.push_back("Startdate"); + // The startdate itself in dd-MMM-yyyy format using the English + // 3-character abbreviations of the month in capitals: dd-MMM-yyyy (MMM = + // 'JAN' | 'FEV' | ...) + int day = std::get<0>(in.m_startDate); + int month = std::get<1>(in.m_startDate); + int year = std::get<2>(in.m_startDate); + fields.push_back(std::format("{:02d}-{}-{}", day ? day : 1, + detail::GetStringFromMonth(month), + year ? year : 1984)); + // The hospital administration code of the investigation, i.e. EEG number + // or PSG number. + fields.push_back( + in.m_detail.m_admincode.empty() ? "X" : in.m_detail.m_admincode); + // A code specifying the responsible investigator or technician. + fields.push_back( + in.m_detail.m_technician.empty() ? "X" : in.m_detail.m_technician); + // A code specifying the used equipment. + fields.push_back( + in.m_detail.m_equipment.empty() ? "X" : in.m_detail.m_equipment); + // Additional information. + std::string info; + std::istringstream f(in.m_detail.m_recordingAdditional); + while (std::getline(f, info, detail::ADDITIONAL_SEPARATOR)) { + fields.push_back(info); + } + std::string recording = ""; + for (auto &field : fields) { + std::replace(field.begin(), field.end(), ' ', + '_'); // replace all '_' to ' ' + recording += field + " "; + } + recording.pop_back(); // remove last " " + out.m_recording(recording); + } + } + return out; } + +} // namespace edfio diff --git a/include/edfio/processor/ProcessorHeaderGeneralFields.hpp b/include/edfio/processor/ProcessorHeaderGeneralFields.hpp index 5f738de..b5fa827 100644 --- a/include/edfio/processor/ProcessorHeaderGeneralFields.hpp +++ b/include/edfio/processor/ProcessorHeaderGeneralFields.hpp @@ -9,315 +9,304 @@ #pragma once -#include "../Utils.hpp" +#include "../Errors.hpp" #include "../core/DataFormat.hpp" #include "../header/HeaderGeneral.hpp" -#include "detail/ProcessorUtils.hpp" +#include "ProcessorUtils.hpp" #include -namespace edfio -{ +namespace edfio { - struct ProcessorHeaderGeneralFields - { - HeaderGeneral operator ()(HeaderGeneralFields in); - }; +inline HeaderGeneral ProcessHeaderGeneralFields(HeaderGeneralFields in) { + HeaderGeneral out; - inline HeaderGeneral ProcessorHeaderGeneralFields::operator()(HeaderGeneralFields in) - { - HeaderGeneral out; + if (/*detail::CheckFormatErrors(in.m_version()) + ||*/ + detail::CheckFormatErrors(in.m_patient()) || + detail::CheckFormatErrors(in.m_recording()) || + detail::CheckFormatErrors(in.m_startDate()) || + detail::CheckFormatErrors(in.m_startTime()) || + detail::CheckFormatErrors(in.m_headerSize()) || + detail::CheckFormatErrors(in.m_reserved()) || + detail::CheckFormatErrors(in.m_datarecordsFile()) || + detail::CheckFormatErrors(in.m_datarecordDuration()) || + detail::CheckFormatErrors(in.m_totalSignals())) { + throw std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); + } - if (/*detail::CheckFormatErrors(in.m_version()) - ||*/ detail::CheckFormatErrors(in.m_patient()) - || detail::CheckFormatErrors(in.m_recording()) - || detail::CheckFormatErrors(in.m_startDate()) - || detail::CheckFormatErrors(in.m_startTime()) - || detail::CheckFormatErrors(in.m_headerSize()) - || detail::CheckFormatErrors(in.m_reserved()) - || detail::CheckFormatErrors(in.m_datarecordsFile()) - || detail::CheckFormatErrors(in.m_datarecordDuration()) - || detail::CheckFormatErrors(in.m_totalSignals())) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } + // Version + { + auto &version = in.m_version(); + if (version.front() == -1) // BDF-file + { + if (version.substr(1) != "BIOSEMI") { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + out.m_version = DataFormat::Bdf; + } else // EDF-file + { + if (version != "0 ") { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + out.m_version = DataFormat::Edf; + } + } + // Patient Name + { + out.m_patient = in.m_patient(); + } + // Recording + { + out.m_recording = in.m_recording(); + } + // Start Date + { + auto &startdate = in.m_startDate(); + if ((startdate[2] != '.') || (startdate[5] != '.') || + !std::isdigit(startdate[0]) || !std::isdigit(startdate[1]) || + !std::isdigit(startdate[3]) || !std::isdigit(startdate[4]) || + !std::isdigit(startdate[6]) || !std::isdigit(startdate[7])) { + throw std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); + } + { + int day{}, month{}, year{}; + auto [p1, e1] = + std::from_chars(startdate.data(), startdate.data() + 2, day); + auto [p2, e2] = + std::from_chars(startdate.data() + 3, startdate.data() + 5, month); + auto [p3, e3] = + std::from_chars(startdate.data() + 6, startdate.data() + 8, year); + if (e1 != std::errc{} || e2 != std::errc{} || e3 != std::errc{} || + day < 1 || day > 31 || month < 1 || month > 12) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + year += year > 84 ? 1900 : 2000; + out.m_startDate = std::make_tuple(day, month, year); + } + } + // Start Time + { + auto &starttime = in.m_startTime(); + if ((starttime[2] != '.') || (starttime[5] != '.') || + !std::isdigit(starttime[0]) || !std::isdigit(starttime[1]) || + !std::isdigit(starttime[3]) || !std::isdigit(starttime[4]) || + !std::isdigit(starttime[6]) || !std::isdigit(starttime[7])) { + throw std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); + } + { + int hour{}, minute{}, second{}; + auto [p1, e1] = + std::from_chars(starttime.data(), starttime.data() + 2, hour); + auto [p2, e2] = + std::from_chars(starttime.data() + 3, starttime.data() + 5, minute); + auto [p3, e3] = + std::from_chars(starttime.data() + 6, starttime.data() + 8, second); + if (e1 != std::errc{} || e2 != std::errc{} || e3 != std::errc{} || + hour > 23 || minute > 59 || second > 59) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + out.m_startTime = std::make_tuple(hour, minute, second); + } + } + // Header Size + { + out.m_headerSize = detail::ParseInt( + in.m_headerSize(), GetError(FileErrc::FileContainsFormatErrors)); + } + // Reserved + { + auto &reserved = in.m_reserved(); + if (IsEdf(out.m_version)) { + if (reserved.find("EDF+C") != std::string::npos) { + out.m_version = DataFormat::EdfPlusC; + } else if (reserved.find("EDF+D") != std::string::npos) { + out.m_version = DataFormat::EdfPlusD; + } + } else if (IsBdf(out.m_version)) { + if (reserved.find("BDF+C") != std::string::npos) { + out.m_version = DataFormat::BdfPlusC; + } else if (reserved.find("BDF+D") != std::string::npos) { + out.m_version = DataFormat::BdfPlusD; + } + } + } + // Datarecords in File + { + out.m_datarecordsFile = detail::ParseLongLong( + in.m_datarecordsFile(), GetError(FileErrc::FileContainsFormatErrors)); + if (out.m_datarecordsFile < 0) { + throw std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); + } + } + // Datarecord Duration + { + double duration = + detail::ParseDouble(in.m_datarecordDuration(), + GetError(FileErrc::FileContainsFormatErrors)); + out.m_datarecordDuration = duration; + if (duration < 0) { + throw std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); + } + out.m_detail.m_fileDuration = + out.m_datarecordDuration * out.m_datarecordsFile; + } + // Number of signals + { + int signals = detail::ParseInt( + in.m_totalSignals(), GetError(FileErrc::FileContainsFormatErrors)); + if (signals <= 0 || (signals * 256 + 256) != out.m_headerSize) { + throw std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); + } + out.m_totalSignals = signals; + } + // Plus Fields + if (IsPlus(out.m_version)) { + // Patient Name + { + auto &details = out.m_detail; + std::vector fields; + std::string field; + std::istringstream f(out.m_patient); + while (std::getline(f, field, ' ')) { + fields.push_back(field); + } + if (fields.size() < 4) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + details.m_patientAdditional.clear(); - // Version - { - auto &version = in.m_version(); - if (version.front() == -1) // BDF-file - { - if (version.substr(1) != "BIOSEMI") - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - out.m_version = DataFormat::Bdf; - } - else // EDF-file - { - if (version != "0 ") - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - out.m_version = DataFormat::Edf; - } - } - // Patient Name - { - out.m_patient = in.m_patient(); - } - // Recording - { - out.m_recording = in.m_recording(); - } - // Start Date - { - auto& startdate = in.m_startDate(); - if ((startdate[2] != '.') || (startdate[5] != '.') - || !std::isdigit(startdate[0]) || !std::isdigit(startdate[1]) - || !std::isdigit(startdate[3]) || !std::isdigit(startdate[4]) - || !std::isdigit(startdate[6]) || !std::isdigit(startdate[7])) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - { - int day{}, month{}, year{}; - auto [p1, e1] = std::from_chars(startdate.data(), startdate.data() + 2, day); - auto [p2, e2] = std::from_chars(startdate.data() + 3, startdate.data() + 5, month); - auto [p3, e3] = std::from_chars(startdate.data() + 6, startdate.data() + 8, year); - if (e1 != std::errc{} || e2 != std::errc{} || e3 != std::errc{} - || day < 1 || day > 31 || month < 1 || month > 12) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - year += year > 84 ? 1900 : 2000; - out.m_startDate = std::make_tuple(day, month, year); - } - } - // Start Time - { - auto& starttime = in.m_startTime(); - if ((starttime[2] != '.') || (starttime[5] != '.') - || !std::isdigit(starttime[0]) || !std::isdigit(starttime[1]) - || !std::isdigit(starttime[3]) || !std::isdigit(starttime[4]) - || !std::isdigit(starttime[6]) || !std::isdigit(starttime[7])) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - { - int hour{}, minute{}, second{}; - auto [p1, e1] = std::from_chars(starttime.data(), starttime.data() + 2, hour); - auto [p2, e2] = std::from_chars(starttime.data() + 3, starttime.data() + 5, minute); - auto [p3, e3] = std::from_chars(starttime.data() + 6, starttime.data() + 8, second); - if (e1 != std::errc{} || e2 != std::errc{} || e3 != std::errc{} - || hour > 23 || minute > 59 || second > 59) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - out.m_startTime = std::make_tuple(hour, minute, second); - } - } - // Header Size - { - out.m_headerSize = detail::ParseInt(in.m_headerSize(), detail::GetError(FileErrc::FileContainsFormatErrors)); - } - // Reserved - { - auto &reserved = in.m_reserved(); - if (IsEdf(out.m_version)) - { - if (reserved.find("EDF+C") != std::string::npos) - { - out.m_version = DataFormat::EdfPlusC; - } - else if (reserved.find("EDF+D") != std::string::npos) - { - out.m_version = DataFormat::EdfPlusD; - } - } - else if (IsBdf(out.m_version)) - { - if (reserved.find("BDF+C") != std::string::npos) - { - out.m_version = DataFormat::BdfPlusC; - } - else if (reserved.find("BDF+D") != std::string::npos) - { - out.m_version = DataFormat::BdfPlusD; - } - } - } - // Datarecords in File - { - out.m_datarecordsFile = detail::ParseLongLong(in.m_datarecordsFile(), detail::GetError(FileErrc::FileContainsFormatErrors)); - if (out.m_datarecordsFile < 0) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - // Datarecord Duration - { - double duration = detail::ParseDouble(in.m_datarecordDuration(), detail::GetError(FileErrc::FileContainsFormatErrors)); - out.m_datarecordDuration = duration; - if (duration < 0) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - out.m_detail.m_fileDuration = out.m_datarecordDuration * out.m_datarecordsFile; - } - // Number of signals - { - int signals = detail::ParseInt(in.m_totalSignals(), detail::GetError(FileErrc::FileContainsFormatErrors)); - if (signals <= 0 || (signals * 256 + 256) != out.m_headerSize) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - out.m_totalSignals = signals; - } - // Plus Fields - if (IsPlus(out.m_version)) - { - // Patient Name - { - auto &details = out.m_detail; - std::vector fields; - std::string field; - std::istringstream f(out.m_patient); - while (std::getline(f, field, ' ')) - { - fields.push_back(field); - } - if (fields.size() < 4) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } + for (size_t i = 0; i < fields.size(); i++) { + auto &str = fields[i]; + std::replace(str.begin(), str.end(), '_', + ' '); // replace all '_' to ' ' + switch (i) { + case 0: // The code by which the patient is known in the hospital + // administration. + details.m_patientCode = str; + break; + case 1: // Sex + if (str == "F" || str == "M" || str == "X") { + details.m_gender = str; + } else { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + break; + case 2: // Birthdate in dd-MMM-yyyy format using the English 3-character + // abbreviations of the month in capitals. 02-AUG-1951 is OK, + // while 2-AUG-1951 is not. + details.m_birthdate = str; + break; + case 3: // The patients name. + details.m_patientName = str; + break; + default: // Additional information. + if (!details.m_patientAdditional.empty()) { + details.m_patientAdditional += detail::ADDITIONAL_SEPARATOR; + } + details.m_patientAdditional.append(str); + break; + } + } + } + // Recording + { + auto &details = out.m_detail; + std::vector fields; + std::string field; + std::istringstream f(out.m_recording); + while (std::getline(f, field, ' ')) { + fields.push_back(field); + } - details.m_patientAdditional.clear(); + if (fields.size() < 5) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } - for (size_t i = 0; i < fields.size(); i++) - { - auto& str = fields[i]; - std::replace(str.begin(), str.end(), '_', ' '); // replace all '_' to ' ' - switch (i) - { - case 0: // The code by which the patient is known in the hospital administration. - details.m_patientCode = str; - break; - case 1: // Sex - if (str == "F" || str == "M" || str == "X") - { - details.m_gender = str; - } - else - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - break; - case 2: // Birthdate in dd-MMM-yyyy format using the English 3-character abbreviations of the month in capitals. 02-AUG-1951 is OK, while 2-AUG-1951 is not. - details.m_birthdate = str; - break; - case 3: // The patients name. - details.m_patientName = str; - break; - default: // Additional information. - if (!details.m_patientAdditional.empty()) - { - details.m_patientAdditional += detail::ADDITIONAL_SEPARATOR; - } - details.m_patientAdditional.append(str); - break; - } - } - } - // Recording - { - auto &details = out.m_detail; - std::vector fields; - std::string field; - std::istringstream f(out.m_recording); - while (std::getline(f, field, ' ')) - { - fields.push_back(field); - } + details.m_recordingAdditional.clear(); - if (fields.size() < 5) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } + for (size_t i = 0; i < fields.size(); i++) { + auto &str = fields[i]; + std::replace(str.begin(), str.end(), '_', + ' '); // replace all '_' to ' ' + if (str != "X") { + switch (i) { + case 0: // The text 'Startdate'. + if (str != "Startdate") { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + break; + case 1: // The startdate itself in dd-MMM-yyyy format using the + // English 3-character abbreviations of the month in capitals: + // dd-MMM-yyyy (MMM = 'JAN' | 'FEV' | ...) + if (str.size() == 11 && str[2] == '-' && str[6] == '-') { + { + int day{}, year{}; + auto [p1, e1] = + std::from_chars(str.data(), str.data() + 2, day); + auto [p2, e2] = + std::from_chars(str.data() + 7, str.data() + 11, year); + int month = detail::GetMonthFromString( + std::string_view{str}.substr(3, 3)); + if (e1 != std::errc{} || e2 != std::errc{} || month == 0) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + out.m_startDate = std::make_tuple(day, month, year); + } + } else { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + break; + case 2: // The hospital administration code of the investigation, i.e. + // EEG number or PSG number. + details.m_admincode = str; + break; + case 3: // A code specifying the responsible investigator or + // technician. + details.m_technician = str; + break; + case 4: // A code specifying the used equipment. + details.m_equipment = str; + break; + default: // Additional information. + if (!details.m_recordingAdditional.empty()) { + details.m_recordingAdditional += detail::ADDITIONAL_SEPARATOR; + } + details.m_recordingAdditional.append(str); + break; + } + } + } + } + } - details.m_recordingAdditional.clear(); - - for (size_t i = 0; i < fields.size(); i++) - { - auto& str = fields[i]; - std::replace(str.begin(), str.end(), '_', ' '); // replace all '_' to ' ' - if (str != "X") - { - switch (i) - { - case 0: // The text 'Startdate'. - if (str != "Startdate") - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - break; - case 1: // The startdate itself in dd-MMM-yyyy format using the English 3-character abbreviations of the month in capitals: dd-MMM-yyyy (MMM = 'JAN' | 'FEV' | ...) - if (str.size() == 11 && str[2] == '-' && str[6] == '-') - { - { - int day{}, year{}; - auto [p1, e1] = std::from_chars(str.data(), str.data() + 2, day); - auto [p2, e2] = std::from_chars(str.data() + 7, str.data() + 11, year); - int month = detail::GetMonthFromString(std::string_view{str}.substr(3, 3)); - if (e1 != std::errc{} || e2 != std::errc{} || month == 0) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - out.m_startDate = std::make_tuple(day, month, year); - } - } - else - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - break; - case 2: // The hospital administration code of the investigation, i.e. EEG number or PSG number. - details.m_admincode = str; - break; - case 3: // A code specifying the responsible investigator or technician. - details.m_technician = str; - break; - case 4: // A code specifying the used equipment. - details.m_equipment = str; - break; - default: // Additional information. - if (!details.m_recordingAdditional.empty()) - { - details.m_recordingAdditional += detail::ADDITIONAL_SEPARATOR; - } - details.m_recordingAdditional.append(str); - break; - } - } - } - } - } - - out.m_patient = detail::ReduceString(out.m_patient); - out.m_recording = detail::ReduceString(out.m_recording); - out.m_reserved = detail::ReduceString(out.m_reserved); - out.m_detail.m_patientCode = detail::ReduceString(out.m_detail.m_patientCode); - out.m_detail.m_gender = detail::ReduceString(out.m_detail.m_gender); - out.m_detail.m_birthdate = detail::ReduceString(out.m_detail.m_birthdate); - out.m_detail.m_patientName = detail::ReduceString(out.m_detail.m_patientName); - out.m_detail.m_patientAdditional = detail::ReduceString(out.m_detail.m_patientAdditional); - out.m_detail.m_admincode = detail::ReduceString(out.m_detail.m_admincode); - out.m_detail.m_technician = detail::ReduceString(out.m_detail.m_technician); - out.m_detail.m_equipment = detail::ReduceString(out.m_detail.m_equipment); - out.m_detail.m_recordingAdditional = detail::ReduceString(out.m_detail.m_recordingAdditional); - - return out; - } + out.m_patient = detail::ReduceString(out.m_patient); + out.m_recording = detail::ReduceString(out.m_recording); + out.m_reserved = detail::ReduceString(out.m_reserved); + out.m_detail.m_patientCode = detail::ReduceString(out.m_detail.m_patientCode); + out.m_detail.m_gender = detail::ReduceString(out.m_detail.m_gender); + out.m_detail.m_birthdate = detail::ReduceString(out.m_detail.m_birthdate); + out.m_detail.m_patientName = detail::ReduceString(out.m_detail.m_patientName); + out.m_detail.m_patientAdditional = + detail::ReduceString(out.m_detail.m_patientAdditional); + out.m_detail.m_admincode = detail::ReduceString(out.m_detail.m_admincode); + out.m_detail.m_technician = detail::ReduceString(out.m_detail.m_technician); + out.m_detail.m_equipment = detail::ReduceString(out.m_detail.m_equipment); + out.m_detail.m_recordingAdditional = + detail::ReduceString(out.m_detail.m_recordingAdditional); + return out; } + +} // namespace edfio diff --git a/include/edfio/processor/ProcessorHeaderSignal.hpp b/include/edfio/processor/ProcessorHeaderSignal.hpp index 61e4d96..3960193 100644 --- a/include/edfio/processor/ProcessorHeaderSignal.hpp +++ b/include/edfio/processor/ProcessorHeaderSignal.hpp @@ -9,22 +9,18 @@ #pragma once -#include "../Utils.hpp" +#include "../Errors.hpp" #include "../core/DataFormat.hpp" #include "../header/HeaderSignal.hpp" -#include "detail/ProcessorUtils.hpp" +#include "ProcessorUtils.hpp" #include #include namespace edfio { -struct ProcessorHeaderSignal { - std::vector operator()(std::vector in); -}; - inline std::vector -ProcessorHeaderSignal::operator()(std::vector in) { +ProcessHeaderSignal(std::vector in) { std::vector out(in.size()); auto &signals = in; diff --git a/include/edfio/processor/ProcessorHeaderSignalFields.hpp b/include/edfio/processor/ProcessorHeaderSignalFields.hpp index 3b6495a..e50a507 100644 --- a/include/edfio/processor/ProcessorHeaderSignalFields.hpp +++ b/include/edfio/processor/ProcessorHeaderSignalFields.hpp @@ -9,291 +9,248 @@ #pragma once -#include "../Utils.hpp" +#include "../Errors.hpp" #include "../core/DataFormat.hpp" #include "../header/HeaderSignal.hpp" -#include "detail/ProcessorUtils.hpp" +#include "ProcessorUtils.hpp" -namespace edfio -{ +namespace edfio { - struct ProcessorHeaderSignalFields - { - ProcessorHeaderSignalFields(DataFormat version, double datarecordDuration) - : m_version(version) - , m_datarecordDuration(datarecordDuration) - {} - std::vector operator ()(std::vector in); - private: - DataFormat m_version; - double m_datarecordDuration; - }; +inline std::vector +ProcessHeaderSignalFields(std::vector in, + DataFormat version, double datarecordDuration) { + std::vector signals(in.size()); - inline std::vector ProcessorHeaderSignalFields::operator()(std::vector in) - { - std::vector signals(in.size()); + for (auto &sigFields : in) { + if (detail::CheckFormatErrors(sigFields.m_label()) || + detail::CheckFormatErrors(sigFields.m_transducer()) || + detail::CheckFormatErrors(sigFields.m_physDimension()) || + detail::CheckFormatErrors(sigFields.m_physicalMin()) || + detail::CheckFormatErrors(sigFields.m_physicalMax()) || + detail::CheckFormatErrors(sigFields.m_digitalMin()) || + detail::CheckFormatErrors(sigFields.m_digitalMax()) || + detail::CheckFormatErrors(sigFields.m_prefilter()) || + detail::CheckFormatErrors(sigFields.m_samplesInDataRecord()) || + detail::CheckFormatErrors(sigFields.m_reserved())) { + throw std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); + } + } - for (auto& sigFields : in) - { - if (detail::CheckFormatErrors(sigFields.m_label()) - || detail::CheckFormatErrors(sigFields.m_transducer()) - || detail::CheckFormatErrors(sigFields.m_physDimension()) - || detail::CheckFormatErrors(sigFields.m_physicalMin()) - || detail::CheckFormatErrors(sigFields.m_physicalMax()) - || detail::CheckFormatErrors(sigFields.m_digitalMin()) - || detail::CheckFormatErrors(sigFields.m_digitalMax()) - || detail::CheckFormatErrors(sigFields.m_prefilter()) - || detail::CheckFormatErrors(sigFields.m_samplesInDataRecord()) - || detail::CheckFormatErrors(sigFields.m_reserved())) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } + // Labels + { + size_t totalAnnotationChannels = 0; + for (size_t idx = 0; idx < signals.size(); idx++) { + auto &signal = signals[idx]; + auto &label = in[idx].m_label(); + if (IsPlus(version)) { + if (label.find("Annotation") != std::string::npos) { + totalAnnotationChannels++; + signal.m_detail.m_isAnnotation = true; + } else { + signal.m_detail.m_isAnnotation = false; + } + } else { + signal.m_detail.m_isAnnotation = false; + } + signal.m_label = label; + } + if (IsPlus(version) && totalAnnotationChannels == 0) { + throw std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); + } + // TODO: check if this is true + /*if (datarecordDuration < 1) + { + if (signals.size() != totalAnnotationChannels || !IsPlus(version)) + { + throw + std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); + } + }*/ + } + // Transducers Types + { + for (size_t idx = 0; idx < signals.size(); idx++) { + auto &signal = signals[idx]; + auto &transducer = in[idx].m_transducer(); - } + signal.m_transducer = transducer; - // Labels - { - size_t totalAnnotationChannels = 0; - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& label = in[idx].m_label(); - if (IsPlus(m_version)) - { - if (label.find("Annotation") != std::string::npos) - { - totalAnnotationChannels++; - signal.m_detail.m_isAnnotation = true; - } - else - { - signal.m_detail.m_isAnnotation = false; - } - } - else - { - signal.m_detail.m_isAnnotation = false; - } - signal.m_label = label; - } - if (IsPlus(m_version) && totalAnnotationChannels == 0) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - // TODO: check if this is true - /*if (m_datarecordDuration < 1) - { - if (signals.size() != totalAnnotationChannels || !IsPlus(m_version)) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - }*/ - } - // Transducers Types - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& transducer = in[idx].m_transducer(); + if (signal.m_detail.m_isAnnotation) { + if (transducer.find_first_not_of(' ') != std::string::npos) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + } + } + } + // Physical Dimensions + { + for (size_t idx = 0; idx < signals.size(); idx++) { + auto &signal = signals[idx]; + auto &physDimension = in[idx].m_physDimension(); - signal.m_transducer = transducer; + signal.m_physDimension = physDimension; + } + } + // Physical Minima + { + for (size_t idx = 0; idx < signals.size(); idx++) { + auto &signal = signals[idx]; + auto &physMin = in[idx].m_physicalMin(); + signal.m_physicalMin = detail::ParseDouble( + physMin, GetError(FileErrc::FileContainsFormatErrors)); + } + } + // Physical Maxima + { + for (size_t idx = 0; idx < signals.size(); idx++) { + auto &signal = signals[idx]; + auto &physMax = in[idx].m_physicalMax(); + signal.m_physicalMax = detail::ParseDouble( + physMax, GetError(FileErrc::FileContainsFormatErrors)); + } + } + // Digital Minima + { + for (size_t idx = 0; idx < signals.size(); idx++) { + auto &signal = signals[idx]; + auto &digMin = in[idx].m_digitalMin(); + int n = detail::ParseInt(digMin, + GetError(FileErrc::FileContainsFormatErrors)); - if (signal.m_detail.m_isAnnotation) - { - if (transducer.find_first_not_of(' ') != std::string::npos) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - } - } - // Physical Dimensions - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& physDimension = in[idx].m_physDimension(); + if (signal.m_detail.m_isAnnotation) { + if (IsEdf(version) && IsPlus(version)) { + if (n != -32768) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + } else if (IsBdf(version) && IsPlus(version)) { + if (n != -8388608) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + } + } else if (IsEdf(version)) { + if ((n > 32767) || (n < -32768)) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + } else if (IsBdf(version)) { + if ((n > 8388607) || (n < -8388608)) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + } + signal.m_digitalMin = n; + } + } + // Digital Maxima + { + for (size_t idx = 0; idx < signals.size(); idx++) { + auto &signal = signals[idx]; + auto &digMax = in[idx].m_digitalMax(); + int n = detail::ParseInt(digMax, + GetError(FileErrc::FileContainsFormatErrors)); - signal.m_physDimension = physDimension; - } - } - // Physical Minima - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& physMin = in[idx].m_physicalMin(); - signal.m_physicalMin = detail::ParseDouble(physMin, detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - // Physical Maxima - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& physMax = in[idx].m_physicalMax(); - signal.m_physicalMax = detail::ParseDouble(physMax, detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - // Digital Minima - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& digMin = in[idx].m_digitalMin(); - int n = detail::ParseInt(digMin, detail::GetError(FileErrc::FileContainsFormatErrors)); + if (signal.m_detail.m_isAnnotation) { + if (IsEdf(version) && IsPlus(version)) { + if (n != 32767) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + } else if (IsBdf(version) && IsPlus(version)) { + if (n != 8388607) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + } + } else if (IsEdf(version)) { + if ((n > 32767) || (n < -32768)) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + } else if (IsBdf(version)) { + if ((n > 8388607) || (n < -8388608)) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + } + signal.m_digitalMax = n; + if (signal.m_digitalMax < (signal.m_digitalMin + 1)) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + } + } + // Prefilter + { + for (size_t idx = 0; idx < signals.size(); idx++) { + auto &signal = signals[idx]; + auto &prefilter = in[idx].m_prefilter(); - if (signal.m_detail.m_isAnnotation) - { - if (IsEdf(m_version) && IsPlus(m_version)) - { - if (n != -32768) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - else if (IsBdf(m_version) && IsPlus(m_version)) - { - if (n != -8388608) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - } - else if (IsEdf(m_version)) - { - if ((n > 32767) || (n < -32768)) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - else if (IsBdf(m_version)) - { - if ((n > 8388607) || (n < -8388608)) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - signal.m_digitalMin = n; - } - } - // Digital Maxima - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& digMax = in[idx].m_digitalMax(); - int n = detail::ParseInt(digMax, detail::GetError(FileErrc::FileContainsFormatErrors)); + signal.m_prefilter = prefilter; - if (signal.m_detail.m_isAnnotation) - { - if (IsEdf(m_version) && IsPlus(m_version)) - { - if (n != 32767) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - else if (IsBdf(m_version) && IsPlus(m_version)) - { - if (n != 8388607) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - } - else if (IsEdf(m_version)) - { - if ((n > 32767) || (n < -32768)) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - else if (IsBdf(m_version)) - { - if ((n > 8388607) || (n < -8388608)) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - signal.m_digitalMax = n; - if (signal.m_digitalMax < (signal.m_digitalMin + 1)) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - } - // Prefilter - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& prefilter = in[idx].m_prefilter(); + if (signal.m_detail.m_isAnnotation) { + if (prefilter.find_first_not_of(' ') != std::string::npos) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + } + } + } + // Samples in each datarecord + { + for (size_t idx = 0; idx < signals.size(); idx++) { + auto &signal = signals[idx]; + auto &nrSamples = in[idx].m_samplesInDataRecord(); + int n = detail::ParseInt(nrSamples, + GetError(FileErrc::FileContainsFormatErrors)); - signal.m_prefilter = prefilter; + if (n < 1) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } + signal.m_samplesInDataRecord = n; + } + } + // Reserved + { + for (size_t idx = 0; idx < signals.size(); idx++) { + auto &signal = signals[idx]; + auto &reserved = in[idx].m_reserved(); + signal.m_reserved = reserved; + } + } + // Details + { + size_t n = 0; + for (size_t idx = 0; idx < signals.size(); idx++) { + auto &signal = signals[idx]; - if (signal.m_detail.m_isAnnotation) - { - if (prefilter.find_first_not_of(' ') != std::string::npos) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - } - } - // Samples in each datarecord - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& nrSamples = in[idx].m_samplesInDataRecord(); - int n = detail::ParseInt(nrSamples, detail::GetError(FileErrc::FileContainsFormatErrors)); + signal.m_detail.m_signalOffset = n; + if (IsBdf(version)) + n += signal.m_samplesInDataRecord * 3; + else if (IsEdf(version)) + n += signal.m_samplesInDataRecord * 2; - if (n < 1) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - signal.m_samplesInDataRecord = n; - } - } - // Reserved - { - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; - auto& reserved = in[idx].m_reserved(); - signal.m_reserved = reserved; - } - } - // Details - { - size_t n = 0; - for (size_t idx = 0; idx < signals.size(); idx++) - { - auto &signal = signals[idx]; + signal.m_detail.m_scaling = + (signal.m_physicalMax - signal.m_physicalMin) / + (signal.m_digitalMax - signal.m_digitalMin); + signal.m_detail.m_offset = + signal.m_physicalMin - + signal.m_detail.m_scaling * signal.m_digitalMin; + } + } - signal.m_detail.m_signalOffset = n; - if (IsBdf(m_version)) - n += signal.m_samplesInDataRecord * 3; - else if (IsEdf(m_version)) - n += signal.m_samplesInDataRecord * 2; - - signal.m_detail.m_scaling = (signal.m_physicalMax - signal.m_physicalMin) / (signal.m_digitalMax - signal.m_digitalMin); - signal.m_detail.m_offset = signal.m_physicalMin - signal.m_detail.m_scaling * signal.m_digitalMin; - } - } - - - for (auto &signal : signals) - { - signal.m_label = detail::ReduceString(signal.m_label); - signal.m_transducer = detail::ReduceString(signal.m_transducer); - signal.m_physDimension = detail::ReduceString(signal.m_physDimension); - signal.m_prefilter = detail::ReduceString(signal.m_prefilter); - signal.m_reserved = detail::ReduceString(signal.m_reserved); - } - - return signals; - } + for (auto &signal : signals) { + signal.m_label = detail::ReduceString(signal.m_label); + signal.m_transducer = detail::ReduceString(signal.m_transducer); + signal.m_physDimension = detail::ReduceString(signal.m_physDimension); + signal.m_prefilter = detail::ReduceString(signal.m_prefilter); + signal.m_reserved = detail::ReduceString(signal.m_reserved); + } + return signals; } + +} // namespace edfio diff --git a/include/edfio/processor/ProcessorSample.hpp b/include/edfio/processor/ProcessorSample.hpp index 2eff135..1a79dfe 100644 --- a/include/edfio/processor/ProcessorSample.hpp +++ b/include/edfio/processor/ProcessorSample.hpp @@ -9,53 +9,45 @@ #pragma once -#include "../core/SampleType.hpp" #include "../core/Record.hpp" +#include "../core/SampleType.hpp" + + +namespace edfio { + +template struct ProcessorSample { + using ProcType = typename Sample::type; + using DigiType = Sample::type; + using PhysType = Sample::type; + + ProcessorSample(double offset, double scaling, size_t sampleSize) + : m_offset(offset), m_scaling(scaling), m_sampleSize(sampleSize) {} -namespace edfio -{ - - template - struct ProcessorSample - { - using ProcType = typename impl::Sample::type; - using DigiType = impl::Sample::type; - using PhysType = impl::Sample::type; - - ProcessorSample(double offset, double scaling, size_t sampleSize) - : m_offset(offset) - , m_scaling(scaling) - , m_sampleSize(sampleSize) - { - } - - Record operator ()(ProcType sample); - - private: - const double m_offset; - const double m_scaling; - size_t m_sampleSize; - }; - - template - inline Record ProcessorSample::operator()(ProcType sample) - { - DigiType value = 0; - if (std::is_same::value) - value = static_cast(sample); - else - value = impl::ConvertSample(m_offset, m_scaling, sample); - - Record record(m_sampleSize); - auto it = record().begin(); - - for (int count = m_sampleSize; count > 0; count--) - { - unsigned char tmp = (value >> (count - 1) * 8); - *it++ = tmp; - } - - return record; - } + Record operator()(ProcType sample); +private: + const double m_offset; + const double m_scaling; + size_t m_sampleSize; +}; + +template +inline Record ProcessorSample::operator()(ProcType sample) { + DigiType value = 0; + if (std::is_same::value) + value = static_cast(sample); + else + value = ConvertSample(m_offset, m_scaling, sample); + + Record record(m_sampleSize); + auto it = record().begin(); + + for (int count = m_sampleSize; count > 0; count--) { + unsigned char tmp = (value >> (count - 1) * 8); + *it++ = tmp; + } + + return record; } + +} // namespace edfio diff --git a/include/edfio/processor/ProcessorSampleRecord.hpp b/include/edfio/processor/ProcessorSampleRecord.hpp index 7de74f6..d382e27 100644 --- a/include/edfio/processor/ProcessorSampleRecord.hpp +++ b/include/edfio/processor/ProcessorSampleRecord.hpp @@ -9,58 +9,50 @@ #pragma once -#include "../core/SampleType.hpp" #include "../core/Record.hpp" +#include "../core/SampleType.hpp" -namespace edfio -{ - template - struct ProcessorSampleRecord - { - using ProcType = typename impl::Sample::type; - using DigiType = impl::Sample::type; - using PhysType = impl::Sample::type; +namespace edfio { - ProcessorSampleRecord(double offset, double scaling) - : m_offset(offset) - , m_scaling(scaling) - { - } +template struct ProcessorSampleRecord { + using ProcType = typename Sample::type; + using DigiType = Sample::type; + using PhysType = Sample::type; - ProcType operator ()(Record record); + ProcessorSampleRecord(double offset, double scaling) + : m_offset(offset), m_scaling(scaling) {} - private: - const double m_offset; - const double m_scaling; - }; + ProcType operator()(Record record); - template - inline typename ProcessorSampleRecord::ProcType - ProcessorSampleRecord::operator()(Record record) - { - DigiType sample = 0; - auto const& bytes = record(); - std::size_t const nbytes = bytes.size(); +private: + const double m_offset; + const double m_scaling; +}; - // Assemble bytes (big-endian order as written by ProcessorSample) - for (std::size_t i = 0; i < nbytes; ++i) - { - sample <<= 8; - sample |= static_cast(bytes[i]); - } +template +inline typename ProcessorSampleRecord::ProcType +ProcessorSampleRecord::operator()(Record record) { + DigiType sample = 0; + auto const &bytes = record(); + std::size_t const nbytes = bytes.size(); - // Sign-extend: if high bit of the MSB is set, the value is negative - if (nbytes > 0 && nbytes < sizeof(DigiType)) - { - unsigned int sign_bit = 1u << (nbytes * 8 - 1); - if (sample & sign_bit) - sample |= ~((1 << (nbytes * 8)) - 1); - } + // Assemble bytes (big-endian order as written by ProcessorSample) + for (std::size_t i = 0; i < nbytes; ++i) { + sample <<= 8; + sample |= static_cast(bytes[i]); + } - if constexpr (std::is_same_v) - return sample; - return impl::ConvertSample(m_offset, m_scaling, sample); - } + // Sign-extend: if high bit of the MSB is set, the value is negative + if (nbytes > 0 && nbytes < sizeof(DigiType)) { + unsigned int sign_bit = 1u << (nbytes * 8 - 1); + if (sample & sign_bit) + sample |= ~((1 << (nbytes * 8)) - 1); + } + if constexpr (std::is_same_v) + return sample; + return ConvertSample(m_offset, m_scaling, sample); } + +} // namespace edfio diff --git a/include/edfio/processor/ProcessorTalRecord.hpp b/include/edfio/processor/ProcessorTalRecord.hpp index 37511e5..0935648 100644 --- a/include/edfio/processor/ProcessorTalRecord.hpp +++ b/include/edfio/processor/ProcessorTalRecord.hpp @@ -9,82 +9,67 @@ #pragma once -#include "../Utils.hpp" +#include "../Errors.hpp" #include "../core/Annotation.hpp" #include "../core/Record.hpp" #include -namespace edfio -{ +namespace edfio { - struct ProcessorTalRecord - { - std::vector operator ()(std::vector record, long long datarecord); - }; +inline std::vector ProcessTalRecord(std::vector record, + long long datarecord) { + std::vector out; - inline std::vector ProcessorTalRecord::operator()(std::vector record, long long datarecord) - { - std::vector out; + // Boundaries + auto first = record.begin(); + auto last = record.end(); - // Boundaries - auto first = record.begin(); - auto last = record.end(); + // TAL MUST start with '+' or '-' + if (*first != '+' && *first != '-') { + throw std::invalid_argument( + GetError(FileErrc::FileContainsInvalidAnnotations)); + } - // TAL MUST start with '+' or '-' - if (*first != '+' && *first != '-') - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsInvalidAnnotations)); - } + double start = 0; + double duration = 0; + bool onset = true; + for (auto it = first; it != last; it++) { + if (std::distance(first, it) > 0) { + // First field is for TAL onset + if (onset) { + if (*it == detail::DURATION_DIV || *it == detail::ANNOTATION_DIV) { + std::string tmp(first, it); + start = detail::ParseDouble( + tmp, GetError(FileErrc::FileContainsInvalidAnnotations)); + onset = false; - double start = 0; - double duration = 0; - bool onset = true; - for (auto it = first; it != last; it++) - { - if (std::distance(first, it) > 0) - { - // First field is for TAL onset - if (onset) - { - if (*it == detail::DURATION_DIV || *it == detail::ANNOTATION_DIV) - { - std::string tmp(first, it); - start = detail::ParseDouble(tmp, detail::GetError(FileErrc::FileContainsInvalidAnnotations)); - onset = false; - - first = it; - } - } - else if (*it == detail::ANNOTATION_DIV) - { - if (*first == detail::DURATION_DIV) - { - std::string tmp(first + 1, it); - duration = detail::ParseDouble(tmp, detail::GetError(FileErrc::FileContainsInvalidAnnotations)); - first = it; - } - else if (*first == detail::ANNOTATION_DIV) - { - std::string tmp(first + 1, it); - - // Check if this TAL is in fact just a timestamp, we don't want this - if (!tmp.empty()) - { - Annotation annot; - annot.m_start = start; - annot.m_duration = duration; - annot.m_annotation = tmp; - annot.m_datarecord = datarecord; - out.emplace_back(std::move(annot)); - } - first = it; - } - } - } - - } - return out; - } + first = it; + } + } else if (*it == detail::ANNOTATION_DIV) { + if (*first == detail::DURATION_DIV) { + std::string tmp(first + 1, it); + duration = detail::ParseDouble( + tmp, GetError(FileErrc::FileContainsInvalidAnnotations)); + first = it; + } else if (*first == detail::ANNOTATION_DIV) { + std::string tmp(first + 1, it); + // Check if this TAL is in fact just a timestamp, we don't want this + if (!tmp.empty()) { + Annotation annot; + annot.m_start = start; + annot.m_duration = duration; + annot.m_annotation = tmp; + annot.m_datarecord = datarecord; + out.emplace_back(std::move(annot)); + } + first = it; + } + } + } + } + return out; } + +} // namespace edfio diff --git a/include/edfio/processor/ProcessorTimeStamp.hpp b/include/edfio/processor/ProcessorTimeStamp.hpp index f1a6dfb..4a64dba 100644 --- a/include/edfio/processor/ProcessorTimeStamp.hpp +++ b/include/edfio/processor/ProcessorTimeStamp.hpp @@ -9,45 +9,38 @@ #pragma once -#include "../Utils.hpp" +#include "../Errors.hpp" #include "../core/Annotation.hpp" #include "../core/Record.hpp" -#include "detail/ProcessorUtils.hpp" +#include "ProcessorUtils.hpp" -namespace edfio -{ +namespace edfio { - struct ProcessorTimeStamp - { - Record operator ()(TimeStamp timestamp); - }; +inline Record ProcessTimeStamp(TimeStamp timestamp) { + std::string ts = detail::to_string_decimal(timestamp.m_start); - inline Record ProcessorTimeStamp::operator()(TimeStamp timestamp) - { - std::string ts = detail::to_string_decimal(timestamp.m_start); + if (ts.empty()) { + throw std::invalid_argument( + GetError(FileErrc::FileWriteInvalidAnnotations)); + } - if (ts.empty()) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileWriteInvalidAnnotations)); - } + size_t plusSignal = 0; + if (timestamp.m_start >= 0) + plusSignal = 1; - size_t plusSignal = 0; - if (timestamp.m_start >= 0) - plusSignal = 1; + Record record(plusSignal + ts.size() + 3); // 20 20 0 + auto it = record().begin(); - Record record(plusSignal + ts.size() + 3); // 20 20 0 - auto it = record().begin(); - - // timestamp - if (plusSignal) - *it++ = '+'; - std::move(ts.begin(), ts.end(), it); - it += ts.size(); - *it++ = 20; // 20 div - *it++ = 20; // 20 div - *it++ = 0; // 0 end - - return record; - } + // timestamp + if (plusSignal) + *it++ = '+'; + std::move(ts.begin(), ts.end(), it); + it += ts.size(); + *it++ = 20; // 20 div + *it++ = 20; // 20 div + *it++ = 0; // 0 end + return record; } + +} // namespace edfio diff --git a/include/edfio/processor/ProcessorTimeStampRecord.hpp b/include/edfio/processor/ProcessorTimeStampRecord.hpp index d8ce9e0..480bd7e 100644 --- a/include/edfio/processor/ProcessorTimeStampRecord.hpp +++ b/include/edfio/processor/ProcessorTimeStampRecord.hpp @@ -9,57 +9,48 @@ #pragma once -#include "../Defs.hpp" -#include "../Utils.hpp" +#include "../Errors.hpp" #include "../core/Annotation.hpp" #include "../core/Record.hpp" #include -namespace edfio -{ - - struct ProcessorTimeStampRecord - { - TimeStamp operator ()(Record record, long long datarecord); - }; - - inline TimeStamp edfio::ProcessorTimeStampRecord::operator()(Record record, long long datarecord) - { - TimeStamp timestamp; - timestamp.m_datarecord = datarecord; - auto& value = record(); - - // TimeStamp MUST start with '+' or '-' - if (value.front() != '+' && value.front() != '-') - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsInvalidAnnotations)); - } - - // Make sure it's a valid timestamp - static const std::vector comp = { detail::ANNOTATION_END , detail::ANNOTATION_DIV }; - auto result = std::find_first_of(value.begin(), value.end(), comp.begin(), comp.end()); - - if (result == value.end()) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsInvalidAnnotations)); - } - else - { - *result = 0; - char* end; - double start = std::strtod(value.data(), &end); - // On error - if (end == value.data()) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsInvalidAnnotations)); - } - else - { - timestamp.m_start = start; - } - } - return timestamp; - } - +namespace edfio { + +inline TimeStamp ProcessTimeStampRecord(Record record, + long long datarecord) { + TimeStamp timestamp; + timestamp.m_datarecord = datarecord; + auto &value = record(); + + // TimeStamp MUST start with '+' or '-' + if (value.front() != '+' && value.front() != '-') { + throw std::invalid_argument( + GetError(FileErrc::FileContainsInvalidAnnotations)); + } + + // Make sure it's a valid timestamp + static const std::vector comp = {detail::ANNOTATION_END, + detail::ANNOTATION_DIV}; + auto result = + std::find_first_of(value.begin(), value.end(), comp.begin(), comp.end()); + + if (result == value.end()) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsInvalidAnnotations)); + } else { + *result = 0; + char *end; + double start = std::strtod(value.data(), &end); + // On error + if (end == value.data()) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsInvalidAnnotations)); + } else { + timestamp.m_start = start; + } + } + return timestamp; } + +} // namespace edfio diff --git a/include/edfio/processor/ProcessorUtils.hpp b/include/edfio/processor/ProcessorUtils.hpp new file mode 100644 index 0000000..900b1e6 --- /dev/null +++ b/include/edfio/processor/ProcessorUtils.hpp @@ -0,0 +1,173 @@ +// +// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. +// +// Official repository: https://github.com/idotta/edfio +// + +#pragma once + +#include "..\Config.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace edfio { +template +inline bool CheckFormatErrors(const std::basic_string &str) { + if constexpr (Check == ProcessorErrorCheck::Permissive) { + return false; + } else { + for (auto c : str) { + if (!std::isprint(static_cast(c))) + return true; + } + return false; + } +} + +template +inline bool CheckFormatErrors(const std::vector &str) { + if constexpr (Check == ProcessorErrorCheck::Permissive) { + return false; + } else { + for (auto c : str) { + if (!std::isprint(static_cast(c))) + return true; + } + return false; + } +} + +} // namespace edfio + +namespace detail { + +inline constexpr char ADDITIONAL_SEPARATOR = '|'; +inline constexpr std::array MONTHS = { + "JAN", "FEB", "MAR", "APR", "MAY", "JUN", + "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; + +template +inline bool CheckFormatErrors(const std::basic_string &str) { + return CheckFormatErrors(str); +} + +template +inline bool CheckFormatErrors(const std::vector &str) { + return CheckFormatErrors(str); +} + +inline int GetMonthFromString(std::string_view str) { + for (size_t idx = 0; idx < MONTHS.size(); idx++) { + if (str == MONTHS[idx]) { + return idx + 1; + } + } + return 0; +} + +inline std::string GetStringFromMonth(size_t idx) { + if (idx >= 1 && idx <= 12) + return std::string{MONTHS[idx - 1]}; + return "JAN"; +} + +inline std::string ReduceString(std::string_view value) { + // Trim leading spaces + auto start = value.find_first_not_of(' '); + if (start == std::string::npos) + return ""; + // Trim trailing spaces + auto end = value.find_last_not_of(' '); + // Collapse interior runs of spaces to single space + std::string result; + result.reserve(end - start + 1); + bool prev_space = false; + for (size_t i = start; i <= end; ++i) { + if (value[i] == ' ') { + if (!prev_space) { + result += ' '; + prev_space = true; + } + } else { + result += value[i]; + prev_space = false; + } + } + return result; +} + +inline std::string_view GetFormatName(DataFormat format) { + if (format == DataFormat::Edf) + return "EDF"; + if (format == DataFormat::EdfPlusC) + return "EDF+C"; + if (format == DataFormat::EdfPlusD) + return "EDF+D"; + if (format == DataFormat::Bdf) + return "BDF"; + if (format == DataFormat::BdfPlusC) + return "BDF+C"; + if (format == DataFormat::BdfPlusD) + return "BDF+D"; + return ""; +} + +inline int ParseInt(std::string_view sv, const char *error_msg) { + while (!sv.empty() && sv.front() == ' ') + sv.remove_prefix(1); + while (!sv.empty() && sv.back() == ' ') + sv.remove_suffix(1); + int value{}; + auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), value); + if (ec != std::errc{}) + throw std::invalid_argument(error_msg); + return value; +} + +inline long long ParseLongLong(std::string_view sv, const char *error_msg) { + while (!sv.empty() && sv.front() == ' ') + sv.remove_prefix(1); + while (!sv.empty() && sv.back() == ' ') + sv.remove_suffix(1); + long long value{}; + auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), value); + if (ec != std::errc{}) + throw std::invalid_argument(error_msg); + return value; +} + +inline double ParseDouble(std::string_view sv, const char *error_msg) { + while (!sv.empty() && sv.front() == ' ') + sv.remove_prefix(1); + while (!sv.empty() && sv.back() == ' ') + sv.remove_suffix(1); + // std::from_chars accepts '-' but not '+' by standard; strip leading '+' + if (!sv.empty() && sv.front() == '+') + sv.remove_prefix(1); + double value{}; + auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), value); + if (ec != std::errc{}) + throw std::invalid_argument(error_msg); + return value; +} + +template inline std::string to_string_decimal(const T &t) { + std::string str{std::to_string(t)}; + std::replace(str.begin(), str.end(), ',', '.'); + int offset{1}; + if (str.find_last_not_of('0') == str.find('.')) { + offset = 0; + } + str.erase(str.find_last_not_of('0') + offset, std::string::npos); + return str; +} +} // namespace detail diff --git a/include/edfio/processor/detail/ProcessorUtils.hpp b/include/edfio/processor/detail/ProcessorUtils.hpp deleted file mode 100644 index dfb907a..0000000 --- a/include/edfio/processor/detail/ProcessorUtils.hpp +++ /dev/null @@ -1,192 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../../Config.hpp" - -#include -#include -#include -#include -#include -#include -#include - -namespace edfio -{ - - namespace impl - { - - template - inline bool CheckFormatErrors(const std::basic_string &str) - { - if constexpr (Check == ProcessorErrorCheck::Permissive) { - return false; - } else { - for (auto c : str) - { - if (!std::isprint(static_cast(c))) - return true; - } - return false; - } - } - - template - inline bool CheckFormatErrors(const std::vector &str) - { - if constexpr (Check == ProcessorErrorCheck::Permissive) { - return false; - } else { - for (auto c : str) - { - if (!std::isprint(static_cast(c))) - return true; - } - return false; - } - } - - } - - namespace detail - { - - inline constexpr char ADDITIONAL_SEPARATOR = '|'; - inline constexpr std::array MONTHS = { - "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" - }; - - template - inline bool CheckFormatErrors(const std::basic_string &str) - { - return impl::CheckFormatErrors(str); - } - - template - inline bool CheckFormatErrors(const std::vector &str) - { - return impl::CheckFormatErrors(str); - } - - inline int GetMonthFromString(std::string_view str) - { - for (size_t idx = 0; idx < MONTHS.size(); idx++) - { - if (str == MONTHS[idx]) - { - return idx + 1; - } - } - return 0; - } - - inline std::string GetStringFromMonth(size_t idx) - { - if (idx >= 1 && idx <= 12) - return std::string{ MONTHS[idx - 1] }; - return "JAN"; - } - - inline std::string ReduceString(std::string_view value) - { - // Trim leading spaces - auto start = value.find_first_not_of(' '); - if (start == std::string::npos) return ""; - // Trim trailing spaces - auto end = value.find_last_not_of(' '); - // Collapse interior runs of spaces to single space - std::string result; - result.reserve(end - start + 1); - bool prev_space = false; - for (size_t i = start; i <= end; ++i) { - if (value[i] == ' ') { - if (!prev_space) { - result += ' '; - prev_space = true; - } - } else { - result += value[i]; - prev_space = false; - } - } - return result; - } - - inline std::string_view GetFormatName(DataFormat format) - { - if (format == DataFormat::Edf) - return "EDF"; - if (format == DataFormat::EdfPlusC) - return "EDF+C"; - if (format == DataFormat::EdfPlusD) - return "EDF+D"; - if (format == DataFormat::Bdf) - return "BDF"; - if (format == DataFormat::BdfPlusC) - return "BDF+C"; - if (format == DataFormat::BdfPlusD) - return "BDF+D"; - return ""; - } - - inline int ParseInt(std::string_view sv, const char* error_msg) - { - while (!sv.empty() && sv.front() == ' ') sv.remove_prefix(1); - while (!sv.empty() && sv.back() == ' ') sv.remove_suffix(1); - int value{}; - auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), value); - if (ec != std::errc{}) - throw std::invalid_argument(error_msg); - return value; - } - - inline long long ParseLongLong(std::string_view sv, const char* error_msg) - { - while (!sv.empty() && sv.front() == ' ') sv.remove_prefix(1); - while (!sv.empty() && sv.back() == ' ') sv.remove_suffix(1); - long long value{}; - auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), value); - if (ec != std::errc{}) - throw std::invalid_argument(error_msg); - return value; - } - - inline double ParseDouble(std::string_view sv, const char* error_msg) - { - while (!sv.empty() && sv.front() == ' ') sv.remove_prefix(1); - while (!sv.empty() && sv.back() == ' ') sv.remove_suffix(1); - // std::from_chars accepts '-' but not '+' by standard; strip leading '+' - if (!sv.empty() && sv.front() == '+') sv.remove_prefix(1); - double value{}; - auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), value); - if (ec != std::errc{}) - throw std::invalid_argument(error_msg); - return value; - } - - template - inline std::string to_string_decimal(const T& t) - { - std::string str{ std::to_string(t) }; - std::replace(str.begin(), str.end(), ',', '.'); - int offset{ 1 }; - if (str.find_last_not_of('0') == str.find('.')) - { - offset = 0; - } - str.erase(str.find_last_not_of('0') + offset, std::string::npos); - return str; - } - - } - -} diff --git a/include/edfio/reader/ReaderHeaderExam.hpp b/include/edfio/reader/ReaderHeaderExam.hpp index 5d8a412..39176b4 100644 --- a/include/edfio/reader/ReaderHeaderExam.hpp +++ b/include/edfio/reader/ReaderHeaderExam.hpp @@ -9,7 +9,7 @@ #pragma once -#include "../Utils.hpp" +#include "../Errors.hpp" #include "../core/StreamIO.hpp" #include "../header/HeaderExam.hpp" #include "../processor/ProcessorHeaderExam.hpp" @@ -20,51 +20,41 @@ #include -namespace edfio -{ - - struct ReaderHeaderExam : Reader - { - HeaderExam operator ()(Stream &stream); - }; - - inline HeaderExam ReaderHeaderExam::operator ()(Stream &stream) - { - // Read general fields - ReaderHeaderGeneral readerGeneral; - auto generalFields = readerGeneral(stream); - // Process general fields - ProcessorHeaderGeneralFields procGeneralFields; - auto general = procGeneralFields(std::move(generalFields)); - - // Read signal fields - ReaderHeaderSignal readerSignals(general.m_totalSignals); - auto signalFields = readerSignals(stream); - // Process signal fields - ProcessorHeaderSignalFields procSignalFields(general.m_version, general.m_datarecordDuration); - auto signals = procSignalFields(std::move(signalFields)); - - // Process header exam - ProcessorHeaderExam procHeader; - auto header = procHeader(std::move(general), std::move(signals)); - - // File size - { - // get current position - auto position = stream.tellg(); - // get length of file - stream.seekg(0, stream.end); - long long length = stream.tellg(); - // send back to previous position - stream.seekg(position, stream.beg); - - if (length != (header.m_general.m_detail.m_recordSize * header.m_general.m_datarecordsFile + header.m_general.m_headerSize)) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileContainsFormatErrors)); - } - } - - return header; - } - +namespace edfio { + +inline HeaderExam ReadHeaderExam(Reader::Stream &stream) { + // Read general fields + auto generalFields = ReadHeaderGeneral(stream); + // Process general fields + auto general = ProcessHeaderGeneralFields(std::move(generalFields)); + + // Read signal fields + auto signalFields = ReadHeaderSignal(stream, general.m_totalSignals); + // Process signal fields + auto signals = ProcessHeaderSignalFields( + std::move(signalFields), general.m_version, general.m_datarecordDuration); + + // Process header exam + auto header = ProcessHeaderExam(std::move(general), std::move(signals)); + + // File size + { + // get current position + auto position = stream.tellg(); + // get length of file + stream.seekg(0, stream.end); + long long length = stream.tellg(); + // send back to previous position + stream.seekg(position, stream.beg); + + if (length != (header.m_general.m_detail.m_recordSize * + header.m_general.m_datarecordsFile + + header.m_general.m_headerSize)) { + throw std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); + } + } + + return header; } + +} // namespace edfio diff --git a/include/edfio/reader/ReaderHeaderGeneral.hpp b/include/edfio/reader/ReaderHeaderGeneral.hpp index 5d63ce3..2945cd0 100644 --- a/include/edfio/reader/ReaderHeaderGeneral.hpp +++ b/include/edfio/reader/ReaderHeaderGeneral.hpp @@ -9,47 +9,37 @@ #pragma once -#include "../Utils.hpp" +#include "../Errors.hpp" #include "../core/StreamIO.hpp" #include "../header/HeaderGeneral.hpp" #include -namespace edfio -{ - - struct ReaderHeaderGeneral : Reader - { - HeaderGeneralFields operator ()(Stream &stream); - }; - - inline HeaderGeneralFields ReaderHeaderGeneral::operator ()(Stream &stream) - { - HeaderGeneralFields hdr; - if (!stream || !stream.is_open()) - throw std::invalid_argument(detail::GetError(FileErrc::FileNotOpened)); - - stream.clear(); - stream.seekg(0, std::ios::beg); - - try - { - stream >> hdr.m_version; - stream >> hdr.m_patient; - stream >> hdr.m_recording; - stream >> hdr.m_startDate; - stream >> hdr.m_startTime; - stream >> hdr.m_headerSize; - stream >> hdr.m_reserved; - stream >> hdr.m_datarecordsFile; - stream >> hdr.m_datarecordDuration; - stream >> hdr.m_totalSignals; - } - catch (const std::exception&) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileReadError)); - } - return hdr; - } - +namespace edfio { + +inline HeaderGeneralFields ReadHeaderGeneral(Reader::Stream &stream) { + HeaderGeneralFields hdr; + if (!stream || !stream.is_open()) + throw std::invalid_argument(GetError(FileErrc::FileNotOpened)); + + stream.clear(); + stream.seekg(0, std::ios::beg); + + try { + stream >> hdr.m_version; + stream >> hdr.m_patient; + stream >> hdr.m_recording; + stream >> hdr.m_startDate; + stream >> hdr.m_startTime; + stream >> hdr.m_headerSize; + stream >> hdr.m_reserved; + stream >> hdr.m_datarecordsFile; + stream >> hdr.m_datarecordDuration; + stream >> hdr.m_totalSignals; + } catch (const std::exception &) { + throw std::invalid_argument(GetError(FileErrc::FileReadError)); + } + return hdr; } + +} // namespace edfio diff --git a/include/edfio/reader/ReaderHeaderSignal.hpp b/include/edfio/reader/ReaderHeaderSignal.hpp index b16f5e1..6b9c4af 100644 --- a/include/edfio/reader/ReaderHeaderSignal.hpp +++ b/include/edfio/reader/ReaderHeaderSignal.hpp @@ -9,7 +9,7 @@ #pragma once -#include "../Utils.hpp" +#include "../Errors.hpp" #include "../core/StreamIO.hpp" #include "../header/HeaderGeneral.hpp" #include "../header/HeaderSignal.hpp" @@ -17,55 +17,42 @@ #include #include -namespace edfio -{ - - struct ReaderHeaderSignal : Reader - { - ReaderHeaderSignal(size_t totalSignals) : m_totalSignals(totalSignals) {} - - std::vector operator ()(Stream &stream); - private: - size_t m_totalSignals = 0; - }; - - inline std::vector ReaderHeaderSignal::operator ()(Stream &stream) - { - std::vector signals(m_totalSignals); - if (!stream || !stream.is_open()) - throw std::invalid_argument(detail::GetError(FileErrc::FileNotOpened)); - - stream.clear(); - stream.seekg(256, std::ios::beg); - - try - { - for (auto &s : signals) - stream >> s.m_label; - for (auto &s : signals) - stream >> s.m_transducer; - for (auto &s : signals) - stream >> s.m_physDimension; - for (auto &s : signals) - stream >> s.m_physicalMin; - for (auto &s : signals) - stream >> s.m_physicalMax; - for (auto &s : signals) - stream >> s.m_digitalMin; - for (auto &s : signals) - stream >> s.m_digitalMax; - for (auto &s : signals) - stream >> s.m_prefilter; - for (auto &s : signals) - stream >> s.m_samplesInDataRecord; - for (auto &s : signals) - stream >> s.m_reserved; - } - catch (const std::exception&) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileReadError)); - } - return signals; - } - +namespace edfio { + +inline std::vector +ReadHeaderSignal(Reader::Stream &stream, size_t totalSignals) { + std::vector signals(totalSignals); + if (!stream || !stream.is_open()) + throw std::invalid_argument(GetError(FileErrc::FileNotOpened)); + + stream.clear(); + stream.seekg(256, std::ios::beg); + + try { + for (auto &s : signals) + stream >> s.m_label; + for (auto &s : signals) + stream >> s.m_transducer; + for (auto &s : signals) + stream >> s.m_physDimension; + for (auto &s : signals) + stream >> s.m_physicalMin; + for (auto &s : signals) + stream >> s.m_physicalMax; + for (auto &s : signals) + stream >> s.m_digitalMin; + for (auto &s : signals) + stream >> s.m_digitalMax; + for (auto &s : signals) + stream >> s.m_prefilter; + for (auto &s : signals) + stream >> s.m_samplesInDataRecord; + for (auto &s : signals) + stream >> s.m_reserved; + } catch (const std::exception &) { + throw std::invalid_argument(GetError(FileErrc::FileReadError)); + } + return signals; } + +} // namespace edfio diff --git a/include/edfio/sink/DataRecordSink.hpp b/include/edfio/sink/DataRecordSink.hpp index 7b0224e..c90d889 100644 --- a/include/edfio/sink/DataRecordSink.hpp +++ b/include/edfio/sink/DataRecordSink.hpp @@ -11,54 +11,46 @@ #include "RecordSink.hpp" -namespace edfio -{ - - class DataRecordSink : public RecordSink - { - public: - - using iterator = RecordSink::iterator; - using const_iterator = iterator; - using reverse_iterator = std::reverse_iterator; - using const_reverse_iterator = std::reverse_iterator; - - DataRecordSink() = delete; - - DataRecordSink(stream_type &stream, size_type recordSize, size_type storeSize, std::streamoff headerOffset) - : RecordSink(stream, recordSize, storeSize, headerOffset) - { - measure(); - } - - protected: - void measure() override - { - auto pos = m_stream.tellp(); - m_stream.seekp(0, std::ios::end); - size_type sz = m_stream.tellp(); - m_stream.seekp(pos); - if (sz < m_headerOffset) - throw std::invalid_argument("Invalid header"); - m_sinkSize = (sz - m_headerOffset) / m_recordSize; - } - - void save(size_type off, value_type value) override - { - measure(); - if (off >= m_sinkSize || off == -1) - { - m_stream.seekp(0, std::ios::end); - m_sinkSize++; - } - else - { - off = m_headerOffset + off * m_recordSize; - m_stream.seekp(off, std::ios::beg); - } - m_value() = std::move(value()); - m_stream << m_value; - } - }; - -} +namespace edfio { + +class DataRecordSink : public RecordSink { +public: + using iterator = RecordSink::iterator; + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + DataRecordSink() = delete; + + DataRecordSink(stream_type &stream, size_type recordSize, size_type storeSize, + std::streamoff headerOffset) + : RecordSink(stream, recordSize, storeSize, headerOffset) { + measure(); + } + +protected: + void measure() override { + auto pos = m_stream.tellp(); + m_stream.seekp(0, std::ios::end); + size_type sz = m_stream.tellp(); + m_stream.seekp(pos); + if (sz < m_headerOffset) + throw std::invalid_argument("Invalid header"); + m_sinkSize = (sz - m_headerOffset) / m_recordSize; + } + + void save(size_type off, value_type value) override { + measure(); + if (off >= m_sinkSize || off == -1) { + m_stream.seekp(0, std::ios::end); + m_sinkSize++; + } else { + off = m_headerOffset + off * m_recordSize; + m_stream.seekp(off, std::ios::beg); + } + m_value() = std::move(value()); + m_stream << m_value; + } +}; + +} // namespace edfio diff --git a/include/edfio/sink/RecordSink.hpp b/include/edfio/sink/RecordSink.hpp index 3863d5a..cc47f1b 100644 --- a/include/edfio/sink/RecordSink.hpp +++ b/include/edfio/sink/RecordSink.hpp @@ -9,265 +9,217 @@ #pragma once -#include "Sink.hpp" #include "../core/Record.hpp" +#include "Sink.hpp" + -#include #include +#include #include -namespace edfio -{ - - class RecordSink : public Sink, Record*, Record&, std::ofstream, std::output_iterator_tag> - { - using base_sink = Sink, Record*, Record&, std::ofstream, std::output_iterator_tag>; - public: - using typename base_sink::stream_type; - using typename base_sink::value_type; - using typename base_sink::pointer; - using typename base_sink::reference; - using typename base_sink::difference_type; - using typename base_sink::size_type; - - class iterator : public base_sink::iterator - { - std::optional m_offset; // nullopt = end - RecordSink *m_context = nullptr; - public: - - // Construction - iterator() = default; - - iterator(RecordSink *context, std::optional offset = std::nullopt) - : m_offset(offset) - , m_context(context) - { - } - - iterator(const iterator &it) - : m_offset(it.m_offset) - , m_context(it.m_context) - { - } - - // Assignment - iterator& operator=(value_type value) - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - if (!m_offset) - throw std::length_error("Cannot assign to end iterator"); - m_context->save(*m_offset, std::move(value)); - return *this; - } - - // Equality (!= auto-generated) - bool operator==(const iterator &it) const - { - return (m_offset == it.m_offset && m_context == it.m_context); - } - - // Three-way comparison (<, >, <=, >= auto-generated) - std::strong_ordering operator<=>(const iterator &it) const - { - if (m_context != it.m_context) - throw std::invalid_argument("Iterators incompatible"); - auto lhs = m_offset.value_or(m_context->size()); - auto rhs = it.m_offset.value_or(it.m_context->size()); - return lhs <=> rhs; - } - - // Pre-increment - iterator& operator++() - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - if (!m_offset) - throw std::length_error("Iterator not incrementable"); - if (++(*m_offset) >= m_context->size()) - m_offset = std::nullopt; - return *this; - } - // Post-increment - iterator operator++(int) - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - iterator tmp = *this; - ++*this; - return tmp; - } - // Pre-decrement - iterator& operator--() - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - if (m_offset && *m_offset == 0) - throw std::length_error("Iterator not decrementable"); - if (!m_offset) - m_offset = m_context->size() - 1; - else - (*m_offset)--; - return *this; - } - // Post-decrement - iterator operator--(int) - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - iterator tmp = *this; - --*this; - return tmp; - } - // Compound addition assignment - iterator& operator+=(size_type off) - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - if (!m_offset) - throw std::length_error("Iterator not incrementable"); - if (*m_offset + off > m_context->size()) - throw std::length_error("Iterator + offset out of range"); - if (*m_offset + off == m_context->size()) - m_offset = std::nullopt; - else - *m_offset += off; - return *this; - } - // Addition - iterator operator+(size_type off) const - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - iterator tmp = *this; - tmp += off; - return tmp; - } - // Compound subtraction assignment - iterator& operator-=(size_type off) - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - auto pos = m_offset.value_or(m_context->size()); - if (pos < off) - throw std::length_error("Iterator - offset out of range"); - m_offset = pos - off; - return *this; - } - // Subtraction - iterator operator-(size_type off) const - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - iterator tmp = *this; - tmp -= off; - return tmp; - } - difference_type operator-(iterator it) const - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - if (m_context != it.m_context) - throw std::invalid_argument("Iterators incompatible"); - auto lhs = static_cast(m_offset.value_or(m_context->size())); - auto rhs = static_cast(it.m_offset.value_or(it.m_context->size())); - return lhs - rhs; - } - - // Dereference - iterator& operator*() - { - return *this; - } - iterator* operator->() - { - return this; - } - - // Subscripting - iterator& operator[](size_type) - { - return *this; - } - }; - - using const_iterator = iterator; - using reverse_iterator = std::reverse_iterator; - using const_reverse_iterator = std::reverse_iterator; - - RecordSink() = delete; - - RecordSink(stream_type &stream, size_type recordSize, size_type sinkSize, std::streamoff headerOffset) - : sink_type(stream) - , m_recordSize(recordSize) - , m_headerOffset(headerOffset) - , m_value(recordSize) - { - } - - iterator begin() - { - return iterator(this, 0); - } - const_iterator begin() const - { - return const_iterator(const_cast(this), 0); - } - const_iterator cbegin() const - { - return const_iterator(const_cast(this), 0); - } - iterator end() - { - return iterator(this); - } - const_iterator end() const - { - return const_iterator(const_cast(this)); - } - const_iterator cend() const - { - return const_iterator(const_cast(this)); - } - reverse_iterator rbegin() - { - return reverse_iterator(end()); - } - const_reverse_iterator rbegin() const - { - return const_reverse_iterator(end()); - } - const_reverse_iterator crbegin() const - { - return const_reverse_iterator(cend()); - } - reverse_iterator rend() - { - return reverse_iterator(begin()); - } - const_reverse_iterator rend() const - { - return const_reverse_iterator(begin()); - } - const_reverse_iterator crend() const - { - return const_reverse_iterator(cbegin()); - } - - size_type size() const - { - return m_sinkSize; - } - - protected: - virtual void measure() = 0; - virtual void save(size_type off, value_type value) = 0; - - size_type m_recordSize; - size_type m_sinkSize; - std::streamoff m_headerOffset; - value_type m_value; - }; -} +namespace edfio { + +class RecordSink : public Sink, Record *, Record &, + std::ofstream, std::output_iterator_tag> { + using base_sink = Sink, Record *, Record &, + std::ofstream, std::output_iterator_tag>; + +public: + using typename base_sink::difference_type; + using typename base_sink::pointer; + using typename base_sink::reference; + using typename base_sink::size_type; + using typename base_sink::stream_type; + using typename base_sink::value_type; + + class iterator : public base_sink::iterator { + std::optional m_offset; // nullopt = end + RecordSink *m_context = nullptr; + + public: + // Construction + iterator() = default; + + iterator(RecordSink *context, + std::optional offset = std::nullopt) + : m_offset(offset), m_context(context) {} + + iterator(const iterator &it) + : m_offset(it.m_offset), m_context(it.m_context) {} + + // Assignment + iterator &operator=(value_type value) { + if (!m_context) + throw std::invalid_argument("Invalid context"); + if (!m_offset) + throw std::length_error("Cannot assign to end iterator"); + m_context->save(*m_offset, std::move(value)); + return *this; + } + + // Equality (!= auto-generated) + bool operator==(const iterator &it) const { + return (m_offset == it.m_offset && m_context == it.m_context); + } + + // Three-way comparison (<, >, <=, >= auto-generated) + std::strong_ordering operator<=>(const iterator &it) const { + if (m_context != it.m_context) + throw std::invalid_argument("Iterators incompatible"); + auto lhs = m_offset.value_or(m_context->size()); + auto rhs = it.m_offset.value_or(it.m_context->size()); + return lhs <=> rhs; + } + + // Pre-increment + iterator &operator++() { + if (!m_context) + throw std::invalid_argument("Invalid context"); + if (!m_offset) + throw std::length_error("Iterator not incrementable"); + if (++(*m_offset) >= m_context->size()) + m_offset = std::nullopt; + return *this; + } + // Post-increment + iterator operator++(int) { + if (!m_context) + throw std::invalid_argument("Invalid context"); + iterator tmp = *this; + ++*this; + return tmp; + } + // Pre-decrement + iterator &operator--() { + if (!m_context) + throw std::invalid_argument("Invalid context"); + if (m_offset && *m_offset == 0) + throw std::length_error("Iterator not decrementable"); + if (!m_offset) + m_offset = m_context->size() - 1; + else + (*m_offset)--; + return *this; + } + // Post-decrement + iterator operator--(int) { + if (!m_context) + throw std::invalid_argument("Invalid context"); + iterator tmp = *this; + --*this; + return tmp; + } + // Compound addition assignment + iterator &operator+=(size_type off) { + if (!m_context) + throw std::invalid_argument("Invalid context"); + if (!m_offset) + throw std::length_error("Iterator not incrementable"); + if (*m_offset + off > m_context->size()) + throw std::length_error("Iterator + offset out of range"); + if (*m_offset + off == m_context->size()) + m_offset = std::nullopt; + else + *m_offset += off; + return *this; + } + // Addition + iterator operator+(size_type off) const { + if (!m_context) + throw std::invalid_argument("Invalid context"); + iterator tmp = *this; + tmp += off; + return tmp; + } + // Compound subtraction assignment + iterator &operator-=(size_type off) { + if (!m_context) + throw std::invalid_argument("Invalid context"); + auto pos = m_offset.value_or(m_context->size()); + if (pos < off) + throw std::length_error("Iterator - offset out of range"); + m_offset = pos - off; + return *this; + } + // Subtraction + iterator operator-(size_type off) const { + if (!m_context) + throw std::invalid_argument("Invalid context"); + iterator tmp = *this; + tmp -= off; + return tmp; + } + difference_type operator-(iterator it) const { + if (!m_context) + throw std::invalid_argument("Invalid context"); + if (m_context != it.m_context) + throw std::invalid_argument("Iterators incompatible"); + auto lhs = + static_cast(m_offset.value_or(m_context->size())); + auto rhs = static_cast( + it.m_offset.value_or(it.m_context->size())); + return lhs - rhs; + } + + // Dereference + iterator &operator*() { return *this; } + iterator *operator->() { return this; } + + // Subscripting + iterator &operator[](size_type) { return *this; } + }; + + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + RecordSink() = delete; + + RecordSink(stream_type &stream, size_type recordSize, size_type sinkSize, + std::streamoff headerOffset) + : sink_type(stream), m_recordSize(recordSize), + m_headerOffset(headerOffset), m_value(recordSize) {} + + iterator begin() { return iterator(this, 0); } + const_iterator begin() const { + return const_iterator(const_cast(this), 0); + } + const_iterator cbegin() const { + return const_iterator(const_cast(this), 0); + } + iterator end() { return iterator(this); } + const_iterator end() const { + return const_iterator(const_cast(this)); + } + const_iterator cend() const { + return const_iterator(const_cast(this)); + } + reverse_iterator rbegin() { return reverse_iterator(end()); } + const_reverse_iterator rbegin() const { + return const_reverse_iterator(end()); + } + const_reverse_iterator crbegin() const { + return const_reverse_iterator(cend()); + } + reverse_iterator rend() { return reverse_iterator(begin()); } + const_reverse_iterator rend() const { + return const_reverse_iterator(begin()); + } + const_reverse_iterator crend() const { + return const_reverse_iterator(cbegin()); + } + + size_type size() const { return m_sinkSize; } + +protected: + virtual void measure() = 0; + virtual void save(size_type off, value_type value) = 0; + + size_type m_recordSize; + size_type m_sinkSize; + std::streamoff m_headerOffset; + value_type m_value; +}; + +} // namespace edfio diff --git a/include/edfio/sink/SignalRecordSink.hpp b/include/edfio/sink/SignalRecordSink.hpp index 24f097b..53171b8 100644 --- a/include/edfio/sink/SignalRecordSink.hpp +++ b/include/edfio/sink/SignalRecordSink.hpp @@ -11,69 +11,61 @@ #include "RecordSink.hpp" -namespace edfio -{ +namespace edfio { - class SignalRecordSink : public RecordSink - { - public: +class SignalRecordSink : public RecordSink { +public: + using iterator = RecordSink::iterator; + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; - using iterator = RecordSink::iterator; - using const_iterator = iterator; - using reverse_iterator = std::reverse_iterator; - using const_reverse_iterator = std::reverse_iterator; + SignalRecordSink() = delete; - SignalRecordSink() = delete; + SignalRecordSink(stream_type &stream, size_type recordSize, + size_type storeSize, std::streamoff headerOffset, + size_type datarecordSize, std::streamoff signalOffset) + : RecordSink(stream, recordSize, storeSize, headerOffset), + m_datarecordSize(datarecordSize), m_signalOffset(signalOffset) { + measure(); + } - SignalRecordSink(stream_type &stream, size_type recordSize, size_type storeSize, std::streamoff headerOffset, size_type datarecordSize, std::streamoff signalOffset) - : RecordSink(stream, recordSize, storeSize, headerOffset) - , m_datarecordSize(datarecordSize) - , m_signalOffset(signalOffset) - { - measure(); - } +protected: + void measure() override { + auto pos = m_stream.tellp(); + m_stream.seekp(0, std::ios::end); + size_type sz = m_stream.tellp(); + m_stream.seekp(pos); + if (sz < m_headerOffset) + throw std::invalid_argument("Invalid header"); - protected: - void measure() override - { - auto pos = m_stream.tellp(); - m_stream.seekp(0, std::ios::end); - size_type sz = m_stream.tellp(); - m_stream.seekp(pos); - if (sz < m_headerOffset) - throw std::invalid_argument("Invalid header"); - - size_type datarecords = (sz - m_headerOffset) / m_datarecordSize; - size_type offset = (sz - m_headerOffset) % m_datarecordSize; - - m_sinkSize = datarecords; - if (offset >= m_signalOffset) - m_sinkSize++; - } + size_type datarecords = (sz - m_headerOffset) / m_datarecordSize; + size_type offset = (sz - m_headerOffset) % m_datarecordSize; - void save(size_type off, value_type value) override - { - measure(); - if (off >= m_sinkSize || off == -1) - { - m_stream.seekp(0, std::ios::end); - size_type sz = m_stream.tellp(); - size_type offset = (sz - m_headerOffset) % m_datarecordSize; - if (offset < m_signalOffset) - throw std::invalid_argument("Invalid signal order"); - m_sinkSize++; - } - else - { - off = m_headerOffset + off * m_datarecordSize + m_signalOffset; - m_stream.seekp(off, std::ios::beg); - } - m_value() = std::move(value()); - m_stream << m_value; - } + m_sinkSize = datarecords; + if (offset >= m_signalOffset) + m_sinkSize++; + } - size_type m_datarecordSize; - std::streamoff m_signalOffset; - }; + void save(size_type off, value_type value) override { + measure(); + if (off >= m_sinkSize || off == -1) { + m_stream.seekp(0, std::ios::end); + size_type sz = m_stream.tellp(); + size_type offset = (sz - m_headerOffset) % m_datarecordSize; + if (offset < m_signalOffset) + throw std::invalid_argument("Invalid signal order"); + m_sinkSize++; + } else { + off = m_headerOffset + off * m_datarecordSize + m_signalOffset; + m_stream.seekp(off, std::ios::beg); + } + m_value() = std::move(value()); + m_stream << m_value; + } -} + size_type m_datarecordSize; + std::streamoff m_signalOffset; +}; + +} // namespace edfio diff --git a/include/edfio/sink/Sink.hpp b/include/edfio/sink/Sink.hpp index eae8cfb..5b70972 100644 --- a/include/edfio/sink/Sink.hpp +++ b/include/edfio/sink/Sink.hpp @@ -11,31 +11,27 @@ #include "../core/Device.hpp" -namespace edfio -{ +namespace edfio { - // A class created in order to have an easier way to write streams - // of specific data through their respective iterators. - template - class Sink : public Device - { - public: - using device_type = Device; - using sink_type = Sink; - using typename device_type::stream_type; - using typename device_type::value_type; - using typename device_type::pointer; - using typename device_type::reference; - using typename device_type::difference_type; - using typename device_type::size_type; - using iterator = typename device_type::iterator; +// A class created in order to have an easier way to write streams +// of specific data through their respective iterators. +template +class Sink : public Device { +public: + using device_type = Device; + using sink_type = Sink; + using typename device_type::difference_type; + using typename device_type::pointer; + using typename device_type::reference; + using typename device_type::size_type; + using typename device_type::stream_type; + using typename device_type::value_type; + using iterator = typename device_type::iterator; - Sink() = delete; + Sink() = delete; - Sink(stream_type &stream) - : device_type(stream) - { - } - }; + Sink(stream_type &stream) : device_type(stream) {} +}; -} +} // namespace edfio diff --git a/include/edfio/sink/detail/SinkUtils.hpp b/include/edfio/sink/detail/SinkUtils.hpp index 547de09..8099ce2 100644 --- a/include/edfio/sink/detail/SinkUtils.hpp +++ b/include/edfio/sink/detail/SinkUtils.hpp @@ -9,37 +9,39 @@ #pragma once -#include "../DataRecordSink.hpp" -#include "../SignalRecordSink.hpp" #include "../../header/HeaderGeneral.hpp" #include "../../header/HeaderSignal.hpp" +#include "../DataRecordSink.hpp" +#include "../SignalRecordSink.hpp" + -namespace edfio -{ - - namespace detail - { - - template - inline DataRecordSink CreateDataRecordSink(Stream &stream, const HeaderGeneral &general) - { - DataRecordSink::size_type recordSize = general.m_detail.m_recordSize; - DataRecordSink::size_type sinkSize = general.m_datarecordsFile; - std::streamoff headerSize = general.m_headerSize; - return DataRecordSink{ stream, recordSize, sinkSize, headerSize }; - } - - template - inline SignalRecordSink CreateSignalRecordSink(Stream &stream, const HeaderGeneral &general, const HeaderSignal &signal) - { - SignalRecordSink::size_type recordSize = general.m_detail.m_recordSize; - SignalRecordSink::size_type sinkSize = general.m_datarecordsFile; - std::streamoff headerSize = general.m_headerSize; - SignalRecordSink::size_type signalSize = signal.m_samplesInDataRecord * GetSampleBytes(general.m_version); - std::streamoff signalOff = signal.m_detail.m_signalOffset; - return SignalRecordSink{ stream, signalSize, sinkSize, headerSize, recordSize, signalOff }; - } - - } - -} \ No newline at end of file +namespace edfio { + +namespace detail { + +template +inline DataRecordSink CreateDataRecordSink(Stream &stream, + const HeaderGeneral &general) { + DataRecordSink::size_type recordSize = general.m_detail.m_recordSize; + DataRecordSink::size_type sinkSize = general.m_datarecordsFile; + std::streamoff headerSize = general.m_headerSize; + return DataRecordSink{stream, recordSize, sinkSize, headerSize}; +} + +template +inline SignalRecordSink CreateSignalRecordSink(Stream &stream, + const HeaderGeneral &general, + const HeaderSignal &signal) { + SignalRecordSink::size_type recordSize = general.m_detail.m_recordSize; + SignalRecordSink::size_type sinkSize = general.m_datarecordsFile; + std::streamoff headerSize = general.m_headerSize; + SignalRecordSink::size_type signalSize = + signal.m_samplesInDataRecord * GetSampleBytes(general.m_version); + std::streamoff signalOff = signal.m_detail.m_signalOffset; + return SignalRecordSink{stream, signalSize, sinkSize, + headerSize, recordSize, signalOff}; +} + +} // namespace detail + +} // namespace edfio \ No newline at end of file diff --git a/include/edfio/store/DatarecordStore.hpp b/include/edfio/store/DatarecordStore.hpp index 1de3a6c..22acff4 100644 --- a/include/edfio/store/DatarecordStore.hpp +++ b/include/edfio/store/DatarecordStore.hpp @@ -11,46 +11,37 @@ #include "RecordStore.hpp" -namespace edfio -{ - - class DataRecordStore : public RecordStore - { - public: - - using iterator = RecordStore::iterator; - using const_iterator = iterator; - using reverse_iterator = std::reverse_iterator; - using const_reverse_iterator = std::reverse_iterator; - - DataRecordStore() = delete; - - DataRecordStore(stream_type &stream, size_type recordSize, size_type storeSize, std::streamoff headerOffset) - : RecordStore(stream, recordSize, storeSize, headerOffset) - { - } - - protected: - - void load(size_type off) const override - { - if (off >= size()) - { - throw std::out_of_range("Iterator not dereferenceable"); - } - - if (!m_stream.good()) - m_stream.clear(); - - auto currentPos = m_stream.tellg(); - auto destPos = m_headerOffset + m_recordSize * off; - if (destPos != currentPos) - { - m_stream.seekg(destPos, std::ios::beg); - } - m_stream >> m_value; - - } - }; - -} +namespace edfio { + +class DataRecordStore : public RecordStore { +public: + using iterator = RecordStore::iterator; + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + DataRecordStore() = delete; + + DataRecordStore(stream_type &stream, size_type recordSize, + size_type storeSize, std::streamoff headerOffset) + : RecordStore(stream, recordSize, storeSize, headerOffset) {} + +protected: + void load(size_type off) const override { + if (off >= size()) { + throw std::out_of_range("Iterator not dereferenceable"); + } + + if (!m_stream.good()) + m_stream.clear(); + + auto currentPos = m_stream.tellg(); + auto destPos = m_headerOffset + m_recordSize * off; + if (destPos != currentPos) { + m_stream.seekg(destPos, std::ios::beg); + } + m_stream >> m_value; + } +}; + +} // namespace edfio diff --git a/include/edfio/store/RecordStore.hpp b/include/edfio/store/RecordStore.hpp index 13d0673..13f6f3e 100644 --- a/include/edfio/store/RecordStore.hpp +++ b/include/edfio/store/RecordStore.hpp @@ -9,252 +9,210 @@ #pragma once -#include "Store.hpp" #include "../core/Record.hpp" +#include "Store.hpp" +#include #include #include -#include - -namespace edfio -{ - - class RecordStore : public Store, Record const*, Record const&, std::ifstream, std::random_access_iterator_tag> - { - using base_store = Store, Record const*, Record const&, std::ifstream, std::random_access_iterator_tag>; - public: - using typename base_store::stream_type; - using typename base_store::value_type; - using typename base_store::pointer; - using typename base_store::reference; - using typename base_store::difference_type; - using typename base_store::size_type; - - class iterator : public base_store::iterator - { - size_type m_offset = 0; // Relative to total of Stores - const RecordStore *m_context = nullptr; - public: - - // Construction - iterator() = default; - - iterator(const RecordStore *context, size_type offset = 0) - : m_offset(offset) - , m_context(context) - { - } - - iterator(const iterator &it) = default; - iterator& operator=(const iterator &it) = default; - - // Equality (!= auto-generated) - bool operator==(const iterator &it) const - { - return (m_offset == it.m_offset && m_context == it.m_context); - } - - // Three-way comparison (<, >, <=, >= auto-generated) - std::strong_ordering operator<=>(const iterator &it) const - { - if (m_context != it.m_context) - throw std::invalid_argument("Iterators incompatible"); - return m_offset <=> it.m_offset; - } - - // Pre-increment - iterator& operator++() - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - if (m_context->size() <= 0 || m_offset + 1 > m_context->size()) - throw std::length_error("Iterator not incrementable"); - m_offset++; - return *this; - } - // Post-increment - iterator operator++(int) - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - iterator tmp = *this; - ++*this; - return tmp; - } - // Pre-decrement - iterator& operator--() - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - if (m_context->size() <= 0 || m_offset == 0) - throw std::length_error("Iterator not decrementable"); - m_offset--; - return *this; - } - // Post-decrement - iterator operator--(int) - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - iterator tmp = *this; - --*this; - return tmp; - } - // Compound addition assignment - iterator& operator+=(difference_type n) - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - if (n < 0) return *this -= static_cast(-n); - auto off = static_cast(n); - if (m_offset + off > m_context->size()) - throw std::length_error("Iterator + offset out of range"); - m_offset += off; - return *this; - } - // Addition - iterator operator+(difference_type n) const - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - iterator tmp = *this; - tmp += n; - return tmp; - } - // Compound subtraction assignment - iterator& operator-=(difference_type n) - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - if (n < 0) return *this += static_cast(-n); - auto off = static_cast(n); - if (m_offset < off) - throw std::length_error("Iterator - offset out of range"); - m_offset -= off; - return *this; - } - // Subtraction - iterator operator-(difference_type n) const - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - iterator tmp = *this; - tmp -= n; - return tmp; - } - difference_type operator-(iterator it) const - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - if (m_context != it.m_context) - throw std::invalid_argument("Iterators incompatible"); - return difference_type(m_offset - it.m_offset); - } - - // Dereference - reference operator*() const - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - return m_context->getR(m_offset); - } - pointer operator->() const - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - return m_context->getP(m_offset); - } - - // Subscripting - reference operator[](difference_type n) const - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - iterator tmp = *this; - tmp += n; - return *tmp; - } - - // n + it (required for random_access_iterator) - friend iterator operator+(difference_type n, const iterator& it) - { - return it + n; - } - }; - - using const_iterator = iterator; - using reverse_iterator = std::reverse_iterator; - using const_reverse_iterator = std::reverse_iterator; - - RecordStore() = delete; - - RecordStore(stream_type &stream, size_type recordSize, size_type storeSize, std::streamoff headerOffset) - : store_type(stream) - , m_recordSize(recordSize) - , m_storeSize(storeSize) - , m_headerOffset(headerOffset) - , m_value(recordSize) - { - } - - iterator begin() const - { - return iterator(this); - } - const_iterator cbegin() const - { - return begin(); - } - iterator end() const - { - return iterator(this, size()); - } - const_iterator cend() const - { - return end(); - } - reverse_iterator rbegin() const - { - return reverse_iterator(end()); - } - const_reverse_iterator crbegin() const - { - return const_reverse_iterator(cend()); - } - reverse_iterator rend() const - { - return reverse_iterator(begin()); - } - const_reverse_iterator crend() const - { - return const_reverse_iterator(cbegin()); - } - - // Overrides - virtual size_type size() const - { - return m_storeSize; - } - - protected: - virtual reference getR(size_type off) const - { - load(off); - return m_value; - } - - virtual pointer getP(size_type off) const - { - load(off); - return &m_value; - } - - virtual void load(size_type off) const = 0; - size_type m_recordSize; - size_type m_storeSize; - std::streamoff m_headerOffset; - mutable value_type m_value; - }; -} +namespace edfio { + +class RecordStore + : public Store, Record const *, Record const &, + std::ifstream, std::random_access_iterator_tag> { + using base_store = + Store, Record const *, Record const &, + std::ifstream, std::random_access_iterator_tag>; + +public: + using typename base_store::difference_type; + using typename base_store::pointer; + using typename base_store::reference; + using typename base_store::size_type; + using typename base_store::stream_type; + using typename base_store::value_type; + + class iterator : public base_store::iterator { + size_type m_offset = 0; // Relative to total of Stores + const RecordStore *m_context = nullptr; + + public: + // Construction + iterator() = default; + + iterator(const RecordStore *context, size_type offset = 0) + : m_offset(offset), m_context(context) {} + + iterator(const iterator &it) = default; + iterator &operator=(const iterator &it) = default; + + // Equality (!= auto-generated) + bool operator==(const iterator &it) const { + return (m_offset == it.m_offset && m_context == it.m_context); + } + + // Three-way comparison (<, >, <=, >= auto-generated) + std::strong_ordering operator<=>(const iterator &it) const { + if (m_context != it.m_context) + throw std::invalid_argument("Iterators incompatible"); + return m_offset <=> it.m_offset; + } + + // Pre-increment + iterator &operator++() { + if (!m_context) + throw std::invalid_argument("Invalid context"); + if (m_context->size() <= 0 || m_offset + 1 > m_context->size()) + throw std::length_error("Iterator not incrementable"); + m_offset++; + return *this; + } + // Post-increment + iterator operator++(int) { + if (!m_context) + throw std::invalid_argument("Invalid context"); + iterator tmp = *this; + ++*this; + return tmp; + } + // Pre-decrement + iterator &operator--() { + if (!m_context) + throw std::invalid_argument("Invalid context"); + if (m_context->size() <= 0 || m_offset == 0) + throw std::length_error("Iterator not decrementable"); + m_offset--; + return *this; + } + // Post-decrement + iterator operator--(int) { + if (!m_context) + throw std::invalid_argument("Invalid context"); + iterator tmp = *this; + --*this; + return tmp; + } + // Compound addition assignment + iterator &operator+=(difference_type n) { + if (!m_context) + throw std::invalid_argument("Invalid context"); + if (n < 0) + return *this -= static_cast(-n); + auto off = static_cast(n); + if (m_offset + off > m_context->size()) + throw std::length_error("Iterator + offset out of range"); + m_offset += off; + return *this; + } + // Addition + iterator operator+(difference_type n) const { + if (!m_context) + throw std::invalid_argument("Invalid context"); + iterator tmp = *this; + tmp += n; + return tmp; + } + // Compound subtraction assignment + iterator &operator-=(difference_type n) { + if (!m_context) + throw std::invalid_argument("Invalid context"); + if (n < 0) + return *this += static_cast(-n); + auto off = static_cast(n); + if (m_offset < off) + throw std::length_error("Iterator - offset out of range"); + m_offset -= off; + return *this; + } + // Subtraction + iterator operator-(difference_type n) const { + if (!m_context) + throw std::invalid_argument("Invalid context"); + iterator tmp = *this; + tmp -= n; + return tmp; + } + difference_type operator-(iterator it) const { + if (!m_context) + throw std::invalid_argument("Invalid context"); + if (m_context != it.m_context) + throw std::invalid_argument("Iterators incompatible"); + return difference_type(m_offset - it.m_offset); + } + + // Dereference + reference operator*() const { + if (!m_context) + throw std::invalid_argument("Invalid context"); + return m_context->getR(m_offset); + } + pointer operator->() const { + if (!m_context) + throw std::invalid_argument("Invalid context"); + return m_context->getP(m_offset); + } + + // Subscripting + reference operator[](difference_type n) const { + if (!m_context) + throw std::invalid_argument("Invalid context"); + iterator tmp = *this; + tmp += n; + return *tmp; + } + + // n + it (required for random_access_iterator) + friend iterator operator+(difference_type n, const iterator &it) { + return it + n; + } + }; + + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + RecordStore() = delete; + + RecordStore(stream_type &stream, size_type recordSize, size_type storeSize, + std::streamoff headerOffset) + : store_type(stream), m_recordSize(recordSize), m_storeSize(storeSize), + m_headerOffset(headerOffset), m_value(recordSize) {} + + iterator begin() const { return iterator(this); } + const_iterator cbegin() const { return begin(); } + iterator end() const { return iterator(this, size()); } + const_iterator cend() const { return end(); } + reverse_iterator rbegin() const { return reverse_iterator(end()); } + const_reverse_iterator crbegin() const { + return const_reverse_iterator(cend()); + } + reverse_iterator rend() const { return reverse_iterator(begin()); } + const_reverse_iterator crend() const { + return const_reverse_iterator(cbegin()); + } + + // Overrides + virtual size_type size() const { return m_storeSize; } + +protected: + virtual reference getR(size_type off) const { + load(off); + return m_value; + } + + virtual pointer getP(size_type off) const { + load(off); + return &m_value; + } + + virtual void load(size_type off) const = 0; + + size_type m_recordSize; + size_type m_storeSize; + std::streamoff m_headerOffset; + mutable value_type m_value; +}; + +} // namespace edfio diff --git a/include/edfio/store/SignalSampleStore.hpp b/include/edfio/store/SignalSampleStore.hpp index 415968d..a1ad3a5 100644 --- a/include/edfio/store/SignalSampleStore.hpp +++ b/include/edfio/store/SignalSampleStore.hpp @@ -11,74 +11,66 @@ #include "RecordStore.hpp" -namespace edfio -{ - - class SignalSampleStore : public RecordStore - { - public: - - using iterator = RecordStore::iterator; - using const_iterator = iterator; - using reverse_iterator = std::reverse_iterator; - using const_reverse_iterator = std::reverse_iterator; - - SignalSampleStore() = delete; - - SignalSampleStore(stream_type &stream, size_type recordSize, size_type storeSize, - std::streamoff headerOffset, size_type datarecordSize, - size_type signalrecordSize, std::streamoff signalOffset) - : RecordStore(stream, recordSize, storeSize, headerOffset) - , m_datarecordSize(datarecordSize) - , m_signalrecordSize(signalrecordSize) - , m_signalOffset(signalOffset) - , m_buffer(recordSize * signalrecordSize) - , m_bufferPos(-1) - { - } - - protected: - - void load(size_type off) const override - { - if (off >= size()) - { - throw std::out_of_range("Iterator not dereferenceable"); - } - - std::streamoff dataRecordOffset = off / m_signalrecordSize; - std::streamoff sampleOffset = off % m_signalrecordSize; - std::streamoff destPos = m_headerOffset + dataRecordOffset * m_datarecordSize + m_signalOffset + sampleOffset * m_recordSize; - - if (m_bufferPos < 0 || (destPos < m_bufferPos || destPos >= m_bufferPos + m_buffer.Size())) - { - readStream(destPos); - m_bufferPos = m_headerOffset + dataRecordOffset * m_datarecordSize + m_signalOffset; - } - - auto first = m_buffer().begin() + sampleOffset * m_recordSize; - std::copy(first, first + m_value.Size(), m_value().begin()); - } - - void readStream(long long newPos) const - { - if (!m_stream.good()) - m_stream.clear(); - - auto oldPos = m_stream.tellg(); - if (newPos != oldPos) - { - m_stream.seekg(newPos, std::ios::beg); - } - m_stream >> m_buffer; - } - - size_type m_datarecordSize; - size_type m_signalrecordSize; - std::streamoff m_signalOffset; - // Samples buffer to decrease read requests - mutable value_type m_buffer; - mutable std::streamoff m_bufferPos; - }; - -} +namespace edfio { + +class SignalSampleStore : public RecordStore { +public: + using iterator = RecordStore::iterator; + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + SignalSampleStore() = delete; + + SignalSampleStore(stream_type &stream, size_type recordSize, + size_type storeSize, std::streamoff headerOffset, + size_type datarecordSize, size_type signalrecordSize, + std::streamoff signalOffset) + : RecordStore(stream, recordSize, storeSize, headerOffset), + m_datarecordSize(datarecordSize), m_signalrecordSize(signalrecordSize), + m_signalOffset(signalOffset), m_buffer(recordSize * signalrecordSize), + m_bufferPos(-1) {} + +protected: + void load(size_type off) const override { + if (off >= size()) { + throw std::out_of_range("Iterator not dereferenceable"); + } + + std::streamoff dataRecordOffset = off / m_signalrecordSize; + std::streamoff sampleOffset = off % m_signalrecordSize; + std::streamoff destPos = m_headerOffset + + dataRecordOffset * m_datarecordSize + + m_signalOffset + sampleOffset * m_recordSize; + + if (m_bufferPos < 0 || + (destPos < m_bufferPos || destPos >= m_bufferPos + m_buffer.Size())) { + readStream(destPos); + m_bufferPos = + m_headerOffset + dataRecordOffset * m_datarecordSize + m_signalOffset; + } + + auto first = m_buffer().begin() + sampleOffset * m_recordSize; + std::copy(first, first + m_value.Size(), m_value().begin()); + } + + void readStream(long long newPos) const { + if (!m_stream.good()) + m_stream.clear(); + + auto oldPos = m_stream.tellg(); + if (newPos != oldPos) { + m_stream.seekg(newPos, std::ios::beg); + } + m_stream >> m_buffer; + } + + size_type m_datarecordSize; + size_type m_signalrecordSize; + std::streamoff m_signalOffset; + // Samples buffer to decrease read requests + mutable value_type m_buffer; + mutable std::streamoff m_bufferPos; +}; + +} // namespace edfio diff --git a/include/edfio/store/SignalrecordStore.hpp b/include/edfio/store/SignalrecordStore.hpp index 73d1c62..531b520 100644 --- a/include/edfio/store/SignalrecordStore.hpp +++ b/include/edfio/store/SignalrecordStore.hpp @@ -11,52 +11,42 @@ #include "RecordStore.hpp" -namespace edfio -{ - - class SignalRecordStore : public RecordStore - { - public: - - using iterator = RecordStore::iterator; - using const_iterator = iterator; - using reverse_iterator = std::reverse_iterator; - using const_reverse_iterator = std::reverse_iterator; - - SignalRecordStore() = delete; - - SignalRecordStore(stream_type &stream, size_type recordSize, size_type storeSize, - std::streamoff headerOffset, size_type datarecordSize, std::streamoff signalOffset) - : RecordStore(stream, recordSize, storeSize, headerOffset) - , m_datarecordSize(datarecordSize) - , m_signalOffset(signalOffset) - { - } - - protected: - - void load(size_type off) const override - { - if (off >= size()) - { - throw std::out_of_range("Iterator not dereferenceable"); - } - - if (!m_stream.good()) - m_stream.clear(); - - auto currentPos = m_stream.tellg(); - auto destPos = m_headerOffset + m_datarecordSize * off + m_signalOffset; - if (destPos != currentPos) - { - m_stream.seekg(destPos, std::ios::beg); - } - m_stream >> m_value; - - } - - size_type m_datarecordSize; - std::streamoff m_signalOffset; - }; - -} +namespace edfio { + +class SignalRecordStore : public RecordStore { +public: + using iterator = RecordStore::iterator; + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + SignalRecordStore() = delete; + + SignalRecordStore(stream_type &stream, size_type recordSize, + size_type storeSize, std::streamoff headerOffset, + size_type datarecordSize, std::streamoff signalOffset) + : RecordStore(stream, recordSize, storeSize, headerOffset), + m_datarecordSize(datarecordSize), m_signalOffset(signalOffset) {} + +protected: + void load(size_type off) const override { + if (off >= size()) { + throw std::out_of_range("Iterator not dereferenceable"); + } + + if (!m_stream.good()) + m_stream.clear(); + + auto currentPos = m_stream.tellg(); + auto destPos = m_headerOffset + m_datarecordSize * off + m_signalOffset; + if (destPos != currentPos) { + m_stream.seekg(destPos, std::ios::beg); + } + m_stream >> m_value; + } + + size_type m_datarecordSize; + std::streamoff m_signalOffset; +}; + +} // namespace edfio diff --git a/include/edfio/store/Store.hpp b/include/edfio/store/Store.hpp index 4943164..8608f6a 100644 --- a/include/edfio/store/Store.hpp +++ b/include/edfio/store/Store.hpp @@ -11,32 +11,28 @@ #include "../core/Device.hpp" -namespace edfio -{ +namespace edfio { - // A class created in order to have an easier way to read streams - // of specific data through their respective iterators. - template - class Store : public Device - { - public: - using device_type = Device; - using store_type = Store; - using typename device_type::stream_type; - using typename device_type::value_type; - using typename device_type::pointer; - using typename device_type::reference; - using typename device_type::difference_type; - using typename device_type::size_type; - using iterator = typename device_type::iterator; - using const_iterator = iterator; +// A class created in order to have an easier way to read streams +// of specific data through their respective iterators. +template +class Store : public Device { +public: + using device_type = Device; + using store_type = Store; + using typename device_type::difference_type; + using typename device_type::pointer; + using typename device_type::reference; + using typename device_type::size_type; + using typename device_type::stream_type; + using typename device_type::value_type; + using iterator = typename device_type::iterator; + using const_iterator = iterator; - Store() = delete; + Store() = delete; - Store(stream_type &stream) - : device_type(stream) - { - } - }; + Store(stream_type &stream) : device_type(stream) {} +}; -} +} // namespace edfio diff --git a/include/edfio/store/TalStore.hpp b/include/edfio/store/TalStore.hpp index 7e2fe21..85eefe1 100644 --- a/include/edfio/store/TalStore.hpp +++ b/include/edfio/store/TalStore.hpp @@ -9,233 +9,188 @@ #pragma once -#include "Store.hpp" #include "../core/Record.hpp" +#include "Store.hpp" #include -namespace edfio -{ - - // TAL - Timestamped Annotation List - // TalStore is a particular kind of Store which iterates - // through a SignalRecordStore corresponding to an Annotation signal - // and dereferences a TAL - class TalStore : public Store::VectorType, Record::VectorType const*, Record::VectorType const&, const Record, std::bidirectional_iterator_tag> - { - using base_store = Store::VectorType, Record::VectorType const*, Record::VectorType const&, const Record, std::bidirectional_iterator_tag>; - public: - using typename base_store::stream_type; - using typename base_store::value_type; - using typename base_store::pointer; - using typename base_store::reference; - using typename base_store::difference_type; - using typename base_store::size_type; - - class iterator : public base_store::iterator - { - friend class TalStore; - size_type m_offset = 0; // Absolute position in current Store - const TalStore *m_context = nullptr; - public: - - // Construction - iterator() = default; - - protected: - iterator(const TalStore *context, size_type off) - : m_offset(off) - , m_context(context) - { - if (m_offset == 0) - ++*this; - } - - public: - iterator(const iterator &it) - : m_offset(it.m_offset) - , m_context(it.m_context) - { - } - - // Assignment - iterator& operator=(const iterator &it) - { - m_context = it.m_context; - m_offset = it.m_offset; - return *this; - } - - // Equality (!= auto-generated) - bool operator==(const iterator &it) const - { - return (m_offset == it.m_offset && m_context == it.m_context); - } - - // Pre-increment - iterator& operator++() - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - - m_offset = m_context->next(m_offset); - return *this; - } - // Post-increment - iterator operator++(int) - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - iterator tmp = *this; - ++*this; - return tmp; - } - // Pre-decrement - iterator& operator--() - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - - m_offset = m_context->prev(m_offset); - return *this; - } - // Post-decrement - iterator operator--(int) - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - iterator tmp = *this; - --*this; - return tmp; - } - - // Dereference - reference operator*() const - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - return m_context->getR(); - } - pointer operator->() const - { - if (!m_context) - throw std::invalid_argument("Invalid context"); - return m_context->getP(); - } - }; - - using const_iterator = iterator; - using reverse_iterator = std::reverse_iterator; - using const_reverse_iterator = std::reverse_iterator; - - TalStore() = delete; - - TalStore(stream_type &stream) - : store_type(stream) - { - } - - iterator begin() const - { - return iterator(this, 0); - } - const_iterator cbegin() const - { - return begin(); - } - iterator end() const - { - return iterator(this, m_stream.Size()); - } - const_iterator cend() const - { - return end(); - } - reverse_iterator rbegin() const - { - return reverse_iterator(end()); - } - const_reverse_iterator crbegin() const - { - return const_reverse_iterator(cend()); - } - reverse_iterator rend() const - { - return reverse_iterator(begin()); - } - const_reverse_iterator crend() const - { - return const_reverse_iterator(cbegin()); - } - - protected: - reference getR() const - { - return m_value; - } - - pointer getP() const - { - return &m_value; - } - - size_type next(size_type off) const - { - if (off >= m_stream().size()) - throw std::length_error("Iterator not incrementable"); - - if (off < m_stream().size()) - { - auto first = m_stream().begin() + off; - auto last = m_stream().end(); - - while (first != last && *first == 0) - { - first++; - off++; - } - - if (first != last) - { - size_type offOld = off; - for (auto it = first; *it != 0 && it != last; it++) - { - off++; - } - if (offOld != off) - { - m_value.assign(first, first + (off - offOld)); - } - } - } - return off; - } - - size_type prev(size_type off) const - { - if (off == 0) - throw std::length_error("Iterator not decrementable"); - - auto const& data = m_stream(); - - // Walk backward from position (off - 1), skipping zeros - size_type pos = off; - while (pos > 0 && data[pos - 1] == 0) - --pos; - - if (pos == 0) - throw std::length_error("Iterator not decrementable"); - - // Find the start of the previous non-zero TAL - size_type end_of_tal = pos; - while (pos > 0 && data[pos - 1] != 0) - --pos; - - m_value.assign(data.begin() + pos, data.begin() + end_of_tal); - return pos; - } - - mutable value_type m_value; - }; - -} +namespace edfio { + +// TAL - Timestamped Annotation List +// TalStore is a particular kind of Store which iterates +// through a SignalRecordStore corresponding to an Annotation signal +// and dereferences a TAL +class TalStore + : public Store::VectorType, Record::VectorType const *, + Record::VectorType const &, const Record, + std::bidirectional_iterator_tag> { + using base_store = + Store::VectorType, Record::VectorType const *, + Record::VectorType const &, const Record, + std::bidirectional_iterator_tag>; + +public: + using typename base_store::difference_type; + using typename base_store::pointer; + using typename base_store::reference; + using typename base_store::size_type; + using typename base_store::stream_type; + using typename base_store::value_type; + + class iterator : public base_store::iterator { + friend class TalStore; + size_type m_offset = 0; // Absolute position in current Store + const TalStore *m_context = nullptr; + + public: + // Construction + iterator() = default; + + protected: + iterator(const TalStore *context, size_type off) + : m_offset(off), m_context(context) { + if (m_offset == 0) + ++*this; + } + + public: + iterator(const iterator &it) + : m_offset(it.m_offset), m_context(it.m_context) {} + + // Assignment + iterator &operator=(const iterator &it) { + m_context = it.m_context; + m_offset = it.m_offset; + return *this; + } + + // Equality (!= auto-generated) + bool operator==(const iterator &it) const { + return (m_offset == it.m_offset && m_context == it.m_context); + } + + // Pre-increment + iterator &operator++() { + if (!m_context) + throw std::invalid_argument("Invalid context"); + + m_offset = m_context->next(m_offset); + return *this; + } + // Post-increment + iterator operator++(int) { + if (!m_context) + throw std::invalid_argument("Invalid context"); + iterator tmp = *this; + ++*this; + return tmp; + } + // Pre-decrement + iterator &operator--() { + if (!m_context) + throw std::invalid_argument("Invalid context"); + + m_offset = m_context->prev(m_offset); + return *this; + } + // Post-decrement + iterator operator--(int) { + if (!m_context) + throw std::invalid_argument("Invalid context"); + iterator tmp = *this; + --*this; + return tmp; + } + + // Dereference + reference operator*() const { + if (!m_context) + throw std::invalid_argument("Invalid context"); + return m_context->getR(); + } + pointer operator->() const { + if (!m_context) + throw std::invalid_argument("Invalid context"); + return m_context->getP(); + } + }; + + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + TalStore() = delete; + + TalStore(stream_type &stream) : store_type(stream) {} + + iterator begin() const { return iterator(this, 0); } + const_iterator cbegin() const { return begin(); } + iterator end() const { return iterator(this, m_stream.Size()); } + const_iterator cend() const { return end(); } + reverse_iterator rbegin() const { return reverse_iterator(end()); } + const_reverse_iterator crbegin() const { + return const_reverse_iterator(cend()); + } + reverse_iterator rend() const { return reverse_iterator(begin()); } + const_reverse_iterator crend() const { + return const_reverse_iterator(cbegin()); + } + +protected: + reference getR() const { return m_value; } + + pointer getP() const { return &m_value; } + + size_type next(size_type off) const { + if (off >= m_stream().size()) + throw std::length_error("Iterator not incrementable"); + + if (off < m_stream().size()) { + auto first = m_stream().begin() + off; + auto last = m_stream().end(); + + while (first != last && *first == 0) { + first++; + off++; + } + + if (first != last) { + size_type offOld = off; + for (auto it = first; *it != 0 && it != last; it++) { + off++; + } + if (offOld != off) { + m_value.assign(first, first + (off - offOld)); + } + } + } + return off; + } + + size_type prev(size_type off) const { + if (off == 0) + throw std::length_error("Iterator not decrementable"); + + auto const &data = m_stream(); + + // Walk backward from position (off - 1), skipping zeros + size_type pos = off; + while (pos > 0 && data[pos - 1] == 0) + --pos; + + if (pos == 0) + throw std::length_error("Iterator not decrementable"); + + // Find the start of the previous non-zero TAL + size_type end_of_tal = pos; + while (pos > 0 && data[pos - 1] != 0) + --pos; + + m_value.assign(data.begin() + pos, data.begin() + end_of_tal); + return pos; + } + + mutable value_type m_value; +}; + +} // namespace edfio diff --git a/include/edfio/store/TimeStampStore.hpp b/include/edfio/store/TimeStampStore.hpp index fbd765b..2a5311d 100644 --- a/include/edfio/store/TimeStampStore.hpp +++ b/include/edfio/store/TimeStampStore.hpp @@ -11,51 +11,42 @@ #include "RecordStore.hpp" -namespace edfio -{ - - class TimeStampStore : public RecordStore - { - public: - - using iterator = RecordStore::iterator; - using const_iterator = iterator; - using reverse_iterator = std::reverse_iterator; - using const_reverse_iterator = std::reverse_iterator; - - TimeStampStore() = delete; - - TimeStampStore(stream_type &stream, size_type recordSize, size_type storeSize, - std::streamoff headerOffset, size_type datarecordSize, std::streamoff signalOffset) - : RecordStore(stream, recordSize, storeSize, headerOffset) - , m_datarecordSize(datarecordSize) - , m_signalOffset(signalOffset) - { - } - - protected: - - void load(size_type off) const override - { - if (off >= size()) - { - throw std::out_of_range("Iterator not dereferenceable"); - } - - if (!m_stream.good()) - m_stream.clear(); - - auto currentPos = m_stream.tellg(); - auto destPos = m_headerOffset + m_datarecordSize * off + m_signalOffset; - if (destPos != currentPos) - { - m_stream.seekg(destPos, std::ios::beg); - } - m_stream.getline(m_value().data(), m_datarecordSize, 20); - } - - size_type m_datarecordSize; - std::streamoff m_signalOffset; - }; - -} +namespace edfio { + +class TimeStampStore : public RecordStore { +public: + using iterator = RecordStore::iterator; + using const_iterator = iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + TimeStampStore() = delete; + + TimeStampStore(stream_type &stream, size_type recordSize, size_type storeSize, + std::streamoff headerOffset, size_type datarecordSize, + std::streamoff signalOffset) + : RecordStore(stream, recordSize, storeSize, headerOffset), + m_datarecordSize(datarecordSize), m_signalOffset(signalOffset) {} + +protected: + void load(size_type off) const override { + if (off >= size()) { + throw std::out_of_range("Iterator not dereferenceable"); + } + + if (!m_stream.good()) + m_stream.clear(); + + auto currentPos = m_stream.tellg(); + auto destPos = m_headerOffset + m_datarecordSize * off + m_signalOffset; + if (destPos != currentPos) { + m_stream.seekg(destPos, std::ios::beg); + } + m_stream.getline(m_value().data(), m_datarecordSize, 20); + } + + size_type m_datarecordSize; + std::streamoff m_signalOffset; +}; + +} // namespace edfio diff --git a/include/edfio/store/detail/StoreUtils.hpp b/include/edfio/store/detail/StoreUtils.hpp index 6a00592..e4ecbca 100644 --- a/include/edfio/store/detail/StoreUtils.hpp +++ b/include/edfio/store/detail/StoreUtils.hpp @@ -9,64 +9,71 @@ #pragma once +#include "../../header/HeaderGeneral.hpp" +#include "../../header/HeaderSignal.hpp" #include "../DataRecordStore.hpp" #include "../SignalRecordStore.hpp" #include "../SignalSampleStore.hpp" #include "../TimeStampStore.hpp" -#include "../../header/HeaderGeneral.hpp" -#include "../../header/HeaderSignal.hpp" #include -namespace edfio -{ +namespace edfio { - namespace detail - { +namespace detail { - template - inline DataRecordStore CreateDataRecordStore(Stream &stream, const HeaderGeneral &general) - { - DataRecordStore::size_type recordSize = general.m_detail.m_recordSize; - DataRecordStore::size_type storeSize = general.m_datarecordsFile; - std::streamoff headerSize = general.m_headerSize; - return DataRecordStore{ stream, recordSize, storeSize, headerSize }; - } +template +inline DataRecordStore CreateDataRecordStore(Stream &stream, + const HeaderGeneral &general) { + DataRecordStore::size_type recordSize = general.m_detail.m_recordSize; + DataRecordStore::size_type storeSize = general.m_datarecordsFile; + std::streamoff headerSize = general.m_headerSize; + return DataRecordStore{stream, recordSize, storeSize, headerSize}; +} - template - inline SignalRecordStore CreateSignalRecordStore(Stream &stream, const HeaderGeneral &general, const HeaderSignal &signal) - { - SignalRecordStore::size_type recordSize = general.m_detail.m_recordSize; - SignalRecordStore::size_type storeSize = general.m_datarecordsFile; - std::streamoff headerSize = general.m_headerSize; - SignalRecordStore::size_type signalSize = signal.m_samplesInDataRecord * GetSampleBytes(general.m_version); - std::streamoff signalOff = signal.m_detail.m_signalOffset; - return SignalRecordStore{ stream, signalSize, storeSize, headerSize, recordSize, signalOff }; - } +template +inline SignalRecordStore CreateSignalRecordStore(Stream &stream, + const HeaderGeneral &general, + const HeaderSignal &signal) { + SignalRecordStore::size_type recordSize = general.m_detail.m_recordSize; + SignalRecordStore::size_type storeSize = general.m_datarecordsFile; + std::streamoff headerSize = general.m_headerSize; + SignalRecordStore::size_type signalSize = + signal.m_samplesInDataRecord * GetSampleBytes(general.m_version); + std::streamoff signalOff = signal.m_detail.m_signalOffset; + return SignalRecordStore{stream, signalSize, storeSize, + headerSize, recordSize, signalOff}; +} - template - inline SignalSampleStore CreateSignalSampleStore(Stream &stream, const HeaderGeneral &general, const HeaderSignal &signal) - { - SignalSampleStore::size_type recordSize = general.m_detail.m_recordSize; - SignalSampleStore::size_type storeSize = general.m_datarecordsFile * signal.m_samplesInDataRecord; - std::streamoff headerSize = general.m_headerSize; - SignalSampleStore::size_type sampleSize = GetSampleBytes(general.m_version); - SignalSampleStore::size_type signalSize = signal.m_samplesInDataRecord; - std::streamoff signalOff = signal.m_detail.m_signalOffset; - return SignalSampleStore{ stream, sampleSize, storeSize, headerSize, recordSize, signalSize, signalOff }; - } +template +inline SignalSampleStore CreateSignalSampleStore(Stream &stream, + const HeaderGeneral &general, + const HeaderSignal &signal) { + SignalSampleStore::size_type recordSize = general.m_detail.m_recordSize; + SignalSampleStore::size_type storeSize = + general.m_datarecordsFile * signal.m_samplesInDataRecord; + std::streamoff headerSize = general.m_headerSize; + SignalSampleStore::size_type sampleSize = GetSampleBytes(general.m_version); + SignalSampleStore::size_type signalSize = signal.m_samplesInDataRecord; + std::streamoff signalOff = signal.m_detail.m_signalOffset; + return SignalSampleStore{stream, sampleSize, storeSize, headerSize, + recordSize, signalSize, signalOff}; +} - template - inline TimeStampStore CreateTimeStampStore(Stream &stream, const HeaderGeneral &general, const HeaderSignal &signal) - { - TimeStampStore::size_type recordSize = general.m_detail.m_recordSize; - TimeStampStore::size_type storeSize = general.m_datarecordsFile; - std::streamoff headerSize = general.m_headerSize; - TimeStampStore::size_type signalSize = signal.m_samplesInDataRecord * GetSampleBytes(general.m_version); - std::streamoff signalOff = signal.m_detail.m_signalOffset; - return TimeStampStore{ stream, signalSize, storeSize, headerSize, recordSize, signalOff }; - } +template +inline TimeStampStore CreateTimeStampStore(Stream &stream, + const HeaderGeneral &general, + const HeaderSignal &signal) { + TimeStampStore::size_type recordSize = general.m_detail.m_recordSize; + TimeStampStore::size_type storeSize = general.m_datarecordsFile; + std::streamoff headerSize = general.m_headerSize; + TimeStampStore::size_type signalSize = + signal.m_samplesInDataRecord * GetSampleBytes(general.m_version); + std::streamoff signalOff = signal.m_detail.m_signalOffset; + return TimeStampStore{stream, signalSize, storeSize, + headerSize, recordSize, signalOff}; +} - } +} // namespace detail -} \ No newline at end of file +} // namespace edfio \ No newline at end of file diff --git a/include/edfio/writer/WriterHeaderExam.hpp b/include/edfio/writer/WriterHeaderExam.hpp index 96b62da..3515a7e 100644 --- a/include/edfio/writer/WriterHeaderExam.hpp +++ b/include/edfio/writer/WriterHeaderExam.hpp @@ -9,37 +9,30 @@ #pragma once +#include "../Errors.hpp" #include "../core/StreamIO.hpp" #include "../header/HeaderExam.hpp" #include "../processor/ProcessorHeaderGeneral.hpp" #include "../processor/ProcessorHeaderSignal.hpp" -#include "../Utils.hpp" #include "WriterHeaderGeneral.hpp" #include "WriterHeaderSignals.hpp" #include -namespace edfio -{ +namespace edfio { - struct WriterHeaderExam : Writer - { - void operator ()(Stream &stream, HeaderExam &input); - }; +inline void WriteHeaderExam(Writer::Stream &stream, HeaderExam &input) { + // Process header general + auto general = ProcessHeaderGeneral(input.m_general); - inline void WriterHeaderExam::operator ()(Stream &stream, HeaderExam &input) - { - // Process header general - auto general = ProcessorHeaderGeneral{}(input.m_general); + // Process signal fields + auto signals = ProcessHeaderSignal(input.m_signals); - // Process signal fields - auto signals = ProcessorHeaderSignal{}(input.m_signals); - - // Write general - WriterHeaderGeneral{}(stream, general); - - // Write signals - WriterHeaderSignals{}(stream, signals); - } + // Write general + WriteHeaderGeneral(stream, general); + // Write signals + WriteHeaderSignals(stream, signals); } + +} // namespace edfio diff --git a/include/edfio/writer/WriterHeaderGeneral.hpp b/include/edfio/writer/WriterHeaderGeneral.hpp index 6e2490e..f716c2b 100644 --- a/include/edfio/writer/WriterHeaderGeneral.hpp +++ b/include/edfio/writer/WriterHeaderGeneral.hpp @@ -9,47 +9,38 @@ #pragma once +#include "../Errors.hpp" #include "../core/StreamIO.hpp" #include "../header/HeaderGeneral.hpp" -#include "../Utils.hpp" #include #include -namespace edfio -{ - - struct WriterHeaderGeneral : Writer - { - void operator ()(Stream &stream, HeaderGeneralFields &input); - }; - - inline void WriterHeaderGeneral::operator()(Stream & stream, HeaderGeneralFields & input) - { - auto &hdr = input; - if (!stream || !stream.is_open()) - throw std::invalid_argument(detail::GetError(FileErrc::FileNotOpened)); - - stream.clear(); - stream.seekp(0, std::ios::beg); - - try - { - stream << hdr.m_version; - stream << hdr.m_patient; - stream << hdr.m_recording; - stream << hdr.m_startDate; - stream << hdr.m_startTime; - stream << hdr.m_headerSize; - stream << hdr.m_reserved; - stream << hdr.m_datarecordsFile; - stream << hdr.m_datarecordDuration; - stream << hdr.m_totalSignals; - } - catch (const std::exception&) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileWriteError)); - } - } - +namespace edfio { + +inline void WriteHeaderGeneral(Writer::Stream &stream, + HeaderGeneralFields &input) { + auto &hdr = input; + if (!stream || !stream.is_open()) + throw std::invalid_argument(GetError(FileErrc::FileNotOpened)); + + stream.clear(); + stream.seekp(0, std::ios::beg); + + try { + stream << hdr.m_version; + stream << hdr.m_patient; + stream << hdr.m_recording; + stream << hdr.m_startDate; + stream << hdr.m_startTime; + stream << hdr.m_headerSize; + stream << hdr.m_reserved; + stream << hdr.m_datarecordsFile; + stream << hdr.m_datarecordDuration; + stream << hdr.m_totalSignals; + } catch (const std::exception &) { + throw std::invalid_argument(GetError(FileErrc::FileWriteError)); + } } + +} // namespace edfio diff --git a/include/edfio/writer/WriterHeaderSignals.hpp b/include/edfio/writer/WriterHeaderSignals.hpp index eb8b001..bc167ee 100644 --- a/include/edfio/writer/WriterHeaderSignals.hpp +++ b/include/edfio/writer/WriterHeaderSignals.hpp @@ -9,55 +9,46 @@ #pragma once +#include "../Errors.hpp" #include "../core/StreamIO.hpp" #include "../header/HeaderSignal.hpp" -#include "../Utils.hpp" #include #include -namespace edfio -{ - - struct WriterHeaderSignals : Writer - { - void operator ()(Stream &stream, std::vector &signals); - }; - - inline void WriterHeaderSignals::operator()(Stream &stream, std::vector &signals) - { - if (!stream || !stream.is_open()) - throw std::invalid_argument(detail::GetError(FileErrc::FileNotOpened)); - try - { - stream.clear(); - stream.seekp(256, std::ios::beg); - - for (auto &s : signals) - stream << s.m_label; - for (auto &s : signals) - stream << s.m_transducer; - for (auto &s : signals) - stream << s.m_physDimension; - for (auto &s : signals) - stream << s.m_physicalMin; - for (auto &s : signals) - stream << s.m_physicalMax; - for (auto &s : signals) - stream << s.m_digitalMin; - for (auto &s : signals) - stream << s.m_digitalMax; - for (auto &s : signals) - stream << s.m_prefilter; - for (auto &s : signals) - stream << s.m_samplesInDataRecord; - for (auto &s : signals) - stream << s.m_reserved; - } - catch (const std::exception&) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileWriteError)); - } - } - +namespace edfio { + +inline void WriteHeaderSignals(Writer::Stream &stream, + std::vector &signals) { + if (!stream || !stream.is_open()) + throw std::invalid_argument(GetError(FileErrc::FileNotOpened)); + try { + stream.clear(); + stream.seekp(256, std::ios::beg); + + for (auto &s : signals) + stream << s.m_label; + for (auto &s : signals) + stream << s.m_transducer; + for (auto &s : signals) + stream << s.m_physDimension; + for (auto &s : signals) + stream << s.m_physicalMin; + for (auto &s : signals) + stream << s.m_physicalMax; + for (auto &s : signals) + stream << s.m_digitalMin; + for (auto &s : signals) + stream << s.m_digitalMax; + for (auto &s : signals) + stream << s.m_prefilter; + for (auto &s : signals) + stream << s.m_samplesInDataRecord; + for (auto &s : signals) + stream << s.m_reserved; + } catch (const std::exception &) { + throw std::invalid_argument(GetError(FileErrc::FileWriteError)); + } } + +} // namespace edfio diff --git a/include/edfio/writer/WriterRecord.hpp b/include/edfio/writer/WriterRecord.hpp deleted file mode 100644 index cf41184..0000000 --- a/include/edfio/writer/WriterRecord.hpp +++ /dev/null @@ -1,41 +0,0 @@ -// -// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) -// -// This source code is licensed under the MIT license found in the -// LICENSE file in the root directory of this source tree. -// -// Official repository: https://github.com/idotta/edfio -// - -#pragma once - -#include "../core/Record.hpp" -#include "../core/StreamIO.hpp" -#include "../Utils.hpp" - -#include - -namespace edfio -{ - - struct WriterRecord : Writer - { - void operator ()(Stream &stream, Record &record); - }; - - inline void WriterRecord::operator()(Stream &stream, Record &record) - { - if (!stream || !stream.is_open()) - throw std::invalid_argument(detail::GetError(FileErrc::FileNotOpened)); - - try - { - stream << record; - } - catch (const std::exception&) - { - throw std::invalid_argument(detail::GetError(FileErrc::FileWriteError)); - } - } - -} diff --git a/tests/test_iterators.cpp b/tests/test_iterators.cpp index d994dd5..92010d9 100644 --- a/tests/test_iterators.cpp +++ b/tests/test_iterators.cpp @@ -17,8 +17,7 @@ static_assert(std::ranges::bidirectional_range); TEST_CASE("RecordStore iterator subtraction and arithmetic (multi-record file)") { std::ifstream stream("test_generator_2.edf", std::ios::binary); REQUIRE(stream.is_open()); - ReaderHeaderExam reader; - auto header = reader(stream); + auto header = ReadHeaderExam(stream); auto store = detail::CreateDataRecordStore(stream, header.m_general); REQUIRE(store.size() == 600); @@ -35,8 +34,7 @@ TEST_CASE("RecordStore iterator subtraction and arithmetic (multi-record file)") TEST_CASE("RecordStore iterator n + it and negative offsets") { std::ifstream stream("test_generator_2.edf", std::ios::binary); REQUIRE(stream.is_open()); - ReaderHeaderExam reader; - auto header = reader(stream); + auto header = ReadHeaderExam(stream); auto store = detail::CreateDataRecordStore(stream, header.m_general); REQUIRE(store.size() >= 10); @@ -59,8 +57,7 @@ TEST_CASE("RecordStore iterator n + it and negative offsets") { TEST_CASE("DataRecordStore iteration works") { std::ifstream stream("test_generator_2.edf", std::ios::binary); REQUIRE(stream.is_open()); - ReaderHeaderExam reader; - auto header = reader(stream); + auto header = ReadHeaderExam(stream); auto store = detail::CreateDataRecordStore(stream, header.m_general); CHECK(store.size() == static_cast(header.m_general.m_datarecordsFile)); @@ -79,8 +76,7 @@ TEST_CASE("DataRecordStore iteration works") { TEST_CASE("RecordStore iterator comparisons via spaceship") { std::ifstream stream("test_generator_2.edf", std::ios::binary); REQUIRE(stream.is_open()); - ReaderHeaderExam reader; - auto header = reader(stream); + auto header = ReadHeaderExam(stream); auto store = detail::CreateDataRecordStore(stream, header.m_general); REQUIRE(store.size() >= 10); @@ -101,8 +97,7 @@ TEST_CASE("RecordStore iterator comparisons via spaceship") { TEST_CASE("RecordStore iterator subscript operator") { std::ifstream stream("test_generator_2.edf", std::ios::binary); REQUIRE(stream.is_open()); - ReaderHeaderExam reader; - auto header = reader(stream); + auto header = ReadHeaderExam(stream); auto store = detail::CreateDataRecordStore(stream, header.m_general); REQUIRE(store.size() >= 3); @@ -116,8 +111,7 @@ TEST_CASE("RecordStore iterator subscript operator") { TEST_CASE("SignalRecordStore iteration works") { std::ifstream stream("test_generator_2.edf", std::ios::binary); REQUIRE(stream.is_open()); - ReaderHeaderExam reader; - auto header = reader(stream); + auto header = ReadHeaderExam(stream); REQUIRE(header.m_signals.size() > 0); auto store = detail::CreateSignalRecordStore(stream, header.m_general, header.m_signals[0]); @@ -136,8 +130,7 @@ TEST_CASE("SignalRecordStore iteration works") { TEST_CASE("DataRecordStore satisfies ranges::random_access_range") { std::ifstream stream("test_generator_2.edf", std::ios::binary); REQUIRE(stream.is_open()); - ReaderHeaderExam reader; - auto header = reader(stream); + auto header = ReadHeaderExam(stream); auto store = detail::CreateDataRecordStore(stream, header.m_general); // Use std::ranges algorithms @@ -155,8 +148,7 @@ TEST_CASE("DataRecordStore satisfies ranges::random_access_range") { TEST_CASE("Const iteration works without const_cast issues") { std::ifstream stream("test_generator_2.edf", std::ios::binary); REQUIRE(stream.is_open()); - ReaderHeaderExam reader; - auto header = reader(stream); + auto header = ReadHeaderExam(stream); auto store = detail::CreateDataRecordStore(stream, header.m_general); // Call const begin/end diff --git a/tests/test_processor_utils.cpp b/tests/test_processor_utils.cpp index 040f839..9425607 100644 --- a/tests/test_processor_utils.cpp +++ b/tests/test_processor_utils.cpp @@ -85,9 +85,9 @@ TEST_CASE("ParseDouble parses floating-point numbers") { } TEST_CASE("GetError returns non-null for all error codes") { - CHECK(edfio::detail::GetError(edfio::FileErrc::FileDoesNotOpen) != nullptr); - CHECK(edfio::detail::GetError(edfio::FileErrc::FileNotOpened) != nullptr); - CHECK(edfio::detail::GetError(edfio::FileErrc::FileReadError) != nullptr); - CHECK(edfio::detail::GetError(edfio::FileErrc::FileContainsFormatErrors) != nullptr); - CHECK(edfio::detail::GetError(edfio::FileErrc::FileWriteError) != nullptr); + CHECK(edfio::GetError(edfio::FileErrc::FileDoesNotOpen) != nullptr); + CHECK(edfio::GetError(edfio::FileErrc::FileNotOpened) != nullptr); + CHECK(edfio::GetError(edfio::FileErrc::FileReadError) != nullptr); + CHECK(edfio::GetError(edfio::FileErrc::FileContainsFormatErrors) != nullptr); + CHECK(edfio::GetError(edfio::FileErrc::FileWriteError) != nullptr); } diff --git a/tests/test_reader.cpp b/tests/test_reader.cpp index ff29571..b3e2e98 100644 --- a/tests/test_reader.cpp +++ b/tests/test_reader.cpp @@ -8,8 +8,7 @@ using namespace edfio; TEST_CASE("Read Calib5.edf header successfully") { std::ifstream stream("Calib5.edf", std::ios::binary); REQUIRE(stream.is_open()); - ReaderHeaderExam reader; - auto header = reader(stream); + auto header = ReadHeaderExam(stream); CHECK(header.m_general.m_totalSignals > 0); CHECK(header.m_general.m_datarecordsFile > 0); @@ -21,8 +20,7 @@ TEST_CASE("Read Calib5.edf header successfully") { TEST_CASE("Calib5.edf is plain EDF format") { std::ifstream stream("Calib5.edf", std::ios::binary); REQUIRE(stream.is_open()); - ReaderHeaderExam reader; - auto header = reader(stream); + auto header = ReadHeaderExam(stream); CHECK(IsEdf(header.m_general.m_version)); CHECK_FALSE(IsBdf(header.m_general.m_version)); @@ -31,8 +29,7 @@ TEST_CASE("Calib5.edf is plain EDF format") { TEST_CASE("Calib5.edf signal headers are valid") { std::ifstream stream("Calib5.edf", std::ios::binary); REQUIRE(stream.is_open()); - ReaderHeaderExam reader; - auto header = reader(stream); + auto header = ReadHeaderExam(stream); for (auto const& sig : header.m_signals) { CHECK(sig.m_samplesInDataRecord > 0); diff --git a/tests/test_writer.cpp b/tests/test_writer.cpp index ea6c814..addd35c 100644 --- a/tests/test_writer.cpp +++ b/tests/test_writer.cpp @@ -10,8 +10,7 @@ TEST_CASE("Write and read back header round-trip") { // Read original std::ifstream instream("Calib5.edf", std::ios::binary); REQUIRE(instream.is_open()); - ReaderHeaderExam reader; - auto header = reader(instream); + auto header = ReadHeaderExam(instream); instream.close(); // Write to temp file @@ -19,8 +18,7 @@ TEST_CASE("Write and read back header round-trip") { { std::ofstream outstream(tmpfile, std::ios::binary); REQUIRE(outstream.is_open()); - WriterHeaderExam writer; - writer(outstream, header); + WriteHeaderExam(outstream, header); // Also write data records std::ifstream instream2("Calib5.edf", std::ios::binary); @@ -36,7 +34,7 @@ TEST_CASE("Write and read back header round-trip") { // Read back std::ifstream checkstream(tmpfile, std::ios::binary); REQUIRE(checkstream.is_open()); - auto header2 = reader(checkstream); + auto header2 = ReadHeaderExam(checkstream); CHECK(header2.m_general.m_totalSignals == header.m_general.m_totalSignals); CHECK(header2.m_general.m_datarecordsFile == header.m_general.m_datarecordsFile); From 343617d614c2db111e26b70d2eb84a5713692242 Mon Sep 17 00:00:00 2001 From: iuri dotta Date: Sun, 1 Mar 2026 09:51:46 -0300 Subject: [PATCH 05/11] Refactor: Update include paths and remove unused detail headers for cleaner structure --- include/edfio/EdfIO.hpp | 8 ++++---- include/edfio/processor/ProcessorUtils.hpp | 8 ++++---- include/edfio/sink/{detail => }/SinkUtils.hpp | 8 ++++---- include/edfio/store/{detail => }/StoreUtils.hpp | 12 ++++++------ 4 files changed, 18 insertions(+), 18 deletions(-) rename include/edfio/sink/{detail => }/SinkUtils.hpp (90%) rename include/edfio/store/{detail => }/StoreUtils.hpp (93%) diff --git a/include/edfio/EdfIO.hpp b/include/edfio/EdfIO.hpp index 2c74353..411de38 100644 --- a/include/edfio/EdfIO.hpp +++ b/include/edfio/EdfIO.hpp @@ -19,17 +19,17 @@ #include "writer/WriterHeaderExam.hpp" // Store -#include "store/DataRecordStore.hpp" -#include "store/SignalRecordStore.hpp" +#include "store/DatarecordStore.hpp" +#include "store/SignalrecordStore.hpp" #include "store/SignalSampleStore.hpp" #include "store/TalStore.hpp" #include "store/TimeStampStore.hpp" -#include "store/detail/StoreUtils.hpp" +#include "store/StoreUtils.hpp" // Sink #include "sink/DataRecordSink.hpp" #include "sink/SignalRecordSink.hpp" -#include "sink/detail/SinkUtils.hpp" +#include "sink/SinkUtils.hpp" // Processor #include "processor/ProcessorAnnotation.hpp" diff --git a/include/edfio/processor/ProcessorUtils.hpp b/include/edfio/processor/ProcessorUtils.hpp index 900b1e6..5412926 100644 --- a/include/edfio/processor/ProcessorUtils.hpp +++ b/include/edfio/processor/ProcessorUtils.hpp @@ -46,8 +46,6 @@ inline bool CheckFormatErrors(const std::vector &str) { } } -} // namespace edfio - namespace detail { inline constexpr char ADDITIONAL_SEPARATOR = '|'; @@ -57,12 +55,12 @@ inline constexpr std::array MONTHS = { template inline bool CheckFormatErrors(const std::basic_string &str) { - return CheckFormatErrors(str); + return edfio::CheckFormatErrors(str); } template inline bool CheckFormatErrors(const std::vector &str) { - return CheckFormatErrors(str); + return edfio::CheckFormatErrors(str); } inline int GetMonthFromString(std::string_view str) { @@ -171,3 +169,5 @@ template inline std::string to_string_decimal(const T &t) { return str; } } // namespace detail + +} // namespace edfio diff --git a/include/edfio/sink/detail/SinkUtils.hpp b/include/edfio/sink/SinkUtils.hpp similarity index 90% rename from include/edfio/sink/detail/SinkUtils.hpp rename to include/edfio/sink/SinkUtils.hpp index 8099ce2..687163e 100644 --- a/include/edfio/sink/detail/SinkUtils.hpp +++ b/include/edfio/sink/SinkUtils.hpp @@ -9,10 +9,10 @@ #pragma once -#include "../../header/HeaderGeneral.hpp" -#include "../../header/HeaderSignal.hpp" -#include "../DataRecordSink.hpp" -#include "../SignalRecordSink.hpp" +#include "../header/HeaderGeneral.hpp" +#include "../header/HeaderSignal.hpp" +#include "DataRecordSink.hpp" +#include "SignalRecordSink.hpp" namespace edfio { diff --git a/include/edfio/store/detail/StoreUtils.hpp b/include/edfio/store/StoreUtils.hpp similarity index 93% rename from include/edfio/store/detail/StoreUtils.hpp rename to include/edfio/store/StoreUtils.hpp index e4ecbca..d0f55db 100644 --- a/include/edfio/store/detail/StoreUtils.hpp +++ b/include/edfio/store/StoreUtils.hpp @@ -9,12 +9,12 @@ #pragma once -#include "../../header/HeaderGeneral.hpp" -#include "../../header/HeaderSignal.hpp" -#include "../DataRecordStore.hpp" -#include "../SignalRecordStore.hpp" -#include "../SignalSampleStore.hpp" -#include "../TimeStampStore.hpp" +#include "../header/HeaderGeneral.hpp" +#include "../header/HeaderSignal.hpp" +#include "DatarecordStore.hpp" +#include "SignalrecordStore.hpp" +#include "SignalSampleStore.hpp" +#include "TimeStampStore.hpp" #include From 565ee30dc50dab88e23d8b1e056c9622ec30afbd Mon Sep 17 00:00:00 2001 From: iuri dotta Date: Sun, 1 Mar 2026 10:21:50 -0300 Subject: [PATCH 06/11] Refactor: Replace long long with int64_t and size_t with uint32_t for consistency and improved type safety --- include/edfio/core/Annotation.hpp | 3 +- include/edfio/core/Device.hpp | 17 +++++----- include/edfio/core/Record.hpp | 4 +-- include/edfio/core/SampleType.hpp | 6 ++-- include/edfio/header/HeaderGeneral.hpp | 14 ++++---- include/edfio/header/HeaderSignal.hpp | 10 +++--- include/edfio/header/HeaderUtils.hpp | 32 +++++++++---------- .../edfio/processor/ProcessorHeaderExam.hpp | 4 ++- .../processor/ProcessorHeaderGeneral.hpp | 20 ++++++------ .../ProcessorHeaderGeneralFields.hpp | 11 ++++--- .../edfio/processor/ProcessorHeaderSignal.hpp | 2 -- .../processor/ProcessorHeaderSignalFields.hpp | 21 ++++++------ include/edfio/processor/ProcessorSample.hpp | 9 +++--- .../edfio/processor/ProcessorSampleRecord.hpp | 9 +++--- .../edfio/processor/ProcessorTalRecord.hpp | 5 ++- .../edfio/processor/ProcessorTimeStamp.hpp | 4 ++- .../processor/ProcessorTimeStampRecord.hpp | 3 +- include/edfio/processor/ProcessorUtils.hpp | 13 ++++---- include/edfio/reader/ReaderHeaderExam.hpp | 3 +- include/edfio/reader/ReaderHeaderSignal.hpp | 3 +- include/edfio/sink/DataRecordSink.hpp | 6 ++-- include/edfio/sink/RecordSink.hpp | 2 +- include/edfio/sink/SignalRecordSink.hpp | 18 +++++++---- include/edfio/store/SignalSampleStore.hpp | 6 ++-- tests/test_iterators.cpp | 11 ++++--- 25 files changed, 129 insertions(+), 107 deletions(-) diff --git a/include/edfio/core/Annotation.hpp b/include/edfio/core/Annotation.hpp index b9db3a9..f1d9c47 100644 --- a/include/edfio/core/Annotation.hpp +++ b/include/edfio/core/Annotation.hpp @@ -9,6 +9,7 @@ #pragma once +#include #include namespace edfio { @@ -21,7 +22,7 @@ static const char ANNOTATION_END = 0; } // namespace detail struct TimeStamp { - long long m_datarecord = 0; + int64_t m_datarecord = 0; double m_start = 0; }; diff --git a/include/edfio/core/Device.hpp b/include/edfio/core/Device.hpp index 0b9e9d0..9ea1db1 100644 --- a/include/edfio/core/Device.hpp +++ b/include/edfio/core/Device.hpp @@ -9,6 +9,7 @@ #pragma once +#include #include namespace edfio { @@ -19,14 +20,14 @@ template class Device { public: - typedef Stream stream_type; - typedef Device device_type; - - typedef Value value_type; - typedef Pointer pointer; - typedef Reference reference; - typedef long long difference_type; - typedef unsigned long long size_type; + using stream_type = Stream; + using device_type = Device; + + using value_type = Value; + using pointer = Pointer; + using reference = Reference; + using difference_type = int64_t; + using size_type = uint64_t; class iterator { public: diff --git a/include/edfio/core/Record.hpp b/include/edfio/core/Record.hpp index a4e6d7e..5041343 100644 --- a/include/edfio/core/Record.hpp +++ b/include/edfio/core/Record.hpp @@ -23,7 +23,7 @@ template struct Record { Record() = delete; - Record(size_t recordSize) : m_value(recordSize, 0) {} + Record(typename VectorType::size_type recordSize) : m_value(recordSize, 0) {} Record(typename VectorType::const_iterator first, typename VectorType::const_iterator last) @@ -34,7 +34,7 @@ template struct Record { Record &operator=(const Record &) = default; Record &operator=(Record &&) = default; - size_t Size() const { return m_value.size(); } + typename VectorType::size_type Size() const { return m_value.size(); } const VectorType &operator()() const { return m_value; } VectorType &operator()() { return m_value; } Record operator+(const Record &record) const { diff --git a/include/edfio/core/SampleType.hpp b/include/edfio/core/SampleType.hpp index 5b8e808..2aa8151 100644 --- a/include/edfio/core/SampleType.hpp +++ b/include/edfio/core/SampleType.hpp @@ -9,9 +9,7 @@ #pragma once -#include "../core/Record.hpp" -#include "../header/HeaderGeneral.hpp" -#include "../header/HeaderSignal.hpp" +#include namespace edfio { @@ -24,7 +22,7 @@ template <> struct Sample { }; template <> struct Sample { - using type = int; + using type = int32_t; }; inline Sample::type diff --git a/include/edfio/header/HeaderGeneral.hpp b/include/edfio/header/HeaderGeneral.hpp index 55f6a19..fede2d5 100644 --- a/include/edfio/header/HeaderGeneral.hpp +++ b/include/edfio/header/HeaderGeneral.hpp @@ -9,6 +9,8 @@ #pragma once +#include + #include "../core/DataFormat.hpp" #include "../core/Field.hpp" @@ -32,7 +34,7 @@ struct HeaderGeneralFields { namespace detail { struct HeaderGeneralDetail { - unsigned int m_recordSize = 0; + uint32_t m_recordSize = 0; double m_fileDuration = 0; std::string m_patientCode; @@ -52,13 +54,13 @@ struct HeaderGeneral { DataFormat m_version = DataFormat::Invalid; std::string m_patient; std::string m_recording; - std::tuple m_startDate; - std::tuple m_startTime; - int m_headerSize = 0; + std::tuple m_startDate; + std::tuple m_startTime; + int32_t m_headerSize = 0; std::string m_reserved; - long long m_datarecordsFile = 0; + int64_t m_datarecordsFile = 0; double m_datarecordDuration = 0; - int m_totalSignals = 0; + int32_t m_totalSignals = 0; // Extra values detail::HeaderGeneralDetail m_detail; }; diff --git a/include/edfio/header/HeaderSignal.hpp b/include/edfio/header/HeaderSignal.hpp index 56da1c7..c1f7278 100644 --- a/include/edfio/header/HeaderSignal.hpp +++ b/include/edfio/header/HeaderSignal.hpp @@ -9,6 +9,8 @@ #pragma once +#include + #include "../core/Field.hpp" #include @@ -30,7 +32,7 @@ struct HeaderSignalFields { namespace detail { struct HeaderSignalDetail { - long m_signalOffset = 0; + int64_t m_signalOffset = 0; double m_scaling = 0; double m_offset = 0; bool m_isAnnotation = false; @@ -44,10 +46,10 @@ struct HeaderSignal { std::string m_physDimension; double m_physicalMin = 0; double m_physicalMax = 0; - int m_digitalMin = 0; - int m_digitalMax = 0; + int32_t m_digitalMin = 0; + int32_t m_digitalMax = 0; std::string m_prefilter; - int m_samplesInDataRecord = 0; + int32_t m_samplesInDataRecord = 0; std::string m_reserved; // Extra values detail::HeaderSignalDetail m_detail; diff --git a/include/edfio/header/HeaderUtils.hpp b/include/edfio/header/HeaderUtils.hpp index a519e1a..bc750ef 100644 --- a/include/edfio/header/HeaderUtils.hpp +++ b/include/edfio/header/HeaderUtils.hpp @@ -9,10 +9,10 @@ #pragma once -#include "../Errors.hpp" #include "HeaderGeneral.hpp" #include "HeaderSignal.hpp" +#include #include #include @@ -20,13 +20,12 @@ namespace edfio { namespace detail { -inline HeaderGeneral -CreateHeaderGeneral(DataFormat version, std::string patient, - std::string recording, int startDateD, int startDateM, - int startDateY, int startTimeH, int startTimeM, - int startTimeS, int headerSize, std::string reserved, - long long datarecordsFile, double datarecordDuration, - const std::vector &signals) { +inline HeaderGeneral CreateHeaderGeneral( + DataFormat version, std::string patient, std::string recording, + int32_t startDateD, int32_t startDateM, int32_t startDateY, + int32_t startTimeH, int32_t startTimeM, int32_t startTimeS, + int32_t headerSize, std::string reserved, int64_t datarecordsFile, + double datarecordDuration, const std::vector &signals) { HeaderGeneral header; header.m_version = version; header.m_patient = patient; @@ -37,7 +36,7 @@ CreateHeaderGeneral(DataFormat version, std::string patient, header.m_reserved = reserved; header.m_datarecordsFile = datarecordsFile; header.m_datarecordDuration = datarecordDuration; - header.m_totalSignals = signals.size(); + header.m_totalSignals = static_cast(signals.size()); // Record size header.m_detail.m_recordSize = 0; @@ -54,10 +53,11 @@ inline HeaderGeneral CreateHeaderGeneralPlus( std::string birthdate, std::string patientName, std::string patientAdditional, std::string admincode, std::string technician, std::string equipment, - std::string recordingAdditional, int startDateD, int startDateM, - int startDateY, int startTimeH, int startTimeM, int startTimeS, - int headerSize, std::string reserved, long long datarecordsFile, - double datarecordDuration, const std::vector &signals) { + std::string recordingAdditional, int32_t startDateD, int32_t startDateM, + int32_t startDateY, int32_t startTimeH, int32_t startTimeM, + int32_t startTimeS, int32_t headerSize, std::string reserved, + int64_t datarecordsFile, double datarecordDuration, + const std::vector &signals) { auto header = CreateHeaderGeneral( version, "", "", startDateD, startDateM, startDateY, startTimeH, startTimeM, startTimeS, headerSize, reserved, datarecordsFile, @@ -77,9 +77,9 @@ inline HeaderGeneral CreateHeaderGeneralPlus( } inline HeaderSignal -CreateHeaderSignal(std::string label, int samplesInDataRecord, - double physicalMin, double physicalMax, int digitalMin, - int digitalMax, long signalOffset = 0, +CreateHeaderSignal(std::string label, int32_t samplesInDataRecord, + double physicalMin, double physicalMax, int32_t digitalMin, + int32_t digitalMax, int64_t signalOffset = 0, bool annotation = false, std::string transducer = "", std::string physDimension = "", std::string prefilter = "", std::string reserved = "") { diff --git a/include/edfio/processor/ProcessorHeaderExam.hpp b/include/edfio/processor/ProcessorHeaderExam.hpp index e96849c..71d9694 100644 --- a/include/edfio/processor/ProcessorHeaderExam.hpp +++ b/include/edfio/processor/ProcessorHeaderExam.hpp @@ -13,12 +13,14 @@ #include "../core/DataFormat.hpp" #include "../header/HeaderExam.hpp" +#include + namespace edfio { inline HeaderExam ProcessHeaderExam(HeaderGeneral header, std::vector signals) { // Record size - size_t recordsize = 0; + uint32_t recordsize = 0; for (auto &signal : signals) { recordsize += signal.m_samplesInDataRecord; } diff --git a/include/edfio/processor/ProcessorHeaderGeneral.hpp b/include/edfio/processor/ProcessorHeaderGeneral.hpp index e6494ad..6ddf684 100644 --- a/include/edfio/processor/ProcessorHeaderGeneral.hpp +++ b/include/edfio/processor/ProcessorHeaderGeneral.hpp @@ -9,11 +9,11 @@ #pragma once -#include "../Errors.hpp" #include "../core/DataFormat.hpp" #include "../header/HeaderGeneral.hpp" #include "ProcessorUtils.hpp" +#include #include #include @@ -42,17 +42,17 @@ inline HeaderGeneralFields ProcessHeaderGeneral(HeaderGeneral in) { } // Start Date { - int day = std::get<0>(in.m_startDate); - int month = std::get<1>(in.m_startDate); - int year = std::get<2>(in.m_startDate); + int32_t day = std::get<0>(in.m_startDate); + int32_t month = std::get<1>(in.m_startDate); + int32_t year = std::get<2>(in.m_startDate); year -= year > 1999 ? 2000 : 1900; out.m_startDate(std::format("{:02d}.{:02d}.{:02d}", day, month, year)); } // Start Time { - int hour = std::get<0>(in.m_startTime); - int minute = std::get<1>(in.m_startTime); - int second = std::get<2>(in.m_startTime); + int32_t hour = std::get<0>(in.m_startTime); + int32_t minute = std::get<1>(in.m_startTime); + int32_t second = std::get<2>(in.m_startTime); out.m_startTime(std::format("{:02d}.{:02d}.{:02d}", hour, minute, second)); } // Header Size @@ -122,9 +122,9 @@ inline HeaderGeneralFields ProcessHeaderGeneral(HeaderGeneral in) { // The startdate itself in dd-MMM-yyyy format using the English // 3-character abbreviations of the month in capitals: dd-MMM-yyyy (MMM = // 'JAN' | 'FEV' | ...) - int day = std::get<0>(in.m_startDate); - int month = std::get<1>(in.m_startDate); - int year = std::get<2>(in.m_startDate); + int32_t day = std::get<0>(in.m_startDate); + int32_t month = std::get<1>(in.m_startDate); + int32_t year = std::get<2>(in.m_startDate); fields.push_back(std::format("{:02d}-{}-{}", day ? day : 1, detail::GetStringFromMonth(month), year ? year : 1984)); diff --git a/include/edfio/processor/ProcessorHeaderGeneralFields.hpp b/include/edfio/processor/ProcessorHeaderGeneralFields.hpp index b5fa827..87abcfc 100644 --- a/include/edfio/processor/ProcessorHeaderGeneralFields.hpp +++ b/include/edfio/processor/ProcessorHeaderGeneralFields.hpp @@ -14,6 +14,7 @@ #include "../header/HeaderGeneral.hpp" #include "ProcessorUtils.hpp" +#include #include namespace edfio { @@ -72,7 +73,7 @@ inline HeaderGeneral ProcessHeaderGeneralFields(HeaderGeneralFields in) { throw std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); } { - int day{}, month{}, year{}; + int32_t day{}, month{}, year{}; auto [p1, e1] = std::from_chars(startdate.data(), startdate.data() + 2, day); auto [p2, e2] = @@ -98,7 +99,7 @@ inline HeaderGeneral ProcessHeaderGeneralFields(HeaderGeneralFields in) { throw std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); } { - int hour{}, minute{}, second{}; + int32_t hour{}, minute{}, second{}; auto [p1, e1] = std::from_chars(starttime.data(), starttime.data() + 2, hour); auto [p2, e2] = @@ -157,7 +158,7 @@ inline HeaderGeneral ProcessHeaderGeneralFields(HeaderGeneralFields in) { } // Number of signals { - int signals = detail::ParseInt( + int32_t signals = detail::ParseInt( in.m_totalSignals(), GetError(FileErrc::FileContainsFormatErrors)); if (signals <= 0 || (signals * 256 + 256) != out.m_headerSize) { throw std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); @@ -250,12 +251,12 @@ inline HeaderGeneral ProcessHeaderGeneralFields(HeaderGeneralFields in) { // dd-MMM-yyyy (MMM = 'JAN' | 'FEV' | ...) if (str.size() == 11 && str[2] == '-' && str[6] == '-') { { - int day{}, year{}; + int32_t day{}, year{}; auto [p1, e1] = std::from_chars(str.data(), str.data() + 2, day); auto [p2, e2] = std::from_chars(str.data() + 7, str.data() + 11, year); - int month = detail::GetMonthFromString( + int32_t month = detail::GetMonthFromString( std::string_view{str}.substr(3, 3)); if (e1 != std::errc{} || e2 != std::errc{} || month == 0) { throw std::invalid_argument( diff --git a/include/edfio/processor/ProcessorHeaderSignal.hpp b/include/edfio/processor/ProcessorHeaderSignal.hpp index 3960193..8f3d6f1 100644 --- a/include/edfio/processor/ProcessorHeaderSignal.hpp +++ b/include/edfio/processor/ProcessorHeaderSignal.hpp @@ -9,8 +9,6 @@ #pragma once -#include "../Errors.hpp" -#include "../core/DataFormat.hpp" #include "../header/HeaderSignal.hpp" #include "ProcessorUtils.hpp" diff --git a/include/edfio/processor/ProcessorHeaderSignalFields.hpp b/include/edfio/processor/ProcessorHeaderSignalFields.hpp index e50a507..ae027ac 100644 --- a/include/edfio/processor/ProcessorHeaderSignalFields.hpp +++ b/include/edfio/processor/ProcessorHeaderSignalFields.hpp @@ -14,11 +14,14 @@ #include "../header/HeaderSignal.hpp" #include "ProcessorUtils.hpp" +#include + namespace edfio { inline std::vector ProcessHeaderSignalFields(std::vector in, - DataFormat version, double datarecordDuration) { + DataFormat version, + [[maybe_unused]] double datarecordDuration) { std::vector signals(in.size()); for (auto &sigFields : in) { @@ -38,7 +41,7 @@ ProcessHeaderSignalFields(std::vector in, // Labels { - size_t totalAnnotationChannels = 0; + uint32_t totalAnnotationChannels = 0; for (size_t idx = 0; idx < signals.size(); idx++) { auto &signal = signals[idx]; auto &label = in[idx].m_label(); @@ -115,8 +118,8 @@ ProcessHeaderSignalFields(std::vector in, for (size_t idx = 0; idx < signals.size(); idx++) { auto &signal = signals[idx]; auto &digMin = in[idx].m_digitalMin(); - int n = detail::ParseInt(digMin, - GetError(FileErrc::FileContainsFormatErrors)); + int32_t n = detail::ParseInt( + digMin, GetError(FileErrc::FileContainsFormatErrors)); if (signal.m_detail.m_isAnnotation) { if (IsEdf(version) && IsPlus(version)) { @@ -149,8 +152,8 @@ ProcessHeaderSignalFields(std::vector in, for (size_t idx = 0; idx < signals.size(); idx++) { auto &signal = signals[idx]; auto &digMax = in[idx].m_digitalMax(); - int n = detail::ParseInt(digMax, - GetError(FileErrc::FileContainsFormatErrors)); + int32_t n = detail::ParseInt( + digMax, GetError(FileErrc::FileContainsFormatErrors)); if (signal.m_detail.m_isAnnotation) { if (IsEdf(version) && IsPlus(version)) { @@ -203,8 +206,8 @@ ProcessHeaderSignalFields(std::vector in, for (size_t idx = 0; idx < signals.size(); idx++) { auto &signal = signals[idx]; auto &nrSamples = in[idx].m_samplesInDataRecord(); - int n = detail::ParseInt(nrSamples, - GetError(FileErrc::FileContainsFormatErrors)); + int32_t n = detail::ParseInt( + nrSamples, GetError(FileErrc::FileContainsFormatErrors)); if (n < 1) { throw std::invalid_argument( @@ -223,7 +226,7 @@ ProcessHeaderSignalFields(std::vector in, } // Details { - size_t n = 0; + uint64_t n = 0; for (size_t idx = 0; idx < signals.size(); idx++) { auto &signal = signals[idx]; diff --git a/include/edfio/processor/ProcessorSample.hpp b/include/edfio/processor/ProcessorSample.hpp index 1a79dfe..c0d4a4b 100644 --- a/include/edfio/processor/ProcessorSample.hpp +++ b/include/edfio/processor/ProcessorSample.hpp @@ -12,6 +12,7 @@ #include "../core/Record.hpp" #include "../core/SampleType.hpp" +#include namespace edfio { @@ -20,7 +21,7 @@ template struct ProcessorSample { using DigiType = Sample::type; using PhysType = Sample::type; - ProcessorSample(double offset, double scaling, size_t sampleSize) + ProcessorSample(double offset, double scaling, uint32_t sampleSize) : m_offset(offset), m_scaling(scaling), m_sampleSize(sampleSize) {} Record operator()(ProcType sample); @@ -28,7 +29,7 @@ template struct ProcessorSample { private: const double m_offset; const double m_scaling; - size_t m_sampleSize; + uint32_t m_sampleSize; }; template @@ -42,8 +43,8 @@ inline Record ProcessorSample::operator()(ProcType sample) { Record record(m_sampleSize); auto it = record().begin(); - for (int count = m_sampleSize; count > 0; count--) { - unsigned char tmp = (value >> (count - 1) * 8); + for (int32_t count = m_sampleSize; count > 0; count--) { + uint8_t tmp = (value >> (count - 1) * 8); *it++ = tmp; } diff --git a/include/edfio/processor/ProcessorSampleRecord.hpp b/include/edfio/processor/ProcessorSampleRecord.hpp index d382e27..c2434e2 100644 --- a/include/edfio/processor/ProcessorSampleRecord.hpp +++ b/include/edfio/processor/ProcessorSampleRecord.hpp @@ -12,6 +12,7 @@ #include "../core/Record.hpp" #include "../core/SampleType.hpp" +#include namespace edfio { @@ -35,19 +36,19 @@ inline typename ProcessorSampleRecord::ProcType ProcessorSampleRecord::operator()(Record record) { DigiType sample = 0; auto const &bytes = record(); - std::size_t const nbytes = bytes.size(); + size_t const nbytes = bytes.size(); // Assemble bytes (big-endian order as written by ProcessorSample) - for (std::size_t i = 0; i < nbytes; ++i) { + for (uint32_t i = 0; i < nbytes; ++i) { sample <<= 8; sample |= static_cast(bytes[i]); } // Sign-extend: if high bit of the MSB is set, the value is negative if (nbytes > 0 && nbytes < sizeof(DigiType)) { - unsigned int sign_bit = 1u << (nbytes * 8 - 1); + uint32_t sign_bit = 1u << (nbytes * 8 - 1); if (sample & sign_bit) - sample |= ~((1 << (nbytes * 8)) - 1); + sample |= ~((int32_t{1} << (nbytes * 8)) - 1); } if constexpr (std::is_same_v) diff --git a/include/edfio/processor/ProcessorTalRecord.hpp b/include/edfio/processor/ProcessorTalRecord.hpp index 0935648..9f27081 100644 --- a/include/edfio/processor/ProcessorTalRecord.hpp +++ b/include/edfio/processor/ProcessorTalRecord.hpp @@ -9,16 +9,15 @@ #pragma once -#include "../Errors.hpp" #include "../core/Annotation.hpp" -#include "../core/Record.hpp" +#include #include namespace edfio { inline std::vector ProcessTalRecord(std::vector record, - long long datarecord) { + int64_t datarecord) { std::vector out; // Boundaries diff --git a/include/edfio/processor/ProcessorTimeStamp.hpp b/include/edfio/processor/ProcessorTimeStamp.hpp index 4a64dba..daa0f47 100644 --- a/include/edfio/processor/ProcessorTimeStamp.hpp +++ b/include/edfio/processor/ProcessorTimeStamp.hpp @@ -14,6 +14,8 @@ #include "../core/Record.hpp" #include "ProcessorUtils.hpp" +#include + namespace edfio { inline Record ProcessTimeStamp(TimeStamp timestamp) { @@ -24,7 +26,7 @@ inline Record ProcessTimeStamp(TimeStamp timestamp) { GetError(FileErrc::FileWriteInvalidAnnotations)); } - size_t plusSignal = 0; + uint32_t plusSignal = 0; if (timestamp.m_start >= 0) plusSignal = 1; diff --git a/include/edfio/processor/ProcessorTimeStampRecord.hpp b/include/edfio/processor/ProcessorTimeStampRecord.hpp index 480bd7e..f848c8f 100644 --- a/include/edfio/processor/ProcessorTimeStampRecord.hpp +++ b/include/edfio/processor/ProcessorTimeStampRecord.hpp @@ -14,11 +14,12 @@ #include "../core/Record.hpp" #include +#include namespace edfio { inline TimeStamp ProcessTimeStampRecord(Record record, - long long datarecord) { + int64_t datarecord) { TimeStamp timestamp; timestamp.m_datarecord = datarecord; auto &value = record(); diff --git a/include/edfio/processor/ProcessorUtils.hpp b/include/edfio/processor/ProcessorUtils.hpp index 5412926..fce0c60 100644 --- a/include/edfio/processor/ProcessorUtils.hpp +++ b/include/edfio/processor/ProcessorUtils.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -63,7 +64,7 @@ inline bool CheckFormatErrors(const std::vector &str) { return edfio::CheckFormatErrors(str); } -inline int GetMonthFromString(std::string_view str) { +inline int32_t GetMonthFromString(std::string_view str) { for (size_t idx = 0; idx < MONTHS.size(); idx++) { if (str == MONTHS[idx]) { return idx + 1; @@ -119,24 +120,24 @@ inline std::string_view GetFormatName(DataFormat format) { return ""; } -inline int ParseInt(std::string_view sv, const char *error_msg) { +inline int32_t ParseInt(std::string_view sv, const char *error_msg) { while (!sv.empty() && sv.front() == ' ') sv.remove_prefix(1); while (!sv.empty() && sv.back() == ' ') sv.remove_suffix(1); - int value{}; + int32_t value{}; auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), value); if (ec != std::errc{}) throw std::invalid_argument(error_msg); return value; } -inline long long ParseLongLong(std::string_view sv, const char *error_msg) { +inline int64_t ParseLongLong(std::string_view sv, const char *error_msg) { while (!sv.empty() && sv.front() == ' ') sv.remove_prefix(1); while (!sv.empty() && sv.back() == ' ') sv.remove_suffix(1); - long long value{}; + int64_t value{}; auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), value); if (ec != std::errc{}) throw std::invalid_argument(error_msg); @@ -161,7 +162,7 @@ inline double ParseDouble(std::string_view sv, const char *error_msg) { template inline std::string to_string_decimal(const T &t) { std::string str{std::to_string(t)}; std::replace(str.begin(), str.end(), ',', '.'); - int offset{1}; + int32_t offset{1}; if (str.find_last_not_of('0') == str.find('.')) { offset = 0; } diff --git a/include/edfio/reader/ReaderHeaderExam.hpp b/include/edfio/reader/ReaderHeaderExam.hpp index 39176b4..a20721a 100644 --- a/include/edfio/reader/ReaderHeaderExam.hpp +++ b/include/edfio/reader/ReaderHeaderExam.hpp @@ -18,6 +18,7 @@ #include "ReaderHeaderGeneral.hpp" #include "ReaderHeaderSignal.hpp" +#include #include namespace edfio { @@ -43,7 +44,7 @@ inline HeaderExam ReadHeaderExam(Reader::Stream &stream) { auto position = stream.tellg(); // get length of file stream.seekg(0, stream.end); - long long length = stream.tellg(); + int64_t length = stream.tellg(); // send back to previous position stream.seekg(position, stream.beg); diff --git a/include/edfio/reader/ReaderHeaderSignal.hpp b/include/edfio/reader/ReaderHeaderSignal.hpp index 6b9c4af..6c21376 100644 --- a/include/edfio/reader/ReaderHeaderSignal.hpp +++ b/include/edfio/reader/ReaderHeaderSignal.hpp @@ -14,13 +14,14 @@ #include "../header/HeaderGeneral.hpp" #include "../header/HeaderSignal.hpp" +#include #include #include namespace edfio { inline std::vector -ReadHeaderSignal(Reader::Stream &stream, size_t totalSignals) { +ReadHeaderSignal(Reader::Stream &stream, uint32_t totalSignals) { std::vector signals(totalSignals); if (!stream || !stream.is_open()) throw std::invalid_argument(GetError(FileErrc::FileNotOpened)); diff --git a/include/edfio/sink/DataRecordSink.hpp b/include/edfio/sink/DataRecordSink.hpp index c90d889..c9dc3a9 100644 --- a/include/edfio/sink/DataRecordSink.hpp +++ b/include/edfio/sink/DataRecordSink.hpp @@ -34,14 +34,14 @@ class DataRecordSink : public RecordSink { m_stream.seekp(0, std::ios::end); size_type sz = m_stream.tellp(); m_stream.seekp(pos); - if (sz < m_headerOffset) + if (sz < static_cast(m_headerOffset)) throw std::invalid_argument("Invalid header"); - m_sinkSize = (sz - m_headerOffset) / m_recordSize; + m_sinkSize = (sz - static_cast(m_headerOffset)) / m_recordSize; } void save(size_type off, value_type value) override { measure(); - if (off >= m_sinkSize || off == -1) { + if (off >= m_sinkSize) { m_stream.seekp(0, std::ios::end); m_sinkSize++; } else { diff --git a/include/edfio/sink/RecordSink.hpp b/include/edfio/sink/RecordSink.hpp index cc47f1b..f8d35fd 100644 --- a/include/edfio/sink/RecordSink.hpp +++ b/include/edfio/sink/RecordSink.hpp @@ -178,7 +178,7 @@ class RecordSink : public Sink, Record *, Record &, RecordSink(stream_type &stream, size_type recordSize, size_type sinkSize, std::streamoff headerOffset) - : sink_type(stream), m_recordSize(recordSize), + : sink_type(stream), m_recordSize(recordSize), m_sinkSize(sinkSize), m_headerOffset(headerOffset), m_value(recordSize) {} iterator begin() { return iterator(this, 0); } diff --git a/include/edfio/sink/SignalRecordSink.hpp b/include/edfio/sink/SignalRecordSink.hpp index 53171b8..8218715 100644 --- a/include/edfio/sink/SignalRecordSink.hpp +++ b/include/edfio/sink/SignalRecordSink.hpp @@ -36,24 +36,28 @@ class SignalRecordSink : public RecordSink { m_stream.seekp(0, std::ios::end); size_type sz = m_stream.tellp(); m_stream.seekp(pos); - if (sz < m_headerOffset) + if (sz < static_cast(m_headerOffset)) throw std::invalid_argument("Invalid header"); - size_type datarecords = (sz - m_headerOffset) / m_datarecordSize; - size_type offset = (sz - m_headerOffset) % m_datarecordSize; + size_type hdrOff = static_cast(m_headerOffset); + size_type sigOff = static_cast(m_signalOffset); + size_type datarecords = (sz - hdrOff) / m_datarecordSize; + size_type offset = (sz - hdrOff) % m_datarecordSize; m_sinkSize = datarecords; - if (offset >= m_signalOffset) + if (offset >= sigOff) m_sinkSize++; } void save(size_type off, value_type value) override { measure(); - if (off >= m_sinkSize || off == -1) { + if (off >= m_sinkSize) { m_stream.seekp(0, std::ios::end); size_type sz = m_stream.tellp(); - size_type offset = (sz - m_headerOffset) % m_datarecordSize; - if (offset < m_signalOffset) + size_type hdrOff = static_cast(m_headerOffset); + size_type sigOff = static_cast(m_signalOffset); + size_type offset = (sz - hdrOff) % m_datarecordSize; + if (offset < sigOff) throw std::invalid_argument("Invalid signal order"); m_sinkSize++; } else { diff --git a/include/edfio/store/SignalSampleStore.hpp b/include/edfio/store/SignalSampleStore.hpp index a1ad3a5..a74214d 100644 --- a/include/edfio/store/SignalSampleStore.hpp +++ b/include/edfio/store/SignalSampleStore.hpp @@ -11,6 +11,8 @@ #include "RecordStore.hpp" +#include + namespace edfio { class SignalSampleStore : public RecordStore { @@ -44,7 +46,7 @@ class SignalSampleStore : public RecordStore { m_signalOffset + sampleOffset * m_recordSize; if (m_bufferPos < 0 || - (destPos < m_bufferPos || destPos >= m_bufferPos + m_buffer.Size())) { + (destPos < m_bufferPos || destPos >= m_bufferPos + static_cast(m_buffer.Size()))) { readStream(destPos); m_bufferPos = m_headerOffset + dataRecordOffset * m_datarecordSize + m_signalOffset; @@ -54,7 +56,7 @@ class SignalSampleStore : public RecordStore { std::copy(first, first + m_value.Size(), m_value().begin()); } - void readStream(long long newPos) const { + void readStream(int64_t newPos) const { if (!m_stream.good()) m_stream.clear(); diff --git a/tests/test_iterators.cpp b/tests/test_iterators.cpp index 92010d9..4c72187 100644 --- a/tests/test_iterators.cpp +++ b/tests/test_iterators.cpp @@ -1,5 +1,6 @@ #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include +#include #include #include #include @@ -60,11 +61,11 @@ TEST_CASE("DataRecordStore iteration works") { auto header = ReadHeaderExam(stream); auto store = detail::CreateDataRecordStore(stream, header.m_general); - CHECK(store.size() == static_cast(header.m_general.m_datarecordsFile)); + CHECK(store.size() == static_cast(header.m_general.m_datarecordsFile)); CHECK(store.size() == 600); // Iterate through first 10 records - size_t count = 0; + uint32_t count = 0; for (auto it = store.begin(); it != store.begin() + 10; ++it) { auto& rec = *it; CHECK(rec.Size() > 0); @@ -115,10 +116,10 @@ TEST_CASE("SignalRecordStore iteration works") { REQUIRE(header.m_signals.size() > 0); auto store = detail::CreateSignalRecordStore(stream, header.m_general, header.m_signals[0]); - CHECK(store.size() == static_cast(header.m_general.m_datarecordsFile)); + CHECK(store.size() == static_cast(header.m_general.m_datarecordsFile)); // Iterate first 5 records - size_t count = 0; + uint32_t count = 0; for (auto it = store.begin(); it != store.begin() + 5; ++it) { auto& rec = *it; CHECK(rec.Size() > 0); @@ -137,7 +138,7 @@ TEST_CASE("DataRecordStore satisfies ranges::random_access_range") { CHECK(std::ranges::distance(store.begin(), store.end()) == 600); // Range-based for loop (proves range concept works) - size_t count = 0; + uint32_t count = 0; for ([[maybe_unused]] auto& rec : store) { ++count; if (count >= 3) break; From 29dfe8fc290c687017ea03018e1e42dcd81af62b Mon Sep 17 00:00:00 2001 From: iuri dotta Date: Sun, 1 Mar 2026 11:01:31 -0300 Subject: [PATCH 07/11] Refactor: Enhance code quality by adding [[nodiscard]] attributes and replacing std::copy with std::ranges::copy for improved readability and performance --- include/edfio/Errors.hpp | 2 +- include/edfio/core/Annotation.hpp | 6 +- include/edfio/core/Field.hpp | 4 +- include/edfio/core/Record.hpp | 9 +-- include/edfio/core/SampleType.hpp | 4 +- include/edfio/header/HeaderGeneral.hpp | 17 +++++- include/edfio/header/HeaderUtils.hpp | 10 ++-- .../edfio/processor/ProcessorAnnotation.hpp | 9 ++- .../processor/ProcessorHeaderGeneral.hpp | 19 ++----- .../ProcessorHeaderGeneralFields.hpp | 13 ++--- .../edfio/processor/ProcessorHeaderSignal.hpp | 47 ++++++++-------- .../processor/ProcessorHeaderSignalFields.hpp | 56 ++++++++----------- .../processor/ProcessorTimeStampRecord.hpp | 8 +-- include/edfio/processor/ProcessorUtils.hpp | 12 ++-- include/edfio/sink/RecordSink.hpp | 2 +- include/edfio/store/RecordStore.hpp | 2 +- include/edfio/store/SignalSampleStore.hpp | 3 +- include/edfio/writer/WriterHeaderSignals.hpp | 4 +- 18 files changed, 113 insertions(+), 114 deletions(-) diff --git a/include/edfio/Errors.hpp b/include/edfio/Errors.hpp index 1afc213..4be0676 100644 --- a/include/edfio/Errors.hpp +++ b/include/edfio/Errors.hpp @@ -21,7 +21,7 @@ enum class FileErrc { FileWriteInvalidAnnotations }; -inline constexpr const char *GetError(FileErrc err) { +[[nodiscard]] inline constexpr const char *GetError(FileErrc err) { switch (err) { case FileErrc::FileDoesNotOpen: return "Error: file does not open"; diff --git a/include/edfio/core/Annotation.hpp b/include/edfio/core/Annotation.hpp index f1d9c47..06c8ff1 100644 --- a/include/edfio/core/Annotation.hpp +++ b/include/edfio/core/Annotation.hpp @@ -16,9 +16,9 @@ namespace edfio { namespace detail { // Each TAL starts with a time stamp Onset21Duration20 -static const char DURATION_DIV = 21; -static const char ANNOTATION_DIV = 20; -static const char ANNOTATION_END = 0; +static constexpr char DURATION_DIV = 21; +static constexpr char ANNOTATION_DIV = 20; +static constexpr char ANNOTATION_END = 0; } // namespace detail struct TimeStamp { diff --git a/include/edfio/core/Field.hpp b/include/edfio/core/Field.hpp index c9569cb..fc94326 100644 --- a/include/edfio/core/Field.hpp +++ b/include/edfio/core/Field.hpp @@ -19,8 +19,8 @@ namespace edfio { template struct Field { using ValueType = CharT; - constexpr size_t Size() const { return Sz; } - const std::basic_string &operator()() const { return m_value; } + [[nodiscard]] constexpr size_t Size() const { return Sz; } + [[nodiscard]] const std::basic_string &operator()() const { return m_value; } std::basic_string &operator()() { return m_value; } void operator()(const std::basic_string &value) { m_value = value; diff --git a/include/edfio/core/Record.hpp b/include/edfio/core/Record.hpp index 5041343..675f9ee 100644 --- a/include/edfio/core/Record.hpp +++ b/include/edfio/core/Record.hpp @@ -9,6 +9,7 @@ #pragma once +#include #include #include @@ -34,13 +35,13 @@ template struct Record { Record &operator=(const Record &) = default; Record &operator=(Record &&) = default; - typename VectorType::size_type Size() const { return m_value.size(); } - const VectorType &operator()() const { return m_value; } + [[nodiscard]] typename VectorType::size_type Size() const { return m_value.size(); } + [[nodiscard]] const VectorType &operator()() const { return m_value; } VectorType &operator()() { return m_value; } Record operator+(const Record &record) const { Record tmp(Size() + record.Size()); - std::copy(m_value.begin(), m_value.end(), tmp().begin()); - std::copy(record().begin(), record().end(), tmp().begin() + Size()); + std::ranges::copy(m_value, tmp().begin()); + std::ranges::copy(record(), tmp().begin() + Size()); return tmp; } diff --git a/include/edfio/core/SampleType.hpp b/include/edfio/core/SampleType.hpp index 2aa8151..bae1494 100644 --- a/include/edfio/core/SampleType.hpp +++ b/include/edfio/core/SampleType.hpp @@ -25,14 +25,14 @@ template <> struct Sample { using type = int32_t; }; -inline Sample::type +[[nodiscard]] constexpr Sample::type ConvertSample(double offset, double scaling, Sample::type sample) { return static_cast::type>((sample - offset) / scaling); } -inline Sample::type +[[nodiscard]] constexpr Sample::type ConvertSample(double offset, double scaling, Sample::type sample) { return scaling * static_cast::type>(sample) + diff --git a/include/edfio/header/HeaderGeneral.hpp b/include/edfio/header/HeaderGeneral.hpp index fede2d5..7209a39 100644 --- a/include/edfio/header/HeaderGeneral.hpp +++ b/include/edfio/header/HeaderGeneral.hpp @@ -15,10 +15,21 @@ #include "../core/Field.hpp" #include -#include namespace edfio { +struct Date { + int32_t day = 0; + int32_t month = 0; + int32_t year = 0; +}; + +struct Time { + int32_t hour = 0; + int32_t minute = 0; + int32_t second = 0; +}; + struct HeaderGeneralFields { Field<8> m_version; Field<80> m_patient; @@ -54,8 +65,8 @@ struct HeaderGeneral { DataFormat m_version = DataFormat::Invalid; std::string m_patient; std::string m_recording; - std::tuple m_startDate; - std::tuple m_startTime; + Date m_startDate; + Time m_startTime; int32_t m_headerSize = 0; std::string m_reserved; int64_t m_datarecordsFile = 0; diff --git a/include/edfio/header/HeaderUtils.hpp b/include/edfio/header/HeaderUtils.hpp index bc750ef..164c131 100644 --- a/include/edfio/header/HeaderUtils.hpp +++ b/include/edfio/header/HeaderUtils.hpp @@ -13,8 +13,8 @@ #include "HeaderSignal.hpp" #include +#include #include -#include namespace edfio { @@ -25,13 +25,13 @@ inline HeaderGeneral CreateHeaderGeneral( int32_t startDateD, int32_t startDateM, int32_t startDateY, int32_t startTimeH, int32_t startTimeM, int32_t startTimeS, int32_t headerSize, std::string reserved, int64_t datarecordsFile, - double datarecordDuration, const std::vector &signals) { + double datarecordDuration, std::span signals) { HeaderGeneral header; header.m_version = version; header.m_patient = patient; header.m_recording = recording; - header.m_startDate = std::make_tuple(startDateD, startDateM, startDateY); - header.m_startTime = std::make_tuple(startTimeH, startTimeM, startTimeS); + header.m_startDate = Date{startDateD, startDateM, startDateY}; + header.m_startTime = Time{startTimeH, startTimeM, startTimeS}; header.m_headerSize = headerSize; header.m_reserved = reserved; header.m_datarecordsFile = datarecordsFile; @@ -57,7 +57,7 @@ inline HeaderGeneral CreateHeaderGeneralPlus( int32_t startDateY, int32_t startTimeH, int32_t startTimeM, int32_t startTimeS, int32_t headerSize, std::string reserved, int64_t datarecordsFile, double datarecordDuration, - const std::vector &signals) { + std::span signals) { auto header = CreateHeaderGeneral( version, "", "", startDateD, startDateM, startDateY, startTimeH, startTimeM, startTimeS, headerSize, reserved, datarecordsFile, diff --git a/include/edfio/processor/ProcessorAnnotation.hpp b/include/edfio/processor/ProcessorAnnotation.hpp index c88f035..e66faa0 100644 --- a/include/edfio/processor/ProcessorAnnotation.hpp +++ b/include/edfio/processor/ProcessorAnnotation.hpp @@ -14,6 +14,9 @@ #include "../core/Record.hpp" #include "ProcessorUtils.hpp" +#include +#include + namespace edfio { inline Record ProcessAnnotation(Annotation annotation) { @@ -42,19 +45,19 @@ inline Record ProcessAnnotation(Annotation annotation) { auto it = record().begin(); // timestamp - std::move(timestamp.begin(), timestamp.end(), it); + std::ranges::copy(timestamp, it); it += timestamp.size(); if (!duration.empty()) { *it++ = 21; // 21 div // duration - std::move(duration.begin(), duration.end(), it); + std::ranges::copy(duration, it); it += duration.size(); } *it++ = 20; // 20 div // annotation - std::move(annotation.m_annotation.begin(), annotation.m_annotation.end(), it); + std::ranges::copy(annotation.m_annotation, it); it += annotation.m_annotation.size(); *it++ = 20; // 20 div diff --git a/include/edfio/processor/ProcessorHeaderGeneral.hpp b/include/edfio/processor/ProcessorHeaderGeneral.hpp index 6ddf684..5a56a32 100644 --- a/include/edfio/processor/ProcessorHeaderGeneral.hpp +++ b/include/edfio/processor/ProcessorHeaderGeneral.hpp @@ -13,6 +13,7 @@ #include "../header/HeaderGeneral.hpp" #include "ProcessorUtils.hpp" +#include #include #include #include @@ -42,17 +43,13 @@ inline HeaderGeneralFields ProcessHeaderGeneral(HeaderGeneral in) { } // Start Date { - int32_t day = std::get<0>(in.m_startDate); - int32_t month = std::get<1>(in.m_startDate); - int32_t year = std::get<2>(in.m_startDate); + auto [day, month, year] = in.m_startDate; year -= year > 1999 ? 2000 : 1900; out.m_startDate(std::format("{:02d}.{:02d}.{:02d}", day, month, year)); } // Start Time { - int32_t hour = std::get<0>(in.m_startTime); - int32_t minute = std::get<1>(in.m_startTime); - int32_t second = std::get<2>(in.m_startTime); + auto [hour, minute, second] = in.m_startTime; out.m_startTime(std::format("{:02d}.{:02d}.{:02d}", hour, minute, second)); } // Header Size @@ -107,8 +104,7 @@ inline HeaderGeneralFields ProcessHeaderGeneral(HeaderGeneral in) { } std::string patient = ""; for (auto &field : fields) { - std::replace(field.begin(), field.end(), ' ', - '_'); // replace all ' ' to '_' + std::ranges::replace(field, ' ', '_'); // replace all ' ' to '_' patient += field + " "; } patient.pop_back(); // remove last " " @@ -122,9 +118,7 @@ inline HeaderGeneralFields ProcessHeaderGeneral(HeaderGeneral in) { // The startdate itself in dd-MMM-yyyy format using the English // 3-character abbreviations of the month in capitals: dd-MMM-yyyy (MMM = // 'JAN' | 'FEV' | ...) - int32_t day = std::get<0>(in.m_startDate); - int32_t month = std::get<1>(in.m_startDate); - int32_t year = std::get<2>(in.m_startDate); + auto [day, month, year] = in.m_startDate; fields.push_back(std::format("{:02d}-{}-{}", day ? day : 1, detail::GetStringFromMonth(month), year ? year : 1984)); @@ -146,8 +140,7 @@ inline HeaderGeneralFields ProcessHeaderGeneral(HeaderGeneral in) { } std::string recording = ""; for (auto &field : fields) { - std::replace(field.begin(), field.end(), ' ', - '_'); // replace all '_' to ' ' + std::ranges::replace(field, ' ', '_'); // replace all '_' to ' ' recording += field + " "; } recording.pop_back(); // remove last " " diff --git a/include/edfio/processor/ProcessorHeaderGeneralFields.hpp b/include/edfio/processor/ProcessorHeaderGeneralFields.hpp index 87abcfc..ff30413 100644 --- a/include/edfio/processor/ProcessorHeaderGeneralFields.hpp +++ b/include/edfio/processor/ProcessorHeaderGeneralFields.hpp @@ -14,6 +14,7 @@ #include "../header/HeaderGeneral.hpp" #include "ProcessorUtils.hpp" +#include #include #include @@ -86,7 +87,7 @@ inline HeaderGeneral ProcessHeaderGeneralFields(HeaderGeneralFields in) { GetError(FileErrc::FileContainsFormatErrors)); } year += year > 84 ? 1900 : 2000; - out.m_startDate = std::make_tuple(day, month, year); + out.m_startDate = Date{day, month, year}; } } // Start Time @@ -111,7 +112,7 @@ inline HeaderGeneral ProcessHeaderGeneralFields(HeaderGeneralFields in) { throw std::invalid_argument( GetError(FileErrc::FileContainsFormatErrors)); } - out.m_startTime = std::make_tuple(hour, minute, second); + out.m_startTime = Time{hour, minute, second}; } } // Header Size @@ -185,8 +186,7 @@ inline HeaderGeneral ProcessHeaderGeneralFields(HeaderGeneralFields in) { for (size_t i = 0; i < fields.size(); i++) { auto &str = fields[i]; - std::replace(str.begin(), str.end(), '_', - ' '); // replace all '_' to ' ' + std::ranges::replace(str, '_', ' '); // replace all '_' to ' ' switch (i) { case 0: // The code by which the patient is known in the hospital // administration. @@ -236,8 +236,7 @@ inline HeaderGeneral ProcessHeaderGeneralFields(HeaderGeneralFields in) { for (size_t i = 0; i < fields.size(); i++) { auto &str = fields[i]; - std::replace(str.begin(), str.end(), '_', - ' '); // replace all '_' to ' ' + std::ranges::replace(str, '_', ' '); // replace all '_' to ' ' if (str != "X") { switch (i) { case 0: // The text 'Startdate'. @@ -262,7 +261,7 @@ inline HeaderGeneral ProcessHeaderGeneralFields(HeaderGeneralFields in) { throw std::invalid_argument( GetError(FileErrc::FileContainsFormatErrors)); } - out.m_startDate = std::make_tuple(day, month, year); + out.m_startDate = Date{day, month, year}; } } else { throw std::invalid_argument( diff --git a/include/edfio/processor/ProcessorHeaderSignal.hpp b/include/edfio/processor/ProcessorHeaderSignal.hpp index 8f3d6f1..242d00a 100644 --- a/include/edfio/processor/ProcessorHeaderSignal.hpp +++ b/include/edfio/processor/ProcessorHeaderSignal.hpp @@ -12,6 +12,7 @@ #include "../header/HeaderSignal.hpp" #include "ProcessorUtils.hpp" +#include #include #include @@ -24,65 +25,65 @@ ProcessHeaderSignal(std::vector in) { // Labels { - for (size_t idx = 0; idx < signals.size(); idx++) { - out[idx].m_label(signals[idx].m_label); + for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { + outSig.m_label(inSig.m_label); } } // Transducers Types { - for (size_t idx = 0; idx < signals.size(); idx++) { - out[idx].m_transducer(signals[idx].m_transducer); + for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { + outSig.m_transducer(inSig.m_transducer); } } // Physical Dimensions { - for (size_t idx = 0; idx < signals.size(); idx++) { - out[idx].m_physDimension(signals[idx].m_physDimension); + for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { + outSig.m_physDimension(inSig.m_physDimension); } } // Physical Minima { - for (size_t idx = 0; idx < signals.size(); idx++) { - out[idx].m_physicalMin( - detail::to_string_decimal(signals[idx].m_physicalMin)); + for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { + outSig.m_physicalMin( + detail::to_string_decimal(inSig.m_physicalMin)); } } // Physical Maxima { - for (size_t idx = 0; idx < signals.size(); idx++) { - out[idx].m_physicalMax( - detail::to_string_decimal(signals[idx].m_physicalMax)); + for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { + outSig.m_physicalMax( + detail::to_string_decimal(inSig.m_physicalMax)); } } // Digital Minima { - for (size_t idx = 0; idx < signals.size(); idx++) { - out[idx].m_digitalMin(std::to_string(signals[idx].m_digitalMin)); + for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { + outSig.m_digitalMin(std::to_string(inSig.m_digitalMin)); } } // Digital Maxima { - for (size_t idx = 0; idx < signals.size(); idx++) { - out[idx].m_digitalMax(std::to_string(signals[idx].m_digitalMax)); + for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { + outSig.m_digitalMax(std::to_string(inSig.m_digitalMax)); } } // Prefilter { - for (size_t idx = 0; idx < signals.size(); idx++) { - out[idx].m_prefilter(signals[idx].m_prefilter); + for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { + outSig.m_prefilter(inSig.m_prefilter); } } // Samples in each datarecord { - for (size_t idx = 0; idx < signals.size(); idx++) { - out[idx].m_samplesInDataRecord( - std::to_string(signals[idx].m_samplesInDataRecord)); + for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { + outSig.m_samplesInDataRecord( + std::to_string(inSig.m_samplesInDataRecord)); } } // Reserved { - for (size_t idx = 0; idx < signals.size(); idx++) { - out[idx].m_reserved(signals[idx].m_reserved); + for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { + outSig.m_reserved(inSig.m_reserved); } } diff --git a/include/edfio/processor/ProcessorHeaderSignalFields.hpp b/include/edfio/processor/ProcessorHeaderSignalFields.hpp index ae027ac..d794ef6 100644 --- a/include/edfio/processor/ProcessorHeaderSignalFields.hpp +++ b/include/edfio/processor/ProcessorHeaderSignalFields.hpp @@ -15,6 +15,8 @@ #include "ProcessorUtils.hpp" #include +#include +#include namespace edfio { @@ -42,9 +44,8 @@ ProcessHeaderSignalFields(std::vector in, // Labels { uint32_t totalAnnotationChannels = 0; - for (size_t idx = 0; idx < signals.size(); idx++) { - auto &signal = signals[idx]; - auto &label = in[idx].m_label(); + for (auto &&[signal, inSig] : std::views::zip(signals, in)) { + auto &label = inSig.m_label(); if (IsPlus(version)) { if (label.find("Annotation") != std::string::npos) { totalAnnotationChannels++; @@ -72,9 +73,8 @@ ProcessHeaderSignalFields(std::vector in, } // Transducers Types { - for (size_t idx = 0; idx < signals.size(); idx++) { - auto &signal = signals[idx]; - auto &transducer = in[idx].m_transducer(); + for (auto &&[signal, inSig] : std::views::zip(signals, in)) { + auto &transducer = inSig.m_transducer(); signal.m_transducer = transducer; @@ -88,36 +88,32 @@ ProcessHeaderSignalFields(std::vector in, } // Physical Dimensions { - for (size_t idx = 0; idx < signals.size(); idx++) { - auto &signal = signals[idx]; - auto &physDimension = in[idx].m_physDimension(); + for (auto &&[signal, inSig] : std::views::zip(signals, in)) { + auto &physDimension = inSig.m_physDimension(); signal.m_physDimension = physDimension; } } // Physical Minima { - for (size_t idx = 0; idx < signals.size(); idx++) { - auto &signal = signals[idx]; - auto &physMin = in[idx].m_physicalMin(); + for (auto &&[signal, inSig] : std::views::zip(signals, in)) { + auto &physMin = inSig.m_physicalMin(); signal.m_physicalMin = detail::ParseDouble( physMin, GetError(FileErrc::FileContainsFormatErrors)); } } // Physical Maxima { - for (size_t idx = 0; idx < signals.size(); idx++) { - auto &signal = signals[idx]; - auto &physMax = in[idx].m_physicalMax(); + for (auto &&[signal, inSig] : std::views::zip(signals, in)) { + auto &physMax = inSig.m_physicalMax(); signal.m_physicalMax = detail::ParseDouble( physMax, GetError(FileErrc::FileContainsFormatErrors)); } } // Digital Minima { - for (size_t idx = 0; idx < signals.size(); idx++) { - auto &signal = signals[idx]; - auto &digMin = in[idx].m_digitalMin(); + for (auto &&[signal, inSig] : std::views::zip(signals, in)) { + auto &digMin = inSig.m_digitalMin(); int32_t n = detail::ParseInt( digMin, GetError(FileErrc::FileContainsFormatErrors)); @@ -149,9 +145,8 @@ ProcessHeaderSignalFields(std::vector in, } // Digital Maxima { - for (size_t idx = 0; idx < signals.size(); idx++) { - auto &signal = signals[idx]; - auto &digMax = in[idx].m_digitalMax(); + for (auto &&[signal, inSig] : std::views::zip(signals, in)) { + auto &digMax = inSig.m_digitalMax(); int32_t n = detail::ParseInt( digMax, GetError(FileErrc::FileContainsFormatErrors)); @@ -187,9 +182,8 @@ ProcessHeaderSignalFields(std::vector in, } // Prefilter { - for (size_t idx = 0; idx < signals.size(); idx++) { - auto &signal = signals[idx]; - auto &prefilter = in[idx].m_prefilter(); + for (auto &&[signal, inSig] : std::views::zip(signals, in)) { + auto &prefilter = inSig.m_prefilter(); signal.m_prefilter = prefilter; @@ -203,9 +197,8 @@ ProcessHeaderSignalFields(std::vector in, } // Samples in each datarecord { - for (size_t idx = 0; idx < signals.size(); idx++) { - auto &signal = signals[idx]; - auto &nrSamples = in[idx].m_samplesInDataRecord(); + for (auto &&[signal, inSig] : std::views::zip(signals, in)) { + auto &nrSamples = inSig.m_samplesInDataRecord(); int32_t n = detail::ParseInt( nrSamples, GetError(FileErrc::FileContainsFormatErrors)); @@ -218,18 +211,15 @@ ProcessHeaderSignalFields(std::vector in, } // Reserved { - for (size_t idx = 0; idx < signals.size(); idx++) { - auto &signal = signals[idx]; - auto &reserved = in[idx].m_reserved(); + for (auto &&[signal, inSig] : std::views::zip(signals, in)) { + auto &reserved = inSig.m_reserved(); signal.m_reserved = reserved; } } // Details { uint64_t n = 0; - for (size_t idx = 0; idx < signals.size(); idx++) { - auto &signal = signals[idx]; - + for (auto &signal : signals) { signal.m_detail.m_signalOffset = n; if (IsBdf(version)) n += signal.m_samplesInDataRecord * 3; diff --git a/include/edfio/processor/ProcessorTimeStampRecord.hpp b/include/edfio/processor/ProcessorTimeStampRecord.hpp index f848c8f..75b73c7 100644 --- a/include/edfio/processor/ProcessorTimeStampRecord.hpp +++ b/include/edfio/processor/ProcessorTimeStampRecord.hpp @@ -14,6 +14,7 @@ #include "../core/Record.hpp" #include +#include #include namespace edfio { @@ -31,10 +32,9 @@ inline TimeStamp ProcessTimeStampRecord(Record record, } // Make sure it's a valid timestamp - static const std::vector comp = {detail::ANNOTATION_END, - detail::ANNOTATION_DIV}; - auto result = - std::find_first_of(value.begin(), value.end(), comp.begin(), comp.end()); + static constexpr std::array comp = {detail::ANNOTATION_END, + detail::ANNOTATION_DIV}; + auto result = std::ranges::find_first_of(value, comp); if (result == value.end()) { throw std::invalid_argument( diff --git a/include/edfio/processor/ProcessorUtils.hpp b/include/edfio/processor/ProcessorUtils.hpp index fce0c60..c6bd927 100644 --- a/include/edfio/processor/ProcessorUtils.hpp +++ b/include/edfio/processor/ProcessorUtils.hpp @@ -16,13 +16,13 @@ #include #include #include +#include #include #include -#include namespace edfio { template -inline bool CheckFormatErrors(const std::basic_string &str) { +[[nodiscard]] inline bool CheckFormatErrors(const std::basic_string &str) { if constexpr (Check == ProcessorErrorCheck::Permissive) { return false; } else { @@ -35,7 +35,7 @@ inline bool CheckFormatErrors(const std::basic_string &str) { } template -inline bool CheckFormatErrors(const std::vector &str) { +[[nodiscard]] inline bool CheckFormatErrors(std::span str) { if constexpr (Check == ProcessorErrorCheck::Permissive) { return false; } else { @@ -55,12 +55,12 @@ inline constexpr std::array MONTHS = { "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; template -inline bool CheckFormatErrors(const std::basic_string &str) { +[[nodiscard]] inline bool CheckFormatErrors(const std::basic_string &str) { return edfio::CheckFormatErrors(str); } template -inline bool CheckFormatErrors(const std::vector &str) { +[[nodiscard]] inline bool CheckFormatErrors(std::span str) { return edfio::CheckFormatErrors(str); } @@ -161,7 +161,7 @@ inline double ParseDouble(std::string_view sv, const char *error_msg) { template inline std::string to_string_decimal(const T &t) { std::string str{std::to_string(t)}; - std::replace(str.begin(), str.end(), ',', '.'); + std::ranges::replace(str, ',', '.'); int32_t offset{1}; if (str.find_last_not_of('0') == str.find('.')) { offset = 0; diff --git a/include/edfio/sink/RecordSink.hpp b/include/edfio/sink/RecordSink.hpp index f8d35fd..f1c5117 100644 --- a/include/edfio/sink/RecordSink.hpp +++ b/include/edfio/sink/RecordSink.hpp @@ -210,7 +210,7 @@ class RecordSink : public Sink, Record *, Record &, return const_reverse_iterator(cbegin()); } - size_type size() const { return m_sinkSize; } + [[nodiscard]] size_type size() const { return m_sinkSize; } protected: virtual void measure() = 0; diff --git a/include/edfio/store/RecordStore.hpp b/include/edfio/store/RecordStore.hpp index 13f6f3e..e561a57 100644 --- a/include/edfio/store/RecordStore.hpp +++ b/include/edfio/store/RecordStore.hpp @@ -194,7 +194,7 @@ class RecordStore } // Overrides - virtual size_type size() const { return m_storeSize; } + [[nodiscard]] virtual size_type size() const { return m_storeSize; } protected: virtual reference getR(size_type off) const { diff --git a/include/edfio/store/SignalSampleStore.hpp b/include/edfio/store/SignalSampleStore.hpp index a74214d..8515a4b 100644 --- a/include/edfio/store/SignalSampleStore.hpp +++ b/include/edfio/store/SignalSampleStore.hpp @@ -11,6 +11,7 @@ #include "RecordStore.hpp" +#include #include namespace edfio { @@ -53,7 +54,7 @@ class SignalSampleStore : public RecordStore { } auto first = m_buffer().begin() + sampleOffset * m_recordSize; - std::copy(first, first + m_value.Size(), m_value().begin()); + std::ranges::copy_n(first, m_value.Size(), m_value().begin()); } void readStream(int64_t newPos) const { diff --git a/include/edfio/writer/WriterHeaderSignals.hpp b/include/edfio/writer/WriterHeaderSignals.hpp index bc167ee..82df7a0 100644 --- a/include/edfio/writer/WriterHeaderSignals.hpp +++ b/include/edfio/writer/WriterHeaderSignals.hpp @@ -13,13 +13,13 @@ #include "../core/StreamIO.hpp" #include "../header/HeaderSignal.hpp" +#include #include -#include namespace edfio { inline void WriteHeaderSignals(Writer::Stream &stream, - std::vector &signals) { + std::span signals) { if (!stream || !stream.is_open()) throw std::invalid_argument(GetError(FileErrc::FileNotOpened)); try { From 7eac4279d5b4bd37a711afc1467ce8f1103e5a22 Mon Sep 17 00:00:00 2001 From: iuri dotta Date: Sun, 1 Mar 2026 11:23:47 -0300 Subject: [PATCH 08/11] Refactor: Update include paths, improve type safety, and enhance readability by modifying function signatures and consolidating loops --- include/edfio/EdfIO.hpp | 7 +- include/edfio/core/Field.hpp | 12 +-- include/edfio/core/Record.hpp | 17 +++-- .../edfio/processor/ProcessorAnnotation.hpp | 1 - .../edfio/processor/ProcessorHeaderSignal.hpp | 73 +++---------------- .../processor/ProcessorHeaderSignalFields.hpp | 9 --- include/edfio/processor/ProcessorSample.hpp | 2 +- include/edfio/processor/ProcessorUtils.hpp | 8 +- include/edfio/sink/RecordSink.hpp | 2 + include/edfio/store/RecordStore.hpp | 2 + include/edfio/store/StoreUtils.hpp | 5 +- include/edfio/writer/WriterHeaderExam.hpp | 2 +- include/edfio/writer/WriterHeaderGeneral.hpp | 3 +- 13 files changed, 46 insertions(+), 97 deletions(-) diff --git a/include/edfio/EdfIO.hpp b/include/edfio/EdfIO.hpp index 411de38..f157d58 100644 --- a/include/edfio/EdfIO.hpp +++ b/include/edfio/EdfIO.hpp @@ -19,12 +19,13 @@ #include "writer/WriterHeaderExam.hpp" // Store -#include "store/DatarecordStore.hpp" -#include "store/SignalrecordStore.hpp" +#include "store/DataRecordStore.hpp" +#include "store/SignalRecordStore.hpp" #include "store/SignalSampleStore.hpp" +#include "store/StoreUtils.hpp" #include "store/TalStore.hpp" #include "store/TimeStampStore.hpp" -#include "store/StoreUtils.hpp" + // Sink #include "sink/DataRecordSink.hpp" diff --git a/include/edfio/core/Field.hpp b/include/edfio/core/Field.hpp index fc94326..5d19d4d 100644 --- a/include/edfio/core/Field.hpp +++ b/include/edfio/core/Field.hpp @@ -9,7 +9,8 @@ #pragma once -#include +#include +#include #include namespace edfio { @@ -20,7 +21,9 @@ template struct Field { using ValueType = CharT; [[nodiscard]] constexpr size_t Size() const { return Sz; } - [[nodiscard]] const std::basic_string &operator()() const { return m_value; } + [[nodiscard]] const std::basic_string &operator()() const { + return m_value; + } std::basic_string &operator()() { return m_value; } void operator()(const std::basic_string &value) { m_value = value; @@ -31,9 +34,8 @@ template struct Field { }; template -std::ostream &operator<<(std::ostream &os, Field &f) { - auto &value = f(); - value.resize(Sz, ' '); +std::ostream &operator<<(std::ostream &os, const Field &f) { + const auto &value = f(); os.write(value.data(), Sz); return os; } diff --git a/include/edfio/core/Record.hpp b/include/edfio/core/Record.hpp index 675f9ee..4264bd0 100644 --- a/include/edfio/core/Record.hpp +++ b/include/edfio/core/Record.hpp @@ -9,8 +9,9 @@ #pragma once -#include -#include +#include +#include +#include #include namespace edfio { @@ -24,7 +25,8 @@ template struct Record { Record() = delete; - Record(typename VectorType::size_type recordSize) : m_value(recordSize, 0) {} + explicit Record(typename VectorType::size_type recordSize) + : m_value(recordSize, 0) {} Record(typename VectorType::const_iterator first, typename VectorType::const_iterator last) @@ -35,7 +37,9 @@ template struct Record { Record &operator=(const Record &) = default; Record &operator=(Record &&) = default; - [[nodiscard]] typename VectorType::size_type Size() const { return m_value.size(); } + [[nodiscard]] typename VectorType::size_type Size() const { + return m_value.size(); + } [[nodiscard]] const VectorType &operator()() const { return m_value; } VectorType &operator()() { return m_value; } Record operator+(const Record &record) const { @@ -49,9 +53,8 @@ template struct Record { }; template -std::ostream &operator<<(std::ostream &os, Record &r) { - auto &record = r(); - record.resize(r.Size(), 0); +std::ostream &operator<<(std::ostream &os, const Record &r) { + const auto &record = r(); os.write(record.data(), r.Size() * sizeof(ValT)); return os; } diff --git a/include/edfio/processor/ProcessorAnnotation.hpp b/include/edfio/processor/ProcessorAnnotation.hpp index e66faa0..ea1e29b 100644 --- a/include/edfio/processor/ProcessorAnnotation.hpp +++ b/include/edfio/processor/ProcessorAnnotation.hpp @@ -14,7 +14,6 @@ #include "../core/Record.hpp" #include "ProcessorUtils.hpp" -#include #include namespace edfio { diff --git a/include/edfio/processor/ProcessorHeaderSignal.hpp b/include/edfio/processor/ProcessorHeaderSignal.hpp index 242d00a..4b945d7 100644 --- a/include/edfio/processor/ProcessorHeaderSignal.hpp +++ b/include/edfio/processor/ProcessorHeaderSignal.hpp @@ -23,68 +23,17 @@ ProcessHeaderSignal(std::vector in) { std::vector out(in.size()); auto &signals = in; - // Labels - { - for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { - outSig.m_label(inSig.m_label); - } - } - // Transducers Types - { - for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { - outSig.m_transducer(inSig.m_transducer); - } - } - // Physical Dimensions - { - for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { - outSig.m_physDimension(inSig.m_physDimension); - } - } - // Physical Minima - { - for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { - outSig.m_physicalMin( - detail::to_string_decimal(inSig.m_physicalMin)); - } - } - // Physical Maxima - { - for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { - outSig.m_physicalMax( - detail::to_string_decimal(inSig.m_physicalMax)); - } - } - // Digital Minima - { - for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { - outSig.m_digitalMin(std::to_string(inSig.m_digitalMin)); - } - } - // Digital Maxima - { - for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { - outSig.m_digitalMax(std::to_string(inSig.m_digitalMax)); - } - } - // Prefilter - { - for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { - outSig.m_prefilter(inSig.m_prefilter); - } - } - // Samples in each datarecord - { - for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { - outSig.m_samplesInDataRecord( - std::to_string(inSig.m_samplesInDataRecord)); - } - } - // Reserved - { - for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { - outSig.m_reserved(inSig.m_reserved); - } + for (auto &&[outSig, inSig] : std::views::zip(out, signals)) { + outSig.m_label(inSig.m_label); + outSig.m_transducer(inSig.m_transducer); + outSig.m_physDimension(inSig.m_physDimension); + outSig.m_physicalMin(detail::to_string_decimal(inSig.m_physicalMin)); + outSig.m_physicalMax(detail::to_string_decimal(inSig.m_physicalMax)); + outSig.m_digitalMin(std::to_string(inSig.m_digitalMin)); + outSig.m_digitalMax(std::to_string(inSig.m_digitalMax)); + outSig.m_prefilter(inSig.m_prefilter); + outSig.m_samplesInDataRecord(std::to_string(inSig.m_samplesInDataRecord)); + outSig.m_reserved(inSig.m_reserved); } return out; diff --git a/include/edfio/processor/ProcessorHeaderSignalFields.hpp b/include/edfio/processor/ProcessorHeaderSignalFields.hpp index d794ef6..fef5f02 100644 --- a/include/edfio/processor/ProcessorHeaderSignalFields.hpp +++ b/include/edfio/processor/ProcessorHeaderSignalFields.hpp @@ -61,15 +61,6 @@ ProcessHeaderSignalFields(std::vector in, if (IsPlus(version) && totalAnnotationChannels == 0) { throw std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); } - // TODO: check if this is true - /*if (datarecordDuration < 1) - { - if (signals.size() != totalAnnotationChannels || !IsPlus(version)) - { - throw - std::invalid_argument(GetError(FileErrc::FileContainsFormatErrors)); - } - }*/ } // Transducers Types { diff --git a/include/edfio/processor/ProcessorSample.hpp b/include/edfio/processor/ProcessorSample.hpp index c0d4a4b..846cb8c 100644 --- a/include/edfio/processor/ProcessorSample.hpp +++ b/include/edfio/processor/ProcessorSample.hpp @@ -43,7 +43,7 @@ inline Record ProcessorSample::operator()(ProcType sample) { Record record(m_sampleSize); auto it = record().begin(); - for (int32_t count = m_sampleSize; count > 0; count--) { + for (uint32_t count = m_sampleSize; count > 0; count--) { uint8_t tmp = (value >> (count - 1) * 8); *it++ = tmp; } diff --git a/include/edfio/processor/ProcessorUtils.hpp b/include/edfio/processor/ProcessorUtils.hpp index c6bd927..4016b9d 100644 --- a/include/edfio/processor/ProcessorUtils.hpp +++ b/include/edfio/processor/ProcessorUtils.hpp @@ -9,7 +9,7 @@ #pragma once -#include "..\Config.hpp" +#include "../Config.hpp" #include #include @@ -22,7 +22,8 @@ namespace edfio { template -[[nodiscard]] inline bool CheckFormatErrors(const std::basic_string &str) { +[[nodiscard]] inline bool +CheckFormatErrors(const std::basic_string &str) { if constexpr (Check == ProcessorErrorCheck::Permissive) { return false; } else { @@ -55,7 +56,8 @@ inline constexpr std::array MONTHS = { "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; template -[[nodiscard]] inline bool CheckFormatErrors(const std::basic_string &str) { +[[nodiscard]] inline bool +CheckFormatErrors(const std::basic_string &str) { return edfio::CheckFormatErrors(str); } diff --git a/include/edfio/sink/RecordSink.hpp b/include/edfio/sink/RecordSink.hpp index f1c5117..9dd237d 100644 --- a/include/edfio/sink/RecordSink.hpp +++ b/include/edfio/sink/RecordSink.hpp @@ -33,6 +33,8 @@ class RecordSink : public Sink, Record *, Record &, using typename base_sink::stream_type; using typename base_sink::value_type; + virtual ~RecordSink() = default; + class iterator : public base_sink::iterator { std::optional m_offset; // nullopt = end RecordSink *m_context = nullptr; diff --git a/include/edfio/store/RecordStore.hpp b/include/edfio/store/RecordStore.hpp index e561a57..8c79c32 100644 --- a/include/edfio/store/RecordStore.hpp +++ b/include/edfio/store/RecordStore.hpp @@ -34,6 +34,8 @@ class RecordStore using typename base_store::stream_type; using typename base_store::value_type; + virtual ~RecordStore() = default; + class iterator : public base_store::iterator { size_type m_offset = 0; // Relative to total of Stores const RecordStore *m_context = nullptr; diff --git a/include/edfio/store/StoreUtils.hpp b/include/edfio/store/StoreUtils.hpp index d0f55db..386fe84 100644 --- a/include/edfio/store/StoreUtils.hpp +++ b/include/edfio/store/StoreUtils.hpp @@ -11,12 +11,11 @@ #include "../header/HeaderGeneral.hpp" #include "../header/HeaderSignal.hpp" -#include "DatarecordStore.hpp" -#include "SignalrecordStore.hpp" +#include "DataRecordStore.hpp" +#include "SignalRecordStore.hpp" #include "SignalSampleStore.hpp" #include "TimeStampStore.hpp" -#include namespace edfio { diff --git a/include/edfio/writer/WriterHeaderExam.hpp b/include/edfio/writer/WriterHeaderExam.hpp index 3515a7e..9c90931 100644 --- a/include/edfio/writer/WriterHeaderExam.hpp +++ b/include/edfio/writer/WriterHeaderExam.hpp @@ -21,7 +21,7 @@ namespace edfio { -inline void WriteHeaderExam(Writer::Stream &stream, HeaderExam &input) { +inline void WriteHeaderExam(Writer::Stream &stream, const HeaderExam &input) { // Process header general auto general = ProcessHeaderGeneral(input.m_general); diff --git a/include/edfio/writer/WriterHeaderGeneral.hpp b/include/edfio/writer/WriterHeaderGeneral.hpp index f716c2b..b25e078 100644 --- a/include/edfio/writer/WriterHeaderGeneral.hpp +++ b/include/edfio/writer/WriterHeaderGeneral.hpp @@ -18,8 +18,7 @@ namespace edfio { -inline void WriteHeaderGeneral(Writer::Stream &stream, - HeaderGeneralFields &input) { +inline void WriteHeaderGeneral(Writer::Stream &stream, const HeaderGeneralFields &input) { auto &hdr = input; if (!stream || !stream.is_open()) throw std::invalid_argument(GetError(FileErrc::FileNotOpened)); From 25d16b516296f8acdc99f448269ef1f28c12d376 Mon Sep 17 00:00:00 2001 From: iuri dotta Date: Sun, 1 Mar 2026 15:12:15 -0300 Subject: [PATCH 09/11] Add unit tests for EDF+ and BDF+ format processing - Implemented tests for EDF+ format detection, annotation channels, and header fields in `test_edfplus.cpp`. - Added tests for timestamp reading, TAL reading, and annotation processing in EDF+ files. - Created tests for BDF+ format detection and annotation channels in `test_edfplus.cpp`. - Introduced comprehensive tests for header signal processing and validation in `test_processors.cpp`. - Included tests for annotation processing, sample conversion round-trip, and decimal string conversion in `test_processors.cpp`. --- Calib5.edf => files/Calib5.edf | Bin files/SC4001EC-Hypnogram.edf | Bin 0 -> 4620 bytes files/test_generator_2.bdf | Bin 0 -> 4024528 bytes .../test_generator_2.edf | Bin include/edfio/header/HeaderUtils.hpp | 10 +- .../processor/ProcessorHeaderSignalFields.hpp | 8 +- .../processor/ProcessorTimeStampRecord.hpp | 12 +- include/edfio/sink/RecordSink.hpp | 28 - tests/CMakeLists.txt | 19 +- tests/test_bdf_reader.cpp | 307 ++++++++ tests/test_edfplus.cpp | 412 ++++++++++ tests/test_processors.cpp | 724 ++++++++++++++++++ 12 files changed, 1479 insertions(+), 41 deletions(-) rename Calib5.edf => files/Calib5.edf (100%) create mode 100644 files/SC4001EC-Hypnogram.edf create mode 100644 files/test_generator_2.bdf rename test_generator_2.edf => files/test_generator_2.edf (100%) create mode 100644 tests/test_bdf_reader.cpp create mode 100644 tests/test_edfplus.cpp create mode 100644 tests/test_processors.cpp diff --git a/Calib5.edf b/files/Calib5.edf similarity index 100% rename from Calib5.edf rename to files/Calib5.edf diff --git a/files/SC4001EC-Hypnogram.edf b/files/SC4001EC-Hypnogram.edf new file mode 100644 index 0000000000000000000000000000000000000000..e095b039716b7a9839da9de093a3e1592355e5e1 GIT binary patch literal 4620 zcmds4%T63Y4CEtr?{i0>em_nTO2Ro>4#_QADXSHO_@J}n_#-F!M+`QADh=;vQPAziz4 z-MTN=K3+SQePA~qKipovf2BjezUS6&`WyJ==HcP-d_4bne0YAPr?l8s*jdH z7Vi`J8=OTk)X5--;N0G43lfQCG$i&Fo7gg66Hz|If7l~D3CI<3#5SE zhP0W5E9o0pR9xb#^~oSr>0s9+3<+*7BnKS)%q|GOm178C0d8m)^M_RIf`zmZambqy zmpNjVQK&}ig}%X=Rj|LICM?i6AN>)@2dMR`D|x zztu6~1I=)0p!aRGaLCmm|Byxt)6}pb${Hv#)k<-eP&1W`_%b$vAZR0WwJR9Xwq~Ny zOqhs_R4f^{j2)s7+{jjDeRJ^}waZ3qND^h=KvZO?TnR@-ea!~r?A=I&8m#KeiF5~F zR_dLGOR)bl86vT@V$%Ont>}@2(X4?2F?{ZZ18u zCui(>rcYre5l8@uniN6AdFBN{y3;BW2yJ0)k%*0@DiGt!A}q#I6<4%W^{I8B+9%@z z!otIBxjp1MED(ldL&Q8PnMABp5oZM(1Vb@6V@#ndxe^6K3M~pHNkPy#YqAu+c`Ij} zf2C`wY8X4Kbo*2+OkX`%un~ud*;m^*2~wkzK=~2`B&>7fM&jZx#W~qP$VjK~I zFgA!Fj3Yu2bVR6l1%ZFKv|&XUr58v=?&hR66j3{4NT-{W?jzH5(u7K$n}VUAn{t4R zv-2|xH8?w(VSQ^LXW7vip-)J0MuVrN&)z)sUWtO8c{=RT%|R{#G5R79>I;lBBzWk> cj5(0@L)DW^L>2#&p=#LugTr^bi~r#B2h=bpQ2+n{ literal 0 HcmV?d00001 diff --git a/files/test_generator_2.bdf b/files/test_generator_2.bdf new file mode 100644 index 0000000000000000000000000000000000000000..dd27e67e519490f469db75436ec3db071c3ebd3a GIT binary patch literal 4024528 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Ξ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Ξ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�KD?o%uj53MuPz8j6P{f2O4-&|b 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

wLlp<4ltmw9 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 in, else if (IsEdf(version)) n += signal.m_samplesInDataRecord * 2; + auto digitalRange = signal.m_digitalMax - signal.m_digitalMin; + if (digitalRange == 0) { + throw std::invalid_argument( + GetError(FileErrc::FileContainsFormatErrors)); + } signal.m_detail.m_scaling = - (signal.m_physicalMax - signal.m_physicalMin) / - (signal.m_digitalMax - signal.m_digitalMin); + (signal.m_physicalMax - signal.m_physicalMin) / digitalRange; signal.m_detail.m_offset = signal.m_physicalMin - signal.m_detail.m_scaling * signal.m_digitalMin; diff --git a/include/edfio/processor/ProcessorTimeStampRecord.hpp b/include/edfio/processor/ProcessorTimeStampRecord.hpp index 75b73c7..648450f 100644 --- a/include/edfio/processor/ProcessorTimeStampRecord.hpp +++ b/include/edfio/processor/ProcessorTimeStampRecord.hpp @@ -15,6 +15,7 @@ #include #include +#include #include namespace edfio { @@ -41,15 +42,14 @@ inline TimeStamp ProcessTimeStampRecord(Record record, GetError(FileErrc::FileContainsInvalidAnnotations)); } else { *result = 0; - char *end; - double start = std::strtod(value.data(), &end); - // On error - if (end == value.data()) { + double start{}; + auto [ptr, ec] = + std::from_chars(value.data(), value.data() + value.size(), start); + if (ec != std::errc{} || ptr == value.data()) { throw std::invalid_argument( GetError(FileErrc::FileContainsInvalidAnnotations)); - } else { - timestamp.m_start = start; } + timestamp.m_start = start; } return timestamp; } diff --git a/include/edfio/sink/RecordSink.hpp b/include/edfio/sink/RecordSink.hpp index 9dd237d..2e77c61 100644 --- a/include/edfio/sink/RecordSink.hpp +++ b/include/edfio/sink/RecordSink.hpp @@ -12,12 +12,10 @@ #include "../core/Record.hpp" #include "Sink.hpp" - #include #include #include - namespace edfio { class RecordSink : public Sink, Record *, Record &, @@ -172,9 +170,7 @@ class RecordSink : public Sink, Record *, Record &, iterator &operator[](size_type) { return *this; } }; - using const_iterator = iterator; using reverse_iterator = std::reverse_iterator; - using const_reverse_iterator = std::reverse_iterator; RecordSink() = delete; @@ -184,33 +180,9 @@ class RecordSink : public Sink, Record *, Record &, m_headerOffset(headerOffset), m_value(recordSize) {} iterator begin() { return iterator(this, 0); } - const_iterator begin() const { - return const_iterator(const_cast(this), 0); - } - const_iterator cbegin() const { - return const_iterator(const_cast(this), 0); - } iterator end() { return iterator(this); } - const_iterator end() const { - return const_iterator(const_cast(this)); - } - const_iterator cend() const { - return const_iterator(const_cast(this)); - } reverse_iterator rbegin() { return reverse_iterator(end()); } - const_reverse_iterator rbegin() const { - return const_reverse_iterator(end()); - } - const_reverse_iterator crbegin() const { - return const_reverse_iterator(cend()); - } reverse_iterator rend() { return reverse_iterator(begin()); } - const_reverse_iterator rend() const { - return const_reverse_iterator(begin()); - } - const_reverse_iterator crend() const { - return const_reverse_iterator(cbegin()); - } [[nodiscard]] size_type size() const { return m_sinkSize; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ca6f5f2..8f6d3bb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,17 +12,27 @@ function(edfio_add_test name) add_test(NAME ${name} COMMAND ${name}) endfunction() -# Copy test fixtures +# Copy test fixtures from files/ directory configure_file( - ${CMAKE_SOURCE_DIR}/Calib5.edf + ${CMAKE_SOURCE_DIR}/files/Calib5.edf ${CMAKE_CURRENT_BINARY_DIR}/Calib5.edf COPYONLY ) configure_file( - ${CMAKE_SOURCE_DIR}/test_generator_2.edf + ${CMAKE_SOURCE_DIR}/files/test_generator_2.edf ${CMAKE_CURRENT_BINARY_DIR}/test_generator_2.edf COPYONLY ) +configure_file( + ${CMAKE_SOURCE_DIR}/files/test_generator_2.bdf + ${CMAKE_CURRENT_BINARY_DIR}/test_generator_2.bdf + COPYONLY +) +configure_file( + ${CMAKE_SOURCE_DIR}/files/SC4001EC-Hypnogram.edf + ${CMAKE_CURRENT_BINARY_DIR}/SC4001EC-Hypnogram.edf + COPYONLY +) # --- Test executables --- edfio_add_test(test_record) @@ -31,3 +41,6 @@ edfio_add_test(test_processor_sample) edfio_add_test(test_iterators) edfio_add_test(test_writer) edfio_add_test(test_processor_utils) +edfio_add_test(test_bdf_reader) +edfio_add_test(test_processors) +edfio_add_test(test_edfplus) diff --git a/tests/test_bdf_reader.cpp b/tests/test_bdf_reader.cpp new file mode 100644 index 0000000..a019a6a --- /dev/null +++ b/tests/test_bdf_reader.cpp @@ -0,0 +1,307 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include +#include +#include + +using namespace edfio; + +// BDF 24-bit signed integer limits +static constexpr int32_t BDF_DIGITAL_MIN = -8388608; // -(2^23) +static constexpr int32_t BDF_DIGITAL_MAX = 8388607; // (2^23) - 1 + +// --------------------------------------------------------------------------- +// 1. Read BDF header successfully +// --------------------------------------------------------------------------- +TEST_CASE("Read BDF header successfully") { + std::ifstream stream("test_generator_2.bdf", std::ios::binary); + REQUIRE(stream.is_open()); + + auto header = ReadHeaderExam(stream); + + CHECK(header.m_general.m_totalSignals > 0); + CHECK(header.m_general.m_headerSize > 0); + CHECK(header.m_general.m_datarecordsFile > 0); + CHECK(header.m_general.m_datarecordDuration > 0.0); + CHECK(header.m_signals.size() == + static_cast(header.m_general.m_totalSignals)); +} + +// --------------------------------------------------------------------------- +// 2. BDF format detection +// --------------------------------------------------------------------------- +TEST_CASE("BDF format detection") { + std::ifstream stream("test_generator_2.bdf", std::ios::binary); + REQUIRE(stream.is_open()); + + auto header = ReadHeaderExam(stream); + + CHECK(IsBdf(header.m_general.m_version)); + CHECK_FALSE(IsEdf(header.m_general.m_version)); + CHECK(GetSampleBytes(header.m_general.m_version) == 3); +} + +// --------------------------------------------------------------------------- +// 3. BDF signal headers valid +// --------------------------------------------------------------------------- +TEST_CASE("BDF signal headers valid") { + std::ifstream stream("test_generator_2.bdf", std::ios::binary); + REQUIRE(stream.is_open()); + + auto header = ReadHeaderExam(stream); + REQUIRE(header.m_signals.size() > 0); + + for (auto const &sig : header.m_signals) { + CHECK(sig.m_samplesInDataRecord > 0); + CHECK(sig.m_digitalMax > sig.m_digitalMin); + // BDF digital range must fit within 24-bit signed bounds + CHECK(sig.m_digitalMin >= BDF_DIGITAL_MIN); + CHECK(sig.m_digitalMax <= BDF_DIGITAL_MAX); + // Physical range must be valid + CHECK(sig.m_physicalMax > sig.m_physicalMin); + // Label should not be empty + CHECK_FALSE(sig.m_label.empty()); + } +} + +// --------------------------------------------------------------------------- +// 4. BDF data record iteration +// --------------------------------------------------------------------------- +TEST_CASE("BDF data record iteration") { + std::ifstream stream("test_generator_2.bdf", std::ios::binary); + REQUIRE(stream.is_open()); + + auto header = ReadHeaderExam(stream); + auto store = detail::CreateDataRecordStore(stream, header.m_general); + + CHECK(store.size() == + static_cast(header.m_general.m_datarecordsFile)); + CHECK(store.size() > 0); + + // Iterate the first few data records and verify they have content + uint32_t count = 0; + auto limit = std::min(store.size(), + static_cast(5)); + for (auto it = store.begin(); it != store.begin() + static_cast(limit); ++it) { + auto const &rec = *it; + CHECK(rec.Size() > 0); + // Each data record should be recordSize bytes + CHECK(rec.Size() == header.m_general.m_detail.m_recordSize); + ++count; + } + CHECK(count == limit); +} + +// --------------------------------------------------------------------------- +// 5. BDF signal record iteration +// --------------------------------------------------------------------------- +TEST_CASE("BDF signal record iteration") { + std::ifstream stream("test_generator_2.bdf", std::ios::binary); + REQUIRE(stream.is_open()); + + auto header = ReadHeaderExam(stream); + REQUIRE(header.m_signals.size() > 0); + + auto const &sig = header.m_signals[0]; + auto store = detail::CreateSignalRecordStore(stream, header.m_general, sig); + + CHECK(store.size() == + static_cast(header.m_general.m_datarecordsFile)); + + // Iterate first few signal records + uint32_t count = 0; + auto limit = std::min(store.size(), + static_cast(5)); + for (auto it = store.begin(); it != store.begin() + static_cast(limit); ++it) { + auto const &rec = *it; + CHECK(rec.Size() > 0); + // Signal record size = samplesInDataRecord * sampleBytes + auto expectedSize = static_cast(sig.m_samplesInDataRecord) * + static_cast(GetSampleBytes(header.m_general.m_version)); + CHECK(rec.Size() == expectedSize); + ++count; + } + CHECK(count == limit); +} + +// --------------------------------------------------------------------------- +// 6. BDF signal sample store +// --------------------------------------------------------------------------- +TEST_CASE("BDF signal sample store") { + std::ifstream stream("test_generator_2.bdf", std::ios::binary); + REQUIRE(stream.is_open()); + + auto header = ReadHeaderExam(stream); + REQUIRE(header.m_signals.size() > 0); + + auto const &sig = header.m_signals[0]; + auto sampleStore = detail::CreateSignalSampleStore(stream, header.m_general, sig); + + auto expectedSize = static_cast(header.m_general.m_datarecordsFile) * + static_cast(sig.m_samplesInDataRecord); + CHECK(sampleStore.size() == expectedSize); + CHECK(sampleStore.size() > 0); +} + +// --------------------------------------------------------------------------- +// 7. BDF sample reading - digital values in valid 24-bit signed range +// --------------------------------------------------------------------------- +TEST_CASE("BDF sample reading - digital values in 24-bit range") { + std::ifstream stream("test_generator_2.bdf", std::ios::binary); + REQUIRE(stream.is_open()); + + auto header = ReadHeaderExam(stream); + REQUIRE(header.m_signals.size() > 0); + + auto const &sig = header.m_signals[0]; + auto sampleStore = detail::CreateSignalSampleStore(stream, header.m_general, sig); + REQUIRE(sampleStore.size() > 0); + + // Digital processor: offset=0, scaling=1 gives raw digital value + ProcessorSampleRecord proc(0.0, 1.0); + + // Read the first N samples and verify they are within 24-bit signed range + auto limit = std::min(sampleStore.size(), + static_cast(100)); + for (auto it = sampleStore.begin(); + it != sampleStore.begin() + static_cast(limit); ++it) { + auto const &sampleRec = *it; + CHECK(sampleRec.Size() == 3); // BDF: 3 bytes per sample + + int32_t digitalVal = proc(sampleRec); + CHECK(digitalVal >= BDF_DIGITAL_MIN); + CHECK(digitalVal <= BDF_DIGITAL_MAX); + } +} + +// --------------------------------------------------------------------------- +// 8. BDF physical sample conversion +// --------------------------------------------------------------------------- +TEST_CASE("BDF physical sample conversion") { + std::ifstream stream("test_generator_2.bdf", std::ios::binary); + REQUIRE(stream.is_open()); + + auto header = ReadHeaderExam(stream); + REQUIRE(header.m_signals.size() > 0); + + auto const &sig = header.m_signals[0]; + auto sampleStore = detail::CreateSignalSampleStore(stream, header.m_general, sig); + REQUIRE(sampleStore.size() > 0); + + // Physical processor uses the signal's calibration parameters + ProcessorSampleRecord physProc( + sig.m_detail.m_offset, sig.m_detail.m_scaling); + + // Verify scaling and offset are reasonable + CHECK(sig.m_detail.m_scaling != 0.0); + + // Read the first N samples and verify physical values fall within the + // declared physical range (with a small tolerance for rounding) + double physMin = sig.m_physicalMin; + double physMax = sig.m_physicalMax; + double tolerance = (physMax - physMin) * 0.001; // 0.1% tolerance + double lowerBound = physMin - tolerance; + double upperBound = physMax + tolerance; + + auto limit = std::min(sampleStore.size(), + static_cast(100)); + for (auto it = sampleStore.begin(); + it != sampleStore.begin() + static_cast(limit); ++it) { + auto const &sampleRec = *it; + double physVal = physProc(sampleRec); + CHECK(physVal >= lowerBound); + CHECK(physVal <= upperBound); + } +} + +// --------------------------------------------------------------------------- +// 9. BDF write and read-back round-trip +// --------------------------------------------------------------------------- +TEST_CASE("BDF write and read-back round-trip") { + // Read original BDF file + std::ifstream instream("test_generator_2.bdf", std::ios::binary); + REQUIRE(instream.is_open()); + auto header = ReadHeaderExam(instream); + instream.close(); + + const char *tmpfile = "test_roundtrip.bdf"; + + // Write header and all data records to a temporary file + { + std::ofstream outstream(tmpfile, std::ios::binary); + REQUIRE(outstream.is_open()); + WriteHeaderExam(outstream, header); + + // Re-open the original for data copy + std::ifstream instream2("test_generator_2.bdf", std::ios::binary); + REQUIRE(instream2.is_open()); + + auto store = detail::CreateDataRecordStore(instream2, header.m_general); + + // Write each data record directly to the output stream + for (auto it = store.begin(); it != store.end(); ++it) { + outstream << *it; + } + } + + // Read back and compare + std::ifstream checkstream(tmpfile, std::ios::binary); + REQUIRE(checkstream.is_open()); + auto header2 = ReadHeaderExam(checkstream); + + // General header fields must match + CHECK(header2.m_general.m_version == header.m_general.m_version); + CHECK(IsBdf(header2.m_general.m_version)); + CHECK(header2.m_general.m_totalSignals == header.m_general.m_totalSignals); + CHECK(header2.m_general.m_datarecordsFile == header.m_general.m_datarecordsFile); + CHECK(header2.m_general.m_datarecordDuration == + doctest::Approx(header.m_general.m_datarecordDuration)); + CHECK(header2.m_general.m_headerSize == header.m_general.m_headerSize); + CHECK(header2.m_general.m_detail.m_recordSize == + header.m_general.m_detail.m_recordSize); + + // Signal headers must match + REQUIRE(header2.m_signals.size() == header.m_signals.size()); + for (size_t i = 0; i < header.m_signals.size(); ++i) { + auto const &orig = header.m_signals[i]; + auto const © = header2.m_signals[i]; + CHECK(copy.m_label == orig.m_label); + CHECK(copy.m_samplesInDataRecord == orig.m_samplesInDataRecord); + CHECK(copy.m_digitalMin == orig.m_digitalMin); + CHECK(copy.m_digitalMax == orig.m_digitalMax); + CHECK(copy.m_physicalMin == doctest::Approx(orig.m_physicalMin)); + CHECK(copy.m_physicalMax == doctest::Approx(orig.m_physicalMax)); + } + + // Verify sample data integrity: compare first few digital samples + { + std::ifstream origStream("test_generator_2.bdf", std::ios::binary); + REQUIRE(origStream.is_open()); + auto origHeader = ReadHeaderExam(origStream); + + auto const &sig = origHeader.m_signals[0]; + auto origSamples = detail::CreateSignalSampleStore( + origStream, origHeader.m_general, sig); + + auto const &sig2 = header2.m_signals[0]; + auto copySamples = detail::CreateSignalSampleStore( + checkstream, header2.m_general, sig2); + + CHECK(copySamples.size() == origSamples.size()); + + ProcessorSampleRecord proc(0.0, 1.0); + + auto limit = std::min(origSamples.size(), + static_cast(50)); + auto origIt = origSamples.begin(); + auto copyIt = copySamples.begin(); + for (uint64_t i = 0; i < limit; ++i, ++origIt, ++copyIt) { + int32_t origVal = proc(*origIt); + int32_t copyVal = proc(*copyIt); + CHECK(copyVal == origVal); + } + } + + checkstream.close(); + std::remove(tmpfile); +} diff --git a/tests/test_edfplus.cpp b/tests/test_edfplus.cpp new file mode 100644 index 0000000..f184b86 --- /dev/null +++ b/tests/test_edfplus.cpp @@ -0,0 +1,412 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include +#include + +using namespace edfio; + +// ---- EDF+ format detection ---- + +TEST_CASE("test_generator_2.edf is EDF+ format") { + std::ifstream stream("test_generator_2.edf", std::ios::binary); + REQUIRE(stream.is_open()); + auto header = ReadHeaderExam(stream); + + CHECK(IsEdf(header.m_general.m_version)); + CHECK(IsPlus(header.m_general.m_version)); + CHECK_FALSE(IsBdf(header.m_general.m_version)); + CHECK(header.m_general.m_version == DataFormat::EdfPlusC); +} + +TEST_CASE("EDF+ has annotation channels") { + std::ifstream stream("test_generator_2.edf", std::ios::binary); + REQUIRE(stream.is_open()); + auto header = ReadHeaderExam(stream); + + // Find annotation channels + int annotCount = 0; + int annotIdx = -1; + for (size_t i = 0; i < header.m_signals.size(); ++i) { + if (header.m_signals[i].m_detail.m_isAnnotation) { + if (annotIdx < 0) + annotIdx = static_cast(i); + ++annotCount; + } + } + CHECK(annotCount > 0); + REQUIRE(annotIdx >= 0); + + // Annotation channel label should contain "Annotation" + CHECK(header.m_signals[annotIdx].m_label.find("Annotation") != + std::string::npos); +} + +// ---- EDF+ header fields ---- + +TEST_CASE("EDF+ patient and recording fields are parsed") { + std::ifstream stream("test_generator_2.edf", std::ios::binary); + REQUIRE(stream.is_open()); + auto header = ReadHeaderExam(stream); + + // EDF+ has structured patient/recording fields + CHECK(header.m_general.m_totalSignals == 12); + CHECK(header.m_general.m_datarecordsFile == 600); + CHECK(header.m_general.m_datarecordDuration == doctest::Approx(1.0)); + CHECK(header.m_general.m_headerSize > 0); + + // Date should be 10-DEC-2009 + CHECK(header.m_general.m_startDate.day == 10); + CHECK(header.m_general.m_startDate.month == 12); + CHECK(header.m_general.m_startDate.year == 2009); +} + +// ---- TimeStamp reading ---- + +TEST_CASE("EDF+ TimeStampStore reads timestamps from annotation channel") { + std::ifstream stream("test_generator_2.edf", std::ios::binary); + REQUIRE(stream.is_open()); + auto header = ReadHeaderExam(stream); + + // Find first annotation signal + const HeaderSignal *annotSignal = nullptr; + for (auto const &sig : header.m_signals) { + if (sig.m_detail.m_isAnnotation) { + annotSignal = &sig; + break; + } + } + REQUIRE(annotSignal != nullptr); + + auto tsStore = + detail::CreateTimeStampStore(stream, header.m_general, *annotSignal); + CHECK(tsStore.size() == static_cast(header.m_general.m_datarecordsFile)); + + // Read first timestamp - TimeStampStore uses getline(delim=20) so the + // result is the raw timestamp string (e.g. "+0"), NOT a full TAL record + auto it = tsStore.begin(); + auto &rec = *it; + CHECK(rec.Size() > 0); + + // The first byte should be '+' or '-' + auto const &data = rec(); + CHECK((data[0] == '+' || data[0] == '-')); + + // Parse the timestamp value directly + std::string tsStr(data.begin(), data.end()); + // Trim null bytes + auto nullPos = tsStr.find('\0'); + if (nullPos != std::string::npos) + tsStr.resize(nullPos); + CHECK(!tsStr.empty()); + // First timestamp should start with '+' + CHECK(tsStr[0] == '+'); +} + +TEST_CASE("EDF+ timestamps are monotonically non-decreasing") { + std::ifstream stream("test_generator_2.edf", std::ios::binary); + REQUIRE(stream.is_open()); + auto header = ReadHeaderExam(stream); + + const HeaderSignal *annotSignal = nullptr; + for (auto const &sig : header.m_signals) { + if (sig.m_detail.m_isAnnotation) { + annotSignal = &sig; + break; + } + } + REQUIRE(annotSignal != nullptr); + + auto tsStore = + detail::CreateTimeStampStore(stream, header.m_general, *annotSignal); + + double prevStart = -1.0; + int64_t count = 0; + for (auto it = tsStore.begin(); it != tsStore.end() && count < 10; ++it, ++count) { + auto &rec = *it; + auto const &data = rec(); + // Extract timestamp string, trimming nulls + std::string tsStr(data.begin(), data.end()); + auto nullPos = tsStr.find('\0'); + if (nullPos != std::string::npos) + tsStr.resize(nullPos); + REQUIRE(!tsStr.empty()); + REQUIRE((tsStr[0] == '+' || tsStr[0] == '-')); + + // Parse the numeric value + double val = detail::ParseDouble( + std::string_view(tsStr).substr(tsStr[0] == '+' ? 1 : 0), + "bad timestamp"); + if (tsStr[0] == '-') + val = -val; + CHECK(val >= prevStart); + prevStart = val; + } + CHECK(count == 10); +} + +// ---- TAL (Time-stamped Annotation List) reading ---- + +TEST_CASE("EDF+ TAL reading via SignalRecordStore") { + std::ifstream stream("test_generator_2.edf", std::ios::binary); + REQUIRE(stream.is_open()); + auto header = ReadHeaderExam(stream); + + const HeaderSignal *annotSignal = nullptr; + for (auto const &sig : header.m_signals) { + if (sig.m_detail.m_isAnnotation) { + annotSignal = &sig; + break; + } + } + REQUIRE(annotSignal != nullptr); + + auto sigStore = + detail::CreateSignalRecordStore(stream, header.m_general, *annotSignal); + + // Read first annotation record + auto it = sigStore.begin(); + auto &rec = *it; + CHECK(rec.Size() > 0); + + // The TAL data is the raw bytes of the annotation signal record + // Convert to vector for ProcessTalRecord + std::vector talData(rec().begin(), rec().end()); + + // First TAL should start with '+' or '-' + REQUIRE(!talData.empty()); + CHECK((talData.front() == '+' || talData.front() == '-')); +} + +TEST_CASE("EDF+ TalStore iterates through TALs") { + std::ifstream stream("test_generator_2.edf", std::ios::binary); + REQUIRE(stream.is_open()); + auto header = ReadHeaderExam(stream); + + const HeaderSignal *annotSignal = nullptr; + for (auto const &sig : header.m_signals) { + if (sig.m_detail.m_isAnnotation) { + annotSignal = &sig; + break; + } + } + REQUIRE(annotSignal != nullptr); + + // Read first signal record to use as TalStore input + auto sigStore = + detail::CreateSignalRecordStore(stream, header.m_general, *annotSignal); + auto sigIt = sigStore.begin(); + auto &rec = *sigIt; + + // Create TalStore from the record + TalStore talStore(rec); + int talCount = 0; + for (auto it = talStore.begin(); it != talStore.end(); ++it) { + auto &tal = *it; + CHECK(!tal.empty()); + ++talCount; + } + // Should have at least the timestamp TAL + CHECK(talCount >= 1); +} + +TEST_CASE("ProcessTalRecord extracts annotations from TAL data") { + // Build a synthetic TAL: "+0\x14\x14\x00" (timestamp only, no annotation text) + std::vector tal1 = {'+', '0', 20, 20, 0}; + auto annots1 = ProcessTalRecord(tal1, 0); + // Timestamp-only TAL should produce no annotations (empty annotation text is skipped) + CHECK(annots1.empty()); + + // Build a TAL with an annotation: "+1.5\x14test annotation\x14\x00" + std::vector tal2; + for (char c : std::string("+1.5")) + tal2.push_back(c); + tal2.push_back(20); // ANNOTATION_DIV + for (char c : std::string("test annotation")) + tal2.push_back(c); + tal2.push_back(20); // ANNOTATION_DIV + tal2.push_back(0); // end + + auto annots2 = ProcessTalRecord(tal2, 5); + REQUIRE(annots2.size() == 1); + CHECK(annots2[0].m_start == doctest::Approx(1.5)); + CHECK(annots2[0].m_duration == doctest::Approx(0.0)); + CHECK(annots2[0].m_annotation == "test annotation"); + CHECK(annots2[0].m_datarecord == 5); +} + +TEST_CASE("ProcessTalRecord with duration") { + // "+2.0\x1530.5\x14my event\x14\x00" + std::vector tal; + for (char c : std::string("+2.0")) + tal.push_back(c); + tal.push_back(21); // DURATION_DIV + for (char c : std::string("30.5")) + tal.push_back(c); + tal.push_back(20); // ANNOTATION_DIV + for (char c : std::string("my event")) + tal.push_back(c); + tal.push_back(20); // ANNOTATION_DIV + tal.push_back(0); + + auto annots = ProcessTalRecord(tal, 0); + REQUIRE(annots.size() == 1); + CHECK(annots[0].m_start == doctest::Approx(2.0)); + CHECK(annots[0].m_duration == doctest::Approx(30.5)); + CHECK(annots[0].m_annotation == "my event"); +} + +TEST_CASE("ProcessTalRecord with multiple annotations in one TAL") { + // "+0\x14event1\x14event2\x14\x00" + std::vector tal; + for (char c : std::string("+0")) + tal.push_back(c); + tal.push_back(20); // ANNOTATION_DIV + for (char c : std::string("event1")) + tal.push_back(c); + tal.push_back(20); // ANNOTATION_DIV + for (char c : std::string("event2")) + tal.push_back(c); + tal.push_back(20); // ANNOTATION_DIV + tal.push_back(0); + + auto annots = ProcessTalRecord(tal, 0); + REQUIRE(annots.size() == 2); + CHECK(annots[0].m_annotation == "event1"); + CHECK(annots[1].m_annotation == "event2"); + CHECK(annots[0].m_start == doctest::Approx(0.0)); + CHECK(annots[1].m_start == doctest::Approx(0.0)); +} + +TEST_CASE("ProcessTalRecord with negative onset") { + std::vector tal; + for (char c : std::string("-5.25")) + tal.push_back(c); + tal.push_back(20); + for (char c : std::string("neg event")) + tal.push_back(c); + tal.push_back(20); + tal.push_back(0); + + auto annots = ProcessTalRecord(tal, 0); + REQUIRE(annots.size() == 1); + CHECK(annots[0].m_start == doctest::Approx(-5.25)); + CHECK(annots[0].m_annotation == "neg event"); +} + +TEST_CASE("ProcessTalRecord throws on invalid TAL") { + // TAL not starting with '+' or '-' + std::vector bad = {'0', 20, 20, 0}; + CHECK_THROWS_AS(ProcessTalRecord(bad, 0), std::invalid_argument); +} + +// ---- Annotation-only EDF+ file (Hypnogram) ---- + +TEST_CASE("Hypnogram EDF+ is annotation-only format") { + std::ifstream stream("SC4001EC-Hypnogram.edf", std::ios::binary); + REQUIRE(stream.is_open()); + auto header = ReadHeaderExam(stream); + + CHECK(IsEdf(header.m_general.m_version)); + CHECK(IsPlus(header.m_general.m_version)); + CHECK(header.m_general.m_version == DataFormat::EdfPlusC); + + // Should have exactly 1 signal (the annotation channel) + CHECK(header.m_general.m_totalSignals == 1); + CHECK(header.m_signals.size() == 1); + CHECK(header.m_signals[0].m_detail.m_isAnnotation); + CHECK(header.m_signals[0].m_label.find("Annotation") != std::string::npos); +} + +TEST_CASE("Hypnogram annotations can be read") { + std::ifstream stream("SC4001EC-Hypnogram.edf", std::ios::binary); + REQUIRE(stream.is_open()); + auto header = ReadHeaderExam(stream); + REQUIRE(header.m_signals.size() == 1); + REQUIRE(header.m_signals[0].m_detail.m_isAnnotation); + + auto const &annotSignal = header.m_signals[0]; + auto sigStore = + detail::CreateSignalRecordStore(stream, header.m_general, annotSignal); + + // Read annotation records and parse TALs + int totalAnnotations = 0; + int64_t drIdx = 0; + for (auto it = sigStore.begin(); it != sigStore.end(); ++it, ++drIdx) { + auto &rec = *it; + std::vector talData(rec().begin(), rec().end()); + if (talData.empty() || (talData.front() != '+' && talData.front() != '-')) + continue; + + auto annots = ProcessTalRecord(talData, drIdx); + totalAnnotations += static_cast(annots.size()); + + for (auto const &a : annots) { + // Sleep stage annotations should have non-empty text + CHECK(!a.m_annotation.empty()); + // Duration should be >= 0 + CHECK(a.m_duration >= 0.0); + } + } + // Hypnogram should have many sleep stage annotations + CHECK(totalAnnotations > 0); +} + +// ---- ProcessTimeStamp (write) ---- + +TEST_CASE("ProcessTimeStamp creates correct record for positive time") { + TimeStamp ts; + ts.m_start = 5.0; + ts.m_datarecord = 5; + auto rec = ProcessTimeStamp(ts); + std::string content(rec().begin(), rec().end()); + // Should start with +5 + CHECK(content[0] == '+'); + CHECK(content[1] == '5'); + // Should end with \x14\x14\x00 + auto sz = rec.Size(); + CHECK(rec()[sz - 3] == 20); + CHECK(rec()[sz - 2] == 20); + CHECK(rec()[sz - 1] == 0); +} + +TEST_CASE("ProcessTimeStamp handles negative time") { + TimeStamp ts; + ts.m_start = -3.5; + ts.m_datarecord = 0; + auto rec = ProcessTimeStamp(ts); + CHECK(rec()[0] == '-'); +} + +TEST_CASE("ProcessTimeStamp handles zero time") { + TimeStamp ts; + ts.m_start = 0.0; + ts.m_datarecord = 0; + auto rec = ProcessTimeStamp(ts); + CHECK(rec()[0] == '+'); +} + +// ---- BDF+ format detection ---- + +TEST_CASE("test_generator_2.bdf is BDF+ format") { + std::ifstream stream("test_generator_2.bdf", std::ios::binary); + REQUIRE(stream.is_open()); + auto header = ReadHeaderExam(stream); + + CHECK(IsBdf(header.m_general.m_version)); + CHECK(IsPlus(header.m_general.m_version)); + CHECK_FALSE(IsEdf(header.m_general.m_version)); + CHECK(header.m_general.m_version == DataFormat::BdfPlusC); +} + +TEST_CASE("BDF+ has annotation channels") { + std::ifstream stream("test_generator_2.bdf", std::ios::binary); + REQUIRE(stream.is_open()); + auto header = ReadHeaderExam(stream); + + int annotCount = 0; + for (auto const &sig : header.m_signals) { + if (sig.m_detail.m_isAnnotation) + ++annotCount; + } + CHECK(annotCount > 0); +} diff --git a/tests/test_processors.cpp b/tests/test_processors.cpp new file mode 100644 index 0000000..a59c172 --- /dev/null +++ b/tests/test_processors.cpp @@ -0,0 +1,724 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include +#include +#include + +using namespace edfio; + +// --------------------------------------------------------------------------- +// Helper: build a minimal HeaderSignal suitable for non-Plus EDF format +// --------------------------------------------------------------------------- +static HeaderSignal MakeEdfSignal(const std::string &label, double physMin, + double physMax, int32_t digMin, + int32_t digMax, int32_t samples) { + HeaderSignal sig; + sig.m_label = label; + sig.m_transducer = "AgAgCl"; + sig.m_physDimension = "uV"; + sig.m_physicalMin = physMin; + sig.m_physicalMax = physMax; + sig.m_digitalMin = digMin; + sig.m_digitalMax = digMax; + sig.m_prefilter = "HP:0.1Hz"; + sig.m_samplesInDataRecord = samples; + sig.m_reserved = ""; + return sig; +} + +// =========================================================================== +// TEST SUITE: ProcessHeaderSignal / ProcessHeaderSignalFields round-trip +// =========================================================================== +TEST_SUITE("ProcessHeaderSignal round-trip") { + + TEST_CASE("EDF signal round-trips through fields and back") { + // 1. Create a known HeaderSignal + HeaderSignal original = MakeEdfSignal("EEG Fp1", -500.0, 500.0, + -32768, 32767, 256); + + // 2. Convert to HeaderSignalFields + std::vector input{original}; + auto fields = ProcessHeaderSignal(std::move(input)); + + REQUIRE(fields.size() == 1); + + // Verify intermediate field values are properly padded + CHECK(fields[0].m_label().size() == 16); + CHECK(fields[0].m_digitalMin().size() == 8); + CHECK(fields[0].m_samplesInDataRecord().size() == 8); + + // 3. Convert back with EDF format (non-Plus, no annotation required) + auto signals = + ProcessHeaderSignalFields(std::move(fields), DataFormat::Edf, 1.0); + + REQUIRE(signals.size() == 1); + const auto &out = signals[0]; + + // Verify all fields round-trip correctly + CHECK(out.m_label == "EEG Fp1"); + CHECK(out.m_transducer == "AgAgCl"); + CHECK(out.m_physDimension == "uV"); + CHECK(out.m_physicalMin == doctest::Approx(-500.0)); + CHECK(out.m_physicalMax == doctest::Approx(500.0)); + CHECK(out.m_digitalMin == -32768); + CHECK(out.m_digitalMax == 32767); + CHECK(out.m_prefilter == "HP:0.1Hz"); + CHECK(out.m_samplesInDataRecord == 256); + + // Verify computed detail fields + double expectedScaling = + (500.0 - (-500.0)) / (32767 - (-32768)); // 1000 / 65535 + CHECK(out.m_detail.m_scaling == doctest::Approx(expectedScaling)); + CHECK(out.m_detail.m_signalOffset == 0); + } + + TEST_CASE("BDF signal round-trips with 24-bit digital range") { + HeaderSignal original = + MakeEdfSignal("EEG Fp2", -3200.0, 3200.0, -8388608, 8388607, 512); + + std::vector input{original}; + auto fields = ProcessHeaderSignal(std::move(input)); + + REQUIRE(fields.size() == 1); + + auto signals = + ProcessHeaderSignalFields(std::move(fields), DataFormat::Bdf, 1.0); + + REQUIRE(signals.size() == 1); + const auto &out = signals[0]; + + CHECK(out.m_digitalMin == -8388608); + CHECK(out.m_digitalMax == 8388607); + CHECK(out.m_physicalMin == doctest::Approx(-3200.0)); + CHECK(out.m_physicalMax == doctest::Approx(3200.0)); + CHECK(out.m_samplesInDataRecord == 512); + + // BDF uses 3 bytes per sample + CHECK(out.m_detail.m_signalOffset == 0); + } + + TEST_CASE("EDF signal with typical 16-bit range") { + HeaderSignal original = + MakeEdfSignal("EMG", -100.0, 100.0, -32768, 32767, 128); + + std::vector input{original}; + auto fields = ProcessHeaderSignal(std::move(input)); + auto signals = + ProcessHeaderSignalFields(std::move(fields), DataFormat::Edf, 1.0); + + REQUIRE(signals.size() == 1); + const auto &out = signals[0]; + + CHECK(out.m_digitalMin == -32768); + CHECK(out.m_digitalMax == 32767); + + double expectedScaling = + (100.0 - (-100.0)) / (32767 - (-32768)); // 200 / 65535 + CHECK(out.m_detail.m_scaling == doctest::Approx(expectedScaling)); + } + + TEST_CASE("Multiple signals: offset accumulates correctly for EDF") { + HeaderSignal sig1 = + MakeEdfSignal("EEG Fp1", -500.0, 500.0, -32768, 32767, 256); + HeaderSignal sig2 = + MakeEdfSignal("EEG Fp2", -500.0, 500.0, -32768, 32767, 128); + + std::vector input{sig1, sig2}; + auto fields = ProcessHeaderSignal(std::move(input)); + auto signals = + ProcessHeaderSignalFields(std::move(fields), DataFormat::Edf, 1.0); + + REQUIRE(signals.size() == 2); + + // First signal offset is 0 + CHECK(signals[0].m_detail.m_signalOffset == 0); + // Second signal offset = first signal's samples * 2 bytes (EDF) + CHECK(signals[1].m_detail.m_signalOffset == 256 * 2); + } + + TEST_CASE("Multiple signals: offset accumulates correctly for BDF") { + HeaderSignal sig1 = + MakeEdfSignal("EEG Fp1", -500.0, 500.0, -8388608, 8388607, 256); + HeaderSignal sig2 = + MakeEdfSignal("EEG Fp2", -500.0, 500.0, -8388608, 8388607, 100); + + std::vector input{sig1, sig2}; + auto fields = ProcessHeaderSignal(std::move(input)); + auto signals = + ProcessHeaderSignalFields(std::move(fields), DataFormat::Bdf, 1.0); + + REQUIRE(signals.size() == 2); + + CHECK(signals[0].m_detail.m_signalOffset == 0); + // Second signal offset = first signal's samples * 3 bytes (BDF) + CHECK(signals[1].m_detail.m_signalOffset == 256 * 3); + } + +} // TEST_SUITE "ProcessHeaderSignal round-trip" + +// =========================================================================== +// TEST SUITE: ProcessHeaderSignalFields validation +// =========================================================================== +TEST_SUITE("ProcessHeaderSignalFields validation") { + + // Helper: create a valid single-signal HeaderSignalFields for non-Plus EDF + static std::vector + MakeValidEdfFields(int32_t digMin = -32768, int32_t digMax = 32767, + int32_t samples = 256) { + HeaderSignalFields f; + f.m_label("EEG Fp1"); + f.m_transducer("AgAgCl"); + f.m_physDimension("uV"); + f.m_physicalMin("-500"); + f.m_physicalMax("500"); + f.m_digitalMin(std::to_string(digMin)); + f.m_digitalMax(std::to_string(digMax)); + f.m_prefilter("HP:0.1Hz"); + f.m_samplesInDataRecord(std::to_string(samples)); + f.m_reserved(""); + return {f}; + } + + TEST_CASE("digitalMax equal to digitalMin throws (div-by-zero)") { + auto fields = MakeValidEdfFields(100, 100, 256); + // digitalMax < digitalMin + 1 means digitalMax == digitalMin fails + CHECK_THROWS_AS( + ProcessHeaderSignalFields(std::move(fields), DataFormat::Edf, 1.0), + std::invalid_argument); + } + + TEST_CASE("digitalMin greater than digitalMax throws") { + auto fields = MakeValidEdfFields(1000, -1000, 256); + CHECK_THROWS_AS( + ProcessHeaderSignalFields(std::move(fields), DataFormat::Edf, 1.0), + std::invalid_argument); + } + + TEST_CASE("samplesInDataRecord less than 1 throws") { + auto fields = MakeValidEdfFields(-32768, 32767, 0); + CHECK_THROWS_AS( + ProcessHeaderSignalFields(std::move(fields), DataFormat::Edf, 1.0), + std::invalid_argument); + } + + TEST_CASE("samplesInDataRecord negative throws") { + auto fields = MakeValidEdfFields(-32768, 32767, -1); + CHECK_THROWS_AS( + ProcessHeaderSignalFields(std::move(fields), DataFormat::Edf, 1.0), + std::invalid_argument); + } + + TEST_CASE("EDF digital min out of range throws") { + // digitalMin = -32769, below EDF 16-bit signed range + auto fields = MakeValidEdfFields(-32769, 32767, 256); + CHECK_THROWS_AS( + ProcessHeaderSignalFields(std::move(fields), DataFormat::Edf, 1.0), + std::invalid_argument); + } + + TEST_CASE("EDF digital max out of range throws") { + // digitalMax = 32768, above EDF 16-bit signed range + auto fields = MakeValidEdfFields(-32768, 32768, 256); + CHECK_THROWS_AS( + ProcessHeaderSignalFields(std::move(fields), DataFormat::Edf, 1.0), + std::invalid_argument); + } + + TEST_CASE("BDF digital min out of range throws") { + // -8388609 is below BDF 24-bit signed range + // Use BDF fields helper + HeaderSignalFields f; + f.m_label("EEG Fp1"); + f.m_transducer("AgAgCl"); + f.m_physDimension("uV"); + f.m_physicalMin("-500"); + f.m_physicalMax("500"); + f.m_digitalMin("-8388609"); + f.m_digitalMax("8388607"); + f.m_prefilter("HP:0.1Hz"); + f.m_samplesInDataRecord("256"); + f.m_reserved(""); + std::vector fields{f}; + CHECK_THROWS_AS( + ProcessHeaderSignalFields(std::move(fields), DataFormat::Bdf, 1.0), + std::invalid_argument); + } + + TEST_CASE("BDF digital max out of range throws") { + HeaderSignalFields f; + f.m_label("EEG Fp1"); + f.m_transducer("AgAgCl"); + f.m_physDimension("uV"); + f.m_physicalMin("-500"); + f.m_physicalMax("500"); + f.m_digitalMin("-8388608"); + f.m_digitalMax("8388608"); + f.m_prefilter("HP:0.1Hz"); + f.m_samplesInDataRecord("256"); + f.m_reserved(""); + std::vector fields{f}; + CHECK_THROWS_AS( + ProcessHeaderSignalFields(std::move(fields), DataFormat::Bdf, 1.0), + std::invalid_argument); + } + + TEST_CASE("Valid EDF boundary values do not throw") { + auto fields = MakeValidEdfFields(-32768, 32767, 1); + CHECK_NOTHROW( + ProcessHeaderSignalFields(std::move(fields), DataFormat::Edf, 1.0)); + } + + TEST_CASE("Valid BDF boundary values do not throw") { + HeaderSignalFields f; + f.m_label("EEG Fp1"); + f.m_transducer("AgAgCl"); + f.m_physDimension("uV"); + f.m_physicalMin("-500"); + f.m_physicalMax("500"); + f.m_digitalMin("-8388608"); + f.m_digitalMax("8388607"); + f.m_prefilter("HP:0.1Hz"); + f.m_samplesInDataRecord("256"); + f.m_reserved(""); + std::vector fields{f}; + CHECK_NOTHROW( + ProcessHeaderSignalFields(std::move(fields), DataFormat::Bdf, 1.0)); + } + +} // TEST_SUITE "ProcessHeaderSignalFields validation" + +// =========================================================================== +// TEST SUITE: ProcessAnnotation +// =========================================================================== +TEST_SUITE("ProcessAnnotation") { + + TEST_CASE("Positive start, no duration") { + Annotation ann; + ann.m_start = 1.5; + ann.m_duration = 0; + ann.m_annotation = "TestEvent"; + + auto record = ProcessAnnotation(ann); + const auto &bytes = record(); + + // Expected layout: "+1.5" \x14 "TestEvent" \x14 \x00 + std::string content(bytes.begin(), bytes.end()); + + // Verify timestamp starts with '+' + CHECK(content[0] == '+'); + // Verify timestamp value is present + CHECK(content.find("+1.5") == 0); + + // Verify ANNOTATION_DIV (0x14 = 20) separates timestamp from annotation + size_t firstDiv = content.find(static_cast(20)); + REQUIRE(firstDiv != std::string::npos); + + // No duration means no DURATION_DIV (21) before the ANNOTATION_DIV + for (size_t i = 0; i < firstDiv; ++i) { + CHECK(content[i] != static_cast(21)); + } + + // Verify annotation text follows the first div + std::string annotText = + content.substr(firstDiv + 1, std::string("TestEvent").size()); + CHECK(annotText == "TestEvent"); + + // Verify record ends with \x14 \x00 + CHECK(bytes[bytes.size() - 2] == static_cast(20)); + CHECK(bytes[bytes.size() - 1] == static_cast(0)); + } + + TEST_CASE("Negative start produces '-' prefix") { + Annotation ann; + ann.m_start = -2.0; + ann.m_duration = 0; + ann.m_annotation = "Onset"; + + auto record = ProcessAnnotation(ann); + const auto &bytes = record(); + std::string content(bytes.begin(), bytes.end()); + + // Negative start should begin with '-', not '+' + CHECK(content[0] == '-'); + CHECK(content.find("-2") == 0); + } + + TEST_CASE("Positive duration includes duration field") { + Annotation ann; + ann.m_start = 10.0; + ann.m_duration = 2.5; + ann.m_annotation = "Stimulus"; + + auto record = ProcessAnnotation(ann); + const auto &bytes = record(); + std::string content(bytes.begin(), bytes.end()); + + // Layout: "+10" \x15 "2.5" \x14 "Stimulus" \x14 \x00 + // DURATION_DIV (21 = 0x15) must be present + size_t durDiv = content.find(static_cast(21)); + REQUIRE(durDiv != std::string::npos); + + // Duration text follows the DURATION_DIV + size_t annotDiv = content.find(static_cast(20)); + std::string durText = content.substr(durDiv + 1, annotDiv - durDiv - 1); + CHECK(durText == "2.5"); + } + + TEST_CASE("Zero duration omits duration field") { + Annotation ann; + ann.m_start = 5.0; + ann.m_duration = 0.0; + ann.m_annotation = "Marker"; + + auto record = ProcessAnnotation(ann); + const auto &bytes = record(); + std::string content(bytes.begin(), bytes.end()); + + // No DURATION_DIV (21) should appear before the first ANNOTATION_DIV (20) + size_t annotDiv = content.find(static_cast(20)); + REQUIRE(annotDiv != std::string::npos); + for (size_t i = 0; i < annotDiv; ++i) { + CHECK(content[i] != static_cast(21)); + } + } + + TEST_CASE("Empty annotation string throws") { + Annotation ann; + ann.m_start = 0.0; + ann.m_duration = 0.0; + ann.m_annotation = ""; + + CHECK_THROWS_AS(ProcessAnnotation(ann), std::invalid_argument); + } + + TEST_CASE("Zero start gets '+' prefix") { + Annotation ann; + ann.m_start = 0.0; + ann.m_duration = 0.0; + ann.m_annotation = "Start"; + + auto record = ProcessAnnotation(ann); + const auto &bytes = record(); + + // 0.0 >= 0, so '+' prefix + CHECK(bytes[0] == '+'); + } + + TEST_CASE("Annotation record has correct total size") { + Annotation ann; + ann.m_start = 3.0; + ann.m_duration = 1.5; + ann.m_annotation = "Event"; + + auto record = ProcessAnnotation(ann); + + // Expected: "+3" (2) + \x15 (1) + "1.5" (3) + \x14 (1) + "Event" (5) + + // \x14 (1) + \x00 (1) = 14 + CHECK(record.Size() == 14); + } + +} // TEST_SUITE "ProcessAnnotation" + +// =========================================================================== +// TEST SUITE: Sample conversion round-trip +// =========================================================================== +TEST_SUITE("Sample conversion round-trip") { + + TEST_CASE("Digital 2-byte round-trip: encode then decode") { + // Use identity conversion (offset=0, scaling=1) + const double offset = 0.0; + const double scaling = 1.0; + const uint32_t sampleSize = 2; // EDF + + ProcessorSample encoder(offset, scaling, + sampleSize); + ProcessorSampleRecord decoder(offset, scaling); + + SUBCASE("Positive value") { + int32_t original = 1234; + auto record = encoder(original); + REQUIRE(record.Size() == 2); + int32_t decoded = decoder(record); + CHECK(decoded == original); + } + + SUBCASE("Negative value") { + int32_t original = -1234; + auto record = encoder(original); + REQUIRE(record.Size() == 2); + int32_t decoded = decoder(record); + CHECK(decoded == original); + } + + SUBCASE("Zero value") { + int32_t original = 0; + auto record = encoder(original); + REQUIRE(record.Size() == 2); + int32_t decoded = decoder(record); + CHECK(decoded == original); + } + } + + TEST_CASE("Digital 3-byte (BDF) round-trip: encode then decode") { + const double offset = 0.0; + const double scaling = 1.0; + const uint32_t sampleSize = 3; // BDF + + ProcessorSample encoder(offset, scaling, + sampleSize); + ProcessorSampleRecord decoder(offset, scaling); + + SUBCASE("Positive value") { + int32_t original = 100000; + auto record = encoder(original); + REQUIRE(record.Size() == 3); + int32_t decoded = decoder(record); + CHECK(decoded == original); + } + + SUBCASE("Negative value") { + int32_t original = -100000; + auto record = encoder(original); + REQUIRE(record.Size() == 3); + int32_t decoded = decoder(record); + CHECK(decoded == original); + } + + SUBCASE("Zero value") { + int32_t original = 0; + auto record = encoder(original); + REQUIRE(record.Size() == 3); + int32_t decoded = decoder(record); + CHECK(decoded == original); + } + } + + TEST_CASE("Physical to digital round-trip through records") { + // Simulate a real EDF signal: physMin=-500, physMax=500, + // digMin=-32768, digMax=32767 + const double physMin = -500.0; + const double physMax = 500.0; + const int32_t digMin = -32768; + const int32_t digMax = 32767; + const double scaling = (physMax - physMin) / (digMax - digMin); + const double offset = physMin - scaling * digMin; + const uint32_t sampleSize = 2; + + // Physical -> Digital -> Record -> Digital -> Physical + double physValue = 123.456; + int32_t digital = ConvertSample(offset, scaling, physValue); + + ProcessorSample encoder(offset, scaling, + sampleSize); + ProcessorSampleRecord decoder(offset, scaling); + + auto record = encoder(digital); + int32_t decodedDigital = decoder(record); + + // Digital value must survive the record round-trip exactly + CHECK(decodedDigital == digital); + + // Convert back to physical + double decodedPhysical = ConvertSample(offset, scaling, decodedDigital); + // Allow small error due to quantization + CHECK(decodedPhysical == doctest::Approx(physValue).epsilon(0.001)); + } + + TEST_CASE("2-byte boundary values round-trip") { + const double offset = 0.0; + const double scaling = 1.0; + const uint32_t sampleSize = 2; + + ProcessorSample encoder(offset, scaling, + sampleSize); + ProcessorSampleRecord decoder(offset, scaling); + + SUBCASE("Max positive: 32767") { + int32_t original = 32767; + auto record = encoder(original); + int32_t decoded = decoder(record); + CHECK(decoded == 32767); + } + + SUBCASE("Min negative: -32768") { + int32_t original = -32768; + auto record = encoder(original); + int32_t decoded = decoder(record); + CHECK(decoded == -32768); + } + + SUBCASE("Minus one: -1") { + int32_t original = -1; + auto record = encoder(original); + int32_t decoded = decoder(record); + CHECK(decoded == -1); + } + + SUBCASE("Plus one: 1") { + int32_t original = 1; + auto record = encoder(original); + int32_t decoded = decoder(record); + CHECK(decoded == 1); + } + } + + TEST_CASE("3-byte boundary values round-trip") { + const double offset = 0.0; + const double scaling = 1.0; + const uint32_t sampleSize = 3; + + ProcessorSample encoder(offset, scaling, + sampleSize); + ProcessorSampleRecord decoder(offset, scaling); + + SUBCASE("Max positive: 8388607") { + int32_t original = 8388607; + auto record = encoder(original); + int32_t decoded = decoder(record); + CHECK(decoded == 8388607); + } + + SUBCASE("Min negative: -8388608") { + int32_t original = -8388608; + auto record = encoder(original); + int32_t decoded = decoder(record); + CHECK(decoded == -8388608); + } + + SUBCASE("Minus one: -1") { + int32_t original = -1; + auto record = encoder(original); + int32_t decoded = decoder(record); + CHECK(decoded == -1); + } + } + + TEST_CASE("ConvertSample digital-to-physical and back") { + const double physMin = -3200.0; + const double physMax = 3200.0; + const int32_t digMin = -8388608; + const int32_t digMax = 8388607; + const double scaling = (physMax - physMin) / (digMax - digMin); + const double offset = physMin - scaling * digMin; + + SUBCASE("Mid-range digital value") { + int32_t digital = 0; + double physical = ConvertSample(offset, scaling, digital); + int32_t backDigital = ConvertSample(offset, scaling, physical); + CHECK(backDigital == digital); + } + + SUBCASE("Max digital value") { + int32_t digital = digMax; + double physical = ConvertSample(offset, scaling, digital); + CHECK(physical == doctest::Approx(physMax).epsilon(0.001)); + } + + SUBCASE("Min digital value") { + int32_t digital = digMin; + double physical = ConvertSample(offset, scaling, digital); + CHECK(physical == doctest::Approx(physMin).epsilon(0.001)); + } + } + + TEST_CASE("Physical round-trip via ProcessorSample") { + // Use Physical sample type which internally converts phys->dig->record + const double physMin = -500.0; + const double physMax = 500.0; + const int32_t digMin = -32768; + const int32_t digMax = 32767; + const double scaling = (physMax - physMin) / (digMax - digMin); + const double offset = physMin - scaling * digMin; + const uint32_t sampleSize = 2; + + ProcessorSample encoder(offset, scaling, + sampleSize); + ProcessorSampleRecord decoder(offset, scaling); + + SUBCASE("Positive physical value") { + double original = 250.0; + auto record = encoder(original); + double decoded = decoder(record); + CHECK(decoded == doctest::Approx(original).epsilon(0.1)); + } + + SUBCASE("Negative physical value") { + double original = -250.0; + auto record = encoder(original); + double decoded = decoder(record); + CHECK(decoded == doctest::Approx(original).epsilon(0.1)); + } + + SUBCASE("Zero physical value") { + double original = 0.0; + auto record = encoder(original); + double decoded = decoder(record); + CHECK(decoded == doctest::Approx(original).epsilon(0.1)); + } + } + +} // TEST_SUITE "Sample conversion round-trip" + +// =========================================================================== +// TEST SUITE: detail::to_string_decimal +// =========================================================================== +TEST_SUITE("to_string_decimal") { + + TEST_CASE("Integer values have no trailing zeros or decimal point") { + // 5.0 should become "5", not "5.000000" + auto result = detail::to_string_decimal(5.0); + CHECK(result == "5"); + + result = detail::to_string_decimal(100.0); + CHECK(result == "100"); + + result = detail::to_string_decimal(0.0); + CHECK(result == "0"); + } + + TEST_CASE("Decimal values have trailing zeros trimmed") { + // 2.5 should become "2.5", not "2.500000" + auto result = detail::to_string_decimal(2.5); + CHECK(result == "2.5"); + + result = detail::to_string_decimal(1.25); + CHECK(result == "1.25"); + + result = detail::to_string_decimal(3.1); + CHECK(result == "3.1"); + } + + TEST_CASE("Negative integer values") { + auto result = detail::to_string_decimal(-10.0); + CHECK(result == "-10"); + + result = detail::to_string_decimal(-1.0); + CHECK(result == "-1"); + } + + TEST_CASE("Negative decimal values") { + auto result = detail::to_string_decimal(-2.5); + CHECK(result == "-2.5"); + + result = detail::to_string_decimal(-0.125); + CHECK(result == "-0.125"); + } + + TEST_CASE("Small fractional values") { + auto result = detail::to_string_decimal(0.5); + CHECK(result == "0.5"); + + result = detail::to_string_decimal(0.1); + // std::to_string(0.1) gives "0.100000", trimmed to "0.1" + CHECK(result == "0.1"); + } + + TEST_CASE("Large integer values") { + auto result = detail::to_string_decimal(32767.0); + CHECK(result == "32767"); + + result = detail::to_string_decimal(-32768.0); + CHECK(result == "-32768"); + } + +} // TEST_SUITE "to_string_decimal" From ce76415da7a83cd354046c52f30cef0b65e30c07 Mon Sep 17 00:00:00 2001 From: iuri dotta Date: Sun, 1 Mar 2026 15:44:05 -0300 Subject: [PATCH 10/11] Add EdfFile class for high-level EDF/BDF file handling - Introduced EdfFile class to facilitate reading and writing EDF/BDF files. - Implemented methods for accessing header information, reading physical and digital samples, and extracting annotations. - Added EdfWriter class for writing EDF/BDF files, including automatic data record count patching on close. - Created cmake configuration file for edfio. - Updated ProcessorUtils to improve decimal string conversion. - Added unit tests for EdfFile and EdfWriter functionalities, covering various scenarios including file reading, signal validation, and round-trip integrity checks. --- .github/workflows/ci.yml | 88 ++++ CMakeLists.txt | 32 ++ README.md | 477 ++++++++++++++++++++- cmake/edfioConfig.cmake.in | 5 + include/edfio/EdfFile.hpp | 316 ++++++++++++++ include/edfio/EdfIO.hpp | 3 + include/edfio/processor/ProcessorUtils.hpp | 24 +- tests/CMakeLists.txt | 11 +- tests/test_edffile.cpp | 410 ++++++++++++++++++ 9 files changed, 1344 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 cmake/edfioConfig.cmake.in create mode 100644 include/edfio/EdfFile.hpp create mode 100644 tests/test_edffile.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f5e97ec --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,88 @@ +name: CI + +on: + push: + branches: [master, main] + pull_request: + +jobs: + build-and-test: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + name: Linux (GCC 13) + compiler: g++-13 + cc: gcc-13 + cxx: g++-13 + packages: ninja-build g++-13 + + - os: ubuntu-latest + name: Linux (Clang 17) + compiler: clang++-17 + cc: clang-17 + cxx: clang++-17 + packages: ninja-build clang-17 + + - os: windows-latest + name: Windows (MSVC) + compiler: msvc + cc: cl + cxx: cl + packages: "" + + - os: macos-latest + name: macOS (AppleClang) + compiler: appleclang + cc: cc + cxx: c++ + packages: ninja + + runs-on: ${{ matrix.os }} + name: ${{ matrix.name }} + # macOS AppleClang lacks std::views::zip and std::from_chars for doubles + continue-on-error: ${{ matrix.os == 'macos-latest' }} + + steps: + - uses: actions/checkout@v4 + + # -- Install CMake 4.0+ (required by the project) -- + - name: Install CMake 4.0+ via pip + run: pip install "cmake>=4.0" + + # -- Install platform dependencies -- + - name: Install dependencies (Ubuntu) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y ${{ matrix.packages }} + + - name: Install dependencies (macOS) + if: runner.os == 'macOS' + run: brew install ${{ matrix.packages }} + + - name: Install dependencies (Windows) + if: runner.os == 'Windows' + run: choco install ninja + + # -- Set up MSVC environment on Windows -- + - name: Set up MSVC Developer Environment + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + + # -- Configure -- + - name: Configure CMake + run: > + cmake -B build -G Ninja + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_C_COMPILER=${{ matrix.cc }} + -DCMAKE_CXX_COMPILER=${{ matrix.cxx }} + + # -- Build -- + - name: Build + run: cmake --build build + + # -- Test -- + - name: Test + run: ctest --test-dir build --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index 3fcbebd..0ee3af9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,7 @@ target_include_directories(edfio INTERFACE $ ) target_compile_features(edfio INTERFACE cxx_std_23) +add_library(edfio::edfio ALIAS edfio) # ---- Tests ---- option(EDFIO_BUILD_TESTS "Build tests" ON) @@ -15,3 +16,34 @@ if(EDFIO_BUILD_TESTS) enable_testing() add_subdirectory(tests) endif() + +# ---- Install targets and packaging ---- +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) + +install(TARGETS edfio EXPORT edfioTargets) +install(DIRECTORY include/edfio DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + +install(EXPORT edfioTargets + FILE edfioTargets.cmake + NAMESPACE edfio:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/edfio +) + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/edfioConfigVersion.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion +) + +configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/edfioConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/edfioConfig.cmake" + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/edfio +) + +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/edfioConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/edfioConfigVersion.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/edfio +) diff --git a/README.md b/README.md index 37960c8..a1e0f8e 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,470 @@ # edfio -A C++ 11 header-only library to read/write EDF(+)/BDF(+) files. +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![C++23](https://img.shields.io/badge/C%2B%2B-23-blue.svg)](https://en.cppreference.com/w/cpp/23) +[![Header-only](https://img.shields.io/badge/header--only-yes-green.svg)]() -### Sample file -The sample file 'Calib5.edf' provided in the root directory of this source tree -was taken from the - -Polyman - -Demodata only for testing. +A modern C++23 header-only library for reading and writing +[EDF](https://www.edfplus.info/specs/edf.html) (European Data Format) and +[BDF](https://www.biosemi.com/faq/file_format.htm) (BioSemi Data Format) +biomedical signal files. -### Terms and Conditions -Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) +## Features -This source code is licensed under the MIT license found in the -LICENSE file in the root directory of this source tree. +- **Header-only** -- zero external dependencies, single `#include` to get started +- **Modern C++23** -- uses `std::span`, `std::ranges`, `constexpr`, `std::from_chars` +- **Iterator-based** random access to data records and individual signal samples +- **Full format coverage** -- EDF, EDF+C, EDF+D, BDF, BDF+C, BDF+D +- **EDF+/BDF+ annotations** -- read and write Time-stamped Annotation Lists (TALs) +- **Two API layers** -- high-level `EdfFile` for common tasks, low-level stores and sinks for full control -Official repository: https://github.com/idotta/edfio +## Requirements + +| Dependency | Minimum version | +|---|---| +| C++ standard | C++23 | +| Clang | 17+ | +| GCC | 13+ | +| MSVC | 19.36+ (Visual Studio 2022 17.6) | +| CMake | 4.0+ | + +## Quick Start + +### High-level API + +```cpp +#include +#include + +int main() { + auto file = edfio::EdfFile::open("recording.edf"); + + std::cout << "Format: " << (file.isEdf() ? "EDF" : "BDF") << "\n"; + std::cout << "Signals: " << file.signalCount() << "\n"; + std::cout << "Data records: " << file.dataRecordCount() << "\n"; + std::cout << "Duration: " << file.duration() << "s\n"; + + // Access header information + auto const& general = file.general(); + auto const& signals = file.signals(); + + // Read physical (scaled) samples for the first signal + auto samples = file.readSignal(0); + + // Read raw digital samples + auto digital = file.readSignalDigital(0); + + // Read annotations (EDF+/BDF+ only) + if (file.isPlus()) { + auto annotations = file.readAnnotations(); + for (auto const& annot : annotations) { + std::cout << annot.m_start << "s: " + << annot.m_annotation << "\n"; + } + } +} +``` + +### Writing with the high-level API + +```cpp +#include + +// Prepare a header from an existing file, or build one manually +auto source = edfio::EdfFile::open("source.edf"); +auto writer = edfio::EdfWriter::create("output.edf", source.general(), source.signals()); + +// Write data records +// ... + +writer.close(); +``` + +## Installation + +### CMake FetchContent (recommended) + +```cmake +include(FetchContent) +FetchContent_Declare( + edfio + GIT_REPOSITORY https://github.com/idotta/edfio.git + GIT_TAG master +) +FetchContent_MakeAvailable(edfio) + +target_link_libraries(your_target PRIVATE edfio) +``` + +### CMake add\_subdirectory + +Clone or add edfio as a git submodule, then: + +```cmake +add_subdirectory(external/edfio) +target_link_libraries(your_target PRIVATE edfio) +``` + +### Copy headers + +Copy the `include/edfio/` directory into your project's include path and ensure +your build system enables C++23 (`-std=c++23` or `cxx_std_23`). + +## Low-level API + +The low-level API provides direct iterator access to data records, signal +records, and individual samples through **stores** (reading) and **sinks** +(writing). + +### Reading a file + +```cpp +#include +#include +#include + +int main() { + std::ifstream stream("recording.edf", std::ios::binary); + + // Parse the full header (general + all signal headers) + auto header = edfio::ReadHeaderExam(stream); + auto const& general = header.m_general; + auto const& signals = header.m_signals; + + std::cout << "Format: " + << (edfio::IsEdf(general.m_version) ? "EDF" : "BDF") + << (edfio::IsPlus(general.m_version) ? "+" : "") << "\n"; + std::cout << "Signals: " << general.m_totalSignals << "\n"; + std::cout << "Data records: " << general.m_datarecordsFile << "\n"; + std::cout << "Record duration: " << general.m_datarecordDuration << "s\n"; + + // Print signal labels + for (auto const& sig : signals) { + std::cout << " " << sig.m_label + << " [" << sig.m_physDimension << "]" + << " " << sig.m_samplesInDataRecord << " samples/record\n"; + } + + // Iterate over raw data records + auto store = edfio::detail::CreateDataRecordStore(stream, general); + for (auto it = store.begin(); it != store.end(); ++it) { + auto const& record = *it; + // record() returns std::vector with the raw bytes + } +} +``` + +### Reading signal samples + +```cpp +#include +#include +#include + +int main() { + std::ifstream stream("recording.edf", std::ios::binary); + auto header = edfio::ReadHeaderExam(stream); + auto const& general = header.m_general; + auto const& signal = header.m_signals[0]; // first signal + + // Create a sample store -- iterates individual samples across all + // data records for a single signal + auto sampleStore = edfio::detail::CreateSignalSampleStore( + stream, general, signal); + + // Processor converts raw bytes to physical (double) or digital (int32_t) values + edfio::ProcessorSampleRecord toPhysical( + signal.m_detail.m_offset, + signal.m_detail.m_scaling); + + std::vector samples; + samples.reserve(sampleStore.size()); + for (auto it = sampleStore.begin(); it != sampleStore.end(); ++it) { + samples.push_back(toPhysical(*it)); + } + + // For digital (raw integer) values, use SampleType::Digital instead: + edfio::ProcessorSampleRecord toDigital( + signal.m_detail.m_offset, + signal.m_detail.m_scaling); +} +``` + +### Reading signal records per data record + +```cpp +// Read signal-level records (one per data record) for a specific signal +auto sigStore = edfio::detail::CreateSignalRecordStore( + stream, header.m_general, header.m_signals[0]); + +for (auto it = sigStore.begin(); it != sigStore.end(); ++it) { + auto const& record = *it; + // record() contains one data record's worth of samples for this signal +} +``` + +## Writing Files + +```cpp +#include +#include + +int main() { + // Read an existing file + std::ifstream inStream("source.edf", std::ios::binary); + auto header = edfio::ReadHeaderExam(inStream); + + // Write header to a new file + std::ofstream outStream("copy.edf", std::ios::binary); + edfio::WriteHeaderExam(outStream, header); + + // Copy data records using store (read) and sink (write) iterators + auto store = edfio::detail::CreateDataRecordStore(inStream, header.m_general); + auto sink = edfio::detail::CreateDataRecordSink(outStream, header.m_general); + auto sinkIt = sink.begin(); + for (auto it = store.begin(); it != store.end(); ++it) { + *sinkIt = *it; + ++sinkIt; + } +} +``` + +### Building a header from scratch + +```cpp +#include +#include +#include +#include + +int main() { + // Define signals + std::vector signals; + signals.push_back(edfio::detail::CreateHeaderSignal( + "EEG Fp1", // label + 256, // samples per data record + -3200.0, // physical min + 3200.0, // physical max + -32768, // digital min + 32767 // digital max + )); + + // Calculate header size: 256 bytes (general) + 256 bytes per signal + int32_t headerSize = 256 + 256 * static_cast(signals.size()); + + // Create the general header + auto general = edfio::detail::CreateHeaderGeneral( + edfio::DataFormat::Edf, + "Patient X", // patient + "Recording 1", // recording + 1, 3, 2026, // start date (day, month, year) + 14, 30, 0, // start time (hour, minute, second) + headerSize, + "", // reserved + 100, // number of data records + 1.0, // data record duration in seconds + signals + ); + + // Assemble and write + edfio::HeaderExam exam{general, signals}; + std::ofstream outStream("new_file.edf", std::ios::binary); + edfio::WriteHeaderExam(outStream, exam); + + // Write sample data using ProcessorSample and DataRecordSink... +} +``` + +## EDF+ / BDF+ Annotations + +EDF+ and BDF+ files store annotations in dedicated signal channels using +Time-stamped Annotation Lists (TALs). The `Annotation` struct holds the onset +time, duration, and annotation text: + +```cpp +struct Annotation : TimeStamp { + double m_duration; + std::string m_annotation; + // inherited: double m_start, int64_t m_datarecord +}; +``` + +### Reading annotations + +```cpp +#include +#include +#include + +int main() { + std::ifstream stream("recording_plus.edf", std::ios::binary); + auto header = edfio::ReadHeaderExam(stream); + + if (!edfio::IsPlus(header.m_general.m_version)) { + std::cout << "Not an EDF+/BDF+ file\n"; + return 0; + } + + // Find the annotation signal + edfio::HeaderSignal const* annotSignal = nullptr; + for (auto const& sig : header.m_signals) { + if (sig.m_detail.m_isAnnotation) { + annotSignal = &sig; + break; + } + } + + // Read annotation records and parse TALs + auto sigStore = edfio::detail::CreateSignalRecordStore( + stream, header.m_general, *annotSignal); + + int64_t drIdx = 0; + for (auto it = sigStore.begin(); it != sigStore.end(); ++it, ++drIdx) { + auto const& rec = *it; + std::vector talData(rec().begin(), rec().end()); + + auto annotations = edfio::ProcessTalRecord(talData, drIdx); + for (auto const& annot : annotations) { + std::cout << " [" << annot.m_start << "s"; + if (annot.m_duration > 0) + std::cout << ", dur=" << annot.m_duration << "s"; + std::cout << "] " << annot.m_annotation << "\n"; + } + } +} +``` + +### Writing annotations + +```cpp +#include + +// Create an annotation +edfio::Annotation annot; +annot.m_start = 1.5; +annot.m_duration = 30.0; +annot.m_annotation = "Sleep Stage W"; +annot.m_datarecord = 0; + +// Serialize to a TAL record +auto record = edfio::ProcessAnnotation(annot); + +// Create a timestamp record for the data record onset +edfio::TimeStamp ts; +ts.m_start = 0.0; +ts.m_datarecord = 0; +auto tsRecord = edfio::ProcessTimeStamp(ts); + +// Combine timestamp + annotation into the annotation signal's data +auto combined = tsRecord + record; +``` + +### Reading timestamps + +EDF+ files include a timestamp at the start of each data record in the +annotation channel. Use `TimeStampStore` to access them: + +```cpp +auto tsStore = edfio::detail::CreateTimeStampStore( + stream, header.m_general, *annotSignal); + +for (auto it = tsStore.begin(); it != tsStore.end(); ++it) { + auto const& rec = *it; + // Parse with ProcessTimeStampRecord for structured TimeStamp data + auto ts = edfio::ProcessTimeStampRecord(*it, /* datarecord index */); +} +``` + +## Data Formats + +edfio detects and supports all standard EDF/BDF variants through the +`DataFormat` enum: + +| Enum value | Description | Sample size | +|---|---|---| +| `DataFormat::Edf` | Standard EDF | 2 bytes (16-bit) | +| `DataFormat::EdfPlusC` | EDF+ Continuous | 2 bytes | +| `DataFormat::EdfPlusD` | EDF+ Discontinuous | 2 bytes | +| `DataFormat::Bdf` | Standard BDF | 3 bytes (24-bit) | +| `DataFormat::BdfPlusC` | BDF+ Continuous | 3 bytes | +| `DataFormat::BdfPlusD` | BDF+ Discontinuous | 3 bytes | + +Helper functions: `IsEdf()`, `IsBdf()`, `IsPlus()`, `GetSampleBytes()`. + +## Building and Testing + +```bash +cmake -B build -G Ninja +cmake --build build +ctest --test-dir build +``` + +The test suite uses [doctest](https://github.com/doctest/doctest) (bundled in +`third_party/`) and runs against sample EDF/EDF+/BDF+ files in the `files/` +directory. + +## Project Structure + +``` +edfio/ + CMakeLists.txt + LICENSE + include/edfio/ + EdfIO.hpp # Main include (low-level API) + EdfFile.hpp # High-level API (coming soon) + core/ + Annotation.hpp # Annotation and TimeStamp structs + DataFormat.hpp # DataFormat enum and helpers + Record.hpp # Record container with stream I/O + SampleType.hpp # SampleType enum, Sample traits, ConvertSample + Field.hpp # Fixed-size header field type + StreamIO.hpp # Reader/Writer stream type aliases + header/ + HeaderExam.hpp # HeaderExam (general + signal headers) + HeaderGeneral.hpp # HeaderGeneral, Date, Time structs + HeaderSignal.hpp # HeaderSignal struct + HeaderUtils.hpp # CreateHeaderGeneral, CreateHeaderSignal + reader/ + ReaderHeaderExam.hpp # ReadHeaderExam() entry point + writer/ + WriterHeaderExam.hpp # WriteHeaderExam() entry point + store/ # Read iterators + DataRecordStore.hpp # Iterate full data records + SignalRecordStore.hpp # Iterate one signal's records + SignalSampleStore.hpp # Iterate individual samples + TimeStampStore.hpp # Iterate EDF+ timestamps + TalStore.hpp # Iterate TALs within a record + StoreUtils.hpp # Factory functions for stores + sink/ # Write iterators + DataRecordSink.hpp # Write full data records + SignalRecordSink.hpp # Write one signal's records + SinkUtils.hpp # Factory functions for sinks + processor/ # Data transformation + ProcessorSampleRecord.hpp # Raw bytes -> physical/digital values + ProcessorSample.hpp # Physical/digital values -> raw bytes + ProcessorTalRecord.hpp # TAL bytes -> vector + ProcessorAnnotation.hpp # Annotation -> TAL record + ProcessorTimeStamp.hpp # TimeStamp -> record + ProcessorTimeStampRecord.hpp # Record -> TimeStamp + tests/ # doctest-based test suite + files/ # Sample EDF/BDF files for testing + third_party/ # Bundled doctest headers +``` + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file +for details. + +Copyright (c) 2017-present Iuri Dotta + +## Contributing + +Contributions are welcome. To get started: + +1. Fork the repository at [github.com/idotta/edfio](https://github.com/idotta/edfio) +2. Create a feature branch from `master` +3. Make sure all tests pass (`ctest --test-dir build`) +4. Keep changes focused -- one feature or fix per pull request +5. Follow existing code style and naming conventions +6. Open a pull request with a clear description of the change diff --git a/cmake/edfioConfig.cmake.in b/cmake/edfioConfig.cmake.in new file mode 100644 index 0000000..49de116 --- /dev/null +++ b/cmake/edfioConfig.cmake.in @@ -0,0 +1,5 @@ +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/edfioTargets.cmake") + +check_required_components(edfio) diff --git a/include/edfio/EdfFile.hpp b/include/edfio/EdfFile.hpp new file mode 100644 index 0000000..a73f99e --- /dev/null +++ b/include/edfio/EdfFile.hpp @@ -0,0 +1,316 @@ +// +// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. +// +// Official repository: https://github.com/idotta/edfio +// + +#pragma once + +#include "EdfIO.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace edfio { + +/// High-level facade for reading EDF/BDF files. +/// +/// Owns the underlying file stream and parsed header. Provides convenient +/// methods to extract physical or digital samples and annotations without +/// manually wiring stores, sinks, processors, and streams. +/// +/// Moveable but not copyable (owns an std::ifstream). +class EdfFile { +public: + EdfFile(const EdfFile &) = delete; + EdfFile &operator=(const EdfFile &) = delete; + EdfFile(EdfFile &&) noexcept = default; + EdfFile &operator=(EdfFile &&) noexcept = default; + + /// Open an EDF/BDF file for reading. + /// + /// @param path Filesystem path to the file. + /// @throws std::runtime_error if the file cannot be opened. + /// @throws std::invalid_argument if the file contains format errors. + [[nodiscard]] static EdfFile open(const std::filesystem::path &path) { + auto stream = std::make_unique(path, std::ios::binary); + if (!stream->is_open()) { + throw std::runtime_error(std::string("Cannot open file: ") + + path.string()); + } + auto header = ReadHeaderExam(*stream); + return EdfFile(std::move(stream), std::move(header)); + } + + // ---- Header access ------------------------------------------------------- + + /// General header fields (patient, recording, timing, etc.). + [[nodiscard]] const HeaderGeneral &general() const noexcept { + return m_header.m_general; + } + + /// Per-signal header fields. + [[nodiscard]] const std::vector &signals() const noexcept { + return m_header.m_signals; + } + + /// Full header exam (general + signals). + [[nodiscard]] const HeaderExam &header() const noexcept { return m_header; } + + // ---- Format queries ------------------------------------------------------- + + /// Data format enum value. + [[nodiscard]] DataFormat format() const noexcept { + return m_header.m_general.m_version; + } + + /// True for any EDF variant (plain EDF, EDF+C, EDF+D). + [[nodiscard]] bool isEdf() const noexcept { return IsEdf(format()); } + + /// True for any BDF variant (plain BDF, BDF+C, BDF+D). + [[nodiscard]] bool isBdf() const noexcept { return IsBdf(format()); } + + /// True for any "plus" variant (EDF+C, EDF+D, BDF+C, BDF+D). + [[nodiscard]] bool isPlus() const noexcept { return IsPlus(format()); } + + // ---- Counts and duration -------------------------------------------------- + + /// Total number of signals (including annotation channels). + [[nodiscard]] int32_t signalCount() const noexcept { + return m_header.m_general.m_totalSignals; + } + + /// Number of data records in the file. + [[nodiscard]] int64_t dataRecordCount() const noexcept { + return m_header.m_general.m_datarecordsFile; + } + + /// Total file duration in seconds + /// (dataRecordCount * datarecordDuration). + [[nodiscard]] double duration() const noexcept { + return m_header.m_general.m_detail.m_fileDuration; + } + + // ---- Sample reading ------------------------------------------------------- + + /// Read all physical-value samples for the given signal index. + /// + /// The returned vector contains one double per sample, converted from the + /// raw digital value using the signal's offset and scaling parameters. + /// + /// @param signalIndex Zero-based index into signals(). + /// @throws std::out_of_range if signalIndex is invalid. + [[nodiscard]] std::vector readSignal(size_t signalIndex) const { + validateSignalIndex(signalIndex); + + auto const &sig = m_header.m_signals[signalIndex]; + auto store = detail::CreateSignalSampleStore(*m_stream, m_header.m_general, + sig); + + ProcessorSampleRecord proc(sig.m_detail.m_offset, + sig.m_detail.m_scaling); + + std::vector result; + result.reserve(store.size()); + for (auto it = store.begin(); it != store.end(); ++it) { + result.push_back(proc(*it)); + } + return result; + } + + /// Read all digital-value samples for the given signal index. + /// + /// The returned vector contains one int32_t per sample, which is the raw + /// (sign-extended) integer stored in the file. + /// + /// @param signalIndex Zero-based index into signals(). + /// @throws std::out_of_range if signalIndex is invalid. + [[nodiscard]] std::vector + readSignalDigital(size_t signalIndex) const { + validateSignalIndex(signalIndex); + + auto const &sig = m_header.m_signals[signalIndex]; + auto store = detail::CreateSignalSampleStore(*m_stream, m_header.m_general, + sig); + + // offset=0, scaling=1 yields the raw digital value. + ProcessorSampleRecord proc(0.0, 1.0); + + std::vector result; + result.reserve(store.size()); + for (auto it = store.begin(); it != store.end(); ++it) { + result.push_back(proc(*it)); + } + return result; + } + + // ---- Annotations ---------------------------------------------------------- + + /// Read all annotations from every annotation channel. + /// + /// Returns an empty vector when the file is not an EDF+/BDF+ variant or + /// contains no annotation channels. + [[nodiscard]] std::vector readAnnotations() const { + std::vector result; + + for (size_t i = 0; i < m_header.m_signals.size(); ++i) { + auto const &sig = m_header.m_signals[i]; + if (!sig.m_detail.m_isAnnotation) + continue; + + auto sigStore = detail::CreateSignalRecordStore( + *m_stream, m_header.m_general, sig); + + int64_t drIdx = 0; + for (auto it = sigStore.begin(); it != sigStore.end(); ++it, ++drIdx) { + auto const &rec = *it; + std::vector talData(rec().begin(), rec().end()); + if (talData.empty() || + (talData.front() != '+' && talData.front() != '-')) + continue; + + auto annots = ProcessTalRecord(talData, drIdx); + result.insert(result.end(), std::make_move_iterator(annots.begin()), + std::make_move_iterator(annots.end())); + } + } + return result; + } + + // ---- Low-level store access (for advanced use) ---------------------------- + + /// Create a DataRecordStore over the file. + [[nodiscard]] DataRecordStore dataRecordStore() const { + return detail::CreateDataRecordStore(*m_stream, m_header.m_general); + } + + /// Create a SignalRecordStore for the given signal index. + /// + /// @param signalIndex Zero-based index into signals(). + /// @throws std::out_of_range if signalIndex is invalid. + [[nodiscard]] SignalRecordStore signalRecordStore(size_t signalIndex) const { + validateSignalIndex(signalIndex); + return detail::CreateSignalRecordStore(*m_stream, m_header.m_general, + m_header.m_signals[signalIndex]); + } + + /// Create a SignalSampleStore for the given signal index. + /// + /// @param signalIndex Zero-based index into signals(). + /// @throws std::out_of_range if signalIndex is invalid. + [[nodiscard]] SignalSampleStore signalSampleStore(size_t signalIndex) const { + validateSignalIndex(signalIndex); + return detail::CreateSignalSampleStore(*m_stream, m_header.m_general, + m_header.m_signals[signalIndex]); + } + +private: + explicit EdfFile(std::unique_ptr stream, HeaderExam header) + : m_stream(std::move(stream)), m_header(std::move(header)) {} + + void validateSignalIndex(size_t signalIndex) const { + if (signalIndex >= m_header.m_signals.size()) { + throw std::out_of_range( + "Signal index " + std::to_string(signalIndex) + + " out of range [0, " + + std::to_string(m_header.m_signals.size()) + ")"); + } + } + + std::unique_ptr m_stream; + HeaderExam m_header; +}; + +/// High-level facade for writing EDF/BDF files. +/// +/// Owns the underlying output stream. Write data records sequentially after +/// the header has been written. +/// +/// Moveable but not copyable (owns an std::ofstream). +class EdfWriter { +public: + EdfWriter(const EdfWriter &) = delete; + EdfWriter &operator=(const EdfWriter &) = delete; + EdfWriter(EdfWriter &&) noexcept = default; + EdfWriter &operator=(EdfWriter &&) noexcept = default; + + /// Create a new EDF/BDF file and write its header. + /// + /// @param path Filesystem path for the new file. + /// @param header Complete header exam to write. + /// @throws std::runtime_error if the file cannot be created. + [[nodiscard]] static EdfWriter create(const std::filesystem::path &path, + const HeaderExam &header) { + auto stream = std::make_unique(path, std::ios::binary); + if (!stream->is_open()) { + throw std::runtime_error(std::string("Cannot create file: ") + + path.string()); + } + WriteHeaderExam(*stream, header); + return EdfWriter(std::move(stream), header.m_general); + } + + /// Write a single data record to the file. + /// + /// The data record count in the header is automatically updated when + /// close() is called (or the writer is destroyed). + void writeDataRecord(const Record &record) { + if (m_stream && m_stream->is_open()) { + *m_stream << record; + ++m_recordsWritten; + } + } + + /// Number of data records written so far. + [[nodiscard]] int64_t recordsWritten() const noexcept { + return m_recordsWritten; + } + + /// Flush, patch the data-record count in the header, and close. + void close() { + if (m_stream && m_stream->is_open()) { + // EDF/BDF header layout: the "number of data records" field is 8 bytes + // starting at offset 236 (after version[8] + patient[80] + + // recording[80] + startDate[8] + startTime[8] + headerSize[8] + + // reserved[44] = 236). + static constexpr std::streamoff kDataRecordCountOffset = 236; + static constexpr size_t kFieldSize = 8; + + m_stream->seekp(kDataRecordCountOffset, std::ios::beg); + auto countStr = std::to_string(m_recordsWritten); + countStr.resize(kFieldSize, ' '); + m_stream->write(countStr.data(), kFieldSize); + + m_stream->flush(); + m_stream->close(); + } + } + + /// Destructor flushes and closes the stream if still open. + ~EdfWriter() { + try { + close(); + } catch (...) { + // Suppress exceptions in destructor. + } + } + +private: + explicit EdfWriter(std::unique_ptr stream, + HeaderGeneral general) + : m_stream(std::move(stream)), m_general(std::move(general)) {} + + std::unique_ptr m_stream; + HeaderGeneral m_general; + int64_t m_recordsWritten = 0; +}; + +} // namespace edfio diff --git a/include/edfio/EdfIO.hpp b/include/edfio/EdfIO.hpp index f157d58..8ef07cd 100644 --- a/include/edfio/EdfIO.hpp +++ b/include/edfio/EdfIO.hpp @@ -40,5 +40,8 @@ #include "processor/ProcessorTimeStamp.hpp" #include "processor/ProcessorTimeStampRecord.hpp" +// High-level facade +#include "EdfFile.hpp" + // STL #include diff --git a/include/edfio/processor/ProcessorUtils.hpp b/include/edfio/processor/ProcessorUtils.hpp index 4016b9d..66a28d0 100644 --- a/include/edfio/processor/ProcessorUtils.hpp +++ b/include/edfio/processor/ProcessorUtils.hpp @@ -162,13 +162,25 @@ inline double ParseDouble(std::string_view sv, const char *error_msg) { } template inline std::string to_string_decimal(const T &t) { - std::string str{std::to_string(t)}; - std::ranges::replace(str, ',', '.'); - int32_t offset{1}; - if (str.find_last_not_of('0') == str.find('.')) { - offset = 0; + // Use std::to_chars for locale-independent conversion (always uses '.') + std::array buf; + auto [ptr, ec] = std::to_chars(buf.data(), buf.data() + buf.size(), t); + if (ec != std::errc{}) + return "0"; + std::string str(buf.data(), ptr); + + // Trim trailing zeros after the decimal point + auto dot = str.find('.'); + if (dot != std::string::npos) { + auto last_nonzero = str.find_last_not_of('0'); + if (last_nonzero == dot) { + // All fractional digits are zero; remove the dot entirely + str.erase(dot); + } else { + // Remove trailing zeros only + str.erase(last_nonzero + 1); + } } - str.erase(str.find_last_not_of('0') + offset, std::string::npos); return str; } } // namespace detail diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8f6d3bb..7fee75d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,9 +6,13 @@ target_include_directories(doctest INTERFACE ${CMAKE_SOURCE_DIR}/third_party) function(edfio_add_test name) add_executable(${name} ${name}.cpp) target_link_libraries(${name} PRIVATE edfio doctest) - target_compile_options(${name} PRIVATE - -Wall -Wextra -Wpedantic -Wno-c++98-compat - ) + if(MSVC) + target_compile_options(${name} PRIVATE /W4 /permissive-) + else() + target_compile_options(${name} PRIVATE + -Wall -Wextra -Wpedantic -Wno-c++98-compat + ) + endif() add_test(NAME ${name} COMMAND ${name}) endfunction() @@ -44,3 +48,4 @@ edfio_add_test(test_processor_utils) edfio_add_test(test_bdf_reader) edfio_add_test(test_processors) edfio_add_test(test_edfplus) +edfio_add_test(test_edffile) diff --git a/tests/test_edffile.cpp b/tests/test_edffile.cpp new file mode 100644 index 0000000..76e2b13 --- /dev/null +++ b/tests/test_edffile.cpp @@ -0,0 +1,410 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include +#include +#include +#include +#include + +using namespace edfio; + +// =========================================================================== +// 1. Open test_generator_2.edf -- format, signal count, duration +// =========================================================================== + +TEST_CASE("EdfFile::open reads test_generator_2.edf successfully") { + auto file = EdfFile::open("test_generator_2.edf"); + + SUBCASE("format is EDF+") { + CHECK(file.format() == DataFormat::EdfPlusC); + CHECK(file.isEdf()); + CHECK(file.isPlus()); + CHECK_FALSE(file.isBdf()); + } + + SUBCASE("signal count matches header") { + CHECK(file.signalCount() == 12); + CHECK(file.signals().size() == 12); + } + + SUBCASE("data record count and duration") { + CHECK(file.dataRecordCount() == 600); + // duration = dataRecordCount * datarecordDuration = 600 * 1.0 = 600 + CHECK(file.duration() == doctest::Approx(600.0)); + } + + SUBCASE("general header is accessible") { + CHECK(file.general().m_headerSize > 0); + CHECK(file.general().m_datarecordDuration == doctest::Approx(1.0)); + CHECK(file.general().m_startDate.day == 10); + CHECK(file.general().m_startDate.month == 12); + CHECK(file.general().m_startDate.year == 2009); + } +} + +// =========================================================================== +// 2. Read physical samples -- verify within physical range +// =========================================================================== + +TEST_CASE("readSignal returns physical values within declared range") { + auto file = EdfFile::open("test_generator_2.edf"); + + // Find first non-annotation signal + size_t sigIdx = 0; + for (size_t i = 0; i < file.signals().size(); ++i) { + if (!file.signals()[i].m_detail.m_isAnnotation) { + sigIdx = i; + break; + } + } + auto const &sig = file.signals()[sigIdx]; + REQUIRE_FALSE(sig.m_detail.m_isAnnotation); + + auto samples = file.readSignal(sigIdx); + REQUIRE_FALSE(samples.empty()); + + // Expected sample count = dataRecordCount * samplesInDataRecord + auto expectedCount = static_cast(file.dataRecordCount()) * + static_cast(sig.m_samplesInDataRecord); + CHECK(samples.size() == expectedCount); + + // Verify first 200 samples are within declared physical range + double physMin = sig.m_physicalMin; + double physMax = sig.m_physicalMax; + double tol = (physMax - physMin) * 0.001; + auto limit = std::min(samples.size(), size_t{200}); + for (size_t i = 0; i < limit; ++i) { + CHECK(samples[i] >= physMin - tol); + CHECK(samples[i] <= physMax + tol); + } +} + +// =========================================================================== +// 3. Read digital samples -- verify within digital range +// =========================================================================== + +TEST_CASE("readSignalDigital returns values within digital range") { + auto file = EdfFile::open("test_generator_2.edf"); + + // Find first non-annotation signal + size_t sigIdx = 0; + for (size_t i = 0; i < file.signals().size(); ++i) { + if (!file.signals()[i].m_detail.m_isAnnotation) { + sigIdx = i; + break; + } + } + auto const &sig = file.signals()[sigIdx]; + REQUIRE_FALSE(sig.m_detail.m_isAnnotation); + + auto samples = file.readSignalDigital(sigIdx); + REQUIRE_FALSE(samples.empty()); + + // EDF uses 16-bit signed integers + int32_t digiMin = sig.m_digitalMin; + int32_t digiMax = sig.m_digitalMax; + + auto limit = std::min(samples.size(), size_t{200}); + for (size_t i = 0; i < limit; ++i) { + CHECK(samples[i] >= digiMin); + CHECK(samples[i] <= digiMax); + } +} + +// =========================================================================== +// 4. Read annotations from EDF+ file +// =========================================================================== + +TEST_CASE("readAnnotations extracts annotations from EDF+ file") { + auto file = EdfFile::open("test_generator_2.edf"); + REQUIRE(file.isPlus()); + + auto annots = file.readAnnotations(); + // EDF+ file should have at least some annotations + CHECK(annots.size() > 0); + + for (auto const &a : annots) { + CHECK_FALSE(a.m_annotation.empty()); + CHECK(a.m_duration >= 0.0); + } +} + +// =========================================================================== +// 5. Open test_generator_2.bdf -- verify BDF detection +// =========================================================================== + +TEST_CASE("EdfFile::open reads BDF file correctly") { + auto file = EdfFile::open("test_generator_2.bdf"); + + CHECK(file.isBdf()); + CHECK(file.isPlus()); + CHECK_FALSE(file.isEdf()); + CHECK(file.format() == DataFormat::BdfPlusC); + CHECK(file.signalCount() > 0); + CHECK(file.dataRecordCount() > 0); + CHECK(file.duration() > 0.0); + + SUBCASE("BDF physical samples within range") { + // Find first non-annotation signal + size_t sigIdx = 0; + for (size_t i = 0; i < file.signals().size(); ++i) { + if (!file.signals()[i].m_detail.m_isAnnotation) { + sigIdx = i; + break; + } + } + auto const &sig = file.signals()[sigIdx]; + REQUIRE_FALSE(sig.m_detail.m_isAnnotation); + + auto samples = file.readSignal(sigIdx); + REQUIRE_FALSE(samples.empty()); + + double physMin = sig.m_physicalMin; + double physMax = sig.m_physicalMax; + double tol = (physMax - physMin) * 0.001; + auto limit = std::min(samples.size(), size_t{100}); + for (size_t i = 0; i < limit; ++i) { + CHECK(samples[i] >= physMin - tol); + CHECK(samples[i] <= physMax + tol); + } + } + + SUBCASE("BDF digital samples within 24-bit range") { + size_t sigIdx = 0; + for (size_t i = 0; i < file.signals().size(); ++i) { + if (!file.signals()[i].m_detail.m_isAnnotation) { + sigIdx = i; + break; + } + } + auto const &sig = file.signals()[sigIdx]; + REQUIRE_FALSE(sig.m_detail.m_isAnnotation); + + auto samples = file.readSignalDigital(sigIdx); + REQUIRE_FALSE(samples.empty()); + + constexpr int32_t BDF_MIN = -8388608; + constexpr int32_t BDF_MAX = 8388607; + auto limit = std::min(samples.size(), size_t{100}); + for (size_t i = 0; i < limit; ++i) { + CHECK(samples[i] >= BDF_MIN); + CHECK(samples[i] <= BDF_MAX); + } + } +} + +// =========================================================================== +// 6. Annotation-only EDF+ file (Hypnogram) +// =========================================================================== + +TEST_CASE("EdfFile reads annotations from annotation-only file") { + auto file = EdfFile::open("SC4001EC-Hypnogram.edf"); + + CHECK(file.isEdf()); + CHECK(file.isPlus()); + CHECK(file.signalCount() == 1); + CHECK(file.signals()[0].m_detail.m_isAnnotation); + + auto annots = file.readAnnotations(); + CHECK(annots.size() > 0); + + for (auto const &a : annots) { + CHECK_FALSE(a.m_annotation.empty()); + CHECK(a.m_duration >= 0.0); + } +} + +// =========================================================================== +// 7. Invalid file path throws std::runtime_error +// =========================================================================== + +TEST_CASE("EdfFile::open throws on non-existent file") { + CHECK_THROWS_AS((void)EdfFile::open("no_such_file_xyz.edf"), std::runtime_error); +} + +// =========================================================================== +// 8. Invalid signal index throws std::out_of_range +// =========================================================================== + +TEST_CASE("EdfFile methods throw on invalid signal index") { + auto file = EdfFile::open("test_generator_2.edf"); + auto badIdx = static_cast(file.signalCount()) + 10; + + CHECK_THROWS_AS((void)file.readSignal(badIdx), std::out_of_range); + CHECK_THROWS_AS((void)file.readSignalDigital(badIdx), std::out_of_range); + CHECK_THROWS_AS((void)file.signalRecordStore(badIdx), std::out_of_range); + CHECK_THROWS_AS((void)file.signalSampleStore(badIdx), std::out_of_range); +} + +// =========================================================================== +// 9. EdfWriter round-trip +// =========================================================================== + +TEST_CASE("EdfWriter round-trip preserves header and data") { + const char *tmpfile = "test_edffile_roundtrip.edf"; + + // Read the original file with EdfFile + auto original = EdfFile::open("test_generator_2.edf"); + auto const &hdr = original.header(); + + // Write using EdfWriter + { + auto writer = EdfWriter::create(tmpfile, hdr); + auto store = original.dataRecordStore(); + for (auto it = store.begin(); it != store.end(); ++it) { + writer.writeDataRecord(*it); + } + writer.close(); + } + + // Read back using EdfFile and verify + { + auto copy = EdfFile::open(tmpfile); + + CHECK(copy.format() == original.format()); + CHECK(copy.signalCount() == original.signalCount()); + CHECK(copy.dataRecordCount() == original.dataRecordCount()); + CHECK(copy.duration() == doctest::Approx(original.duration())); + + REQUIRE(copy.signals().size() == original.signals().size()); + for (size_t i = 0; i < copy.signals().size(); ++i) { + CHECK(copy.signals()[i].m_label == original.signals()[i].m_label); + CHECK(copy.signals()[i].m_samplesInDataRecord == + original.signals()[i].m_samplesInDataRecord); + } + + // Compare first non-annotation signal's digital samples + size_t sigIdx = 0; + for (size_t i = 0; i < copy.signals().size(); ++i) { + if (!copy.signals()[i].m_detail.m_isAnnotation) { + sigIdx = i; + break; + } + } + + auto origSamples = original.readSignalDigital(sigIdx); + auto copySamples = copy.readSignalDigital(sigIdx); + REQUIRE(copySamples.size() == origSamples.size()); + + auto limit = std::min(copySamples.size(), size_t{200}); + for (size_t i = 0; i < limit; ++i) { + CHECK(copySamples[i] == origSamples[i]); + } + } + + std::remove(tmpfile); +} + +// =========================================================================== +// 10. EdfWriter auto-patches data record count on close +// =========================================================================== + +TEST_CASE("EdfWriter auto-patches data record count in header") { + const char *tmpfile = "test_autocount.edf"; + + auto original = EdfFile::open("test_generator_2.edf"); + auto hdr = original.header(); + + // Deliberately set count to 0 -- the writer should fix it on close + hdr.m_general.m_datarecordsFile = 0; + + { + auto writer = EdfWriter::create(tmpfile, hdr); + auto store = original.dataRecordStore(); + int64_t written = 0; + for (auto it = store.begin(); it != store.end(); ++it) { + writer.writeDataRecord(*it); + ++written; + } + CHECK(writer.recordsWritten() == written); + CHECK(written == 600); + writer.close(); + } + + // Read back -- the header should now have the correct count + { + auto copy = EdfFile::open(tmpfile); + CHECK(copy.dataRecordCount() == 600); + CHECK(copy.duration() == doctest::Approx(600.0)); + + // Verify data integrity too + size_t sigIdx = 0; + for (size_t i = 0; i < copy.signals().size(); ++i) { + if (!copy.signals()[i].m_detail.m_isAnnotation) { + sigIdx = i; + break; + } + } + auto origSamples = original.readSignalDigital(sigIdx); + auto copySamples = copy.readSignalDigital(sigIdx); + REQUIRE(copySamples.size() == origSamples.size()); + for (size_t i = 0; i < std::min(copySamples.size(), size_t{100}); ++i) { + CHECK(copySamples[i] == origSamples[i]); + } + } + + std::remove(tmpfile); +} + +TEST_CASE("EdfWriter destructor auto-patches count without explicit close") { + const char *tmpfile = "test_autocount_dtor.edf"; + + auto original = EdfFile::open("Calib5.edf"); + auto hdr = original.header(); + hdr.m_general.m_datarecordsFile = -1; // intentionally wrong + + { + auto writer = EdfWriter::create(tmpfile, hdr); + auto store = original.dataRecordStore(); + for (auto it = store.begin(); it != store.end(); ++it) { + writer.writeDataRecord(*it); + } + // No explicit close() -- destructor should handle it + } + + auto copy = EdfFile::open(tmpfile); + CHECK(copy.dataRecordCount() == original.dataRecordCount()); + + std::remove(tmpfile); +} + +// =========================================================================== +// 11. EdfFile is moveable +// =========================================================================== + +TEST_CASE("EdfFile supports move semantics") { + auto file = EdfFile::open("test_generator_2.edf"); + auto moved = std::move(file); + + CHECK(moved.signalCount() == 12); + CHECK(moved.format() == DataFormat::EdfPlusC); +} + +// =========================================================================== +// 11. Low-level store access through EdfFile +// =========================================================================== + +TEST_CASE("EdfFile provides low-level store access") { + auto file = EdfFile::open("test_generator_2.edf"); + + SUBCASE("dataRecordStore") { + auto store = file.dataRecordStore(); + CHECK(store.size() == + static_cast(file.dataRecordCount())); + } + + SUBCASE("signalRecordStore") { + size_t sigIdx = 0; + auto store = file.signalRecordStore(sigIdx); + CHECK(store.size() == + static_cast(file.dataRecordCount())); + } + + SUBCASE("signalSampleStore") { + size_t sigIdx = 0; + auto const &sig = file.signals()[sigIdx]; + auto store = file.signalSampleStore(sigIdx); + auto expected = static_cast(file.dataRecordCount()) * + static_cast(sig.m_samplesInDataRecord); + CHECK(store.size() == expected); + } +} From 989b48f60ee248439870d798d115ef28c9e3c91e Mon Sep 17 00:00:00 2001 From: iuri dotta Date: Sun, 1 Mar 2026 15:48:04 -0300 Subject: [PATCH 11/11] Refactor: Add EdfWriter class for high-level EDF/BDF file writing and update EdfIO include paths --- include/edfio/EdfFile.hpp | 106 ++++------------------------------- include/edfio/EdfIO.hpp | 1 + include/edfio/EdfWriter.hpp | 108 ++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 95 deletions(-) create mode 100644 include/edfio/EdfWriter.hpp diff --git a/include/edfio/EdfFile.hpp b/include/edfio/EdfFile.hpp index a73f99e..ed632e2 100644 --- a/include/edfio/EdfFile.hpp +++ b/include/edfio/EdfFile.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include namespace edfio { @@ -112,8 +113,8 @@ class EdfFile { validateSignalIndex(signalIndex); auto const &sig = m_header.m_signals[signalIndex]; - auto store = detail::CreateSignalSampleStore(*m_stream, m_header.m_general, - sig); + auto store = + detail::CreateSignalSampleStore(*m_stream, m_header.m_general, sig); ProcessorSampleRecord proc(sig.m_detail.m_offset, sig.m_detail.m_scaling); @@ -138,8 +139,8 @@ class EdfFile { validateSignalIndex(signalIndex); auto const &sig = m_header.m_signals[signalIndex]; - auto store = detail::CreateSignalSampleStore(*m_stream, m_header.m_general, - sig); + auto store = + detail::CreateSignalSampleStore(*m_stream, m_header.m_general, sig); // offset=0, scaling=1 yields the raw digital value. ProcessorSampleRecord proc(0.0, 1.0); @@ -166,8 +167,8 @@ class EdfFile { if (!sig.m_detail.m_isAnnotation) continue; - auto sigStore = detail::CreateSignalRecordStore( - *m_stream, m_header.m_general, sig); + auto sigStore = + detail::CreateSignalRecordStore(*m_stream, m_header.m_general, sig); int64_t drIdx = 0; for (auto it = sigStore.begin(); it != sigStore.end(); ++it, ++drIdx) { @@ -209,7 +210,7 @@ class EdfFile { [[nodiscard]] SignalSampleStore signalSampleStore(size_t signalIndex) const { validateSignalIndex(signalIndex); return detail::CreateSignalSampleStore(*m_stream, m_header.m_general, - m_header.m_signals[signalIndex]); + m_header.m_signals[signalIndex]); } private: @@ -218,10 +219,9 @@ class EdfFile { void validateSignalIndex(size_t signalIndex) const { if (signalIndex >= m_header.m_signals.size()) { - throw std::out_of_range( - "Signal index " + std::to_string(signalIndex) + - " out of range [0, " + - std::to_string(m_header.m_signals.size()) + ")"); + throw std::out_of_range("Signal index " + std::to_string(signalIndex) + + " out of range [0, " + + std::to_string(m_header.m_signals.size()) + ")"); } } @@ -229,88 +229,4 @@ class EdfFile { HeaderExam m_header; }; -/// High-level facade for writing EDF/BDF files. -/// -/// Owns the underlying output stream. Write data records sequentially after -/// the header has been written. -/// -/// Moveable but not copyable (owns an std::ofstream). -class EdfWriter { -public: - EdfWriter(const EdfWriter &) = delete; - EdfWriter &operator=(const EdfWriter &) = delete; - EdfWriter(EdfWriter &&) noexcept = default; - EdfWriter &operator=(EdfWriter &&) noexcept = default; - - /// Create a new EDF/BDF file and write its header. - /// - /// @param path Filesystem path for the new file. - /// @param header Complete header exam to write. - /// @throws std::runtime_error if the file cannot be created. - [[nodiscard]] static EdfWriter create(const std::filesystem::path &path, - const HeaderExam &header) { - auto stream = std::make_unique(path, std::ios::binary); - if (!stream->is_open()) { - throw std::runtime_error(std::string("Cannot create file: ") + - path.string()); - } - WriteHeaderExam(*stream, header); - return EdfWriter(std::move(stream), header.m_general); - } - - /// Write a single data record to the file. - /// - /// The data record count in the header is automatically updated when - /// close() is called (or the writer is destroyed). - void writeDataRecord(const Record &record) { - if (m_stream && m_stream->is_open()) { - *m_stream << record; - ++m_recordsWritten; - } - } - - /// Number of data records written so far. - [[nodiscard]] int64_t recordsWritten() const noexcept { - return m_recordsWritten; - } - - /// Flush, patch the data-record count in the header, and close. - void close() { - if (m_stream && m_stream->is_open()) { - // EDF/BDF header layout: the "number of data records" field is 8 bytes - // starting at offset 236 (after version[8] + patient[80] + - // recording[80] + startDate[8] + startTime[8] + headerSize[8] + - // reserved[44] = 236). - static constexpr std::streamoff kDataRecordCountOffset = 236; - static constexpr size_t kFieldSize = 8; - - m_stream->seekp(kDataRecordCountOffset, std::ios::beg); - auto countStr = std::to_string(m_recordsWritten); - countStr.resize(kFieldSize, ' '); - m_stream->write(countStr.data(), kFieldSize); - - m_stream->flush(); - m_stream->close(); - } - } - - /// Destructor flushes and closes the stream if still open. - ~EdfWriter() { - try { - close(); - } catch (...) { - // Suppress exceptions in destructor. - } - } - -private: - explicit EdfWriter(std::unique_ptr stream, - HeaderGeneral general) - : m_stream(std::move(stream)), m_general(std::move(general)) {} - - std::unique_ptr m_stream; - HeaderGeneral m_general; - int64_t m_recordsWritten = 0; -}; - } // namespace edfio diff --git a/include/edfio/EdfIO.hpp b/include/edfio/EdfIO.hpp index 8ef07cd..41a00e1 100644 --- a/include/edfio/EdfIO.hpp +++ b/include/edfio/EdfIO.hpp @@ -42,6 +42,7 @@ // High-level facade #include "EdfFile.hpp" +#include "EdfWriter.hpp" // STL #include diff --git a/include/edfio/EdfWriter.hpp b/include/edfio/EdfWriter.hpp new file mode 100644 index 0000000..b64f5df --- /dev/null +++ b/include/edfio/EdfWriter.hpp @@ -0,0 +1,108 @@ +// +// Copyright(c) 2017-present Iuri Dotta (dotta dot iuri at gmail dot com) +// +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. +// +// Official repository: https://github.com/idotta/edfio +// + +#pragma once + +#include "EdfIO.hpp" + +#include +#include +#include +#include +#include +#include + +namespace edfio { + +/// High-level facade for writing EDF/BDF files. +/// +/// Owns the underlying output stream. Write data records sequentially after +/// the header has been written. The data-record count in the header is +/// automatically patched on close() (or destruction). +/// +/// Moveable but not copyable (owns an std::ofstream). +class EdfWriter { +public: + EdfWriter(const EdfWriter &) = delete; + EdfWriter &operator=(const EdfWriter &) = delete; + EdfWriter(EdfWriter &&) noexcept = default; + EdfWriter &operator=(EdfWriter &&) noexcept = default; + + /// Create a new EDF/BDF file and write its header. + /// + /// @param path Filesystem path for the new file. + /// @param header Complete header exam to write. + /// @throws std::runtime_error if the file cannot be created. + [[nodiscard]] static EdfWriter create(const std::filesystem::path &path, + const HeaderExam &header) { + auto stream = std::make_unique(path, std::ios::binary); + if (!stream->is_open()) { + throw std::runtime_error(std::string("Cannot create file: ") + + path.string()); + } + WriteHeaderExam(*stream, header); + return EdfWriter(std::move(stream), header.m_general); + } + + /// Write a single data record to the file. + /// + /// The data record count in the header is automatically updated when + /// close() is called (or the writer is destroyed). + void writeDataRecord(const Record &record) { + if (m_stream && m_stream->is_open()) { + *m_stream << record; + ++m_recordsWritten; + } + } + + /// Number of data records written so far. + [[nodiscard]] int64_t recordsWritten() const noexcept { + return m_recordsWritten; + } + + /// Flush, patch the data-record count in the header, and close. + void close() { + if (m_stream && m_stream->is_open()) { + // EDF/BDF header layout: the "number of data records" field is 8 bytes + // starting at offset 236 (after version[8] + patient[80] + + // recording[80] + startDate[8] + startTime[8] + headerSize[8] + + // reserved[44] = 236). + static constexpr std::streamoff kDataRecordCountOffset = 236; + static constexpr size_t kFieldSize = 8; + + m_stream->seekp(kDataRecordCountOffset, std::ios::beg); + auto countStr = std::to_string(m_recordsWritten); + countStr.resize(kFieldSize, ' '); + m_stream->write(countStr.data(), kFieldSize); + + m_stream->flush(); + m_stream->close(); + } + } + + /// Destructor flushes and closes the stream if still open. + ~EdfWriter() { + try { + close(); + } catch (...) { + // Suppress exceptions in destructor. + } + } + +private: + explicit EdfWriter(std::unique_ptr stream, + HeaderGeneral general) + : m_stream(std::move(stream)), m_general(std::move(general)) {} + + std::unique_ptr m_stream; + HeaderGeneral m_general; + int64_t m_recordsWritten = 0; +}; + +} // namespace edfio