16
头图

1. Background

Currency conversion plugin github address

This is a lightweight digital-to-currency string plugin written by me, because it is very interesting to find the currency-related knowledge of various countries, and the related plugins on the current market are relatively old, so I also want to share this interesting knowledge with you.

This plugin can convert a number into a currency string format by the user, for example, the number 12.345 is converted into a currency string:
image.png

There are many interesting ways to identify currencies in the world today. For example, the decimal point of "Indonesia" is a "comma", the thousandth of Indonesia is a "period", the currency symbol of Singapore is actually "SGD", and the symbol of the Japanese yen is also "¥" ", Since there are so many strange ways of writing, of course, we must unify them and solve them.

2. Small class of currency knowledge

- decimal point

Our country uses "." (period) as the decimal point, but countries such as 'Indonesia', 'Germany', 'Brazil' and other countries use "," (comma) to identify the decimal point:

 中国:   123.45
印尼:   123,45
- thousandths

Like the decimal point, our country uses "," (comma) as the thousandth place, and countries such as 'Indonesia' use "." (period) to identify the thousandth place:

 中国:   12,345,678
印尼:   12.345.678
- exact digits

In our country, it is more common to reserve two decimal places, that is, "cents" accurate to "yuanjiaofen", but for example, "Vietnamese Dong" has no concept of decimals. It may be because "Vietnamese Dong" often costs tens of thousands, so the decimal price is almost It doesn't make sense anymore:

2022/5/20: 1 RMB ≈ 3400 VND

To help you understand, you can take a look at this news together:
image.png

- length and position of currency symbols

We are familiar with "¥" and "$". They are only represented by a symbol. In fact, there are many identification methods with a length greater than 1 in the world:

 印尼: Rp 123
新加坡: SGD 123
蒙古语: CN¥ 123

Not only the length is different, but also the position of the currency symbol is different. Many countries put the currency symbol at the back:

 中国  ¥ 123
德国  123 €
越南  123 ₫
冰岛  123 ISK
- repetition of currency symbols

For example, the Japanese yen uses "¥" as well:
image.png

- Representation of negative numbers

Let's take a look at the representation in excel:
image.png

It can be seen that the default is "minus symbol + currency symbol + amount", but I found through the Internet that many people's websites use the writing method of "currency symbol + minus symbol + amount", so my understanding is that users need to be given free choice. right.

 中国
-¥123
¥-123

新加坡
-SGD123
SGD-123

越南: 货币符号在后面不存在这些烦恼
-123₫

Third, the existing programs on the Internet

The first one is: the accounting.js warehouse address (4.8k Star) is used by many people. The bad point is that users need to specify the display rules, that is, the default translation is US dollars. Other translation forms and currency symbols require users customize:
image.png

And a large number of issues pointed out that the calculation accuracy of this library has bugs.
Old library from 11 years ago, and no ts support.

The second is: currencyFormatter.js warehouse address (632 Star) All the configurations in it are all listed in the code. The writing method is not really elegant. To put it bluntly, how to display each language is built in the plug-in, but the user cannot Use free combination display mode, such as $123.4万 this combination display mode:

image.png

The old library from 6 years ago needs to update the configuration file in real time, and there is no ts support.

4. Requirement analysis before making plug-ins

After learning the above knowledge, we can do some demand statistics before making "plug-ins":

  1. Requirements: Convert numbers to any national currency format.
  2. Requirement: The display and hide of the thousand colon can be controlled.
  3. Requirements: The calculation method of the amount can be freely specified "rounding", "rounding down", "rounding up".
    Scenario: The default calculation method is rounding, but if the order of magnitude is large, the error will be relatively large. For example, if a merchant sells 1 item of 1 yuan, the expert can get a 1.4% commission for bringing the item, so each item is 1 * 0.014 , according to the rounding calculation, it will become 0.01 yuan, but if it is rounded up, it will be 0.02 yuan. The difference between the two display methods is very large.
  4. Requirements: The number of decimal places to be reserved can be freely specified. For example, the user needs to make the Vietnamese dong accurate to one decimal place in the actual scene.
    Scenario: It is still an example of selling goods. For example, if a merchant sells 1 item of 1 yuan, the expert can get a 1.4% commission for bringing the item, so each item is 1 * 0.014, but the default in our country is to keep two decimal places. At this time, you can Let the user specify n decimal places.
  5. Requirements: Provide methods to return detailed currency information to assist users in playing tricks.
    Scenario: The format method only returns the formatted currency string, and it is necessary to provide a method to return large and comprehensive information to facilitate the user to freely assemble the display method. The format of the return value is as follows:
 // 比如格式化 12345.67 为人民币, 返回的详细信息
{
      isFront: true, // 货币符号是否在金额前方
      currencySymbol: "¥" // 货币符号
      formatValue: "12,345.67", // 货币
      value: 12345.67, // 原本的值
      currencyString: "¥12,345.67", // 格式化后的货币
      negativeNumber: false, // 是否为负数
}

