diff --git a/src/epmodel/ScheduleInterval/ScheduleFile.cpp b/src/epmodel/ScheduleInterval/ScheduleFile.cpp index e994df2127..f6d2019b57 100644 --- a/src/epmodel/ScheduleInterval/ScheduleFile.cpp +++ b/src/epmodel/ScheduleInterval/ScheduleFile.cpp @@ -13,15 +13,47 @@ #include #include #include +#include +#include + +#include "../utilities/data/TimeSeries.hpp" +#include "../model/YearDescription.hpp" +#include "../model/YearDescription_Impl.hpp" namespace openstudio { namespace epmodel { - ScheduleFile::ScheduleFile(const Model& model) : Schedule(ScheduleFile::iddObjectType(), model) { + // ScheduleFile(const ExternalFile& externalfile, int column = 1, int rowsToSkip = 0) : Schedule(ScheduleFile::iddObjectType(), model) {} + + ScheduleFile::ScheduleFile(const Model& model, int column, int rowsToSkip) : Schedule(ScheduleFile::iddObjectType(), model) { // Mirror preserved counterpart constructor behavior for required scalar fields. bool ok = true; - ok &= setColumnNumber(1); - ok &= setRowstoSkipatTop(0); + ok &= setColumnNumber(column); + ok &= setRowstoSkipatTop(rowsToSkip); + OS_ASSERT(ok); + } + + ScheduleFile::ScheduleFile(const Model& model, const openstudio::path& filePath, int column, int rowsToSkip, bool translateFileWithRelativePath) + : Schedule(ScheduleFile::iddObjectType(), model) { + + openstudio::path p; + if (!exists(filePath)) { + this->remove(); + LOG_FREE_AND_THROW("openstudio.epmodel.ScheduleFile", "Cannot find file \"" << toString(filePath) << "\" for " << briefDescription()); + } else { + if (translateFileWithRelativePath) { + p = filePath; + } else { + // make the path correct for this system + p = system_complete(filePath); + } + } + OS_ASSERT(exists(filePath)); + + bool ok = true; + ok &= setFileName(toString(p)); + ok &= setColumnNumber(column); + ok &= setRowstoSkipatTop(rowsToSkip); OS_ASSERT(ok); } @@ -39,6 +71,14 @@ namespace epmodel { return getIddKeyNames(IddFactory::instance().getObject(iddObjectType()).get(), openstudio::Schedule_FileFields::MinutesperItem); } + std::string ScheduleFile::fileName() const { + return getImpl()->fileName(); + } + + bool ScheduleFile::setFileName(std::string fileName) { + return getImpl()->setFileName(fileName); + } + int ScheduleFile::columnNumber() const { return getImpl()->columnNumber(); } @@ -99,7 +139,7 @@ namespace epmodel { getImpl()->resetInterpolatetoTimestep(); } - boost::optional ScheduleFile::minutesperItem() const { + int ScheduleFile::minutesperItem() const { return getImpl()->minutesperItem(); } @@ -139,6 +179,72 @@ namespace epmodel { getImpl()->resetAdjustScheduleforDaylightSavings(); } + boost::optional ScheduleFile::csvFile() const { + return getImpl()->csvFile(); + } + + bool ScheduleFile::translateFileWithRelativePath() const { + DEPRECATED_AT_MSG(4, 0, 0, "Schedule:File is no longer 'translated'."); + return false; + } + + bool ScheduleFile::isTranslateFileWithRelativePathDefaulted() const { + DEPRECATED_AT_MSG(4, 0, 0, "Schedule:File is no longer 'translated'."); + return false; + } + + bool ScheduleFile::setTranslateFileWithRelativePath(bool translateFileWithRelativePath) { + DEPRECATED_AT_MSG(4, 0, 0, "Schedule:File is no longer 'translated'."); + return false; + } + + void ScheduleFile::resetTranslateFileWithRelativePath() { + DEPRECATED_AT_MSG(4, 0, 0, "Schedule:File is no longer 'translated'."); + } + + openstudio::path ScheduleFile::translatedFilePath() const { + return getImpl()->translatedFilePath(); + } + + openstudio::TimeSeries ScheduleFile::timeSeries() const { + return getImpl()->timeSeries(); + } + + boost::optional ScheduleFile::fromTimeSeries(const openstudio::TimeSeries& timeSeries, Model& model) { + boost::optional result; + + boost::optional intervalTime = timeSeries.intervalLength(); + if (intervalTime) { + result = ScheduleFile(model, 2); // FIXME: FT ScheduleFixedInterval wrote the dateTimes to file + const std::string name = result->nameString(); + openstudio::path filePath = toPath(name + ".csv"); + + CSVFile csvFile; + csvFile.addColumn(timeSeries.dateTimes()); + csvFile.addColumn(timeSeries.values()); + csvFile.saveAs(filePath); + + openstudio::path p; + if (!exists(filePath)) { + result->remove(); + LOG_FREE_AND_THROW("openstudio.epmodel.ScheduleFile", "Cannot find file \"" << toString(filePath) << "\" for " << result->briefDescription()); + } else { + // make the path correct for this system + p = system_complete(filePath); + } + OS_ASSERT(exists(filePath)); + + bool ok = true; + ok &= result->setFileName(toString(p)); + ok &= result->getImpl()->setTimeSeries(timeSeries); + OS_ASSERT(ok); + } else { + LOG_FREE(Warn, "openstudio.epmodel.ScheduleFile", "Timeseries does not have an interval length defined, but ScheduleVariableInterval is deprecated"); + } + + return result; + } + } // namespace epmodel } // namespace openstudio @@ -150,6 +256,115 @@ namespace epmodel { return openstudio::Schedule_FileFields::ScheduleTypeLimitsName; } + openstudio::TimeSeries ScheduleFile_Impl::timeSeries() const { + boost::optional csvFile = this->csvFile(); + if (!csvFile) { + LOG_FREE_AND_THROW("openstudio.epmodel.ScheduleFile", "Did not set File Name for " << briefDescription()); + } + OS_ASSERT(csvFile); + + int columnIndex = this->columnNumber() - 1; + std::vector values = csvFile->getColumnAsStringVector(columnIndex); + const int rowsToSkip = this->rowstoSkipatTop(); + Vector vectorValues(values.size() - rowsToSkip); + for (size_t i = rowsToSkip; i < values.size(); ++i) { + double value = std::stod(values[i]); + vectorValues[i - rowsToSkip] = value; + } + + int minutesperItem = this->minutesperItem(); + openstudio::Time intervalLength(0, 0, minutesperItem); + + int year = 2009; + // FIXME + // if (boost::optional yd = this->model().getOptionalUniqueModelObject()) { + // year = yd->assumedYear(); + // } + Date startDate(MonthOfYear::Jan, 1, year); + + openstudio::TimeSeries result(startDate, intervalLength, vectorValues, ""); + + // error checking + DateTimeVector dateTimes = result.dateTimes(); + DateTime lastDateTime = DateTime(Date(MonthOfYear::Dec, 31, year), openstudio::Time(0, 24)); + if (dateTimes.back() != lastDateTime) { + LOG_FREE(Warn, "openstudio.epmodel.ScheduleFile", "With " << minutesperItem << " minutes per item, last date time is " << dateTimes.back() << " for " << this->fileName() << " referenced by " << briefDescription()); + } + + return result; + } + + bool ScheduleFile_Impl::setTimeSeries(const openstudio::TimeSeries& timeSeries) { + boost::optional intervalTime = timeSeries.intervalLength(); + if (!intervalTime) { + return false; + } + + auto intervalLengthAsInteger = [](const double value) -> int { + double integralPart = 0.0; + if (std::modf(value, &integralPart) == 0.0) { + // The intervalLength is actually an int, not a double + return static_cast(integralPart); + } + return -1; + }; + + // check the interval + const double intervalLengthDouble = intervalTime->totalMinutes(); + const int intervalLength = intervalLengthAsInteger(intervalLengthDouble); + if (intervalLength < 0) { + return false; + } + + // check that first report is whole number of intervals from start date + const DateTime firstReportDateTime = timeSeries.firstReportDateTime(); + const DateTime startDateTime = timeSeries.startDateTime(); + const Date startDate = startDateTime.date(); + + const Time firstReportTime = firstReportDateTime.time(); + + const double numIntervalsToFirstReportDouble = std::max(1.0, firstReportTime.totalMinutes() / intervalLengthDouble); + const int numIntervalsToFirstReport = intervalLengthAsInteger(numIntervalsToFirstReportDouble); + if (numIntervalsToFirstReport < 0) { + return false; + } + + // check the values + const openstudio::Vector values = timeSeries.values(); + for (size_t pos = 0; const auto& value : values) { + // Check validity, cannot be NaN, Inf, etc + if (std::isinf(value)) { + LOG_FREE(Warn, "openstudio.epmodel.ScheduleFile", "There is Infinity on position " << pos << " in the timeSeries provided for " << briefDescription()); + return false; + } else if (std::isnan(value)) { + LOG_FREE(Warn, "openstudio.epmodel.ScheduleFile", "There is a NaN on position " << pos << " in the timeSeries provided for " << briefDescription()); + return false; + } + ++pos; + } + + bool ok = true; + ok &= this->setMinutesperItem(intervalLength); + // Do we actually need the following? They aren't required in the IDD. + //ok &= this->setNumberofHoursofData(8760); + //ok &= this->setColumnSeparator("Comma"); + //ok &= this->setInterpolatetoTimestep(true); + //ok &= this->setAdjustScheduleforDaylightSavings(true); + return true; + } + + std::string ScheduleFile_Impl::fileName() const { + const auto value = getString(openstudio::Schedule_FileFields::FileName, true); + OS_ASSERT(value); + return *value; + } + + bool ScheduleFile_Impl::setFileName(std::string fileName) { + const bool result = setString(openstudio::Schedule_FileFields::FileName, fileName); + OS_ASSERT(result); + return result; + } + int ScheduleFile_Impl::columnNumber() const { const auto value = getInt(openstudio::Schedule_FileFields::ColumnNumber, true); OS_ASSERT(value); @@ -220,10 +435,10 @@ namespace epmodel { OS_ASSERT(setString(openstudio::Schedule_FileFields::InterpolatetoTimestep, "")); } - boost::optional ScheduleFile_Impl::minutesperItem() const { + int ScheduleFile_Impl::minutesperItem() const { const auto value = getInt(openstudio::Schedule_FileFields::MinutesperItem, true); OS_ASSERT(value); - return std::to_string(*value); + return *value; } bool ScheduleFile_Impl::isMinutesperItemDefaulted() const { @@ -259,6 +474,21 @@ namespace epmodel { OS_ASSERT(setString(openstudio::Schedule_FileFields::AdjustScheduleforDaylightSavings, "")); } + boost::optional ScheduleFile_Impl::csvFile() const { + boost::optional csvFile; + openstudio::path filePath = this->fileName(); + csvFile = CSVFile::load(filePath); + return csvFile; + } + + openstudio::path ScheduleFile_Impl::translatedFilePath() const { + openstudio::path filePath = this->fileName(); + if (!exists(filePath)) { + LOG_FREE(Warn, "openstudio.epmodel.ScheduleFile", "Cannot find file \"" << filePath << "\""); + } + return filePath; + } + std::vector ScheduleFile_Impl::columnSeparatorValues() const { return openstudio::epmodel::ScheduleFile::columnSeparatorValues(); } diff --git a/src/epmodel/ScheduleInterval/ScheduleFile.hpp b/src/epmodel/ScheduleInterval/ScheduleFile.hpp index 75ec6d7d60..f25fe106e0 100644 --- a/src/epmodel/ScheduleInterval/ScheduleFile.hpp +++ b/src/epmodel/ScheduleInterval/ScheduleFile.hpp @@ -8,6 +8,9 @@ #include "EPModelAPI.hpp" #include "Schedule/Schedule.hpp" +#include "../../utilities/core/Path.hpp" +#include "../../utilities/filetypes/CSVFile.hpp" +#include #include @@ -15,6 +18,7 @@ #include namespace openstudio { + namespace epmodel { class Model; @@ -53,7 +57,9 @@ namespace epmodel { class EPMODEL_API ScheduleFile : public Schedule { public: - explicit ScheduleFile(const Model& model); + // explicit ScheduleFile(const ExternalFile& externalfile, int column = 1, int rowsToSkip = 0); // FIXME: how do we maintain this? + explicit ScheduleFile(const Model& model, int column = 1, int rowsToSkip = 0); // new ctor + explicit ScheduleFile(const Model& model, const openstudio::path& filePath, int column = 1, int rowsToSkip = 0, bool translateFileWithRelativePath = false); // old ctor virtual ~ScheduleFile() override = default; ScheduleFile(const ScheduleFile& other) = default; @@ -66,6 +72,9 @@ namespace epmodel { static std::vector columnSeparatorValues(); static std::vector minutesperItemValues(); + std::string fileName() const; + bool setFileName(std::string fileName); + int columnNumber() const; bool setColumnNumber(int columnNumber); @@ -86,7 +95,7 @@ namespace epmodel { bool setInterpolatetoTimestep(bool interpolatetoTimestep); void resetInterpolatetoTimestep(); - boost::optional minutesperItem() const; + int minutesperItem() const; bool isMinutesperItemDefaulted() const; bool setMinutesperItem(const std::string& minutesperItem); bool setMinutesperItem(int minutesperItem); @@ -97,6 +106,17 @@ namespace epmodel { bool setAdjustScheduleforDaylightSavings(bool adjustScheduleforDaylightSavings); void resetAdjustScheduleforDaylightSavings(); + openstudio::TimeSeries timeSeries() const; + static boost::optional fromTimeSeries(const openstudio::TimeSeries& timeSeries, Model& model); + + // Extra setters/getters + boost::optional csvFile() const; + OS_DEPRECATED(4, 0, 0) bool translateFileWithRelativePath() const; + OS_DEPRECATED(4, 0, 0) bool isTranslateFileWithRelativePathDefaulted() const; + OS_DEPRECATED(4, 0, 0) bool setTranslateFileWithRelativePath(bool translateFileWithRelativePath); + OS_DEPRECATED(4, 0, 0) void resetTranslateFileWithRelativePath(); + openstudio::path translatedFilePath() const; + protected: using ImplType = detail::ScheduleFile_Impl; @@ -105,6 +125,9 @@ namespace epmodel { friend class openstudio::detail::IdfObject_Impl; explicit ScheduleFile(std::shared_ptr impl); + + private: + REGISTER_LOGGER("openstudio.epmodel.ScheduleFile"); }; } // namespace epmodel diff --git a/src/epmodel/ScheduleInterval/ScheduleFile_Impl.hpp b/src/epmodel/ScheduleInterval/ScheduleFile_Impl.hpp index eca16de7e4..d9b4aa0c47 100644 --- a/src/epmodel/ScheduleInterval/ScheduleFile_Impl.hpp +++ b/src/epmodel/ScheduleInterval/ScheduleFile_Impl.hpp @@ -7,6 +7,7 @@ #define EPMODEL_SCHEDULEFILE_IMPL_HPP #include "Schedule/Schedule_Impl.hpp" +#include "../utilities/filetypes/CSVFile.hpp" #include @@ -20,6 +21,9 @@ namespace epmodel { using Schedule_Impl::Schedule_Impl; virtual ~ScheduleFile_Impl() override = default; + std::string fileName() const; + bool setFileName(std::string fileName); + int columnNumber() const; bool setColumnNumber(int columnNumber); @@ -40,7 +44,7 @@ namespace epmodel { bool setInterpolatetoTimestep(bool interpolatetoTimestep); void resetInterpolatetoTimestep(); - boost::optional minutesperItem() const; + int minutesperItem() const; bool isMinutesperItemDefaulted() const; bool setMinutesperItem(int minutesperItem); void resetMinutesperItem(); @@ -50,6 +54,18 @@ namespace epmodel { bool setAdjustScheduleforDaylightSavings(bool adjustScheduleforDaylightSavings); void resetAdjustScheduleforDaylightSavings(); + openstudio::TimeSeries timeSeries() const; + bool setTimeSeries(const openstudio::TimeSeries& timeSeries); + + // Extra setters/getters + boost::optional csvFile() const; + // bool translateFileWithRelativePath() const; + // bool isTranslateFileWithRelativePathDefaulted() const; + // void ensureNoLeapDays(); + // bool setTranslateFileWithRelativePath(bool translateFileWithRelativePath); + // void resetTranslateFileWithRelativePath(); + openstudio::path translatedFilePath() const; + std::vector columnSeparatorValues() const; std::vector minutesperItemValues() const; diff --git a/src/epmodel/test/ScheduleFile_GTest.cpp b/src/epmodel/test/ScheduleFile_GTest.cpp index da192e9caa..cf6383a1f8 100644 --- a/src/epmodel/test/ScheduleFile_GTest.cpp +++ b/src/epmodel/test/ScheduleFile_GTest.cpp @@ -7,7 +7,12 @@ #include "EPModelFixture.hpp" #include "../ScheduleInterval/ScheduleFile.hpp" +#include "../ScheduleInterval/ScheduleFile_Impl.hpp" +#include "../../utilities/data/TimeSeries.hpp" +#include "../../utilities/core/StringStreamLogSink.hpp" + +using namespace openstudio; using namespace openstudio::epmodel; TEST_F(EPModelFixture, ScheduleFile_DefaultConstructor) { @@ -47,23 +52,343 @@ TEST_F(EPModelFixture, ScheduleFile_ScalarAccessors_RoundTrip) { schedule.resetInterpolatetoTimestep(); EXPECT_FALSE(schedule.interpolatetoTimestep()); - EXPECT_TRUE(schedule.minutesperItem()); - EXPECT_EQ("60", schedule.minutesperItem().get()); + EXPECT_EQ(60, schedule.minutesperItem()); EXPECT_TRUE(schedule.setMinutesperItem(15)); - ASSERT_TRUE(schedule.minutesperItem()); - EXPECT_EQ("15", schedule.minutesperItem().get()); + EXPECT_EQ(15, schedule.minutesperItem()); EXPECT_TRUE(schedule.setMinutesperItem("30")); - ASSERT_TRUE(schedule.minutesperItem()); - EXPECT_EQ("30", schedule.minutesperItem().get()); + EXPECT_EQ(30, schedule.minutesperItem()); EXPECT_FALSE(schedule.setMinutesperItem("BAD")); schedule.resetMinutesperItem(); EXPECT_TRUE(schedule.isMinutesperItemDefaulted()); - ASSERT_TRUE(schedule.minutesperItem()); - EXPECT_EQ("60", schedule.minutesperItem().get()); + EXPECT_EQ(60, schedule.minutesperItem()); EXPECT_TRUE(schedule.adjustScheduleforDaylightSavings()); EXPECT_TRUE(schedule.setAdjustScheduleforDaylightSavings(false)); EXPECT_FALSE(schedule.adjustScheduleforDaylightSavings()); schedule.resetAdjustScheduleforDaylightSavings(); EXPECT_TRUE(schedule.adjustScheduleforDaylightSavings()); + + boost::optional csvFile = schedule.csvFile(); + EXPECT_FALSE(csvFile); +} + +TEST_F(EPModelFixture, ScheduleFile_AltCtor) { + Model model; + // EXPECT_EQ(0u, model.getConcreteModelObjects().size()); + EXPECT_EQ(0u, model.getConcreteModelObjects().size()); + + path p = resourcesPath() / toPath("model/schedulefile.csv"); + EXPECT_TRUE(exists(p)); + +/* path expectedDestDir; + std::vector absoluteFilePaths = model.workflowJSON().absoluteFilePaths(); + if (absoluteFilePaths.empty()) { + expectedDestDir = model.workflowJSON().absoluteRootDir(); + } else { + expectedDestDir = absoluteFilePaths[0]; + } + + if (exists(expectedDestDir)) { + removeDirectory(expectedDestDir); + } + ASSERT_FALSE(exists(expectedDestDir)); */ + + ScheduleFile schedule(model, openstudio::toString(p)); + EXPECT_EQ(1u, model.getConcreteModelObjects().size()); +/* EXPECT_EQ(1u, model.getConcreteModelObjects().size()); + ExternalFile externalfile = schedule.externalFile(); + EXPECT_EQ(1u, externalfile.scheduleFiles().size()); + EXPECT_EQ(openstudio::toString(p), externalfile.fileName()); + EXPECT_TRUE(externalfile.isColumnSeparatorDefaulted()); + EXPECT_FALSE(equivalent(expectedDestDir / externalfile.fileName(), externalfile.filePath())); + EXPECT_TRUE(exists(externalfile.filePath())); + EXPECT_EQ(p, externalfile.filePath()); */ + EXPECT_TRUE(schedule.isNumberofHoursofDataDefaulted()); + EXPECT_EQ(1, schedule.columnNumber()); + schedule.setColumnNumber(1); + EXPECT_EQ(1, schedule.columnNumber()); + EXPECT_EQ(0, schedule.rowstoSkipatTop()); + schedule.setRowstoSkipatTop(1); + EXPECT_EQ(1, schedule.rowstoSkipatTop()); + + EXPECT_EQ("Comma", schedule.columnSeparator()); + EXPECT_TRUE(schedule.isColumnSeparatorDefaulted()); + EXPECT_TRUE(schedule.setColumnSeparator("Tab")); + EXPECT_EQ("Tab", schedule.columnSeparator()); + + ScheduleFile schedule2(model, openstudio::toString(p)); + EXPECT_EQ(2u, model.getConcreteModelObjects().size()); +/* EXPECT_EQ(1u, model.getConcreteModelObjects().size()); + ExternalFile externalfile2 = schedule2.externalFile(); + EXPECT_EQ(2u, externalfile.scheduleFiles().size()); + EXPECT_EQ(externalfile.handle(), externalfile2.handle()); */ + EXPECT_TRUE(schedule2.isNumberofHoursofDataDefaulted()); + EXPECT_EQ(1, schedule2.columnNumber()); + schedule2.setColumnNumber(2); + EXPECT_EQ(2, schedule2.columnNumber()); + EXPECT_EQ(0, schedule2.rowstoSkipatTop()); + schedule2.setRowstoSkipatTop(1); + EXPECT_EQ(1, schedule2.rowstoSkipatTop()); + + openstudio::path filePath = schedule2.getImpl()->fileName(); + ASSERT_TRUE(exists(filePath)); + EXPECT_EQ("schedulefile.csv", filePath.filename()); + + schedule2.remove(); + EXPECT_EQ(1u, model.getConcreteModelObjects().size()); +} + +TEST_F(EPModelFixture, ScheduleFile_ExtraSettersGetters) { + Model model; + + path p = toPath("resources/model/schedulefile.csv"); // relative path + EXPECT_TRUE(exists(p)); + + bool translateFileWithRelativePath = false; + ScheduleFile schedule(model, openstudio::toString(p), 1, 0, translateFileWithRelativePath); + + boost::optional csvFile = schedule.csvFile(); + ASSERT_TRUE(csvFile); + EXPECT_EQ(3u, csvFile->numColumns()); + EXPECT_EQ(8761u, csvFile->numRows()); + EXPECT_FALSE(schedule.translateFileWithRelativePath()); + EXPECT_FALSE(schedule.isTranslateFileWithRelativePathDefaulted()); + EXPECT_FALSE(schedule.setTranslateFileWithRelativePath(true)); + schedule.resetTranslateFileWithRelativePath(); + openstudio::path filePath = schedule.getImpl()->fileName(); + ASSERT_TRUE(exists(filePath)); + EXPECT_NE(p, filePath); + EXPECT_NE(p, schedule.translatedFilePath()); + + translateFileWithRelativePath = true; + ScheduleFile schedule2(model, openstudio::toString(p), 1, 0, translateFileWithRelativePath); + + boost::optional csvFile2 = schedule2.csvFile(); + ASSERT_TRUE(csvFile2); + EXPECT_EQ(3u, csvFile2->numColumns()); + EXPECT_EQ(8761u, csvFile2->numRows()); + EXPECT_FALSE(schedule2.translateFileWithRelativePath()); + EXPECT_FALSE(schedule2.isTranslateFileWithRelativePathDefaulted()); + EXPECT_FALSE(schedule2.setTranslateFileWithRelativePath(true)); + schedule2.resetTranslateFileWithRelativePath(); + openstudio::path filePath2 = schedule2.getImpl()->fileName(); + ASSERT_TRUE(exists(filePath2)); + EXPECT_EQ(p, filePath2); + EXPECT_EQ(p, schedule2.translatedFilePath()); +} + +TEST_F(EPModelFixture, ScheduleFile_CheckCannotFindFile) { + Model model; + + StringStreamLogSink sink; + sink.setLogLevel(Warn); + + path p = resourcesPath() / toPath("model/schedulefile2.csv"); + EXPECT_FALSE(exists(p)); + EXPECT_THROW(ScheduleFile(model, openstudio::toString(p)), openstudio::Exception); + EXPECT_EQ(1, sink.logMessages().size()); + EXPECT_EQ("openstudio.epmodel.ScheduleFile", sink.logMessages().front().logChannel()); + EXPECT_TRUE(sink.logMessages().front().logMessage().find("Cannot find file") != std::string::npos); + EXPECT_TRUE(sink.logMessages().front().logMessage().find("resources/model/schedulefile2.csv\" for Object of type 'Schedule:File' and named 'Schedule File 1'") != std::string::npos); + + path p2 = resourcesPath() / toPath("model/schedulefile.csv"); + EXPECT_TRUE(exists(p2)); + ScheduleFile schedule(model, openstudio::toString(p2)); + EXPECT_TRUE(schedule.getImpl()->setFileName(toString(p))); + + StringStreamLogSink sink2; + sink2.setLogLevel(Warn); + + EXPECT_EQ(p, schedule.translatedFilePath()); + EXPECT_EQ(1, sink2.logMessages().size()); + EXPECT_EQ("openstudio.epmodel.ScheduleFile", sink2.logMessages().front().logChannel()); + EXPECT_TRUE(sink2.logMessages().front().logMessage().find("Cannot find file") != std::string::npos); + EXPECT_TRUE(sink2.logMessages().front().logMessage().find("resources/model/schedulefile2.csv\"") != std::string::npos); +} + +TEST_F(EPModelFixture, ScheduleFile_fromTimeSeries_intervalLengthYes) { + Model model; + + // hourly + Date startDate(MonthOfYear::Jan, 1); + Time intervalLength(0, 0, 60); + Vector values(8760); + for (unsigned i = 0; i < values.size(); ++i) { + values[i] = i % 24; + } + + TimeSeries timeSeries(startDate, intervalLength, values, ""); + + boost::optional schedule = ScheduleFile::fromTimeSeries(timeSeries, model); + ASSERT_TRUE(schedule); + openstudio::path filePath = schedule->getImpl()->fileName(); + ASSERT_TRUE(exists(filePath)); + EXPECT_EQ("Schedule File 1.csv", filePath.filename()); + EXPECT_EQ(2, schedule->columnNumber()); + EXPECT_EQ(0, schedule->rowstoSkipatTop()); + ASSERT_TRUE(schedule->numberofHoursofData()); + EXPECT_EQ(8760, schedule->numberofHoursofData().get()); + EXPECT_EQ("Comma", schedule->columnSeparator()); + EXPECT_FALSE(schedule->interpolatetoTimestep()); + EXPECT_EQ(60, schedule->minutesperItem()); + EXPECT_TRUE(schedule->adjustScheduleforDaylightSavings()); + + // 15-minutely + Time intervalLength2(0, 0, 15); + Vector values2(4 * 8760); + for (unsigned i = 0; i < values2.size(); ++i) { + values2[i] = i % 24; + } + + TimeSeries timeSeries2(startDate, intervalLength2, values2, ""); + + boost::optional schedule2 = ScheduleFile::fromTimeSeries(timeSeries2, model); + ASSERT_TRUE(schedule2); + openstudio::path filePath2 = schedule2->getImpl()->fileName(); + ASSERT_TRUE(exists(filePath2)); + EXPECT_EQ("Schedule File 2.csv", filePath2.filename()); + EXPECT_EQ(2, schedule2->columnNumber()); + EXPECT_EQ(0, schedule2->rowstoSkipatTop()); + ASSERT_TRUE(schedule2->numberofHoursofData()); + EXPECT_EQ(8760, schedule2->numberofHoursofData().get()); + EXPECT_EQ("Comma", schedule2->columnSeparator()); + EXPECT_FALSE(schedule2->interpolatetoTimestep()); + ASSERT_TRUE(schedule2->minutesperItem()); + EXPECT_EQ(15, schedule2->minutesperItem()); + EXPECT_TRUE(schedule2->adjustScheduleforDaylightSavings()); +} + +TEST_F(EPModelFixture, ScheduleFile_fromTimeSeries_intervalLengthNo) { + Model model; + + StringStreamLogSink sink; + sink.setLogLevel(Warn); + + Date startDate(MonthOfYear::Jan, 1); + Time intervalLength(0, 0, 60); + std::vector dateTimes; + Vector values(8760); + for (unsigned i = 0; i < values.size(); ++i) { + dateTimes.push_back(DateTime(startDate, intervalLength * (i + 1))); + values[i] = i % 24; + } + + TimeSeries timeSeries(dateTimes, values, ""); + + boost::optional schedule = ScheduleFile::fromTimeSeries(timeSeries, model); + ASSERT_FALSE(schedule); + EXPECT_EQ(1, sink.logMessages().size()); + EXPECT_EQ("openstudio.epmodel.ScheduleFile", sink.logMessages().front().logChannel()); + EXPECT_EQ("Timeseries does not have an interval length defined, but ScheduleVariableInterval is deprecated", sink.logMessages().front().logMessage()); +} + +TEST_F(EPModelFixture, ScheduleFile_timeSeries) { + Model model; + + StringStreamLogSink sink; + sink.setLogLevel(Warn); + + ScheduleFile schedule(model); + EXPECT_EQ(0, sink.logMessages().size()); + + EXPECT_EQ("", schedule.fileName()); + EXPECT_EQ(0, sink.logMessages().size()); + boost::optional csvFile = schedule.csvFile(); + EXPECT_FALSE(csvFile); + EXPECT_EQ(1, sink.logMessages().size()); + EXPECT_EQ("openstudio.CSVFile", sink.logMessages()[0].logChannel()); + EXPECT_EQ("Path '' is not a CSVFile file", sink.logMessages()[0].logMessage()); + EXPECT_THROW(schedule.timeSeries(), openstudio::Exception); + EXPECT_EQ(3, sink.logMessages().size()); + EXPECT_EQ("openstudio.CSVFile", sink.logMessages()[1].logChannel()); + EXPECT_EQ("Path '' is not a CSVFile file", sink.logMessages()[1].logMessage()); + EXPECT_EQ("openstudio.epmodel.ScheduleFile", sink.logMessages()[2].logChannel()); + EXPECT_EQ("Did not set File Name for Object of type 'Schedule:File' and named 'Schedule File 1'", sink.logMessages()[2].logMessage()); + + path p = resourcesPath() / toPath("model/schedulefile.csv"); + EXPECT_TRUE(exists(p)); + + int columnNumber = 1; + const int rowsToSkip = 1; + ScheduleFile schedule2(model, openstudio::toString(p), columnNumber, rowsToSkip); + openstudio::TimeSeries timeSeries; + boost::optional intervalTime; + double intervalLengthDouble; + DateTimeVector dateTimes; + DateTime firstReportDateTime; + DateTime firstDateTime; + DateTime secondDateTime; + DateTime lastDateTime; + + timeSeries = schedule2.timeSeries(); + intervalTime = timeSeries.intervalLength(); + ASSERT_TRUE(intervalTime); + intervalLengthDouble = intervalTime->totalMinutes(); + EXPECT_EQ(intervalLengthDouble, schedule2.minutesperItem()); + dateTimes = timeSeries.dateTimes(); + EXPECT_EQ(8760, dateTimes.size()); + EXPECT_EQ(8760, timeSeries.values().size()); + firstReportDateTime = DateTime(Date(MonthOfYear::Jan, 1), openstudio::Time(0, 0, schedule2.minutesperItem())); + EXPECT_EQ(firstReportDateTime, timeSeries.firstReportDateTime()); + firstDateTime = DateTime(Date(MonthOfYear::Jan, 1), openstudio::Time(0, 0, schedule2.minutesperItem())); + EXPECT_EQ(firstDateTime, dateTimes[0]); + secondDateTime = firstDateTime + openstudio::Time(0, 0, schedule2.minutesperItem()); + EXPECT_EQ(secondDateTime, dateTimes[1]); + lastDateTime = DateTime(Date(MonthOfYear::Dec, 31), openstudio::Time(0, 24)); + EXPECT_EQ(lastDateTime, dateTimes.back()); + EXPECT_EQ(1, timeSeries.values(0)); + EXPECT_EQ(8760, timeSeries.values(8759)); + + columnNumber = 2; + schedule2.setColumnNumber(columnNumber); + timeSeries = schedule2.timeSeries(); + intervalTime = timeSeries.intervalLength(); + ASSERT_TRUE(intervalTime); + intervalLengthDouble = intervalTime->totalMinutes(); + EXPECT_EQ(intervalLengthDouble, schedule2.minutesperItem()); + dateTimes = timeSeries.dateTimes(); + EXPECT_EQ(8760, dateTimes.size()); + EXPECT_EQ(8760, timeSeries.values().size()); + firstReportDateTime = DateTime(Date(MonthOfYear::Jan, 1), openstudio::Time(0, 0, schedule2.minutesperItem())); + EXPECT_EQ(firstReportDateTime, timeSeries.firstReportDateTime()); + firstDateTime = DateTime(Date(MonthOfYear::Jan, 1), openstudio::Time(0, 0, schedule2.minutesperItem())); + EXPECT_EQ(firstDateTime, dateTimes[0]); + secondDateTime = firstDateTime + openstudio::Time(0, 0, schedule2.minutesperItem()); + EXPECT_EQ(secondDateTime, dateTimes[1]); + lastDateTime = DateTime(Date(MonthOfYear::Dec, 31), openstudio::Time(0, 24)); + EXPECT_EQ(lastDateTime, dateTimes.back()); + EXPECT_EQ(8759, timeSeries.values(0)); + EXPECT_EQ(0, timeSeries.values(8759)); + + StringStreamLogSink sink2; + sink2.setLogLevel(Warn); + + columnNumber = 3; + schedule2.setColumnNumber(columnNumber); + schedule2.setMinutesperItem(15); // this trips the error checking + timeSeries = schedule2.timeSeries(); + intervalTime = timeSeries.intervalLength(); + ASSERT_TRUE(intervalTime); + intervalLengthDouble = intervalTime->totalMinutes(); + EXPECT_EQ(intervalLengthDouble, schedule2.minutesperItem()); + dateTimes = timeSeries.dateTimes(); + EXPECT_EQ(8760, dateTimes.size()); + EXPECT_EQ(8760, timeSeries.values().size()); + firstReportDateTime = DateTime(Date(MonthOfYear::Jan, 1), openstudio::Time(0, 0, schedule2.minutesperItem())); + EXPECT_EQ(firstReportDateTime, timeSeries.firstReportDateTime()); + firstDateTime = DateTime(Date(MonthOfYear::Jan, 1), openstudio::Time(0, 0, schedule2.minutesperItem())); + EXPECT_EQ(firstDateTime, dateTimes[0]); + secondDateTime = firstDateTime + openstudio::Time(0, 0, schedule2.minutesperItem()); + EXPECT_EQ(secondDateTime, dateTimes[1]); + lastDateTime = DateTime(Date(MonthOfYear::Apr, 2), openstudio::Time(0, 6)); + EXPECT_EQ(lastDateTime, dateTimes.back()); + EXPECT_EQ(0.207618, timeSeries.values(0)); + EXPECT_EQ(0.62146, timeSeries.values(8759)); + + EXPECT_EQ(1, sink2.logMessages().size()); + EXPECT_EQ("openstudio.epmodel.ScheduleFile", sink2.logMessages().front().logChannel()); + EXPECT_TRUE(sink2.logMessages().front().logMessage().find("With 15 minutes per item, last date time is 2009-Apr-02 06:00:00") != std::string::npos); + EXPECT_TRUE(sink2.logMessages().front().logMessage().find("resources/model/schedulefile.csv referenced by Object of type 'Schedule:File' and named 'Schedule File 2'") != std::string::npos); + + // TODO: try setting a leap year }