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
This commit is contained in:
@@ -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(),
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user