5. Who is Intl.NumberFormat?

The native method Intl.NumberFormat is the constructor class for the language-sensitive formatted number class

Intl.NumberFormat MDN

Today's protagonist is late at this time. We can use the Intl.NumberFormat method to construct the method of formatting currency natively supported by the browser. Let's first look at the basic usage:

 var number = 123456.789;

// 德语使用逗号作为小数点,使用.作为千位分隔符
console.log(new Intl.NumberFormat('de-DE').format(number));
// → 123.456,789

// 大多数阿拉伯语国家使用阿拉伯语数字
console.log(new Intl.NumberFormat('ar-EG').format(number));
// → ١٢٣٤٥٦٫٧٨٩

// India uses thousands/lakh/crore separators
console.log(new Intl.NumberFormat('en-IN').format(number));
// → 1,23,456.789

On the PC side, the ie browser has poor compatibility with the Intl.NumberFormat method. Its compatibility is as follows:

image.png

6. How to specify the country & currency symbol, what happens if I randomly specify it?

For example, we want to convert to the Chinese currency format in China:

 new Intl.NumberFormat(
  'zh', 
  { 
    style: 'currency', 
    currency: 'CNY' 
   }
).format(12.345);

// ¥12.35

The 'zh' parameter in the above code represents the Chinese region, currency: 'CNY' represents the use of the RMB symbol, I have organized the website on how to query the code of the specified region:

Query country code: BCP 47 language tag
Query currency codes: ISO 4217 currency codes

What will happen if I randomly specify the currency?

Every time I learn a new API, I can't help but try what happens if I don't fill in the specifications, such as the following:

 // 土耳其
new Intl.NumberFormat(
  'tr-TR', 
  { 
    style: 'currency', 
    currency: 'TRY' 
   }
).format(12345.678);

// ₺12.345,68

// 我将地区指定为'土耳其' 货币指定为 '人民币'
new Intl.NumberFormat(
  'tr-TR', 
  { 
    style: 'currency', 
    currency: 'CNY' 
   }
).format(12345.678);

// CN¥12.345,68

It can be seen from the above that the writing of the decimal point and thousandth of the currency is still the standard of 'Turkish', but the currency symbol has become 'CN¥', that is, because more than one country uses the ¥ symbol, so Turkey needs to be prefixed with CN. To distinguish countries, so the country’s currency is displayed directly using the original currency symbol, not CN¥ in China, but direct ¥.

localized symbols

The '¥' of RMB has become 'CN¥' in Turkey. I searched online and found that both writings identify RMB, so it is the local display method used to distinguish currency symbols in Turkey. At this time, I suddenly thought of Japanese yen. Also ¥ , so how does Turkey show these also using the ¥ symbol?

 new Intl.NumberFormat(
  'tr-TR', 
  { 
    style: 'currency', 
      currency: "JPY", 
   }
).format(12345.678);

// ¥12.346   日元默认没有小数

The yen is directly displayed as ¥, so it is necessary to distinguish countries by different spellings.

7. Facing multiple countries

I have encountered this scenario in my actual business and need to convert numbers into currencies of multiple countries, so our plugin needs to support the following usage.

To initialize various configuration parameters, assuming that our plugin exports a CurrencyFormat method, addFormatType must specify a name when adding a format configuration, which is convenient for subsequent calls to the specified method:

Below is the usage of my plugin

 const currencyFormat = new CurrencyFormat();
  currencyFormat.addFormatType("人民币", {
    locale:'zh',
    currency: "CNY"
    // ... 其余配置
  });
  
  currencyFormat.addFormatType("新加坡", {
    locale:'zh-SG',
    currency: "SGD",
    // ... 其余配置
  });
  
  currencyFormat.addFormatType("日元", {
    locale:'ja-JP',
    currency: "JPY",
    // ... 其余配置
  });

Here is how it is used:

 currencyFormat.format('人民币', 12.34)
currencyFormat.format('新加坡', 12.34)
currencyFormat.format('日元', 12.34)

The above method can be achieved, and it can be called anywhere after only configuring it once.

