6

I'm flabbergasted that it seems that boost::date_time can write date/time strings that it cannot read.

Consider the following example code:

#include <boost/date_time/local_time/local_time.hpp>
#include <iostream>
#include <locale>

class PointTime : public boost::local_time::local_date_time {
    typedef boost::local_time::local_time_input_facet   input_facet_t;
    typedef boost::local_time::local_time_facet         output_face_t;

  public:
    static input_facet_t const s_input_facet;
    static output_face_t const s_output_facet;
    static std::locale const s_input_locale;
    static std::locale const s_output_locale;

  public:
    PointTime(std::string const& str);
};

PointTime::input_facet_t const PointTime::s_input_facet("%Y-%m-%dT%H:%M:%S%q", 1);
PointTime::output_face_t const PointTime::s_output_facet("%Y-%m-%dT%H:%M:%S%q",
    boost::local_time::local_time_facet::period_formatter_type(),
    boost::local_time::local_time_facet::special_values_formatter_type(),
    boost::local_time::local_time_facet::date_gen_formatter_type(),
    1);
std::locale const PointTime::s_input_locale(std::locale::classic(), &PointTime::s_input_facet);
std::locale const PointTime::s_output_locale(std::locale::classic(), &PointTime::s_output_facet);

PointTime::PointTime(std::string const& str) : boost::local_time::local_date_time(boost::local_time::not_a_date_time)
{
  std::istringstream is(str);
  is.imbue(s_input_locale);
  is >> *this;
  std::string s;
  is >> s;
  std::cout << "Left after parsing: \"" << s << "\"." << std::endl;
}

int main()
{
  std::string ts("2005-10-15T13:12:11-0700");
  std::cout << "ts = " << ts << std::endl;

  try
  {
    PointTime t(ts);
    std::cout.imbue(PointTime::s_output_locale);
    std::cout << "t =  " << t << std::endl;
  }
  catch (boost::bad_lexical_cast const& error)
  {
    std::cout << error.what() << std::endl;
  }

  return 0;
}

This outputs:

ts = 2005-10-15T13:12:11-0700
Left after parsing: "-0700".
t = 2005-10-15T13:12:11Z

I understand why it does this: the %q format is documented to be "output only", but I can't find a way to actually read back this string?! How should I do this?

Carlo Wood
  • 5,648
  • 2
  • 35
  • 47

2 Answers2

5

In the end I used a custom method to read back these strings.

I am using a class derived from boost::posix_time::ptime to save storage space (over deriving from boost::local_time::local_date_time) because I'm not using boost-date-time's timezone support at all anymore.

However, when printing a date as string I want it to end on 'Z' to make clear that it is in Zulu time (UTC, GMT) and not local time. I achieved that by using an output facet with a format string that ends on a 'Z'.

