diff options
Diffstat (limited to 'intl/components/gtest')
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" |