Actually write the basic code structure:
 class CurrencyFormat {
  formatObj = new Map();
  addFormatType(typeName, options) {
      const { locale, currency } = options;
      const formatFn = new Intl.NumberFormat(locale, {
        currency,
        style: "currency"
      }).format;
      this.formatObj.set(typeName, { formatFn });
  }
  format(typeName, val) {
    const formatItem = this.formatObj.get(typeName);
    if (formatItem) {
      const { formatFn } = formatItem;
      return formatFn(val);
    }
    return "-";
  }
}

Instantiate Intl.NumberFormat, and then call the corresponding strength each time.

8. Hide and Display the Thousand Colons

This can be done directly through a property, and when useGrouping is false, the 'thousands sign' will not be displayed.

 new Intl.NumberFormat(locale, {
        currency,
        style: "currency"
        useGrouping: false
      });

9. Freely specify reserved digits

For example, China retains two decimal places by default, but the user needs to view the numbers with three decimal places. In this case, it is necessary for the user to specify the number of digits to be retained after currency formatting. The parameter name is maximumFractionDigits:

Note that an error will be reported if maximumFractionDigits is a negative number:

 new Intl.NumberFormat(locale, {
        currency,
        style: "currency"
        useGrouping: false,
        maximumFractionDigits: 3
      });

The point here is that if the user does not specify maximumFractionDigits, then it is the default value of the reserved decimal for the 'country' used by the user. In my plugin, this default value will be used for the specified calculation later, so how do I know the current calculation Is it accurate to a few decimal places?

Unfortunately, the native does not provide a method to obtain the calculation accuracy of a certain area, so it needs to be calculated manually:

When calculating, pay attention to the special printing format:

 // 通过编号系统中的nu扩展键请求, 例如中文十进制数字
console.log(new Intl.NumberFormat('zh-Hans-CN-u-nu-hanidec').format(number));
// → 一二三,四五六.七八九
method one:

Regular matching, if the number 1.234567 is converted into a 'currency string' such as '$1.23' or '1.23₫' , the matching is performed from the back to the front, starting from the first digit encountered, if encounter '.' or ',' to stop matching the length of the returned structure.

But there is actually a more elegant regular way. When I pass in 0 for formatting, I only need to match the length of number 2 in the pattern of "number 1 + decimal point + number 2":

 let maxFractionDigits = 0;
      const currencyTempString = formatFn(0).replace(/\s/g, "");
      const regVal = /[0〇]+[\.\,]([0〇]+)/g;
      const resArr = regVal.exec(currencyTempString);
      if (resArr) {
        maxFractionDigits = resArr?.[1]?.length;
      }
Method two:

Enter the number 0 for the first number, the converted 'currency string' such as '$0.00' or '0.00₫', enter 0 for the second number but specify 0 decimal places, and then subtract the lengths of the two strings. Subtract one again to limit this number to a minimum of 0.

Pseudocode: Default exact digits = Math.max( '$0.00' length - '$0' length - '.' length, 0).

There is a big pit in this method, that is, the execution before the node 13 version may report an error, because at this time, when maximumFractionDigits is passed in 0, it may report an error!

Ten, optional "rounding", "rounding down", "rounding up"

The Intl.NumberFormat method defaults to 'rounding' to calculate the amount, but the actual scenario may require the user to specify the rules of "round down" and "round up", which I define here as calculationType: 'ceil ' | 'floor' to control, and the parameter maxFractionDigits is required to specify how many decimal places need to be retained:

How to use the plugin:

 currencyFormat.addFormatType("人民币", {
    locale:'zh',
    currency: "CNY"
    calculationType: 'ceil'
  });

The format code of the plugin:

 format(typeName, val) {
    const formatItem = this.formatObj.get(typeName);
    if (formatItem) {
      const { formatFn, calculationType, maxFractionDigits } = formatItem;
      const multiple = Math.pow(10, maxFractionDigits);
      if (calculationType === "ceil" || calculationType === "floor") {
        val = Math[calculationType](val * multiple) / multiple;
      }
      const currencyString = formatFn(val);
      return currencyString;
    }
    return "-";
  }

In this code, we get the calculation type specified by the calculationType variable, and then multiply the data passed in by the user to the power of maxFractionDigits of 10, and then perform the Math operation. After the calculation, divide it by the power of maxFractionDigits of 10.

11. Compatibility with Negative Numbers

If the currency is negative, such as '-12.34', the plugin will return "-$12.34" by default, but in some scenarios, it needs to be displayed as "$-12.34", if the region is designated as Singapore, it will display "-SGD12.35", obviously the negative sign is in the outer layer It's a bit unclear, so let's deal with this next.

12. Return detailed information

