12.51. DD 51: Fractional Digits¶
12.51.1. Summary¶
This design document specifies how an amount’s fractional digits should be rendered. Note that UIs that cannot render amounts as specified (e.g. because the display does not support super script digits) may ignore the rendering advice provided by the protocol under this DD.
12.51.2. Motivation¶
Since different currencies have different ways to show/render fractionals, the end-user apps should follow these guidelines.
12.51.3. Requirements¶
There was already a specification for ScopedCurrencyInfo - which got renamed to CurrencySpecification.
We need three core characteristics for fractional digits for each currency:
the number of fractional digits e in [0..8] the user may ‘e’nter in a TextInputField
the number of fractional digits n in [0..8] to be rendered as ‘n’ormal characters (same font and size as the integer digits). All additional fractional digits will be rendered as SuperScriptDigits as known from gas filling stations. The UI should never round or truncate any amount, but always render all existing digits (except trailing zeroes, see c).
the number of fractional digits z in [0..8] to be rendered as trailing ‘z’eroes (including SuperScript digits). E.g. if z = 2 (and n = 2), then render $5 as
$ 5.00
. If z = 3 (and n = 2), then render $5 as$ 5.00⁰
with two normal trailing zeroes and one superscript trailing zero.
The values e, n, and z are independent from each other. Each could be any value from 0 to 8. However, when a user enters an amount, s/he should be able to input all normal fractionals. Thus e should never be smaller than n.
Usually, all these three numbers have the same value (e = n = z), which means that in case of e.g. ‘2’ (used for €,$,£) the user can enter cent/penny values (but not a fraction of those), these cents/pennies are always shown (even if they are 0) as two normal digits after the decimal separator, and fractions of a cent/penny are rendered as SuperScriptDigits, but appear only if they are not trailing zeroes. For japanese ¥, all three values could be 0, which means that the user cannot enter fractions at all. If there are fractions they would never be rendered as normal digits but always as SuperScript, and appear only if they are not trailing zeroes.
Additionally, some cryptocurrencies have such huge units, that they are
commonly rendered in milli-units, such as mBTC (milliBTC, 1/1000 of a BTC),
Gwei (Giga-WEI), Mwei (Million-WEI), Kwei (Kilo-WEI), or
Mether/Kether/Gether/Tether and more “logical” units such as Szabo and
Finney. See https://coinguides.org/ethereum-unit-converter-gwei-ether/
if
you want a good laugh. Regardless of the self-inflicted insanity here, this
could also make sense for inflated currencies in some cases. So we probably
should also have the ability to ship such a conversion map.
12.51.4. Proposed Solution¶
12.51.4.1. Protocol considerations¶
The exchange, bank and merchant backends would need to be configured (via
their configuration files) to return the following CurrencySpecification in their
/config
and/or /keys
endpoints. The bank returns this so that the
bank SPA can render amounts correctly, the exchange informs the wallets about
the desired way to render the currency, and the merchant backend informs the
merchant SPA — independently of any particular exchange being used — how
the merchant SPA should render amounts. Hence, the information will need to be
provisioned by all three services.
public struct CurrencySpecification: Codable, Sendable { // e.g. “Japanese Yen” or "Bitcoin (Mainnet)" let name: String // how many digits the user may enter after the decimal separator let fractional_input_digits: Int // €,$,£: 2; some arabic currencies: 3, ¥: 0 let fractional_normal_digits: Int // usually same as fractionalNormalDigits, but e.g. might be 2 for ¥ let fractional_trailing_zero_digits: Int // specifies whether the keys in `alt_unit_names' are symbols // (e.g. €, k€) or names (e.g. BTC, mBTC), so that apps can decide // how to render it (e.g. EUR 10 vs €10) let alt_unit_names_are_symbols: Bool // map of powers of 10 to alternative currency names / symbols, // must always have an entry under "0" that defines the base name, // e.g. "0 : €" or "3 : k€". For BTC, would be "0 : BTC, -3 : mBTC". // This way, we can also communicate the currency symbol to be used. let alt_unit_names: [Int : String] }
(Note: decimal_separator, group_separator and is_currency_name_leading were removed from this struct since they should always be taken from the user’s locale.)
For very large (2400000) or very tiny amounts (0.000056) the software would then first represent the number compactly without any fraction (so for our examples above, 24 * 10^6 and 56 * 10^-6) and then search for the nearest fit in the alt_unit_names table. The result might then be 24000 KGELD or 0.056 mGELD, assuming the map had entries for 3 and -3 respectively. Depending on the table, the result could also be 24 MGELD (6 : MGELD), or 5.6 nGELD (assuming -6 : nGeld). Fractional rendering rules would still be applied to the alternative unit name, alas the “fractional_input_digits” would always apply to the unit currency and may need to be adjusted if amounts are input using an alternative unit name.
12.51.4.2. Configuration syntax¶
Each currency should be specified in its own subsystem-independent currency, with the section name prefixed with “currency-”. In that section. The map could be given directly in JSON. For example:
[currency-euro] ENABLED = YES name = "Euro" code = "EUR" fractional_input_digits = 2 fractional_normal_digits = 2 fractional_trailing_zero_digits = 2 alt_unit_names_are_symbols = YES alt_unit_names = {"0":"€"} [currency-japanese-yen] ENABLED = YES name = "Japanese Yen" code = "JPY" fractional_input_digits = 2 fractional_normal_digits = 0 fractional_trailing_zero_digits = 2 alt_unit_names_are_symbols = YES alt_unit_names = {"0":"¥"} [currency-bitcoin-mainnet] ENABLED = NO name = "Bitcoin (Mainnet)" code = "BITCOINBTC" fractional_input_digits = 8 fractional_normal_digits = 3 fractional_trailing_zero_digits = 0 alt_unit_names_are_symbols = NO alt_unit_names = {"0":"BTC","-3":"mBTC"} [currency-ethereum] ENABLED = NO name = "WAI-ETHER (Ethereum)" code = "EthereumWAI" fractional_input_digits = 0 fractional_normal_digits = 0 fractional_trailing_zero_digits = 0 alt_unit_names_are_symbols = NO alt_unit_names = {"0":"WAI","3":"KWAI","6":"MWAI","9":"GWAI","12":"Szabo","15":"Finney","18":"Ether","21":"KEther","24":"MEther"}
12.51.4.3. Implementation considerations¶
iOS has a built-in currency formatter, which can be configured from a locale.
It knows how to deal with group-separators and where to apply them (e.g. India
uses a mixture of thousands and hundreds instead of putting the separator
after each 3 digits like western currencies). Set the formatter’s parameter
maximumFractionDigits
to 8, then it will not round the value and thus can
be used for the whole amount. Set its parameter minimumFractionDigits
to
‘z’ (fractionalTrailingZeroDigits
) to let it automatically add trailing
zeroes. Then convert all fractional digits after ‘n’
(fractionalNormalDigits
) to SuperScript digits.
The field alt_unit_names_are_symbols
was introduced in order to help UIs
better decide how to render amounts with unit names (e.g. BTC, mBTC) instead
of unit symbols (e.g. €, k€). Typically, currency symbols (in Android and iOS)
are rendered, depending on the locale, before or after the amount without a
space in between (e.g. €10), however, if the given currency has names instead
of symbols for its units, rendering amounts without a space in between
(e.g. BTC10) is not ideal and results in ugliness and user
dissatisfaction. When this field is set to true
, it is expected that the
wallet apps will render the amount with a space in between.
12.51.5. Definition of Done¶
(Only applicable to design documents that describe a new feature. While the DoD is not satisfied yet, a user-facing feature must be behind a feature flag or dev-mode flag.)
Configuration (INI) format finalized and documented in taler.conf man page [DONE]
Endpoints of libeufin-bank, fakebank, exchange and merchant return the information
SPAs use the information to render amounts
Wallet-core passes rendering information to wallet UIs
Cashier, Android PoS, WebExtension, Android and iOS Wallet render amounts accordingly
12.51.6. Alternatives¶
None, we cannot confuse users by rendering amounts in ways that break cultural standards, and we cannot round and have numbers in balances not add up.
12.51.7. Drawbacks¶
12.51.8. Discussion / Q&A¶
We probably should NOT have the decimalSeparator in this definition. Instead that should be taken from the locale of the user, so they see currency amounts formatted like they’re used to. If we really keep this, then we would also need the groupSeparator to ensure it is not identical to the decimalSeparator. Better to leave this can of worms to the operating system our app runs on, and render according to the user’s preferences (locale)…
However, instead of decimalSeparator we could specify the locale this currency belongs to.
(This should be filled in with results from discussions on mailing lists / personal communication.)