1
votes

I have written a C++ class that represent a Date and I use strptime/strftime to write and instantiate Dates from string.

The full code is here

When I run it several times on my linux using the bash in the tab "Sample output", sometimes I got the same Date created and parsed back, sometimes I get the date with a shift of one hour (my timezone it UTC+1).

So, what is happening here, I have no clue!

#ifndef DOLIPRANE_TIMEUNIT_HPP
#define DOLIPRANE_TIMEUNIT_HPP

enum TimeUnit  {
    DAY,
    HOUR,
    MINUTE,
    SECOND
  };
#endif
#ifndef DOLIPRANE_DATE_HPP
#define DOLIPRANE_DATE_HPP

#include <ctime>
#include <string>


class Date
{
public:

  Date();
  Date(time_t epoch);

  /**
   * Expected format: dd/MM/YYYY HH:mm:[ss]
   */
  Date(const std::string &date);

  ~Date();

  void
  add(long val, TimeUnit u = SECOND);


  bool
  operator==(const Date &other) const;

  bool
  operator!=(const Date &other) const;

  bool
  operator<(const Date &other) const;

  bool
  operator<=(const Date &other) const;

  bool
  operator>(const Date &other) const;

  bool
  operator>=(const Date &other) const;

  friend std::ostream&
  operator<<(std::ostream &, const Date&);

  friend std::istream&
  operator>>(std::istream &, Date&);

private:

  static const std::string FORMAT;

  time_t m_time;


};
#endif


#include <iostream>
#include <stdexcept>
#include <ctime>

const char SEPARATOR=';';

const std::string Date::FORMAT="%d/%m/%Y %H:%M:%S";

Date::Date()
{
  m_time = time(NULL);
}

Date::Date(time_t epoch) 
  : m_time(epoch)
{}

Date::Date(const std::string &date)
{
  struct tm t;
  const char* ptr = strptime(date.c_str(), FORMAT.c_str(), &t);
  if (!ptr) {
    std::string cause = "Cannot parse date ";
    cause += date;
    throw std::invalid_argument(cause);
  }
  m_time = mktime(&t);
  if (m_time == -1) {
    std::string cause = "Cannot compute epoch from " + date;
    throw std::range_error(cause);
  }
}

Date::~Date()
{
}

void
Date::add(long val, TimeUnit u) {

  switch(u){
  case DAY:
    m_time += 86400*val;
    break;
  case HOUR:
    m_time += 3600*val;
    break;
  case MINUTE:
    m_time += 60*val;
    break;
  case SECOND:
    m_time += val;
    break;
  default:
    throw std::invalid_argument("Unknown TimeUnit specified");
  }
}

bool
Date::operator==(const Date& o) const
{
  return m_time == o.m_time;
}

bool
Date::operator!=(const Date& o) const
{
  return ! (*this==o);
}

bool
Date::operator<(const Date &other) const
{
  return m_time < other.m_time;
}

bool
Date::operator<=(const Date &other) const
{
  return m_time <= other.m_time;
}

bool
Date::operator>(const Date &other) const
{
  return m_time > other.m_time;
}

bool
Date::operator>=(const Date &other) const
{
  return m_time >= other.m_time;
}

std::ostream&
operator<<(std::ostream& out, const Date &d)
{
  struct tm* tm = localtime(&d.m_time);
  char buffer[20];
  strftime(buffer, 20, Date::FORMAT.c_str(), tm);
  out << buffer << SEPARATOR;
  return out;
}

std::istream&
operator>>(std::istream &in, Date &d)
{
  std::string buf;
  std::getline(in, buf, SEPARATOR);
  Date o(buf);
  d = o;
  return in;
}

#include <iostream>
#include <fstream>

int
main(void)
{
  Date d;
  std::cout << d << std::endl;
  std::ofstream out("tmp.txt");
  out << d;
  out.close();

  std::ifstream in("tmp.txt");
  Date d2;
  in >> d2;
  in.close();
  std::cout << d2 << std::endl;

}

And finally, how I'm testing it:

$ for i in `seq 1 10`; do echo "test $i:"; ./test; rm tmp.txt; done
test 1:
26/03/2016 00:30:31;
26/03/2016 00:30:31;
test 2:
26/03/2016 00:30:31;
26/03/2016 00:30:31;
test 3:
26/03/2016 00:30:31;
25/03/2016 23:30:31;
test 4:
26/03/2016 00:30:31;
26/03/2016 00:30:31;
test 5:
26/03/2016 00:30:31;
25/03/2016 23:30:31;
test 6:
26/03/2016 00:30:31;
26/03/2016 00:30:31;
test 7:
26/03/2016 00:30:31;
25/03/2016 23:30:31;
test 8:
26/03/2016 00:30:31;
25/03/2016 23:30:31;
test 9:
26/03/2016 00:30:31;
25/03/2016 23:30:31;
test 10:
26/03/2016 00:30:31;
26/03/2016 00:30:31;
2
Provide a minimal working example that exhibits the behavior and include it here, dont link to the code somewhere (once the link goes down the question becomes useless to everyone else).Borgleader
I cannot reproduce it on OS X. He said he was running on ubuntu, but I didn't get what version.xaxxon
I just took all the tabs from the link, cat'd them together (compiled it to make sure it worked) and edited the question with the full code (you owe me remi)xaxxon
since this is c++ and not c, please remove the c taguser3629249

2 Answers

4
votes

The problem is
struct tm t;
in the Date constructor that takes a string, which never initializes the object, and thus its tm_isdst field has an unspecified value. Set the tm_isdst field properly and you will get consistent results over multiple runs.

3
votes

Most likely, the problem is daylight-savings time (also called summer time).

struct tm has a field called tm_isdst. A positive value in that field indicates that the broken-down time is in DST. A 0 indicates that it is not DST.

strftime will correctly set the field, but strptime does not touch it (at least, in the glibc implementation). mktime expects it to be correctly set, but fortunately it allows it to be set to a negative value, meaning "I don't know." In that case, mktime will try to figure it out. (It might not be possible, because there is one hour every year which repeats, once as summer time and once as winter time after the clock change.)

Leaving the tm_isdst field uninitialized, as in your code is undefined (and unpredictable) behaviour. The normally correct strategy is to set it to -1 after the call to strptime and before the call to mktime.

It is generally best practice to zero out or otherwise initialize the struct tm before calling strptime, since that function only sets the fields corresponding to the format.