diff --git a/src/Utils.hpp b/src/Utils.hpp index eb113fc7..e784284c 100644 --- a/src/Utils.hpp +++ b/src/Utils.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -61,6 +62,26 @@ static inline std::string getCurrentTime() return currentTime; } +/** + * @brief Check that a string is formatted in ISO8601 format + * + * This function only validates the regex pattern but does not check that + * the time values specified are indeed valid. + * + * @return bool indicating whether the string is in ISO8601 form + */ +static inline bool isISO8601Date(const std::string& dateStr) +{ + // Define the regex pattern for ISO 8601 extended format with timezone offset + // Allow one or more fractional seconds digits + const std::string iso8601Pattern = + R"(^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+[+-]\d{2}:\d{2}$)"; + std::regex pattern(iso8601Pattern); + + // Check if the date string matches the regex pattern + return std::regex_match(dateStr, pattern); +} + /** * @brief Factory method to create an IO object. * @return A pointer to a BaseIO object diff --git a/src/nwb/NWBFile.cpp b/src/nwb/NWBFile.cpp index 87d3ade4..b19dfb28 100644 --- a/src/nwb/NWBFile.cpp +++ b/src/nwb/NWBFile.cpp @@ -49,15 +49,42 @@ NWBFile::~NWBFile() {} Status NWBFile::initialize(const std::string& identifierText, const std::string& description, - const std::string& dataCollection) + const std::string& dataCollection, + const std::string& sessionStartTime, + const std::string& timestampsReferenceTime) { if (!m_io->isOpen()) { return Status::Failure; } + std::string currentTime = getCurrentTime(); + // use the current time if sessionStartTime is empty + std::string useSessionStartTime = + (!sessionStartTime.empty()) ? sessionStartTime : currentTime; + // use the current time if timestampsReferenceTime is empty + std::string useTimestampsReferenceTime = (!timestampsReferenceTime.empty()) + ? timestampsReferenceTime + : currentTime; + // check that sessionStartTime and timestampsReferenceTime are ISO8601 + if (!isISO8601Date(useSessionStartTime)) { + std::cerr << "NWBFile::initialize sessionStartTime not in ISO8601 format: " + << useSessionStartTime << std::endl; + return Status::Failure; + } + if (!isISO8601Date(useTimestampsReferenceTime)) { + std::cerr + << "NWBFile::initialize timestampsReferenceTime not in ISO8601 format: " + << useTimestampsReferenceTime << std::endl; + return Status::Failure; + } + // Check that the file is empty and initialize if it is bool fileInitialized = isInitialized(); if (!fileInitialized) { - return createFileStructure(identifierText, description, dataCollection); + return createFileStructure(identifierText, + description, + dataCollection, + useSessionStartTime, + useTimestampsReferenceTime); } else { return Status::Success; // File is already initialized } @@ -102,7 +129,9 @@ Status NWBFile::finalize() Status NWBFile::createFileStructure(const std::string& identifierText, const std::string& description, - const std::string& dataCollection) + const std::string& dataCollection, + const std::string& sessionStartTime, + const std::string& timestampsReferenceTime) { if (!m_io->canModifyObjects()) { return Status::Failure; @@ -133,12 +162,12 @@ Status NWBFile::createFileStructure(const std::string& identifierText, cacheSpecifications("hdmf-experimental", AQNWB::SPEC::HDMF_EXPERIMENTAL::version, AQNWB::SPEC::HDMF_EXPERIMENTAL::specVariables); - std::string time = getCurrentTime(); - std::vector timeVec = {time}; + std::vector timeVec = {sessionStartTime}; m_io->createStringDataSet("/file_create_date", timeVec); m_io->createStringDataSet("/session_description", description); - m_io->createStringDataSet("/session_start_time", time); - m_io->createStringDataSet("/timestamps_reference_time", time); + m_io->createStringDataSet("/session_start_time", sessionStartTime); + m_io->createStringDataSet("/timestamps_reference_time", + timestampsReferenceTime); m_io->createStringDataSet("/identifier", identifierText); return Status::Success; } diff --git a/src/nwb/NWBFile.hpp b/src/nwb/NWBFile.hpp index 69ed79bf..9234b69b 100644 --- a/src/nwb/NWBFile.hpp +++ b/src/nwb/NWBFile.hpp @@ -67,10 +67,16 @@ class NWBFile : public Container * @param identifierText The identifier text for the NWBFile. * @param description A description of the NWBFile session. * @param dataCollection Information about the data collection methods. + * @param sessionStartTime ISO formatted time string with the session start + * time. If empty (default), then the getCurrentTime() will be used. + * @param timestampsReferenceTime ISO formatted time string with the timestamp + * reference time. If empty (default), then the getCurrentTime() will be used. */ Status initialize(const std::string& identifierText, const std::string& description = "a recording session", - const std::string& dataCollection = ""); + const std::string& dataCollection = "", + const std::string& sessionStartTime = "", + const std::string& timestampsReferenceTime = ""); /** * @brief Check if the NWB file is initialized. @@ -155,11 +161,17 @@ class NWBFile : public Container * @param identifierText The identifier text for the NWBFile. * @param description A description of the NWBFile session. * @param dataCollection Information about the data collection methods. + * @param sessionStartTime ISO formatted time string with the session start + * time + * @param timestampsReferenceTime ISO formatted time string with the timestamp + * reference time * @return Status The status of the file structure creation. */ Status createFileStructure(const std::string& identifierText, const std::string& description, - const std::string& dataCollection); + const std::string& dataCollection, + const std::string& sessionStartTime, + const std::string& timestampsReferenceTime); private: /** diff --git a/tests/testNWBFile.cpp b/tests/testNWBFile.cpp index 9bbd8f29..f87165f2 100644 --- a/tests/testNWBFile.cpp +++ b/tests/testNWBFile.cpp @@ -26,6 +26,39 @@ TEST_CASE("saveNWBFile", "[nwb]") nwbfile.finalize(); } +TEST_CASE("initialize", "[nwb]") +{ + std::string filename = getTestFilePath("testInitializeNWBFile.nwb"); + + // initialize nwbfile object and create base structure + auto io = std::make_shared(filename); + io->open(); + NWB::NWBFile nwbfile(io); + + // bad session start time + Status initStatus = nwbfile.initialize(generateUuid(), + "test file", + "no collection", + "bad time", + AQNWB::getCurrentTime()); + REQUIRE(initStatus == Status::Failure); + + // bad timestamp reference time + initStatus = nwbfile.initialize(generateUuid(), + "test file", + "no collection", + AQNWB::getCurrentTime(), + "bad time"); + REQUIRE(initStatus == Status::Failure); + + // check that regular init with current times works + initStatus = nwbfile.initialize(generateUuid()); + REQUIRE(initStatus == Status::Success); + REQUIRE(nwbfile.isInitialized()); + nwbfile.finalize(); + io->close(); +} + TEST_CASE("createElectricalSeries", "[nwb]") { std::string filename = getTestFilePath("createElectricalSeries.nwb"); diff --git a/tests/testUtilsFunctions.cpp b/tests/testUtilsFunctions.cpp index 3a538f5f..3072a05c 100644 --- a/tests/testUtilsFunctions.cpp +++ b/tests/testUtilsFunctions.cpp @@ -4,6 +4,37 @@ #include "Utils.hpp" +TEST_CASE("isISO8601Date function tests", "[utils]") +{ + SECTION("Valid ISO 8601 date strings") + { + REQUIRE(AQNWB::isISO8601Date("2018-09-28T14:43:54.123+02:00")); + REQUIRE(AQNWB::isISO8601Date("2025-01-19T00:40:03.214144-08:00")); + REQUIRE(AQNWB::isISO8601Date("2021-12-31T23:59:59.999999+00:00")); + REQUIRE(AQNWB::isISO8601Date("2000-01-01T00:00:00.0+01:00")); + REQUIRE(AQNWB::isISO8601Date( + "2018-09-28T14:43:54.12345+02:00")); // Allow for too many fractional + // seconds + } + + SECTION("Invalid ISO 8601 date strings") + { + REQUIRE_FALSE(AQNWB::isISO8601Date( + "2018-09-28 14:43:54.123+02:00")); // Space instead of 'T' + REQUIRE_FALSE(AQNWB::isISO8601Date( + "2018-09-28T14:43:54+02:00")); // Missing fractional seconds + REQUIRE_FALSE(AQNWB::isISO8601Date( + "2018-09-28T14:43:54.123+0200")); // Missing colon in timezone + REQUIRE_FALSE(AQNWB::isISO8601Date( + "2018-09-28T14:43:54.123Z")); // Missing timezone offset + REQUIRE_FALSE(AQNWB::isISO8601Date( + "2018-09-28T14:43:54.123-0800")); // Incorrect timezone format + REQUIRE_FALSE( + AQNWB::isISO8601Date("2018-09-28T14:43:54.123")); // Missing timezone + REQUIRE_FALSE(AQNWB::isISO8601Date("Random text 1213")); + } +} + TEST_CASE("Test UUID generation", "[utils]") { // Test that generated UUIDs are valid