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: except InvalidOperation:
raise Exception("from_amount is not a number") raise Exception("from_amount is not a number")
return { return {
"from_amount": self.daemon.fx.ccy_amount_str(from_amount, False, from_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, False, to_ccy), "to_amount": self.daemon.fx.ccy_amount_str(to_amount, add_thousands_sep=False, ccy=to_ccy),
"from_ccy": from_ccy, "from_ccy": from_ccy,
"to_ccy": to_ccy, "to_ccy": to_ccy,
"source": self.daemon.fx.exchange.name(), "source": self.daemon.fx.exchange.name(),

19
electrum/exchange_rate.py

@ -556,17 +556,24 @@ class FxThread(ThreadJob, EventListener):
return d.get(ccy, []) return d.get(ccy, [])
@staticmethod @staticmethod
def remove_thousands_separator(text): def remove_thousands_separator(text: str) -> str:
return text.replace(',', '') # FIXME use THOUSAND_SEPARATOR in util 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) 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: try:
rounded_amount = round(amount, prec) rounded_amount = round(amount, prec)
except decimal.InvalidOperation: except decimal.InvalidOperation:
rounded_amount = amount 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): async def run(self):
while True: while True:
@ -683,7 +690,7 @@ class FxThread(ThreadJob, EventListener):
def format_fiat(self, value: Decimal) -> str: def format_fiat(self, value: Decimal) -> str:
if value.is_nan(): if value.is_nan():
return _("No data") 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: def history_rate(self, d_t: Optional[datetime]) -> Decimal:
if d_t is None: if d_t is None:

4
electrum/gui/qml/qefx.py

@ -108,7 +108,7 @@ class QEFX(QObject, QtEventListener):
except: except:
return '' return ''
if plain: 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: else:
return self.fx.value_str(satoshis, rate) return self.fx.value_str(satoshis, rate)
@ -133,7 +133,7 @@ class QEFX(QObject, QtEventListener):
return '' return ''
dt = datetime.fromtimestamp(int(td)) dt = datetime.fromtimestamp(int(td))
if plain: 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: else:
return self.fx.historical_value_str(satoshis, dt) 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 .util import char_width_in_lineedit, ColorScheme
from electrum.util import (format_satoshis_plain, decimal_point_to_base_unit_name, 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 from electrum.bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
@ -66,13 +66,13 @@ class AmountEdit(SizedFreezableLineEdit):
return return
pos = self.cursorPosition() pos = self.cursorPosition()
chars = '0123456789' 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]) s = ''.join([i for i in text if i in chars])
if not self.is_int: if not self.is_int:
if '.' in s: if DECIMAL_POINT in s:
p = s.find('.') p = s.find(DECIMAL_POINT)
s = s.replace('.','') s = s.replace(DECIMAL_POINT, '')
s = s[:p] + '.' + s[p:p+self.max_precision()] s = s[:p] + DECIMAL_POINT + s[p:p+self.max_precision()]
if self.max_amount: if self.max_amount:
if (amt := self._get_amount_from_text(s)) and amt >= 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) 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]: def _get_amount_from_text(self, text: str) -> Union[None, Decimal, int]:
try: try:
text = text.replace(DECIMAL_POINT, '.')
return (int if self.is_int else Decimal)(text) return (int if self.is_int else Decimal)(text)
except: except:
return None return None
@ -127,6 +128,7 @@ class BTCAmountEdit(AmountEdit):
def _get_amount_from_text(self, text): def _get_amount_from_text(self, text):
# returns amt in satoshis # returns amt in satoshis
try: try:
text = text.replace(DECIMAL_POINT, '.')
x = Decimal(text) x = Decimal(text)
except: except:
return None return None
@ -141,7 +143,9 @@ class BTCAmountEdit(AmountEdit):
return Decimal(amount) if not self.is_int else int(amount) return Decimal(amount) if not self.is_int else int(amount)
def _get_text_from_amount(self, amount_sat): 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): def setAmount(self, amount_sat):
if amount_sat is None: if amount_sat is None:

2
electrum/gui/qt/main_window.py

@ -926,7 +926,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
else: else:
fiat_e.follows = True fiat_e.follows = True
fiat_e.setText(self.fx.ccy_amount_str( 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.setStyleSheet(ColorScheme.BLUE.as_stylesheet())
fiat_e.follows = False fiat_e.follows = False

8
electrum/tests/test_wallet.py

@ -133,19 +133,19 @@ class TestFiat(ElectrumTestCase):
self.fx = FakeFxThread(FakeExchange(Decimal('1000.001'))) self.fx = FakeFxThread(FakeExchange(Decimal('1000.001')))
default_fiat = Abstract_Wallet.default_fiat_value(self.wallet, txid, self.fx, self.value_sat) default_fiat = Abstract_Wallet.default_fiat_value(self.wallet, txid, self.fx, self.value_sat)
self.assertEqual(Decimal('1000.001'), default_fiat) 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): 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)) 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] 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.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '', self.fx, self.value_sat))
self.assertNotIn(txid, self.fiat_value[ccy]) 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 # 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): 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): def test_empty_resets(self):
self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '', self.fx, self.value_sat)) 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 threading
import hmac import hmac
import stat import stat
from locale import localeconv import locale
import asyncio import asyncio
import urllib.request, urllib.parse, urllib.error import urllib.request, urllib.parse, urllib.error
import builtins import builtins
@ -698,7 +698,11 @@ def format_satoshis_plain(
# We enforce that we have at least that available. # We enforce that we have at least that available.
assert decimal.getcontext().prec >= 28, f"PyDecimal precision too low: {decimal.getcontext().prec}" 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( def format_satoshis(
@ -737,9 +741,9 @@ def format_satoshis(
sign = integer_part[0] if integer_part[0] in ("+", "-") else "" sign = integer_part[0] if integer_part[0] in ("+", "-") else ""
if sign == "-": if sign == "-":
integer_part = integer_part[1:] 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 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 result = integer_part + DECIMAL_POINT + fract_part
# add leading/trailing whitespaces so that numbers can be aligned in a column # add leading/trailing whitespaces so that numbers can be aligned in a column
if whitespaces: if whitespaces:

4
electrum/wallet.py

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

Loading…
Cancel
Save