Browse Source

locale amounts: consistently use "." as dec point, and " " as thou sep

Always use "." as decimal point, and " " as thousands separator.

Previously,
- for decimal point, we were using
  - "." in some places (e.g. AmountEdit, most fiat amounts), and
  - `locale.localeconv()['decimal_point']` in others.
- for thousands separator, we were using
  - "," in some places (most fiat amounts), and
  - " " in others (format_satoshis)

I think it is better to be consistent even if whatever we pick differs from the locale.
Using whitespace for thousands separator (vs comma) is probably less confusing for people
whose locale would user "." for ts and "," for dp (as in e.g. German).

The alternative option would be to always use the locale. Even if we decide to do that later,
this refactoring should be useful.

closes https://github.com/spesmilo/electrum/issues/2629
master
SomberNight 3 years ago
parent
commit
2a9909c252
No known key found for this signature in database
GPG Key ID: B33B5F232C6271E9
  1. 4
      electrum/commands.py
  2. 19
      electrum/exchange_rate.py
  3. 4
      electrum/gui/qml/qefx.py
  4. 18
      electrum/gui/qt/amountedit.py
  5. 2
      electrum/gui/qt/main_window.py
  6. 8
      electrum/tests/test_wallet.py
  7. 12
      electrum/util.py
  8. 4
      electrum/wallet.py

4
electrum/commands.py

@ -1339,8 +1339,8 @@ class Commands:
except InvalidOperation:
raise Exception("from_amount is not a number")
return {
"from_amount": self.daemon.fx.ccy_amount_str(from_amount, False, from_ccy),
"to_amount": self.daemon.fx.ccy_amount_str(to_amount, False, to_ccy),
"from_amount": self.daemon.fx.ccy_amount_str(from_amount, add_thousands_sep=False, ccy=from_ccy),
"to_amount": self.daemon.fx.ccy_amount_str(to_amount, add_thousands_sep=False, ccy=to_ccy),
"from_ccy": from_ccy,
"to_ccy": to_ccy,
"source": self.daemon.fx.exchange.name(),

19
electrum/exchange_rate.py

@ -556,17 +556,24 @@ class FxThread(ThreadJob, EventListener):
return d.get(ccy, [])
@staticmethod
def remove_thousands_separator(text):
return text.replace(',', '') # FIXME use THOUSAND_SEPARATOR in util
def remove_thousands_separator(text: str) -> str:
return text.replace(util.THOUSANDS_SEP, "")
def ccy_amount_str(self, amount, commas, ccy=None):
def ccy_amount_str(self, amount, *, add_thousands_sep: bool = False, ccy=None) -> str:
prec = CCY_PRECISIONS.get(self.ccy if ccy is None else ccy, 2)
fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec)) # FIXME use util.THOUSAND_SEPARATOR and util.DECIMAL_POINT
fmt_str = "{:%s.%df}" % ("," if add_thousands_sep else "", max(0, prec))
try:
rounded_amount = round(amount, prec)
except decimal.InvalidOperation:
rounded_amount = amount
return fmt_str.format(rounded_amount)
text = fmt_str.format(rounded_amount)
# replace "," -> THOUSANDS_SEP
# replace "." -> DECIMAL_POINT
dp_loc = text.find(".")
text = text.replace(",", util.THOUSANDS_SEP)
if dp_loc == -1:
return text
return text[:dp_loc] + util.DECIMAL_POINT + text[dp_loc+1:]
async def run(self):
while True:
@ -683,7 +690,7 @@ class FxThread(ThreadJob, EventListener):
def format_fiat(self, value: Decimal) -> str:
if value.is_nan():
return _("No data")
return "%s" % (self.ccy_amount_str(value, True))
return self.ccy_amount_str(value, add_thousands_sep=True)
def history_rate(self, d_t: Optional[datetime]) -> Decimal:
if d_t is None:

4
electrum/gui/qml/qefx.py

@ -108,7 +108,7 @@ class QEFX(QObject, QtEventListener):
except:
return ''
if plain:
return self.fx.ccy_amount_str(self.fx.fiat_value(satoshis, rate), False)
return self.fx.ccy_amount_str(self.fx.fiat_value(satoshis, rate), add_thousands_sep=False)
else:
return self.fx.value_str(satoshis, rate)
@ -133,7 +133,7 @@ class QEFX(QObject, QtEventListener):
return ''
dt = datetime.fromtimestamp(int(td))
if plain:
return self.fx.ccy_amount_str(self.fx.historical_value(satoshis, dt), False)
return self.fx.ccy_amount_str(self.fx.historical_value(satoshis, dt), add_thousands_sep=False)
else:
return self.fx.historical_value_str(satoshis, dt)

18
electrum/gui/qt/amountedit.py

