在打字稿中使用货币价值
#javascript #网络开发人员 #typescript #finance

过去几年为金融科技公司工作,我不得不经常与货币价值合作。我学到了一些方便的模式,用于处理格式化货币价值。在这篇文章中,我将分解一些我最喜欢的模式,以及我学到的艰难方法,因此您不需要。

格式和本地化

正如我上一篇关于internationalization的文章所述,对于软件工程师来说,本地化软件的本地化很重要,以便用户以最能理解的方式接收信息。由于与数字有关,因此有多种不同地区使用的格式。例如,在美国,我们将格式化一千二十三十四美元和56美分,例如:$ 1,234.56。在德国,那将是1.234,56美元。在墨西哥,这将是1,234.56美元。

入门

我们货币格式的基石将是Intl.NumberFormat。此时,浏览器对Intl.NumberFormat的支持状况良好,more than 97% of global Internet users使用支持它的浏览器。

创建Intl.NumberFormat格式非常简单:

const formatter = new Intl.NumberFormat('default', {
  style: 'currency',
  currency: 'USD',
});

// Try replacing 'default' with various locales like 'en-US', 'de-DE', 'ar-EG', and 'zh-CN', for example.
formatter.format(1234.56);

还有其他可用选项可以查看here

试试看!

不受支持的货币

在开发各种财务应用程序时,我了解到,如果您将不支持的货币(例如许多加密货币)传递给Intl.NumberFormat构造函数,它将丢失错误。以虚构的REACT硬币为例,将抛出以下错误:RangeError: Invalid currency code : REACT

我解决此问题的一种方式是首先尝试使用提供的选项实例化Intl.NumberFormat,然后如果不支持所选的货币,则将其倒退到比特币(BTC)格式。

一个示例:

const getNoOpFormatter = (
  locale: string = 'default',
  options?: Intl.NumberFormatOptions
) => ({
  format: (x: number | bigint | undefined) => x?.toString() || '',
  formatToParts: (x: number | bigint | undefined) => [
    { type: 'unknown' as Intl.NumberFormatPartTypes, value: x?.toString() || '' }
  ],
  resolvedOptions: new Intl.NumberFormat(locale, options).resolvedOptions
});

export const getCurrencyFormatter = (
  locale: string = 'default',
  options?: Intl.NumberFormatOptions
): Intl.NumberFormat => {
  try {
    return new Intl.NumberFormat(locale, options);
  } catch {
    if (options?.style === 'currency' && options?.currency) {
      const rootFormatter = new Intl.NumberFormat(locale, {
        ...options,
        currency: 'BTC'
      });

      return {
        format: (x: number | bigint | undefined) =>
          rootFormatter
            .formatToParts(x)
            .map((part) =>
              part.type === 'currency' ? options.currency : part.value
            )
            .join(''),
        formatToParts: (x: number | bigint | undefined) =>
          rootFormatter.formatToParts(x).map((part) =>
            part.type === 'currency'
              ? ({
                  ...part,
                  value: options.currency
                } as Intl.NumberFormatPart)
              : part
          ),
        resolvedOptions: rootFormatter.resolvedOptions
      };
    }

    return getNoOpFormatter(locale, options);
  }
};

但是,这种方法有一个陷阱。它默认为2 maximumFractionDigits。根据您的货币,这可能是或不够的。您需要覆盖该选项以提供足够的分数数字。

算术和比较操作

算术的字符串值

随着诸如分数共享所有权和加密货币之类的概念继续在全球经济中实现和扩展,
JavaScript应用程序将在处理数字方面越来越困难,主要是由于两个问题:

  • 浮点精度:浮点数的计算不一定是确定性的,并产生错误的结果。
0.1 + 0.2 === 0.3; // false 🤯 try it in your browser console. I get: 0.30000000000000004
  • 数字大于{number.max_safe_integer},而小于{number.min_safe_integer}的数字超出范围,产生不正确的 当执行算术操作时,当在任一方向上超过任一方向时值。

对于金融应用,API通常返回货币价值,余额,股票/货币头寸和其他金额作为字符串。我发现处理此问题的最好方法是使用big.js之类的库。

