mathp | 6758be03 | 2017-01-13 04:49:50 | [diff] [blame] | 1 | // Copyright 2017 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | #include "components/payments/currency_formatter.h" |
| 6 | |
| 7 | #include <memory> |
| 8 | |
| 9 | #include "base/numerics/safe_conversions.h" |
| 10 | #include "base/strings/string_util.h" |
| 11 | #include "base/strings/utf_string_conversions.h" |
| 12 | #include "third_party/icu/source/common/unicode/stringpiece.h" |
| 13 | #include "third_party/icu/source/common/unicode/uchar.h" |
| 14 | #include "third_party/icu/source/common/unicode/unistr.h" |
| 15 | #include "third_party/icu/source/common/unicode/utypes.h" |
| 16 | |
| 17 | namespace payments { |
| 18 | |
| 19 | const char kIso4217CurrencySystem[] = "urn:iso:std:iso:4217"; |
mathp | 94af7a6 | 2017-01-14 11:10:05 | [diff] [blame] | 20 | |
mathp | 6758be03 | 2017-01-13 04:49:50 | [diff] [blame] | 21 | namespace { |
| 22 | |
| 23 | // Support a maximum of 10 fractional digits, similar to the ISO20022 standard. |
| 24 | // https://www.iso20022.org/standardsrepository/public/wqt/Description/mx/dico/ |
| 25 | // datatypes/_L8ZcEp0gEeOo48XfssNw8w |
| 26 | const int kMaximumNumFractionalDigits = 10; |
| 27 | |
| 28 | // Max currency code length. Length of currency code can be at most 2048. |
| 29 | const static size_t kMaxCurrencyCodeLength = 2048; |
| 30 | |
mathp | 94af7a6 | 2017-01-14 11:10:05 | [diff] [blame] | 31 | // Currency codes longer than 6 characters get truncated to 5 + ellipsis. |
| 32 | const static size_t kMaxCurrencyCodeDisplayedChars = 6; |
| 33 | |
| 34 | // Used to truncate long currency codes. |
| 35 | const char kEllipsis[] = "\xE2\x80\xA6"; |
| 36 | |
mathp | 6758be03 | 2017-01-13 04:49:50 | [diff] [blame] | 37 | // Returns whether the |currency_code| is valid to be used in ICU. |
| 38 | bool ShouldUseCurrencyCode(const std::string& currency_code, |
jinho.bang | 4276454 | 2017-01-24 14:42:56 | [diff] [blame] | 39 | const std::string& currency_system) { |
| 40 | return (currency_system.empty() || |
| 41 | currency_system == kIso4217CurrencySystem) && |
mathp | 6758be03 | 2017-01-13 04:49:50 | [diff] [blame] | 42 | !currency_code.empty() && |
| 43 | currency_code.size() <= kMaxCurrencyCodeLength; |
| 44 | } |
| 45 | |
mathp | 94af7a6 | 2017-01-14 11:10:05 | [diff] [blame] | 46 | std::string FormatCurrencyCode(const std::string& currency_code) { |
| 47 | return currency_code.length() < kMaxCurrencyCodeDisplayedChars |
| 48 | ? currency_code |
| 49 | : currency_code.substr(0, kMaxCurrencyCodeDisplayedChars - 1) + |
| 50 | kEllipsis; |
| 51 | } |
| 52 | |
mathp | 6758be03 | 2017-01-13 04:49:50 | [diff] [blame] | 53 | } // namespace |
| 54 | |
jinho.bang | 4276454 | 2017-01-24 14:42:56 | [diff] [blame] | 55 | CurrencyFormatter::CurrencyFormatter(const std::string& currency_code, |
| 56 | const std::string& currency_system, |
| 57 | const std::string& locale_name) |
mathp | 94af7a6 | 2017-01-14 11:10:05 | [diff] [blame] | 58 | : locale_(locale_name.c_str()), |
| 59 | formatted_currency_code_(FormatCurrencyCode(currency_code)) { |
mathp | 6758be03 | 2017-01-13 04:49:50 | [diff] [blame] | 60 | UErrorCode error_code = U_ZERO_ERROR; |
| 61 | icu_formatter_.reset( |
| 62 | icu::NumberFormat::createCurrencyInstance(locale_, error_code)); |
| 63 | if (U_FAILURE(error_code)) { |
mathp | 6758be03 | 2017-01-13 04:49:50 | [diff] [blame] | 64 | LOG(ERROR) << "Failed to initialize the currency formatter for " |
mathp | 94af7a6 | 2017-01-14 11:10:05 | [diff] [blame] | 65 | << locale_name; |
mathp | 6758be03 | 2017-01-13 04:49:50 | [diff] [blame] | 66 | return; |
| 67 | } |
| 68 | |
| 69 | if (ShouldUseCurrencyCode(currency_code, currency_system)) { |
| 70 | currency_code_.reset(new icu::UnicodeString( |
| 71 | currency_code.c_str(), |
| 72 | base::checked_cast<int32_t>(currency_code.size()))); |
| 73 | } else { |
| 74 | // For non-ISO4217 currency system/code, we use a dummy code which is not |
| 75 | // going to appear in the output (stripped in Format()). This is because ICU |
| 76 | // NumberFormat will not accept an empty currency code. Under these |
| 77 | // circumstances, the number amount will be formatted according to locale, |
| 78 | // which is desirable (e.g. "55.00" -> "55,00" in fr_FR). |
| 79 | currency_code_.reset(new icu::UnicodeString("DUM", 3)); |
| 80 | } |
| 81 | |
| 82 | icu_formatter_->setCurrency(currency_code_->getBuffer(), error_code); |
| 83 | if (U_FAILURE(error_code)) { |
| 84 | std::string currency_code_str; |
| 85 | currency_code_->toUTF8String(currency_code_str); |
| 86 | LOG(ERROR) << "Could not set currency code on currency formatter: " |
| 87 | << currency_code_str; |
| 88 | return; |
| 89 | } |
| 90 | |
| 91 | icu_formatter_->setMaximumFractionDigits(kMaximumNumFractionalDigits); |
| 92 | } |
| 93 | |
| 94 | CurrencyFormatter::~CurrencyFormatter() {} |
| 95 | |
| 96 | base::string16 CurrencyFormatter::Format(const std::string& amount) { |
| 97 | // It's possible that the ICU formatter didn't initialize properly. |
| 98 | if (!icu_formatter_ || !icu_formatter_->getCurrency()) |
| 99 | return base::UTF8ToUTF16(amount); |
| 100 | |
| 101 | icu::UnicodeString output; |
| 102 | UErrorCode error_code = U_ZERO_ERROR; |
| 103 | icu_formatter_->format(icu::StringPiece(amount.c_str()), output, nullptr, |
| 104 | error_code); |
| 105 | |
| 106 | if (output.isEmpty()) |
| 107 | return base::UTF8ToUTF16(amount); |
| 108 | |
| 109 | // Explicitly removes the currency code (truncated to its 3-letter and |
| 110 | // 2-letter versions) from the output, because callers are expected to |
| 111 | // display the currency code alongside this result. |
| 112 | // |
| 113 | // 3+ letters: If currency code is "ABCDEF" or "BTX", this code will |
| 114 | // transform "ABC55.00"/"BTX55.00" to "55.00". |
| 115 | // 2 letters: If currency code is "CAD", this code will transform "CA$55.00" |
| 116 | // to "$55.00" (en_US) or "55,00 $ CA" to "55,00 $" (fr_FR). |
| 117 | icu::UnicodeString tmp_currency_code(*currency_code_); |
| 118 | tmp_currency_code.truncate(3); |
| 119 | output.findAndReplace(tmp_currency_code, ""); |
| 120 | tmp_currency_code.truncate(2); |
| 121 | output.findAndReplace(tmp_currency_code, ""); |
| 122 | // Trims any unicode whitespace (including non-breaking space). |
| 123 | if (u_isUWhiteSpace(output[0])) { |
| 124 | output.remove(0, 1); |
| 125 | } |
| 126 | if (u_isUWhiteSpace(output[output.length() - 1])) { |
| 127 | output.remove(output.length() - 1, 1); |
| 128 | } |
| 129 | |
| 130 | std::string output_str; |
| 131 | output.toUTF8String(output_str); |
| 132 | return base::UTF8ToUTF16(output_str); |
| 133 | } |
| 134 | |
| 135 | } // namespace payments |