summaryrefslogtreecommitdiffstats
path: root/intl/components/gtest
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /intl/components/gtest
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'intl/components/gtest')
-rw-r--r--intl/components/gtest/TestBidi.cpp314
-rw-r--r--intl/components/gtest/TestBuffer.h149
-rw-r--r--intl/components/gtest/TestCalendar.cpp156
-rw-r--r--intl/components/gtest/TestCollator.cpp347
-rw-r--r--intl/components/gtest/TestCurrency.cpp34
-rw-r--r--intl/components/gtest/TestDateIntervalFormat.cpp200
-rw-r--r--intl/components/gtest/TestDateTimeFormat.cpp621
-rw-r--r--intl/components/gtest/TestDisplayNames.cpp635
-rw-r--r--intl/components/gtest/TestIDNA.cpp118
-rw-r--r--intl/components/gtest/TestListFormat.cpp162
-rw-r--r--intl/components/gtest/TestLocale.cpp152
-rw-r--r--intl/components/gtest/TestLocaleCanonicalizer.cpp60
-rw-r--r--intl/components/gtest/TestMeasureUnit.cpp43
-rw-r--r--intl/components/gtest/TestNumberFormat.cpp236
-rw-r--r--intl/components/gtest/TestNumberParser.cpp34
-rw-r--r--intl/components/gtest/TestNumberRangeFormat.cpp118
-rw-r--r--intl/components/gtest/TestNumberingSystem.cpp23
-rw-r--r--intl/components/gtest/TestPluralRules.cpp671
-rw-r--r--intl/components/gtest/TestRelativeTimeFormat.cpp161
-rw-r--r--intl/components/gtest/TestScript.cpp62
-rw-r--r--intl/components/gtest/TestString.cpp260
-rw-r--r--intl/components/gtest/TestTimeZone.cpp249
-rw-r--r--intl/components/gtest/moz.build30
23 files changed, 4835 insertions, 0 deletions
diff --git a/intl/components/gtest/TestBidi.cpp b/intl/components/gtest/TestBidi.cpp
new file mode 100644
index 0000000000..f928f890a6
--- /dev/null
+++ b/intl/components/gtest/TestBidi.cpp
@@ -0,0 +1,314 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "mozilla/intl/Bidi.h"
+#include "mozilla/Span.h"
+namespace mozilla::intl {
+
+struct VisualRun {
+ Span<const char16_t> string;
+ BidiDirection direction;
+};
+
+/**
+ * An iterator for visual runs in a paragraph. See Bug 1736597 for integrating
+ * this into the public API.
+ */
+class MOZ_STACK_CLASS VisualRunIter {
+ public:
+ VisualRunIter(Bidi& aBidi, Span<const char16_t> aParagraph,
+ BidiEmbeddingLevel aLevel)
+ : mBidi(aBidi), mParagraph(aParagraph) {
+ // Crash in case of errors by calling unwrap. If this were a real API, this
+ // would be a TryCreate call.
+ mBidi.SetParagraph(aParagraph, aLevel).unwrap();
+ mRunCount = mBidi.CountRuns().unwrap();
+ }
+
+ Maybe<VisualRun> Next() {
+ if (mRunIndex >= mRunCount) {
+ return Nothing();
+ }
+
+ int32_t stringIndex = -1;
+ int32_t stringLength = -1;
+
+ BidiDirection direction =
+ mBidi.GetVisualRun(mRunIndex, &stringIndex, &stringLength);
+
+ Span<const char16_t> string(mParagraph.Elements() + stringIndex,
+ stringLength);
+ mRunIndex++;
+ return Some(VisualRun{string, direction});
+ }
+
+ private:
+ Bidi& mBidi;
+ Span<const char16_t> mParagraph = Span<const char16_t>();
+ int32_t mRunIndex = 0;
+ int32_t mRunCount = 0;
+};
+
+struct LogicalRun {
+ Span<const char16_t> string;
+ BidiEmbeddingLevel embeddingLevel;
+};
+
+/**
+ * An iterator for logical runs in a paragraph. See Bug 1736597 for integrating
+ * this into the public API.
+ */
+class MOZ_STACK_CLASS LogicalRunIter {
+ public:
+ LogicalRunIter(Bidi& aBidi, Span<const char16_t> aParagraph,
+ BidiEmbeddingLevel aLevel)
+ : mBidi(aBidi), mParagraph(aParagraph) {
+ // Crash in case of errors by calling unwrap. If this were a real API, this
+ // would be a TryCreate call.
+ mBidi.SetParagraph(aParagraph, aLevel).unwrap();
+ mBidi.CountRuns().unwrap();
+ }
+
+ Maybe<LogicalRun> Next() {
+ if (mRunIndex >= static_cast<int32_t>(mParagraph.Length())) {
+ return Nothing();
+ }
+
+ int32_t logicalLimit;
+
+ BidiEmbeddingLevel embeddingLevel;
+ mBidi.GetLogicalRun(mRunIndex, &logicalLimit, &embeddingLevel);
+
+ Span<const char16_t> string(mParagraph.Elements() + mRunIndex,
+ logicalLimit - mRunIndex);
+
+ mRunIndex = logicalLimit;
+ return Some(LogicalRun{string, embeddingLevel});
+ }
+
+ private:
+ Bidi& mBidi;
+ Span<const char16_t> mParagraph = Span<const char16_t>();
+ int32_t mRunIndex = 0;
+};
+
+TEST(IntlBidi, SimpleLTR)
+{
+ Bidi bidi{};
+ LogicalRunIter logicalRunIter(bidi, MakeStringSpan(u"this is a paragraph"),
+ BidiEmbeddingLevel::DefaultLTR());
+ ASSERT_EQ(bidi.GetParagraphEmbeddingLevel(), 0);
+ ASSERT_EQ(bidi.GetParagraphDirection(), Bidi::ParagraphDirection::LTR);
+
+ {
+ auto logicalRun = logicalRunIter.Next();
+ ASSERT_TRUE(logicalRun.isSome());
+ ASSERT_EQ(logicalRun->string, MakeStringSpan(u"this is a paragraph"));
+ ASSERT_EQ(logicalRun->embeddingLevel, 0);
+ ASSERT_EQ(logicalRun->embeddingLevel.Direction(), BidiDirection::LTR);
+ }
+
+ {
+ auto logicalRun = logicalRunIter.Next();
+ ASSERT_TRUE(logicalRun.isNothing());
+ }
+}
+
+TEST(IntlBidi, SimpleRTL)
+{
+ Bidi bidi{};
+ LogicalRunIter logicalRunIter(bidi, MakeStringSpan(u"فايرفوكس رائع"),
+ BidiEmbeddingLevel::DefaultLTR());
+ ASSERT_EQ(bidi.GetParagraphEmbeddingLevel(), 1);
+ ASSERT_EQ(bidi.GetParagraphDirection(), Bidi::ParagraphDirection::RTL);
+
+ {
+ auto logicalRun = logicalRunIter.Next();
+ ASSERT_TRUE(logicalRun.isSome());
+ ASSERT_EQ(logicalRun->string, MakeStringSpan(u"فايرفوكس رائع"));
+ ASSERT_EQ(logicalRun->embeddingLevel.Direction(), BidiDirection::RTL);
+ ASSERT_EQ(logicalRun->embeddingLevel, 1);
+ }
+
+ {
+ auto logicalRun = logicalRunIter.Next();
+ ASSERT_TRUE(logicalRun.isNothing());
+ }
+}
+
+TEST(IntlBidi, MultiLevel)
+{
+ Bidi bidi{};
+ LogicalRunIter logicalRunIter(
+ bidi, MakeStringSpan(u"Firefox is awesome: رائع Firefox"),
+ BidiEmbeddingLevel::DefaultLTR());
+ ASSERT_EQ(bidi.GetParagraphEmbeddingLevel(), 0);
+ ASSERT_EQ(bidi.GetParagraphDirection(), Bidi::ParagraphDirection::Mixed);
+
+ {
+ auto logicalRun = logicalRunIter.Next();
+ ASSERT_TRUE(logicalRun.isSome());
+ ASSERT_EQ(logicalRun->string, MakeStringSpan(u"Firefox is awesome: "));
+ ASSERT_EQ(logicalRun->embeddingLevel, 0);
+ }
+ {
+ auto logicalRun = logicalRunIter.Next();
+ ASSERT_TRUE(logicalRun.isSome());
+ ASSERT_EQ(logicalRun->string, MakeStringSpan(u"رائع"));
+ ASSERT_EQ(logicalRun->embeddingLevel, 1);
+ }
+ {
+ auto logicalRun = logicalRunIter.Next();
+ ASSERT_TRUE(logicalRun.isSome());
+ ASSERT_EQ(logicalRun->string, MakeStringSpan(u" Firefox"));
+ ASSERT_EQ(logicalRun->embeddingLevel, 0);
+ }
+ {
+ auto logicalRun = logicalRunIter.Next();
+ ASSERT_TRUE(logicalRun.isNothing());
+ }
+}
+
+TEST(IntlBidi, RtlOverride)
+{
+ Bidi bidi{};
+ // Set the paragraph using the RTL embedding mark U+202B, and the LTR
+ // embedding mark U+202A to increase the embedding level. This mark switches
+ // the weakly directional character "_". This demonstrates that embedding
+ // levels can be computed.
+ LogicalRunIter logicalRunIter(
+ bidi, MakeStringSpan(u"ltr\u202b___رائع___\u202a___ltr__"),
+ BidiEmbeddingLevel::DefaultLTR());
+ ASSERT_EQ(bidi.GetParagraphEmbeddingLevel(), 0);
+ ASSERT_EQ(bidi.GetParagraphDirection(), Bidi::ParagraphDirection::Mixed);
+
+ {
+ auto logicalRun = logicalRunIter.Next();
+ ASSERT_TRUE(logicalRun.isSome());
+ ASSERT_EQ(logicalRun->string, MakeStringSpan(u"ltr"));
+ ASSERT_EQ(logicalRun->embeddingLevel, 0);
+ ASSERT_EQ(logicalRun->embeddingLevel.Direction(), BidiDirection::LTR);
+ }
+ {
+ auto logicalRun = logicalRunIter.Next();
+ ASSERT_TRUE(logicalRun.isSome());
+ ASSERT_EQ(logicalRun->string, MakeStringSpan(u"\u202b___رائع___"));
+ ASSERT_EQ(logicalRun->embeddingLevel, 1);
+ ASSERT_EQ(logicalRun->embeddingLevel.Direction(), BidiDirection::RTL);
+ }
+ {
+ auto logicalRun = logicalRunIter.Next();
+ ASSERT_TRUE(logicalRun.isSome());
+ ASSERT_EQ(logicalRun->string, MakeStringSpan(u"\u202a___ltr__"));
+ ASSERT_EQ(logicalRun->embeddingLevel, 2);
+ ASSERT_EQ(logicalRun->embeddingLevel.Direction(), BidiDirection::LTR);
+ }
+ {
+ auto logicalRun = logicalRunIter.Next();
+ ASSERT_TRUE(logicalRun.isNothing());
+ }
+}
+
+TEST(IntlBidi, VisualRuns)
+{
+ Bidi bidi{};
+
+ VisualRunIter visualRunIter(
+ bidi,
+ MakeStringSpan(
+ u"first visual run التشغيل البصري الثاني third visual run"),
+ BidiEmbeddingLevel::DefaultLTR());
+ {
+ Maybe<VisualRun> run = visualRunIter.Next();
+ ASSERT_TRUE(run.isSome());
+ ASSERT_EQ(run->string, MakeStringSpan(u"first visual run "));
+ ASSERT_EQ(run->direction, BidiDirection::LTR);
+ }
+ {
+ Maybe<VisualRun> run = visualRunIter.Next();
+ ASSERT_TRUE(run.isSome());
+ ASSERT_EQ(run->string, MakeStringSpan(u"التشغيل البصري الثاني"));
+ ASSERT_EQ(run->direction, BidiDirection::RTL);
+ }
+ {
+ Maybe<VisualRun> run = visualRunIter.Next();
+ ASSERT_TRUE(run.isSome());
+ ASSERT_EQ(run->string, MakeStringSpan(u" third visual run"));
+ ASSERT_EQ(run->direction, BidiDirection::LTR);
+ }
+ {
+ Maybe<VisualRun> run = visualRunIter.Next();
+ ASSERT_TRUE(run.isNothing());
+ }
+}
+
+TEST(IntlBidi, VisualRunsWithEmbeds)
+{
+ // Compare this test to the logical order test.
+ Bidi bidi{};
+ VisualRunIter visualRunIter(
+ bidi, MakeStringSpan(u"ltr\u202b___رائع___\u202a___ltr___"),
+ BidiEmbeddingLevel::DefaultLTR());
+ {
+ Maybe<VisualRun> run = visualRunIter.Next();
+ ASSERT_TRUE(run.isSome());
+ ASSERT_EQ(run->string, MakeStringSpan(u"ltr"));
+ ASSERT_EQ(run->direction, BidiDirection::LTR);
+ }
+ {
+ Maybe<VisualRun> run = visualRunIter.Next();
+ ASSERT_TRUE(run.isSome());
+ ASSERT_EQ(run->string, MakeStringSpan(u"\u202a___ltr___"));
+ ASSERT_EQ(run->direction, BidiDirection::LTR);
+ }
+ {
+ Maybe<VisualRun> run = visualRunIter.Next();
+ ASSERT_TRUE(run.isSome());
+ ASSERT_EQ(run->string, MakeStringSpan(u"\u202b___رائع___"));
+ ASSERT_EQ(run->direction, BidiDirection::RTL);
+ }
+ {
+ Maybe<VisualRun> run = visualRunIter.Next();
+ ASSERT_TRUE(run.isNothing());
+ }
+}
+
+// The full Bidi class can be found in [1].
+//
+// [1]: https://www.unicode.org/Public/UNIDATA/extracted/DerivedBidiClass.txt
+TEST(IntlBidi, GetBaseDirection)
+{
+ // Return Neutral as default if empty string is provided.
+ ASSERT_EQ(Bidi::GetBaseDirection(nullptr), Bidi::BaseDirection::Neutral);
+
+ // White space(WS) is classified as Neutral.
+ ASSERT_EQ(Bidi::GetBaseDirection(MakeStringSpan(u" ")),
+ Bidi::BaseDirection::Neutral);
+
+ // 000A and 000D are paragraph separators(BS), which are also classified as
+ // Neutral.
+ ASSERT_EQ(Bidi::GetBaseDirection(MakeStringSpan(u"\u000A")),
+ Bidi::BaseDirection::Neutral);
+ ASSERT_EQ(Bidi::GetBaseDirection(MakeStringSpan(u"\u000D")),
+ Bidi::BaseDirection::Neutral);
+
+ // 0620..063f are Arabic letters, which is of type AL.
+ ASSERT_EQ(Bidi::GetBaseDirection(MakeStringSpan(u"\u0620\u0621\u0622")),
+ Bidi::BaseDirection::RTL);
+ ASSERT_EQ(Bidi::GetBaseDirection(MakeStringSpan(u" \u0620\u0621\u0622")),
+ Bidi::BaseDirection::RTL);
+ ASSERT_EQ(Bidi::GetBaseDirection(MakeStringSpan(u"\u0620\u0621\u0622ABC")),
+ Bidi::BaseDirection::RTL);
+
+ // First strong character is of English letters.
+ ASSERT_EQ(Bidi::GetBaseDirection(MakeStringSpan(u"ABC")),
+ Bidi::BaseDirection::LTR);
+ ASSERT_EQ(Bidi::GetBaseDirection(MakeStringSpan(u" ABC")),
+ Bidi::BaseDirection::LTR);
+ ASSERT_EQ(Bidi::GetBaseDirection(MakeStringSpan(u"ABC\u0620")),
+ Bidi::BaseDirection::LTR);
+}
+
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/TestBuffer.h b/intl/components/gtest/TestBuffer.h
new file mode 100644
index 0000000000..69412ba521
--- /dev/null
+++ b/intl/components/gtest/TestBuffer.h
@@ -0,0 +1,149 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef intl_components_gtest_TestBuffer_h_
+#define intl_components_gtest_TestBuffer_h_
+
+#include <string_view>
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Utf8.h"
+#include "mozilla/Vector.h"
+
+namespace mozilla::intl {
+
+/**
+ * A test buffer for interfacing with unified intl classes.
+ * Closely resembles the FormatBuffer class, but without
+ * JavaScript-specific implementation details.
+ */
+template <typename C, size_t inlineCapacity = 0>
+class TestBuffer {
+ public:
+ using CharType = C;
+
+ // Only allow moves, and not copies, as this class owns the mozilla::Vector.
+ TestBuffer(TestBuffer&& other) noexcept = default;
+ TestBuffer& operator=(TestBuffer&& other) noexcept = default;
+
+ explicit TestBuffer(const size_t aSize = 0) { reserve(aSize); }
+
+ /**
+ * Ensures the buffer has enough space to accommodate |aSize| elemtns.
+ */
+ bool reserve(const size_t aSize) { return mBuffer.reserve(aSize); }
+
+ /**
+ * Returns the raw data inside the buffer.
+ */
+ CharType* data() { return mBuffer.begin(); }
+
+ /**
+ * Returns the count of elements in written to the buffer.
+ */
+ size_t length() const { return mBuffer.length(); }
+
+ /**
+ * Returns the buffer's overall capacity.
+ */
+ size_t capacity() const { return mBuffer.capacity(); }
+
+ /**
+ * Resizes the buffer to the given amount of written elements.
+ * This is necessary because the buffer gets written to across
+ * FFI boundaries, so this needs to happen in a separate step.
+ */
+ void written(size_t aAmount) {
+ MOZ_ASSERT(aAmount <= mBuffer.capacity());
+ mozilla::DebugOnly<bool> result = mBuffer.resizeUninitialized(aAmount);
+ MOZ_ASSERT(result);
+ }
+
+ /**
+ * Get a string view into the buffer, which is useful for test assertions.
+ */
+ std::basic_string_view<CharType> get_string_view() {
+ return std::basic_string_view<CharType>(data(), length());
+ }
+
+ /**
+ * Clear the buffer, allowing it to be re-used.
+ */
+ void clear() { mBuffer.clear(); }
+
+ /**
+ * A utility function to convert UTF-16 strings to UTF-8 strings so that they
+ * can be logged to stderr.
+ */
+ static std::string toUtf8(mozilla::Span<const char16_t> input) {
+ size_t buff_len = input.Length() * 3;
+ std::string result(buff_len, ' ');
+ result.reserve(buff_len);
+ size_t result_len =
+ ConvertUtf16toUtf8(input, mozilla::Span(result.data(), buff_len));
+ result.resize(result_len);
+ return result;
+ }
+
+ /**
+ * String buffers, especially UTF-16, do not assert nicely, and are difficult
+ * to debug. This function is verbose in that it prints the buffer contents
+ * and expected contents to stderr when they do not match.
+ *
+ * Usage:
+ * ASSERT_TRUE(buffer.assertStringView(u"9/23/2002, 8:07:30 PM"));
+ *
+ * Here is what gtests output:
+ *
+ * Expected equality of these values:
+ * buffer.get_string_view()
+ * Which is: { '0' (48, 0x30), '9' (57, 0x39), '/' (47, 0x2F), ... }
+ * "9/23/2002, 8:07:30 PM"
+ * Which is: 0x11600afb9
+ *
+ * Here is what this method outputs:
+ *
+ * The buffer did not match:
+ * Buffer:
+ * u"9/23/2002, 8:07:30 PM"
+ * Expected:
+ * u"09/23/2002, 08:07:30 PM"
+ */
+ [[nodiscard]] bool verboseMatches(const CharType* aExpected) {
+ std::basic_string_view<CharType> actualSV(data(), length());
+ std::basic_string_view<CharType> expectedSV(aExpected);
+
+ if (actualSV.compare(expectedSV) == 0) {
+ return true;
+ }
+
+ static_assert(std::is_same_v<CharType, char> ||
+ std::is_same_v<CharType, char16_t>);
+
+ std::string actual;
+ std::string expected;
+ const char* startQuote;
+
+ if constexpr (std::is_same_v<CharType, char>) {
+ actual = std::string(actualSV);
+ expected = std::string(expectedSV);
+ startQuote = "\"";
+ }
+ if constexpr (std::is_same_v<CharType, char16_t>) {
+ actual = toUtf8(actualSV);
+ expected = toUtf8(expectedSV);
+ startQuote = "u\"";
+ }
+
+ fprintf(stderr, "The buffer did not match:\n");
+ fprintf(stderr, " Buffer:\n %s%s\"\n", startQuote, actual.c_str());
+ fprintf(stderr, " Expected:\n %s%s\"\n", startQuote, expected.c_str());
+
+ return false;
+ }
+
+ Vector<C, inlineCapacity> mBuffer{};
+};
+
+} // namespace mozilla::intl
+
+#endif
diff --git a/intl/components/gtest/TestCalendar.cpp b/intl/components/gtest/TestCalendar.cpp
new file mode 100644
index 0000000000..a40fea0027
--- /dev/null
+++ b/intl/components/gtest/TestCalendar.cpp
@@ -0,0 +1,156 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "mozilla/intl/Calendar.h"
+#include "mozilla/intl/DateTimeFormat.h"
+#include "mozilla/Span.h"
+#include "TestBuffer.h"
+
+namespace mozilla::intl {
+
+// Firefox 1.0 release date.
+const double CALENDAR_DATE = 1032800850000.0;
+
+TEST(IntlCalendar, GetLegacyKeywordValuesForLocale)
+{
+ bool hasGregorian = false;
+ bool hasIslamic = false;
+ auto gregorian = MakeStringSpan("gregorian");
+ auto islamic = MakeStringSpan("islamic");
+ auto keywords = Calendar::GetLegacyKeywordValuesForLocale("en-US").unwrap();
+ for (auto name : keywords) {
+ // Check a few keywords, as this list may not be stable between ICU updates.
+ if (name.unwrap() == gregorian) {
+ hasGregorian = true;
+ }
+ if (name.unwrap() == islamic) {
+ hasIslamic = true;
+ }
+ }
+ ASSERT_TRUE(hasGregorian);
+ ASSERT_TRUE(hasIslamic);
+}
+
+TEST(IntlCalendar, GetBcp47KeywordValuesForLocale)
+{
+ bool hasGregory = false;
+ bool hasIslamic = false;
+ auto gregory = MakeStringSpan("gregory");
+ auto islamic = MakeStringSpan("islamic");
+ auto keywords = Calendar::GetBcp47KeywordValuesForLocale("en-US").unwrap();
+ for (auto name : keywords) {
+ // Check a few keywords, as this list may not be stable between ICU updates.
+ if (name.unwrap() == gregory) {
+ hasGregory = true;
+ }
+ if (name.unwrap() == islamic) {
+ hasIslamic = true;
+ }
+ }
+ ASSERT_TRUE(hasGregory);
+ ASSERT_TRUE(hasIslamic);
+}
+
+TEST(IntlCalendar, GetBcp47KeywordValuesForLocaleCommonlyUsed)
+{
+ bool hasGregory = false;
+ bool hasIslamic = false;
+ auto gregory = MakeStringSpan("gregory");
+ auto islamic = MakeStringSpan("islamic");
+ auto keywords = Calendar::GetBcp47KeywordValuesForLocale(
+ "en-US", Calendar::CommonlyUsed::Yes)
+ .unwrap();
+ for (auto name : keywords) {
+ // Check a few keywords, as this list may not be stable between ICU updates.
+ if (name.unwrap() == gregory) {
+ hasGregory = true;
+ }
+ if (name.unwrap() == islamic) {
+ hasIslamic = true;
+ }
+ }
+ ASSERT_TRUE(hasGregory);
+ ASSERT_FALSE(hasIslamic);
+}
+
+TEST(IntlCalendar, GetBcp47Type)
+{
+ auto calendar =
+ Calendar::TryCreate("en-US", Some(MakeStringSpan(u"GMT+3"))).unwrap();
+ ASSERT_STREQ(calendar->GetBcp47Type().unwrap().data(), "gregory");
+}
+
+TEST(IntlCalendar, SetTimeInMs)
+{
+ auto calendar =
+ Calendar::TryCreate("en-US", Some(MakeStringSpan(u"GMT+3"))).unwrap();
+
+ // There is no way to verify the results. Unwrap the results to ensure it
+ // doesn't fail, but don't check the values.
+ calendar->SetTimeInMs(CALENDAR_DATE).unwrap();
+}
+
+TEST(IntlCalendar, CloneFrom)
+{
+ DateTimeFormat::StyleBag style;
+ style.date = Some(DateTimeFormat::Style::Medium);
+ style.time = Some(DateTimeFormat::Style::Medium);
+ auto dtFormat = DateTimeFormat::TryCreateFromStyle(
+ MakeStringSpan("en-US"), style,
+ // It's ok to pass nullptr here, as it will cause format
+ // operations to fail, but this test is only checking
+ // calendar cloning.
+ nullptr, Some(MakeStringSpan(u"America/Chicago")))
+ .unwrap();
+
+ dtFormat->CloneCalendar(CALENDAR_DATE).unwrap();
+}
+
+TEST(IntlCalendar, GetWeekend)
+{
+ auto calendar_en_US = Calendar::TryCreate("en-US").unwrap();
+ auto weekend_en_US = calendar_en_US->GetWeekend().unwrap();
+ ASSERT_EQ(weekend_en_US, EnumSet({Weekday::Saturday, Weekday::Sunday}));
+
+ auto calendar_de_DE = Calendar::TryCreate("de-DE").unwrap();
+ auto weekend_de_DE = calendar_de_DE->GetWeekend().unwrap();
+ ASSERT_EQ(weekend_de_DE, EnumSet({Weekday::Saturday, Weekday::Sunday}));
+
+ auto calendar_ar_EG = Calendar::TryCreate("ar-EG").unwrap();
+ auto weekend_ar_EG = calendar_ar_EG->GetWeekend().unwrap();
+ ASSERT_EQ(weekend_ar_EG, EnumSet({Weekday::Friday, Weekday::Saturday}));
+}
+
+TEST(IntlCalendar, GetFirstDayOfWeek)
+{
+ auto calendar_en_US = Calendar::TryCreate("en-US").unwrap();
+ auto firstDayOfWeek_en_US = calendar_en_US->GetFirstDayOfWeek();
+ ASSERT_EQ(firstDayOfWeek_en_US, Weekday::Sunday);
+
+ auto calendar_de_DE = Calendar::TryCreate("de-DE").unwrap();
+ auto firstDayOfWeek_de_DE = calendar_de_DE->GetFirstDayOfWeek();
+ ASSERT_EQ(firstDayOfWeek_de_DE, Weekday::Monday);
+
+ auto calendar_ar_EG = Calendar::TryCreate("ar-EG").unwrap();
+ auto firstDayOfWeek_ar_EG = calendar_ar_EG->GetFirstDayOfWeek();
+ ASSERT_EQ(firstDayOfWeek_ar_EG, Weekday::Saturday);
+}
+
+TEST(IntlCalendar, GetMinimalDaysInFirstWeek)
+{
+ auto calendar_en_US = Calendar::TryCreate("en-US").unwrap();
+ auto minimalDays_en_US = calendar_en_US->GetMinimalDaysInFirstWeek();
+ ASSERT_EQ(minimalDays_en_US, 1);
+
+ auto calendar_de_DE = Calendar::TryCreate("de-DE").unwrap();
+ auto minimalDays_de_DE = calendar_de_DE->GetMinimalDaysInFirstWeek();
+ ASSERT_EQ(minimalDays_de_DE, 4);
+
+ auto calendar_ar_EG = Calendar::TryCreate("ar-EG").unwrap();
+ auto minimalDays_ar_EG = calendar_ar_EG->GetMinimalDaysInFirstWeek();
+ ASSERT_EQ(minimalDays_ar_EG, 1);
+}
+
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/TestCollator.cpp b/intl/components/gtest/TestCollator.cpp
new file mode 100644
index 0000000000..d19eeff089
--- /dev/null
+++ b/intl/components/gtest/TestCollator.cpp
@@ -0,0 +1,347 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include <string.h>
+#include <string_view>
+#include "mozilla/intl/Collator.h"
+#include "mozilla/Span.h"
+#include "TestBuffer.h"
+
+namespace mozilla::intl {
+
+TEST(IntlCollator, SetAttributesInternal)
+{
+ // Run through each settings to make sure MOZ_ASSERT is not triggered for
+ // misconfigured attributes.
+ auto result = Collator::TryCreate("en-US");
+ ASSERT_TRUE(result.isOk());
+ auto collator = result.unwrap();
+
+ collator->SetStrength(Collator::Strength::Primary);
+ collator->SetStrength(Collator::Strength::Secondary);
+ collator->SetStrength(Collator::Strength::Tertiary);
+ collator->SetStrength(Collator::Strength::Quaternary);
+ collator->SetStrength(Collator::Strength::Identical);
+ collator->SetStrength(Collator::Strength::Default);
+
+ collator->SetAlternateHandling(Collator::AlternateHandling::NonIgnorable)
+ .unwrap();
+ collator->SetAlternateHandling(Collator::AlternateHandling::Shifted).unwrap();
+ collator->SetAlternateHandling(Collator::AlternateHandling::Default).unwrap();
+
+ collator->SetCaseFirst(Collator::CaseFirst::False).unwrap();
+ collator->SetCaseFirst(Collator::CaseFirst::Upper).unwrap();
+ collator->SetCaseFirst(Collator::CaseFirst::Lower).unwrap();
+
+ collator->SetCaseLevel(Collator::Feature::On).unwrap();
+ collator->SetCaseLevel(Collator::Feature::Off).unwrap();
+ collator->SetCaseLevel(Collator::Feature::Default).unwrap();
+
+ collator->SetNumericCollation(Collator::Feature::On).unwrap();
+ collator->SetNumericCollation(Collator::Feature::Off).unwrap();
+ collator->SetNumericCollation(Collator::Feature::Default).unwrap();
+
+ collator->SetNormalizationMode(Collator::Feature::On).unwrap();
+ collator->SetNormalizationMode(Collator::Feature::Off).unwrap();
+ collator->SetNormalizationMode(Collator::Feature::Default).unwrap();
+}
+
+TEST(IntlCollator, GetSortKey)
+{
+ // Do some light sort key comparisons to ensure everything is wired up
+ // correctly. This is not doing extensive correctness testing.
+ auto result = Collator::TryCreate("en-US");
+ ASSERT_TRUE(result.isOk());
+ auto collator = result.unwrap();
+ TestBuffer<uint8_t> bufferA;
+ TestBuffer<uint8_t> bufferB;
+
+ auto compareSortKeys = [&](const char16_t* a, const char16_t* b) {
+ collator->GetSortKey(MakeStringSpan(a), bufferA).unwrap();
+ collator->GetSortKey(MakeStringSpan(b), bufferB).unwrap();
+ return strcmp(reinterpret_cast<const char*>(bufferA.data()),
+ reinterpret_cast<const char*>(bufferB.data()));
+ };
+
+ ASSERT_TRUE(compareSortKeys(u"aaa", u"bbb") < 0);
+ ASSERT_TRUE(compareSortKeys(u"bbb", u"aaa") > 0);
+ ASSERT_TRUE(compareSortKeys(u"aaa", u"aaa") == 0);
+ ASSERT_TRUE(compareSortKeys(u"👍", u"👎") < 0);
+}
+
+TEST(IntlCollator, CompareStrings)
+{
+ // Do some light string comparisons to ensure everything is wired up
+ // correctly. This is not doing extensive correctness testing.
+ auto result = Collator::TryCreate("en-US");
+ ASSERT_TRUE(result.isOk());
+ auto collator = result.unwrap();
+ TestBuffer<uint8_t> bufferA;
+ TestBuffer<uint8_t> bufferB;
+
+ ASSERT_EQ(collator->CompareStrings(u"aaa", u"bbb"), -1);
+ ASSERT_EQ(collator->CompareStrings(u"bbb", u"aaa"), 1);
+ ASSERT_EQ(collator->CompareStrings(u"aaa", u"aaa"), 0);
+ ASSERT_EQ(collator->CompareStrings(u"👍", u"👎"), -1);
+}
+
+TEST(IntlCollator, SetOptionsSensitivity)
+{
+ // Test the ECMA 402 sensitivity behavior per:
+ // https://tc39.es/ecma402/#sec-collator-comparestrings
+ auto result = Collator::TryCreate("en-US");
+ ASSERT_TRUE(result.isOk());
+ auto collator = result.unwrap();
+
+ TestBuffer<uint8_t> bufferA;
+ TestBuffer<uint8_t> bufferB;
+ ICUResult optResult = Ok();
+ Collator::Options options{};
+
+ options.sensitivity = Collator::Sensitivity::Base;
+ optResult = collator->SetOptions(options);
+ ASSERT_TRUE(optResult.isOk());
+ ASSERT_EQ(collator->CompareStrings(u"a", u"b"), -1);
+ ASSERT_EQ(collator->CompareStrings(u"a", u"á"), 0);
+ ASSERT_EQ(collator->CompareStrings(u"a", u"A"), 0);
+
+ options.sensitivity = Collator::Sensitivity::Accent;
+ optResult = collator->SetOptions(options);
+ ASSERT_TRUE(optResult.isOk());
+ ASSERT_EQ(collator->CompareStrings(u"a", u"b"), -1);
+ ASSERT_EQ(collator->CompareStrings(u"a", u"á"), -1);
+ ASSERT_EQ(collator->CompareStrings(u"a", u"A"), 0);
+
+ options.sensitivity = Collator::Sensitivity::Case;
+ optResult = collator->SetOptions(options);
+ ASSERT_TRUE(optResult.isOk());
+ ASSERT_EQ(collator->CompareStrings(u"a", u"b"), -1);
+ ASSERT_EQ(collator->CompareStrings(u"a", u"á"), 0);
+ ASSERT_EQ(collator->CompareStrings(u"a", u"A"), -1);
+
+ options.sensitivity = Collator::Sensitivity::Variant;
+ optResult = collator->SetOptions(options);
+ ASSERT_TRUE(optResult.isOk());
+ ASSERT_EQ(collator->CompareStrings(u"a", u"b"), -1);
+ ASSERT_EQ(collator->CompareStrings(u"a", u"á"), -1);
+ ASSERT_EQ(collator->CompareStrings(u"a", u"A"), -1);
+}
+
+TEST(IntlCollator, LocaleSensitiveCollations)
+{
+ UniquePtr<Collator> collator = nullptr;
+ TestBuffer<uint8_t> bufferA;
+ TestBuffer<uint8_t> bufferB;
+
+ auto changeLocale = [&](const char* locale) {
+ auto result = Collator::TryCreate(locale);
+ ASSERT_TRUE(result.isOk());
+ collator = result.unwrap();
+
+ Collator::Options options{};
+ options.sensitivity = Collator::Sensitivity::Base;
+ auto optResult = collator->SetOptions(options);
+ ASSERT_TRUE(optResult.isOk());
+ };
+
+ // Swedish treats "Ö" as a separate character, which sorts after "Z".
+ changeLocale("en-US");
+ ASSERT_EQ(collator->CompareStrings(u"Österreich", u"Västervik"), -1);
+ changeLocale("sv-SE");
+ ASSERT_EQ(collator->CompareStrings(u"Österreich", u"Västervik"), 1);
+
+ // Country names in their respective scripts.
+ auto china = MakeStringSpan(u"中国");
+ auto japan = MakeStringSpan(u"日本");
+ auto korea = MakeStringSpan(u"한국");
+
+ changeLocale("en-US");
+ ASSERT_EQ(collator->CompareStrings(china, japan), -1);
+ ASSERT_EQ(collator->CompareStrings(china, korea), 1);
+ changeLocale("zh");
+ ASSERT_EQ(collator->CompareStrings(china, japan), 1);
+ ASSERT_EQ(collator->CompareStrings(china, korea), -1);
+ changeLocale("ja");
+ ASSERT_EQ(collator->CompareStrings(china, japan), -1);
+ ASSERT_EQ(collator->CompareStrings(china, korea), -1);
+ changeLocale("ko");
+ ASSERT_EQ(collator->CompareStrings(china, japan), 1);
+ ASSERT_EQ(collator->CompareStrings(china, korea), -1);
+}
+
+TEST(IntlCollator, IgnorePunctuation)
+{
+ TestBuffer<uint8_t> bufferA;
+ TestBuffer<uint8_t> bufferB;
+
+ auto result = Collator::TryCreate("en-US");
+ ASSERT_TRUE(result.isOk());
+ auto collator = result.unwrap();
+ Collator::Options options{};
+ options.ignorePunctuation = true;
+
+ auto optResult = collator->SetOptions(options);
+ ASSERT_TRUE(optResult.isOk());
+
+ ASSERT_EQ(collator->CompareStrings(u"aa", u".bb"), -1);
+
+ options.ignorePunctuation = false;
+ optResult = collator->SetOptions(options);
+ ASSERT_TRUE(optResult.isOk());
+
+ ASSERT_EQ(collator->CompareStrings(u"aa", u".bb"), 1);
+}
+
+TEST(IntlCollator, GetBcp47KeywordValuesForLocale)
+{
+ auto extsResult = Collator::GetBcp47KeywordValuesForLocale("de");
+ ASSERT_TRUE(extsResult.isOk());
+ auto extensions = extsResult.unwrap();
+
+ // Since this list is dependent on ICU, and may change between upgrades, only
+ // test a subset of the keywords.
+ auto standard = MakeStringSpan("standard");
+ auto search = MakeStringSpan("search");
+ auto phonebk = MakeStringSpan("phonebk"); // Valid BCP 47.
+ auto phonebook = MakeStringSpan("phonebook"); // Not valid BCP 47.
+ bool hasStandard = false;
+ bool hasSearch = false;
+ bool hasPhonebk = false;
+ bool hasPhonebook = false;
+
+ for (auto extensionResult : extensions) {
+ ASSERT_TRUE(extensionResult.isOk());
+ auto extension = extensionResult.unwrap();
+ hasStandard |= extension == standard;
+ hasSearch |= extension == search;
+ hasPhonebk |= extension == phonebk;
+ hasPhonebook |= extension == phonebook;
+ }
+
+ ASSERT_TRUE(hasStandard);
+ ASSERT_TRUE(hasSearch);
+ ASSERT_TRUE(hasPhonebk);
+
+ ASSERT_FALSE(hasPhonebook); // Not valid BCP 47.
+}
+
+TEST(IntlCollator, GetBcp47KeywordValuesForLocaleCommonlyUsed)
+{
+ auto extsResult = Collator::GetBcp47KeywordValuesForLocale(
+ "fr", Collator::CommonlyUsed::Yes);
+ ASSERT_TRUE(extsResult.isOk());
+ auto extensions = extsResult.unwrap();
+
+ // Since this list is dependent on ICU, and may change between upgrades, only
+ // test a subset of the keywords.
+ auto standard = MakeStringSpan("standard");
+ auto search = MakeStringSpan("search");
+ auto phonebk = MakeStringSpan("phonebk"); // Valid BCP 47.
+ auto phonebook = MakeStringSpan("phonebook"); // Not valid BCP 47.
+ bool hasStandard = false;
+ bool hasSearch = false;
+ bool hasPhonebk = false;
+ bool hasPhonebook = false;
+
+ for (auto extensionResult : extensions) {
+ ASSERT_TRUE(extensionResult.isOk());
+ auto extension = extensionResult.unwrap();
+ hasStandard |= extension == standard;
+ hasSearch |= extension == search;
+ hasPhonebk |= extension == phonebk;
+ hasPhonebook |= extension == phonebook;
+ }
+
+ ASSERT_TRUE(hasStandard);
+ ASSERT_TRUE(hasSearch);
+
+ ASSERT_FALSE(hasPhonebk); // Not commonly used in French.
+ ASSERT_FALSE(hasPhonebook); // Not valid BCP 47.
+}
+
+TEST(IntlCollator, GetBcp47KeywordValues)
+{
+ auto extsResult = Collator::GetBcp47KeywordValues();
+ ASSERT_TRUE(extsResult.isOk());
+ auto extensions = extsResult.unwrap();
+
+ // Since this list is dependent on ICU, and may change between upgrades, only
+ // test a subset of the keywords.
+ auto standard = MakeStringSpan("standard");
+ auto search = MakeStringSpan("search");
+ auto phonebk = MakeStringSpan("phonebk"); // Valid BCP 47.
+ auto phonebook = MakeStringSpan("phonebook"); // Not valid BCP 47.
+ bool hasStandard = false;
+ bool hasSearch = false;
+ bool hasPhonebk = false;
+ bool hasPhonebook = false;
+
+ for (auto extensionResult : extensions) {
+ ASSERT_TRUE(extensionResult.isOk());
+ auto extension = extensionResult.unwrap();
+ hasStandard |= extension == standard;
+ hasSearch |= extension == search;
+ hasPhonebk |= extension == phonebk;
+ hasPhonebook |= extension == phonebook;
+ }
+
+ ASSERT_TRUE(hasStandard);
+ ASSERT_TRUE(hasSearch);
+ ASSERT_TRUE(hasPhonebk);
+
+ ASSERT_FALSE(hasPhonebook); // Not valid BCP 47.
+}
+
+TEST(IntlCollator, GetAvailableLocales)
+{
+ using namespace std::literals;
+
+ int32_t english = 0;
+ int32_t german = 0;
+ int32_t chinese = 0;
+
+ // Since this list is dependent on ICU, and may change between upgrades, only
+ // test a subset of the available locales.
+ for (const char* locale : Collator::GetAvailableLocales()) {
+ if (locale == "en"sv) {
+ english++;
+ } else if (locale == "de"sv) {
+ german++;
+ } else if (locale == "zh"sv) {
+ chinese++;
+ }
+ }
+
+ // Each locale should be found exactly once.
+ ASSERT_EQ(english, 1);
+ ASSERT_EQ(german, 1);
+ ASSERT_EQ(chinese, 1);
+}
+
+TEST(IntlCollator, GetCaseFirst)
+{
+ auto result = Collator::TryCreate("en-US");
+ ASSERT_TRUE(result.isOk());
+ auto collator = result.unwrap();
+
+ auto caseFirst = collator->GetCaseFirst();
+ ASSERT_TRUE(caseFirst.isOk());
+ ASSERT_EQ(caseFirst.unwrap(), Collator::CaseFirst::False);
+
+ for (auto kf : {Collator::CaseFirst::Upper, Collator::CaseFirst::Lower,
+ Collator::CaseFirst::False}) {
+ Collator::Options options{};
+ options.caseFirst = kf;
+
+ auto optResult = collator->SetOptions(options);
+ ASSERT_TRUE(optResult.isOk());
+
+ auto caseFirst = collator->GetCaseFirst();
+ ASSERT_TRUE(caseFirst.isOk());
+ ASSERT_EQ(caseFirst.unwrap(), kf);
+ }
+}
+
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/TestCurrency.cpp b/intl/components/gtest/TestCurrency.cpp
new file mode 100644
index 0000000000..c0afcfb326
--- /dev/null
+++ b/intl/components/gtest/TestCurrency.cpp
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "mozilla/intl/Currency.h"
+#include "mozilla/Span.h"
+
+namespace mozilla::intl {
+
+TEST(IntlCurrency, GetISOCurrencies)
+{
+ bool hasUSDollar = false;
+ bool hasEuro = false;
+ constexpr auto usdollar = MakeStringSpan("USD");
+ constexpr auto euro = MakeStringSpan("EUR");
+
+ auto currencies = Currency::GetISOCurrencies().unwrap();
+ for (auto currency : currencies) {
+ // Check a few currencies, as the list may not be stable between ICU
+ // updates.
+ if (currency.unwrap() == usdollar) {
+ hasUSDollar = true;
+ }
+ if (currency.unwrap() == euro) {
+ hasEuro = true;
+ }
+ }
+
+ ASSERT_TRUE(hasUSDollar);
+ ASSERT_TRUE(hasEuro);
+}
+
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/TestDateIntervalFormat.cpp b/intl/components/gtest/TestDateIntervalFormat.cpp
new file mode 100644
index 0000000000..cc4d4b9552
--- /dev/null
+++ b/intl/components/gtest/TestDateIntervalFormat.cpp
@@ -0,0 +1,200 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "mozilla/intl/DateIntervalFormat.h"
+#include "mozilla/intl/DateTimeFormat.h"
+#include "mozilla/intl/DateTimePart.h"
+#include "mozilla/Span.h"
+
+#include "unicode/uformattedvalue.h"
+
+#include "TestBuffer.h"
+
+namespace mozilla::intl {
+
+const double DATE201901030000GMT = 1546473600000.0;
+const double DATE201901050000GMT = 1546646400000.0;
+
+TEST(IntlDateIntervalFormat, TryFormatDateTime)
+{
+ UniquePtr<DateIntervalFormat> dif =
+ DateIntervalFormat::TryCreate(MakeStringSpan("en-US"),
+ MakeStringSpan(u"MMddHHmm"),
+ MakeStringSpan(u"GMT"))
+ .unwrap();
+
+ AutoFormattedDateInterval formatted;
+
+ // Pass two same Date time, 'equal' should be true.
+ bool equal;
+ auto result = dif->TryFormatDateTime(DATE201901030000GMT, DATE201901030000GMT,
+ formatted, &equal);
+ ASSERT_TRUE(result.isOk());
+ ASSERT_TRUE(equal);
+
+ auto spanResult = formatted.ToSpan();
+ ASSERT_TRUE(spanResult.isOk());
+
+ ASSERT_EQ(spanResult.unwrap(), MakeStringSpan(u"01/03, 00:00"));
+
+ result = dif->TryFormatDateTime(DATE201901030000GMT, DATE201901050000GMT,
+ formatted, &equal);
+ ASSERT_TRUE(result.isOk());
+ ASSERT_FALSE(equal);
+
+ spanResult = formatted.ToSpan();
+ ASSERT_TRUE(spanResult.isOk());
+ ASSERT_EQ(spanResult.unwrap(),
+ MakeStringSpan(u"01/03, 00:00 – 01/05, 00:00"));
+}
+
+TEST(IntlDateIntervalFormat, TryFormatCalendar)
+{
+ auto dateTimePatternGenerator =
+ DateTimePatternGenerator::TryCreate("en").unwrap();
+
+ DateTimeFormat::ComponentsBag components;
+ components.year = Some(DateTimeFormat::Numeric::Numeric);
+ components.month = Some(DateTimeFormat::Month::TwoDigit);
+ components.day = Some(DateTimeFormat::Numeric::TwoDigit);
+ components.hour = Some(DateTimeFormat::Numeric::TwoDigit);
+ components.minute = Some(DateTimeFormat::Numeric::TwoDigit);
+ components.hour12 = Some(false);
+
+ UniquePtr<DateTimeFormat> dtFormat =
+ DateTimeFormat::TryCreateFromComponents(
+ MakeStringSpan("en-US"), components, dateTimePatternGenerator.get(),
+ Some(MakeStringSpan(u"GMT")))
+ .unwrap();
+
+ UniquePtr<DateIntervalFormat> dif =
+ DateIntervalFormat::TryCreate(MakeStringSpan("en-US"),
+ MakeStringSpan(u"MMddHHmm"),
+ MakeStringSpan(u"GMT"))
+ .unwrap();
+
+ AutoFormattedDateInterval formatted;
+
+ // Two Calendar objects with the same date time.
+ auto sameCal = dtFormat->CloneCalendar(DATE201901030000GMT);
+ ASSERT_TRUE(sameCal.isOk());
+
+ auto cal = sameCal.unwrap();
+ bool equal;
+ auto result = dif->TryFormatCalendar(*cal, *cal, formatted, &equal);
+ ASSERT_TRUE(result.isOk());
+ ASSERT_TRUE(equal);
+
+ auto spanResult = formatted.ToSpan();
+ ASSERT_TRUE(spanResult.isOk());
+ ASSERT_EQ(spanResult.unwrap(), MakeStringSpan(u"01/03, 00:00"));
+
+ auto startCal = dtFormat->CloneCalendar(DATE201901030000GMT);
+ ASSERT_TRUE(startCal.isOk());
+ auto endCal = dtFormat->CloneCalendar(DATE201901050000GMT);
+ ASSERT_TRUE(endCal.isOk());
+
+ result = dif->TryFormatCalendar(*startCal.unwrap(), *endCal.unwrap(),
+ formatted, &equal);
+ ASSERT_TRUE(result.isOk());
+ ASSERT_FALSE(equal);
+
+ spanResult = formatted.ToSpan();
+ ASSERT_TRUE(spanResult.isOk());
+ ASSERT_EQ(spanResult.unwrap(),
+ MakeStringSpan(u"01/03, 00:00 – 01/05, 00:00"));
+}
+
+TEST(IntlDateIntervalFormat, TryFormattedToParts)
+{
+ UniquePtr<DateIntervalFormat> dif =
+ DateIntervalFormat::TryCreate(MakeStringSpan("en-US"),
+ MakeStringSpan(u"MMddHHmm"),
+ MakeStringSpan(u"GMT"))
+ .unwrap();
+
+ AutoFormattedDateInterval formatted;
+ bool equal;
+ auto result = dif->TryFormatDateTime(DATE201901030000GMT, DATE201901050000GMT,
+ formatted, &equal);
+ ASSERT_TRUE(result.isOk());
+ ASSERT_FALSE(equal);
+
+ Span<const char16_t> formattedSpan = formatted.ToSpan().unwrap();
+ ASSERT_EQ(formattedSpan, MakeStringSpan(u"01/03, 00:00 – 01/05, 00:00"));
+
+ mozilla::intl::DateTimePartVector parts;
+ result = dif->TryFormattedToParts(formatted, parts);
+ ASSERT_TRUE(result.isOk());
+
+ auto getSubSpan = [formattedSpan, &parts](size_t index) {
+ size_t start = index == 0 ? 0 : parts[index - 1].mEndIndex;
+ size_t end = parts[index].mEndIndex;
+ return formattedSpan.FromTo(start, end);
+ };
+
+ ASSERT_EQ(parts[0].mType, DateTimePartType::Month);
+ ASSERT_EQ(getSubSpan(0), MakeStringSpan(u"01"));
+ ASSERT_EQ(parts[0].mSource, DateTimePartSource::StartRange);
+
+ ASSERT_EQ(parts[1].mType, DateTimePartType::Literal);
+ ASSERT_EQ(getSubSpan(1), MakeStringSpan(u"/"));
+ ASSERT_EQ(parts[1].mSource, DateTimePartSource::StartRange);
+
+ ASSERT_EQ(parts[2].mType, DateTimePartType::Day);
+ ASSERT_EQ(getSubSpan(2), MakeStringSpan(u"03"));
+ ASSERT_EQ(parts[2].mSource, DateTimePartSource::StartRange);
+
+ ASSERT_EQ(parts[3].mType, DateTimePartType::Literal);
+ ASSERT_EQ(getSubSpan(3), MakeStringSpan(u", "));
+ ASSERT_EQ(parts[3].mSource, DateTimePartSource::StartRange);
+
+ ASSERT_EQ(parts[4].mType, DateTimePartType::Hour);
+ ASSERT_EQ(getSubSpan(4), MakeStringSpan(u"00"));
+ ASSERT_EQ(parts[4].mSource, DateTimePartSource::StartRange);
+
+ ASSERT_EQ(parts[5].mType, DateTimePartType::Literal);
+ ASSERT_EQ(getSubSpan(5), MakeStringSpan(u":"));
+ ASSERT_EQ(parts[5].mSource, DateTimePartSource::StartRange);
+
+ ASSERT_EQ(parts[6].mType, DateTimePartType::Minute);
+ ASSERT_EQ(getSubSpan(6), MakeStringSpan(u"00"));
+ ASSERT_EQ(parts[6].mSource, DateTimePartSource::StartRange);
+
+ ASSERT_EQ(parts[7].mType, DateTimePartType::Literal);
+ ASSERT_EQ(getSubSpan(7), MakeStringSpan(u" – "));
+ ASSERT_EQ(parts[7].mSource, DateTimePartSource::Shared);
+
+ ASSERT_EQ(parts[8].mType, DateTimePartType::Month);
+ ASSERT_EQ(getSubSpan(8), MakeStringSpan(u"01"));
+ ASSERT_EQ(parts[8].mSource, DateTimePartSource::EndRange);
+
+ ASSERT_EQ(parts[9].mType, DateTimePartType::Literal);
+ ASSERT_EQ(getSubSpan(9), MakeStringSpan(u"/"));
+ ASSERT_EQ(parts[9].mSource, DateTimePartSource::EndRange);
+
+ ASSERT_EQ(parts[10].mType, DateTimePartType::Day);
+ ASSERT_EQ(getSubSpan(10), MakeStringSpan(u"05"));
+ ASSERT_EQ(parts[10].mSource, DateTimePartSource::EndRange);
+
+ ASSERT_EQ(parts[11].mType, DateTimePartType::Literal);
+ ASSERT_EQ(getSubSpan(11), MakeStringSpan(u", "));
+ ASSERT_EQ(parts[11].mSource, DateTimePartSource::EndRange);
+
+ ASSERT_EQ(parts[12].mType, DateTimePartType::Hour);
+ ASSERT_EQ(getSubSpan(12), MakeStringSpan(u"00"));
+ ASSERT_EQ(parts[12].mSource, DateTimePartSource::EndRange);
+
+ ASSERT_EQ(parts[13].mType, DateTimePartType::Literal);
+ ASSERT_EQ(getSubSpan(13), MakeStringSpan(u":"));
+ ASSERT_EQ(parts[13].mSource, DateTimePartSource::EndRange);
+
+ ASSERT_EQ(parts[14].mType, DateTimePartType::Minute);
+ ASSERT_EQ(getSubSpan(14), MakeStringSpan(u"00"));
+ ASSERT_EQ(parts[14].mSource, DateTimePartSource::EndRange);
+
+ ASSERT_EQ(parts.length(), 15u);
+}
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/TestDateTimeFormat.cpp b/intl/components/gtest/TestDateTimeFormat.cpp
new file mode 100644
index 0000000000..057e26ea13
--- /dev/null
+++ b/intl/components/gtest/TestDateTimeFormat.cpp
@@ -0,0 +1,621 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "mozilla/intl/Calendar.h"
+#include "mozilla/intl/DateTimeFormat.h"
+#include "mozilla/intl/DateTimePart.h"
+#include "mozilla/intl/DateTimePatternGenerator.h"
+#include "mozilla/Span.h"
+#include "TestBuffer.h"
+
+#include <string_view>
+
+namespace mozilla::intl {
+
+// Firefox 1.0 release date.
+const double DATE = 1032800850000.0;
+
+static UniquePtr<DateTimeFormat> testStyle(
+ const char* aLocale, DateTimeFormat::StyleBag& aStyleBag) {
+ // Always specify a time zone in the tests, otherwise it will use the system
+ // time zone which can vary between test runs.
+ auto timeZone = Some(MakeStringSpan(u"GMT+3"));
+ auto gen = DateTimePatternGenerator::TryCreate("en").unwrap();
+ return DateTimeFormat::TryCreateFromStyle(MakeStringSpan(aLocale), aStyleBag,
+ gen.get(), timeZone)
+ .unwrap();
+}
+
+TEST(IntlDateTimeFormat, Style_enUS_utf8)
+{
+ DateTimeFormat::StyleBag style;
+ style.date = Some(DateTimeFormat::Style::Medium);
+ style.time = Some(DateTimeFormat::Style::Medium);
+
+ auto dtFormat = testStyle("en-US", style);
+ TestBuffer<char> buffer;
+ dtFormat->TryFormat(DATE, buffer).unwrap();
+
+ ASSERT_TRUE(buffer.verboseMatches("Sep 23, 2002, 8:07:30 PM"));
+}
+
+TEST(IntlDateTimeFormat, Style_enUS_utf16)
+{
+ DateTimeFormat::StyleBag style;
+ style.date = Some(DateTimeFormat::Style::Medium);
+ style.time = Some(DateTimeFormat::Style::Medium);
+
+ auto dtFormat = testStyle("en-US", style);
+ TestBuffer<char16_t> buffer;
+ dtFormat->TryFormat(DATE, buffer).unwrap();
+
+ ASSERT_TRUE(buffer.verboseMatches(u"Sep 23, 2002, 8:07:30 PM"));
+}
+
+TEST(IntlDateTimeFormat, Style_ar_utf8)
+{
+ DateTimeFormat::StyleBag style;
+ style.time = Some(DateTimeFormat::Style::Medium);
+
+ auto dtFormat = testStyle("ar", style);
+ TestBuffer<char> buffer;
+ dtFormat->TryFormat(DATE, buffer).unwrap();
+
+ ASSERT_TRUE(buffer.verboseMatches("٨:٠٧:٣٠ م"));
+}
+
+TEST(IntlDateTimeFormat, Style_ar_utf16)
+{
+ DateTimeFormat::StyleBag style;
+ style.time = Some(DateTimeFormat::Style::Medium);
+
+ auto dtFormat = testStyle("ar", style);
+ TestBuffer<char16_t> buffer;
+ dtFormat->TryFormat(DATE, buffer).unwrap();
+
+ ASSERT_TRUE(buffer.verboseMatches(u"٨:٠٧:٣٠ م"));
+}
+
+TEST(IntlDateTimeFormat, Style_enUS_fallback_to_default_styles)
+{
+ DateTimeFormat::StyleBag style;
+
+ auto dtFormat = testStyle("en-US", style);
+ TestBuffer<char> buffer;
+ dtFormat->TryFormat(DATE, buffer).unwrap();
+
+ ASSERT_TRUE(buffer.verboseMatches("Sep 23, 2002, 8:07:30 PM"));
+}
+
+TEST(IntlDateTimeFormat, Time_zone_IANA_identifier)
+{
+ auto gen = DateTimePatternGenerator::TryCreate("en").unwrap();
+
+ DateTimeFormat::StyleBag style;
+ style.date = Some(DateTimeFormat::Style::Medium);
+ style.time = Some(DateTimeFormat::Style::Medium);
+
+ auto dtFormat = DateTimeFormat::TryCreateFromStyle(
+ MakeStringSpan("en-US"), style, gen.get(),
+ Some(MakeStringSpan(u"America/Chicago")))
+ .unwrap();
+ TestBuffer<char> buffer;
+ dtFormat->TryFormat(DATE, buffer).unwrap();
+ ASSERT_TRUE(buffer.verboseMatches("Sep 23, 2002, 12:07:30 PM"));
+}
+
+TEST(IntlDateTimeFormat, GetAllowedHourCycles)
+{
+ auto allowed_en_US = DateTimeFormat::GetAllowedHourCycles(
+ MakeStringSpan("en"), Some(MakeStringSpan("US")))
+ .unwrap();
+
+ ASSERT_TRUE(allowed_en_US.length() == 2);
+ ASSERT_EQ(allowed_en_US[0], DateTimeFormat::HourCycle::H12);
+ ASSERT_EQ(allowed_en_US[1], DateTimeFormat::HourCycle::H23);
+
+ auto allowed_de =
+ DateTimeFormat::GetAllowedHourCycles(MakeStringSpan("de"), Nothing())
+ .unwrap();
+
+ ASSERT_TRUE(allowed_de.length() == 2);
+ ASSERT_EQ(allowed_de[0], DateTimeFormat::HourCycle::H23);
+ ASSERT_EQ(allowed_de[1], DateTimeFormat::HourCycle::H12);
+}
+
+TEST(IntlDateTimePatternGenerator, GetBestPattern)
+{
+ auto gen = DateTimePatternGenerator::TryCreate("en").unwrap();
+ TestBuffer<char16_t> buffer;
+
+ gen->GetBestPattern(MakeStringSpan(u"yMd"), buffer).unwrap();
+ ASSERT_TRUE(buffer.verboseMatches(u"M/d/y"));
+}
+
+TEST(IntlDateTimePatternGenerator, GetSkeleton)
+{
+ auto gen = DateTimePatternGenerator::TryCreate("en").unwrap();
+ TestBuffer<char16_t> buffer;
+
+ DateTimePatternGenerator::GetSkeleton(MakeStringSpan(u"M/d/y"), buffer)
+ .unwrap();
+ ASSERT_TRUE(buffer.verboseMatches(u"yMd"));
+}
+
+TEST(IntlDateTimePatternGenerator, GetPlaceholderPattern)
+{
+ auto gen = DateTimePatternGenerator::TryCreate("en").unwrap();
+ auto span = gen->GetPlaceholderPattern();
+ // The default date-time pattern for 'en' locale is u"{1}, {0}".
+ ASSERT_EQ(span, MakeStringSpan(u"{1}, {0}"));
+}
+
+// A utility function to help test the DateTimeFormat::ComponentsBag.
+[[nodiscard]] bool FormatComponents(
+ TestBuffer<char16_t>& aBuffer, DateTimeFormat::ComponentsBag& aComponents,
+ Span<const char> aLocale = MakeStringSpan("en-US")) {
+ UniquePtr<DateTimePatternGenerator> gen = nullptr;
+ auto dateTimePatternGenerator =
+ DateTimePatternGenerator::TryCreate(aLocale.data()).unwrap();
+
+ auto dtFormat = DateTimeFormat::TryCreateFromComponents(
+ aLocale, aComponents, dateTimePatternGenerator.get(),
+ Some(MakeStringSpan(u"GMT+3")));
+ if (dtFormat.isErr()) {
+ fprintf(stderr, "Could not create a DateTimeFormat\n");
+ return false;
+ }
+
+ auto result = dtFormat.unwrap()->TryFormat(DATE, aBuffer);
+ if (result.isErr()) {
+ fprintf(stderr, "Could not format a DateTimeFormat\n");
+ return false;
+ }
+
+ return true;
+}
+
+TEST(IntlDateTimeFormat, Components)
+{
+ DateTimeFormat::ComponentsBag components{};
+
+ components.year = Some(DateTimeFormat::Numeric::Numeric);
+ components.month = Some(DateTimeFormat::Month::Numeric);
+ components.day = Some(DateTimeFormat::Numeric::Numeric);
+
+ components.hour = Some(DateTimeFormat::Numeric::Numeric);
+ components.minute = Some(DateTimeFormat::Numeric::TwoDigit);
+ components.second = Some(DateTimeFormat::Numeric::TwoDigit);
+
+ TestBuffer<char16_t> buffer;
+ ASSERT_TRUE(FormatComponents(buffer, components));
+ ASSERT_TRUE(buffer.verboseMatches(u"9/23/2002, 8:07:30 PM"));
+}
+
+TEST(IntlDateTimeFormat, Components_es_ES)
+{
+ DateTimeFormat::ComponentsBag components{};
+
+ components.year = Some(DateTimeFormat::Numeric::Numeric);
+ components.month = Some(DateTimeFormat::Month::Numeric);
+ components.day = Some(DateTimeFormat::Numeric::Numeric);
+
+ components.hour = Some(DateTimeFormat::Numeric::Numeric);
+ components.minute = Some(DateTimeFormat::Numeric::TwoDigit);
+ components.second = Some(DateTimeFormat::Numeric::TwoDigit);
+
+ TestBuffer<char16_t> buffer;
+ ASSERT_TRUE(FormatComponents(buffer, components, MakeStringSpan("es-ES")));
+ ASSERT_TRUE(buffer.verboseMatches(u"23/9/2002, 20:07:30"));
+}
+
+TEST(IntlDateTimeFormat, ComponentsAll)
+{
+ // Use most all of the components.
+ DateTimeFormat::ComponentsBag components{};
+
+ components.era = Some(DateTimeFormat::Text::Short);
+
+ components.year = Some(DateTimeFormat::Numeric::Numeric);
+ components.month = Some(DateTimeFormat::Month::Numeric);
+ components.day = Some(DateTimeFormat::Numeric::Numeric);
+
+ components.weekday = Some(DateTimeFormat::Text::Short);
+
+ components.hour = Some(DateTimeFormat::Numeric::Numeric);
+ components.minute = Some(DateTimeFormat::Numeric::TwoDigit);
+ components.second = Some(DateTimeFormat::Numeric::TwoDigit);
+
+ components.timeZoneName = Some(DateTimeFormat::TimeZoneName::Short);
+ components.hourCycle = Some(DateTimeFormat::HourCycle::H24);
+ components.fractionalSecondDigits = Some(3);
+
+ TestBuffer<char16_t> buffer;
+ ASSERT_TRUE(FormatComponents(buffer, components));
+ ASSERT_TRUE(buffer.verboseMatches(u"Mon, 9 23, 2002 AD, 20:07:30.000 GMT+3"));
+}
+
+TEST(IntlDateTimeFormat, ComponentsHour12Default)
+{
+ // Assert the behavior of the default "en-US" 12 hour time with day period.
+ DateTimeFormat::ComponentsBag components{};
+ components.hour = Some(DateTimeFormat::Numeric::Numeric);
+ components.minute = Some(DateTimeFormat::Numeric::Numeric);
+
+ TestBuffer<char16_t> buffer;
+ ASSERT_TRUE(FormatComponents(buffer, components));
+ ASSERT_TRUE(buffer.verboseMatches(u"8:07 PM"));
+}
+
+TEST(IntlDateTimeFormat, ComponentsHour24)
+{
+ // Test the behavior of using 24 hour time to override the default of
+ // hour 12 with a day period.
+ DateTimeFormat::ComponentsBag components{};
+ components.hour = Some(DateTimeFormat::Numeric::Numeric);
+ components.minute = Some(DateTimeFormat::Numeric::Numeric);
+ components.hour12 = Some(false);
+
+ TestBuffer<char16_t> buffer;
+ ASSERT_TRUE(FormatComponents(buffer, components));
+ ASSERT_TRUE(buffer.verboseMatches(u"20:07"));
+}
+
+TEST(IntlDateTimeFormat, ComponentsHour12DayPeriod)
+{
+ // Test the behavior of specifying a specific day period.
+ DateTimeFormat::ComponentsBag components{};
+
+ components.hour = Some(DateTimeFormat::Numeric::Numeric);
+ components.minute = Some(DateTimeFormat::Numeric::Numeric);
+ components.dayPeriod = Some(DateTimeFormat::Text::Long);
+
+ TestBuffer<char16_t> buffer;
+ ASSERT_TRUE(FormatComponents(buffer, components));
+ ASSERT_TRUE(buffer.verboseMatches(u"8:07 in the evening"));
+}
+
+const char* ToString(uint8_t b) { return "uint8_t"; }
+const char* ToString(bool b) { return b ? "true" : "false"; }
+
+template <typename T>
+const char* ToString(Maybe<T> option) {
+ if (option) {
+ if constexpr (std::is_same_v<T, bool> || std::is_same_v<T, uint8_t>) {
+ return ToString(*option);
+ } else {
+ return DateTimeFormat::ToString(*option);
+ }
+ }
+ return "Nothing";
+}
+
+template <typename T>
+[[nodiscard]] bool VerboseEquals(T expected, T actual, const char* msg) {
+ if (expected != actual) {
+ fprintf(stderr, "%s\n Actual: %s\nExpected: %s\n", msg, ToString(actual),
+ ToString(expected));
+ return false;
+ }
+ return true;
+}
+
+// A testing utility for getting nice errors when ComponentsBags don't match.
+[[nodiscard]] bool VerboseEquals(DateTimeFormat::ComponentsBag& expected,
+ DateTimeFormat::ComponentsBag& actual) {
+ // clang-format off
+ return
+ VerboseEquals(expected.era, actual.era, "Components do not match: bag.era") &&
+ VerboseEquals(expected.year, actual.year, "Components do not match: bag.year") &&
+ VerboseEquals(expected.month, actual.month, "Components do not match: bag.month") &&
+ VerboseEquals(expected.day, actual.day, "Components do not match: bag.day") &&
+ VerboseEquals(expected.weekday, actual.weekday, "Components do not match: bag.weekday") &&
+ VerboseEquals(expected.hour, actual.hour, "Components do not match: bag.hour") &&
+ VerboseEquals(expected.minute, actual.minute, "Components do not match: bag.minute") &&
+ VerboseEquals(expected.second, actual.second, "Components do not match: bag.second") &&
+ VerboseEquals(expected.timeZoneName, actual.timeZoneName, "Components do not match: bag.timeZoneName") &&
+ VerboseEquals(expected.hour12, actual.hour12, "Components do not match: bag.hour12") &&
+ VerboseEquals(expected.hourCycle, actual.hourCycle, "Components do not match: bag.hourCycle") &&
+ VerboseEquals(expected.dayPeriod, actual.dayPeriod, "Components do not match: bag.dayPeriod") &&
+ VerboseEquals(expected.fractionalSecondDigits, actual.fractionalSecondDigits, "Components do not match: bag.fractionalSecondDigits");
+ // clang-format on
+}
+
+// A utility function to help test the DateTimeFormat::ComponentsBag.
+[[nodiscard]] bool ResolveComponentsBag(
+ DateTimeFormat::ComponentsBag& aComponentsIn,
+ DateTimeFormat::ComponentsBag* aComponentsOut,
+ Span<const char> aLocale = MakeStringSpan("en-US")) {
+ UniquePtr<DateTimePatternGenerator> gen = nullptr;
+ auto dateTimePatternGenerator =
+ DateTimePatternGenerator::TryCreate("en").unwrap();
+ auto dtFormat = DateTimeFormat::TryCreateFromComponents(
+ aLocale, aComponentsIn, dateTimePatternGenerator.get(),
+ Some(MakeStringSpan(u"GMT+3")));
+ if (dtFormat.isErr()) {
+ fprintf(stderr, "Could not create a DateTimeFormat\n");
+ return false;
+ }
+
+ auto result = dtFormat.unwrap()->ResolveComponents();
+ if (result.isErr()) {
+ fprintf(stderr, "Could not resolve the components\n");
+ return false;
+ }
+
+ *aComponentsOut = result.unwrap();
+ return true;
+}
+
+TEST(IntlDateTimeFormat, ResolvedComponentsDate)
+{
+ DateTimeFormat::ComponentsBag input{};
+ {
+ input.year = Some(DateTimeFormat::Numeric::Numeric);
+ input.month = Some(DateTimeFormat::Month::Numeric);
+ input.day = Some(DateTimeFormat::Numeric::Numeric);
+ }
+
+ DateTimeFormat::ComponentsBag expected = input;
+
+ DateTimeFormat::ComponentsBag resolved{};
+ ASSERT_TRUE(ResolveComponentsBag(input, &resolved));
+ ASSERT_TRUE(VerboseEquals(expected, resolved));
+}
+
+TEST(IntlDateTimeFormat, ResolvedComponentsAll)
+{
+ DateTimeFormat::ComponentsBag input{};
+ {
+ input.era = Some(DateTimeFormat::Text::Short);
+
+ input.year = Some(DateTimeFormat::Numeric::Numeric);
+ input.month = Some(DateTimeFormat::Month::Numeric);
+ input.day = Some(DateTimeFormat::Numeric::Numeric);
+
+ input.weekday = Some(DateTimeFormat::Text::Short);
+
+ input.hour = Some(DateTimeFormat::Numeric::Numeric);
+ input.minute = Some(DateTimeFormat::Numeric::TwoDigit);
+ input.second = Some(DateTimeFormat::Numeric::TwoDigit);
+
+ input.timeZoneName = Some(DateTimeFormat::TimeZoneName::Short);
+ input.hourCycle = Some(DateTimeFormat::HourCycle::H24);
+ input.fractionalSecondDigits = Some(3);
+ }
+
+ DateTimeFormat::ComponentsBag expected = input;
+ {
+ expected.hour = Some(DateTimeFormat::Numeric::TwoDigit);
+ expected.hourCycle = Some(DateTimeFormat::HourCycle::H24);
+ expected.hour12 = Some(false);
+ }
+
+ DateTimeFormat::ComponentsBag resolved{};
+ ASSERT_TRUE(ResolveComponentsBag(input, &resolved));
+ ASSERT_TRUE(VerboseEquals(expected, resolved));
+}
+
+TEST(IntlDateTimeFormat, ResolvedComponentsHourDayPeriod)
+{
+ DateTimeFormat::ComponentsBag input{};
+ {
+ input.hour = Some(DateTimeFormat::Numeric::Numeric);
+ input.minute = Some(DateTimeFormat::Numeric::Numeric);
+ }
+
+ DateTimeFormat::ComponentsBag expected = input;
+ {
+ expected.minute = Some(DateTimeFormat::Numeric::TwoDigit);
+ expected.hourCycle = Some(DateTimeFormat::HourCycle::H12);
+ expected.hour12 = Some(true);
+ }
+
+ DateTimeFormat::ComponentsBag resolved{};
+ ASSERT_TRUE(ResolveComponentsBag(input, &resolved));
+ ASSERT_TRUE(VerboseEquals(expected, resolved));
+}
+
+TEST(IntlDateTimeFormat, ResolvedComponentsHour12)
+{
+ DateTimeFormat::ComponentsBag input{};
+ {
+ input.hour = Some(DateTimeFormat::Numeric::Numeric);
+ input.minute = Some(DateTimeFormat::Numeric::Numeric);
+ input.hour12 = Some(false);
+ }
+
+ DateTimeFormat::ComponentsBag expected = input;
+ {
+ expected.hour = Some(DateTimeFormat::Numeric::TwoDigit);
+ expected.minute = Some(DateTimeFormat::Numeric::TwoDigit);
+ expected.hourCycle = Some(DateTimeFormat::HourCycle::H23);
+ expected.hour12 = Some(false);
+ }
+
+ DateTimeFormat::ComponentsBag resolved{};
+ ASSERT_TRUE(ResolveComponentsBag(input, &resolved));
+ ASSERT_TRUE(VerboseEquals(expected, resolved));
+}
+
+TEST(IntlDateTimeFormat, GetOriginalSkeleton)
+{
+ // Demonstrate that the original skeleton and the resolved skeleton can
+ // differ.
+ DateTimeFormat::ComponentsBag components{};
+ components.month = Some(DateTimeFormat::Month::Narrow);
+ components.day = Some(DateTimeFormat::Numeric::TwoDigit);
+
+ const char* locale = "zh-Hans-CN";
+ auto dateTimePatternGenerator =
+ DateTimePatternGenerator::TryCreate(locale).unwrap();
+
+ auto result = DateTimeFormat::TryCreateFromComponents(
+ MakeStringSpan(locale), components, dateTimePatternGenerator.get(),
+ Some(MakeStringSpan(u"GMT+3")));
+ ASSERT_TRUE(result.isOk());
+ auto dtFormat = result.unwrap();
+
+ TestBuffer<char16_t> originalSkeleton;
+ auto originalSkeletonResult = dtFormat->GetOriginalSkeleton(originalSkeleton);
+ ASSERT_TRUE(originalSkeletonResult.isOk());
+ ASSERT_TRUE(originalSkeleton.verboseMatches(u"MMMMMdd"));
+
+ TestBuffer<char16_t> pattern;
+ auto patternResult = dtFormat->GetPattern(pattern);
+ ASSERT_TRUE(patternResult.isOk());
+ ASSERT_TRUE(pattern.verboseMatches(u"M月dd日"));
+
+ TestBuffer<char16_t> resolvedSkeleton;
+ auto resolvedSkeletonResult = DateTimePatternGenerator::GetSkeleton(
+ Span(pattern.data(), pattern.length()), resolvedSkeleton);
+
+ ASSERT_TRUE(resolvedSkeletonResult.isOk());
+ ASSERT_TRUE(resolvedSkeleton.verboseMatches(u"Mdd"));
+}
+
+TEST(IntlDateTimeFormat, GetAvailableLocales)
+{
+ using namespace std::literals;
+
+ int32_t english = 0;
+ int32_t german = 0;
+ int32_t chinese = 0;
+
+ // Since this list is dependent on ICU, and may change between upgrades, only
+ // test a subset of the available locales.
+ for (const char* locale : DateTimeFormat::GetAvailableLocales()) {
+ if (locale == "en"sv) {
+ english++;
+ } else if (locale == "de"sv) {
+ german++;
+ } else if (locale == "zh"sv) {
+ chinese++;
+ }
+ }
+
+ // Each locale should be found exactly once.
+ ASSERT_EQ(english, 1);
+ ASSERT_EQ(german, 1);
+ ASSERT_EQ(chinese, 1);
+}
+
+TEST(IntlDateTimeFormat, TryFormatToParts)
+{
+ auto dateTimePatternGenerator =
+ DateTimePatternGenerator::TryCreate("en").unwrap();
+
+ DateTimeFormat::ComponentsBag components;
+ components.year = Some(DateTimeFormat::Numeric::Numeric);
+ components.month = Some(DateTimeFormat::Month::TwoDigit);
+ components.day = Some(DateTimeFormat::Numeric::TwoDigit);
+ components.hour = Some(DateTimeFormat::Numeric::TwoDigit);
+ components.minute = Some(DateTimeFormat::Numeric::TwoDigit);
+ components.hour12 = Some(false);
+
+ UniquePtr<DateTimeFormat> dtFormat =
+ DateTimeFormat::TryCreateFromComponents(
+ MakeStringSpan("en-US"), components, dateTimePatternGenerator.get(),
+ Some(MakeStringSpan(u"GMT")))
+ .unwrap();
+
+ TestBuffer<char16_t> buffer;
+ mozilla::intl::DateTimePartVector parts;
+ auto result = dtFormat->TryFormatToParts(DATE, buffer, parts);
+ ASSERT_TRUE(result.isOk());
+
+ std::u16string_view strView = buffer.get_string_view();
+ ASSERT_EQ(strView, u"09/23/2002, 17:07");
+
+ auto getSubStringView = [strView, &parts](size_t index) {
+ size_t pos = index == 0 ? 0 : parts[index - 1].mEndIndex;
+ size_t count = parts[index].mEndIndex - pos;
+ return strView.substr(pos, count);
+ };
+
+ ASSERT_EQ(parts[0].mType, DateTimePartType::Month);
+ ASSERT_EQ(getSubStringView(0), u"09");
+
+ ASSERT_EQ(parts[1].mType, DateTimePartType::Literal);
+ ASSERT_EQ(getSubStringView(1), u"/");
+
+ ASSERT_EQ(parts[2].mType, DateTimePartType::Day);
+ ASSERT_EQ(getSubStringView(2), u"23");
+
+ ASSERT_EQ(parts[3].mType, DateTimePartType::Literal);
+ ASSERT_EQ(getSubStringView(3), u"/");
+
+ ASSERT_EQ(parts[4].mType, DateTimePartType::Year);
+ ASSERT_EQ(getSubStringView(4), u"2002");
+
+ ASSERT_EQ(parts[5].mType, DateTimePartType::Literal);
+ ASSERT_EQ(getSubStringView(5), u", ");
+
+ ASSERT_EQ(parts[6].mType, DateTimePartType::Hour);
+ ASSERT_EQ(getSubStringView(6), u"17");
+
+ ASSERT_EQ(parts[7].mType, DateTimePartType::Literal);
+ ASSERT_EQ(getSubStringView(7), u":");
+
+ ASSERT_EQ(parts[8].mType, DateTimePartType::Minute);
+ ASSERT_EQ(getSubStringView(8), u"07");
+
+ ASSERT_EQ(parts.length(), 9u);
+}
+
+TEST(IntlDateTimeFormat, SetStartTimeIfGregorian)
+{
+ DateTimeFormat::StyleBag style{};
+ style.date = Some(DateTimeFormat::Style::Long);
+
+ auto timeZone = Some(MakeStringSpan(u"UTC"));
+
+ // Beginning of ECMAScript time.
+ constexpr double StartOfTime = -8.64e15;
+
+ // Gregorian change date defaults to October 15, 1582 in ICU. Test with a date
+ // before the default change date, in this case January 1, 1582.
+ constexpr double FirstJanuary1582 = -12244089600000.0;
+
+ // One year expressed in milliseconds.
+ constexpr double oneYear = (365 * 24 * 60 * 60) * 1000.0;
+
+ // Test with and without explicit calendar. The start time of the calendar can
+ // only be adjusted for the Gregorian and the ISO-8601 calendar.
+ for (const char* locale : {
+ "en-US",
+ "en-US-u-ca-gregory",
+ "en-US-u-ca-iso8601",
+ }) {
+ auto gen = DateTimePatternGenerator::TryCreate(locale).unwrap();
+
+ auto dtFormat = DateTimeFormat::TryCreateFromStyle(
+ MakeStringSpan(locale), style, gen.get(), timeZone)
+ .unwrap();
+
+ TestBuffer<char> buffer;
+
+ // Before the default Gregorian change date, so interpreted in the Julian
+ // calendar, which is December 22, 1581.
+ dtFormat->TryFormat(FirstJanuary1582, buffer).unwrap();
+ ASSERT_TRUE(buffer.verboseMatches("December 22, 1581"));
+
+ // After default Gregorian change date, so January 1, 1583.
+ dtFormat->TryFormat(FirstJanuary1582 + oneYear, buffer).unwrap();
+ ASSERT_TRUE(buffer.verboseMatches("January 1, 1583"));
+
+ // Adjust the start time to use a proleptic Gregorian calendar.
+ dtFormat->SetStartTimeIfGregorian(StartOfTime);
+
+ // Now interpreted in proleptic Gregorian calendar at January 1, 1582.
+ dtFormat->TryFormat(FirstJanuary1582, buffer).unwrap();
+ ASSERT_TRUE(buffer.verboseMatches("January 1, 1582"));
+
+ // Still January 1, 1583.
+ dtFormat->TryFormat(FirstJanuary1582 + oneYear, buffer).unwrap();
+ ASSERT_TRUE(buffer.verboseMatches("January 1, 1583"));
+ }
+}
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/TestDisplayNames.cpp b/intl/components/gtest/TestDisplayNames.cpp
new file mode 100644
index 0000000000..36980f484b
--- /dev/null
+++ b/intl/components/gtest/TestDisplayNames.cpp
@@ -0,0 +1,635 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "TestBuffer.h"
+#include "mozilla/intl/DisplayNames.h"
+
+namespace mozilla::intl {
+
+TEST(IntlDisplayNames, Script)
+{
+ TestBuffer<char16_t> buffer;
+
+ DisplayNames::Options options{};
+ options.style = DisplayNames::Style::Long;
+
+ {
+ auto result = DisplayNames::TryCreate("en-US", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ ASSERT_TRUE(displayNames->GetScript(buffer, MakeStringSpan("Hans")).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Simplified Han"));
+
+ buffer.clear();
+ {
+ // The code is too long here.
+ auto err =
+ displayNames->GetScript(buffer, MakeStringSpan("ThisIsTooLong"));
+ ASSERT_TRUE(err.isErr());
+ ASSERT_EQ(err.unwrapErr(), DisplayNamesError::InvalidOption);
+ ASSERT_TRUE(buffer.verboseMatches(u""));
+ }
+
+ // Test fallbacking for unknown scripts.
+
+ buffer.clear();
+ ASSERT_TRUE(displayNames
+ ->GetScript(buffer, MakeStringSpan("Fake"),
+ DisplayNames::Fallback::None)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u""));
+
+ buffer.clear();
+ ASSERT_TRUE(displayNames
+ ->GetScript(buffer, MakeStringSpan("Fake"),
+ DisplayNames::Fallback::Code)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Fake"));
+ }
+
+ {
+ auto result = DisplayNames::TryCreate("es-ES", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ buffer.clear();
+ ASSERT_TRUE(displayNames->GetScript(buffer, MakeStringSpan("Hans")).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"han simplificado"));
+ }
+
+ options.style = DisplayNames::Style::Short;
+ {
+ auto result = DisplayNames::TryCreate("en-US", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ buffer.clear();
+ ASSERT_TRUE(displayNames->GetScript(buffer, MakeStringSpan("Hans")).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Simplified"));
+
+ // Test fallbacking for unknown scripts.
+ buffer.clear();
+ ASSERT_TRUE(displayNames
+ ->GetScript(buffer, MakeStringSpan("Fake"),
+ DisplayNames::Fallback::None)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u""));
+
+ buffer.clear();
+ ASSERT_TRUE(displayNames
+ ->GetScript(buffer, MakeStringSpan("Fake"),
+ DisplayNames::Fallback::Code)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Fake"));
+ }
+
+ {
+ auto result = DisplayNames::TryCreate("es-ES", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ buffer.clear();
+ ASSERT_TRUE(displayNames->GetScript(buffer, MakeStringSpan("Hans")).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"simplificado"));
+ }
+}
+
+TEST(IntlDisplayNames, Language)
+{
+ TestBuffer<char16_t> buffer;
+ DisplayNames::Options options{};
+
+ {
+ auto result = DisplayNames::TryCreate("en-US", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ ASSERT_TRUE(
+ displayNames->GetLanguage(buffer, MakeStringSpan("es-ES")).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Spanish (Spain)"));
+
+ buffer.clear();
+ ASSERT_TRUE(
+ displayNames->GetLanguage(buffer, MakeStringSpan("zh-Hant")).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Chinese (Traditional)"));
+
+ // The undefined locale returns an empty string.
+ buffer.clear();
+ ASSERT_TRUE(
+ displayNames->GetLanguage(buffer, MakeStringSpan("und")).isOk());
+ ASSERT_TRUE(buffer.get_string_view().empty());
+
+ // Invalid locales are an error.
+ buffer.clear();
+ ASSERT_EQ(
+ displayNames->GetLanguage(buffer, MakeStringSpan("asdf")).unwrapErr(),
+ DisplayNamesError::InvalidOption);
+ ASSERT_TRUE(buffer.get_string_view().empty());
+
+ // Unknown locales return an empty string.
+ buffer.clear();
+ ASSERT_TRUE(displayNames
+ ->GetLanguage(buffer, MakeStringSpan("zz"),
+ DisplayNames::Fallback::None)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u""));
+
+ // Unknown locales can fallback to the language code.
+ buffer.clear();
+ ASSERT_TRUE(displayNames
+ ->GetLanguage(buffer, MakeStringSpan("zz-US"),
+ DisplayNames::Fallback::Code)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"zz-US"));
+
+ // Unknown locales with a unicode extension error. Is this correct?
+ buffer.clear();
+ ASSERT_TRUE(displayNames
+ ->GetLanguage(buffer, MakeStringSpan("zz-US-u-ca-chinese"),
+ DisplayNames::Fallback::Code)
+ .isErr());
+ ASSERT_TRUE(buffer.verboseMatches(u""));
+ }
+ {
+ auto result = DisplayNames::TryCreate("es-ES", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ buffer.clear();
+ ASSERT_TRUE(
+ displayNames->GetLanguage(buffer, MakeStringSpan("es-ES")).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"español (España)"));
+
+ buffer.clear();
+ ASSERT_TRUE(
+ displayNames->GetLanguage(buffer, MakeStringSpan("zh-Hant")).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"chino (tradicional)"));
+ }
+}
+
+TEST(IntlDisplayNames, Region)
+{
+ TestBuffer<char16_t> buffer;
+
+ DisplayNames::Options options{};
+ options.style = DisplayNames::Style::Long;
+
+ {
+ auto result = DisplayNames::TryCreate("en-US", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ ASSERT_TRUE(displayNames->GetRegion(buffer, MakeStringSpan("US")).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"United States"));
+
+ ASSERT_TRUE(displayNames->GetRegion(buffer, MakeStringSpan("ES")).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Spain"));
+
+ ASSERT_TRUE(displayNames
+ ->GetRegion(buffer, MakeStringSpan("ZX"),
+ DisplayNames::Fallback::None)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u""));
+
+ ASSERT_TRUE(displayNames
+ ->GetRegion(buffer, MakeStringSpan("ZX"),
+ DisplayNames::Fallback::Code)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"ZX"));
+ }
+ {
+ auto result = DisplayNames::TryCreate("es-ES", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ ASSERT_TRUE(displayNames->GetRegion(buffer, MakeStringSpan("US")).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Estados Unidos"));
+
+ ASSERT_TRUE(displayNames->GetRegion(buffer, MakeStringSpan("ES")).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"España"));
+ }
+}
+
+TEST(IntlDisplayNames, Currency)
+{
+ TestBuffer<char16_t> buffer;
+
+ DisplayNames::Options options{};
+ options.style = DisplayNames::Style::Long;
+
+ auto result = DisplayNames::TryCreate("en-US", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+ ASSERT_TRUE(displayNames->GetCurrency(buffer, MakeStringSpan("EUR")).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Euro"));
+
+ ASSERT_TRUE(displayNames->GetCurrency(buffer, MakeStringSpan("USD")).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"US Dollar"));
+
+ ASSERT_TRUE(displayNames
+ ->GetCurrency(buffer, MakeStringSpan("moz"),
+ DisplayNames::Fallback::None)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u""));
+
+ ASSERT_TRUE(displayNames
+ ->GetCurrency(buffer, MakeStringSpan("moz"),
+ DisplayNames::Fallback::Code)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"MOZ"));
+
+ // Invalid options.
+ {
+ // Code with fewer than 3 characters.
+ auto err = displayNames->GetCurrency(buffer, MakeStringSpan("US"));
+ ASSERT_TRUE(err.isErr());
+ ASSERT_EQ(err.unwrapErr(), DisplayNamesError::InvalidOption);
+ }
+ {
+ // Code with more than 3 characters.
+ auto err = displayNames->GetCurrency(buffer, MakeStringSpan("USDDDDDDD"));
+ ASSERT_TRUE(err.isErr());
+ ASSERT_EQ(err.unwrapErr(), DisplayNamesError::InvalidOption);
+ }
+ {
+ // Code with non-ascii alpha letters/
+ auto err = displayNames->GetCurrency(buffer, MakeStringSpan("US1"));
+ ASSERT_TRUE(err.isErr());
+ ASSERT_EQ(err.unwrapErr(), DisplayNamesError::InvalidOption);
+ }
+}
+
+TEST(IntlDisplayNames, Calendar)
+{
+ TestBuffer<char16_t> buffer;
+
+ DisplayNames::Options options{};
+ auto result = DisplayNames::TryCreate("en-US", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ ASSERT_TRUE(
+ displayNames->GetCalendar(buffer, MakeStringSpan("buddhist")).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Buddhist Calendar"));
+
+ ASSERT_TRUE(
+ displayNames->GetCalendar(buffer, MakeStringSpan("gregory")).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Gregorian Calendar"));
+
+ ASSERT_TRUE(
+ displayNames->GetCalendar(buffer, MakeStringSpan("GREGORY")).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Gregorian Calendar"));
+
+ {
+ // Code with non-ascii alpha letters.
+ auto err = displayNames->GetCalendar(buffer, MakeStringSpan("🥸 not ascii"));
+ ASSERT_TRUE(err.isErr());
+ ASSERT_EQ(err.unwrapErr(), DisplayNamesError::InvalidOption);
+ }
+ {
+ // Empty string.
+ auto err = displayNames->GetCalendar(buffer, MakeStringSpan(""));
+ ASSERT_TRUE(err.isErr());
+ ASSERT_EQ(err.unwrapErr(), DisplayNamesError::InvalidOption);
+ }
+ {
+ // Non-valid ascii.
+ auto err = displayNames->GetCalendar(
+ buffer, MakeStringSpan("ascii-but_not(valid)1234"));
+ ASSERT_TRUE(err.isErr());
+ ASSERT_EQ(err.unwrapErr(), DisplayNamesError::InvalidOption);
+ }
+
+ // Test fallbacking.
+
+ ASSERT_TRUE(displayNames
+ ->GetCalendar(buffer, MakeStringSpan("moz"),
+ DisplayNames::Fallback::None)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u""));
+
+ ASSERT_TRUE(displayNames
+ ->GetCalendar(buffer, MakeStringSpan("moz"),
+ DisplayNames::Fallback::Code)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"moz"));
+
+ ASSERT_TRUE(displayNames
+ ->GetCalendar(buffer, MakeStringSpan("MOZ"),
+ DisplayNames::Fallback::Code)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"moz"));
+}
+
+TEST(IntlDisplayNames, Weekday)
+{
+ TestBuffer<char16_t> buffer;
+
+ DisplayNames::Options options{};
+ Span<const char> calendar{};
+ auto result = DisplayNames::TryCreate("en-US", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ ASSERT_TRUE(
+ displayNames->GetWeekday(buffer, Weekday::Monday, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Monday"));
+ ASSERT_TRUE(
+ displayNames->GetWeekday(buffer, Weekday::Tuesday, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Tuesday"));
+ ASSERT_TRUE(
+ displayNames->GetWeekday(buffer, Weekday::Wednesday, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Wednesday"));
+ ASSERT_TRUE(
+ displayNames->GetWeekday(buffer, Weekday::Thursday, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Thursday"));
+ ASSERT_TRUE(
+ displayNames->GetWeekday(buffer, Weekday::Friday, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Friday"));
+ ASSERT_TRUE(
+ displayNames->GetWeekday(buffer, Weekday::Saturday, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Saturday"));
+ ASSERT_TRUE(
+ displayNames->GetWeekday(buffer, Weekday::Sunday, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Sunday"));
+}
+
+TEST(IntlDisplayNames, WeekdaySpanish)
+{
+ TestBuffer<char16_t> buffer;
+
+ DisplayNames::Options options{};
+ Span<const char> calendar{};
+ auto result = DisplayNames::TryCreate("es-ES", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ ASSERT_TRUE(
+ displayNames->GetWeekday(buffer, Weekday::Monday, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"lunes"));
+ ASSERT_TRUE(
+ displayNames->GetWeekday(buffer, Weekday::Tuesday, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"martes"));
+ ASSERT_TRUE(
+ displayNames->GetWeekday(buffer, Weekday::Wednesday, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"miércoles"));
+ ASSERT_TRUE(
+ displayNames->GetWeekday(buffer, Weekday::Thursday, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"jueves"));
+ ASSERT_TRUE(
+ displayNames->GetWeekday(buffer, Weekday::Friday, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"viernes"));
+ ASSERT_TRUE(
+ displayNames->GetWeekday(buffer, Weekday::Saturday, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"sábado"));
+ ASSERT_TRUE(
+ displayNames->GetWeekday(buffer, Weekday::Sunday, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"domingo"));
+}
+
+TEST(IntlDisplayNames, Month)
+{
+ TestBuffer<char16_t> buffer;
+
+ DisplayNames::Options options{};
+ Span<const char> calendar{};
+ auto result = DisplayNames::TryCreate("en-US", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::January, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"January"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::February, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"February"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::March, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"March"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::April, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"April"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::May, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"May"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::June, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"June"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::July, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"July"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::August, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"August"));
+ ASSERT_TRUE(
+ displayNames->GetMonth(buffer, Month::September, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"September"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::October, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"October"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::November, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"November"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::December, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"December"));
+ ASSERT_TRUE(
+ displayNames->GetMonth(buffer, Month::Undecimber, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u""));
+}
+
+TEST(IntlDisplayNames, MonthHebrew)
+{
+ TestBuffer<char16_t> buffer;
+
+ DisplayNames::Options options{};
+ Span<const char> calendar{};
+ auto result = DisplayNames::TryCreate("en-u-ca-hebrew", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::January, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Tishri"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::February, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Heshvan"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::March, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Kislev"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::April, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Tevet"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::May, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Shevat"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::June, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Adar I"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::July, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Adar"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::August, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Nisan"));
+ ASSERT_TRUE(
+ displayNames->GetMonth(buffer, Month::September, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Iyar"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::October, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Sivan"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::November, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Tamuz"));
+ ASSERT_TRUE(displayNames->GetMonth(buffer, Month::December, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Av"));
+ ASSERT_TRUE(
+ displayNames->GetMonth(buffer, Month::Undecimber, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Elul"));
+}
+
+TEST(IntlDisplayNames, MonthCalendarOption)
+{
+ TestBuffer<char16_t> buffer;
+
+ {
+ // No calendar.
+ DisplayNames::Options options{};
+ Span<const char> calendar{};
+ auto result = DisplayNames::TryCreate("en", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ ASSERT_TRUE(
+ displayNames->GetMonth(buffer, Month::January, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"January"));
+ }
+ {
+ // Switch to hebrew.
+ DisplayNames::Options options{};
+ Span<const char> calendar = MakeStringSpan("hebrew");
+ auto result = DisplayNames::TryCreate("en", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ ASSERT_TRUE(
+ displayNames->GetMonth(buffer, Month::January, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Tishri"));
+ }
+ {
+ // Conflicting tags.
+ DisplayNames::Options options{};
+ Span<const char> calendar = MakeStringSpan("hebrew");
+ auto result = DisplayNames::TryCreate("en-u-ca-gregory", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ ASSERT_TRUE(
+ displayNames->GetMonth(buffer, Month::January, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"Tishri"));
+ }
+ {
+ // Conflicting tags.
+ DisplayNames::Options options{};
+ Span<const char> calendar = MakeStringSpan("gregory");
+ auto result = DisplayNames::TryCreate("en-u-ca-hebrew", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ ASSERT_TRUE(
+ displayNames->GetMonth(buffer, Month::January, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"January"));
+ }
+}
+
+TEST(IntlDisplayNames, Quarter)
+{
+ TestBuffer<char16_t> buffer;
+
+ DisplayNames::Options options{};
+ Span<const char> calendar{};
+ auto result = DisplayNames::TryCreate("en-US", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ ASSERT_TRUE(displayNames->GetQuarter(buffer, Quarter::Q1, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"1st quarter"));
+ ASSERT_TRUE(displayNames->GetQuarter(buffer, Quarter::Q2, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"2nd quarter"));
+ ASSERT_TRUE(displayNames->GetQuarter(buffer, Quarter::Q3, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"3rd quarter"));
+ ASSERT_TRUE(displayNames->GetQuarter(buffer, Quarter::Q4, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"4th quarter"));
+}
+
+TEST(IntlDisplayNames, DayPeriod_en_US)
+{
+ TestBuffer<char16_t> buffer;
+
+ DisplayNames::Options options{};
+ Span<const char> calendar{};
+ auto result = DisplayNames::TryCreate("en-US", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ ASSERT_TRUE(
+ displayNames->GetDayPeriod(buffer, DayPeriod::AM, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"AM"));
+ ASSERT_TRUE(
+ displayNames->GetDayPeriod(buffer, DayPeriod::PM, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"PM"));
+}
+
+TEST(IntlDisplayNames, DayPeriod_ar)
+{
+ TestBuffer<char16_t> buffer;
+ DisplayNames::Options options{};
+ Span<const char> calendar{};
+ auto result = DisplayNames::TryCreate("ar", options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+
+ ASSERT_TRUE(
+ displayNames->GetDayPeriod(buffer, DayPeriod::AM, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"ص"));
+ ASSERT_TRUE(
+ displayNames->GetDayPeriod(buffer, DayPeriod::PM, calendar).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"م"));
+}
+
+TEST(IntlDisplayNames, DateTimeField)
+{
+ TestBuffer<char16_t> buffer;
+
+ DisplayNames::Options options{};
+ Span<const char> locale = MakeStringSpan("en-US");
+ auto result = DisplayNames::TryCreate(locale.data(), options);
+ ASSERT_TRUE(result.isOk());
+ auto displayNames = result.unwrap();
+ auto gen = DateTimePatternGenerator::TryCreate(locale.data()).unwrap();
+
+ ASSERT_TRUE(
+ displayNames->GetDateTimeField(buffer, DateTimeField::Year, *gen).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"year"));
+ ASSERT_TRUE(
+ displayNames->GetDateTimeField(buffer, DateTimeField::Quarter, *gen)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"quarter"));
+ ASSERT_TRUE(displayNames->GetDateTimeField(buffer, DateTimeField::Month, *gen)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"month"));
+ ASSERT_TRUE(
+ displayNames->GetDateTimeField(buffer, DateTimeField::WeekOfYear, *gen)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"week"));
+ ASSERT_TRUE(
+ displayNames->GetDateTimeField(buffer, DateTimeField::Weekday, *gen)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"day of the week"));
+ ASSERT_TRUE(
+ displayNames->GetDateTimeField(buffer, DateTimeField::Day, *gen).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"day"));
+ ASSERT_TRUE(
+ displayNames->GetDateTimeField(buffer, DateTimeField::DayPeriod, *gen)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"AM/PM"));
+ ASSERT_TRUE(
+ displayNames->GetDateTimeField(buffer, DateTimeField::Hour, *gen).isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"hour"));
+ ASSERT_TRUE(
+ displayNames->GetDateTimeField(buffer, DateTimeField::Minute, *gen)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"minute"));
+ ASSERT_TRUE(
+ displayNames->GetDateTimeField(buffer, DateTimeField::Second, *gen)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"second"));
+ ASSERT_TRUE(
+ displayNames->GetDateTimeField(buffer, DateTimeField::TimeZoneName, *gen)
+ .isOk());
+ ASSERT_TRUE(buffer.verboseMatches(u"time zone"));
+}
+
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/TestIDNA.cpp b/intl/components/gtest/TestIDNA.cpp
new file mode 100644
index 0000000000..e1c7a501bc
--- /dev/null
+++ b/intl/components/gtest/TestIDNA.cpp
@@ -0,0 +1,118 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "mozilla/intl/IDNA.h"
+#include "mozilla/Span.h"
+#include "TestBuffer.h"
+
+namespace mozilla::intl {
+
+TEST(IntlIDNA, LabelToUnicodeBasic)
+{
+ auto createResult = IDNA::TryCreate(IDNA::ProcessingType::NonTransitional);
+ ASSERT_TRUE(createResult.isOk());
+ auto idna = createResult.unwrap();
+
+ // 'A' and 'ª' are mapped to 'a'.
+ TestBuffer<char16_t> buf16;
+ auto convertResult = idna->LabelToUnicode(MakeStringSpan(u"Aa\u00aa"), buf16);
+ ASSERT_TRUE(convertResult.isOk());
+ intl::IDNA::Info info = convertResult.unwrap();
+ ASSERT_TRUE(!info.HasErrors());
+ ASSERT_EQ(buf16.get_string_view(), u"aaa");
+
+ buf16.clear();
+ // For nontransitional processing 'ß' is still mapped to 'ß'.
+ convertResult = idna->LabelToUnicode(MakeStringSpan(u"Faß"), buf16);
+ ASSERT_TRUE(convertResult.isOk());
+ info = convertResult.unwrap();
+ ASSERT_TRUE(!info.HasErrors());
+ ASSERT_EQ(buf16.get_string_view(), u"faß");
+}
+
+TEST(IntlIDNA, LabelToUnicodeBasicTransitional)
+{
+ auto createResult = IDNA::TryCreate(IDNA::ProcessingType::Transitional);
+ ASSERT_TRUE(createResult.isOk());
+ auto idna = createResult.unwrap();
+
+ TestBuffer<char16_t> buf16;
+ // For transitional processing 'ß' will be mapped to 'ss'.
+ auto convertResult = idna->LabelToUnicode(MakeStringSpan(u"Faß"), buf16);
+ ASSERT_TRUE(convertResult.isOk());
+ intl::IDNA::Info info = convertResult.unwrap();
+ ASSERT_TRUE(!info.HasErrors());
+ ASSERT_EQ(buf16.get_string_view(), u"fass");
+}
+
+TEST(IntlIDNA, LabelToUnicodeHasErrors)
+{
+ auto createResult = IDNA::TryCreate(IDNA::ProcessingType::NonTransitional);
+ ASSERT_TRUE(createResult.isOk());
+ auto idna = createResult.unwrap();
+ TestBuffer<char16_t> buf16;
+ // \u0378 is a reserved charactor, conversion should be disallowed.
+ auto convertResult = idna->LabelToUnicode(MakeStringSpan(u"\u0378"), buf16);
+ ASSERT_TRUE(convertResult.isOk());
+ intl::IDNA::Info info = convertResult.unwrap();
+ ASSERT_TRUE(info.HasErrors());
+
+ buf16.clear();
+ // FULL STOP '.' is not allowed.
+ convertResult = idna->LabelToUnicode(MakeStringSpan(u"a.b"), buf16);
+ ASSERT_TRUE(convertResult.isOk());
+ info = convertResult.unwrap();
+ ASSERT_TRUE(info.HasErrors());
+}
+
+TEST(IntlIDNA, LabelToUnicodeHasInvalidPunycode)
+{
+ auto createResult = IDNA::TryCreate(IDNA::ProcessingType::NonTransitional);
+ ASSERT_TRUE(createResult.isOk());
+ auto idna = createResult.unwrap();
+ TestBuffer<char16_t> buf16;
+ auto convertResult =
+ idna->LabelToUnicode(MakeStringSpan(u"xn--a-ecp.ru"), buf16);
+ ASSERT_TRUE(convertResult.isOk());
+ intl::IDNA::Info info = convertResult.unwrap();
+ ASSERT_TRUE(info.HasInvalidPunycode());
+
+ buf16.clear();
+ convertResult = idna->LabelToUnicode(MakeStringSpan(u"xn--0.pt"), buf16);
+ ASSERT_TRUE(convertResult.isOk());
+ info = convertResult.unwrap();
+ ASSERT_TRUE(info.HasInvalidPunycode());
+}
+
+TEST(IntlIDNA, LabelToUnicodeHasInvalidHyphen)
+{
+ auto createResult = IDNA::TryCreate(IDNA::ProcessingType::NonTransitional);
+ ASSERT_TRUE(createResult.isOk());
+ auto idna = createResult.unwrap();
+ TestBuffer<char16_t> buf16;
+
+ // Leading hyphen.
+ auto convertResult = idna->LabelToUnicode(MakeStringSpan(u"-a"), buf16);
+ ASSERT_TRUE(convertResult.isOk());
+ intl::IDNA::Info info = convertResult.unwrap();
+ ASSERT_TRUE(info.HasErrors());
+ ASSERT_TRUE(info.HasInvalidHyphen());
+
+ buf16.clear();
+ // Trailing hyphen.
+ convertResult = idna->LabelToUnicode(MakeStringSpan(u"a-"), buf16);
+ ASSERT_TRUE(convertResult.isOk());
+ info = convertResult.unwrap();
+ ASSERT_TRUE(info.HasInvalidHyphen());
+
+ buf16.clear();
+ // Contains hyphens in both 3rd and 4th positions.
+ convertResult = idna->LabelToUnicode(MakeStringSpan(u"ab--c"), buf16);
+ ASSERT_TRUE(convertResult.isOk());
+ info = convertResult.unwrap();
+ ASSERT_TRUE(info.HasInvalidHyphen());
+}
+
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/TestListFormat.cpp b/intl/components/gtest/TestListFormat.cpp
new file mode 100644
index 0000000000..85759d4985
--- /dev/null
+++ b/intl/components/gtest/TestListFormat.cpp
@@ -0,0 +1,162 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "mozilla/intl/ListFormat.h"
+#include "mozilla/Span.h"
+#include "TestBuffer.h"
+
+namespace mozilla::intl {
+
+// Test ListFormat.format with default options.
+TEST(IntlListFormat, FormatDefault)
+{
+ ListFormat::Options options;
+ UniquePtr<ListFormat> lf =
+ ListFormat::TryCreate(MakeStringSpan("en-US"), options).unwrap();
+ ListFormat::StringList list;
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Alice")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Bob")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Charlie")));
+ TestBuffer<char16_t> buf16;
+ ASSERT_TRUE(lf->Format(list, buf16).isOk());
+ ASSERT_EQ(buf16.get_string_view(), u"Alice, Bob, and Charlie");
+
+ UniquePtr<ListFormat> lfDe =
+ ListFormat::TryCreate(MakeStringSpan("de"), options).unwrap();
+ ASSERT_TRUE(lfDe->Format(list, buf16).isOk());
+ ASSERT_EQ(buf16.get_string_view(), u"Alice, Bob und Charlie");
+}
+
+// Test ListFormat.format with Type::Conjunction and other styles.
+TEST(IntlListFormat, FormatConjunction)
+{
+ ListFormat::Options options{ListFormat::Type::Conjunction,
+ ListFormat::Style::Narrow};
+ UniquePtr<ListFormat> lf =
+ ListFormat::TryCreate(MakeStringSpan("en-US"), options).unwrap();
+ ListFormat::StringList list;
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Alice")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Bob")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Charlie")));
+ TestBuffer<char16_t> buf16;
+ ASSERT_TRUE(lf->Format(list, buf16).isOk());
+ ASSERT_EQ(buf16.get_string_view(), u"Alice, Bob, Charlie");
+
+ ListFormat::Options optionsSh{ListFormat::Type::Conjunction,
+ ListFormat::Style::Short};
+ UniquePtr<ListFormat> lfSh =
+ ListFormat::TryCreate(MakeStringSpan("en-US"), optionsSh).unwrap();
+ ASSERT_TRUE(lfSh->Format(list, buf16).isOk());
+ ASSERT_EQ(buf16.get_string_view(), u"Alice, Bob, & Charlie");
+}
+
+// Test ListFormat.format with Type::Disjunction.
+TEST(IntlListFormat, FormatDisjunction)
+{
+ // When Type is Disjunction, the results will be the same regardless of the
+ // style for most locales, so simply test with Style::Long.
+ ListFormat::Options options{ListFormat::Type::Disjunction,
+ ListFormat::Style::Long};
+ UniquePtr<ListFormat> lf =
+ ListFormat::TryCreate(MakeStringSpan("en-US"), options).unwrap();
+ ListFormat::StringList list;
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Alice")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Bob")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Charlie")));
+ TestBuffer<char16_t> buf16;
+ ASSERT_TRUE(lf->Format(list, buf16).isOk());
+ ASSERT_EQ(buf16.get_string_view(), u"Alice, Bob, or Charlie");
+}
+
+// Test ListFormat.format with Type::Unit.
+TEST(IntlListFormat, FormatUnit)
+{
+ ListFormat::Options options{ListFormat::Type::Unit, ListFormat::Style::Long};
+ // For locale "en", Style::Long and Style::Short have the same result, so just
+ // test Style::Long here.
+ UniquePtr<ListFormat> lf =
+ ListFormat::TryCreate(MakeStringSpan("en-US"), options).unwrap();
+ ListFormat::StringList list;
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Alice")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Bob")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Charlie")));
+ TestBuffer<char16_t> buf16;
+ ASSERT_TRUE(lf->Format(list, buf16).isOk());
+ ASSERT_EQ(buf16.get_string_view(), u"Alice, Bob, Charlie");
+
+ ListFormat::Options optionsNa{ListFormat::Type::Unit,
+ ListFormat::Style::Narrow};
+ UniquePtr<ListFormat> lfNa =
+ ListFormat::TryCreate(MakeStringSpan("en-US"), optionsNa).unwrap();
+ ASSERT_TRUE(lfNa->Format(list, buf16).isOk());
+ ASSERT_EQ(buf16.get_string_view(), u"Alice Bob Charlie");
+}
+
+// Pass a long list (list.length() > DEFAULT_LIST_LENGTH) and check the result
+// is still correct. (result.length > INITIAL_CHAR_BUFFER_SIZE)
+TEST(IntlListFormat, FormatBufferLength)
+{
+ ListFormat::Options options;
+ UniquePtr<ListFormat> lf =
+ ListFormat::TryCreate(MakeStringSpan("en-US"), options).unwrap();
+ ListFormat::StringList list;
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Alice")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Bob")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Charlie")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"David")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Eve")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Frank")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Grace")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Heidi")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Ivan")));
+ TestBuffer<char16_t> buf16;
+ ASSERT_TRUE(lf->Format(list, buf16).isOk());
+ ASSERT_EQ(buf16.get_string_view(),
+ u"Alice, Bob, Charlie, David, Eve, Frank, Grace, Heidi, and Ivan");
+}
+
+TEST(IntlListFormat, FormatToParts)
+{
+ ListFormat::Options options;
+ UniquePtr<ListFormat> lf =
+ ListFormat::TryCreate(MakeStringSpan("en-US"), options).unwrap();
+ ListFormat::StringList list;
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Alice")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Bob")));
+ MOZ_RELEASE_ASSERT(list.append(MakeStringSpan(u"Charlie")));
+
+ TestBuffer<char16_t> buf16;
+ mozilla::intl::ListFormat::PartVector parts;
+ ASSERT_TRUE(lf->FormatToParts(list, buf16, parts).isOk());
+
+ std::u16string_view strView = buf16.get_string_view();
+ ASSERT_EQ(strView, u"Alice, Bob, and Charlie");
+
+ // 3 elements, and 2 literals.
+ ASSERT_EQ((parts.length()), (5u));
+
+ auto getSubStringView = [strView, &parts](size_t index) {
+ size_t pos = index == 0 ? 0 : parts[index - 1].second;
+ size_t count = parts[index].second - pos;
+ return strView.substr(pos, count);
+ };
+
+ ASSERT_EQ(parts[0].first, ListFormat::PartType::Element);
+ ASSERT_EQ(getSubStringView(0), u"Alice");
+
+ ASSERT_EQ(parts[1].first, ListFormat::PartType::Literal);
+ ASSERT_EQ(getSubStringView(1), u", ");
+
+ ASSERT_EQ(parts[2].first, ListFormat::PartType::Element);
+ ASSERT_EQ(getSubStringView(2), u"Bob");
+
+ ASSERT_EQ(parts[3].first, ListFormat::PartType::Literal);
+ ASSERT_EQ(getSubStringView(3), u", and ");
+
+ ASSERT_EQ(parts[4].first, ListFormat::PartType::Element);
+ ASSERT_EQ(getSubStringView(4), u"Charlie");
+}
+
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/TestLocale.cpp b/intl/components/gtest/TestLocale.cpp
new file mode 100644
index 0000000000..e4cc6a093b
--- /dev/null
+++ b/intl/components/gtest/TestLocale.cpp
@@ -0,0 +1,152 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "mozilla/intl/Locale.h"
+#include "mozilla/Span.h"
+
+#include "TestBuffer.h"
+
+namespace mozilla::intl {
+
+TEST(IntlLocale, LocaleSettersAndGetters)
+{
+ Locale locale;
+ locale.SetLanguage("fr");
+ locale.SetRegion("CA");
+ locale.SetScript("Latn");
+ ASSERT_TRUE(
+ locale.SetUnicodeExtension(MakeStringSpan("u-ca-gregory")).isOk());
+ ASSERT_TRUE(locale.Language().EqualTo("fr"));
+ ASSERT_TRUE(locale.Region().EqualTo("CA"));
+ ASSERT_TRUE(locale.Script().EqualTo("Latn"));
+ ASSERT_EQ(locale.GetUnicodeExtension().value(),
+ MakeStringSpan("u-ca-gregory"));
+
+ TestBuffer<char> buffer;
+ ASSERT_TRUE(locale.ToString(buffer).isOk());
+ ASSERT_TRUE(buffer.verboseMatches("fr-Latn-CA-u-ca-gregory"));
+
+ // No setters for variants or other extensions...
+ Locale locale2;
+ ASSERT_TRUE(LocaleParser::TryParse(
+ MakeStringSpan("fr-CA-fonipa-t-es-AR-h0-hybrid"), locale2)
+ .isOk());
+ ASSERT_EQ(locale2.Variants()[0], MakeStringSpan("fonipa"));
+ ASSERT_EQ(locale2.Extensions()[0], MakeStringSpan("t-es-AR-h0-hybrid"));
+ locale2.ClearVariants();
+ ASSERT_EQ(locale2.Variants().length(), 0UL);
+}
+
+TEST(IntlLocale, LocaleMove)
+{
+ Locale locale;
+ ASSERT_TRUE(
+ LocaleParser::TryParse(
+ MakeStringSpan(
+ "fr-Latn-CA-fonipa-u-ca-gregory-t-es-AR-h0-hybrid-x-private"),
+ locale)
+ .isOk());
+
+ ASSERT_TRUE(locale.Language().EqualTo("fr"));
+ ASSERT_TRUE(locale.Script().EqualTo("Latn"));
+ ASSERT_TRUE(locale.Region().EqualTo("CA"));
+ ASSERT_EQ(locale.Variants()[0], MakeStringSpan("fonipa"));
+ ASSERT_EQ(locale.Extensions()[0], MakeStringSpan("u-ca-gregory"));
+ ASSERT_EQ(locale.Extensions()[1], MakeStringSpan("t-es-AR-h0-hybrid"));
+ ASSERT_EQ(locale.GetUnicodeExtension().value(),
+ MakeStringSpan("u-ca-gregory"));
+ ASSERT_EQ(locale.PrivateUse().value(), MakeStringSpan("x-private"));
+
+ Locale locale2 = std::move(locale);
+
+ ASSERT_TRUE(locale2.Language().EqualTo("fr"));
+ ASSERT_TRUE(locale2.Script().EqualTo("Latn"));
+ ASSERT_TRUE(locale2.Region().EqualTo("CA"));
+ ASSERT_EQ(locale2.Variants()[0], MakeStringSpan("fonipa"));
+ ASSERT_EQ(locale2.Extensions()[0], MakeStringSpan("u-ca-gregory"));
+ ASSERT_EQ(locale2.Extensions()[1], MakeStringSpan("t-es-AR-h0-hybrid"));
+ ASSERT_EQ(locale2.GetUnicodeExtension().value(),
+ MakeStringSpan("u-ca-gregory"));
+ ASSERT_EQ(locale2.PrivateUse().value(), MakeStringSpan("x-private"));
+}
+
+TEST(IntlLocale, LocaleParser)
+{
+ const char* tags[] = {
+ "en-US", "en-GB", "es-AR", "it", "zh-Hans-CN",
+ "de-AT", "pl", "fr-FR", "de-AT", "sr-Cyrl-SR",
+ "nb-NO", "fr-FR", "mk", "uk", "und-PL",
+ "und-Latn-AM", "ug-Cyrl", "sr-ME", "mn-Mong", "lif-Limb",
+ "gan", "zh-Hant", "yue-Hans", "unr", "unr-Deva",
+ "und-Thai-CN", "ug-Cyrl", "en-Latn-DE", "pl-FR", "de-CH",
+ "tuq", "sr-ME", "ng", "klx", "kk-Arab",
+ "en-Cyrl", "und-Cyrl-UK", "und-Arab", "und-Arab-FO"};
+
+ for (const auto* tag : tags) {
+ Locale locale;
+ ASSERT_TRUE(LocaleParser::TryParse(MakeStringSpan(tag), locale).isOk());
+ }
+}
+
+TEST(IntlLocale, LikelySubtags)
+{
+ Locale locale;
+ ASSERT_TRUE(LocaleParser::TryParse(MakeStringSpan("zh"), locale).isOk());
+ ASSERT_TRUE(locale.AddLikelySubtags().isOk());
+ TestBuffer<char> buffer;
+ ASSERT_TRUE(locale.ToString(buffer).isOk());
+ ASSERT_TRUE(buffer.verboseMatches("zh-Hans-CN"));
+ ASSERT_TRUE(locale.RemoveLikelySubtags().isOk());
+ buffer.clear();
+ ASSERT_TRUE(locale.ToString(buffer).isOk());
+ ASSERT_TRUE(buffer.verboseMatches("zh"));
+}
+
+TEST(IntlLocale, Canonicalize)
+{
+ Locale locale;
+ ASSERT_TRUE(
+ LocaleParser::TryParse(MakeStringSpan("nob-bokmal"), locale).isOk());
+ ASSERT_TRUE(locale.Canonicalize().isOk());
+ TestBuffer<char> buffer;
+ ASSERT_TRUE(locale.ToString(buffer).isOk());
+ ASSERT_TRUE(buffer.verboseMatches("nb"));
+}
+
+// These tests are dependent on the machine that this test is being run on.
+TEST(IntlLocale, SystemDependentTests)
+{
+ // e.g. "en_US"
+ const char* locale = Locale::GetDefaultLocale();
+ ASSERT_TRUE(locale != nullptr);
+}
+
+TEST(IntlLocale, GetAvailableLocales)
+{
+ using namespace std::literals;
+
+ int32_t english = 0;
+ int32_t german = 0;
+ int32_t chinese = 0;
+
+ // Since this list is dependent on ICU, and may change between upgrades, only
+ // test a subset of the available locales.
+ for (const char* locale : Locale::GetAvailableLocales()) {
+ if (locale == "en"sv) {
+ english++;
+ } else if (locale == "de"sv) {
+ german++;
+ } else if (locale == "zh"sv) {
+ chinese++;
+ }
+ }
+
+ // Each locale should be found exactly once.
+ ASSERT_EQ(english, 1);
+ ASSERT_EQ(german, 1);
+ ASSERT_EQ(chinese, 1);
+}
+
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/TestLocaleCanonicalizer.cpp b/intl/components/gtest/TestLocaleCanonicalizer.cpp
new file mode 100644
index 0000000000..de1d7d1353
--- /dev/null
+++ b/intl/components/gtest/TestLocaleCanonicalizer.cpp
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "mozilla/intl/LocaleCanonicalizer.h"
+#include "mozilla/Span.h"
+
+namespace mozilla::intl {
+
+static void CheckLocaleResult(LocaleCanonicalizer::Vector& ascii,
+ const char* before, const char* after) {
+ auto result = LocaleCanonicalizer::CanonicalizeICULevel1(before, ascii);
+ ASSERT_TRUE(result.isOk());
+ ASSERT_EQ(Span(const_cast<const char*>(ascii.begin()), ascii.length()),
+ MakeStringSpan(after));
+}
+
+/**
+ * Asserts the behavior of canonicalization as defined in:
+ * http://userguide.icu-project.org/locale#TOC-Canonicalization
+ */
+TEST(IntlLocaleCanonicalizer, CanonicalizeICULevel1)
+{
+ LocaleCanonicalizer::Vector ascii{};
+
+ // Canonicalizes en-US
+ CheckLocaleResult(ascii, "en-US", "en_US");
+ // Canonicalizes POSIX
+ CheckLocaleResult(ascii, "en-US-posix", "en_US_POSIX");
+ // und gets changed to an empty string
+ CheckLocaleResult(ascii, "und", "");
+ // retains incorrect locales
+ CheckLocaleResult(ascii, "asdf", "asdf");
+ // makes text uppercase
+ CheckLocaleResult(ascii, "es-es", "es_ES");
+ // Converts 3 letter country codes to 2 letter.
+ CheckLocaleResult(ascii, "en-USA", "en_US");
+ // Does not perform level 2 canonicalization where the result would be
+ // fr_FR@currency=EUR
+ CheckLocaleResult(ascii, "fr-fr@EURO", "fr_FR_EURO");
+ // Removes the .utf8 ends
+ CheckLocaleResult(ascii, "ar-MA.utf8", "ar_MA");
+
+ // Allows valid ascii inputs
+ CheckLocaleResult(
+ ascii,
+ "abcdefghijlkmnopqrstuvwxyzABCDEFGHIJLKMNOPQRSTUVWXYZ-_.0123456789",
+ "abcdefghijlkmnopqrstuvwxyzabcdefghijlkmnopqrstuvwxyz__");
+ CheckLocaleResult(ascii, "exotic ascii:", "exotic ascii:");
+
+ // Does not accept non-ascii inputs.
+ ASSERT_EQ(LocaleCanonicalizer::CanonicalizeICULevel1("👍", ascii).unwrapErr(),
+ ICUError::InternalError);
+ ASSERT_EQ(
+ LocaleCanonicalizer::CanonicalizeICULevel1("ᏣᎳᎩ", ascii).unwrapErr(),
+ ICUError::InternalError);
+}
+
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/TestMeasureUnit.cpp b/intl/components/gtest/TestMeasureUnit.cpp
new file mode 100644
index 0000000000..377ad9dc02
--- /dev/null
+++ b/intl/components/gtest/TestMeasureUnit.cpp
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "mozilla/intl/MeasureUnit.h"
+#include "mozilla/Span.h"
+
+namespace mozilla::intl {
+
+TEST(IntlMeasureUnit, GetAvailable)
+{
+ auto units = MeasureUnit::GetAvailable();
+ ASSERT_TRUE(units.isOk());
+
+ // Test a subset of the installed measurement units.
+ auto gigabyte = MakeStringSpan("gigabyte");
+ auto liter = MakeStringSpan("liter");
+ auto meter = MakeStringSpan("meter");
+ auto meters = MakeStringSpan("meters"); // Plural "meters" is invalid.
+
+ bool hasGigabyte = false;
+ bool hasLiter = false;
+ bool hasMeter = false;
+ bool hasMeters = false;
+
+ for (auto unit : units.unwrap()) {
+ ASSERT_TRUE(unit.isOk());
+ auto span = unit.unwrap();
+
+ hasGigabyte |= span == gigabyte;
+ hasLiter |= span == liter;
+ hasMeter |= span == meter;
+ hasMeters |= span == meters;
+ }
+
+ ASSERT_TRUE(hasGigabyte);
+ ASSERT_TRUE(hasLiter);
+ ASSERT_TRUE(hasMeter);
+ ASSERT_FALSE(hasMeters);
+}
+
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/TestNumberFormat.cpp b/intl/components/gtest/TestNumberFormat.cpp
new file mode 100644
index 0000000000..e13ca7fc36
--- /dev/null
+++ b/intl/components/gtest/TestNumberFormat.cpp
@@ -0,0 +1,236 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "mozilla/intl/NumberFormat.h"
+#include "TestBuffer.h"
+
+#include <string_view>
+
+namespace mozilla {
+namespace intl {
+
+TEST(IntlNumberFormat, Basic)
+{
+ NumberFormatOptions options;
+ UniquePtr<NumberFormat> nf =
+ NumberFormat::TryCreate("en-US", options).unwrap();
+ TestBuffer<char> buf8;
+ ASSERT_TRUE(nf->format(1234.56, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "1,234.56");
+ TestBuffer<char16_t> buf16;
+ ASSERT_TRUE(nf->format(1234.56, buf16).isOk());
+ ASSERT_EQ(buf16.get_string_view(), u"1,234.56");
+ const char16_t* res16 = nf->format(1234.56).unwrap().data();
+ ASSERT_TRUE(res16 != nullptr);
+ ASSERT_EQ(std::u16string_view(res16), u"1,234.56");
+
+ UniquePtr<NumberFormat> nfAr =
+ NumberFormat::TryCreate("ar", options).unwrap();
+ ASSERT_TRUE(nfAr->format(1234.56, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "١٬٢٣٤٫٥٦");
+ ASSERT_TRUE(nfAr->format(1234.56, buf16).isOk());
+ ASSERT_EQ(buf16.get_string_view(), u"١٬٢٣٤٫٥٦");
+ res16 = nfAr->format(1234.56).unwrap().data();
+ ASSERT_TRUE(res16 != nullptr);
+ ASSERT_EQ(std::u16string_view(res16), u"١٬٢٣٤٫٥٦");
+}
+
+TEST(IntlNumberFormat, Numbers)
+{
+ NumberFormatOptions options;
+ UniquePtr<NumberFormat> nf =
+ NumberFormat::TryCreate("es-ES", options).unwrap();
+ TestBuffer<char> buf8;
+ ASSERT_TRUE(nf->format(123456.789, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "123.456,789");
+ TestBuffer<char16_t> buf16;
+ ASSERT_TRUE(nf->format(123456.789, buf16).isOk());
+ ASSERT_EQ(buf16.get_string_view(), u"123.456,789");
+
+ const char16_t* res = nf->format(123456.789).unwrap().data();
+ ASSERT_TRUE(res != nullptr);
+ ASSERT_EQ(std::u16string_view(res), u"123.456,789");
+}
+
+TEST(IntlNumberFormat, SignificantDigits)
+{
+ NumberFormatOptions options;
+ options.mSignificantDigits = Some(std::make_pair(3, 5));
+ UniquePtr<NumberFormat> nf =
+ NumberFormat::TryCreate("es-ES", options).unwrap();
+ TestBuffer<char> buf8;
+ ASSERT_TRUE(nf->format(123456.789, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "123.460");
+ ASSERT_TRUE(nf->format(0.7, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "0,700");
+}
+
+TEST(IntlNumberFormat, Currency)
+{
+ NumberFormatOptions options;
+ options.mCurrency =
+ Some(std::make_pair("MXN", NumberFormatOptions::CurrencyDisplay::Symbol));
+ UniquePtr<NumberFormat> nf =
+ NumberFormat::TryCreate("es-MX", options).unwrap();
+ TestBuffer<char> buf8;
+ ASSERT_TRUE(nf->format(123456.789, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "$123,456.79");
+ TestBuffer<char16_t> buf16;
+ ASSERT_TRUE(nf->format(123456.789, buf16).isOk());
+ ASSERT_EQ(buf16.get_string_view(), u"$123,456.79");
+ const char16_t* res = nf->format(123456.789).unwrap().data();
+ ASSERT_TRUE(res != nullptr);
+ ASSERT_EQ(std::u16string_view(res), u"$123,456.79");
+}
+
+TEST(IntlNumberFormat, Unit)
+{
+ NumberFormatOptions options;
+ options.mUnit = Some(std::make_pair("meter-per-second",
+ NumberFormatOptions::UnitDisplay::Long));
+ UniquePtr<NumberFormat> nf =
+ NumberFormat::TryCreate("es-MX", options).unwrap();
+ TestBuffer<char> buf8;
+ ASSERT_TRUE(nf->format(12.34, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "12.34 metros por segundo");
+ TestBuffer<char16_t> buf16;
+ ASSERT_TRUE(nf->format(12.34, buf16).isOk());
+ ASSERT_EQ(buf16.get_string_view(), u"12.34 metros por segundo");
+ const char16_t* res = nf->format(12.34).unwrap().data();
+ ASSERT_TRUE(res != nullptr);
+ ASSERT_EQ(std::u16string_view(res), u"12.34 metros por segundo");
+
+ // Create a string view into a longer string and make sure everything works
+ // correctly.
+ const char* unit = "meter-per-second-with-some-trailing-garbage";
+ options.mUnit = Some(std::make_pair(std::string_view(unit, 5),
+ NumberFormatOptions::UnitDisplay::Long));
+ UniquePtr<NumberFormat> nf2 =
+ NumberFormat::TryCreate("es-MX", options).unwrap();
+ res = nf2->format(12.34).unwrap().data();
+ ASSERT_TRUE(res != nullptr);
+ ASSERT_EQ(std::u16string_view(res), u"12.34 metros");
+
+ options.mUnit = Some(std::make_pair(std::string_view(unit, 16),
+ NumberFormatOptions::UnitDisplay::Long));
+ UniquePtr<NumberFormat> nf3 =
+ NumberFormat::TryCreate("es-MX", options).unwrap();
+ res = nf3->format(12.34).unwrap().data();
+ ASSERT_TRUE(res != nullptr);
+ ASSERT_EQ(std::u16string_view(res), u"12.34 metros por segundo");
+}
+
+TEST(IntlNumberFormat, RoundingMode)
+{
+ NumberFormatOptions options;
+ options.mFractionDigits = Some(std::make_pair(0, 2));
+ options.mStripTrailingZero = true;
+ options.mRoundingIncrement = 5;
+ options.mRoundingMode = NumberFormatOptions::RoundingMode::Ceil;
+
+ UniquePtr<NumberFormat> nf = NumberFormat::TryCreate("en", options).unwrap();
+
+ const char16_t* res16 = nf->format(1.92).unwrap().data();
+ ASSERT_TRUE(res16 != nullptr);
+ ASSERT_EQ(std::u16string_view(res16), u"1.95");
+
+ res16 = nf->format(1.96).unwrap().data();
+ ASSERT_TRUE(res16 != nullptr);
+ ASSERT_EQ(std::u16string_view(res16), u"2");
+}
+
+TEST(IntlNumberFormat, Grouping)
+{
+ NumberFormatOptions options;
+ options.mGrouping = NumberFormatOptions::Grouping::Min2;
+
+ UniquePtr<NumberFormat> nf = NumberFormat::TryCreate("en", options).unwrap();
+
+ const char16_t* res16 = nf->format(1'000.0).unwrap().data();
+ ASSERT_TRUE(res16 != nullptr);
+ ASSERT_EQ(std::u16string_view(res16), u"1000");
+
+ res16 = nf->format(10'000.0).unwrap().data();
+ ASSERT_TRUE(res16 != nullptr);
+ ASSERT_EQ(std::u16string_view(res16), u"10,000");
+}
+
+TEST(IntlNumberFormat, RoundingPriority)
+{
+ NumberFormatOptions options;
+ options.mFractionDigits = Some(std::make_pair(2, 2));
+ options.mSignificantDigits = Some(std::make_pair(1, 2));
+ options.mRoundingPriority =
+ NumberFormatOptions::RoundingPriority::LessPrecision;
+
+ UniquePtr<NumberFormat> nf1 = NumberFormat::TryCreate("en", options).unwrap();
+
+ const char16_t* res16 = nf1->format(4.321).unwrap().data();
+ ASSERT_TRUE(res16 != nullptr);
+ ASSERT_EQ(std::u16string_view(res16), u"4.3");
+
+ options.mRoundingPriority =
+ NumberFormatOptions::RoundingPriority::MorePrecision;
+
+ UniquePtr<NumberFormat> nf2 = NumberFormat::TryCreate("en", options).unwrap();
+
+ res16 = nf2->format(4.321).unwrap().data();
+ ASSERT_TRUE(res16 != nullptr);
+ ASSERT_EQ(std::u16string_view(res16), u"4.32");
+}
+
+TEST(IntlNumberFormat, FormatToParts)
+{
+ NumberFormatOptions options;
+ UniquePtr<NumberFormat> nf =
+ NumberFormat::TryCreate("es-ES", options).unwrap();
+ NumberPartVector parts;
+ const char16_t* res = nf->formatToParts(123456.789, parts).unwrap().data();
+ ASSERT_TRUE(res != nullptr);
+ ASSERT_EQ(std::u16string_view(res), u"123.456,789");
+ ASSERT_EQ(parts.length(), 5U);
+
+ // NumberFormat only ever produces number parts with NumberPartSource::Shared.
+
+ ASSERT_EQ(parts[0],
+ (NumberPart{NumberPartType::Integer, NumberPartSource::Shared, 3}));
+ ASSERT_EQ(parts[1],
+ (NumberPart{NumberPartType::Group, NumberPartSource::Shared, 4}));
+ ASSERT_EQ(parts[2],
+ (NumberPart{NumberPartType::Integer, NumberPartSource::Shared, 7}));
+ ASSERT_EQ(parts[3],
+ (NumberPart{NumberPartType::Decimal, NumberPartSource::Shared, 8}));
+ ASSERT_EQ(parts[4], (NumberPart{NumberPartType::Fraction,
+ NumberPartSource::Shared, 11}));
+}
+
+TEST(IntlNumberFormat, GetAvailableLocales)
+{
+ using namespace std::literals;
+
+ int32_t english = 0;
+ int32_t german = 0;
+ int32_t chinese = 0;
+
+ // Since this list is dependent on ICU, and may change between upgrades, only
+ // test a subset of the available locales.
+ for (const char* locale : NumberFormat::GetAvailableLocales()) {
+ if (locale == "en"sv) {
+ english++;
+ } else if (locale == "de"sv) {
+ german++;
+ } else if (locale == "zh"sv) {
+ chinese++;
+ }
+ }
+
+ // Each locale should be found exactly once.
+ ASSERT_EQ(english, 1);
+ ASSERT_EQ(german, 1);
+ ASSERT_EQ(chinese, 1);
+}
+
+} // namespace intl
+} // namespace mozilla
diff --git a/intl/components/gtest/TestNumberParser.cpp b/intl/components/gtest/TestNumberParser.cpp
new file mode 100644
index 0000000000..3995295913
--- /dev/null
+++ b/intl/components/gtest/TestNumberParser.cpp
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "mozilla/intl/NumberParser.h"
+
+namespace mozilla::intl {
+
+TEST(IntlNumberParser, Basic)
+{
+ // Try with an English locale
+ UniquePtr<NumberParser> np = NumberParser::TryCreate("en-US", true).unwrap();
+ auto result = np->ParseDouble(MakeStringSpan(u"1,234.56"));
+ ASSERT_TRUE(result.isOk());
+ ASSERT_EQ(result.unwrap().first, 1234.56);
+ ASSERT_EQ(result.unwrap().second, 8);
+
+ // Disable grouping, parsing will stop at the first comma
+ np = NumberParser::TryCreate("en-US", false).unwrap();
+ result = np->ParseDouble(MakeStringSpan(u"1,234.56"));
+ ASSERT_TRUE(result.isOk());
+ ASSERT_EQ(result.unwrap().first, 1);
+ ASSERT_EQ(result.unwrap().second, 1);
+
+ // Try with a Spanish locale
+ np = NumberParser::TryCreate("es-CR", true).unwrap();
+ result = np->ParseDouble(MakeStringSpan(u"1234,56"));
+ ASSERT_TRUE(result.isOk());
+ ASSERT_EQ(result.unwrap().first, 1234.56);
+ ASSERT_EQ(result.unwrap().second, 7);
+}
+
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/TestNumberRangeFormat.cpp b/intl/components/gtest/TestNumberRangeFormat.cpp
new file mode 100644
index 0000000000..f26212e3d5
--- /dev/null
+++ b/intl/components/gtest/TestNumberRangeFormat.cpp
@@ -0,0 +1,118 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include <string_view>
+
+#include "mozilla/intl/NumberRangeFormat.h"
+#include "./TestBuffer.h"
+
+namespace mozilla {
+namespace intl {
+
+using namespace std::literals;
+
+TEST(IntlNumberRangeFormat, Basic)
+{
+ NumberRangeFormatOptions options;
+ UniquePtr<NumberRangeFormat> nf =
+ NumberRangeFormat::TryCreate("en-US", options).unwrap();
+
+ const char16_t* res16 = nf->format(1234.56, 1234.56).unwrap().data();
+ ASSERT_TRUE(res16 != nullptr);
+ ASSERT_EQ(res16, u"1,234.56"sv);
+
+ options.mRangeIdentityFallback = NumberRangeFormatOptions::Approximately;
+ nf = std::move(NumberRangeFormat::TryCreate("en-US", options).unwrap());
+
+ res16 = nf->format("1234.56", "1234.56").unwrap().data();
+ ASSERT_TRUE(res16 != nullptr);
+ ASSERT_EQ(res16, u"~1,234.56"sv);
+
+ res16 = nf->format("1234.56", "2999.89").unwrap().data();
+ ASSERT_TRUE(res16 != nullptr);
+ ASSERT_EQ(res16, u"1,234.56–2,999.89"sv);
+
+ nf = std::move(NumberRangeFormat::TryCreate("ar", options).unwrap());
+
+ res16 = nfAr->format(1234.56, 1234.56).unwrap().data();
+ ASSERT_TRUE(res16 != nullptr);
+ ASSERT_EQ(res16, u"~١٬٢٣٤٫٥٦"sv);
+
+ res16 = nfAr->format(1234.56, 2999.89).unwrap().data();
+ ASSERT_TRUE(res16 != nullptr);
+ ASSERT_EQ(res16, u"١٬٢٣٤٫٥٦–٢٬٩٩٩٫٨٩"sv);
+}
+
+TEST(IntlNumberRangeFormat, Currency)
+{
+ NumberRangeFormatOptions options;
+ options.mCurrency = Some(
+ std::make_pair("MXN", NumberRangeFormatOptions::CurrencyDisplay::Symbol));
+ UniquePtr<NumberRangeFormat> nf =
+ NumberRangeFormat::TryCreate("es-MX", options).unwrap();
+
+ const char16_t* res = nf->format(123456.789, 299999.89).unwrap().data();
+ ASSERT_TRUE(res != nullptr);
+ ASSERT_EQ(std::u16string_view(res), u"$123,456.79 - $299,999.89"sv);
+
+ options.mCurrency = Some(
+ std::make_pair("EUR", NumberRangeFormatOptions::CurrencyDisplay::Symbol));
+ nf = std::move(NumberRangeFormat::TryCreate("fr", options).unwrap());
+
+ res = nf->format(123456.789, 299999.89).unwrap().data();
+ ASSERT_TRUE(res != nullptr);
+ ASSERT_EQ(std::u16string_view(res), u"123 456,79–299 999,89 €"sv);
+}
+
+TEST(IntlNumberRangeFormat, Unit)
+{
+ NumberRangeFormatOptions options;
+ options.mUnit = Some(std::make_pair(
+ "meter-per-second", NumberRangeFormatOptions::UnitDisplay::Long));
+ UniquePtr<NumberRangeFormat> nf =
+ NumberRangeFormat::TryCreate("es-MX", options).unwrap();
+
+ const char16_t* res = nf->format(12.34, 56.78).unwrap().data();
+ ASSERT_TRUE(res != nullptr);
+ ASSERT_EQ(std::u16string_view(res), u"12.34-56.78 metros por segundo");
+}
+
+TEST(IntlNumberRangeFormat, FormatToParts)
+{
+ NumberRangeFormatOptions options;
+ UniquePtr<NumberRangeFormat> nf =
+ NumberRangeFormat::TryCreate("es-ES", options).unwrap();
+ NumberPartVector parts;
+ const char16_t* res =
+ nf->formatToParts(123456.789, 299999.89, parts).unwrap().data();
+ ASSERT_TRUE(res != nullptr);
+ ASSERT_EQ(std::u16string_view(res), u"123.456,789-299.999,89"sv);
+ ASSERT_EQ(parts.length(), 11U);
+ ASSERT_EQ(parts[0],
+ (NumberPart{NumberPartType::Integer, NumberPartSource::Start, 3}));
+ ASSERT_EQ(parts[1],
+ (NumberPart{NumberPartType::Group, NumberPartSource::Start, 4}));
+ ASSERT_EQ(parts[2],
+ (NumberPart{NumberPartType::Integer, NumberPartSource::Start, 7}));
+ ASSERT_EQ(parts[3],
+ (NumberPart{NumberPartType::Decimal, NumberPartSource::Start, 8}));
+ ASSERT_EQ(parts[4], (NumberPart{NumberPartType::Fraction,
+ NumberPartSource::Start, 11}));
+ ASSERT_EQ(parts[5], (NumberPart{NumberPartType::Fraction,
+ NumberPartSource::Shared, 12}));
+ ASSERT_EQ(parts[6],
+ (NumberPart{NumberPartType::Integer, NumberPartSource::End, 15}));
+ ASSERT_EQ(parts[7],
+ (NumberPart{NumberPartType::Group, NumberPartSource::End, 16}));
+ ASSERT_EQ(parts[8],
+ (NumberPart{NumberPartType::Integer, NumberPartSource::End, 19}));
+ ASSERT_EQ(parts[9],
+ (NumberPart{NumberPartType::Decimal, NumberPartSource::End, 20}));
+ ASSERT_EQ(parts[10],
+ (NumberPart{NumberPartType::Fraction, NumberPartSource::End, 23}));
+}
+
+} // namespace intl
+} // namespace mozilla
diff --git a/intl/components/gtest/TestNumberingSystem.cpp b/intl/components/gtest/TestNumberingSystem.cpp
new file mode 100644
index 0000000000..c6f15b6559
--- /dev/null
+++ b/intl/components/gtest/TestNumberingSystem.cpp
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "mozilla/intl/NumberingSystem.h"
+#include "mozilla/Span.h"
+
+namespace mozilla::intl {
+
+TEST(IntlNumberingSystem, GetName)
+{
+ auto numbers_en = NumberingSystem::TryCreate("en").unwrap();
+ ASSERT_EQ(numbers_en->GetName().unwrap(), MakeStringSpan("latn"));
+
+ auto numbers_ar = NumberingSystem::TryCreate("ar").unwrap();
+ ASSERT_EQ(numbers_ar->GetName().unwrap(), MakeStringSpan("arab"));
+
+ auto numbers_ff_Adlm = NumberingSystem::TryCreate("ff-Adlm").unwrap();
+ ASSERT_EQ(numbers_ff_Adlm->GetName().unwrap(), MakeStringSpan("adlm"));
+}
+
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/TestPluralRules.cpp b/intl/components/gtest/TestPluralRules.cpp
new file mode 100644
index 0000000000..d02b1ff5fa
--- /dev/null
+++ b/intl/components/gtest/TestPluralRules.cpp
@@ -0,0 +1,671 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/Vector.h"
+#include "mozilla/intl/PluralRules.h"
+
+#define TEST_SELECT(actual, expected) \
+ do { \
+ ASSERT_TRUE(actual.isOk()); \
+ ASSERT_EQ(actual.unwrap(), expected); \
+ } while (false)
+
+namespace mozilla {
+namespace intl {
+
+TEST(IntlPluralRules, CategoriesEnCardinal)
+{
+ PluralRulesOptions defaultOptions;
+
+ auto prResult = PluralRules::TryCreate("en", defaultOptions);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ auto catResult = pr->Categories();
+ ASSERT_TRUE(catResult.isOk());
+ auto categories = catResult.unwrap();
+
+ ASSERT_EQ(categories.size(), 2u);
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::One));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Other));
+}
+
+TEST(IntlPluralRules, CategoriesEnOrdinal)
+{
+ PluralRulesOptions options;
+ options.mPluralType = PluralRules::Type::Ordinal;
+
+ auto prResult = PluralRules::TryCreate("en", options);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ auto catResult = pr->Categories();
+ ASSERT_TRUE(catResult.isOk());
+ auto categories = catResult.unwrap();
+
+ ASSERT_EQ(categories.size(), 4u);
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Few));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::One));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Other));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Two));
+}
+
+TEST(IntlPluralRules, CategoriesCyCardinal)
+{
+ PluralRulesOptions defaultOptions;
+
+ auto prResult = PluralRules::TryCreate("cy", defaultOptions);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ auto catResult = pr->Categories();
+ ASSERT_TRUE(catResult.isOk());
+ auto categories = catResult.unwrap();
+
+ ASSERT_EQ(categories.size(), 6u);
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Few));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::One));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Other));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Many));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Two));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Zero));
+}
+
+TEST(IntlPluralRules, CategoriesCyOrdinal)
+{
+ PluralRulesOptions options;
+ options.mPluralType = PluralRules::Type::Ordinal;
+
+ auto prResult = PluralRules::TryCreate("cy", options);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ auto catResult = pr->Categories();
+ ASSERT_TRUE(catResult.isOk());
+ auto categories = catResult.unwrap();
+
+ ASSERT_EQ(categories.size(), 6u);
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Few));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::One));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Other));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Many));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Two));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Zero));
+}
+
+TEST(IntlPluralRules, CategoriesBrCardinal)
+{
+ PluralRulesOptions defaultOptions;
+
+ auto prResult = PluralRules::TryCreate("br", defaultOptions);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ auto catResult = pr->Categories();
+ ASSERT_TRUE(catResult.isOk());
+ auto categories = catResult.unwrap();
+
+ ASSERT_EQ(categories.size(), 5u);
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Few));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::One));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Other));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Many));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Two));
+}
+
+TEST(IntlPluralRules, CategoriesBrOrdinal)
+{
+ PluralRulesOptions options;
+ options.mPluralType = PluralRules::Type::Ordinal;
+
+ auto prResult = PluralRules::TryCreate("br", options);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ auto catResult = pr->Categories();
+ ASSERT_TRUE(catResult.isOk());
+ auto categories = catResult.unwrap();
+
+ ASSERT_EQ(categories.size(), 1u);
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Other));
+}
+
+TEST(IntlPluralRules, CategoriesHsbCardinal)
+{
+ PluralRulesOptions defaultOptions;
+
+ auto prResult = PluralRules::TryCreate("hsb", defaultOptions);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ auto catResult = pr->Categories();
+ ASSERT_TRUE(catResult.isOk());
+ auto categories = catResult.unwrap();
+
+ ASSERT_EQ(categories.size(), 4u);
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Few));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::One));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Other));
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Two));
+}
+
+TEST(IntlPluralRules, CategoriesHsbOrdinal)
+{
+ PluralRulesOptions options;
+ options.mPluralType = PluralRules::Type::Ordinal;
+
+ auto prResult = PluralRules::TryCreate("hsb", options);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ auto catResult = pr->Categories();
+ ASSERT_TRUE(catResult.isOk());
+ auto categories = catResult.unwrap();
+
+ ASSERT_EQ(categories.size(), 1u);
+ ASSERT_TRUE(categories.contains(PluralRules::Keyword::Other));
+}
+
+// PluralRules should define the sort order of the keywords.
+// ICU returns these keywords in alphabetical order, so our implementation
+// should do the same.
+//
+// https://github.com/tc39/ecma402/issues/578
+TEST(IntlPluralRules, CategoriesSortOrder)
+{
+ PluralRulesOptions defaultOptions;
+
+ auto prResult = PluralRules::TryCreate("cy", defaultOptions);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ PluralRules::Keyword expected[] = {
+ PluralRules::Keyword::Few, PluralRules::Keyword::Many,
+ PluralRules::Keyword::One, PluralRules::Keyword::Other,
+ PluralRules::Keyword::Two, PluralRules::Keyword::Zero,
+ };
+
+ // Categories() returns an EnumSet so we want to ensure it still iterates
+ // over elements in the expected sorted order.
+ size_t index = 0;
+
+ auto catResult = pr->Categories();
+ ASSERT_TRUE(catResult.isOk());
+ auto categories = catResult.unwrap();
+
+ for (const PluralRules::Keyword keyword : categories) {
+ ASSERT_EQ(keyword, expected[index++]);
+ }
+}
+
+// en Cardinal Plural Rules
+// one: i = 1 and v = 0 @integer 1
+// other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …
+// @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …
+TEST(IntlPluralRules, SelectEnCardinal)
+{
+ PluralRulesOptions defaultOptions;
+
+ auto prResult = PluralRules::TryCreate("en", defaultOptions);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ TEST_SELECT(pr->Select(0.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(1.00), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(1.01), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(0.99), PluralRules::Keyword::Other);
+}
+
+// en Ordinal Plural Rules
+// one: n % 10 = 1 and n % 100 != 11
+// @integer 1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …,
+// two: n % 10 = 2 and n % 100 != 12
+// @integer 2, 22, 32, 42, 52, 62, 72, 82, 102, 1002, …,
+// few: n % 10 = 3 and n % 100 != 13
+// @integer 3, 23, 33, 43, 53, 63, 73, 83, 103, 1003, …,
+// other: @integer 0, 4~18, 100, 1000, 10000, 100000, 1000000, …
+TEST(IntlPluralRules, SelectEnOrdinal)
+{
+ PluralRulesOptions options;
+ options.mPluralType = PluralRules::Type::Ordinal;
+
+ auto prResult = PluralRules::TryCreate("en", options);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ TEST_SELECT(pr->Select(01.00), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(21.00), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(31.00), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(41.00), PluralRules::Keyword::One);
+
+ TEST_SELECT(pr->Select(02.00), PluralRules::Keyword::Two);
+ TEST_SELECT(pr->Select(22.00), PluralRules::Keyword::Two);
+ TEST_SELECT(pr->Select(32.00), PluralRules::Keyword::Two);
+ TEST_SELECT(pr->Select(42.00), PluralRules::Keyword::Two);
+
+ TEST_SELECT(pr->Select(03.00), PluralRules::Keyword::Few);
+ TEST_SELECT(pr->Select(23.00), PluralRules::Keyword::Few);
+ TEST_SELECT(pr->Select(33.00), PluralRules::Keyword::Few);
+ TEST_SELECT(pr->Select(43.00), PluralRules::Keyword::Few);
+
+ TEST_SELECT(pr->Select(00.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(11.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(12.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(13.00), PluralRules::Keyword::Other);
+}
+
+// cy Cardinal Plural Rules
+// zero: n = 0 @integer 0 @decimal 0.0, 0.00, 0.000, 0.0000,
+// one: n = 1 @integer 1 @decimal 1.0, 1.00, 1.000, 1.0000,
+// two: n = 2 @integer 2 @decimal 2.0, 2.00, 2.000, 2.0000,
+// few: n = 3 @integer 3 @decimal 3.0, 3.00, 3.000, 3.0000,
+// many: n = 6 @integer 6 @decimal 6.0, 6.00, 6.000, 6.0000,
+// other: @integer 4, 5, 7~20, 100, 1000, 10000, 100000, 1000000, …
+// @decimal 0.1~0.9, 1.1~1.7, 10.0, 100.0, 1000.0, 10000.0, 100000.0, …
+TEST(IntlPluralRules, SelectCyCardinal)
+{
+ PluralRulesOptions defaultOptions;
+
+ auto prResult = PluralRules::TryCreate("cy", defaultOptions);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ TEST_SELECT(pr->Select(0.00), PluralRules::Keyword::Zero);
+ TEST_SELECT(pr->Select(1.00), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(2.00), PluralRules::Keyword::Two);
+ TEST_SELECT(pr->Select(3.00), PluralRules::Keyword::Few);
+ TEST_SELECT(pr->Select(4.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(5.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(6.00), PluralRules::Keyword::Many);
+ TEST_SELECT(pr->Select(7.00), PluralRules::Keyword::Other);
+}
+
+// cy Ordinal Plural Rules
+// zero: n = 0,7,8,9 @integer 0, 7~9,
+// one: n = 1 @integer 1,
+// two: n = 2 @integer 2,
+// few: n = 3,4 @integer 3, 4,
+// many: n = 5,6 @integer 5, 6,
+// other: @integer 10~25, 100, 1000, 10000, 100000, 1000000, …
+TEST(IntlPluralRules, SelectCyOrdinal)
+{
+ PluralRulesOptions options;
+ options.mPluralType = PluralRules::Type::Ordinal;
+
+ auto prResult = PluralRules::TryCreate("cy", options);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ TEST_SELECT(pr->Select(0.00), PluralRules::Keyword::Zero);
+ TEST_SELECT(pr->Select(7.00), PluralRules::Keyword::Zero);
+ TEST_SELECT(pr->Select(8.00), PluralRules::Keyword::Zero);
+ TEST_SELECT(pr->Select(9.00), PluralRules::Keyword::Zero);
+
+ TEST_SELECT(pr->Select(1.00), PluralRules::Keyword::One);
+
+ TEST_SELECT(pr->Select(2.00), PluralRules::Keyword::Two);
+
+ TEST_SELECT(pr->Select(3.00), PluralRules::Keyword::Few);
+ TEST_SELECT(pr->Select(4.00), PluralRules::Keyword::Few);
+
+ TEST_SELECT(pr->Select(5.00), PluralRules::Keyword::Many);
+ TEST_SELECT(pr->Select(6.00), PluralRules::Keyword::Many);
+
+ TEST_SELECT(pr->Select(10.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(11.00), PluralRules::Keyword::Other);
+}
+
+// br Cardinal Plural Rules
+// one: n % 10 = 1 and n % 100 != 11,71,91
+// @integer 1, 21, 31, 41, 51, 61, 81, 101, 1001, …
+// @decimal 1.0, 21.0, 31.0, 41.0, 51.0, 61.0, 81.0, 101.0, 1001.0, …
+// two: n % 10 = 2 and n % 100 != 12,72,92
+// @integer 2, 22, 32, 42, 52, 62, 82, 102, 1002, …
+// @decimal 2.0, 22.0, 32.0, 42.0, 52.0, 62.0, 82.0, 102.0, 1002.0, …
+// few: n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99
+// @integer 3, 4, 9, 23, 24, 29, 33, 34, 39, 43, 44, 49, 103, 1003, …
+// @decimal 3.0, 4.0, 9.0, 23.0, 24.0, 29.0, 33.0, 34.0, 103.0, 1003.0, …
+// many: n != 0 and n % 1000000 = 0
+// @integer 1000000, …
+// @decimal 1000000.0, 1000000.00, 1000000.000, 1000000.0000, …
+// other: @integer 0, 5~8, 10~20, 100, 1000, 10000, 100000, …
+// @decimal 0.0~0.9, 1.1~1.6, 10.0, 100.0, 1000.0, 10000.0, 100000.0, …
+TEST(IntlPluralRules, SelectBrCardinal)
+{
+ PluralRulesOptions defaultOptions;
+
+ auto prResult = PluralRules::TryCreate("br", defaultOptions);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ TEST_SELECT(pr->Select(00.00), PluralRules::Keyword::Other);
+
+ TEST_SELECT(pr->Select(01.00), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(11.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(21.00), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(31.00), PluralRules::Keyword::One);
+
+ TEST_SELECT(pr->Select(02.00), PluralRules::Keyword::Two);
+ TEST_SELECT(pr->Select(12.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(22.00), PluralRules::Keyword::Two);
+ TEST_SELECT(pr->Select(32.00), PluralRules::Keyword::Two);
+
+ TEST_SELECT(pr->Select(03.00), PluralRules::Keyword::Few);
+ TEST_SELECT(pr->Select(04.00), PluralRules::Keyword::Few);
+ TEST_SELECT(pr->Select(09.00), PluralRules::Keyword::Few);
+ TEST_SELECT(pr->Select(23.00), PluralRules::Keyword::Few);
+
+ TEST_SELECT(pr->Select(1000000), PluralRules::Keyword::Many);
+
+ TEST_SELECT(pr->Select(999999), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(1000005), PluralRules::Keyword::Other);
+}
+
+// br Ordinal Plural Rules
+// br has no rules for Ordinal, so everything is PluralRules::Keyword::Other.
+TEST(IntlPluralRules, SelectBrOrdinal)
+{
+ PluralRulesOptions options;
+ options.mPluralType = PluralRules::Type::Ordinal;
+
+ auto prResult = PluralRules::TryCreate("br", options);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ TEST_SELECT(pr->Select(00.00), PluralRules::Keyword::Other);
+
+ TEST_SELECT(pr->Select(01.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(11.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(21.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(31.00), PluralRules::Keyword::Other);
+
+ TEST_SELECT(pr->Select(02.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(12.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(22.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(32.00), PluralRules::Keyword::Other);
+
+ TEST_SELECT(pr->Select(03.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(04.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(09.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(23.00), PluralRules::Keyword::Other);
+
+ TEST_SELECT(pr->Select(1000000), PluralRules::Keyword::Other);
+
+ TEST_SELECT(pr->Select(999999), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(1000005), PluralRules::Keyword::Other);
+}
+
+// hsb Cardinal Plural Rules
+// one: v = 0 and i % 100 = 1 or f % 100 = 1
+// @integer 1, 101, 201, 301, 401, 501, 601, 701, 1001, …
+// @decimal 0.1, 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 7.1, 10.1, 100.1, 1000.1,
+// …,
+// two: v = 0 and i % 100 = 2 or f % 100 = 2
+// @integer 2, 102, 202, 302, 402, 502, 602, 702, 1002, …
+// @decimal 0.2, 1.2, 2.2, 3.2, 4.2, 5.2, 6.2, 7.2, 10.2, 100.2, 1000.2,
+// …,
+// few: v = 0 and i % 100 = 3..4 or f % 100 = 3..4
+// @integer 3, 4, 103, 104, 203, 204, 303, 304, 403, 404, 503, 504, 603,
+// 604, 703, 704, 1003, …
+// @decimal 0.3,
+// 0.4, 1.3, 1.4, 2.3, 2.4, 3.3, 3.4, 4.3, 4.4, 5.3, 5.4, 6.3, 6.4, 7.3, 7.4,
+// 10.3, 100.3, 1000.3, …,
+// other: @integer 0, 5~19, 100, 1000, 10000, 100000, 1000000, …
+// @decimal 0.0, 0.5~1.0, 1.5~2.0, 2.5~2.7, 10.0, 100.0, 1000.0, 10000.0,
+// 100000.0, 1000000.0, …
+TEST(IntlPluralRules, SelectHsbCardinal)
+{
+ PluralRulesOptions defaultOptions;
+
+ auto prResult = PluralRules::TryCreate("hsb", defaultOptions);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ TEST_SELECT(pr->Select(1.00), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(101.00), PluralRules::Keyword::One);
+
+ TEST_SELECT(pr->Select(2.00), PluralRules::Keyword::Two);
+ TEST_SELECT(pr->Select(102.00), PluralRules::Keyword::Two);
+
+ TEST_SELECT(pr->Select(3.00), PluralRules::Keyword::Few);
+ TEST_SELECT(pr->Select(4.00), PluralRules::Keyword::Few);
+ TEST_SELECT(pr->Select(103.00), PluralRules::Keyword::Few);
+
+ TEST_SELECT(pr->Select(0.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(5.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(19.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(100.00), PluralRules::Keyword::Other);
+}
+
+// hsb Ordinal Plural Rules
+// other: @integer 0~15, 100, 1000, 10000, 100000, 1000000, …
+TEST(IntlPluralRules, SelectHsbOrdinal)
+{
+ PluralRulesOptions options;
+ options.mPluralType = PluralRules::Type::Ordinal;
+
+ auto prResult = PluralRules::TryCreate("hsb", options);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ TEST_SELECT(pr->Select(00.00), PluralRules::Keyword::Other);
+
+ TEST_SELECT(pr->Select(01.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(11.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(21.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(31.00), PluralRules::Keyword::Other);
+
+ TEST_SELECT(pr->Select(02.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(12.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(22.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(32.00), PluralRules::Keyword::Other);
+
+ TEST_SELECT(pr->Select(03.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(04.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(09.00), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(23.00), PluralRules::Keyword::Other);
+
+ TEST_SELECT(pr->Select(1000000), PluralRules::Keyword::Other);
+
+ TEST_SELECT(pr->Select(999999), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(1000005), PluralRules::Keyword::Other);
+}
+
+TEST(IntlPluralRules, DefaultFractionDigits)
+{
+ PluralRulesOptions defaultOptions;
+
+ auto prResult = PluralRules::TryCreate("en", defaultOptions);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ TEST_SELECT(pr->Select(1.000), PluralRules::Keyword::One);
+
+ TEST_SELECT(pr->Select(1.100), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(1.010), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(1.001), PluralRules::Keyword::Other);
+
+ TEST_SELECT(pr->Select(0.900), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(0.990), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(0.999), PluralRules::Keyword::Other);
+}
+
+TEST(IntlPluralRules, MaxFractionDigitsZero)
+{
+ PluralRulesOptions options;
+ options.mFractionDigits = Some(std::pair<uint32_t, uint32_t>(0, 0));
+
+ auto prResult = PluralRules::TryCreate("en", options);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ TEST_SELECT(pr->Select(1.000), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(1.100), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(1.010), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(1.001), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(0.900), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(0.990), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(0.999), PluralRules::Keyword::One);
+}
+
+TEST(IntlPluralRules, MaxFractionDigitsOne)
+{
+ PluralRulesOptions options;
+ options.mFractionDigits = Some(std::pair<uint32_t, uint32_t>(0, 1));
+
+ auto prResult = PluralRules::TryCreate("en", options);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ TEST_SELECT(pr->Select(1.000), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(1.010), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(1.001), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(0.990), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(0.999), PluralRules::Keyword::One);
+
+ TEST_SELECT(pr->Select(1.100), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(0.900), PluralRules::Keyword::Other);
+}
+
+TEST(IntlPluralRules, MaxSignificantDigitsOne)
+{
+ PluralRulesOptions options;
+ options.mSignificantDigits = Some(std::pair<uint32_t, uint32_t>(1, 1));
+
+ auto prResult = PluralRules::TryCreate("en", options);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ TEST_SELECT(pr->Select(1.000), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(1.100), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(1.010), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(1.001), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(0.990), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(0.999), PluralRules::Keyword::One);
+
+ TEST_SELECT(pr->Select(0.900), PluralRules::Keyword::Other);
+}
+
+TEST(IntlPluralRules, MaxFractionDigitsTwo)
+{
+ PluralRulesOptions options;
+ options.mFractionDigits = Some(std::pair<uint32_t, uint32_t>(0, 2));
+
+ auto prResult = PluralRules::TryCreate("en", options);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ TEST_SELECT(pr->Select(1.000), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(1.001), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(0.999), PluralRules::Keyword::One);
+
+ TEST_SELECT(pr->Select(1.100), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(1.010), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(0.900), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(0.990), PluralRules::Keyword::Other);
+}
+
+TEST(IntlPluralRules, MaxSignificantDigitsTwo)
+{
+ PluralRulesOptions options;
+ options.mSignificantDigits = Some(std::pair<uint32_t, uint32_t>(1, 2));
+
+ auto prResult = PluralRules::TryCreate("en", options);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ TEST_SELECT(pr->Select(1.000), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(1.010), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(1.001), PluralRules::Keyword::One);
+ TEST_SELECT(pr->Select(0.999), PluralRules::Keyword::One);
+
+ TEST_SELECT(pr->Select(1.100), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(0.900), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->Select(0.990), PluralRules::Keyword::Other);
+}
+
+// en Plural Range Rules
+// other: one other
+// other: other one
+// other: other other
+TEST(IntlPluralRules, SelectRangeEn)
+{
+ for (auto type : {PluralRules::Type::Cardinal, PluralRules::Type::Ordinal}) {
+ PluralRulesOptions options;
+ options.mPluralType = type;
+ auto prResult = PluralRules::TryCreate("en", options);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ TEST_SELECT(pr->SelectRange(0, 0), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->SelectRange(0, 1), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->SelectRange(0, 2), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->SelectRange(1, 1), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->SelectRange(1, 2), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->SelectRange(1, 10), PluralRules::Keyword::Other);
+ }
+}
+
+// fr Cardinal Plural Rules
+// one: i = 0,1
+// @integer 0, 1
+// @decimal 0.0~1.5
+// many: e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5
+// @integer 1000000, 1c6, 2c6, 3c6, 4c6, 5c6, 6c6, …
+// @decimal 1.0000001c6, 1.1c6, 2.0000001c6, 2.1c6, 3.0000001c6, 3.1c6,
+// …
+// other: @integer 2~17, 100, 1000, 10000, 100000, 1c3, 2c3, 3c3, 4c3, 5c3,
+// 6c3, …
+// @decimal 2.0~3.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0,
+// 1000000.0, 1.0001c3, 1.1c3, 2.0001c3, 2.1c3, 3.0001c3, 3.1c3, …
+//
+// fr Plural Range Rules
+// one: one one
+// other: one other
+// other: other other
+TEST(IntlPluralRules, SelectRangeFrCardinal)
+{
+ PluralRulesOptions options;
+ options.mPluralType = PluralRules::Type::Cardinal;
+ auto prResult = PluralRules::TryCreate("fr", options);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ TEST_SELECT(pr->SelectRange(0, 0), PluralRules::Keyword::One);
+ TEST_SELECT(pr->SelectRange(0, 1), PluralRules::Keyword::One);
+ TEST_SELECT(pr->SelectRange(0, 2), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->SelectRange(1, 1), PluralRules::Keyword::One);
+ TEST_SELECT(pr->SelectRange(1, 2), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->SelectRange(1, 10), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->SelectRange(1, 1000000), PluralRules::Keyword::Other);
+}
+
+// fr Ordinal Plural Rules
+// one: n = 1 @integer 1
+// other: @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, …
+//
+// fr Plural Range Rules
+// one: one one
+// other: one other
+// other: other other
+TEST(IntlPluralRules, SelectRangeFrOrdinal)
+{
+ PluralRulesOptions options;
+ options.mPluralType = PluralRules::Type::Ordinal;
+ auto prResult = PluralRules::TryCreate("fr", options);
+ ASSERT_TRUE(prResult.isOk());
+ auto pr = prResult.unwrap();
+
+ TEST_SELECT(pr->SelectRange(0, 0), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->SelectRange(0, 1), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->SelectRange(0, 2), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->SelectRange(1, 1), PluralRules::Keyword::One);
+ TEST_SELECT(pr->SelectRange(1, 2), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->SelectRange(1, 10), PluralRules::Keyword::Other);
+ TEST_SELECT(pr->SelectRange(1, 1000000), PluralRules::Keyword::Other);
+}
+
+} // namespace intl
+} // namespace mozilla
diff --git a/intl/components/gtest/TestRelativeTimeFormat.cpp b/intl/components/gtest/TestRelativeTimeFormat.cpp
new file mode 100644
index 0000000000..eced3961d7
--- /dev/null
+++ b/intl/components/gtest/TestRelativeTimeFormat.cpp
@@ -0,0 +1,161 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "mozilla/intl/RelativeTimeFormat.h"
+#include "TestBuffer.h"
+
+namespace mozilla::intl {
+
+TEST(IntlRelativeTimeFormat, Basic)
+{
+ RelativeTimeFormatOptions options = {};
+ Result<UniquePtr<RelativeTimeFormat>, ICUError> res =
+ RelativeTimeFormat::TryCreate("en-US", options);
+ ASSERT_TRUE(res.isOk());
+ UniquePtr<RelativeTimeFormat> rtf = res.unwrap();
+ TestBuffer<char> buf8;
+ ASSERT_TRUE(
+ rtf->format(1.2, RelativeTimeFormat::FormatUnit::Day, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "in 1.2 days");
+
+ TestBuffer<char16_t> buf16;
+ ASSERT_TRUE(
+ rtf->format(1.2, RelativeTimeFormat::FormatUnit::Day, buf16).isOk());
+ ASSERT_EQ(buf16.get_string_view(), u"in 1.2 days");
+
+ res = RelativeTimeFormat::TryCreate("es-AR", options);
+ ASSERT_TRUE(res.isOk());
+ rtf = res.unwrap();
+ buf8.clear();
+ ASSERT_TRUE(
+ rtf->format(1.2, RelativeTimeFormat::FormatUnit::Day, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "dentro de 1,2 días");
+
+ buf16.clear();
+ ASSERT_TRUE(
+ rtf->format(1.2, RelativeTimeFormat::FormatUnit::Day, buf16).isOk());
+ ASSERT_EQ(buf16.get_string_view(), u"dentro de 1,2 días");
+
+ res = RelativeTimeFormat::TryCreate("ar", options);
+ ASSERT_TRUE(res.isOk());
+ rtf = res.unwrap();
+ buf8.clear();
+ ASSERT_TRUE(
+ rtf->format(1.2, RelativeTimeFormat::FormatUnit::Day, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "خلال ١٫٢ يوم");
+
+ buf16.clear();
+ ASSERT_TRUE(
+ rtf->format(1.2, RelativeTimeFormat::FormatUnit::Day, buf16).isOk());
+ ASSERT_EQ(buf16.get_string_view(), u"خلال ١٫٢ يوم");
+}
+
+TEST(IntlRelativeTimeFormat, Options)
+{
+ RelativeTimeFormatOptions options = {
+ RelativeTimeFormatOptions::Style::Short,
+ RelativeTimeFormatOptions::Numeric::Auto};
+ Result<UniquePtr<RelativeTimeFormat>, ICUError> res =
+ RelativeTimeFormat::TryCreate("fr", options);
+ ASSERT_TRUE(res.isOk());
+ UniquePtr<RelativeTimeFormat> rtf = res.unwrap();
+ TestBuffer<char> buf8;
+ ASSERT_TRUE(
+ rtf->format(-3.14, RelativeTimeFormat::FormatUnit::Year, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "il y a 3,14 a");
+
+ options = {RelativeTimeFormatOptions::Style::Narrow,
+ RelativeTimeFormatOptions::Numeric::Auto};
+ res = RelativeTimeFormat::TryCreate("fr", options);
+ ASSERT_TRUE(res.isOk());
+ rtf = res.unwrap();
+ buf8.clear();
+ ASSERT_TRUE(
+ rtf->format(-3.14, RelativeTimeFormat::FormatUnit::Year, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "-3,14 a");
+
+ options = {RelativeTimeFormatOptions::Style::Long,
+ RelativeTimeFormatOptions::Numeric::Auto};
+ res = RelativeTimeFormat::TryCreate("fr", options);
+ ASSERT_TRUE(res.isOk());
+ rtf = res.unwrap();
+ buf8.clear();
+ ASSERT_TRUE(
+ rtf->format(-3.14, RelativeTimeFormat::FormatUnit::Year, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "il y a 3,14 ans");
+
+ options = {RelativeTimeFormatOptions::Style::Long,
+ RelativeTimeFormatOptions::Numeric::Auto};
+ res = RelativeTimeFormat::TryCreate("fr", options);
+ ASSERT_TRUE(res.isOk());
+ rtf = res.unwrap();
+ buf8.clear();
+ ASSERT_TRUE(
+ rtf->format(-1, RelativeTimeFormat::FormatUnit::Year, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "l’année dernière");
+}
+
+TEST(IntlRelativeTimeFormat, Units)
+{
+ RelativeTimeFormatOptions options = {};
+ Result<UniquePtr<RelativeTimeFormat>, ICUError> res =
+ RelativeTimeFormat::TryCreate("en-US", options);
+ ASSERT_TRUE(res.isOk());
+ UniquePtr<RelativeTimeFormat> rtf = res.unwrap();
+ TestBuffer<char> buf8;
+ ASSERT_TRUE(
+ rtf->format(1.2, RelativeTimeFormat::FormatUnit::Second, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "in 1.2 seconds");
+ ASSERT_TRUE(
+ rtf->format(1.2, RelativeTimeFormat::FormatUnit::Minute, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "in 1.2 minutes");
+ ASSERT_TRUE(
+ rtf->format(1.2, RelativeTimeFormat::FormatUnit::Hour, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "in 1.2 hours");
+ ASSERT_TRUE(
+ rtf->format(1.2, RelativeTimeFormat::FormatUnit::Day, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "in 1.2 days");
+ ASSERT_TRUE(
+ rtf->format(1.2, RelativeTimeFormat::FormatUnit::Week, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "in 1.2 weeks");
+ ASSERT_TRUE(
+ rtf->format(1.2, RelativeTimeFormat::FormatUnit::Month, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "in 1.2 months");
+ ASSERT_TRUE(
+ rtf->format(1.2, RelativeTimeFormat::FormatUnit::Quarter, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "in 1.2 quarters");
+ ASSERT_TRUE(
+ rtf->format(1.2, RelativeTimeFormat::FormatUnit::Year, buf8).isOk());
+ ASSERT_EQ(buf8.get_string_view(), "in 1.2 years");
+}
+
+TEST(IntlRelativeTimeFormat, FormatToParts)
+{
+ RelativeTimeFormatOptions options = {
+ RelativeTimeFormatOptions::Style::Long,
+ RelativeTimeFormatOptions::Numeric::Auto};
+ Result<UniquePtr<RelativeTimeFormat>, ICUError> res =
+ RelativeTimeFormat::TryCreate("es-AR", options);
+ ASSERT_TRUE(res.isOk());
+ UniquePtr<RelativeTimeFormat> rtf = res.unwrap();
+ NumberPartVector parts;
+ Result<Span<const char16_t>, ICUError> strRes =
+ rtf->formatToParts(-1.2, RelativeTimeFormat::FormatUnit::Year, parts);
+ ASSERT_TRUE(strRes.isOk());
+ ASSERT_EQ(strRes.unwrap(), MakeStringSpan(u"hace 1,2 años"));
+ ASSERT_EQ(parts.length(), 5U);
+ ASSERT_EQ(parts[0],
+ (NumberPart{NumberPartType::Literal, NumberPartSource::Shared, 5}));
+ ASSERT_EQ(parts[1],
+ (NumberPart{NumberPartType::Integer, NumberPartSource::Shared, 6}));
+ ASSERT_EQ(parts[2],
+ (NumberPart{NumberPartType::Decimal, NumberPartSource::Shared, 7}));
+ ASSERT_EQ(parts[3], (NumberPart{NumberPartType::Fraction,
+ NumberPartSource::Shared, 8}));
+ ASSERT_EQ(parts[4], (NumberPart{NumberPartType::Literal,
+ NumberPartSource::Shared, 13}));
+}
+
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/TestScript.cpp b/intl/components/gtest/TestScript.cpp
new file mode 100644
index 0000000000..72d8cd1087
--- /dev/null
+++ b/intl/components/gtest/TestScript.cpp
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "mozilla/intl/UnicodeProperties.h"
+#include "mozilla/intl/UnicodeScriptCodes.h"
+
+namespace mozilla::intl {
+TEST(IntlScript, GetExtensions)
+{
+ UnicodeProperties::ScriptExtensionVector extensions;
+
+ // 0x0000..0x0040 are Common.
+ for (char32_t ch = 0; ch < 0x0041; ch++) {
+ ASSERT_TRUE(UnicodeProperties::GetExtensions(ch, extensions).isOk());
+ ASSERT_EQ(extensions.length(), 1u);
+ ASSERT_EQ(Script(extensions[0]), Script::COMMON);
+ }
+
+ // 0x0300..0x0341 are Inherited.
+ for (char32_t ch = 0x300; ch < 0x0341; ch++) {
+ ASSERT_TRUE(UnicodeProperties::GetExtensions(ch, extensions).isOk());
+ ASSERT_EQ(extensions.length(), 1u);
+ ASSERT_EQ(Script(extensions[0]), Script::INHERITED);
+ }
+
+ // 0x1cf7's script code is Common, but its script extension is Beng.
+ ASSERT_TRUE(UnicodeProperties::GetExtensions(0x1cf7, extensions).isOk());
+ ASSERT_EQ(extensions.length(), 1u);
+ ASSERT_EQ(Script(extensions[0]), Script::BENGALI);
+
+ // ؿ
+ // https://unicode-table.com/en/063F/
+ // This character doesn't have any script extension, so the script code is
+ // returned.
+ ASSERT_TRUE(UnicodeProperties::GetExtensions(0x063f, extensions).isOk());
+ ASSERT_EQ(extensions.length(), 1u);
+ ASSERT_EQ(Script(extensions[0]), Script::ARABIC);
+
+ // 0xff65 is the unicode character '・', see https://unicode-table.com/en/FF65/
+ // Halfwidth Katakana Middle Dot.
+ ASSERT_TRUE(UnicodeProperties::GetExtensions(0xff65, extensions).isOk());
+
+ // 0xff65 should have the following script extensions:
+ // Bopo Hang Hani Hira Kana Yiii.
+ ASSERT_EQ(extensions.length(), 6u);
+
+ ASSERT_EQ(Script(extensions[0]), Script::BOPOMOFO);
+ ASSERT_EQ(Script(extensions[1]), Script::HAN);
+ ASSERT_EQ(Script(extensions[2]), Script::HANGUL);
+ ASSERT_EQ(Script(extensions[3]), Script::HIRAGANA);
+ ASSERT_EQ(Script(extensions[4]), Script::KATAKANA);
+ ASSERT_EQ(Script(extensions[5]), Script::YI);
+
+ // The max code point is 0x10ffff, so 0x110000 should be invalid.
+ // Script::UNKNOWN should be returned for an invalid code point.
+ ASSERT_TRUE(UnicodeProperties::GetExtensions(0x110000, extensions).isOk());
+ ASSERT_EQ(extensions.length(), 1u);
+ ASSERT_EQ(Script(extensions[0]), Script::UNKNOWN);
+}
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/TestString.cpp b/intl/components/gtest/TestString.cpp
new file mode 100644
index 0000000000..5b4d24b3b7
--- /dev/null
+++ b/intl/components/gtest/TestString.cpp
@@ -0,0 +1,260 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "mozilla/intl/String.h"
+#include "mozilla/Span.h"
+#include "mozilla/TextUtils.h"
+
+#include <algorithm>
+
+#include "TestBuffer.h"
+
+namespace mozilla::intl {
+
+static Result<std::u16string_view, ICUError> ToLocaleLowerCase(
+ const char* aLocale, const char16_t* aString,
+ TestBuffer<char16_t>& aBuffer) {
+ aBuffer.clear();
+
+ MOZ_TRY(String::ToLocaleLowerCase(aLocale, MakeStringSpan(aString), aBuffer));
+
+ return aBuffer.get_string_view();
+}
+
+static Result<std::u16string_view, ICUError> ToLocaleUpperCase(
+ const char* aLocale, const char16_t* aString,
+ TestBuffer<char16_t>& aBuffer) {
+ aBuffer.clear();
+
+ MOZ_TRY(String::ToLocaleUpperCase(aLocale, MakeStringSpan(aString), aBuffer));
+
+ return aBuffer.get_string_view();
+}
+
+TEST(IntlString, ToLocaleLowerCase)
+{
+ TestBuffer<char16_t> buf;
+
+ ASSERT_EQ(ToLocaleLowerCase("en", u"test", buf).unwrap(), u"test");
+ ASSERT_EQ(ToLocaleLowerCase("en", u"TEST", buf).unwrap(), u"test");
+
+ // Turkish dotless i.
+ ASSERT_EQ(ToLocaleLowerCase("tr", u"I", buf).unwrap(), u"ı");
+ ASSERT_EQ(ToLocaleLowerCase("tr", u"İ", buf).unwrap(), u"i");
+ ASSERT_EQ(ToLocaleLowerCase("tr", u"I\u0307", buf).unwrap(), u"i");
+}
+
+TEST(IntlString, ToLocaleUpperCase)
+{
+ TestBuffer<char16_t> buf;
+
+ ASSERT_EQ(ToLocaleUpperCase("en", u"test", buf).unwrap(), u"TEST");
+ ASSERT_EQ(ToLocaleUpperCase("en", u"TEST", buf).unwrap(), u"TEST");
+
+ // Turkish dotless i.
+ ASSERT_EQ(ToLocaleUpperCase("tr", u"i", buf).unwrap(), u"İ");
+ ASSERT_EQ(ToLocaleUpperCase("tr", u"ı", buf).unwrap(), u"I");
+
+ // Output can be longer than the input string.
+ ASSERT_EQ(ToLocaleUpperCase("en", u"Größenmaßstäbe", buf).unwrap(),
+ u"GRÖSSENMASSSTÄBE");
+}
+
+TEST(IntlString, NormalizeNFC)
+{
+ using namespace std::literals;
+
+ using NormalizationForm = String::NormalizationForm;
+ using AlreadyNormalized = String::AlreadyNormalized;
+
+ TestBuffer<char16_t> buf;
+
+ auto alreadyNormalized =
+ String::Normalize(NormalizationForm::NFC, u""sv, buf);
+ ASSERT_EQ(alreadyNormalized.unwrap(), AlreadyNormalized::Yes);
+ ASSERT_EQ(buf.get_string_view(), u"");
+
+ alreadyNormalized =
+ String::Normalize(NormalizationForm::NFC, u"abcdef"sv, buf);
+ ASSERT_EQ(alreadyNormalized.unwrap(), AlreadyNormalized::Yes);
+ ASSERT_EQ(buf.get_string_view(), u"");
+
+ alreadyNormalized =
+ String::Normalize(NormalizationForm::NFC, u"a\u0308"sv, buf);
+ ASSERT_EQ(alreadyNormalized.unwrap(), AlreadyNormalized::No);
+ ASSERT_EQ(buf.get_string_view(), u"ä");
+
+ buf.clear();
+
+ alreadyNormalized = String::Normalize(NormalizationForm::NFC, u"½"sv, buf);
+ ASSERT_EQ(alreadyNormalized.unwrap(), AlreadyNormalized::Yes);
+ ASSERT_EQ(buf.get_string_view(), u"");
+}
+
+TEST(IntlString, NormalizeNFD)
+{
+ using namespace std::literals;
+
+ using NormalizationForm = String::NormalizationForm;
+ using AlreadyNormalized = String::AlreadyNormalized;
+
+ TestBuffer<char16_t> buf;
+
+ auto alreadyNormalized =
+ String::Normalize(NormalizationForm::NFD, u""sv, buf);
+ ASSERT_EQ(alreadyNormalized.unwrap(), AlreadyNormalized::Yes);
+ ASSERT_EQ(buf.get_string_view(), u"");
+
+ alreadyNormalized =
+ String::Normalize(NormalizationForm::NFD, u"abcdef"sv, buf);
+ ASSERT_EQ(alreadyNormalized.unwrap(), AlreadyNormalized::Yes);
+ ASSERT_EQ(buf.get_string_view(), u"");
+
+ alreadyNormalized = String::Normalize(NormalizationForm::NFD, u"ä"sv, buf);
+ ASSERT_EQ(alreadyNormalized.unwrap(), AlreadyNormalized::No);
+ ASSERT_EQ(buf.get_string_view(), u"a\u0308");
+
+ buf.clear();
+
+ alreadyNormalized = String::Normalize(NormalizationForm::NFD, u"½"sv, buf);
+ ASSERT_EQ(alreadyNormalized.unwrap(), AlreadyNormalized::Yes);
+ ASSERT_EQ(buf.get_string_view(), u"");
+
+ // Test with inline capacity.
+ TestBuffer<char16_t, 2> buf2;
+
+ alreadyNormalized = String::Normalize(NormalizationForm::NFD, u" ç"sv, buf2);
+ ASSERT_EQ(alreadyNormalized.unwrap(), AlreadyNormalized::No);
+ ASSERT_EQ(buf2.get_string_view(), u" c\u0327");
+}
+
+TEST(IntlString, NormalizeNFKC)
+{
+ using namespace std::literals;
+
+ using NormalizationForm = String::NormalizationForm;
+ using AlreadyNormalized = String::AlreadyNormalized;
+
+ TestBuffer<char16_t> buf;
+
+ auto alreadyNormalized =
+ String::Normalize(NormalizationForm::NFKC, u""sv, buf);
+ ASSERT_EQ(alreadyNormalized.unwrap(), AlreadyNormalized::Yes);
+ ASSERT_EQ(buf.get_string_view(), u"");
+
+ alreadyNormalized =
+ String::Normalize(NormalizationForm::NFKC, u"abcdef"sv, buf);
+ ASSERT_EQ(alreadyNormalized.unwrap(), AlreadyNormalized::Yes);
+ ASSERT_EQ(buf.get_string_view(), u"");
+
+ alreadyNormalized =
+ String::Normalize(NormalizationForm::NFKC, u"a\u0308"sv, buf);
+ ASSERT_EQ(alreadyNormalized.unwrap(), AlreadyNormalized::No);
+ ASSERT_EQ(buf.get_string_view(), u"ä");
+
+ buf.clear();
+
+ alreadyNormalized = String::Normalize(NormalizationForm::NFKC, u"½"sv, buf);
+ ASSERT_EQ(alreadyNormalized.unwrap(), AlreadyNormalized::No);
+ ASSERT_EQ(buf.get_string_view(), u"1⁄2");
+}
+
+TEST(IntlString, NormalizeNFKD)
+{
+ using namespace std::literals;
+
+ using NormalizationForm = String::NormalizationForm;
+ using AlreadyNormalized = String::AlreadyNormalized;
+
+ TestBuffer<char16_t> buf;
+
+ auto alreadyNormalized =
+ String::Normalize(NormalizationForm::NFKD, u""sv, buf);
+ ASSERT_EQ(alreadyNormalized.unwrap(), AlreadyNormalized::Yes);
+ ASSERT_EQ(buf.get_string_view(), u"");
+
+ alreadyNormalized =
+ String::Normalize(NormalizationForm::NFKD, u"abcdef"sv, buf);
+ ASSERT_EQ(alreadyNormalized.unwrap(), AlreadyNormalized::Yes);
+ ASSERT_EQ(buf.get_string_view(), u"");
+
+ alreadyNormalized = String::Normalize(NormalizationForm::NFKD, u"ä"sv, buf);
+ ASSERT_EQ(alreadyNormalized.unwrap(), AlreadyNormalized::No);
+ ASSERT_EQ(buf.get_string_view(), u"a\u0308");
+
+ buf.clear();
+
+ alreadyNormalized = String::Normalize(NormalizationForm::NFKD, u"½"sv, buf);
+ ASSERT_EQ(alreadyNormalized.unwrap(), AlreadyNormalized::No);
+ ASSERT_EQ(buf.get_string_view(), u"1⁄2");
+}
+
+TEST(IntlString, ComposePairNFC)
+{
+ // Pair of base characters do not compose
+ ASSERT_EQ(String::ComposePairNFC(U'a', U'b'), U'\0');
+ // Base letter + accent
+ ASSERT_EQ(String::ComposePairNFC(U'a', U'\u0308'), U'ä');
+ // Accented letter + a further accent
+ ASSERT_EQ(String::ComposePairNFC(U'ä', U'\u0304'), U'ǟ');
+ // Accented letter + a further accent, but doubly-accented form is not
+ // available
+ ASSERT_EQ(String::ComposePairNFC(U'ä', U'\u0301'), U'\0');
+ // These do not compose because although U+0344 has the decomposition <0308,
+ // 0301> (see below), it also has the Full_Composition_Exclusion property.
+ ASSERT_EQ(String::ComposePairNFC(U'\u0308', U'\u0301'), U'\0');
+ // Supplementary-plane letter + accent
+ ASSERT_EQ(String::ComposePairNFC(U'\U00011099', U'\U000110BA'),
+ U'\U0001109A');
+}
+
+TEST(IntlString, DecomposeRawNFD)
+{
+ char32_t buf[2];
+ // Non-decomposable character
+ ASSERT_EQ(String::DecomposeRawNFD(U'a', buf), 0);
+ // Singleton decomposition
+ ASSERT_EQ(String::DecomposeRawNFD(U'\u212A', buf), 1);
+ ASSERT_EQ(buf[0], U'K');
+ // Simple accented letter
+ ASSERT_EQ(String::DecomposeRawNFD(U'ä', buf), 2);
+ ASSERT_EQ(buf[0], U'a');
+ ASSERT_EQ(buf[1], U'\u0308');
+ // Double-accented letter decomposes by only one level
+ ASSERT_EQ(String::DecomposeRawNFD(U'ǟ', buf), 2);
+ ASSERT_EQ(buf[0], U'ä');
+ ASSERT_EQ(buf[1], U'\u0304');
+ // Non-starter can decompose, but will not recompose (see above)
+ ASSERT_EQ(String::DecomposeRawNFD(U'\u0344', buf), 2);
+ ASSERT_EQ(buf[0], U'\u0308');
+ ASSERT_EQ(buf[1], U'\u0301');
+ // Supplementary-plane letter with decomposition
+ ASSERT_EQ(String::DecomposeRawNFD(U'\U0001109A', buf), 2);
+ ASSERT_EQ(buf[0], U'\U00011099');
+ ASSERT_EQ(buf[1], U'\U000110BA');
+}
+
+TEST(IntlString, IsCased)
+{
+ ASSERT_TRUE(String::IsCased(U'a'));
+ ASSERT_FALSE(String::IsCased(U'0'));
+}
+
+TEST(IntlString, IsCaseIgnorable)
+{
+ ASSERT_FALSE(String::IsCaseIgnorable(U'a'));
+ ASSERT_TRUE(String::IsCaseIgnorable(U'.'));
+}
+
+TEST(IntlString, GetUnicodeVersion)
+{
+ auto version = String::GetUnicodeVersion();
+
+ ASSERT_TRUE(std::all_of(version.begin(), version.end(), [](char ch) {
+ return IsAsciiDigit(ch) || ch == '.';
+ }));
+}
+
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/TestTimeZone.cpp b/intl/components/gtest/TestTimeZone.cpp
new file mode 100644
index 0000000000..dc274150c7
--- /dev/null
+++ b/intl/components/gtest/TestTimeZone.cpp
@@ -0,0 +1,249 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+
+#include "mozilla/intl/TimeZone.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Span.h"
+#include "mozilla/TextUtils.h"
+#include "TestBuffer.h"
+
+#include <algorithm>
+#include <string>
+
+namespace mozilla::intl {
+
+// Firefox 1.0 release date.
+static constexpr int64_t RELEASE_DATE = 1'032'800'850'000;
+
+// Date.UTC(2021, 11-1, 7, 2, 0, 0)
+static constexpr int64_t DST_CHANGE_DATE = 1'636'250'400'000;
+
+// These tests are dependent on the machine that this test is being run on.
+// Unwrap the results to ensure it doesn't fail, but don't check the values.
+TEST(IntlTimeZone, SystemDependentTests)
+{
+ // e.g. "America/Chicago"
+ TestBuffer<char16_t> buffer;
+ TimeZone::GetDefaultTimeZone(buffer).unwrap();
+}
+
+TEST(IntlTimeZone, GetRawOffsetMs)
+{
+ auto timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"GMT+3"))).unwrap();
+ ASSERT_EQ(timeZone->GetRawOffsetMs().unwrap(), 3 * 60 * 60 * 1000);
+
+ timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"Etc/GMT+3"))).unwrap();
+ ASSERT_EQ(timeZone->GetRawOffsetMs().unwrap(), -(3 * 60 * 60 * 1000));
+
+ timeZone =
+ TimeZone::TryCreate(Some(MakeStringSpan(u"America/New_York"))).unwrap();
+ ASSERT_EQ(timeZone->GetRawOffsetMs().unwrap(), -(5 * 60 * 60 * 1000));
+}
+
+TEST(IntlTimeZone, GetDSTOffsetMs)
+{
+ auto timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"GMT+3"))).unwrap();
+ ASSERT_EQ(timeZone->GetDSTOffsetMs(0).unwrap(), 0);
+
+ timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"Etc/GMT+3"))).unwrap();
+ ASSERT_EQ(timeZone->GetDSTOffsetMs(0).unwrap(), 0);
+
+ timeZone =
+ TimeZone::TryCreate(Some(MakeStringSpan(u"America/New_York"))).unwrap();
+ ASSERT_EQ(timeZone->GetDSTOffsetMs(0).unwrap(), 0);
+ ASSERT_EQ(timeZone->GetDSTOffsetMs(RELEASE_DATE).unwrap(),
+ 1 * 60 * 60 * 1000);
+ ASSERT_EQ(timeZone->GetDSTOffsetMs(DST_CHANGE_DATE).unwrap(),
+ 1 * 60 * 60 * 1000);
+}
+
+TEST(IntlTimeZone, GetOffsetMs)
+{
+ auto timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"GMT+3"))).unwrap();
+ ASSERT_EQ(timeZone->GetOffsetMs(0).unwrap(), 3 * 60 * 60 * 1000);
+
+ timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"Etc/GMT+3"))).unwrap();
+ ASSERT_EQ(timeZone->GetOffsetMs(0).unwrap(), -(3 * 60 * 60 * 1000));
+
+ timeZone =
+ TimeZone::TryCreate(Some(MakeStringSpan(u"America/New_York"))).unwrap();
+ ASSERT_EQ(timeZone->GetOffsetMs(0).unwrap(), -(5 * 60 * 60 * 1000));
+ ASSERT_EQ(timeZone->GetOffsetMs(RELEASE_DATE).unwrap(),
+ -(4 * 60 * 60 * 1000));
+ ASSERT_EQ(timeZone->GetOffsetMs(DST_CHANGE_DATE).unwrap(),
+ -(4 * 60 * 60 * 1000));
+}
+
+TEST(IntlTimeZone, GetUTCOffsetMs)
+{
+ auto timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"GMT+3"))).unwrap();
+ ASSERT_EQ(timeZone->GetUTCOffsetMs(0).unwrap(), 3 * 60 * 60 * 1000);
+
+ timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"Etc/GMT+3"))).unwrap();
+ ASSERT_EQ(timeZone->GetUTCOffsetMs(0).unwrap(), -(3 * 60 * 60 * 1000));
+
+ timeZone =
+ TimeZone::TryCreate(Some(MakeStringSpan(u"America/New_York"))).unwrap();
+ ASSERT_EQ(timeZone->GetUTCOffsetMs(0).unwrap(), -(5 * 60 * 60 * 1000));
+ ASSERT_EQ(timeZone->GetUTCOffsetMs(RELEASE_DATE).unwrap(),
+ -(4 * 60 * 60 * 1000));
+ ASSERT_EQ(timeZone->GetUTCOffsetMs(DST_CHANGE_DATE).unwrap(),
+ -(5 * 60 * 60 * 1000));
+}
+
+TEST(IntlTimeZone, GetDisplayName)
+{
+ using DaylightSavings = TimeZone::DaylightSavings;
+
+ TestBuffer<char16_t> buffer;
+
+ auto timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"GMT+3"))).unwrap();
+
+ buffer.clear();
+ timeZone->GetDisplayName("en", DaylightSavings::No, buffer).unwrap();
+ ASSERT_EQ(buffer.get_string_view(), u"GMT+03:00");
+
+ buffer.clear();
+ timeZone->GetDisplayName("en", DaylightSavings::Yes, buffer).unwrap();
+ ASSERT_EQ(buffer.get_string_view(), u"GMT+03:00");
+
+ timeZone = TimeZone::TryCreate(Some(MakeStringSpan(u"Etc/GMT+3"))).unwrap();
+
+ buffer.clear();
+ timeZone->GetDisplayName("en", DaylightSavings::No, buffer).unwrap();
+ ASSERT_EQ(buffer.get_string_view(), u"GMT-03:00");
+
+ buffer.clear();
+ timeZone->GetDisplayName("en", DaylightSavings::Yes, buffer).unwrap();
+ ASSERT_EQ(buffer.get_string_view(), u"GMT-03:00");
+
+ timeZone =
+ TimeZone::TryCreate(Some(MakeStringSpan(u"America/New_York"))).unwrap();
+
+ buffer.clear();
+ timeZone->GetDisplayName("en", DaylightSavings::No, buffer).unwrap();
+ ASSERT_EQ(buffer.get_string_view(), u"Eastern Standard Time");
+
+ buffer.clear();
+ timeZone->GetDisplayName("en", DaylightSavings::Yes, buffer).unwrap();
+ ASSERT_EQ(buffer.get_string_view(), u"Eastern Daylight Time");
+}
+
+TEST(IntlTimeZone, GetCanonicalTimeZoneID)
+{
+ TestBuffer<char16_t> buffer;
+
+ // Providing a canonical time zone results in the same string at the end.
+ TimeZone::GetCanonicalTimeZoneID(MakeStringSpan(u"America/Chicago"), buffer)
+ .unwrap();
+ ASSERT_EQ(buffer.get_string_view(), u"America/Chicago");
+
+ // Providing an alias will result in the canonical representation.
+ TimeZone::GetCanonicalTimeZoneID(MakeStringSpan(u"Europe/Belfast"), buffer)
+ .unwrap();
+ ASSERT_EQ(buffer.get_string_view(), u"Europe/London");
+
+ // An unknown time zone results in an error.
+ ASSERT_TRUE(TimeZone::GetCanonicalTimeZoneID(
+ MakeStringSpan(u"Not a time zone"), buffer)
+ .isErr());
+}
+
+TEST(IntlTimeZone, GetAvailableTimeZones)
+{
+ constexpr auto EuropeBerlin = MakeStringSpan("Europe/Berlin");
+ constexpr auto EuropeBusingen = MakeStringSpan("Europe/Busingen");
+
+ auto timeZones = TimeZone::GetAvailableTimeZones("DE").unwrap();
+
+ bool hasEuropeBerlin = false;
+ bool hasEuropeBusingen = false;
+
+ for (auto timeZone : timeZones) {
+ auto span = timeZone.unwrap();
+ if (span == EuropeBerlin) {
+ ASSERT_FALSE(hasEuropeBerlin);
+ hasEuropeBerlin = true;
+ } else if (span == EuropeBusingen) {
+ ASSERT_FALSE(hasEuropeBusingen);
+ hasEuropeBusingen = true;
+ } else {
+ std::string str(span.data(), span.size());
+ ADD_FAILURE() << "Unexpected time zone: " << str;
+ }
+ }
+
+ ASSERT_TRUE(hasEuropeBerlin);
+ ASSERT_TRUE(hasEuropeBusingen);
+}
+
+TEST(IntlTimeZone, GetAvailableTimeZonesNoRegion)
+{
+ constexpr auto AmericaNewYork = MakeStringSpan("America/New_York");
+ constexpr auto AsiaTokyo = MakeStringSpan("Asia/Tokyo");
+ constexpr auto EuropeParis = MakeStringSpan("Europe/Paris");
+
+ auto timeZones = TimeZone::GetAvailableTimeZones().unwrap();
+
+ bool hasAmericaNewYork = false;
+ bool hasAsiaTokyo = false;
+ bool hasEuropeParis = false;
+
+ for (auto timeZone : timeZones) {
+ auto span = timeZone.unwrap();
+ if (span == AmericaNewYork) {
+ ASSERT_FALSE(hasAmericaNewYork);
+ hasAmericaNewYork = true;
+ } else if (span == AsiaTokyo) {
+ ASSERT_FALSE(hasAsiaTokyo);
+ hasAsiaTokyo = true;
+ } else if (span == EuropeParis) {
+ ASSERT_FALSE(hasEuropeParis);
+ hasEuropeParis = true;
+ }
+ }
+
+ ASSERT_TRUE(hasAmericaNewYork);
+ ASSERT_TRUE(hasAsiaTokyo);
+ ASSERT_TRUE(hasEuropeParis);
+}
+
+TEST(IntlTimeZone, GetTZDataVersion)
+{
+ // From <https://data.iana.org/time-zones/tz-link.html#download>:
+ //
+ // "Since 1996, each version has been a four-digit year followed by lower-case
+ // letter (a through z, then za through zz, then zza through zzz, and so on)."
+ //
+ // More than 26 releases are unlikely or at least never happend. 2009 got
+ // quite close with 21 releases, but that was the first time ever with more
+ // than twenty releases in a single year.
+ //
+ // Should this assertion ever fail, because more than 26 releases were issued,
+ // update it accordingly. And in that case we should be extra cautious that
+ // all time zone functionality in Firefox and in external libraries we're
+ // using can cope with more than 26 tzdata releases.
+ //
+ // Also see <https://mm.icann.org/pipermail/tz/2021-September/030621.html>:
+ //
+ // "For Android having 2021a1 and 2021b would be inconvenient. Because there
+ // are hardcoded places which expect that tzdata version is exactly 5
+ // characters."
+
+ auto version = TimeZone::GetTZDataVersion().unwrap();
+ auto [year, release] = version.SplitAt(4);
+
+ ASSERT_TRUE(std::all_of(year.begin(), year.end(), IsAsciiDigit<char>));
+ ASSERT_TRUE(IsAsciiAlpha(release[0]));
+
+ // ICU issued a non-standard release "2021a1".
+ ASSERT_TRUE(release.Length() == 1 || release.Length() == 2);
+
+ if (release.Length() == 2) {
+ ASSERT_TRUE(IsAsciiDigit(release[1]));
+ }
+}
+
+} // namespace mozilla::intl
diff --git a/intl/components/gtest/moz.build b/intl/components/gtest/moz.build
new file mode 100644
index 0000000000..c9ccc4cda3
--- /dev/null
+++ b/intl/components/gtest/moz.build
@@ -0,0 +1,30 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ "TestBidi.cpp",
+ "TestCalendar.cpp",
+ "TestCollator.cpp",
+ "TestCurrency.cpp",
+ "TestDateIntervalFormat.cpp",
+ "TestDateTimeFormat.cpp",
+ "TestDisplayNames.cpp",
+ "TestIDNA.cpp",
+ "TestListFormat.cpp",
+ "TestLocale.cpp",
+ "TestLocaleCanonicalizer.cpp",
+ "TestMeasureUnit.cpp",
+ "TestNumberFormat.cpp",
+ "TestNumberingSystem.cpp",
+ "TestNumberParser.cpp",
+ "TestPluralRules.cpp",
+ "TestRelativeTimeFormat.cpp",
+ "TestScript.cpp",
+ "TestString.cpp",
+ "TestTimeZone.cpp",
+]
+
+FINAL_LIBRARY = "xul-gtest"