Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to read an ISO timezone with boost::date_time?

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?

like image 371
Carlo Wood Avatar asked Sep 09 '14 18:09

Carlo Wood


1 Answers

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
like image 69
Carlo Wood Avatar answered Oct 09 '22 09:10

Carlo Wood