使用诸如big.js之类的库,您可以将字符串号值转换为Big对象,该对象可以安全地执行算术和比较操作。例如:

import Big from 'big.js';

new Big(Number.MAX_SAFE_INTEGER).times(5).toString() === '45035996273704955'; // true
new Big(Number.MIN_SAFE_INTEGER).times(5).toString() === '-45035996273704955'; // true
new Big(0.1).add(0.2).eq(0.3); // true

您可以看到,通过使用big.js,我们面临的JavaScript数字原始的问题可以解决。但是,这对我们提出了另一个问题。我们有两种主要方法可以从我们的大对象中获取可用的原始价值:toString()toNumber()

这是事情再次变得有趣的地方。

Intl.NumberFormat.prototype.format()当您传递数字或bigint值时,它具有很好的浏览器支持,但是字符串数值尚未得到充分支持,并且在编写时为considered experimental。有趣的是,在Chrome,Firefox甚至Safari的最新版本中,传递的字符串数值似乎对我有用。考虑到这一点,使用从Big.prototype.toString() 可能工作的值。让我们考虑一下我们的其他选择。

Big.prototype.toNumber()将返回一个JavaScript号码原始版本,但可能会丢失精度。根据文档,您可以设置Big.strict = true;,如果被调用的数字不能转换为原始数字,则会导致Big.prototype.toNumber()投掷,而无需精确损失。根据您的应用程序中数字的大小,这可能是可以接受的。

似乎我们只剩下两种解决方案,这些解决方案在某些情况下完成工作,但我认为我们可以将其进一步迈出一步。

import Big from 'big.js';

/**
 * Note that in strict mode, you'll need to pass string or bigint 
 * values as the BigSource for various Big methods and the
 * constructor
*/
Big.strict = true;

const safelyFormatNumberWithFallback = (formatter: Intl.NumberFormat, value: Big) => {
  // First, attempt to format the Big as a number primitive
  try {
    return formatter.format(value.toNumber());
  } catch {}

  // Second, attempt to format the Big as a string primitive
  try {
    return formatter.format(value.toString());
  } catch {}

  // As a fallback, simply return the ugly string value
  return value.toString();
}

奖金:将所有内容放在一个反应​​组件中

import React, { BaseHTMLAttributes, useMemo } from 'react';
import Big from 'big.js';
import { getCurrencyFormatter, safelyFormatNumberWithFallback } from '../helpers/number'; // The functions from above

interface Props extends BaseHTMLAttributes<HTMLSpanElement>, Omit<Intl.NumberFormatOptions, 'style'> {
  locale?: string;
  value?: Big;
}

export const FormatCurrencyValue: React.FC<Props> = ({
  currency = 'USD',
  currencySign,
  useGrouping,
  minimumIntegerDigits,
  minimumFractionDigits,
  maximumFractionDigits,
  minimumSignificantDigits,
  maximumSignificantDigits,
  locale = 'default',
  value,
  ...rest
}) => {
  const numberFormatter: Intl.NumberFormat = useMemo(
    () => getCurrencyFormatter(locale, {
      currency,
      currencySign,
      useGrouping,
      minimumIntegerDigits,
      minimumFractionDigits,
      maximumFractionDigits,
      minimumSignificantDigits,
      maximumSignificantDigits,
    }),
    [
      locale,
      currency,
      currencySign,
      useGrouping,
      minimumIntegerDigits,
      minimumFractionDigits,
      maximumFractionDigits,
      minimumSignificantDigits,
      maximumSignificantDigits,
    ],
  );

  return (
    // I find it helpful to pass the raw value data attribute down to make debugging easier from a quick DOM inspection
    <span data-value={value?.toString()} {...rest}>
      {safelyFormatNumberWithFallback(numberFormatter, value)}
    </span>
  );
}

分开镜头

我想以一种意见结束这篇文章:货币价值(或一般数值)最好在单拼字体中呈现。它们允许用户更快,准确地扫描数据。

一如既往,如果我错过了什么或犯了一个错误,请在Twitter上与我联系。如果您学到了一些东西,请随时分享。