Merge pull request #400 from leofidus/1.7-localformat

1.7 number format according to system local
This commit is contained in:
langerhans 2014-04-02 22:02:22 +02:00
commit eaf799add2
8 changed files with 252 additions and 54 deletions

View file

@ -14,6 +14,46 @@
#include <QKeyEvent>
#include <qmath.h> // for qPow()
/** QDoubleSpinBox that shows number group seperators.
* In Qt 5.3+ this could be replaced with QAbstractSpinBox::setGroupSeparatorShown(true)
* See https://bugreports.qt-project.org/browse/QTBUG-5142
*
* TODO: We should not use a QDoubleSpinBox at all but implement our own
* spinbox for fixed-point numbers.
*/
class AmountSpinBox: public QDoubleSpinBox
{
public:
explicit AmountSpinBox(QWidget *parent):
QDoubleSpinBox(parent)
{
}
QString textFromValue(double value) const
{
return QLocale().toString(value, 'f', decimals());
}
QValidator::State validate (QString &text, int &pos) const
{
bool ok = false;
QValidator::State rv = QDoubleSpinBox::validate(text, pos);
if (rv == QValidator::Acceptable)
{
// Make sure that we only return acceptable if group seperators
// are in the right place. If not, a fixup step is needed first so
// return Intermediate.
QLocale().toDouble(text, &ok);
if (!ok)
return QValidator::Intermediate;
}
return rv;
}
double valueFromText(const QString& text) const
{
return QLocale().toDouble(text);
}
};
BitcoinAmountField::BitcoinAmountField(QWidget *parent) :
QWidget(parent),
amount(0),
@ -21,8 +61,7 @@ BitcoinAmountField::BitcoinAmountField(QWidget *parent) :
{
nSingleStep = 100000; // satoshis
amount = new QDoubleSpinBox(this);
amount->setLocale(QLocale::c());
amount = new AmountSpinBox(this);
amount->installEventFilter(this);
amount->setMaximumWidth(170);
@ -52,7 +91,7 @@ void BitcoinAmountField::setText(const QString &text)
if (text.isEmpty())
amount->clear();
else
amount->setValue(text.toDouble());
amount->setValue(QLocale().toDouble(text));
}
void BitcoinAmountField::clear()
@ -99,17 +138,6 @@ bool BitcoinAmountField::eventFilter(QObject *object, QEvent *event)
// Clear invalid flag on focus
setValid(true);
}
else if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease)
{
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Comma)
{
// Translate a comma into a period
QKeyEvent periodKeyEvent(event->type(), Qt::Key_Period, keyEvent->modifiers(), ".", keyEvent->isAutoRepeat(), keyEvent->count());
QApplication::sendEvent(object, &periodKeyEvent);
return true;
}
}
return QWidget::eventFilter(object, event);
}

View file

@ -5,6 +5,7 @@
#include "bitcoinunits.h"
#include <QStringList>
#include <QLocale>
BitcoinUnits::BitcoinUnits(QObject *parent):
QAbstractListModel(parent),
@ -108,71 +109,89 @@ int BitcoinUnits::decimals(int unit)
}
}
QString BitcoinUnits::format(int unit, qint64 n, bool fPlus)
QString BitcoinUnits::format(int unit, qint64 n, 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
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');
// 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(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)
else if (fPlus && n >= 0)
quotient_str.insert(0, '+');
return quotient_str + QString(".") + remainder_str;
return quotient_str + locale.decimalPoint() + remainder_str;
}
QString BitcoinUnits::formatWithUnit(int unit, qint64 amount, bool plussign)
QString BitcoinUnits::formatWithUnit(int unit, qint64 amount, bool plussign, bool trim, const QLocale &locale)
{
return format(unit, amount, plussign) + QString(" ") + name(unit);
return format(unit, amount, plussign, trim) + QString(" ") + name(unit);
}
bool BitcoinUnits::parse(int unit, const QString &value, qint64 *val_out)
bool BitcoinUnits::parse(int unit, const QString &value, qint64 *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);
QStringList parts = 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
}
qint64 retvalue = str.toLongLong(&ok);
if(val_out)
{
*val_out = retvalue;
*val_out = whole * coin + decimals;
}
return ok;
}

