Implement a nice money input field.

This commit is contained in:
John Preston 2021-04-01 13:27:39 +04:00
parent 0188719d04
commit 1cc1f380d0
9 changed files with 481 additions and 168 deletions

View file

@ -1891,9 +1891,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_payments_password_title" = "Payment Confirmation";
"lng_payments_password_description" = "Your card {card} is on file. To pay with this card, please enter your 2-Step-Verification password.";
"lng_payments_password_submit" = "Pay";
"lng_payments_tips_label" = "Tips";
"lng_payments_tips_title" = "Tips";
"lng_payments_tips_enter" = "Enter tips amount";
"lng_payments_tips_label" = "Tip (Optional)";
"lng_payments_tips_add" = "Add Tip";
"lng_payments_tips_box_title" = "Add Tip";
"lng_payments_tips_max" = "Max possible tip amount: {amount}";
"lng_call_status_incoming" = "is calling you...";
"lng_call_status_connecting" = "connecting...";

View file

@ -418,7 +418,8 @@ bool CheckoutProcess::panelWebviewNavigationAttempt(const QString &uri) {
}
void CheckoutProcess::panelCancelEdit() {
if (_submitState != SubmitState::None) {
if (_submitState != SubmitState::None
&& _submitState != SubmitState::Validated) {
return;
}
showForm();
@ -463,7 +464,8 @@ void CheckoutProcess::showForm() {
}
void CheckoutProcess::showEditInformation(Ui::InformationField field) {
if (_submitState != SubmitState::None) {
if (_submitState != SubmitState::None
&& _submitState != SubmitState::Validated) {
return;
}
_panel->showEditInformation(
@ -473,6 +475,8 @@ void CheckoutProcess::showEditInformation(Ui::InformationField field) {
}
void CheckoutProcess::showInformationError(Ui::InformationField field) {
Expects(_submitState != SubmitState::Validated);
if (_submitState != SubmitState::None) {
return;
}
@ -483,7 +487,8 @@ void CheckoutProcess::showInformationError(Ui::InformationField field) {
}
void CheckoutProcess::showCardError(Ui::CardField field) {
if (_submitState != SubmitState::None) {
if (_submitState != SubmitState::None
&& _submitState != SubmitState::Validated) {
return;
}
_panel->showCardError(_form->paymentMethod().ui.native, field);

View file

@ -59,6 +59,10 @@ paymentsIconPhone: icon {{ "payments/payment_phone", menuIconFg }};
paymentsIconShippingMethod: icon {{ "payments/payment_shipping", menuIconFg }};
paymentsField: defaultInputField;
paymentsFieldAdditional: FlatLabel(defaultFlatLabel) {
style: boxTextStyle;
}
paymentsFieldPadding: margins(28px, 0px, 28px, 2px);
paymentsSaveCheckboxPadding: margins(28px, 20px, 28px, 8px);
paymentsExpireCvcSkip: 34px;
@ -79,3 +83,9 @@ paymentsShippingPrice: FlatLabel(defaultFlatLabel) {
}
paymentsShippingLabelPosition: point(43px, 8px);
paymentsShippingPricePosition: point(43px, 29px);
paymentTipsErrorLabel: FlatLabel(defaultFlatLabel) {
minWidth: 275px;
textFg: boxTextFgError;
}
paymentTipsErrorPadding: margins(22px, 6px, 22px, 0px);

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/input_fields.h"
#include "ui/boxes/country_select_box.h"
#include "ui/text/format_values.h"
#include "ui/ui_utility.h"
#include "ui/special_fields.h"
#include "data/data_countries.h"
@ -16,12 +17,190 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/event_filter.h"
#include "styles/style_payments.h"
#include <QtCore/QRegularExpression>
namespace Payments::Ui {
namespace {
struct SimpleFieldState {
QString value;
int position = 0;
};
[[nodiscard]] char FieldThousandsSeparator(const CurrencyRule &rule) {
return (rule.thousands == '.' || rule.thousands == ',')
? ' '
: rule.thousands;
}
[[nodiscard]] QString RemoveNonNumbers(QString value) {
return value.replace(QRegularExpression("[^0-9]"), QString());
}
[[nodiscard]] SimpleFieldState CleanMoneyState(
const CurrencyRule &rule,
SimpleFieldState state) {
const auto withDecimal = state.value.replace(
QChar('.'),
rule.decimal
).replace(
QChar(','),
rule.decimal
);
const auto digitsLimit = 16 - rule.exponent;
const auto beforePosition = state.value.mid(0, state.position);
auto decimalPosition = withDecimal.lastIndexOf(rule.decimal);
if (decimalPosition < 0) {
state = {
.value = RemoveNonNumbers(state.value),
.position = RemoveNonNumbers(beforePosition).size(),
};
} else {
const auto onlyNumbersBeforeDecimal = RemoveNonNumbers(
state.value.mid(0, decimalPosition));
state = {
.value = (onlyNumbersBeforeDecimal
+ QChar(rule.decimal)
+ RemoveNonNumbers(state.value.mid(decimalPosition + 1))),
.position = (RemoveNonNumbers(beforePosition).size()
+ (state.position > decimalPosition ? 1 : 0)),
};
decimalPosition = onlyNumbersBeforeDecimal.size();
const auto maxLength = decimalPosition + 1 + rule.exponent;
if (state.value.size() > maxLength) {
state = {
.value = state.value.mid(0, maxLength),
.position = std::min(state.position, maxLength),
};
}
}
if (!state.value.isEmpty() && state.value[0] == QChar(rule.decimal)) {
state = {
.value = QChar('0') + state.value,
.position = state.position + 1,
};
if (decimalPosition >= 0) {
++decimalPosition;
}
}
auto skip = 0;
while (state.value.size() > skip + 1
&& state.value[skip] == QChar('0')
&& state.value[skip + 1] != QChar(rule.decimal)) {
++skip;
}
state = {
.value = state.value.mid(skip),
.position = std::max(state.position - skip, 0),
};
if (decimalPosition >= 0) {
Assert(decimalPosition >= skip);
decimalPosition -= skip;
}
if (decimalPosition > digitsLimit) {
state = {
.value = (state.value.mid(0, digitsLimit)
+ state.value.mid(decimalPosition)),
.position = (state.position > digitsLimit
? std::max(
state.position - (decimalPosition - digitsLimit),
digitsLimit)
: state.position),
};
}
return state;
}
[[nodiscard]] SimpleFieldState PostprocessMoneyResult(
const CurrencyRule &rule,
SimpleFieldState result) {
const auto position = result.value.indexOf(rule.decimal);
const auto from = (position >= 0) ? position : result.value.size();
for (auto insertAt = from - 3; insertAt > 0; insertAt -= 3) {
result.value.insert(insertAt, QChar(FieldThousandsSeparator(rule)));
if (result.position >= insertAt) {
++result.position;
}
}
return result;
}
[[nodiscard]] bool IsBackspace(const FieldValidateRequest &request) {
return (request.wasAnchor == request.wasPosition)
&& (request.wasPosition == request.nowPosition + 1)
&& (request.wasValue.midRef(0, request.wasPosition - 1)
== request.nowValue.midRef(0, request.nowPosition))
&& (request.wasValue.midRef(request.wasPosition)
== request.nowValue.midRef(request.nowPosition));
}
[[nodiscard]] bool IsDelete(const FieldValidateRequest &request) {
return (request.wasAnchor == request.wasPosition)
&& (request.wasPosition == request.nowPosition)
&& (request.wasValue.midRef(0, request.wasPosition)
== request.nowValue.midRef(0, request.nowPosition))
&& (request.wasValue.midRef(request.wasPosition + 1)
== request.nowValue.midRef(request.nowPosition));
}
[[nodiscard]] auto MoneyValidator(const CurrencyRule &rule) {
return [=](FieldValidateRequest request) {
const auto realNowState = [&] {
const auto backspaced = IsBackspace(request);
const auto deleted = IsDelete(request);
if (!backspaced && !deleted) {
return CleanMoneyState(rule, {
.value = request.nowValue,
.position = request.nowPosition,
});
}
const auto realWasState = CleanMoneyState(rule, {
.value = request.wasValue,
.position = request.wasPosition,
});
const auto changedValue = deleted
? (realWasState.value.mid(0, realWasState.position)
+ realWasState.value.mid(realWasState.position + 1))
: (realWasState.position > 1)
? (realWasState.value.mid(0, realWasState.position - 1)
+ realWasState.value.mid(realWasState.position))
: realWasState.value.mid(realWasState.position);
return SimpleFieldState{
.value = changedValue,
.position = (deleted
? realWasState.position
: std::max(realWasState.position - 1, 0))
};
}();
const auto postprocessed = PostprocessMoneyResult(
rule,
realNowState);
return FieldValidateResult{
.value = postprocessed.value,
.position = postprocessed.position,
};
};
}
[[nodiscard]] QString Parse(const FieldConfig &config) {
if (config.type == FieldType::Country) {
return Data::CountryNameByISO2(config.value);
} else if (config.type == FieldType::Money) {
const auto amount = config.value.toLongLong();
if (!amount) {
return QString();
}
const auto rule = LookupCurrencyRule(config.currency);
const auto value = std::abs(amount) / std::pow(10., rule.exponent);
const auto precision = (!rule.stripDotZero
|| std::floor(value) != value)
? rule.exponent
: 0;
return FormatWithSeparators(
value,
precision,
rule.decimal,
FieldThousandsSeparator(rule));
}
return config.value;
}
@ -32,6 +211,20 @@ namespace {
const QString &countryIso2) {
if (config.type == FieldType::Country) {
return countryIso2;
} else if (config.type == FieldType::Money) {
const auto rule = LookupCurrencyRule(config.currency);
const auto real = QString(parsed).replace(
QChar(rule.decimal),
QChar('.')
).replace(
QChar(','),
QChar('.')
).replace(
QRegularExpression("[^0-9\\.]"),
QString()
).toDouble();
return QString::number(
int64(std::round(real * std::pow(10., rule.exponent))));
}
return parsed;
}
@ -46,7 +239,7 @@ namespace {
case FieldType::CardCVC:
case FieldType::Country:
case FieldType::Phone:
case FieldType::PriceAmount:
case FieldType::Money:
return true;
}
Unexpected("FieldType in Payments::Ui::UseMaskedField.");
@ -68,7 +261,7 @@ namespace {
case FieldType::CardCVC:
case FieldType::Country:
case FieldType::Phone:
case FieldType::PriceAmount:
case FieldType::Money:
return base::make_unique_q<RpWidget>(parent);
}
Unexpected("FieldType in Payments::Ui::CreateWrap.");
@ -82,9 +275,106 @@ namespace {
: static_cast<InputField*>(wrap.get());
}
[[nodiscard]] MaskedInputField *CreateMoneyField(
not_null<RpWidget*> wrap,
FieldConfig &config,
rpl::producer<> textPossiblyChanged) {
struct State {
CurrencyRule rule;
style::InputField st;
QString currencyText;
int currencySkip = 0;
FlatLabel *left = nullptr;
FlatLabel *right = nullptr;
};
const auto state = wrap->lifetime().make_state<State>(State{
.rule = LookupCurrencyRule(config.currency),
.st = st::paymentsField,
});
const auto &rule = state->rule;
state->currencySkip = rule.space ? state->st.font->spacew : 0;
state->currencyText = ((!rule.left && rule.space)
? QString(QChar(' '))
: QString()) + (*rule.international
? QString(rule.international)
: config.currency) + ((rule.left && rule.space)
? QString(QChar(' '))
: QString());
if (rule.left) {
state->left = CreateChild<FlatLabel>(
wrap.get(),
state->currencyText,
st::paymentsFieldAdditional);
}
state->right = CreateChild<FlatLabel>(
wrap.get(),
QString(),
st::paymentsFieldAdditional);
const auto leftSkip = state->left
? (state->left->naturalWidth() + state->currencySkip)
: 0;
const auto rightSkip = st::paymentsFieldAdditional.style.font->width(
QString(QChar(rule.decimal))
+ QString(QChar('0')).repeated(rule.exponent)
+ (rule.left ? QString() : state->currencyText));
state->st.textMargins += QMargins(leftSkip, 0, rightSkip, 0);
state->st.placeholderMargins -= QMargins(leftSkip, 0, rightSkip, 0);
const auto result = CreateChild<MaskedInputField>(
wrap.get(),
state->st,
std::move(config.placeholder),
Parse(config));
result->setPlaceholderHidden(true);
if (state->left) {
state->left->move(0, state->st.textMargins.top());
}
const auto updateRight = [=] {
const auto text = result->getLastText();
const auto width = state->st.font->width(text);
const auto rect = result->getTextRect();
const auto &rule = state->rule;
const auto symbol = QChar(rule.decimal);
const auto decimal = text.indexOf(symbol);
const auto zeros = (decimal >= 0)
? std::max(rule.exponent - (text.size() - decimal - 1), 0)
: rule.stripDotZero
? 0
: rule.exponent;
const auto valueDecimalSeparator = (decimal >= 0 || !zeros)
? QString()
: QString(symbol);
const auto zeroString = QString(QChar('0'));
const auto valueRightPart = (text.isEmpty() ? zeroString : QString())
+ valueDecimalSeparator
+ zeroString.repeated(zeros);
const auto right = valueRightPart
+ (rule.left ? QString() : state->currencyText);
state->right->setText(right);
state->right->setTextColorOverride(valueRightPart.isEmpty()
? std::nullopt
: std::make_optional(st::windowSubTextFg->c));
state->right->move(
(state->st.textMargins.left()
+ width
+ ((rule.left || !valueRightPart.isEmpty())
? 0
: state->currencySkip)),
state->st.textMargins.top());
};
std::move(
textPossiblyChanged
) | rpl::start_with_next(updateRight, result->lifetime());
if (state->left) {
state->left->raise();
}
state->right->raise();
return result;
}
[[nodiscard]] MaskedInputField *LookupMaskedField(
not_null<RpWidget*> wrap,
FieldConfig &config) {
FieldConfig &config,
rpl::producer<> textPossiblyChanged) {
if (!UseMaskedField(config.type)) {
return nullptr;
}
@ -96,7 +386,6 @@ namespace {
case FieldType::CardExpireDate:
case FieldType::CardCVC:
case FieldType::Country:
case FieldType::PriceAmount:
return CreateChild<MaskedInputField>(
wrap.get(),
st::paymentsField,
@ -109,6 +398,11 @@ namespace {
std::move(config.placeholder),
ExtractPhonePrefix(config.defaultPhone),
Parse(config));
case FieldType::Money:
return CreateMoneyField(
wrap,
config,
std::move(textPossiblyChanged));
}
Unexpected("FieldType in Payments::Ui::LookupMaskedField.");
}
@ -119,7 +413,10 @@ Field::Field(QWidget *parent, FieldConfig &&config)
: _config(config)
, _wrap(CreateWrap(parent, config))
, _input(LookupInputField(_wrap.get(), config))
, _masked(LookupMaskedField(_wrap.get(), config))
, _masked(LookupMaskedField(
_wrap.get(),
config,
_textPossiblyChanged.events_starting_with({})))
, _countryIso2(config.value) {
if (_masked) {
setupMaskedGeometry();
@ -129,6 +426,8 @@ Field::Field(QWidget *parent, FieldConfig &&config)
}
if (const auto &validator = config.validator) {
setupValidator(validator);
} else if (config.type == FieldType::Money) {
setupValidator(MoneyValidator(LookupCurrencyRule(config.currency)));
}
setupFrontBackspace();
}
@ -210,7 +509,7 @@ void Field::setupValidator(Fn<ValidateResult(ValidateRequest)> validator) {
const auto selectionStart = _masked->selectionStart();
const auto selectionEnd = _masked->selectionEnd();
return {
.value = value(),
.value = _masked->getLastText(),
.position = position,
.anchor = (selectionStart == selectionEnd
? position
@ -221,7 +520,7 @@ void Field::setupValidator(Fn<ValidateResult(ValidateRequest)> validator) {
}
const auto cursor = _input->textCursor();
return {
.value = value(),
.value = _input->getLastText(),
.position = cursor.position(),
.anchor = cursor.anchor(),
};
@ -253,6 +552,7 @@ void Field::setupValidator(Fn<ValidateResult(ValidateRequest)> validator) {
const auto guard = gsl::finally([&] {
_validating = false;
save();
_textPossiblyChanged.fire({});
});
const auto now = state();

View file

@ -29,7 +29,7 @@ enum class FieldType {
Country,
Phone,
Email,
PriceAmount,
Money,
};
struct FieldValidateRequest {
@ -83,6 +83,7 @@ struct FieldConfig {
QString value;
Fn<FieldValidateResult(FieldValidateRequest)> validator;
Fn<void(object_ptr<BoxContent>)> showBox;
QString currency;
QString defaultPhone;
QString defaultCountry;
};
@ -124,6 +125,7 @@ private:
const base::unique_qptr<RpWidget> _wrap;
rpl::event_stream<> _frontBackspace;
rpl::event_stream<> _finished;
rpl::event_stream<> _textPossiblyChanged; // Must be above _masked.
InputField *_input = nullptr;
MaskedInputField *_masked = nullptr;
QString _countryIso2;

View file

@ -279,7 +279,9 @@ void FormSummary::setupPrices(not_null<VerticalLayout*> layout) {
add(tr::lng_payments_tips_label(tr::now), tips);
}
} else if (_invoice.tipsMax > 0) {
const auto text = formatAmount(_invoice.tipsSelected);
const auto text = _invoice.tipsSelected
? formatAmount(_invoice.tipsSelected)
: tr::lng_payments_tips_add(tr::now);
const auto label = addRow(
tr::lng_payments_tips_label(tr::now),
Ui::Text::Link(text, "internal:edit_tips"));

View file

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "payments/ui/payments_field.h"
#include "ui/widgets/separate_panel.h"
#include "ui/widgets/checkbox.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/boxes/single_choice_box.h"
#include "ui/text/format_values.h"
#include "lang/lang_keys.h"
@ -23,18 +24,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_layers.h"
namespace Payments::Ui {
namespace {
[[nodiscard]] auto PriceAmountValidator(int64 min, int64 max) {
return [=](FieldValidateRequest request) {
return FieldValidateResult{
.value = request.nowValue,
.position = request.nowPosition,
};
};
}
} // namespace
Panel::Panel(not_null<PanelDelegate*> delegate)
: _delegate(delegate)
@ -193,27 +182,52 @@ void Panel::chooseTips(const Invoice &invoice) {
const auto min = invoice.tipsMin;
const auto max = invoice.tipsMax;
const auto now = invoice.tipsSelected;
const auto currency = invoice.currency;
showBox(Box([=](not_null<Ui::GenericBox*> box) {
box->setTitle(tr::lng_payments_tips_title());
box->setTitle(tr::lng_payments_tips_box_title());
const auto row = box->lifetime().make_state<Field>(
box,
FieldConfig{
.type = FieldType::PriceAmount,
.placeholder = tr::lng_payments_tips_enter(),
.type = FieldType::Money,
.value = QString::number(now),
.validator = PriceAmountValidator(min, max),
.currency = ([&]() -> QString {
static auto counter = 0;
switch (++counter % 9) {
case 0: return "USD";
case 1: return "EUR";
case 2: return "IRR";
case 3: return "BRL";
case 4: return "ALL";
case 5: return "AZN";
case 6: return "CHF";
case 7: return "DKK";
case 8: return "KZT";
}
return currency;
})(), // #TODO payments currency,
});
box->setFocusCallback([=] {
row->setFocusFast();
});
box->addRow(row->ownedWidget());
box->addRow(object_ptr<FlatLabel>(box, "Min: " + QString::number(min), st::defaultFlatLabel));
box->addRow(object_ptr<FlatLabel>(box, "Max: " + QString::number(max), st::defaultFlatLabel));
const auto errorWrap = box->addRow(
object_ptr<FadeWrap<FlatLabel>>(
box,
object_ptr<FlatLabel>(
box,
tr::lng_payments_tips_max(
lt_amount,
rpl::single(FillAmountAndCurrency(max, currency))),
st::paymentTipsErrorLabel)),
st::paymentTipsErrorPadding);
errorWrap->hide(anim::type::instant);
box->addButton(tr::lng_settings_save(), [=] {
const auto value = row->value().toLongLong();
if (value < min || value > max) {
if (value < min) {
row->showError();
} else if (value > max) {
row->showError();
errorWrap->show(anim::type::normal);
} else {
_delegate->panelChangeTips(value);
box->closeBox();

View file

@ -45,40 +45,6 @@ namespace {
return phrase(tr::now, lt_ready, readyStr, lt_total, totalStr, lt_mb, mb);
}
[[nodiscard]] QString FormatWithSeparators(
double amount,
int precision,
char decimal,
char thousands) {
Expects(decimal != 0);
// Thanks https://stackoverflow.com/a/5058949
struct FormattingHelper : std::numpunct<char> {
FormattingHelper(char decimal, char thousands)
: decimal(decimal)
, thousands(thousands) {
}
char do_decimal_point() const override { return decimal; }
char do_thousands_sep() const override { return thousands; }
char decimal = '.';
char thousands = ',';
};
auto stream = std::ostringstream();
stream.imbue(std::locale(
stream.getloc(),
new FormattingHelper(decimal, thousands ? thousands : '?')));
stream.precision(precision);
stream << std::fixed << amount;
auto result = QString::fromStdString(stream.str());
if (!thousands) {
result.replace('?', QString());
}
return result;
}
} // namespace
QString FormatSizeText(qint64 size) {
@ -162,16 +128,37 @@ QString FormatPlayedText(qint64 played, qint64 duration) {
}
QString FillAmountAndCurrency(int64 amount, const QString &currency) {
struct Rule {
//const char *name = "";
//const char *native = "";
const char *international = "";
char thousands = ',';
char decimal = '.';
bool left = true;
bool space = false;
};
static const auto kRules = std::vector<std::pair<QString, Rule>>{
const auto rule = LookupCurrencyRule(currency);
const auto prefix = (amount < 0)
? QString::fromUtf8("\xe2\x88\x92")
: QString();
const auto value = std::abs(amount) / std::pow(10., rule.exponent);
const auto name = (*rule.international)
? QString::fromUtf8(rule.international)
: currency;
auto result = prefix;
if (rule.left) {
result.append(name);
if (rule.space) result.append(' ');
}
const auto precision = (!rule.stripDotZero || std::floor(value) != value)
? rule.exponent
: 0;
result.append(FormatWithSeparators(
value,
precision,
rule.decimal,
rule.thousands));
if (!rule.left) {
if (rule.space) result.append(' ');
result.append(name);
}
return result;
}
CurrencyRule LookupCurrencyRule(const QString &currency) {
static const auto kRules = std::vector<std::pair<QString, CurrencyRule>>{
{ u"AED"_q, { "", ',', '.', true, true } },
{ u"AFN"_q, {} },
{ u"ALL"_q, { "", '.', ',', false } },
@ -185,11 +172,11 @@ QString FillAmountAndCurrency(int64 amount, const QString &currency) {
{ u"BND"_q, { "", '.', ',', } },
{ u"BOB"_q, { "", '.', ',', true, true } },
{ u"BRL"_q, { "R$", '.', ',', true, true } },
{ u"BHD"_q, { "", ',', '.', true, true } },
{ u"BYR"_q, { "", ' ', ',', false, true } },
{ u"BHD"_q, { "", ',', '.', true, true, 3 } },
{ u"BYR"_q, { "", ' ', ',', false, true, 0 } },
{ u"CAD"_q, { "CA$" } },
{ u"CHF"_q, { "", '\'', '.', false, true } },
{ u"CLP"_q, { "", '.', ',', true, true } },
{ u"CLP"_q, { "", '.', ',', true, true, 0 } },
{ u"CNY"_q, { "\x43\x4E\xC2\xA5" } },
{ u"COP"_q, { "", '.', ',', true, true } },
{ u"CRC"_q, { "", '.', ',', } },
@ -209,12 +196,12 @@ QString FillAmountAndCurrency(int64 amount, const QString &currency) {
{ u"IDR"_q, { "", '.', ',', } },
{ u"ILS"_q, { "\xE2\x82\xAA", ',', '.', true, true } },
{ u"INR"_q, { "\xE2\x82\xB9" } },
{ u"ISK"_q, { "", '.', ',', false, true } },
{ u"ISK"_q, { "", '.', ',', false, true, 0 } },
{ u"JMD"_q, {} },
{ u"JPY"_q, { "\xC2\xA5" } },
{ u"JPY"_q, { "\xC2\xA5", ',', '.', true, false, 0 } },
{ u"KES"_q, {} },
{ u"KGS"_q, { "", ' ', '-', false, true } },
{ u"KRW"_q, { "\xE2\x82\xA9" } },
{ u"KRW"_q, { "\xE2\x82\xA9", ',', '.', true, false, 0 } },
{ u"KZT"_q, { "", ' ', '-', } },
{ u"LBP"_q, { "", ',', '.', true, true } },
{ u"LKR"_q, { "", ',', '.', true, true } },
@ -236,7 +223,7 @@ QString FillAmountAndCurrency(int64 amount, const QString &currency) {
{ u"PHP"_q, {} },
{ u"PKR"_q, {} },
{ u"PLN"_q, { "", ' ', ',', false, true } },
{ u"PYG"_q, { "", '.', ',', true, true } },
{ u"PYG"_q, { "", '.', ',', true, true, 0 } },
{ u"QAR"_q, { "", ',', '.', true, true } },
{ u"RON"_q, { "", '.', ',', false, true } },
{ u"RSD"_q, { "", '.', ',', false, true } },
@ -251,27 +238,27 @@ QString FillAmountAndCurrency(int64 amount, const QString &currency) {
{ u"TWD"_q, { "NT$" } },
{ u"TZS"_q, {} },
{ u"UAH"_q, { "", ' ', ',', false } },
{ u"UGX"_q, {} },
{ u"UGX"_q, { "", ',', '.', true, false, 0 } },
{ u"USD"_q, { "$" } },
{ u"UYU"_q, { "", '.', ',', true, true } },
{ u"UZS"_q, { "", ' ', ',', false, true } },
{ u"VND"_q, { "\xE2\x82\xAB", '.', ',', false, true } },
{ u"VND"_q, { "\xE2\x82\xAB", '.', ',', false, true, 0 } },
{ u"YER"_q, { "", ',', '.', true, true } },
{ u"ZAR"_q, { "", ',', '.', true, true } },
{ u"IRR"_q, { "", ',', '/', false, true } },
{ u"IQD"_q, { "", ',', '.', true, true } },
{ u"IRR"_q, { "", ',', '/', false, true, 2, true } },
{ u"IQD"_q, { "", ',', '.', true, true, 3 } },
{ u"VEF"_q, { "", '.', ',', true, true } },
{ u"SYP"_q, { "", ',', '.', true, true } },
//{ u"VUV"_q, { "", ',', '.', false } },
//{ u"VUV"_q, { "", ',', '.', false, false, 0 } },
//{ u"WST"_q, {} },
//{ u"XAF"_q, { "FCFA", ',', '.', false } },
//{ u"XAF"_q, { "FCFA", ',', '.', false, false, 0 } },
//{ u"XCD"_q, {} },
//{ u"XOF"_q, { "CFA", ' ', ',', false } },
//{ u"XPF"_q, { "", ',', '.', false } },
//{ u"XOF"_q, { "CFA", ' ', ',', false, false, 0 } },
//{ u"XPF"_q, { "", ',', '.', false, false, 0 } },
//{ u"ZMW"_q, {} },
//{ u"ANG"_q, {} },
//{ u"RWF"_q, { "", ' ', ',', true, true } },
//{ u"RWF"_q, { "", ' ', ',', true, true, 0 } },
//{ u"PGK"_q, {} },
//{ u"TOP"_q, {} },
//{ u"SBD"_q, {} },
@ -286,109 +273,85 @@ QString FillAmountAndCurrency(int64 amount, const QString &currency) {
//{ u"AOA"_q, {} },
//{ u"AWG"_q, {} },
//{ u"BBD"_q, {} },
//{ u"BIF"_q, { "", ',', '.', false } },
//{ u"BIF"_q, { "", ',', '.', false, false, 0 } },
//{ u"BMD"_q, {} },
//{ u"BSD"_q, {} },
//{ u"BWP"_q, {} },
//{ u"BZD"_q, {} },
//{ u"CDF"_q, { "", ',', '.', false } },
//{ u"CVE"_q, {} },
//{ u"DJF"_q, { "", ',', '.', false } },
//{ u"CVE"_q, { "", ',', '.', true, false, 0 } },
//{ u"DJF"_q, { "", ',', '.', false, false, 0 } },
//{ u"ETB"_q, {} },
//{ u"FJD"_q, {} },
//{ u"FKP"_q, {} },
//{ u"GIP"_q, {} },
//{ u"GMD"_q, { "", ',', '.', false } },
//{ u"GNF"_q, { "", ',', '.', false } },
//{ u"GNF"_q, { "", ',', '.', false, false, 0 } },
//{ u"GYD"_q, {} },
//{ u"HTG"_q, {} },
//{ u"KHR"_q, { "", ',', '.', false } },
//{ u"KMF"_q, { "", ',', '.', false } },
//{ u"KMF"_q, { "", ',', '.', false, false, 0 } },
//{ u"KYD"_q, {} },
//{ u"LAK"_q, { "", ',', '.', false } },
//{ u"LRD"_q, {} },
//{ u"LSL"_q, { "", ',', '.', false } },
//{ u"MGA"_q, {} },
//{ u"MGA"_q, { "", ',', '.', true, false, 0 } },
//{ u"MKD"_q, { "", '.', ',', false, true } },
//{ u"MOP"_q, {} },
//{ u"MWK"_q, {} },
//{ u"NAD"_q, {} },
//{ u"CLF"_q, { "", ',', '.', true, false, 4 } },
//{ u"JOD"_q, { "", ',', '.', true, false, 3 } },
//{ u"KWD"_q, { "", ',', '.', true, false, 3 } },
//{ u"LYD"_q, { "", ',', '.', true, false, 3 } },
//{ u"OMR"_q, { "", ',', '.', true, false, 3 } },
//{ u"TND"_q, { "", ',', '.', true, false, 3 } },
//{ u"UYI"_q, { "", ',', '.', true, false, 0 } },
//{ u"MRO"_q, { "", ',', '.', true, false, 1 } },
};
static const auto kRulesMap = [] {
// flat_multi_map_pair_type lacks some required constructors :(
auto &&pairs = kRules | ranges::views::transform([](auto &&pair) {
return base::flat_multi_map_pair_type<QString, Rule>(
auto &&list = kRules | ranges::views::transform([](auto &&pair) {
return base::flat_multi_map_pair_type<QString, CurrencyRule>(
pair.first,
pair.second);
});
return base::flat_map<QString, Rule>(begin(pairs), end(pairs));
return base::flat_map<QString, CurrencyRule>(begin(list), end(list));
}();
static const auto kExponents = base::flat_map<QString, int>{
{ u"CLF"_q, 4 },
{ u"BHD"_q, 3 },
{ u"IQD"_q, 3 },
{ u"JOD"_q, 3 },
{ u"KWD"_q, 3 },
{ u"LYD"_q, 3 },
{ u"OMR"_q, 3 },
{ u"TND"_q, 3 },
{ u"BIF"_q, 0 },
{ u"BYR"_q, 0 },
{ u"CLP"_q, 0 },
{ u"CVE"_q, 0 },
{ u"DJF"_q, 0 },
{ u"GNF"_q, 0 },
{ u"ISK"_q, 0 },
{ u"JPY"_q, 0 },
{ u"KMF"_q, 0 },
{ u"KRW"_q, 0 },
{ u"MGA"_q, 0 },
{ u"PYG"_q, 0 },
{ u"RWF"_q, 0 },
{ u"UGX"_q, 0 },
{ u"UYI"_q, 0 },
{ u"VND"_q, 0 },
{ u"VUV"_q, 0 },
{ u"XAF"_q, 0 },
{ u"XOF"_q, 0 },
{ u"XPF"_q, 0 },
{ u"MRO"_q, 1 },
const auto i = kRulesMap.find(currency);
return (i != end(kRulesMap)) ? i->second : CurrencyRule{};
}
[[nodiscard]] QString FormatWithSeparators(
double amount,
int precision,
char decimal,
char thousands) {
Expects(decimal != 0);
// Thanks https://stackoverflow.com/a/5058949
struct FormattingHelper : std::numpunct<char> {
FormattingHelper(char decimal, char thousands)
: decimal(decimal)
, thousands(thousands) {
}
char do_decimal_point() const override { return decimal; }
char do_thousands_sep() const override { return thousands; }
char decimal = '.';
char thousands = ',';
};
const auto prefix = (amount < 0)
? QString::fromUtf8("\xe2\x88\x92")
: QString();
const auto exponentIt = kExponents.find(currency);
const auto exponent = (exponentIt != end(kExponents))
? exponentIt->second
: 2;
const auto value = std::abs(amount) / std::pow(10., exponent);
const auto ruleIt = kRulesMap.find(currency);
if (ruleIt == end(kRulesMap)) {
return prefix + QLocale::system().toCurrencyString(value, currency);
}
const auto &rule = ruleIt->second;
const auto name = (*rule.international)
? QString::fromUtf8(rule.international)
: currency;
auto result = prefix;
if (rule.left) {
result.append(name);
if (rule.space) result.append(' ');
}
const auto precision = (currency != u"IRR"_q
|| std::floor(value) != value)
? exponent
: 0;
result.append(FormatWithSeparators(
value,
precision,
rule.decimal,
rule.thousands));
if (!rule.left) {
if (rule.space) result.append(' ');
result.append(name);
auto stream = std::ostringstream();
stream.imbue(std::locale(
stream.getloc(),
new FormattingHelper(decimal, thousands ? thousands : '?')));
stream.precision(precision);
stream << std::fixed << amount;
auto result = QString::fromStdString(stream.str());
if (!thousands) {
result.replace('?', QString());
}
return result;
}

View file

@ -23,9 +23,25 @@ inline constexpr auto FileStatusSizeFailed = 0x7FFFFFF2;
[[nodiscard]] QString FormatGifAndSizeText(qint64 size);
[[nodiscard]] QString FormatPlayedText(qint64 played, qint64 duration);
struct CurrencyRule {
const char *international = "";
char thousands = ',';
char decimal = '.';
bool left = true;
bool space = false;
int exponent = 2;
bool stripDotZero = false;
};
[[nodiscard]] QString FillAmountAndCurrency(
int64 amount,
const QString &currency);
[[nodiscard]] CurrencyRule LookupCurrencyRule(const QString &currency);
[[nodiscard]] QString FormatWithSeparators(
double amount,
int precision,
char decimal,
char thousands);
[[nodiscard]] QString ComposeNameString(
const QString &filename,