@ -10,7 +10,7 @@ from PyQt5.QtWidgets import (QLineEdit, QStyle, QStyleOptionFrame, QSizePolicy)
from .util import char_width_in_lineedit, ColorScheme
from electrum.util import (format_satoshis_plain, decimal_point_to_base_unit_name,
FEERATE_PRECISION, quantize_feerate)
FEERATE_PRECISION, quantize_feerate, DECIMAL_POINT)
from electrum.bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
@ -66,13 +66,13 @@ class AmountEdit(SizedFreezableLineEdit):
return
pos = self.cursorPosition()
chars = '0123456789'
if not self.is_int: chars +='.'
if not self.is_int: chars += DECIMAL_POINT
s = ''.join([i for i in text if i in chars])
if not self.is_int:
if '.' in s:
p = s.find('.')
s = s.replace('.','')
s = s[:p] + '.' + s[p:p+self.max_precision()]
if DECIMAL_POINT in s:
p = s.find(DECIMAL_POINT)
s = s.replace(DECIMAL_POINT, '')
s = s[:p] + DECIMAL_POINT + s[p:p+self.max_precision()]
if self.max_amount:
if (amt := self._get_amount_from_text(s)) and amt >= self.max_amount:
s = self._get_text_from_amount(self.max_amount)
@ -95,6 +95,7 @@ class AmountEdit(SizedFreezableLineEdit):
def _get_amount_from_text(self, text: str) -> Union[None, Decimal, int]:
try:
text = text.replace(DECIMAL_POINT, '.')
return (int if self.is_int else Decimal)(text)
except:
return None
@ -127,6 +128,7 @@ class BTCAmountEdit(AmountEdit):
def _get_amount_from_text(self, text):
# returns amt in satoshis
try:
text = text.replace(DECIMAL_POINT, '.')
x = Decimal(text)
except:
return None
@ -141,7 +143,9 @@ class BTCAmountEdit(AmountEdit):
return Decimal(amount) if not self.is_int else int(amount)
def _get_text_from_amount(self, amount_sat):
return format_satoshis_plain(amount_sat, decimal_point=self.decimal_point())
text = format_satoshis_plain(amount_sat, decimal_point=self.decimal_point())
text = text.replace('.', DECIMAL_POINT)
return text
def setAmount(self, amount_sat):
if amount_sat is None:

2
electrum/gui/qt/main_window.py

@ -926,7 +926,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
else:
fiat_e.follows = True
fiat_e.setText(self.fx.ccy_amount_str(
amount * Decimal(rate) / COIN, False))
amount * Decimal(rate) / COIN, add_thousands_sep=False))
fiat_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet())
fiat_e.follows = False

8
electrum/tests/test_wallet.py

@ -133,19 +133,19 @@ class TestFiat(ElectrumTestCase):
self.fx = FakeFxThread(FakeExchange(Decimal('1000.001')))
default_fiat = Abstract_Wallet.default_fiat_value(self.wallet, txid, self.fx, self.value_sat)
self.assertEqual(Decimal('1000.001'), default_fiat)
self.assertEqual('1,000.00', self.fx.ccy_amount_str(default_fiat, commas=True))
self.assertEqual('1 000.00', self.fx.ccy_amount_str(default_fiat, add_thousands_sep=True))
def test_save_fiat_and_reset(self):
self.assertEqual(False, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '1000.01', self.fx, self.value_sat))
saved = self.fiat_value[ccy][txid]
self.assertEqual('1,000.01', self.fx.ccy_amount_str(Decimal(saved), commas=True))
self.assertEqual('1 000.01', self.fx.ccy_amount_str(Decimal(saved), add_thousands_sep=True))
self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '', self.fx, self.value_sat))
self.assertNotIn(txid, self.fiat_value[ccy])
# even though we are not setting it to the exact fiat value according to the exchange rate, precision is truncated away
self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '1,000.002', self.fx, self.value_sat))
self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '1 000.002', self.fx, self.value_sat))
def test_too_high_precision_value_resets_with_no_saved_value(self):
self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '1,000.001', self.fx, self.value_sat))
self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '1 000.001', self.fx, self.value_sat))
def test_empty_resets(self):
self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '', self.fx, self.value_sat))

12
electrum/util.py

@ -33,7 +33,7 @@ import urllib
import threading
import hmac
import stat
from locale import localeconv
import locale
import asyncio
import urllib.request, urllib.parse, urllib.error
import builtins
@ -698,7 +698,11 @@ def format_satoshis_plain(
# We enforce that we have at least that available.
assert decimal.getcontext().prec >= 28, f"PyDecimal precision too low: {decimal.getcontext().prec}"
DECIMAL_POINT = localeconv()['decimal_point'] # type: str
# DECIMAL_POINT = locale.localeconv()['decimal_point'] # type: str
DECIMAL_POINT = "."
THOUSANDS_SEP = " "
assert len(DECIMAL_POINT) == 1, f"DECIMAL_POINT has unexpected len. {DECIMAL_POINT!r}"
assert len(THOUSANDS_SEP) == 1, f"THOUSANDS_SEP has unexpected len. {THOUSANDS_SEP!r}"
def format_satoshis(
@ -737,9 +741,9 @@ def format_satoshis(
sign = integer_part[0] if integer_part[0] in ("+", "-") else ""
if sign == "-":
integer_part = integer_part[1:]
integer_part = "{:,}".format(int(integer_part)).replace(',', " ")
integer_part = "{:,}".format(int(integer_part)).replace(',', THOUSANDS_SEP)
integer_part = sign + integer_part
fract_part = " ".join(fract_part[i:i+3] for i in range(0, len(fract_part), 3))
fract_part = THOUSANDS_SEP.join(fract_part[i:i+3] for i in range(0, len(fract_part), 3))
result = integer_part + DECIMAL_POINT + fract_part
# add leading/trailing whitespaces so that numbers can be aligned in a column
if whitespaces:

4
electrum/wallet.py

@ -612,13 +612,13 @@ class Abstract_Wallet(ABC, Logger, EventListener):
# and not util, also have fx remove it
text = fx.remove_thousands_separator(text)
def_fiat = self.default_fiat_value(txid, fx, value_sat)
formatted = fx.ccy_amount_str(def_fiat, commas=False)
formatted = fx.ccy_amount_str(def_fiat, add_thousands_sep=False)
def_fiat_rounded = Decimal(formatted)
reset = not text
if not reset:
try:
text_dec = Decimal(text)
text_dec_rounded = Decimal(fx.ccy_amount_str(text_dec, commas=False))
text_dec_rounded = Decimal(fx.ccy_amount_str(text_dec, add_thousands_sep=False))
reset = text_dec_rounded == def_fiat_rounded
except:
# garbage. not resetting, but not saving either

Loading…
Cancel
Save