The default format returned by the plugin is "$12.34", but in fact, we may need to modify the style, such as adding a space "$12.34" to the currency symbol and amount, or to hide the symbol and only display "12.34", and the above ' Negative numbers' problem, so it is necessary to return details to the user:

 // 比如格式化 12345.67 为人民币
{
      isFront: true, // 货币符号是否在金额前方
      currencySymbol: "¥" // 货币符号
      formatValue: "12,345.67", // 货币
      value: 12345.67, // 原本的值
      currencyString: "¥12,345.67", // 格式化后的货币
      negativeNumber: false, // 是否为负数
}

A new formatDetail method is added, where the absolute value of the number is taken before currency formatting. The code is:

 getFrontCurrencySymbol = (val) => /^[^\d一二三四五六七八九]+/g.exec(val)?.[0] ?? "";
  getAfterCurrencySymbol = (val) => /[^\d一二三四五六七八九]+$/g.exec(val)?.[0] ?? "";
  formatDetail(typeName, val) {
    const value = Math.abs(val);
    const currencyString = this.format(typeName, value);
    const frontCurrencySymbol = this.getFrontCurrencySymbol(currencyString);
    if (frontCurrencySymbol) {
      return {
        isFront: true,
        currencySymbol: frontCurrencySymbol,
        formatValue: currencyString.slice(frontCurrencySymbol.length) || "0",
        value,
        currencyString,
        negativeNumber: val < 0
      };
    }
    const afterCurrencySymbol = this.getAfterCurrencySymbol(currencyString);
    return {
      isFront: false,
      currencySymbol: afterCurrencySymbol,
      formatValue: currencyString.slice(0, -afterCurrencySymbol.length) || "0",
      value,
      currencyString,
      negativeNumber: val < 0
    };

The usage and return value are as follows:

 currencyFormat.addFormatType("中文汉字", {
        locale:'zh-Hans-CN-u-nu-hanidec',
        currency: "CNY",
      });
      
      console.log('中文汉字',currencyFormat.formatDetail('中文汉字', 12345.67));

image.png

13. Abbreviations

The so-called abbreviation is the pattern in the picture:
image.png

That is, how to display long numbers, I have added a formatAbbreviation method here. This method can return the string after the number is abbreviated. The usage is as follows:

 const currencyFormat = new CurrencyFormat();
currencyFormat.addFormatType("en_gb", {
    locale: "en-GB",
    currency: "GBP",
});

currencyFormat.formatAbbreviation("en_gb", 123456.789)

// 打印结果是 £123K

The configuration scheme is as follows, for example, if you want Vietnamese Dong to be abbreviated in Chinese:

 const currencyFormat = new CurrencyFormat();
   currencyFormat.addFormatType("demo_越南_中文", {
        locale: 'vi-VN',
        currency: "VND",
        validAbbreviations: {
            "3": '千',
            "4": '万',
            "8": "亿",
            "13": "兆"
        },
    });

The validAbbreviations property specifies how many digits to abbreviate when the number exceeds, and specifies the symbol after the abbreviated, for example, the above picture is '1000' converted to '1 thousand ₫'.

Of course, we have a set of basic conversion rules built in by default:

 validAbbreviationsTypeEN = {
        3: "K",
        6: "M",
        9: "B",
        12: "T",
    };

Then I will briefly explain its principle. In fact, I use the existing api "formatDetail" to get the currency details object. At this time, I get the real value, and cyclically compare this value with validAbbreviationsTypeEN to see his number of digits. What abbreviations should be added.

But you should also pay attention to the location of the abbreviation. Because the currency symbol has front and back, there is no special treatment when the currency symbol is in the front, but if the currency symbol is in the back, you need to display the abbreviation first and then display the currency symbol. For example shown below:

 123456.789   -->  Rp12万
   123456.789   -->  12万₫

14. Specified symbols

The real business taught me a lesson. Not all scenarios are carried out according to some international standards. For example, "S$" needs to be displayed for the Singapore Alliance, but "$" should be displayed internationally. At this time, I don't want to press The standard display symbol requires us to allow developers to freely specify the currency symbol, so I added the targetCurrency attribute:

 const currencyFormat = new CurrencyFormat();
currencyFormat.addFormatType("en_gb_targetCurrency1", {
    locale: "en-GB",
    currency: "GBP",
    targetCurrency: "xxxx"
});

currencyFormat.format("en_gb_targetCurrency1", 123456.789)

// 最后输出的结果是"xxxx123,456.79

The principle is also relatively straightforward. At the end of the format method, determine whether there is a targetCurrency attribute, and then replace it with the current currency symbol.

end

That's it this time, hope to progress with you.


lulu_up
5.7k 声望6.9k 粉丝

自信自律, 终身学习, 创业者