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.
This commit is contained in:
John Ralls 2019-02-08 11:40:21 -08:00
parent 67dbfca0e7
commit 7d7da8e2c4
3 changed files with 108 additions and 29 deletions

View File

@ -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 <glib.h>
}
#include <clocale>
#include <boost/locale.hpp>
#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;
}

View File

@ -40,6 +40,9 @@ extern "C"
#include <sstream>
#include <string>
#include <vector>
#ifdef __MINGW32__
#include <codecvt>
#endif
#include <gnc-locale-utils.hpp>
#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<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
{
namespace as = boost::locale::as;
#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(gnc_get_locale());
ss << as::ftime(format)
<< as::time_zone(m_time.zone()->std_zone_name())
<< static_cast<time64>(*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<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(gnc_get_locale());
ss << as::ftime(format) << as::gmt << static_cast<time64>(*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; }

View File

@ -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");
}
}