// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.payments; import java.text.DecimalFormatSymbols; import java.util.Currency; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Formatter for currency strings that can be too large to parse into numbers. * https://w3c.github.io/browser-payment-api/specs/paymentrequest.html#currencyamount */ public class CurrencyStringFormatter { // Amount value pattern and capture group numbers. private static final String AMOUNT_VALUE_PATTERN = "^(-?)([0-9]+)(\\.([0-9]+))?$"; private static final int OPTIONAL_NEGATIVE_GROUP = 1; private static final int DIGITS_BETWEEN_NEGATIVE_AND_PERIOD_GROUP = 2; private static final int DIGITS_AFTER_PERIOD_GROUP = 4; // Max currency code length. Maximum length of currency code can be at most 2048. private static final int MAX_CURRENCY_CODE_LEN = 2048; // Currency code exceeding 6 chars will be ellipsized during formatting for display. private static final int MAX_CURRENCY_CHARS = 6; // Unicode character for ellipsis. private static final String ELLIPSIS = "\u2026"; // Formatting constants. private static final int DIGIT_GROUPING_SIZE = 3; private final Pattern mAmountValuePattern; /** * The currency formatted for display. Currency can be any string of at most * 2048 characters.Currency code more than 6 character is formatted to first * 5 characters and ellipsis. */ public final String mFormattedCurrencyCode; /** * The symbol for the currency specified on the bill. For example, the symbol for "USD" is "$". */ private final String mCurrencySymbol; /** * The number of digits after the decimal separator for the currency specified on the bill. For * example, 2 for "USD" and 0 for "JPY". */ private final int mDefaultFractionDigits; /** * The number grouping separator for the current locale. For example, "," in US. 3-digit groups * are assumed. */ private final char mGroupingSeparator; /** * The monetary decimal separator for the current locale. For example, "." in US and "," in * France. */ private final char mMonetaryDecimalSeparator; /** * Builds the formatter for the given currency code and the current user locale. * * @param currencyCode The currency code. Most commonly, this follows ISO 4217 format: 3 upper * case ASCII letters. For example, "USD". Format is not restricted. Should * not be null. * @param userLocale User's current locale. Should not be null. */ public CurrencyStringFormatter(String currencyCode, Locale userLocale) { assert currencyCode != null : "currencyCode should not be null"; assert userLocale != null : "userLocale should not be null"; mAmountValuePattern = Pattern.compile(AMOUNT_VALUE_PATTERN); mFormattedCurrencyCode = currencyCode.length() <= MAX_CURRENCY_CHARS ? currencyCode : currencyCode.substring(0, MAX_CURRENCY_CHARS - 1) + ELLIPSIS; String currencySymbol; int defaultFractionDigits; try { Currency currency = Currency.getInstance(currencyCode); currencySymbol = currency.getSymbol(); defaultFractionDigits = currency.getDefaultFractionDigits(); } catch (IllegalArgumentException e) { // The spec does not limit the currencies to official ISO 4217 currency code list, which // is used by java.util.Currency. For example, "BTX" (bitcoin) is not an official ISO // 4217 currency code, but is allowed by the spec. currencySymbol = ""; defaultFractionDigits = 0; } // If the prefix of the currency symbol matches the prefix of the currency code, remove the // matching prefix from the symbol. The UI already shows the currency code, so there's no // need to show duplicate information. String symbol = ""; for (int i = 0; i < currencySymbol.length(); i++) { if (i >= currencyCode.length() || currencySymbol.charAt(i) != currencyCode.charAt(i)) { symbol = currencySymbol.substring(i); break; } } mCurrencySymbol = symbol; mDefaultFractionDigits = defaultFractionDigits; // Use the symbols from user's current locale. For example, use "," for decimal separator in // France, even if paying in "USD". DecimalFormatSymbols symbols = new DecimalFormatSymbols(userLocale); mGroupingSeparator = symbols.getGroupingSeparator(); mMonetaryDecimalSeparator = symbols.getMonetaryDecimalSeparator(); } /** * Returns true if the amount value string is in valid format. * * @param amountValue The number to check for validity. * @return Whether the number is in valid format. */ public boolean isValidAmountValue(String amountValue) { return amountValue != null && mAmountValuePattern.matcher(amountValue).matches(); } /** * Returns true if the currency code string is in valid format. * * @param amountCurrencyCode The currency code to check for validity. * @return Whether the currency code is in valid format. */ public boolean isValidAmountCurrencyCode(String amountCurrencyCode) { return amountCurrencyCode != null && amountCurrencyCode.length() <= MAX_CURRENCY_CODE_LEN; } /** @return The currency code formatted for display. */ public String getFormattedCurrencyCode() { return mFormattedCurrencyCode; } /** * Formats the currency string for display. Does not parse the string into a number, because it * might be too large. The number is formatted for the current locale and follows the symbol of * the currency code. * * @param amountValue The number to format. Should be in "^-?[0-9]+(\.[0-9]+)?$" format. Should * not be null. * @return The currency symbol followed by a space and the formatted number. */ public String format(String amountValue) { assert amountValue != null : "amountValue should not be null"; Matcher m = mAmountValuePattern.matcher(amountValue); // Required to capture the groups. boolean matches = m.matches(); assert matches; StringBuilder result = new StringBuilder(m.group(OPTIONAL_NEGATIVE_GROUP)); result.append(mCurrencySymbol); int digitStart = result.length(); result.append(m.group(DIGITS_BETWEEN_NEGATIVE_AND_PERIOD_GROUP)); for (int i = result.length() - DIGIT_GROUPING_SIZE; i > digitStart; i -= DIGIT_GROUPING_SIZE) { result.insert(i, mGroupingSeparator); } String decimals = m.group(DIGITS_AFTER_PERIOD_GROUP); int numberOfDecimals = decimals == null ? 0 : decimals.length(); if (numberOfDecimals > 0 || mDefaultFractionDigits > 0) { result.append(mMonetaryDecimalSeparator); if (null != decimals) result.append(decimals); for (int i = numberOfDecimals; i < mDefaultFractionDigits; i++) { result.append("0"); } } return result.toString(); } }