qt: Replace thin spaces with locale-specific number formatting

This commit is contained in:
J Ross Nicoll 2015-09-07 18:44:59 +01:00
parent 82d8665dad
commit 8cc0d8165f
6 changed files with 93 additions and 46 deletions

View file

@ -121,6 +121,9 @@ static void initTranslations(QTranslator &qtTranslatorBase, QTranslator &qtTrans
// 1) System default language
QString lang_territory = GetLangTerritory();
// Set default locale for amount and date formatting according to the selected language
QLocale::setDefault(QLocale(lang_territory));
// Convert to "de" only by truncating "_DE"
QString lang = lang_territory;
lang.truncate(lang_territory.lastIndexOf('_'));

View file

@ -25,7 +25,7 @@ public:
explicit AmountSpinBox(QWidget *parent):
QAbstractSpinBox(parent),
currentUnit(BitcoinUnits::BTC),
singleStep(100000000) // koinu
singleStep(COIN) // koinu
{
setAlignment(Qt::AlignRight);
@ -48,7 +48,7 @@ public:
CAmount val = parse(input, &valid);
if(valid)
{
input = BitcoinUnits::format(currentUnit, val, false, BitcoinUnits::separatorAlways);
input = BitcoinUnits::format(currentUnit, val, false, true);
lineEdit()->setText(input);
}
}
@ -60,7 +60,7 @@ public:
void setValue(const CAmount& value)
{
lineEdit()->setText(BitcoinUnits::format(currentUnit, value, false, BitcoinUnits::separatorAlways));
lineEdit()->setText(BitcoinUnits::format(currentUnit, value, false, true));
emit valueChanged();
}
@ -195,7 +195,6 @@ BitcoinAmountField::BitcoinAmountField(QWidget *parent) :
amount(0)
{
amount = new AmountSpinBox(this);
amount->setLocale(QLocale::c());
amount->installEventFilter(this);
amount->setMaximumWidth(170);
@ -220,6 +219,14 @@ BitcoinAmountField::BitcoinAmountField(QWidget *parent) :
unitChanged(unit->currentIndex());
}
void BitcoinAmountField::setText(const QString &text)
{
if (text.isEmpty())
amount->clear();
else
amount->setValue(QLocale().toDouble(text));
}
void BitcoinAmountField::clear()
{
amount->clear();

View file

@ -15,6 +15,8 @@ QT_BEGIN_NAMESPACE
class QValueComboBox;
QT_END_NAMESPACE
class AmountSpinBox;
/** Widget for entering bitcoin amounts.
*/
class BitcoinAmountField: public QWidget
@ -67,6 +69,8 @@ private:
AmountSpinBox *amount;
QValueComboBox *unit;
void setText(const QString &text);
QString text() const;
private slots:
void unitChanged(int idx);

View file

@ -7,6 +7,7 @@
#include "primitives/transaction.h"
#include <QStringList>
#include <QLocale>
BitcoinUnits::BitcoinUnits(QObject *parent):
QAbstractListModel(parent),
@ -69,6 +70,17 @@ qint64 BitcoinUnits::factor(int unit)
}
}
qint64 BitcoinUnits::maxAmount(int unit)
{
switch(unit)
{
case BTC: return Q_INT64_C(900000000000); //less than the coin supply until the year 2170
case mBTC: return Q_INT64_C(900000000000000);
case uBTC: return Q_INT64_C(900000000000000000); // Slightly under max value for int64
default: return 0;
}
}
int BitcoinUnits::decimals(int unit)
{
switch(unit)
@ -80,37 +92,42 @@ int BitcoinUnits::decimals(int unit)
}
}
QString BitcoinUnits::format(int unit, const CAmount& nIn, bool fPlus, SeparatorStyle separators)
QString BitcoinUnits::format(int unit, const CAmount& nIn, bool fPlus, bool fTrim, const QLocale &locale_in)
{
// Note: not using straight sprintf here because we do NOT want
// localized number formatting.
if(!valid(unit))
return QString(); // Refuse to format invalid unit
qint64 n = (qint64)nIn;
QLocale locale(locale_in);
qint64 coin = factor(unit);
int num_decimals = decimals(unit);
qint64 n_abs = (n > 0 ? n : -n);
qint64 quotient = n_abs / coin;
qint64 remainder = n_abs % coin;
QString quotient_str = QString::number(quotient);
QString remainder_str = QString::number(remainder).rightJustified(num_decimals, '0');
// Quotient has group (decimal) separators if locale has this enabled
QString quotient_str = locale.toString(quotient);
// Remainder does not have group separators
locale.setNumberOptions(QLocale::OmitGroupSeparator | QLocale::RejectGroupSeparator);
QString remainder_str = locale.toString(remainder).rightJustified(num_decimals, '0');
// Use SI-style thin space separators as these are locale independent and can't be
// confused with the decimal marker.
QChar thin_sp(THIN_SP_CP);
int q_size = quotient_str.size();
if (separators == separatorAlways || (separators == separatorStandard && q_size > 4))
for (int i = 3; i < q_size; i += 3)
quotient_str.insert(q_size - i, thin_sp);
if(fTrim)
{
// Right-trim excess zeros after the decimal point
int nTrim = 0;
for (int i = remainder_str.size()-1; i>=2 && (remainder_str.at(i) == '0'); --i)
++nTrim;
remainder_str.chop(nTrim);
}
if (n < 0)
quotient_str.insert(0, '-');
else if (fPlus && n > 0)
quotient_str.insert(0, '+');
return quotient_str + QString(".") + remainder_str;
return quotient_str + locale.decimalPoint() + remainder_str;
}
// TODO: Review all remaining calls to BitcoinUnits::formatWithUnit to
// TODO: determine whether the output is used in a plain text context
// TODO: or an HTML context (and replace with
@ -126,54 +143,64 @@ QString BitcoinUnits::format(int unit, const CAmount& nIn, bool fPlus, Separator
// Please take care to use formatHtmlWithUnit instead, when
// appropriate.
QString BitcoinUnits::formatWithUnit(int unit, const CAmount& amount, bool plussign, SeparatorStyle separators)
QString BitcoinUnits::formatWithUnit(int unit, const CAmount& amount, bool plussign, bool trim, const QLocale &locale)
{
return format(unit, amount, plussign, separators) + QString(" ") + name(unit);
return format(unit, amount, plussign, trim) + QString(" ") + name(unit);
}
QString BitcoinUnits::formatHtmlWithUnit(int unit, const CAmount& amount, bool plussign, SeparatorStyle separators)
QString BitcoinUnits::formatHtmlWithUnit(int unit, const CAmount& amount, bool plussign, bool trim, const QLocale &locale)
{
QString str(formatWithUnit(unit, amount, plussign, separators));
QString str(formatWithUnit(unit, amount, plussign, trim, locale));
str.replace(QChar(THIN_SP_CP), QString(THIN_SP_HTML));
return QString("<span style='white-space: nowrap;'>%1</span>").arg(str);
}
bool BitcoinUnits::parse(int unit, const QString &value, CAmount *val_out)
bool BitcoinUnits::parse(int unit, const QString &value, CAmount *val_out, const QLocale &locale_in)
{
if(!valid(unit) || value.isEmpty())
return false; // Refuse to parse invalid unit or empty string
QLocale locale(locale_in);
qint64 coin = factor(unit);
int num_decimals = decimals(unit);
// Ignore spaces and thin spaces when parsing
QStringList parts = removeSpaces(value).split(".");
QStringList parts = value.split(locale.decimalPoint());
bool ok = false;
if(parts.size() > 2)
{
return false; // More than one dot
}
QString whole = parts[0];
QString decimals;
return false; // More than one decimal point
// Parse whole part (may include locale-specific group separators)
#if QT_VERSION < 0x050000
qint64 whole = locale.toLongLong(parts[0], &ok, 10);
#else
qint64 whole = locale.toLongLong(parts[0], &ok);
#endif
if(!ok)
return false; // Parse error
if(whole > maxAmount(unit) || whole < 0)
return false; // Overflow or underflow
// Parse decimals part (if present, may not include group separators)
qint64 decimals = 0;
if(parts.size() > 1)
{
decimals = parts[1];
if(parts[1].size() > num_decimals)
return false; // Exceeds max precision
locale.setNumberOptions(QLocale::OmitGroupSeparator | QLocale::RejectGroupSeparator);
#if QT_VERSION < 0x050000
decimals = locale.toLongLong(parts[1].leftJustified(num_decimals, '0'), &ok, 10);
#else
decimals = locale.toLongLong(parts[1].leftJustified(num_decimals, '0'), &ok);
#endif
if(!ok || decimals < 0)
return false; // Parse error
}
if(decimals.size() > num_decimals)
{
return false; // Exceeds max precision
}
bool ok = false;
QString str = whole + decimals.leftJustified(num_decimals, '0');
if(str.size() > 18)
{
return false; // Longer numbers will exceed 63 bits
}
CAmount retvalue(str.toLongLong(&ok));
if(val_out)
{
*val_out = retvalue;
*val_out = whole * coin + decimals;
}
return ok;
}

View file

@ -9,6 +9,7 @@
#include <QAbstractListModel>
#include <QString>
#include <QLocale>
// U+2009 THIN SPACE = UTF-8 E2 80 89
#define REAL_THIN_SP_CP 0x2009
@ -82,15 +83,17 @@ public:
static QString description(int unit);
//! Number of Satoshis (1e-8) per unit
static qint64 factor(int unit);
//! Max amount per unit
static qint64 maxAmount(int unit);
//! Number of decimals left
static int decimals(int unit);
//! Format as string
static QString format(int unit, const CAmount& amount, bool plussign=false, SeparatorStyle separators=separatorStandard);
static QString format(int unit, const CAmount& amount, bool plussign=false, bool trim=true, const QLocale &locale=QLocale());
//! Format as string (with unit)
static QString formatWithUnit(int unit, const CAmount& amount, bool plussign=false, SeparatorStyle separators=separatorStandard);
static QString formatHtmlWithUnit(int unit, const CAmount& amount, bool plussign=false, SeparatorStyle separators=separatorStandard);
static QString formatWithUnit(int unit, const CAmount& amount, bool plussign=false, bool trim=true, const QLocale &locale=QLocale());
static QString formatHtmlWithUnit(int unit, const CAmount& amount, bool plussign=false, bool trim=true, const QLocale &locale=QLocale());
//! Parse string to coin amount
static bool parse(int unit, const QString &value, CAmount *val_out);
static bool parse(int unit, const QString &value, CAmount *val_out, const QLocale &locale=QLocale());
//! Gets title for amount column including current display unit if optionsModel reference available */
static QString getAmountColumnTitle(int unit);
///@}

View file

@ -161,7 +161,10 @@ bool parseBitcoinURI(const QUrl &uri, SendCoinsRecipient *out)
{
if(!i->second.isEmpty())
{
if(!BitcoinUnits::parse(BitcoinUnits::BTC, i->second, &rv.amount))
// Parse amount in C locale with no number separators
QLocale locale(QLocale::c());
locale.setNumberOptions(QLocale::OmitGroupSeparator | QLocale::RejectGroupSeparator);
if(!BitcoinUnits::parse(BitcoinUnits::BTC, i->second, &rv.amount, locale))
{
return false;
}