blob: 1dc2c10dee379a98d66e154507bdb3e85b47c00a [file] [log] [blame]
mathp6758be032017-01-13 04:49:501// 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
17namespace payments {
18
19const char kIso4217CurrencySystem[] = "urn:iso:std:iso:4217";
mathp94af7a62017-01-14 11:10:0520
mathp6758be032017-01-13 04:49:5021namespace {
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
26const int kMaximumNumFractionalDigits = 10;
27
28// Max currency code length. Length of currency code can be at most 2048.
29const static size_t kMaxCurrencyCodeLength = 2048;
30
mathp94af7a62017-01-14 11:10:0531// Currency codes longer than 6 characters get truncated to 5 + ellipsis.
32const static size_t kMaxCurrencyCodeDisplayedChars = 6;
33
34// Used to truncate long currency codes.
35const char kEllipsis[] = "\xE2\x80\xA6";
36
mathp6758be032017-01-13 04:49:5037// Returns whether the |currency_code| is valid to be used in ICU.
38bool ShouldUseCurrencyCode(const std::string& currency_code,
jinho.bang42764542017-01-24 14:42:5639 const std::string& currency_system) {
40 return (currency_system.empty() ||
41 currency_system == kIso4217CurrencySystem) &&
mathp6758be032017-01-13 04:49:5042 !currency_code.empty() &&
43 currency_code.size() <= kMaxCurrencyCodeLength;
44}
45
mathp94af7a62017-01-14 11:10:0546std::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
mathp6758be032017-01-13 04:49:5053} // namespace
54
jinho.bang42764542017-01-24 14:42:5655CurrencyFormatter::CurrencyFormatter(const std::string& currency_code,
56 const std::string& currency_system,
57 const std::string& locale_name)
mathp94af7a62017-01-14 11:10:0558 : locale_(locale_name.c_str()),
59 formatted_currency_code_(FormatCurrencyCode(currency_code)) {
mathp6758be032017-01-13 04:49:5060 UErrorCode error_code = U_ZERO_ERROR;
61 icu_formatter_.reset(
62 icu::NumberFormat::createCurrencyInstance(locale_, error_code));
63 if (U_FAILURE(error_code)) {
mathp6758be032017-01-13 04:49:5064 LOG(ERROR) << "Failed to initialize the currency formatter for "
mathp94af7a62017-01-14 11:10:0565 << locale_name;
mathp6758be032017-01-13 04:49:5066 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
94CurrencyFormatter::~CurrencyFormatter() {}
95
96base::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