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