Merge pull request #8139 from SomberNight/202301_locale_decimal_point
locale amounts: consistently use "." as dec point, and " " as thou sep
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