From 7d7da8e2c44d117e3124a671e02b9d7f248d4d92 Mon Sep 17 00:00:00 2001 From: John Ralls Date: Fri, 8 Feb 2019 11:40:21 -0800 Subject: [PATCH] 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. --- libgnucash/core-utils/gnc-locale-utils.cpp | 32 +++--- libgnucash/engine/gnc-datetime.cpp | 99 +++++++++++++++++-- libgnucash/engine/test/gtest-gnc-datetime.cpp | 6 +- 3 files changed, 108 insertions(+), 29 deletions(-) diff --git a/libgnucash/core-utils/gnc-locale-utils.cpp b/libgnucash/core-utils/gnc-locale-utils.cpp index e3de961903..6707b39f4c 100644 --- a/libgnucash/core-utils/gnc-locale-utils.cpp +++ b/libgnucash/core-utils/gnc-locale-utils.cpp @@ -19,7 +19,10 @@ * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 * * Boston, MA 02110-1301, USA gnu@gnu.org * \********************************************************************/ - +extern "C" +{ +#include +} #include #include #include "gnc-locale-utils.hpp" @@ -50,29 +53,22 @@ gnc_get_locale() tried_already = true; try { - cached = gen(""); + cached = std::locale(""); } catch (const std::runtime_error& err) { - std::string c_locale(setlocale(LC_ALL, nullptr)); +#ifdef __MINGW32__ + char* locale = g_win32_getlocale(); +#else + char* locale = g_strdup(setlocale(LC_ALL, "")); +#endif + std::string c_locale(locale); std::cerr << "[gnc_get_locale] Failed to create app-default locale from " << c_locale << " because " << err.what() << "\n"; - auto dot = c_locale.find("."); - if (dot != std::string::npos) - { - try - { - cached = gen(c_locale.substr(0, dot)); - } - catch (std::runtime_error& err2) - { - std::cerr << "[gnc_get_locale] Failed to create app-default locale from " << c_locale << " because " << err.what() << " so using the 'C' locale for C++.\n"; - } - } - else - { std::cerr << "[gnc_get_locale] Using the 'C' locale for C++\n"; - } + g_free(locale); } } return cached; } + + diff --git a/libgnucash/engine/gnc-datetime.cpp b/libgnucash/engine/gnc-datetime.cpp index e1946ec9bd..2192ffd3fe 100644 --- a/libgnucash/engine/gnc-datetime.cpp +++ b/libgnucash/engine/gnc-datetime.cpp @@ -40,6 +40,9 @@ extern "C" #include #include #include +#ifdef __MINGW32__ +#include +#endif #include #include "gnc-timezone.hpp" #include "gnc-datetime.hpp" @@ -263,7 +266,9 @@ public: 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; + std::string format_zulu(const char* format) const { + return this->format(format); + } private: Date m_greg; @@ -428,27 +433,98 @@ normalize_format (const std::string& format) }); 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, 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 { - namespace as = boost::locale::as; +#ifdef __MINGW32__ + auto tz = m_time.zone(); + auto tm = static_cast(*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(gnc_get_locale()); - ss << as::ftime(format) - << as::time_zone(m_time.zone()->std_zone_name()) - << static_cast(*this); + 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 { - namespace as = boost::locale::as; +#ifdef __MINGW32__ + auto tz = m_time.zone(); + auto tm = static_cast(*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(gnc_get_locale()); - ss << as::ftime(format) << as::gmt << static_cast(*this); + ss.imbue(std::locale(gnc_get_locale(), output_facet)); + ss << zulu_time; return ss.str(); +#endif } std::string @@ -517,14 +593,17 @@ GncDateImpl::year_month_day() const 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())); - // FIXME Rather than imbueing a locale below we probably should set std::locale::global appropriately somewhere. 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; } diff --git a/libgnucash/engine/test/gtest-gnc-datetime.cpp b/libgnucash/engine/test/gtest-gnc-datetime.cpp index 970ad0d46b..debcd00f93 100644 --- a/libgnucash/engine/test/gtest-gnc-datetime.cpp +++ b/libgnucash/engine/test/gtest-gnc-datetime.cpp @@ -360,7 +360,11 @@ TEST(gnc_datetime_constructors, test_gncdate_BST_transition) { const ymd begins = {2018, 03, 25}; const ymd ends = {2018, 10, 28}; +#ifdef __MINGW32__ + TimeZoneProvider tzp{"GMT Standard Time"}; +#else TimeZoneProvider tzp("Europe/London"); +#endif _set_tzp(tzp); GncDateTime btime(GncDate(begins.year, begins.month, begins.day), DayPart::start); GncDateTime etime(GncDate(ends.year, ends.month, ends.day), DayPart::start); @@ -392,7 +396,7 @@ TEST(gnc_datetime_constructors, test_gncdate_neutral_constructor) if (gncdt.offset() >= max_western_offset && gncdt.offset() <= max_eastern_offset) { - EXPECT_EQ(atime.format("%d-%m-%Y %H:%M:%S %Z"), "20-04-2017 10:59:00 GMT"); + EXPECT_EQ(atime.format("%d-%m-%Y %H:%M:%S %Z"), "20-04-2017 10:59:00 UTC"); } }