gnucash/libgnucash/engine/gnc-datetime.cpp
John Ralls 7d7da8e2c4 Bug 797067 - Date displayed incorrectly in register take two.
Revert using boost::locale to generate std::locales as boost::locale-
generated locales don't implement std::locale::facet and there was
a bug in the boost::locale ICU wrapper code that caused the wrong year
to be output for the last 3 days of December.

GCC's libstdc++ supports only the "C" locale on Windows and throws if
one attempts to create any other kind. For dates we work around this
by using wstrftime() to format according to locale and then convert
the UTF16 string to UTF8. wstrftime() interprets the time zone flags
%z, %Z, and %ZP differently so we process those first before calling
strftime. This will have the unfortunate effect of not localizing
timezone names but it's as close as we can get.
2019-02-08 11:56:32 -08:00

735 lines
23 KiB
C++

/********************************************************************\
* gnc-datetime.cpp -- Date and Time classes for GnuCash *
* *
* Copyright 2015 John Ralls <jralls@ceridwen.us> *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 2 of *
* the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License*
* along with this program; if not, contact: *
* *
* Free Software Foundation Voice: +1-617-542-5942 *
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
* Boston, MA 02110-1301, USA gnu@gnu.org *
* *
\********************************************************************/
extern "C"
{
#include <config.h>
#include "platform.h"
}
#include <boost/date_time/gregorian/gregorian.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/date_time/local_time/local_time.hpp>
#include <boost/locale.hpp>
#include <boost/regex.hpp>
#include <libintl.h>
#include <locale.h>
#include <map>
#include <memory>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#ifdef __MINGW32__
#include <codecvt>
#endif
#include <gnc-locale-utils.hpp>
#include "gnc-timezone.hpp"
#include "gnc-datetime.hpp"
#include "qoflog.h"
static const char* log_module = "gnc.engine";
#define N_(string) string //So that xgettext will find it
using Date = boost::gregorian::date;
using Month = boost::gregorian::greg_month;
using PTime = boost::posix_time::ptime;
using LDT = boost::local_time::local_date_time;
using Duration = boost::posix_time::time_duration;
using LDTBase = boost::local_time::local_date_time_base<PTime, boost::date_time::time_zone_base<PTime, char>>;
using boost::date_time::not_a_date_time;
using time64 = int64_t;
static const TimeZoneProvider ltzp;
static const TimeZoneProvider* tzp = &ltzp;
// For converting to/from POSIX time.
static const PTime unix_epoch (Date(1970, boost::gregorian::Jan, 1),
boost::posix_time::seconds(0));
static const TZ_Ptr utc_zone(new boost::local_time::posix_time_zone("UTC-0"));
/* Backdoor to enable unittests to temporarily override the timezone: */
void _set_tzp(TimeZoneProvider& tz);
void _reset_tzp();
/* To ensure things aren't overly screwed up by setting the nanosecond clock for boost::date_time. Don't do it, though, it doesn't get us anything and slows down the date/time library. */
#ifndef BOOST_DATE_TIME_HAS_NANOSECONDS
static constexpr auto ticks_per_second = INT64_C(1000000);
#else
static constexpr auto ticks_per_second = INT64_C(1000000000);
#endif
/* Vector of date formats understood by gnucash and corresponding regex
* to parse each from an external source
* Note: while the format names are using a "-" as separator, the
* regexes will accept any of "-/.' " and will also work for dates
* without separators.
*/
const std::vector<GncDateFormat> GncDate::c_formats ({
GncDateFormat {
N_("y-m-d"),
"(?:" // either y-m-d
"(?<YEAR>[0-9]+)[-/.' ]+"
"(?<MONTH>[0-9]+)[-/.' ]+"
"(?<DAY>[0-9]+)"
"|" // or CCYYMMDD
"(?<YEAR>[0-9]{4})"
"(?<MONTH>[0-9]{2})"
"(?<DAY>[0-9]{2})"
")"
},
GncDateFormat {
N_("d-m-y"),
"(?:" // either d-m-y
"(?<DAY>[0-9]+)[-/.' ]+"
"(?<MONTH>[0-9]+)[-/.' ]+"
"(?<YEAR>[0-9]+)"
"|" // or DDMMCCYY
"(?<DAY>[0-9]{2})"
"(?<MONTH>[0-9]{2})"
"(?<YEAR>[0-9]{4})"
")"
},
GncDateFormat {
N_("m-d-y"),
"(?:" // either m-d-y
"(?<MONTH>[0-9]+)[-/.' ]+"
"(?<DAY>[0-9]+)[-/.' ]+"
"(?<YEAR>[0-9]+)"
"|" // or MMDDCCYY
"(?<MONTH>[0-9]{2})"
"(?<DAY>[0-9]{2})"
"(?<YEAR>[0-9]{4})"
")"
},
// Note year is still checked for in the regexes below
// This is to be able to raise an error if one is found for a yearless date format
GncDateFormat {
(N_("d-m")),
"(?:" // either d-m(-y)
"(?<DAY>[0-9]+)[-/.' ]+"
"(?<MONTH>[0-9]+)(?:[-/.' ]+"
"(?<YEAR>[0-9]+))?"
"|" // or DDMM(CCYY)
"(?<DAY>[0-9]{2})"
"(?<MONTH>[0-9]{2})"
"(?<YEAR>[0-9]+)?"
")"
},
GncDateFormat {
(N_("m-d")),
"(?:" // either m-d(-y)
"(?<MONTH>[0-9]+)[-/.' ]+"
"(?<DAY>[0-9]+)(?:[-/.' ]+"
"(?<YEAR>[0-9]+))?"
"|" // or MMDD(CCYY)
"(?<MONTH>[0-9]{2})"
"(?<DAY>[0-9]{2})"
"(?<YEAR>[0-9]+)?"
")"
}
});
/** Private implementation of GncDateTime. See the documentation for that class.
*/
static LDT
LDT_from_unix_local(const time64 time)
{
try
{
PTime temp(unix_epoch.date(),
boost::posix_time::hours(time / 3600) +
boost::posix_time::seconds(time % 3600));
auto tz = tzp->get(temp.date().year());
return LDT(temp, tz);
}
catch(boost::gregorian::bad_year&)
{
throw(std::invalid_argument("Time value is outside the supported year range."));
}
}
static LDT
LDT_from_struct_tm(const struct tm tm)
{
Date tdate;
Duration tdur;
TZ_Ptr tz;
try
{
tdate = boost::gregorian::date_from_tm(tm);
tdur = boost::posix_time::time_duration(tm.tm_hour, tm.tm_min,
tm.tm_sec, 0);
tz = tzp->get(tdate.year());
LDT ldt(tdate, tdur, tz, LDTBase::EXCEPTION_ON_ERROR);
return ldt;
}
catch(boost::gregorian::bad_year&)
{
throw(std::invalid_argument("Time value is outside the supported year range."));
}
catch(boost::local_time::time_label_invalid&)
{
throw(std::invalid_argument("Struct tm does not resolve to a valid time."));
}
catch(boost::local_time::ambiguous_result&)
{
/* We plunked down in the middle of a DST change. Try constructing the
* LDT three hours later to get a valid result then back up those three
* hours to have the time we want.
*/
using boost::posix_time::hours;
auto hour = tm.tm_hour;
tdur += hours(3);
LDT ldt(tdate, tdur, tz, LDTBase::NOT_DATE_TIME_ON_ERROR);
if (ldt.is_special())
throw(std::invalid_argument("Couldn't create a valid datetime."));
ldt -= hours(3);
return ldt;
}
}
using TD = boost::posix_time::time_duration;
void
_set_tzp(TimeZoneProvider& new_tzp)
{
tzp = &new_tzp;
}
void
_reset_tzp()
{
tzp = &ltzp;
}
class GncDateTimeImpl
{
public:
GncDateTimeImpl() : m_time(boost::local_time::local_sec_clock::local_time(tzp->get(boost::gregorian::day_clock::local_day().year()))) {}
GncDateTimeImpl(const time64 time) : m_time(LDT_from_unix_local(time)) {}
GncDateTimeImpl(const struct tm tm) : m_time(LDT_from_struct_tm(tm)) {}
GncDateTimeImpl(const GncDateImpl& date, DayPart part = DayPart::neutral);
GncDateTimeImpl(std::string str);
GncDateTimeImpl(PTime&& pt) : m_time(pt, tzp->get(pt.date().year())) {}
GncDateTimeImpl(LDT&& ldt) : m_time(ldt) {}
operator time64() const;
operator struct tm() const;
void now() { m_time = boost::local_time::local_sec_clock::local_time(tzp->get(boost::gregorian::day_clock::local_day().year())); }
long offset() const;
struct tm utc_tm() const { return to_tm(m_time.utc_time()); }
std::unique_ptr<GncDateImpl> date() const;
std::string format(const char* format) const;
std::string format_zulu(const char* format) const;
std::string format_iso8601() const;
static std::string timestamp();
private:
LDT m_time;
static const TD time_of_day[3];
};
const TD GncDateTimeImpl::time_of_day[3] = {TD(0, 0, 0), TD(10, 59, 0), TD(23, 59, 59)};
/** Private implementation of GncDate. See the documentation for that class.
*/
class GncDateImpl
{
public:
GncDateImpl(): m_greg(boost::gregorian::day_clock::local_day()) {}
GncDateImpl(const int year, const int month, const int day) :
m_greg(year, static_cast<Month>(month), day) {}
GncDateImpl(Date d) : m_greg(d) {}
GncDateImpl(const std::string str, const std::string fmt);
void today() { m_greg = boost::gregorian::day_clock::local_day(); }
ymd year_month_day() const;
std::string format(const char* format) const;
std::string format_zulu(const char* format) const {
return this->format(format);
}
private:
Date m_greg;
friend GncDateTimeImpl::GncDateTimeImpl(const GncDateImpl&, DayPart);
friend bool operator<(const GncDateImpl&, const GncDateImpl&);
friend bool operator>(const GncDateImpl&, const GncDateImpl&);
friend bool operator==(const GncDateImpl&, const GncDateImpl&);
friend bool operator<=(const GncDateImpl&, const GncDateImpl&);
friend bool operator>=(const GncDateImpl&, const GncDateImpl&);
friend bool operator!=(const GncDateImpl&, const GncDateImpl&);
};
/* Member function definitions for GncDateTimeImpl.
*/
GncDateTimeImpl::GncDateTimeImpl(const GncDateImpl& date, DayPart part) :
m_time(date.m_greg, time_of_day[part], tzp->get(date.m_greg.year()),
LDT::NOT_DATE_TIME_ON_ERROR)
{
using boost::posix_time::hours;
if (m_time.is_not_a_date_time())
{
try
{
auto t_o_d = time_of_day[part] + hours(3);
LDT time(date.m_greg, t_o_d, tzp->get(date.m_greg.year()),
LDT::EXCEPTION_ON_ERROR);
m_time = time - hours(3);
}
catch(boost::gregorian::bad_year&)
{
throw(std::invalid_argument("Time value is outside the supported year range."));
}
}
if (part == DayPart::neutral)
{
try
{
auto offset = m_time.local_time() - m_time.utc_time();
m_time = LDT(date.m_greg, time_of_day[part], utc_zone,
LDT::EXCEPTION_ON_ERROR);
if (offset < hours(-10))
m_time -= hours(offset.hours() + 10);
if (offset > hours(13))
m_time -= hours(offset.hours() - 11);
}
catch(boost::gregorian::bad_year&)
{
throw(std::invalid_argument("Time value is outside the supported year range."));
}
}
}
using PTZ = boost::local_time::posix_time_zone;
static TZ_Ptr
tz_from_string(std::string str)
{
if (str.empty()) return utc_zone;
std::string tzstr = "XXX" + str;
if (tzstr.length() > 6 && tzstr[6] != ':') //6 for XXXsHH, s is + or -
tzstr.insert(6, ":");
if (tzstr.length() > 9 && tzstr[9] != ':') //9 for XXXsHH:MM
{
tzstr.insert(9, ":");
}
return TZ_Ptr(new PTZ(tzstr));
}
GncDateTimeImpl::GncDateTimeImpl(std::string str) :
m_time(unix_epoch, utc_zone)
{
if (str.empty()) return;
TZ_Ptr tzptr;
try
{
static const boost::regex delim_iso("^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(?:\\.\\d{0,9})?)\\s*([+-]\\d{2}(?::?\\d{2})?)?$");
static const boost::regex non_delim("^(\\d{14}(?:\\.\\d{0,9})?)\\s*([+-]\\d{2}\\s*(:?\\d{2})?)?$");
PTime pdt;
boost::smatch sm;
if (regex_match(str, sm, non_delim))
{
std::string time_str(sm[1]);
time_str.insert(8, "T");
pdt = boost::posix_time::from_iso_string(time_str);
}
else if (regex_match(str, sm, delim_iso))
{
pdt = boost::posix_time::time_from_string(sm[1]);
}
else
{
throw(std::invalid_argument("The date string was not formatted in a way that GncDateTime(std::string) knows how to parse."));
}
std::string tzstr("");
if (sm[2].matched)
tzstr += sm[2];
tzptr = tz_from_string(tzstr);
m_time = LDT(pdt.date(), pdt.time_of_day(), tzptr,
LDTBase::NOT_DATE_TIME_ON_ERROR);
}
catch(boost::gregorian::bad_year&)
{
throw(std::invalid_argument("The date string was outside of the supported year range."));
}
/* Bug 767824: A GLib bug in parsing the UTC timezone on Windows may have
* created a bogus timezone of a random number of minutes. Since there are
* no fractional-hour timezones around the prime meridian we can safely
* check for this in files by resetting to UTC if there's a
* less-than-an-hour offset.
*/
auto offset = tzptr->base_utc_offset().seconds();
if (offset != 0 && std::abs(offset) < 3600)
m_time = m_time.local_time_in(utc_zone);
}
GncDateTimeImpl::operator time64() const
{
auto duration = m_time.utc_time() - unix_epoch;
auto secs = duration.ticks();
secs /= ticks_per_second;
return secs;
}
GncDateTimeImpl::operator struct tm() const
{
struct tm time = to_tm(m_time);
#if HAVE_STRUCT_TM_GMTOFF
time.tm_gmtoff = offset();
#endif
return time;
}
long
GncDateTimeImpl::offset() const
{
auto offset = m_time.local_time() - m_time.utc_time();
return offset.total_seconds();
}
std::unique_ptr<GncDateImpl>
GncDateTimeImpl::date() const
{
return std::unique_ptr<GncDateImpl>(new GncDateImpl(m_time.local_time().date()));
}
/* The 'O', 'E', and '-' format modifiers are not supported by
* boost's output facets. Remove them.
*/
static inline std::string
normalize_format (const std::string& format)
{
bool is_pct = false;
std::string normalized;
std::remove_copy_if(
format.begin(), format.end(), back_inserter(normalized),
[&is_pct](char e){
bool r = (is_pct && (e == 'E' || e == 'O' || e == '-'));
is_pct = e == '%';
return r;
});
return normalized;
}
#ifdef __MINGW32__
constexpr size_t DATEBUFLEN = 100;
static std::string
win_date_format(std::string format, struct tm tm)
{
wchar_t buf[DATEBUFLEN];
memset(buf, 0, DATEBUFLEN);
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> conv;
auto numchars = wcsftime(buf, DATEBUFLEN - 1, conv.from_bytes(format).c_str(), &tm);
return conv.to_bytes(buf);
}
/* Microsoft's strftime uses the time zone flags differently from
* boost::date_time so we need to handle any before passing the
* format string to strftime.
*/
inline std::string
win_format_tz_abbrev (std::string format, TZ_Ptr tz, bool is_dst)
{
size_t pos = format.find("%z");
if (pos != std::string::npos)
{
auto tzabbr = tz->has_dst() && is_dst ? tz->dst_zone_abbrev() :
tz->std_zone_abbrev();
format.replace(pos, 2, tzabbr);
}
return format;
}
inline std::string
win_format_tz_name (std::string format, TZ_Ptr tz, bool is_dst)
{
size_t pos = format.find("%Z");
if (pos != std::string::npos)
{
auto tzname = tz->has_dst() && is_dst ? tz->dst_zone_name() :
tz->std_zone_name();
format.replace(pos, 2, tzname);
}
return format;
}
inline std::string
win_format_tz_posix (std::string format, TZ_Ptr tz)
{
size_t pos = format.find("%ZP");
if (pos != std::string::npos)
format.replace(pos, 2, tz->to_posix_string());
return format;
}
#endif
std::string
GncDateTimeImpl::format(const char* format) const
{
#ifdef __MINGW32__
auto tz = m_time.zone();
auto tm = static_cast<struct tm>(*this);
auto sformat = win_format_tz_abbrev(format, tz, tm.tm_isdst);
sformat = win_format_tz_name(sformat, tz, tm.tm_isdst);
sformat = win_format_tz_posix(sformat, tz);
return win_date_format(sformat, tm);
#else
using Facet = boost::local_time::local_time_facet;
auto output_facet(new Facet(normalize_format(format).c_str()));
std::stringstream ss;
ss.imbue(std::locale(gnc_get_locale(), output_facet));
ss << m_time;
return ss.str();
#endif
}
std::string
GncDateTimeImpl::format_zulu(const char* format) const
{
#ifdef __MINGW32__
auto tz = m_time.zone();
auto tm = static_cast<struct tm>(*this);
auto sformat = win_format_tz_abbrev(format, tz, tm.tm_isdst);
sformat = win_format_tz_name(sformat, tz, tm.tm_isdst);
sformat = win_format_tz_posix(sformat, tz);
return win_date_format(sformat, utc_tm());
#else
using Facet = boost::local_time::local_time_facet;
auto offset = m_time.local_time() - m_time.utc_time();
auto zulu_time = m_time - offset;
auto output_facet(new Facet(normalize_format(format).c_str()));
std::stringstream ss;
ss.imbue(std::locale(gnc_get_locale(), output_facet));
ss << zulu_time;
return ss.str();
#endif
}
std::string
GncDateTimeImpl::format_iso8601() const
{
auto str = boost::posix_time::to_iso_extended_string(m_time.utc_time());
str[10] = ' ';
return str.substr(0, 19);
}
std::string
GncDateTimeImpl::timestamp()
{
GncDateTimeImpl gdt;
auto str = boost::posix_time::to_iso_string(gdt.m_time.local_time());
return str.substr(0, 8) + str.substr(9, 15);
}
/* Member function definitions for GncDateImpl.
*/
GncDateImpl::GncDateImpl(const std::string str, const std::string fmt) :
m_greg(boost::gregorian::day_clock::local_day()) /* Temporarily initialized to today, will be used and adjusted in the code below */
{
auto iter = std::find_if(GncDate::c_formats.cbegin(), GncDate::c_formats.cend(),
[&fmt](const GncDateFormat& v){ return (v.m_fmt == fmt); } );
if (iter == GncDate::c_formats.cend())
throw std::invalid_argument(N_("Unknown date format specifier passed as argument."));
boost::regex r(iter->m_re);
boost::smatch what;
if(!boost::regex_search(str, what, r)) // regex didn't find a match
throw std::invalid_argument (N_("Value can't be parsed into a date using the selected date format."));
// Bail out if a year was found with a yearless format specifier
auto fmt_has_year = (fmt.find('y') != std::string::npos);
if (!fmt_has_year && (what.length("YEAR") != 0))
throw std::invalid_argument (N_("Value appears to contain a year while the selected format forbids this."));
int year;
if (fmt_has_year)
{
/* The input dates have a year, so use that one */
year = std::stoi (what.str("YEAR"));
/* We assume two-digit years to be in the range 1969 - 2068. */
if (year < 69)
year += 2000;
else if (year < 100)
year += 1900;
}
else /* The input dates have no year, so use current year */
year = m_greg.year(); // Can use m_greg here as it was already initialized in the initializer list earlier
m_greg = Date(year,
static_cast<Month>(std::stoi (what.str("MONTH"))),
std::stoi (what.str("DAY")));
}
ymd
GncDateImpl::year_month_day() const
{
auto boost_ymd = m_greg.year_month_day();
return {boost_ymd.year, boost_ymd.month.as_number(), boost_ymd.day};
}
std::string
GncDateImpl::format(const char* format) const
{
#ifdef __MINGW32__
return win_date_format(format, to_tm(m_greg));
#else
using Facet = boost::gregorian::date_facet;
std::stringstream ss;
//The stream destructor frees the facet, so it must be heap-allocated.
auto output_facet(new Facet(normalize_format(format).c_str()));
ss.imbue(std::locale(gnc_get_locale(), output_facet));
ss << m_greg;
return ss.str();
#endif
}
bool operator<(const GncDateImpl& a, const GncDateImpl& b) { return a.m_greg < b.m_greg; }
bool operator>(const GncDateImpl& a, const GncDateImpl& b) { return a.m_greg > b.m_greg; }
bool operator==(const GncDateImpl& a, const GncDateImpl& b) { return a.m_greg == b.m_greg; }
bool operator<=(const GncDateImpl& a, const GncDateImpl& b) { return a.m_greg <= b.m_greg; }
bool operator>=(const GncDateImpl& a, const GncDateImpl& b) { return a.m_greg >= b.m_greg; }
bool operator!=(const GncDateImpl& a, const GncDateImpl& b) { return a.m_greg != b.m_greg; }
/* =================== Presentation-class Implementations ====================*/
/* GncDateTime */
GncDateTime::GncDateTime() : m_impl(new GncDateTimeImpl) {}
GncDateTime::GncDateTime(const time64 time) :
m_impl(new GncDateTimeImpl(time)) {}
GncDateTime::GncDateTime(const struct tm tm) :
m_impl(new GncDateTimeImpl(tm)) {}
GncDateTime::GncDateTime(std::string str) :
m_impl(new GncDateTimeImpl(str)) {}
GncDateTime::~GncDateTime() = default;
GncDateTime::GncDateTime(const GncDate& date, DayPart part) :
m_impl(new GncDateTimeImpl(*(date.m_impl), part)) {}
void
GncDateTime::now()
{
m_impl->now();
}
GncDateTime::operator time64() const
{
return m_impl->operator time64();
}
GncDateTime::operator struct tm() const
{
return m_impl->operator struct tm();
}
long
GncDateTime::offset() const
{
return m_impl->offset();
}
struct tm
GncDateTime::utc_tm() const
{
return m_impl->utc_tm();
}
GncDate
GncDateTime::date() const
{
return GncDate(m_impl->date());
}
std::string
GncDateTime::format(const char* format) const
{
return m_impl->format(format);
}
std::string
GncDateTime::format_zulu(const char* format) const
{
return m_impl->format_zulu(format);
}
std::string
GncDateTime::format_iso8601() const
{
return m_impl->format_iso8601();
}
std::string
GncDateTime::timestamp()
{
return GncDateTimeImpl::timestamp();
}
/* GncDate */
GncDate::GncDate() : m_impl{new GncDateImpl} {}
GncDate::GncDate(int year, int month, int day) :
m_impl(new GncDateImpl(year, month, day)) {}
GncDate::GncDate(const std::string str, const std::string fmt) :
m_impl(new GncDateImpl(str, fmt)) {}
GncDate::GncDate(std::unique_ptr<GncDateImpl> impl) :
m_impl(std::move(impl)) {}
GncDate::GncDate(const GncDate& a) :
m_impl(new GncDateImpl(*a.m_impl)) {}
GncDate::GncDate(GncDate&&) = default;
GncDate::~GncDate() = default;
GncDate&
GncDate::operator=(const GncDate& a)
{
m_impl.reset(new GncDateImpl(*a.m_impl));
return *this;
}
GncDate&
GncDate::operator=(GncDate&&) = default;
void
GncDate::today()
{
m_impl->today();
}
std::string
GncDate::format(const char* format)
{
return m_impl->format(format);
}
ymd
GncDate::year_month_day() const
{
return m_impl->year_month_day();
}
bool operator<(const GncDate& a, const GncDate& b) { return *(a.m_impl) < *(b.m_impl); }
bool operator>(const GncDate& a, const GncDate& b) { return *(a.m_impl) > *(b.m_impl); }
bool operator==(const GncDate& a, const GncDate& b) { return *(a.m_impl) == *(b.m_impl); }
bool operator<=(const GncDate& a, const GncDate& b) { return *(a.m_impl) <= *(b.m_impl); }
bool operator>=(const GncDate& a, const GncDate& b) { return *(a.m_impl) >= *(b.m_impl); }
bool operator!=(const GncDate& a, const GncDate& b) { return *(a.m_impl) != *(b.m_impl); }