Beispiel #1
0
void FloodPlot::timeseriesData(TimeSeries tsData)
{
  if (tsData.values().empty()){
    return;
  }

  m_startDateTime = tsData.firstReportDateTime();
  m_endDateTime = tsData.firstReportDateTime() + Time(tsData.daysFromFirstReport(tsData.daysFromFirstReport().size()-1));
  m_duration = (m_endDateTime-m_startDateTime).totalDays();
  m_xAxisMin = 0.0;
  m_xAxisMax = m_duration;
  if (m_plot2DTimeAxis == NULL) 
  {
    m_plot2DTimeAxis = new Plot2DTimeAxis(m_startDateTime, m_duration);
    m_qwtPlot->setAxisTitle(QwtPlot::xBottom, " Simulation Time");
    m_qwtPlot->setAxisScale(QwtPlot::xBottom, 0, m_duration);
    m_qwtPlot->setAxisScaleDraw(QwtPlot::xBottom, m_plot2DTimeAxis);
    m_qwtPlot->setAxisLabelRotation(QwtPlot::xBottom, -90.0);
    m_qwtPlot->setAxisLabelAlignment(QwtPlot::xBottom, Qt::AlignLeft | Qt::AlignBottom);
  } 
  else 
  {
    m_plot2DTimeAxis->startDateTime(m_startDateTime);
    m_plot2DTimeAxis->duration(m_duration);
  }

  TimeSeriesFloodPlotData::Ptr data = TimeSeriesFloodPlotData::create(tsData);
  floodPlotData(data);
}
Beispiel #2
0
TimeSeriesFloodPlotData::TimeSeriesFloodPlotData(TimeSeries timeSeries,  QwtDoubleInterval colorMapRange)
: m_timeSeries(timeSeries),
  m_minValue(minimum(timeSeries.values())),
  m_maxValue(maximum(timeSeries.values())),
  m_minX(timeSeries.firstReportDateTime().date().dayOfYear()),
  m_maxX(ceil(timeSeries.daysFromFirstReport()[timeSeries.daysFromFirstReport().size()-1]+timeSeries.firstReportDateTime().date().dayOfYear()+timeSeries.firstReportDateTime().time().totalDays())), // end day
  m_minY(0), // start hour
  m_maxY(24), // end hour
  m_startFractionalDay(timeSeries.firstReportDateTime().date().dayOfYear()+timeSeries.firstReportDateTime().time().totalDays()),
  m_colorMapRange(colorMapRange)
{
  // data range
  setBoundingRect(QwtDoubleRect(m_minX, m_minY, m_maxX-m_minX, m_maxY-m_minY));
}
Beispiel #3
0
TimeSeriesLinePlotData::TimeSeriesLinePlotData(TimeSeries timeSeries, double fracDaysOffset)
: m_timeSeries(timeSeries),
  m_minX(timeSeries.firstReportDateTime().date().dayOfYear()+timeSeries.firstReportDateTime().time().totalDays()),
  m_maxX(timeSeries.daysFromFirstReport()[timeSeries.daysFromFirstReport().size()-1]+timeSeries.firstReportDateTime().date().dayOfYear()+timeSeries.firstReportDateTime().time().totalDays()), // end day
  m_minY(minimum(timeSeries.values())),
  m_maxY(maximum(timeSeries.values())),
  m_size(timeSeries.values().size())
{
  m_boundingRect = QwtDoubleRect(m_minX, m_minY, (m_maxX - m_minX), (m_maxY - m_minY));
  m_minValue = m_minY;
  m_maxValue = m_maxY;
  m_units = timeSeries.units();
  m_fracDaysOffset = fracDaysOffset; // note updating in xValue does not affect scaled axis
  m_x = m_timeSeries.daysFromFirstReport();
  m_y = m_timeSeries.values();
}
Beispiel #4
0
void LinePlot::timeseriesData(TimeSeries tsData, const std::string& name, QColor color)
{
  if (tsData.values().empty()){
    return;
  }

  double offset=0.0;
  if (m_plot2DTimeAxis == NULL) 
  {
    m_startDateTime = tsData.firstReportDateTime();
    m_endDateTime = tsData.firstReportDateTime() + Time(tsData.daysFromFirstReport(tsData.daysFromFirstReport().size()-1));
    m_duration = (m_endDateTime-m_startDateTime).totalDays();
    m_plot2DTimeAxis = new Plot2DTimeAxis(m_startDateTime, m_duration);
    m_qwtPlot->setAxisTitle(QwtPlot::xBottom, " Simulation Time");
    m_qwtPlot->setAxisScaleDraw(QwtPlot::xBottom, m_plot2DTimeAxis);
    m_xAxisMin = 0.0;
    m_xAxisMax = m_duration;
    m_qwtPlot->setAxisScale(QwtPlot::xBottom, 0, m_duration);
    m_qwtPlot->setAxisLabelRotation(QwtPlot::xBottom, -90.0);
    m_qwtPlot->setAxisLabelAlignment(QwtPlot::xBottom, Qt::AlignLeft | Qt::AlignBottom);
  } 
  else 
  {
    if (tsData.firstReportDateTime() < m_startDateTime) {
      m_xAxisMin = (tsData.firstReportDateTime() - m_startDateTime).totalDays();
      offset = m_xAxisMin;
    } else {
      offset = (tsData.firstReportDateTime() - m_startDateTime).totalDays();
    }

    if ((tsData.firstReportDateTime() + Time(tsData.daysFromFirstReport(tsData.daysFromFirstReport().size()-1))) > m_endDateTime) {
      m_xAxisMax += ((tsData.firstReportDateTime() + Time(tsData.daysFromFirstReport(tsData.daysFromFirstReport().size()-1))) - m_endDateTime).totalDays();
    }
  }

  TimeSeriesLinePlotData::Ptr data = TimeSeriesLinePlotData::create(tsData, offset);
  linePlotData(data, name, color, offset);
}
TEST_F(DataFixture,TimeSeries_AddSubtractSameTimePeriod)
{
  std::string units = "W";

  Date startDate(Date(MonthOfYear(MonthOfYear::Feb),21));
  DateTime startDateTime(startDate, Time(0,1,0,0));

  // interval
  Time interval = Time(0,1,0,0);
  Vector intervalValues(3);
  intervalValues(0) = 0;
  intervalValues(1) = 1;
  intervalValues(2) = 2;

  TimeSeries intervalTimeSeries(startDateTime, interval, intervalValues, units);
  ASSERT_TRUE(!intervalTimeSeries.values().empty());

  // detailed
  DateTimeVector dateTimes;
  dateTimes.push_back(startDateTime + Time(0,0,0,0));
  dateTimes.push_back(startDateTime + Time(0,0,30,0));
  dateTimes.push_back(startDateTime + Time(0,1,0,0));
  dateTimes.push_back(startDateTime + Time(0,1,30,0));
  dateTimes.push_back(startDateTime + Time(0,2,0,0));
  Vector detailedValues(5);
  detailedValues(0) = 0.0; // 1:00
  detailedValues(1) = 0.5; // 1:30
  detailedValues(2) = 1.0; // 2:00
  detailedValues(3) = 1.5; // 2:30
  detailedValues(4) = 2.0; // 3:00

  TimeSeries detailedTimeSeries(dateTimes, detailedValues, units);
  ASSERT_TRUE(!detailedTimeSeries.values().empty());

  // sum and difference
  TimeSeries sum = intervalTimeSeries + detailedTimeSeries;
  TimeSeries diff1 = intervalTimeSeries - detailedTimeSeries;
  TimeSeries diff2 = detailedTimeSeries - intervalTimeSeries;
  ASSERT_TRUE(!sum.values().empty());
  ASSERT_TRUE(!diff1.values().empty());
  ASSERT_TRUE(!diff2.values().empty());

//  EXPECT_EQ((unsigned)5, sum.dateTimes().size());
//  EXPECT_EQ((unsigned)5, diff1.dateTimes().size());
//  EXPECT_EQ((unsigned)5, diff2.dateTimes().size());
  EXPECT_EQ((unsigned)5, sum.daysFromFirstReport().size());
  EXPECT_EQ((unsigned)5, diff1.daysFromFirstReport().size());
  EXPECT_EQ((unsigned)5, diff2.daysFromFirstReport().size());

//  EXPECT_EQ(startDateTime, sum.dateTimes().front());
//  EXPECT_EQ(startDateTime, diff1.dateTimes().front());
//  EXPECT_EQ(startDateTime, diff2.dateTimes().front());
  EXPECT_EQ(startDateTime, sum.firstReportDateTime());
  EXPECT_EQ(startDateTime, diff1.firstReportDateTime());
  EXPECT_EQ(startDateTime, diff2.firstReportDateTime());

  DateTime endDateTime = startDateTime + Time(0,2,0,0);
//  EXPECT_EQ(endDateTime, sum.dateTimes().back());
//  EXPECT_EQ(endDateTime, diff1.dateTimes().back());
//  EXPECT_EQ(endDateTime, diff2.dateTimes().back());
  EXPECT_EQ(endDateTime, sum.firstReportDateTime() + Time(sum.daysFromFirstReport(sum.daysFromFirstReport().size()-1)));
  EXPECT_EQ(endDateTime, diff1.firstReportDateTime() +  Time(diff1.daysFromFirstReport(diff1.daysFromFirstReport().size()-1)));
  EXPECT_EQ(endDateTime, diff2.firstReportDateTime() +  Time(diff2.daysFromFirstReport(diff2.daysFromFirstReport().size()-1)));

  // 1:00
  EXPECT_EQ(0, sum.value(Time(0,0,0,0)));
  EXPECT_EQ(0, diff1.value(Time(0,0,0,0)));
  EXPECT_EQ(0, diff2.value(Time(0,0,0,0)));

  // 1:30
  EXPECT_EQ(1.5, sum.value(Time(0,0,30,0)));
  EXPECT_EQ(0.5, diff1.value(Time(0,0,30,0)));
  EXPECT_EQ(-0.5, diff2.value(Time(0,0,30,0)));

  // 2:00
  EXPECT_EQ(2, sum.value(Time(0,1,0,0)));
  EXPECT_EQ(0.0, diff1.value(Time(0,1,0,0)));
  EXPECT_EQ(0.0, diff2.value(Time(0,1,0,0)));

  // 2:30
  EXPECT_EQ(3.5, sum.value(Time(0,1,30,0)));
  EXPECT_EQ(0.5, diff1.value(Time(0,1,30,0)));
  EXPECT_EQ(-0.5, diff2.value(Time(0,1,30,0)));

  // Test helper function for summing a vector.
  TimeSeriesVector sumAndDiffs;
  sumAndDiffs.push_back(sum);
  sumAndDiffs.push_back(diff1);
  sumAndDiffs.push_back(diff2);

  TimeSeries ans = openstudio::sum(sumAndDiffs);
  EXPECT_FALSE(ans.values().empty());
  // 1:00
  EXPECT_DOUBLE_EQ(0, ans.value(Time(0,0,0,0)));
  // 1:30
  EXPECT_DOUBLE_EQ(1.5, ans.value(Time(0,0,30,0)));
  // 2:00
  EXPECT_DOUBLE_EQ(2.0, ans.value(Time(0,1,0,0)));
  // 2:30
  EXPECT_DOUBLE_EQ(3.5, ans.value(Time(0,1,30,0)));

  // Test multiplication and division with a scalar
  sumAndDiffs.push_back(sum/2.0);
  sumAndDiffs.push_back(3.0*diff1);
  ans = openstudio::sum(sumAndDiffs);
  EXPECT_FALSE(ans.values().empty());
  // 1:00
  EXPECT_DOUBLE_EQ(0, ans.value(Time(0,0,0,0)));
  // 1:30
  EXPECT_DOUBLE_EQ(3.75, ans.value(Time(0,0,30,0)));
  // 2:00
  EXPECT_DOUBLE_EQ(3.0, ans.value(Time(0,1,0,0)));
  // 2:30
  EXPECT_DOUBLE_EQ(6.75, ans.value(Time(0,1,30,0)));
}
boost::optional<IdfObject> ForwardTranslator::translateScheduleVariableInterval( ScheduleVariableInterval & modelObject )
{
  IdfObject idfObject( openstudio::IddObjectType::Schedule_Compact );

  m_idfObjects.push_back(idfObject);

  idfObject.setName(modelObject.name().get());

  boost::optional<ScheduleTypeLimits> scheduleTypeLimits = modelObject.scheduleTypeLimits();
  if (scheduleTypeLimits){
    boost::optional<IdfObject> idfScheduleTypeLimits = translateAndMapModelObject(*scheduleTypeLimits);
    if (idfScheduleTypeLimits){
      idfObject.setString(Schedule_CompactFields::ScheduleTypeLimitsName, idfScheduleTypeLimits->name().get());
    }
  }

  TimeSeries timeseries = modelObject.timeSeries();
  // Check that the time series has at least one point
  if(timeseries.values().size() == 0)
  {
    LOG(Error,"Time series in schedule '" << modelObject.name().get() << "' has no values, schedule will not be translated");
    return boost::optional<IdfObject>();
  }
  DateTime firstReportDateTime = timeseries.firstReportDateTime();
  Vector daysFromFirst = timeseries.daysFromFirstReport();
  std::vector<long> secondsFromFirst = timeseries.secondsFromFirstReport();
  Vector values = timeseries.values();

  // We aren't using this - should we?
  std::string interpolateField = "Interpolate:No";
  if (modelObject.interpolatetoTimestep()){
    interpolateField = "Interpolate:Yes";
  }

  // New version assumes that the interval is less than one day.
  // The original version did not, so it was a bit more complicated.
  // The last date data was written
  Date lastDate = firstReportDateTime.date();
  Time dayDelta = Time(1.0);
  // The day number of the date that data was last written relative to the first date
  //double lastDay = 0.0;
  int lastDay = 0;
  // Adjust the floating point day delta to be relative to the beginning of the first day and
  // shift the start of the loop if needed
  int secondShift = firstReportDateTime.time().totalSeconds();
  unsigned int start = 0;
  if(secondShift == 0) {
    start = 1;
  } else {
    for(unsigned int i=0;i<secondsFromFirst.size();i++) {
      secondsFromFirst[i] += secondShift;
    }
  }

  // Start the input into the schedule object
  unsigned fieldIndex = Schedule_CompactFields::ScheduleTypeLimitsName + 1;
  //idfObject.setString(fieldIndex, interpolateField);
  //++fieldIndex;
  fieldIndex = startNewDay(idfObject,fieldIndex,lastDate);

  for(unsigned int i=start; i < values.size()-1; i++){
    // Loop over the time series values and write out values to the
    // schedule. This version is based on the seconds from the start
    // of the time series, so should not be vulnerable to round-off.
    // It was translated from the day version, so there could be
    // issues associated with that.
    //
    // We still have a potential aliasing problem unless the API has
    // enforced that the times in the time series are all distinct when
    // rounded to the minute. Is that happening?
    int secondsFromStartOfDay = secondsFromFirst[i] % 86400;
    int today = (secondsFromFirst[i]-secondsFromStartOfDay)/86400;
    // Check to see if we are at the end of a day.
    if(secondsFromStartOfDay==0 || secondsFromStartOfDay==86400) {
      // This value is an end of day value, so end the day and set up the next
      // Note that 00:00:00 counts as the end of the previous day - we only write
      // out the 24:00:00 value and not both.
      fieldIndex = addUntil(idfObject,fieldIndex,24,0,values[i]);
      lastDate += dayDelta;
      fieldIndex = startNewDay(idfObject,fieldIndex,lastDate);
    } else {
      // This still could be on a different day
      if(today != lastDay) {
        // We're on a new day, need a 24:00:00 value and set up the next day
        fieldIndex = addUntil(idfObject,fieldIndex,24,0,values[i]);
        lastDate += dayDelta;
        fieldIndex = startNewDay(idfObject,fieldIndex,lastDate);
      }
      if(values[i] == values[i+1]){
        // Bail on values that match the next value
        continue;
      }
      // Write out the current entry
      Time time(0,0,0,secondsFromStartOfDay);
      int hours = time.hours();
      int minutes = time.minutes() + floor((time.seconds()/60.0) + 0.5);
      // This is a little dangerous, but all of the problematic 24:00
      // times that might need to cause a day++ should be caught above.
      if(minutes==60){
        hours += 1;
        minutes = 0;
      }
      fieldIndex = addUntil(idfObject,fieldIndex,hours,minutes,values[i]);
    }
    lastDay = today;
  }
  // Handle the last point a little differently to make sure that the schedule ends exactly on the end of a day
  unsigned int i = values.size()-1;
  // We'll skip a sanity check here, but it might be a good idea to add one at some point
  fieldIndex = addUntil(idfObject,fieldIndex,24,0,values[i]);

  return idfObject;
}
boost::optional<IdfObject> ForwardTranslator::translateScheduleFixedInterval( ScheduleFixedInterval & modelObject )
{
  IdfObject idfObject( openstudio::IddObjectType::Schedule_Compact );

  m_idfObjects.push_back(idfObject);

  idfObject.setName(modelObject.name().get());
  
  boost::optional<ScheduleTypeLimits> scheduleTypeLimits = modelObject.scheduleTypeLimits();
  if (scheduleTypeLimits){
    boost::optional<IdfObject> idfScheduleTypeLimits = translateAndMapModelObject(*scheduleTypeLimits);
    if (idfScheduleTypeLimits){
      idfObject.setString(Schedule_CompactFields::ScheduleTypeLimitsName, idfScheduleTypeLimits->name().get());
    }
  }

  TimeSeries timeseries = modelObject.timeSeries();
  // Check that the time series has at least one point
  if(timeseries.values().size() == 0)
  {
    LOG(Error,"Time series in schedule '" << modelObject.name().get() << "' has no values, schedule will not be translated");
    return boost::optional<IdfObject>();
  }
  DateTime firstReportDateTime = timeseries.firstReportDateTime();
  Vector daysFromFirst = timeseries.daysFromFirstReport();
  Vector values = timeseries.values();

  // We aren't using this - should we?
  std::string interpolateField;
  if (modelObject.interpolatetoTimestep()){
    interpolateField = "Interpolate:Yes";
  }else{
    interpolateField = "Interpolate:No";
  }

  // New version assumes that the interval is less than one day.
  // The original version did not, so it was a bit more complicated.
  // 5.787x10^-6 days is a little less than half a second
  double eps = 5.787e-6;
  double intervalDays = modelObject.intervalLength();
  // The last date data was written
  Date lastDate = firstReportDateTime.date();
  Time dayDelta = Time(1.0);
  // The day number of the date that data was last written relative to the first date
  double lastDay = 0.0; 
  // Adjust the floating point day delta to be relative to the beginning of the first day and
  // shift the start of the loop if needed
  double timeShift = firstReportDateTime.time().totalDays();
  unsigned int start = 0;
  if(timeShift == 0.0)
  {
    start = 1;
  }
  else
  {
    for(unsigned int i=0;i<daysFromFirst.size();i++)
    {
      daysFromFirst[i] += timeShift;
    }
  }

  // Start the input into the schedule object
  unsigned fieldIndex = Schedule_CompactFields::ScheduleTypeLimitsName + 1;
  fieldIndex = startNewDay(idfObject,fieldIndex,lastDate);

  for(unsigned int i=start; i < values.size()-1; i++)
  {
    // We could loop over the entire array and use the fact that the
    // last entry in the daysFromFirstReport vector should be a round
    // number to avoid logic. However, this whole thing is very, very
    // sensitive to round off issues. We still have a HUGE aliasing
    // problem unless the API has enforced that the times in the 
    // time series are all distinct when rounded to the minute. Is that
    // happening?
    double today = floor(daysFromFirst[i]);
    double hms = daysFromFirst[i]-today;
    // Here, we need to make sure that we aren't nearly the end of a day
    if(fabs(1.0-hms) < eps)
    {
      today += 1;
      hms = 0.0;
    }
    if(hms < eps)
    {
      // This value is an end of day value, so end the day and set up the next
      fieldIndex = addUntil(idfObject,fieldIndex,24,0,values[i]);
      lastDate += dayDelta;
      fieldIndex = startNewDay(idfObject,fieldIndex,lastDate);
    }
    else
    {
      if(today != lastDay)
      {
        // We're on a new day, need a 24:00:00 value and set up the next day
        fieldIndex = addUntil(idfObject,fieldIndex,24,0,values[i]);
        lastDate += dayDelta;
        fieldIndex = startNewDay(idfObject,fieldIndex,lastDate);
      }
      if(values[i] == values[i+1])
      {
        // Bail on values that match the next value
        continue;
      }
      // Write out the current entry
      Time time(hms);
      int hours = time.hours();
      int minutes = time.minutes() + floor((time.seconds()/60.0) + 0.5);
      // This is a little dangerous, but all of the problematic 24:00 
      // times that might need to cause a day++ should be caught above.
      if(minutes==60)
      {
        hours += 1;
        minutes = 0;
      }
      fieldIndex = addUntil(idfObject,fieldIndex,hours,minutes,values[i]);
    }
    lastDay = today;
  }
  // Handle the last point a little differently to make sure that the schedule ends exactly on the end of a day
  unsigned int i = values.size()-1;
  // We'll skip a sanity check here, but it might be a good idea to add one at some point
  fieldIndex = addUntil(idfObject,fieldIndex,24,0,values[i]);
  
  return idfObject;
}
TEST_F(EnergyPlusFixture, ForwardTranslator_ScheduleFixedInterval_Hourly_Shifted)
{
  // Create the values vector
  Vector values = linspace(1, 8760, 8760);

  // Create a time series that starts at 12/31 23:00
  TimeSeries timeseries(DateTime(Date(MonthOfYear::Jan, 1)), Time(0, 1, 0), values, "");

  Model model;

  // Create a schedule and make sure it worked
  boost::optional<ScheduleInterval> scheduleInterval = ScheduleInterval::fromTimeSeries(timeseries, model);
  ASSERT_TRUE(scheduleInterval);
  EXPECT_TRUE(scheduleInterval->optionalCast<ScheduleFixedInterval>());

  // Verify that the schedule gives us back the time series
  TimeSeries ts = scheduleInterval->timeSeries();
  // Oops, it doesn't. Maybe it shouldn't give back the exact time series, but it can't do this.
  // Without this check, the schedule manages to pass everything else and the test succeeds.
  EXPECT_EQ(DateTime(Date(MonthOfYear::Jan, 1), Time(0,0,0)), ts.firstReportDateTime());

  // Forward translate the schedule
  ForwardTranslator ft;
  Workspace workspace = ft.translateModel(model);

  std::vector<WorkspaceObject> objects = workspace.getObjectsByType(IddObjectType::Schedule_Compact);
  ASSERT_EQ(1u, objects.size());

  boost::regex throughRegex("^Through:\\s*(.*)/\\s*(.*)\\s*");
  boost::regex untilRegex("^Until:\\s*(.*):(.*)\\s*");

  // Write out the schedule -  keep this around for now
  //workspace.save(toPath("./ForwardTranslator_ScheduleFixedInterval_Hourly_Shifted.idf"), true);

  // Check the contents of the output
  unsigned N = objects[0].numFields();
  boost::optional<Date> lastDateThrough;
  bool until24Found = false;
  bool nextValueShouldBeLast = false;
  unsigned numUntils = 0;

  int currentHour = 0;
  for (unsigned i = 0; i < N; ++i) {
    boost::optional<std::string> field = objects[0].getString(i, true, false);
    ASSERT_TRUE(field);

    if (nextValueShouldBeLast) {
      double value = boost::lexical_cast<double>(*field);
      EXPECT_EQ(8760.0, value);
      nextValueShouldBeLast = false;
    }

    boost::smatch throughMatches;
    if (boost::regex_search(*field, throughMatches, throughRegex)) {

      currentHour = 1;

      std::string monthText(throughMatches[1].first, throughMatches[1].second);
      std::string dayText(throughMatches[2].first, throughMatches[2].second);

      int month = boost::lexical_cast<int>(monthText);
      int day = boost::lexical_cast<int>(dayText);

      Date date(MonthOfYear(month), day);
      if (lastDateThrough) {
        // check that this date is greater than last date
        EXPECT_TRUE(date > *lastDateThrough) << date << " <= " << *lastDateThrough;

        // DLM: this schedule should not wrap around to 1/1, it should end on 12/31 at 24:00

        // check that last date was closed at 24:00
        EXPECT_TRUE(until24Found);
      }
      lastDateThrough = date;
      until24Found = false;
    }

    boost::smatch untilMatches;
    if (boost::regex_search(*field, untilMatches, untilRegex)) {

      numUntils += 1;

      std::string hrText(untilMatches[1].first, untilMatches[1].second);
      std::string minText(untilMatches[2].first, untilMatches[2].second);

      int hr = boost::lexical_cast<int>(hrText);
      int min = boost::lexical_cast<int>(minText);
      EXPECT_EQ(currentHour, hr);
      ++currentHour;
      EXPECT_EQ(0, min);

      if ((hr == 24) && (min == 0)) {
        until24Found = true;
      }

      // should NOT see Until: 00:00,
      EXPECT_FALSE((hr == 0) && (min == 0));

      if ((lastDateThrough == Date(MonthOfYear(12), 31)) && until24Found) {
        nextValueShouldBeLast = true;
      }
    }
  }
  bool lastUntil24Found = until24Found;

  // check last date was closed
  EXPECT_TRUE(lastUntil24Found);

  // check that there were 8760 untils
  EXPECT_EQ(8760, numUntils);
}