View file

@ -7,6 +7,7 @@
#include <QAbstractListModel>
#include <QString>
#include <QLocale>
/** Bitcoin unit definitions. Encapsulates parsing and formatting
and serves as list model for drop-down selection boxes.
@ -52,11 +53,11 @@ public:
//! Number of decimals left
static int decimals(int unit);
//! Format as string
static QString format(int unit, qint64 amount, bool plussign=false);
static QString format(int unit, qint64 amount, bool plussign=false, bool trim=true, const QLocale &locale=QLocale());
//! Format as string (with unit)
static QString formatWithUnit(int unit, qint64 amount, bool plussign=false);
static QString formatWithUnit(int unit, qint64 amount, bool plussign=false, bool trim=true, const QLocale &locale=QLocale());
//! Parse string to coin amount
static bool parse(int unit, const QString &value, qint64 *val_out);
static bool parse(int unit, const QString &value, qint64 *val_out, const QLocale &locale=QLocale());
///@}
//! @name AbstractListModel implementation

View file

@ -58,7 +58,7 @@ namespace GUIUtil {
QString dateTimeStr(const QDateTime &date)
{
return date.date().toString(Qt::SystemLocaleShortDate) + QString(" ") + date.toString("hh:mm");
return date.date().toString(Qt::DefaultLocaleShortDate) + QString(" ") + date.toString("hh:mm");
}
QString dateTimeStr(qint64 nTime)
@ -133,7 +133,10 @@ bool parseBitcoinURI(const QUrl &uri, SendCoinsRecipient *out)
{
if(!i->second.isEmpty())
{
if(!BitcoinUnits::parse(BitcoinUnits::DOGE, 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::DOGE, i->second, &rv.amount, locale))
{
return false;
}

View file

@ -8,13 +8,16 @@ AM_CPPFLAGS += -I$(top_srcdir)/src \
bin_PROGRAMS = test_dogecoin-qt
TESTS = test_dogecoin-qt
TEST_QT_MOC_CPP = moc_uritests.cpp
TEST_QT_MOC_CPP = \
moc_bitcoinunitstests.cpp \
moc_uritests.cpp
if ENABLE_WALLET
TEST_QT_MOC_CPP += moc_paymentservertests.cpp
endif
TEST_QT_H = \
bitcoinunitstests.h \
uritests.h \
paymentrequestdata.h \
paymentservertests.h
@ -24,6 +27,7 @@ BUILT_SOURCES = $(TEST_QT_MOC_CPP)
test_dogecoin_qt_CPPFLAGS = $(AM_CPPFLAGS) $(QT_INCLUDES) $(QT_TEST_INCLUDES)
test_dogecoin_qt_SOURCES = \
bitcoinunitstests.cpp \
test_main.cpp \
uritests.cpp \
$(TEST_QT_H)

View file

@ -0,0 +1,123 @@
#include "bitcoinunitstests.h"
#include "bitcoinunits.h"
#include <QUrl>
void BitcoinUnitsTests::parseTests()
{
qint64 value = 0;
/// Tests with en_US locale
QLocale locale1("en_US");
QVERIFY(BitcoinUnits::parse(BitcoinUnits::DOGE, "0", &value, locale1));
QCOMPARE(value, 0LL);
QVERIFY(BitcoinUnits::parse(BitcoinUnits::DOGE, "1", &value, locale1));
QCOMPARE(value, 100000000LL);
QVERIFY(BitcoinUnits::parse(BitcoinUnits::DOGE, "1.0", &value, locale1));
QCOMPARE(value, 100000000LL);
QVERIFY(BitcoinUnits::parse(BitcoinUnits::DOGE, "10.0", &value, locale1));
QCOMPARE(value, 1000000000LL);
QVERIFY(BitcoinUnits::parse(BitcoinUnits::DOGE, "1,000.0", &value, locale1));
QCOMPARE(value, 100000000000LL);
QVERIFY(BitcoinUnits::parse(BitcoinUnits::DOGE, "1,000,000.0", &value, locale1));
QCOMPARE(value, 100000000000000LL);
QVERIFY(BitcoinUnits::parse(BitcoinUnits::Koinu, "1,000,000,000", &value, locale1));
QCOMPARE(value, 1000000000LL);
QVERIFY(BitcoinUnits::parse(BitcoinUnits::kDOGE, "1.0", &value, locale1));
QCOMPARE(value, 100000000000LL);
QVERIFY(BitcoinUnits::parse(BitcoinUnits::MDOGE, "1.0", &value, locale1));
QCOMPARE(value, 100000000000000LL);
QVERIFY(BitcoinUnits::parse(BitcoinUnits::kDOGE, "0.001", &value, locale1));
QCOMPARE(value, 100000000LL);
QVERIFY(BitcoinUnits::parse(BitcoinUnits::MDOGE, "0.000001", &value, locale1));
QCOMPARE(value, 100000000LL);
QVERIFY(BitcoinUnits::parse(BitcoinUnits::DOGE, "0.00000001", &value, locale1));
QCOMPARE(value, 1LL);
QVERIFY(BitcoinUnits::parse(BitcoinUnits::kDOGE, "0.00000000001", &value, locale1));
QCOMPARE(value, 1LL);
QVERIFY(BitcoinUnits::parse(BitcoinUnits::MDOGE, "0.00000000000001", &value, locale1));
QCOMPARE(value, 1LL);
//Without seperator
QVERIFY(BitcoinUnits::parse(BitcoinUnits::DOGE, "1000.0", &value, locale1));
QCOMPARE(value, 100000000000LL);
//MAX_MONEY
QVERIFY(BitcoinUnits::parse(BitcoinUnits::DOGE, "10000000000", &value, locale1));
QCOMPARE(value, 1000000000000000000LL);
// Fail: group separator in wrong place
QVERIFY(!BitcoinUnits::parse(BitcoinUnits::DOGE, "0,00", &value, locale1));
// Fail: group separator in decimals
QVERIFY(!BitcoinUnits::parse(BitcoinUnits::DOGE, "0.0,000", &value, locale1));
// Fail: multiple decimal separators
QVERIFY(!BitcoinUnits::parse(BitcoinUnits::DOGE, "0.000.000", &value, locale1));
/// Tests with nl_NL locale
QLocale locale2("nl_NL");
QVERIFY(BitcoinUnits::parse(BitcoinUnits::DOGE, "1", &value, locale2));
QCOMPARE(value, 100000000LL);
QVERIFY(BitcoinUnits::parse(BitcoinUnits::DOGE, "1,0", &value, locale2));
QCOMPARE(value, 100000000LL);
QVERIFY(BitcoinUnits::parse(BitcoinUnits::Koinu, "1.000.000", &value, locale2));
QCOMPARE(value, 1000000LL);
// Fail: multiple decimal separators
QVERIFY(!BitcoinUnits::parse(BitcoinUnits::DOGE, "0,000,000", &value, locale2));
/// Tests with de_CH locale
QLocale locale3("de_CH");
QVERIFY(BitcoinUnits::parse(BitcoinUnits::DOGE, "123'456.78", &value, locale3));
QCOMPARE(value, 12345678000000LL);
// Fail: multiple decimal separators
QVERIFY(!BitcoinUnits::parse(BitcoinUnits::DOGE, "0.000.000", &value, locale3));
/// Tests with c locale
QLocale locale4(QLocale::c());
locale4.setNumberOptions(QLocale::OmitGroupSeparator | QLocale::RejectGroupSeparator);
QVERIFY(BitcoinUnits::parse(BitcoinUnits::DOGE, "1000.00000000", &value, locale4));
QCOMPARE(value, 100000000000LL);
// Fail: group separator
QVERIFY(!BitcoinUnits::parse(BitcoinUnits::DOGE, "1,000.00", &value, locale4));
// Fail: too many decimals
QVERIFY(!BitcoinUnits::parse(BitcoinUnits::DOGE, "1000.000000000", &value, locale4));
QVERIFY(!BitcoinUnits::parse(BitcoinUnits::Koinu, "1.0", &value, locale4));
//no overflow because Dogecoin has unlimited money supply
/*// Fail: overflow
QVERIFY(!BitcoinUnits::parse(BitcoinUnits::DOGE, "10000000000.1", &value, locale4));
QVERIFY(!BitcoinUnits::parse(BitcoinUnits::DOGE, "92233720368547758090.0", &value, locale4));*/
// Fail: underflow
QVERIFY(!BitcoinUnits::parse(BitcoinUnits::DOGE, "-1000000.0", &value, locale4));
// Fail: sign in decimals
QVERIFY(!BitcoinUnits::parse(BitcoinUnits::DOGE, "0.-1000000", &value, locale4));
}
void BitcoinUnitsTests::formatTests()
{
/// Tests with en_US locale
QLocale locale1("en_US");
QCOMPARE(BitcoinUnits::format(BitcoinUnits::DOGE, 0, false, false, locale1), QString("0.00000000"));
QCOMPARE(BitcoinUnits::format(BitcoinUnits::DOGE, 0, false, true, locale1), QString("0.00"));
QCOMPARE(BitcoinUnits::format(BitcoinUnits::kDOGE, 0, false, false, locale1), QString("0.00000000000"));
QCOMPARE(BitcoinUnits::format(BitcoinUnits::MDOGE, 0, false, false, locale1), QString("0.00000000000000"));
QCOMPARE(BitcoinUnits::format(BitcoinUnits::Koinu, 0, false, false, locale1), QString("0.0"));
QCOMPARE(BitcoinUnits::format(BitcoinUnits::DOGE, 0, true, false, locale1), QString("+0.00000000"));
QCOMPARE(BitcoinUnits::format(BitcoinUnits::Koinu, 100000000, false, true, locale1), QString("100,000,000.0"));
QCOMPARE(BitcoinUnits::format(BitcoinUnits::Koinu, 100000000, true, true, locale1), QString("+100,000,000.0"));
QCOMPARE(BitcoinUnits::formatWithUnit(BitcoinUnits::DOGE, 100000000000000LL, false, true, locale1), QString("1,000,000.00 DOGE"));
QCOMPARE(BitcoinUnits::formatWithUnit(BitcoinUnits::kDOGE, 100000000000000LL, false, true, locale1), QString("1,000.00 kDOGE"));
QCOMPARE(BitcoinUnits::formatWithUnit(BitcoinUnits::MDOGE, 100000000000000LL, false, true, locale1), QString("1.00 MDOGE"));
/// Tests with nl_NL locale
QLocale locale2("nl_NL");
QCOMPARE(BitcoinUnits::format(BitcoinUnits::DOGE, 100000000000000LL, false, true, locale2), QString("1.000.000,00"));
/// Tests with de_CH locale
QLocale locale3("de_CH");
QCOMPARE(BitcoinUnits::format(BitcoinUnits::DOGE, 100000000000000LL, false, true, locale3), QString("1'000'000.00"));
/// Tests with c locale (with and without group separators)
QLocale locale4(QLocale::c());
locale4.setNumberOptions(QLocale::OmitGroupSeparator | QLocale::RejectGroupSeparator);
QCOMPARE(BitcoinUnits::format(BitcoinUnits::DOGE, 100000000000000LL, false, true, QLocale::c()), QString("1,000,000.00"));
QCOMPARE(BitcoinUnits::format(BitcoinUnits::DOGE, 100000000000000LL, false, true, locale4), QString("1000000.00"));
}

View file

@ -0,0 +1,16 @@
#ifndef BITCOINUNITSTESTS_H
#define BITCOINUNITSTESTS_H
#include <QObject>
#include <QTest>
class BitcoinUnitsTests : public QObject
{
Q_OBJECT
private slots:
void formatTests();
void parseTests();
};
#endif // BITCOINUNITSTESTS_H

View file

@ -3,10 +3,11 @@
#include "bitcoin-config.h"
#endif
#include "bitcoinunitstests.h"
#include "uritests.h"
#ifdef ENABLE_WALLET
#include "paymentservertests.h"
#endif
#include "uritests.h"
#include <QCoreApplication>
#include <QObject>
@ -38,6 +39,9 @@ int main(int argc, char *argv[])
if (QTest::qExec(&test2) != 0)
fInvalid = true;
#endif
BitcoinUnitsTests test3;
if (QTest::qExec(&test3) != 0)
fInvalid = true;
return fInvalid;
}