The PointTime class defines a few (input and output) facets (namely, ISOInputFacet, ExtendedISOInputFacet and a mix of those called LLInputFacet because Linden Lab thought that that was the ISO standard and uses it for SecondLife(tm), ugh. Likewise ISOOutputFacet, ExtendedISOOutputFacet and LLOutputFacet.

The class instantiates a static version of each of the six facets because I don't feel like calling new/delete for a facet every time I create, read or write a PointTime.

The main trick is in this function:

std::istream& operator>>(std::istream& is, PointTime& point_time)
{
  is >> static_cast<boost::posix_time::ptime&>(point_time);
  std::locale loc(is.getloc());
  if (std::has_facet<PointTime::InputFacet>(loc))
  {
    std::use_facet<PointTime::InputFacet>(loc).parse_timezone(is, point_time);
  }
  return is;
}

which peeks at the input facet being used and when it's one derived from PointTime::InputFacet then it calls a member function to parse a possible remaining timezone.

The test code that I wrote is given below. Perhaps it can serve as example code for others.

#include <boost/date_time/posix_time/posix_time_io.hpp>
#include <iostream>
#include <locale>
#include <cassert>

class TZSignOrZ;

class PointTime : public boost::posix_time::ptime {
  public:
    struct InputFacet : public boost::posix_time::time_input_facet {
        InputFacet(char const* format, size_t ref_arg) : boost::posix_time::time_input_facet(format, ref_arg) { }
        static void parse_iso_timezone(std::istream& is, PointTime& point_time, bool colon);
        static void parse_hours_and_minutes(std::istream& is, PointTime& point_time, TZSignOrZ const& sign, bool colon);
        virtual void parse_timezone(std::istream& is, PointTime& point_time) const = 0;
    };

    struct ISOInputFacet : public InputFacet {
        ISOInputFacet(size_t ref_arg = 0) : InputFacet("%Y%m%dT%H%M%S%F", ref_arg) { }
        /*virtual*/ void parse_timezone(std::istream& is, PointTime& point_time) const;
    };

    struct ExtendedISOInputFacet : public InputFacet {
        ExtendedISOInputFacet(size_t ref_arg = 0) : InputFacet("%Y-%m-%d %H:%M:%S%F", ref_arg) { }
        /*virtual*/ void parse_timezone(std::istream& is, PointTime& point_time) const;
    };

    struct LLInputFacet : public InputFacet {
        LLInputFacet(size_t ref_arg = 0) : InputFacet("%Y-%m-%dT%H:%M:%S%F", ref_arg) { }
        /*virtual*/ void parse_timezone(std::istream& is, PointTime& point_time) const;
    };

    struct OutputFacet : public boost::posix_time::time_facet {
        OutputFacet(char const* format, size_t ref_arg) :
          boost::posix_time::time_facet(
              format, boost::posix_time::time_facet::period_formatter_type(),
              boost::posix_time::time_facet::special_values_formatter_type(),
              boost::posix_time::time_facet::date_gen_formatter_type(), ref_arg) { }
    };

    struct ISOOutputFacet : public OutputFacet {
        ISOOutputFacet(size_t ref_arg = 0) : OutputFacet("%Y%m%dT%H%M%S%FZ", ref_arg) { }
    };

    struct ExtendedISOOutputFacet : public OutputFacet {
        ExtendedISOOutputFacet(size_t ref_arg = 0) : OutputFacet("%Y-%m-%d %H:%M:%S%FZ", ref_arg) { }
    };

    struct LLOutputFacet : public OutputFacet {
        LLOutputFacet(size_t ref_arg = 0) : OutputFacet("%Y-%m-%dT%H:%M:%S%FZ", ref_arg) { }
    };

    static ISOInputFacet s_iso_input_facet;
    static ExtendedISOInputFacet s_extended_iso_input_facet;
    static LLInputFacet s_ll_input_facet;
    static ISOOutputFacet s_iso_output_facet;
    static ExtendedISOOutputFacet s_extended_iso_output_facet;
    static LLOutputFacet s_ll_output_facet;

  public:
    PointTime(void) { }
    PointTime(std::string const& str, InputFacet const& input_facet = s_extended_iso_input_facet);

    friend std::istream& operator>>(std::istream& os, PointTime& point_time);
};

PointTime::ISOInputFacet PointTime::s_iso_input_facet(1);
PointTime::ExtendedISOInputFacet PointTime::s_extended_iso_input_facet(1);
PointTime::LLInputFacet PointTime::s_ll_input_facet(1);
PointTime::ISOOutputFacet PointTime::s_iso_output_facet(1);
PointTime::ExtendedISOOutputFacet PointTime::s_extended_iso_output_facet(1);
PointTime::LLOutputFacet PointTime::s_ll_output_facet(1);

PointTime::PointTime(std::string const& str, InputFacet const& input_facet) : boost::posix_time::ptime(boost::posix_time::not_a_date_time)
{
  std::istringstream is(str);
  is.imbue(std::locale(std::locale::classic(), &input_facet));
  is >> *this;
}

std::istream& operator>>(std::istream& is, PointTime& point_time)
{
  is >> static_cast<boost::posix_time::ptime&>(point_time);
  std::locale loc(is.getloc());
  if (std::has_facet<PointTime::InputFacet>(loc))
  {
    std::use_facet<PointTime::InputFacet>(loc).parse_timezone(is, point_time);
  }
  return is;
}

struct TZSignOrZ {
  char c;
  friend std::istream& operator>>(std::istream& is, TZSignOrZ& sign)
  {
    sign.c = is.get();
    if (!is.good() || (sign.c != 'Z' && sign.c != '+' && sign.c != '-'))
    {
      if (!is.fail())
      {
        is.putback(sign.c);
      }
      throw std::runtime_error("Failed to read TZSignOrZ: first character after date-time is not a 'Z' or a sign.");
    }
    return is;
  }
  bool is_utc(void) const { return c == 'Z'; }
  bool is_negative(void) const { return c == '-'; }
};

struct TZColon {
  friend std::istream& operator>>(std::istream& is, TZColon& colon)
  {
    char c = is.get();
    if (!is.good() || c != ':')
      throw std::runtime_error("Failed to read TZColon: expected a colon between hours and minutes in time zone of date-time.");
    return is;
  }
};

struct TZTwoDigits {
  unsigned char val;
  friend std::istream& operator>>(std::istream& is, TZTwoDigits& digits)
  {
    char c1, c2;
    is >> std::noskipws >> c1 >> c2;
    if (!is.good() || !std::isdigit(c1) || !std::isdigit(c2))
      throw std::runtime_error("Failed to read TZTwoDigits: expected two digits for hours or minutes part in time zone of data-time.");
    digits.val = c2 - '0' + 10 * (c1 - '0');
    return is;
  }
};

void PointTime::InputFacet::parse_hours_and_minutes(std::istream& is, PointTime& point_time, TZSignOrZ const& sign, bool colon)
{
  TZTwoDigits hours, minutes;
  is >> hours;
  if (colon)
  {
    TZColon colon;
    is >> colon;
  }
  is >> minutes;
  boost::posix_time::time_duration duration(hours.val, minutes.val, 0, 0);
  if (sign.is_negative())
    point_time += duration;
  else
    point_time -= duration;
}

void PointTime::InputFacet::parse_iso_timezone(std::istream& is, PointTime& point_time, bool colon)
{
  if (!is.good())
    return; // Don't throw when the input stream is already bad.
  TZSignOrZ sign; 
  try
  {
    is >> sign;
  }
  catch (std::runtime_error const&)
  {
    // It is actually allowed to have no time zone.
    return;
  }
  parse_hours_and_minutes(is, point_time, sign, colon);
}

void PointTime::ISOInputFacet::parse_timezone(std::istream& is, PointTime& point_time) const
{
  parse_iso_timezone(is, point_time, false);
}

void PointTime::ExtendedISOInputFacet::parse_timezone(std::istream& is, PointTime& point_time) const
{
  parse_iso_timezone(is, point_time, true);
}

void PointTime::LLInputFacet::parse_timezone(std::istream& is, PointTime& point_time) const
{
  if (!is.good())
    return; // Don't throw when the input stream is already bad.
  TZSignOrZ sign;
  is >> sign;
  if (sign.is_utc())
    return;
  parse_hours_and_minutes(is, point_time, sign, false);
}

int main() 
{
  std::string test_strings[] = {
    "2014-09-10T00:12:34+0230",
    "2014-09-10T00:12:34.000567+0230",
    "2014-09-10T00:12:34.567+0230",
    "2014-09-10T21:32:34-0230",
    "2014-09-10T21:32:34.000567-0230",
    "2014-09-10T21:32:34.567-0230",
    "2014-09-10 00:12:34+02:30",
    "2014-09-10 00:12:34.000567+02:30",
    "2014-09-10 00:12:34.567+02:30",
    "2014-09-10 21:32:34-02:30",
    "2014-09-10 21:32:34.000567-02:30",
    "2014-09-10 21:32:34.567-02:30",
    "2014-09-10 00:12:34 hello!",
    "2014-09-10 00:12:34.000567***",
    "2014-09-10 00:12:34.567 hello!",
    "2014-09-10T00:12:34 +0230",
    "2014-09-10T00:12:34+ 0230",
    "2014-09-10T00:12:34+02 30",
    "2014-09-10T00:12:34 +02:30",
    "2014-09-10T00:12:34+ 02:30",
    "2014-09-10T00:12:34+02: 30",
    "2014-09-10T00:12:34+02 :30"
  };
  int const n = sizeof(test_strings) / sizeof(test_strings[0]);

  PointTime::InputFacet const* facets[] = {
    &PointTime::s_ll_input_facet,
    &PointTime::s_extended_iso_input_facet
  };

  std::cout << std::left << std::setw(32) << "INPUT" << "    " <<
  std::setw(40) << "LL input format" << "    " <<
  std::setw(40) << "Extended ISO input format" << std::endl;
  for (int i = 0 ; i < n ; ++i)
  {
    std::cout << std::left << std::setw(32) << test_strings[i];

    for (int j = 0; j < 2; ++j)
    {
      try
      {
        PointTime t;
        std::istringstream is(test_strings[i]);
        is.imbue(std::locale(std::locale::classic(), facets[j]));
        is >> t;
        std::string leftover;
        is >> std::noskipws;
        std::getline(is, leftover);
        std::ostringstream oss;
        oss.imbue(std::locale(std::locale::classic(), &PointTime::s_extended_iso_output_facet));
        oss << t;
        if (!leftover.empty())
        {
          oss << " [\"" << leftover << "\"]";
        }
        std::cout << " -- " << std::setw(40) << oss.str();
      }
      catch (boost::bad_lexical_cast const& error)
      {
        std::cout << " -- " << std::setw(40) << "invalid date-time";
      }
      catch (std::runtime_error const& error)
      {
        std::cout << " -- " << std::setw(40) << "invalid TZ";
      }
    }
    std::cout << std::endl;
  }

  return 0;
}

The output of this program is:

INPUT                               LL input format                             Extended ISO input format               
2014-09-10T00:12:34+0230         -- 2014-09-09 21:42:34Z                     -- invalid TZ                              
2014-09-10T00:12:34.000567+0230  -- 2014-09-09 21:42:34.000567Z              -- invalid TZ                              
2014-09-10T00:12:34.567+0230     -- 2014-09-09 21:42:34.567000Z              -- invalid TZ                              
2014-09-10T21:32:34-0230         -- 2014-09-11 00:02:34Z                     -- invalid TZ                              
2014-09-10T21:32:34.000567-0230  -- 2014-09-11 00:02:34.000567Z              -- invalid TZ                              
2014-09-10T21:32:34.567-0230     -- 2014-09-11 00:02:34.567000Z              -- invalid TZ                              
2014-09-10 00:12:34+02:30        -- invalid TZ                               -- 2014-09-09 21:42:34Z                    
2014-09-10 00:12:34.000567+02:30 -- invalid TZ                               -- 2014-09-09 21:42:34.000567Z             
2014-09-10 00:12:34.567+02:30    -- invalid TZ                               -- 2014-09-09 21:42:34.567000Z             
2014-09-10 21:32:34-02:30        -- invalid TZ                               -- 2014-09-11 00:02:34Z                    
2014-09-10 21:32:34.000567-02:30 -- invalid TZ                               -- 2014-09-11 00:02:34.000567Z             
2014-09-10 21:32:34.567-02:30    -- invalid TZ                               -- 2014-09-11 00:02:34.567000Z             
2014-09-10 00:12:34 hello!       -- invalid TZ                               -- 2014-09-10 00:12:34Z [" hello!"]        
2014-09-10 00:12:34.000567***    -- invalid TZ                               -- 2014-09-10 00:12:34.000567Z ["***"]     
2014-09-10 00:12:34.567 hello!   -- invalid TZ                               -- 2014-09-10 00:12:34.567000Z [" hello!"] 
2014-09-10T00:12:34 +0230        -- invalid TZ                               -- 2014-09-10 00:12:34Z [" +0230"]         
2014-09-10T00:12:34+ 0230        -- invalid TZ                               -- invalid TZ                              
2014-09-10T00:12:34+02 30        -- invalid TZ                               -- invalid TZ                              
2014-09-10T00:12:34 +02:30       -- invalid TZ                               -- 2014-09-10 00:12:34Z [" +02:30"]        
2014-09-10T00:12:34+ 02:30       -- invalid TZ                               -- invalid TZ                              
2014-09-10T00:12:34+02: 30       -- invalid TZ                               -- invalid TZ                              
2014-09-10T00:12:34+02 :30       -- invalid TZ                               -- invalid TZ
Carlo Wood
  • 5,648
  • 2
  • 35
  • 47
  • At a casual glance this looks excellent. I might review this for my own code base. Thanks – sehe Sep 10 '14 at 15:44
  • A final touch that I made in the real code: renamed PointTime to TimePoint. Using PointTime makes no sense (I was confused by the abbreviation ptime, but that probably stands for POSIX time, even though it is in the namespace posix_time. Probably a better name would have been boost::date_time::posix_time::point). – Carlo Wood Sep 10 '14 at 16:35
4

The sad state of affairs is, yes, this is currently a limitation

Some new flags have been added, and others overridden. The input system supports only specific flags, therefore, not all flags that work for output will work with input (we are currently working to correct this situation).

Meanwhile, you might be able to leverage Boost Locale's input/output facilities: http://www.boost.org/doc/libs/1_55_0/libs/locale/doc/html/group__manipulators.html

sehe
  • 374,641
  • 47
  • 450
  • 633