From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- intl/components/gtest/TestBidi.cpp | 314 +++++ intl/components/gtest/TestBuffer.h | 149 +++ intl/components/gtest/TestCalendar.cpp | 156 +++ intl/components/gtest/TestCollator.cpp | 347 +++++ intl/components/gtest/TestCurrency.cpp | 34 + intl/components/gtest/TestDateIntervalFormat.cpp | 200 +++ intl/components/gtest/TestDateTimeFormat.cpp | 621 +++++++++ intl/components/gtest/TestDisplayNames.cpp | 635 +++++++++ intl/components/gtest/TestIDNA.cpp | 118 ++ intl/components/gtest/TestListFormat.cpp | 162 +++ intl/components/gtest/TestLocale.cpp | 152 +++ intl/components/gtest/TestLocaleCanonicalizer.cpp | 60 + intl/components/gtest/TestMeasureUnit.cpp | 43 + intl/components/gtest/TestNumberFormat.cpp | 236 ++++ intl/components/gtest/TestNumberParser.cpp | 34 + intl/components/gtest/TestNumberRangeFormat.cpp | 118 ++ intl/components/gtest/TestNumberingSystem.cpp | 23 + intl/components/gtest/TestPluralRules.cpp | 671 ++++++++++ intl/components/gtest/TestRelativeTimeFormat.cpp | 161 +++ intl/components/gtest/TestScript.cpp | 62 + intl/components/gtest/TestString.cpp | 260 ++++ intl/components/gtest/TestTimeZone.cpp | 249 ++++ intl/components/gtest/moz.build | 30 + intl/components/moz.build | 82 ++ intl/components/src/Bidi.cpp | 138 ++ intl/components/src/Bidi.h | 160 +++ intl/components/src/BidiClass.h | 49 + intl/components/src/BidiEmbeddingLevel.cpp | 53 + intl/components/src/BidiEmbeddingLevel.h | 113 ++ intl/components/src/Calendar.cpp | 172 +++ intl/components/src/Calendar.h | 133 ++ intl/components/src/Collator.cpp | 305 +++++ intl/components/src/Collator.h | 322 +++++ intl/components/src/Currency.cpp | 22 + intl/components/src/Currency.h | 30 + intl/components/src/DateIntervalFormat.cpp | 266 ++++ intl/components/src/DateIntervalFormat.h | 107 ++ intl/components/src/DateTimeFormat.cpp | 1140 ++++++++++++++++ intl/components/src/DateTimeFormat.h | 593 +++++++++ intl/components/src/DateTimeFormatUtils.cpp | 104 ++ intl/components/src/DateTimeFormatUtils.h | 14 + intl/components/src/DateTimePart.h | 84 ++ intl/components/src/DateTimePatternGenerator.cpp | 49 + intl/components/src/DateTimePatternGenerator.h | 161 +++ intl/components/src/DisplayNames.cpp | 234 ++++ intl/components/src/DisplayNames.h | 971 ++++++++++++++ intl/components/src/FormatBuffer.h | 77 ++ intl/components/src/GeneralCategory.h | 52 + intl/components/src/ICU4CGlue.cpp | 44 + intl/components/src/ICU4CGlue.h | 722 ++++++++++ intl/components/src/ICU4CLibrary.cpp | 41 + intl/components/src/ICU4CLibrary.h | 74 ++ intl/components/src/ICUError.h | 118 ++ intl/components/src/IDNA.cpp | 26 + intl/components/src/IDNA.h | 130 ++ intl/components/src/ListFormat.cpp | 132 ++ intl/components/src/ListFormat.h | 223 ++++ intl/components/src/Locale.cpp | 1471 +++++++++++++++++++++ intl/components/src/Locale.h | 773 +++++++++++ intl/components/src/LocaleCanonicalizer.cpp | 36 + intl/components/src/LocaleCanonicalizer.h | 43 + intl/components/src/LocaleGenerated.cpp | 1208 +++++++++++++++++ intl/components/src/MeasureUnit.cpp | 110 ++ intl/components/src/MeasureUnit.h | 155 +++ intl/components/src/MeasureUnitGenerated.h | 70 + intl/components/src/NumberFormat.cpp | 154 +++ intl/components/src/NumberFormat.h | 426 ++++++ intl/components/src/NumberFormatFields.cpp | 396 ++++++ intl/components/src/NumberFormatFields.h | 91 ++ intl/components/src/NumberFormatterSkeleton.cpp | 473 +++++++ intl/components/src/NumberFormatterSkeleton.h | 110 ++ intl/components/src/NumberParser.cpp | 45 + intl/components/src/NumberParser.h | 46 + intl/components/src/NumberPart.h | 53 + intl/components/src/NumberRangeFormat.cpp | 215 +++ intl/components/src/NumberRangeFormat.h | 237 ++++ intl/components/src/NumberingSystem.cpp | 38 + intl/components/src/NumberingSystem.h | 56 + intl/components/src/PluralRules.cpp | 180 +++ intl/components/src/PluralRules.h | 221 ++++ intl/components/src/RelativeTimeFormat.cpp | 153 +++ intl/components/src/RelativeTimeFormat.h | 146 ++ intl/components/src/ScopedICUObject.h | 40 + intl/components/src/String.cpp | 13 + intl/components/src/String.h | 256 ++++ intl/components/src/TimeZone.cpp | 344 +++++ intl/components/src/TimeZone.h | 257 ++++ intl/components/src/UnicodeProperties.h | 310 +++++ intl/components/src/UnicodeScriptCodes.h | 261 ++++ 89 files changed, 20163 insertions(+) create mode 100644 intl/components/gtest/TestBidi.cpp create mode 100644 intl/components/gtest/TestBuffer.h create mode 100644 intl/components/gtest/TestCalendar.cpp create mode 100644 intl/components/gtest/TestCollator.cpp create mode 100644 intl/components/gtest/TestCurrency.cpp create mode 100644 intl/components/gtest/TestDateIntervalFormat.cpp create mode 100644 intl/components/gtest/TestDateTimeFormat.cpp create mode 100644 intl/components/gtest/TestDisplayNames.cpp create mode 100644 intl/components/gtest/TestIDNA.cpp create mode 100644 intl/components/gtest/TestListFormat.cpp create mode 100644 intl/components/gtest/TestLocale.cpp create mode 100644 intl/components/gtest/TestLocaleCanonicalizer.cpp create mode 100644 intl/components/gtest/TestMeasureUnit.cpp create mode 100644 intl/components/gtest/TestNumberFormat.cpp create mode 100644 intl/components/gtest/TestNumberParser.cpp create mode 100644 intl/components/gtest/TestNumberRangeFormat.cpp create mode 100644 intl/components/gtest/TestNumberingSystem.cpp create mode 100644 intl/components/gtest/TestPluralRules.cpp create mode 100644 intl/components/gtest/TestRelativeTimeFormat.cpp create mode 100644 intl/components/gtest/TestScript.cpp create mode 100644 intl/components/gtest/TestString.cpp create mode 100644 intl/components/gtest/TestTimeZone.cpp create mode 100644 intl/components/gtest/moz.build create mode 100644 intl/components/moz.build create mode 100644 intl/components/src/Bidi.cpp create mode 100644 intl/components/src/Bidi.h create mode 100644 intl/components/src/BidiClass.h create mode 100644 intl/components/src/BidiEmbeddingLevel.cpp create mode 100644 intl/components/src/BidiEmbeddingLevel.h create mode 100644 intl/components/src/Calendar.cpp create mode 100644 intl/components/src/Calendar.h create mode 100644 intl/components/src/Collator.cpp create mode 100644 intl/components/src/Collator.h create mode 100644 intl/components/src/Currency.cpp create mode 100644 intl/components/src/Currency.h create mode 100644 intl/components/src/DateIntervalFormat.cpp create mode 100644 intl/components/src/DateIntervalFormat.h create mode 100644 intl/components/src/DateTimeFormat.cpp create mode 100644 intl/components/src/DateTimeFormat.h create mode 100644 intl/components/src/DateTimeFormatUtils.cpp create mode 100644 intl/components/src/DateTimeFormatUtils.h create mode 100644 intl/components/src/DateTimePart.h create mode 100644 intl/components/src/DateTimePatternGenerator.cpp create mode 100644 intl/components/src/DateTimePatternGenerator.h create mode 100644 intl/components/src/DisplayNames.cpp create mode 100644 intl/components/src/DisplayNames.h create mode 100644 intl/components/src/FormatBuffer.h create mode 100644 intl/components/src/GeneralCategory.h create mode 100644 intl/components/src/ICU4CGlue.cpp create mode 100644 intl/components/src/ICU4CGlue.h create mode 100644 intl/components/src/ICU4CLibrary.cpp create mode 100644 intl/components/src/ICU4CLibrary.h create mode 100644 intl/components/src/ICUError.h create mode 100644 intl/components/src/IDNA.cpp create mode 100644 intl/components/src/IDNA.h create mode 100644 intl/components/src/ListFormat.cpp create mode 100644 intl/components/src/ListFormat.h create mode 100644 intl/components/src/Locale.cpp create mode 100644 intl/components/src/Locale.h create mode 100644 intl/components/src/LocaleCanonicalizer.cpp create mode 100644 intl/components/src/LocaleCanonicalizer.h create mode 100644 intl/components/src/LocaleGenerated.cpp create mode 100644 intl/components/src/MeasureUnit.cpp create mode 100644 intl/components/src/MeasureUnit.h create mode 100644 intl/components/src/MeasureUnitGenerated.h create mode 100644 intl/components/src/NumberFormat.cpp create mode 100644 intl/components/src/NumberFormat.h create mode 100644 intl/components/src/NumberFormatFields.cpp create mode 100644 intl/components/src/NumberFormatFields.h create mode 100644 intl/components/src/NumberFormatterSkeleton.cpp create mode 100644 intl/components/src/NumberFormatterSkeleton.h create mode 100644 intl/components/src/NumberParser.cpp create mode 100644 intl/components/src/NumberParser.h create mode 100644 intl/components/src/NumberPart.h create mode 100644 intl/components/src/NumberRangeFormat.cpp create mode 100644 intl/components/src/NumberRangeFormat.h create mode 100644 intl/components/src/NumberingSystem.cpp create mode 100644 intl/components/src/NumberingSystem.h create mode 100644 intl/components/src/PluralRules.cpp create mode 100644 intl/components/src/PluralRules.h create mode 100644 intl/components/src/RelativeTimeFormat.cpp create mode 100644 intl/components/src/RelativeTimeFormat.h create mode 100644 intl/components/src/ScopedICUObject.h create mode 100644 intl/components/src/String.cpp create mode 100644 intl/components/src/String.h create mode 100644 intl/components/src/TimeZone.cpp create mode 100644 intl/components/src/TimeZone.h create mode 100644 intl/components/src/UnicodeProperties.h create mode 100644 intl/components/src/UnicodeScriptCodes.h (limited to 'intl/components') 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 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 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 Next() { + if (mRunIndex >= mRunCount) { + return Nothing(); + } + + int32_t stringIndex = -1; + int32_t stringLength = -1; + + BidiDirection direction = + mBidi.GetVisualRun(mRunIndex, &stringIndex, &stringLength); + + Span string(mParagraph.Elements() + stringIndex, + stringLength); + mRunIndex++; + return Some(VisualRun{string, direction}); + } + + private: + Bidi& mBidi; + Span mParagraph = Span(); + int32_t mRunIndex = 0; + int32_t mRunCount = 0; +}; + +struct LogicalRun { + Span 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 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 Next() { + if (mRunIndex >= static_cast(mParagraph.Length())) { + return Nothing(); + } + + int32_t logicalLimit; + + BidiEmbeddingLevel embeddingLevel; + mBidi.GetLogicalRun(mRunIndex, &logicalLimit, &embeddingLevel); + + Span string(mParagraph.Elements() + mRunIndex, + logicalLimit - mRunIndex); + + mRunIndex = logicalLimit; + return Some(LogicalRun{string, embeddingLevel}); + } + + private: + Bidi& mBidi; + Span mParagraph = Span(); + 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 run = visualRunIter.Next(); + ASSERT_TRUE(run.isSome()); + ASSERT_EQ(run->string, MakeStringSpan(u"first visual run ")); + ASSERT_EQ(run->direction, BidiDirection::LTR); + } + { + Maybe run = visualRunIter.Next(); + ASSERT_TRUE(run.isSome()); + ASSERT_EQ(run->string, MakeStringSpan(u"التشغيل البصري الثاني")); + ASSERT_EQ(run->direction, BidiDirection::RTL); + } + { + Maybe run = visualRunIter.Next(); + ASSERT_TRUE(run.isSome()); + ASSERT_EQ(run->string, MakeStringSpan(u" third visual run")); + ASSERT_EQ(run->direction, BidiDirection::LTR); + } + { + Maybe 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 run = visualRunIter.Next(); + ASSERT_TRUE(run.isSome()); + ASSERT_EQ(run->string, MakeStringSpan(u"ltr")); + ASSERT_EQ(run->direction, BidiDirection::LTR); + } + { + Maybe run = visualRunIter.Next(); + ASSERT_TRUE(run.isSome()); + ASSERT_EQ(run->string, MakeStringSpan(u"\u202a___ltr___")); + ASSERT_EQ(run->direction, BidiDirection::LTR); + } + { + Maybe run = visualRunIter.Next(); + ASSERT_TRUE(run.isSome()); + ASSERT_EQ(run->string, MakeStringSpan(u"\u202b___رائع___")); + ASSERT_EQ(run->direction, BidiDirection::RTL); + } + { + Maybe 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 +#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 +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 result = mBuffer.resizeUninitialized(aAmount); + MOZ_ASSERT(result); + } + + /** + * Get a string view into the buffer, which is useful for test assertions. + */ + std::basic_string_view get_string_view() { + return std::basic_string_view(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 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 actualSV(data(), length()); + std::basic_string_view expectedSV(aExpected); + + if (actualSV.compare(expectedSV) == 0) { + return true; + } + + static_assert(std::is_same_v || + std::is_same_v); + + std::string actual; + std::string expected; + const char* startQuote; + + if constexpr (std::is_same_v) { + actual = std::string(actualSV); + expected = std::string(expectedSV); + startQuote = "\""; + } + if constexpr (std::is_same_v) { + 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 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 +#include +#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 bufferA; + TestBuffer 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(bufferA.data()), + reinterpret_cast(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 bufferA; + TestBuffer 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 bufferA; + TestBuffer 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 = nullptr; + TestBuffer bufferA; + TestBuffer 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 bufferA; + TestBuffer 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 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 dtFormat = + DateTimeFormat::TryCreateFromComponents( + MakeStringSpan("en-US"), components, dateTimePatternGenerator.get(), + Some(MakeStringSpan(u"GMT"))) + .unwrap(); + + UniquePtr 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 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 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 + +namespace mozilla::intl { + +// Firefox 1.0 release date. +const double DATE = 1032800850000.0; + +static UniquePtr 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 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 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 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 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 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 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 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 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& aBuffer, DateTimeFormat::ComponentsBag& aComponents, + Span aLocale = MakeStringSpan("en-US")) { + UniquePtr 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 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 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 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 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 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 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 +const char* ToString(Maybe option) { + if (option) { + if constexpr (std::is_same_v || std::is_same_v) { + return ToString(*option); + } else { + return DateTimeFormat::ToString(*option); + } + } + return "Nothing"; +} + +template +[[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 aLocale = MakeStringSpan("en-US")) { + UniquePtr 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 originalSkeleton; + auto originalSkeletonResult = dtFormat->GetOriginalSkeleton(originalSkeleton); + ASSERT_TRUE(originalSkeletonResult.isOk()); + ASSERT_TRUE(originalSkeleton.verboseMatches(u"MMMMMdd")); + + TestBuffer pattern; + auto patternResult = dtFormat->GetPattern(pattern); + ASSERT_TRUE(patternResult.isOk()); + ASSERT_TRUE(pattern.verboseMatches(u"M月dd日")); + + TestBuffer 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 dtFormat = + DateTimeFormat::TryCreateFromComponents( + MakeStringSpan("en-US"), components, dateTimePatternGenerator.get(), + Some(MakeStringSpan(u"GMT"))) + .unwrap(); + + TestBuffer 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 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 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 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 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 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 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 buffer; + + DisplayNames::Options options{}; + Span 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 buffer; + + DisplayNames::Options options{}; + Span 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 buffer; + + DisplayNames::Options options{}; + Span 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 buffer; + + DisplayNames::Options options{}; + Span 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 buffer; + + { + // No calendar. + DisplayNames::Options options{}; + Span 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 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 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 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 buffer; + + DisplayNames::Options options{}; + Span 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 buffer; + + DisplayNames::Options options{}; + Span 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 buffer; + DisplayNames::Options options{}; + Span 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 buffer; + + DisplayNames::Options options{}; + Span 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 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 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 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 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 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 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 buf16; + ASSERT_TRUE(lf->Format(list, buf16).isOk()); + ASSERT_EQ(buf16.get_string_view(), u"Alice, Bob, and Charlie"); + + UniquePtr 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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(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 + +namespace mozilla { +namespace intl { + +TEST(IntlNumberFormat, Basic) +{ + NumberFormatOptions options; + UniquePtr nf = + NumberFormat::TryCreate("en-US", options).unwrap(); + TestBuffer buf8; + ASSERT_TRUE(nf->format(1234.56, buf8).isOk()); + ASSERT_EQ(buf8.get_string_view(), "1,234.56"); + TestBuffer 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 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 nf = + NumberFormat::TryCreate("es-ES", options).unwrap(); + TestBuffer buf8; + ASSERT_TRUE(nf->format(123456.789, buf8).isOk()); + ASSERT_EQ(buf8.get_string_view(), "123.456,789"); + TestBuffer 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 nf = + NumberFormat::TryCreate("es-ES", options).unwrap(); + TestBuffer 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 nf = + NumberFormat::TryCreate("es-MX", options).unwrap(); + TestBuffer buf8; + ASSERT_TRUE(nf->format(123456.789, buf8).isOk()); + ASSERT_EQ(buf8.get_string_view(), "$123,456.79"); + TestBuffer 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 nf = + NumberFormat::TryCreate("es-MX", options).unwrap(); + TestBuffer buf8; + ASSERT_TRUE(nf->format(12.34, buf8).isOk()); + ASSERT_EQ(buf8.get_string_view(), "12.34 metros por segundo"); + TestBuffer 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 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 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 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 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 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 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 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 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 + +#include "mozilla/intl/NumberRangeFormat.h" +#include "./TestBuffer.h" + +namespace mozilla { +namespace intl { + +using namespace std::literals; + +TEST(IntlNumberRangeFormat, Basic) +{ + NumberRangeFormatOptions options; + UniquePtr 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 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 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 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(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(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(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(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(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, ICUError> res = + RelativeTimeFormat::TryCreate("en-US", options); + ASSERT_TRUE(res.isOk()); + UniquePtr rtf = res.unwrap(); + TestBuffer buf8; + ASSERT_TRUE( + rtf->format(1.2, RelativeTimeFormat::FormatUnit::Day, buf8).isOk()); + ASSERT_EQ(buf8.get_string_view(), "in 1.2 days"); + + TestBuffer 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, ICUError> res = + RelativeTimeFormat::TryCreate("fr", options); + ASSERT_TRUE(res.isOk()); + UniquePtr rtf = res.unwrap(); + TestBuffer 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, ICUError> res = + RelativeTimeFormat::TryCreate("en-US", options); + ASSERT_TRUE(res.isOk()); + UniquePtr rtf = res.unwrap(); + TestBuffer 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, ICUError> res = + RelativeTimeFormat::TryCreate("es-AR", options); + ASSERT_TRUE(res.isOk()); + UniquePtr rtf = res.unwrap(); + NumberPartVector parts; + Result, 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 + +#include "TestBuffer.h" + +namespace mozilla::intl { + +static Result ToLocaleLowerCase( + const char* aLocale, const char16_t* aString, + TestBuffer& aBuffer) { + aBuffer.clear(); + + MOZ_TRY(String::ToLocaleLowerCase(aLocale, MakeStringSpan(aString), aBuffer)); + + return aBuffer.get_string_view(); +} + +static Result ToLocaleUpperCase( + const char* aLocale, const char16_t* aString, + TestBuffer& aBuffer) { + aBuffer.clear(); + + MOZ_TRY(String::ToLocaleUpperCase(aLocale, MakeStringSpan(aString), aBuffer)); + + return aBuffer.get_string_view(); +} + +TEST(IntlString, ToLocaleLowerCase) +{ + TestBuffer 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 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 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 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 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 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 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 +#include + +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 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 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 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 : + // + // "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 : + // + // "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)); + 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" diff --git a/intl/components/moz.build b/intl/components/moz.build new file mode 100644 index 0000000000..8e0c99b386 --- /dev/null +++ b/intl/components/moz.build @@ -0,0 +1,82 @@ +# -*- 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/. +EXPORTS.mozilla.intl = [ + "src/Bidi.h", + "src/BidiClass.h", + "src/BidiEmbeddingLevel.h", + "src/Calendar.h", + "src/Collator.h", + "src/Currency.h", + "src/DateIntervalFormat.h", + "src/DateTimeFormat.h", + "src/DateTimePart.h", + "src/DateTimePatternGenerator.h", + "src/DisplayNames.h", + "src/FormatBuffer.h", + "src/GeneralCategory.h", + "src/ICU4CGlue.h", + "src/ICU4CLibrary.h", + "src/ICUError.h", + "src/IDNA.h", + "src/ListFormat.h", + "src/Locale.h", + "src/LocaleCanonicalizer.h", + "src/MeasureUnit.h", + "src/MeasureUnitGenerated.h", + "src/NumberFormat.h", + "src/NumberingSystem.h", + "src/NumberParser.h", + "src/NumberPart.h", + "src/NumberRangeFormat.h", + "src/PluralRules.h", + "src/RelativeTimeFormat.h", + "src/String.h", + "src/TimeZone.h", + "src/UnicodeProperties.h", + "src/UnicodeScriptCodes.h", +] + +UNIFIED_SOURCES += [ + "src/Bidi.cpp", + "src/BidiEmbeddingLevel.cpp", + "src/Calendar.cpp", + "src/Collator.cpp", + "src/Currency.cpp", + "src/DateIntervalFormat.cpp", + "src/DateTimeFormat.cpp", + "src/DateTimeFormatUtils.cpp", + "src/DateTimePatternGenerator.cpp", + "src/DisplayNames.cpp", + "src/ICU4CGlue.cpp", + "src/ICU4CLibrary.cpp", + "src/IDNA.cpp", + "src/ListFormat.cpp", + "src/Locale.cpp", + "src/LocaleCanonicalizer.cpp", + "src/LocaleGenerated.cpp", + "src/MeasureUnit.cpp", + "src/NumberFormat.cpp", + "src/NumberFormatFields.cpp", + "src/NumberFormatterSkeleton.cpp", + "src/NumberingSystem.cpp", + "src/NumberParser.cpp", + "src/NumberRangeFormat.cpp", + "src/PluralRules.cpp", + "src/RelativeTimeFormat.cpp", + "src/String.cpp", + "src/TimeZone.cpp", +] + +if not CONFIG["JS_STANDALONE"]: + TEST_DIRS += ["gtest"] + +# At the time of this writing the MOZ_HAS_MOZGLUE define must be true in order to +# correctly include ConvertUtf8toUtf16 in certain include paths, otherwise it results +# in a compile time "undeclared identifier" error. See: +# https://searchfox.org/mozilla-central/rev/6371054f6260a5f8844846439297547f7cfeeedd/mfbt/Utf8.h#277 +DEFINES["MOZ_HAS_MOZGLUE"] = True + +Library("intlcomponents") diff --git a/intl/components/src/Bidi.cpp b/intl/components/src/Bidi.cpp new file mode 100644 index 0000000000..2ce355c8eb --- /dev/null +++ b/intl/components/src/Bidi.cpp @@ -0,0 +1,138 @@ +/* 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 "mozilla/intl/Bidi.h" +#include "mozilla/Casting.h" +#include "mozilla/intl/ICU4CGlue.h" + +#include "unicode/ubidi.h" + +namespace mozilla::intl { + +Bidi::Bidi() { mBidi = ubidi_open(); } +Bidi::~Bidi() { ubidi_close(mBidi.GetMut()); } + +ICUResult Bidi::SetParagraph(Span aParagraph, + BidiEmbeddingLevel aLevel) { + // Do not allow any reordering of the runs, as this can change the + // performance characteristics of working with runs. In the default mode, + // the levels can be iterated over directly, rather than relying on computing + // logical runs on the fly. This can have negative performance characteristics + // compared to iterating over the levels. + // + // In the UBIDI_REORDER_RUNS_ONLY the levels are encoded with additional + // information which can be safely ignored in this Bidi implementation. + // Note that this check is here since setting the mode must be done before + // calls to setting the paragraph. + MOZ_ASSERT(ubidi_getReorderingMode(mBidi.GetMut()) == UBIDI_REORDER_DEFAULT); + + UErrorCode status = U_ZERO_ERROR; + ubidi_setPara(mBidi.GetMut(), aParagraph.Elements(), + AssertedCast(aParagraph.Length()), aLevel, nullptr, + &status); + + mLevels = nullptr; + + return ToICUResult(status); +} + +Bidi::ParagraphDirection Bidi::GetParagraphDirection() const { + switch (ubidi_getDirection(mBidi.GetConst())) { + case UBIDI_LTR: + return Bidi::ParagraphDirection::LTR; + case UBIDI_RTL: + return Bidi::ParagraphDirection::RTL; + case UBIDI_MIXED: + return Bidi::ParagraphDirection::Mixed; + case UBIDI_NEUTRAL: + // This is only used in `ubidi_getBaseDirection` which is unused in this + // API. + MOZ_ASSERT_UNREACHABLE("Unexpected UBiDiDirection value."); + }; + return Bidi::ParagraphDirection::Mixed; +} + +/* static */ +void Bidi::ReorderVisual(const BidiEmbeddingLevel* aLevels, int32_t aLength, + int32_t* aIndexMap) { + ubidi_reorderVisual(reinterpret_cast(aLevels), aLength, + aIndexMap); +} + +/* static */ +Bidi::BaseDirection Bidi::GetBaseDirection(Span aParagraph) { + UBiDiDirection direction = ubidi_getBaseDirection( + aParagraph.Elements(), AssertedCast(aParagraph.Length())); + + switch (direction) { + case UBIDI_LTR: + return Bidi::BaseDirection::LTR; + case UBIDI_RTL: + return Bidi::BaseDirection::RTL; + case UBIDI_NEUTRAL: + return Bidi::BaseDirection::Neutral; + case UBIDI_MIXED: + MOZ_ASSERT_UNREACHABLE("Unexpected UBiDiDirection value."); + } + + return Bidi::BaseDirection::Neutral; +} + +static BidiDirection ToBidiDirection(UBiDiDirection aDirection) { + switch (aDirection) { + case UBIDI_LTR: + return BidiDirection::LTR; + case UBIDI_RTL: + return BidiDirection::RTL; + case UBIDI_MIXED: + case UBIDI_NEUTRAL: + MOZ_ASSERT_UNREACHABLE("Unexpected UBiDiDirection value."); + } + return BidiDirection::LTR; +} + +Result Bidi::CountRuns() { + UErrorCode status = U_ZERO_ERROR; + int32_t runCount = ubidi_countRuns(mBidi.GetMut(), &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + + mLength = ubidi_getProcessedLength(mBidi.GetConst()); + mLevels = mLength > 0 ? reinterpret_cast( + ubidi_getLevels(mBidi.GetMut(), &status)) + : nullptr; + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + + return runCount; +} + +void Bidi::GetLogicalRun(int32_t aLogicalStart, int32_t* aLogicalLimitOut, + BidiEmbeddingLevel* aLevelOut) { + MOZ_ASSERT(mLevels, "CountRuns hasn't been run?"); + MOZ_RELEASE_ASSERT(aLogicalStart < mLength, "Out of bound"); + BidiEmbeddingLevel level = mLevels[aLogicalStart]; + int32_t limit; + for (limit = aLogicalStart + 1; limit < mLength; limit++) { + if (mLevels[limit] != level) { + break; + } + } + *aLogicalLimitOut = limit; + *aLevelOut = level; +} + +BidiEmbeddingLevel Bidi::GetParagraphEmbeddingLevel() const { + return BidiEmbeddingLevel(ubidi_getParaLevel(mBidi.GetConst())); +} + +BidiDirection Bidi::GetVisualRun(int32_t aRunIndex, int32_t* aLogicalStart, + int32_t* aLength) { + return ToBidiDirection( + ubidi_getVisualRun(mBidi.GetMut(), aRunIndex, aLogicalStart, aLength)); +} + +} // namespace mozilla::intl diff --git a/intl/components/src/Bidi.h b/intl/components/src/Bidi.h new file mode 100644 index 0000000000..9b7fba73e2 --- /dev/null +++ b/intl/components/src/Bidi.h @@ -0,0 +1,160 @@ +/* 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_Bidi_h_ +#define intl_components_Bidi_h_ + +#include "mozilla/intl/BidiEmbeddingLevel.h" +#include "mozilla/intl/ICU4CGlue.h" + +struct UBiDi; + +namespace mozilla::intl { + +/** + * This component is a Mozilla-focused API for working with bidirectional (bidi) + * text. Text is commonly displayed left to right (LTR), especially for + * Latin-based alphabets. However, languages like Arabic and Hebrew displays + * text right to left (RTL). When displaying text, LTR and RTL text can be + * combined together in the same paragraph. This class gives tools for working + * with unidirectional, and mixed direction paragraphs. + * + * See the Unicode Bidirectional Algorithm document for implementation details: + * https://unicode.org/reports/tr9/ + */ +class Bidi final { + public: + Bidi(); + ~Bidi(); + + // Not copyable or movable + Bidi(const Bidi&) = delete; + Bidi& operator=(const Bidi&) = delete; + + /** + * This enum indicates the text direction for the set paragraph. Some + * paragraphs are unidirectional, where they only have one direction, or a + * paragraph could use both LTR and RTL. In this case the paragraph's + * direction would be mixed. + */ + enum class ParagraphDirection { LTR, RTL, Mixed }; + + /** + * Set the current paragraph of text to analyze for its bidi properties. This + * performs the Unicode bidi algorithm as specified by: + * https://unicode.org/reports/tr9/ + * + * After setting the text, the other getter methods can be used to find out + * the directionality of the paragraph text. + */ + ICUResult SetParagraph(Span aParagraph, + BidiEmbeddingLevel aLevel); + + /** + * Get the embedding level for the paragraph that was set by SetParagraph. + */ + BidiEmbeddingLevel GetParagraphEmbeddingLevel() const; + + /** + * Get the directionality of the paragraph text that was set by SetParagraph. + */ + ParagraphDirection GetParagraphDirection() const; + + /** + * Get the number of runs. This function may invoke the actual reordering on + * the Bidi object, after SetParagraph may have resolved only the levels of + * the text. Therefore, `CountRuns` may have to allocate memory, and may fail + * doing so. + */ + Result CountRuns(); + + /** + * Get the next logical run. The logical runs are a run of text that has the + * same directionality and embedding level. These runs are in memory order, + * and not in display order. + * + * Important! `Bidi::CountRuns` must be called before calling this method. + * + * @param aLogicalStart is the offset into the paragraph text that marks the + * logical start of the text. + * @param aLogicalLimitOut is an out param that is the length of the string + * that makes up the logical run. + * @param aLevelOut is an out parameter that returns the embedding level for + * the run + */ + void GetLogicalRun(int32_t aLogicalStart, int32_t* aLogicalLimitOut, + BidiEmbeddingLevel* aLevelOut); + + /** + * This is a convenience function that does not use the ICU Bidi object. + * It is intended to be used for when an application has determined the + * embedding levels of objects (character sequences) and just needs to have + * them reordered (L2). + * + * @param aLevels is an array with `aLength` levels that have been + * determined by the application. + * + * @param aLength is the number of levels in the array, or, semantically, + * the number of objects to be reordered. It must be greater than 0. + * + * @param aIndexMap is a pointer to an array of `aLength` + * indexes which will reflect the reordering of the characters. + * The array does not need to be initialized. + * The index map will result in + * `aIndexMap[aVisualIndex]==aLogicalIndex`. + */ + static void ReorderVisual(const BidiEmbeddingLevel* aLevels, int32_t aLength, + int32_t* aIndexMap); + + /** + * This enum indicates the bidi character type of the first strong character + * for the set paragraph. + * LTR: bidi character type 'L'. + * RTL: bidi character type 'R' or 'AL'. + * Neutral: The rest of bidi character types. + */ + enum class BaseDirection { LTR, RTL, Neutral }; + + /** + * Get the base direction of the paragraph. + */ + static BaseDirection GetBaseDirection(Span aParagraph); + + /** + * Get one run's logical start, length, and directionality. In an RTL run, the + * character at the logical start is visually on the right of the displayed + * run. The length is the number of characters in the run. + * `Bidi::CountRuns` should be called before the runs are retrieved. + * + * @param aRunIndex is the number of the run in visual order, in the + * range `[0..CountRuns-1]`. + * + * @param aLogicalStart is the first logical character index in the text. + * The pointer may be `nullptr` if this index is not needed. + * + * @param aLength is the number of characters (at least one) in the run. + * The pointer may be `nullptr` if this is not needed. + * + * Note that in right-to-left runs, the code places modifier letters before + * base characters and second surrogates before first ones. + */ + BidiDirection GetVisualRun(int32_t aRunIndex, int32_t* aLogicalStart, + int32_t* aLength); + + private: + ICUPointer mBidi = ICUPointer(nullptr); + + /** + * An array of levels that is the same length as the paragraph from + * `Bidi::SetParagraph`. + */ + const BidiEmbeddingLevel* mLevels = nullptr; + + /** + * The length of the paragraph from `Bidi::SetParagraph`. + */ + int32_t mLength = 0; +}; + +} // namespace mozilla::intl +#endif diff --git a/intl/components/src/BidiClass.h b/intl/components/src/BidiClass.h new file mode 100644 index 0000000000..04a861a382 --- /dev/null +++ b/intl/components/src/BidiClass.h @@ -0,0 +1,49 @@ +/* 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_BidiClass_h_ +#define intl_components_BidiClass_h_ + +#include + +namespace mozilla::intl { + +/** + * Read ftp://ftp.unicode.org/Public/UNIDATA/ReadMe-Latest.txt + * section BIDIRECTIONAL PROPERTIES + * for the detailed definition of the following categories + * + * The values here must match the equivalents in %bidicategorycode in + * mozilla/intl/unicharutil/tools/genUnicodePropertyData.pl, + * and must also match the values used by ICU's UCharDirection. + */ +enum class BidiClass : uint8_t { + LeftToRight = 0, + RightToLeft = 1, + EuropeanNumber = 2, + EuropeanNumberSeparator = 3, + EuropeanNumberTerminator = 4, + ArabicNumber = 5, + CommonNumberSeparator = 6, + BlockSeparator = 7, + SegmentSeparator = 8, + WhiteSpaceNeutral = 9, + OtherNeutral = 10, + LeftToRightEmbedding = 11, + LeftToRightOverride = 12, + RightToLeftArabic = 13, + RightToLeftEmbedding = 14, + RightToLeftOverride = 15, + PopDirectionalFormat = 16, + DirNonSpacingMark = 17, + BoundaryNeutral = 18, + FirstStrongIsolate = 19, + LeftToRightIsolate = 20, + RightToLeftIsolate = 21, + PopDirectionalIsolate = 22, + BidiClassCount +}; + +} // namespace mozilla::intl + +#endif diff --git a/intl/components/src/BidiEmbeddingLevel.cpp b/intl/components/src/BidiEmbeddingLevel.cpp new file mode 100644 index 0000000000..d3ef5da937 --- /dev/null +++ b/intl/components/src/BidiEmbeddingLevel.cpp @@ -0,0 +1,53 @@ +/* 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 "mozilla/intl/BidiEmbeddingLevel.h" +#include "mozilla/Casting.h" +#include "mozilla/intl/ICU4CGlue.h" + +#include "unicode/ubidi.h" + +namespace mozilla::intl { + +bool BidiEmbeddingLevel::IsDefaultLTR() const { + return mValue == UBIDI_DEFAULT_LTR; +}; + +bool BidiEmbeddingLevel::IsDefaultRTL() const { + return mValue == UBIDI_DEFAULT_RTL; +}; + +bool BidiEmbeddingLevel::IsRTL() const { + // If the least significant bit is 1, then the embedding level + // is right-to-left. + // If the least significant bit is 0, then the embedding level + // is left-to-right. + return (mValue & 0x1) == 1; +}; + +bool BidiEmbeddingLevel::IsLTR() const { return !IsRTL(); }; + +bool BidiEmbeddingLevel::IsSameDirection(BidiEmbeddingLevel aOther) const { + return (((mValue ^ aOther) & 1) == 0); +} + +BidiEmbeddingLevel BidiEmbeddingLevel::LTR() { return BidiEmbeddingLevel(0); }; + +BidiEmbeddingLevel BidiEmbeddingLevel::RTL() { return BidiEmbeddingLevel(1); }; + +BidiEmbeddingLevel BidiEmbeddingLevel::DefaultLTR() { + return BidiEmbeddingLevel(UBIDI_DEFAULT_LTR); +}; + +BidiEmbeddingLevel BidiEmbeddingLevel::DefaultRTL() { + return BidiEmbeddingLevel(UBIDI_DEFAULT_RTL); +}; + +BidiDirection BidiEmbeddingLevel::Direction() { + return IsRTL() ? BidiDirection::RTL : BidiDirection::LTR; +}; + +uint8_t BidiEmbeddingLevel::Value() const { return mValue; } + +} // namespace mozilla::intl diff --git a/intl/components/src/BidiEmbeddingLevel.h b/intl/components/src/BidiEmbeddingLevel.h new file mode 100644 index 0000000000..1628b6392f --- /dev/null +++ b/intl/components/src/BidiEmbeddingLevel.h @@ -0,0 +1,113 @@ +/* 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_BidiEmbeddingLevel_h_ +#define intl_components_BidiEmbeddingLevel_h_ + +#include + +/** + * This file has the BidiEmbeddingLevel and BidiDirection enum broken out from + * the main Bidi class for faster includes. This code is used in Layout which + * could trigger long build times when changing core mozilla::intl files. + */ +namespace mozilla::intl { + +/** + * This enum unambiguously classifies text runs as either being left to right, + * or right to left. + */ +enum class BidiDirection : uint8_t { + // Left to right text. + LTR = 0, + // Right to left text. + RTL = 1, +}; + +/** + * Embedding levels are numbers that indicate how deeply the bidi text is + * embedded, and the direction of text on that embedding level. When switching + * between strongly LTR code points and strongly RTL code points the embedding + * level normally switches between an embedding level of 0 (LTR) and 1 (RTL). + * The only time the embedding level increases is if the embedding code points + * are used. This is the Left-to-Right Embedding (LRE) code point (U+202A), or + * the Right-to-Left Embedding (RLE) code point (U+202B). The minimum + * embedding level of text is zero, and the maximum explicit depth is 125. + * + * The most significant bit is reserved for additional meaning. It can be used + * to signify in certain APIs that the text should by default be LTR or RTL if + * no strongly directional code points are found. + * + * Bug 1736595: At the time of this writing, some places in Gecko code use a 1 + * in the most significant bit to indicate that an embedding level has not + * been set. This leads to an ambiguous understanding of what the most + * significant bit actually means. + */ +class BidiEmbeddingLevel { + public: + explicit BidiEmbeddingLevel(uint8_t aValue) : mValue(aValue) {} + explicit BidiEmbeddingLevel(int aValue) + : mValue(static_cast(aValue)) {} + + BidiEmbeddingLevel() = default; + + // Enable the copy operators, but disable move as this is only a uint8_t. + BidiEmbeddingLevel(const BidiEmbeddingLevel& other) = default; + BidiEmbeddingLevel& operator=(const BidiEmbeddingLevel& other) = default; + + /** + * Determine the direction of the embedding level by looking at the least + * significant bit. If it is 0, then it is LTR. If it is 1, then it is RTL. + */ + BidiDirection Direction(); + + /** + * Create a left-to-right embedding level. + */ + static BidiEmbeddingLevel LTR(); + + /** + * Create an right-to-left embedding level. + */ + static BidiEmbeddingLevel RTL(); + + /** + * When passed into `SetParagraph`, the direction is determined by first + * strongly directional character, with the default set to left-to-right if + * none is found. + * + * This is encoded with the highest bit set to 1. + */ + static BidiEmbeddingLevel DefaultLTR(); + + /** + * When passed into `SetParagraph`, the direction is determined by first + * strongly directional character, with the default set to right-to-left if + * none is found. + * + * * This is encoded with the highest and lowest bits set to 1. + */ + static BidiEmbeddingLevel DefaultRTL(); + + bool IsDefaultLTR() const; + bool IsDefaultRTL() const; + bool IsLTR() const; + bool IsRTL() const; + bool IsSameDirection(BidiEmbeddingLevel aOther) const; + + /** + * Get the underlying value as a uint8_t. + */ + uint8_t Value() const; + + /** + * Implicitly convert to the underlying value. + */ + operator uint8_t() const { return mValue; } + + private: + uint8_t mValue = 0; +}; + +} // namespace mozilla::intl +#endif diff --git a/intl/components/src/Calendar.cpp b/intl/components/src/Calendar.cpp new file mode 100644 index 0000000000..d44dedaaae --- /dev/null +++ b/intl/components/src/Calendar.cpp @@ -0,0 +1,172 @@ +/* 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 "mozilla/intl/Calendar.h" + +#include "unicode/ucal.h" +#include "unicode/uloc.h" +#include "unicode/utypes.h" + +namespace mozilla::intl { + +/* static */ +Result, ICUError> Calendar::TryCreate( + const char* aLocale, Maybe> aTimeZoneOverride) { + UErrorCode status = U_ZERO_ERROR; + const UChar* zoneID = nullptr; + int32_t zoneIDLen = 0; + if (aTimeZoneOverride) { + zoneIDLen = static_cast(aTimeZoneOverride->Length()); + zoneID = aTimeZoneOverride->Elements(); + } + + UCalendar* calendar = + ucal_open(zoneID, zoneIDLen, aLocale, UCAL_DEFAULT, &status); + + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + + return MakeUnique(calendar); +} + +Result, ICUError> Calendar::GetBcp47Type() { + UErrorCode status = U_ZERO_ERROR; + const char* oldType = ucal_getType(mCalendar, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + const char* bcp47Type = uloc_toUnicodeLocaleType("calendar", oldType); + + if (!bcp47Type) { + return Err(ICUError::InternalError); + } + + return MakeStringSpan(bcp47Type); +} + +static Weekday WeekdayFromDaysOfWeek(UCalendarDaysOfWeek weekday) { + switch (weekday) { + case UCAL_MONDAY: + return Weekday::Monday; + case UCAL_TUESDAY: + return Weekday::Tuesday; + case UCAL_WEDNESDAY: + return Weekday::Wednesday; + case UCAL_THURSDAY: + return Weekday::Thursday; + case UCAL_FRIDAY: + return Weekday::Friday; + case UCAL_SATURDAY: + return Weekday::Saturday; + case UCAL_SUNDAY: + return Weekday::Sunday; + } + MOZ_CRASH("unexpected weekday value"); +} + +Result, ICUError> Calendar::GetWeekend() { + static_assert(static_cast(UCAL_SUNDAY) == 1); + static_assert(static_cast(UCAL_SATURDAY) == 7); + + UErrorCode status = U_ZERO_ERROR; + + EnumSet weekend; + for (int32_t i = UCAL_SUNDAY; i <= UCAL_SATURDAY; i++) { + auto dayOfWeek = static_cast(i); + auto type = ucal_getDayOfWeekType(mCalendar, dayOfWeek, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + + switch (type) { + case UCAL_WEEKEND_ONSET: + // Treat days which start as a weekday as weekdays. + [[fallthrough]]; + case UCAL_WEEKDAY: + break; + + case UCAL_WEEKEND_CEASE: + // Treat days which start as a weekend day as weekend days. + [[fallthrough]]; + case UCAL_WEEKEND: + weekend += WeekdayFromDaysOfWeek(dayOfWeek); + break; + } + } + + return weekend; +} + +Weekday Calendar::GetFirstDayOfWeek() { + int32_t firstDayOfWeek = ucal_getAttribute(mCalendar, UCAL_FIRST_DAY_OF_WEEK); + MOZ_ASSERT(UCAL_SUNDAY <= firstDayOfWeek && firstDayOfWeek <= UCAL_SATURDAY); + + return WeekdayFromDaysOfWeek( + static_cast(firstDayOfWeek)); +} + +int32_t Calendar::GetMinimalDaysInFirstWeek() { + int32_t minimalDays = + ucal_getAttribute(mCalendar, UCAL_MINIMAL_DAYS_IN_FIRST_WEEK); + MOZ_ASSERT(1 <= minimalDays && minimalDays <= 7); + + return minimalDays; +} + +Result Calendar::SetTimeInMs(double aUnixEpoch) { + UErrorCode status = U_ZERO_ERROR; + ucal_setMillis(mCalendar, aUnixEpoch, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + return Ok{}; +} + +/* static */ +Result, ICUError> +Calendar::GetLegacyKeywordValuesForLocale(const char* aLocale) { + UErrorCode status = U_ZERO_ERROR; + UEnumeration* enumeration = ucal_getKeywordValuesForLocale( + "calendar", aLocale, /* commonlyUsed */ false, &status); + + if (U_SUCCESS(status)) { + return SpanEnumeration(enumeration); + } + + return Err(ToICUError(status)); +} + +/* static */ +SpanResult Calendar::LegacyIdentifierToBcp47(const char* aIdentifier, + int32_t aLength) { + if (aIdentifier == nullptr) { + return Err(InternalError{}); + } + // aLength is not needed here, as the ICU call uses the null terminated + // string. + return MakeStringSpan(uloc_toUnicodeLocaleType("ca", aIdentifier)); +} + +/* static */ +Result +Calendar::GetBcp47KeywordValuesForLocale(const char* aLocale, + CommonlyUsed aCommonlyUsed) { + UErrorCode status = U_ZERO_ERROR; + UEnumeration* enumeration = ucal_getKeywordValuesForLocale( + "calendar", aLocale, static_cast(aCommonlyUsed), &status); + + if (U_SUCCESS(status)) { + return Bcp47IdentifierEnumeration(enumeration); + } + + return Err(ToICUError(status)); +} + +Calendar::~Calendar() { + MOZ_ASSERT(mCalendar); + ucal_close(mCalendar); +} + +} // namespace mozilla::intl diff --git a/intl/components/src/Calendar.h b/intl/components/src/Calendar.h new file mode 100644 index 0000000000..32975bc376 --- /dev/null +++ b/intl/components/src/Calendar.h @@ -0,0 +1,133 @@ +/* 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_Calendar_h_ +#define intl_components_Calendar_h_ + +#include "mozilla/Assertions.h" +#include "mozilla/EnumSet.h" +#include "mozilla/intl/ICU4CGlue.h" +#include "mozilla/intl/ICUError.h" +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "mozilla/Span.h" +#include "mozilla/UniquePtr.h" + +using UCalendar = void*; + +namespace mozilla::intl { + +/** + * Weekdays in the ISO-8601 calendar. + */ +enum class Weekday : uint8_t { + Monday = 1, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, + Sunday, +}; + +/** + * This component is a Mozilla-focused API for working with calendar systems in + * internationalization code. It is used in coordination with other operations + * such as datetime formatting. + */ +class Calendar final { + public: + explicit Calendar(UCalendar* aCalendar) : mCalendar(aCalendar) { + MOZ_ASSERT(aCalendar); + }; + + // Do not allow copy as this class owns the ICU resource. Move is not + // currently implemented, but a custom move operator could be created if + // needed. + Calendar(const Calendar&) = delete; + Calendar& operator=(const Calendar&) = delete; + + /** + * Create a Calendar. + */ + static Result, ICUError> TryCreate( + const char* aLocale, + Maybe> aTimeZoneOverride = Nothing{}); + + /** + * Get the BCP 47 keyword value string designating the calendar type. For + * instance "gregory", "chinese", "islamic-civil", etc. + */ + Result, ICUError> GetBcp47Type(); + + /** + * Return the set of weekdays which are considered as part of the weekend. + */ + Result, ICUError> GetWeekend(); + + /** + * Return the weekday which is considered the first day of the week. + */ + Weekday GetFirstDayOfWeek(); + + /** + * Return the minimal number of days in the first week of a year. + */ + int32_t GetMinimalDaysInFirstWeek(); + + /** + * Set the time for the calendar relative to the number of milliseconds since + * 1 January 1970, UTC. + */ + Result SetTimeInMs(double aUnixEpoch); + + /** + * Return ICU legacy keywords, such as "gregorian", "islamic", + * "islamic-civil", "hebrew", etc. + */ + static Result, ICUError> + GetLegacyKeywordValuesForLocale(const char* aLocale); + + private: + /** + * Internal function to convert a legacy calendar identifier to the newer + * BCP 47 identifier. + */ + static SpanResult LegacyIdentifierToBcp47(const char* aIdentifier, + int32_t aLength); + + public: + enum class CommonlyUsed : bool { + /** + * Select all possible values, even when not commonly used by a locale. + */ + No, + + /** + * Only select the values which are commonly used by a locale. + */ + Yes, + }; + + using Bcp47IdentifierEnumeration = + Enumeration, Calendar::LegacyIdentifierToBcp47>; + + /** + * Return BCP 47 Unicode locale extension type keywords. + */ + static Result + GetBcp47KeywordValuesForLocale(const char* aLocale, + CommonlyUsed aCommonlyUsed = CommonlyUsed::No); + + ~Calendar(); + + private: + friend class DateIntervalFormat; + UCalendar* GetUCalendar() const { return mCalendar; } + + UCalendar* mCalendar = nullptr; +}; + +} // namespace mozilla::intl + +#endif diff --git a/intl/components/src/Collator.cpp b/intl/components/src/Collator.cpp new file mode 100644 index 0000000000..93052932de --- /dev/null +++ b/intl/components/src/Collator.cpp @@ -0,0 +1,305 @@ +/* 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 +#include +#include "mozilla/intl/Collator.h" + +namespace mozilla::intl { + +Collator::Collator(UCollator* aCollator) : mCollator(aCollator) { + MOZ_ASSERT(aCollator); +} + +Collator::~Collator() { + if (mCollator.GetMut()) { + ucol_close(mCollator.GetMut()); + } +} + +Result, ICUError> Collator::TryCreate(const char* aLocale) { + UErrorCode status = U_ZERO_ERROR; + UCollator* collator = ucol_open(IcuLocale(aLocale), &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + return MakeUnique(collator); +}; + +int32_t Collator::CompareStrings(Span aSource, + Span aTarget) const { + switch (ucol_strcoll(mCollator.GetConst(), aSource.data(), + static_cast(aSource.size()), aTarget.data(), + static_cast(aTarget.size()))) { + case UCOL_LESS: + return -1; + case UCOL_EQUAL: + return 0; + case UCOL_GREATER: + return 1; + } + MOZ_ASSERT_UNREACHABLE("ucol_strcoll returned bad UCollationResult"); + return 0; +} + +int32_t Collator::CompareSortKeys(Span aKey1, + Span aKey2) const { + size_t minLength = std::min(aKey1.Length(), aKey2.Length()); + int32_t tmpResult = strncmp((const char*)aKey1.Elements(), + (const char*)aKey2.Elements(), minLength); + if (tmpResult < 0) { + return -1; + } + if (tmpResult > 0) { + return 1; + } + if (aKey1.Length() > minLength) { + // First string contains second one, so comes later, hence return > 0. + return 1; + } + if (aKey2.Length() > minLength) { + // First string is a substring of second one, so comes earlier, + // hence return < 0. + return -1; + } + return 0; +} + +static UColAttributeValue CaseFirstToICU(Collator::CaseFirst caseFirst) { + switch (caseFirst) { + case Collator::CaseFirst::False: + return UCOL_OFF; + case Collator::CaseFirst::Upper: + return UCOL_UPPER_FIRST; + case Collator::CaseFirst::Lower: + return UCOL_LOWER_FIRST; + } + + MOZ_ASSERT_UNREACHABLE(); + return UCOL_DEFAULT; +} + +// Define this as a macro to work around exposing the UColAttributeValue type to +// the header file. Collation::Feature is private to the class. +#define FEATURE_TO_ICU(featureICU, feature) \ + switch (feature) { \ + case Collator::Feature::On: \ + (featureICU) = UCOL_ON; \ + break; \ + case Collator::Feature::Off: \ + (featureICU) = UCOL_OFF; \ + break; \ + case Collator::Feature::Default: \ + (featureICU) = UCOL_DEFAULT; \ + break; \ + } + +void Collator::SetStrength(Collator::Strength aStrength) { + UColAttributeValue strength; + switch (aStrength) { + case Collator::Strength::Default: + strength = UCOL_DEFAULT_STRENGTH; + break; + case Collator::Strength::Primary: + strength = UCOL_PRIMARY; + break; + case Collator::Strength::Secondary: + strength = UCOL_SECONDARY; + break; + case Collator::Strength::Tertiary: + strength = UCOL_TERTIARY; + break; + case Collator::Strength::Quaternary: + strength = UCOL_QUATERNARY; + break; + case Collator::Strength::Identical: + strength = UCOL_IDENTICAL; + break; + } + + ucol_setStrength(mCollator.GetMut(), strength); +} + +ICUResult Collator::SetCaseLevel(Collator::Feature aFeature) { + UErrorCode status = U_ZERO_ERROR; + UColAttributeValue featureICU; + FEATURE_TO_ICU(featureICU, aFeature); + ucol_setAttribute(mCollator.GetMut(), UCOL_CASE_LEVEL, featureICU, &status); + return ToICUResult(status); +} + +ICUResult Collator::SetAlternateHandling( + Collator::AlternateHandling aAlternateHandling) { + UErrorCode status = U_ZERO_ERROR; + UColAttributeValue handling; + switch (aAlternateHandling) { + case Collator::AlternateHandling::NonIgnorable: + handling = UCOL_NON_IGNORABLE; + break; + case Collator::AlternateHandling::Shifted: + handling = UCOL_SHIFTED; + break; + case Collator::AlternateHandling::Default: + handling = UCOL_DEFAULT; + break; + } + + ucol_setAttribute(mCollator.GetMut(), UCOL_ALTERNATE_HANDLING, handling, + &status); + return ToICUResult(status); +} + +ICUResult Collator::SetNumericCollation(Collator::Feature aFeature) { + UErrorCode status = U_ZERO_ERROR; + UColAttributeValue featureICU; + FEATURE_TO_ICU(featureICU, aFeature); + + ucol_setAttribute(mCollator.GetMut(), UCOL_NUMERIC_COLLATION, featureICU, + &status); + return ToICUResult(status); +} + +ICUResult Collator::SetNormalizationMode(Collator::Feature aFeature) { + UErrorCode status = U_ZERO_ERROR; + UColAttributeValue featureICU; + FEATURE_TO_ICU(featureICU, aFeature); + ucol_setAttribute(mCollator.GetMut(), UCOL_NORMALIZATION_MODE, featureICU, + &status); + return ToICUResult(status); +} + +ICUResult Collator::SetCaseFirst(Collator::CaseFirst aCaseFirst) { + UErrorCode status = U_ZERO_ERROR; + ucol_setAttribute(mCollator.GetMut(), UCOL_CASE_FIRST, + CaseFirstToICU(aCaseFirst), &status); + return ToICUResult(status); +} + +ICUResult Collator::SetOptions(const Options& aOptions, + const Maybe aPrevOptions) { + if (aPrevOptions && + // Check the equality of the previous options. + aPrevOptions->sensitivity == aOptions.sensitivity && + aPrevOptions->caseFirst == aOptions.caseFirst && + aPrevOptions->ignorePunctuation == aOptions.ignorePunctuation && + aPrevOptions->numeric == aOptions.numeric) { + return Ok(); + } + + Collator::Strength strength = Collator::Strength::Default; + Collator::Feature caseLevel = Collator::Feature::Off; + switch (aOptions.sensitivity) { + case Collator::Sensitivity::Base: + strength = Collator::Strength::Primary; + break; + case Collator::Sensitivity::Accent: + strength = Collator::Strength::Secondary; + break; + case Collator::Sensitivity::Case: + caseLevel = Collator::Feature::On; + strength = Collator::Strength::Primary; + break; + case Collator::Sensitivity::Variant: + strength = Collator::Strength::Tertiary; + break; + } + + SetStrength(strength); + + ICUResult result = Ok(); + + // According to the ICU team, UCOL_SHIFTED causes punctuation to be + // ignored. Looking at Unicode Technical Report 35, Unicode Locale Data + // Markup Language, "shifted" causes whitespace and punctuation to be + // ignored - that's a bit more than asked for, but there's no way to get + // less. + result = this->SetAlternateHandling( + aOptions.ignorePunctuation ? Collator::AlternateHandling::Shifted + : Collator::AlternateHandling::Default); + if (result.isErr()) { + return result; + } + + result = SetCaseLevel(caseLevel); + if (result.isErr()) { + return result; + } + + result = SetNumericCollation(aOptions.numeric ? Collator::Feature::On + : Collator::Feature::Off); + if (result.isErr()) { + return result; + } + + // Normalization is always on to meet the canonical equivalence requirement. + result = SetNormalizationMode(Collator::Feature::On); + if (result.isErr()) { + return result; + } + + result = SetCaseFirst(aOptions.caseFirst); + if (result.isErr()) { + return result; + } + return Ok(); +} + +#undef FEATURE_TO_ICU + +Result Collator::GetCaseFirst() const { + UErrorCode status = U_ZERO_ERROR; + UColAttributeValue caseFirst = + ucol_getAttribute(mCollator.GetConst(), UCOL_CASE_FIRST, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + + if (caseFirst == UCOL_OFF) { + return CaseFirst::False; + } + if (caseFirst == UCOL_UPPER_FIRST) { + return CaseFirst::Upper; + } + MOZ_ASSERT(caseFirst == UCOL_LOWER_FIRST); + return CaseFirst::Lower; +} + +/* static */ +Result +Collator::GetBcp47KeywordValuesForLocale(const char* aLocale, + CommonlyUsed aCommonlyUsed) { + UErrorCode status = U_ZERO_ERROR; + UEnumeration* enumeration = ucol_getKeywordValuesForLocale( + "collation", aLocale, static_cast(aCommonlyUsed), &status); + + if (U_SUCCESS(status)) { + return Bcp47ExtEnumeration(enumeration); + } + + return Err(ToICUError(status)); +} + +/* static */ +Result +Collator::GetBcp47KeywordValues() { + UErrorCode status = U_ZERO_ERROR; + UEnumeration* enumeration = ucol_getKeywordValues("collation", &status); + + if (U_SUCCESS(status)) { + return Bcp47ExtEnumeration(enumeration); + } + + return Err(ToICUError(status)); +} + +/* static */ +SpanResult Collator::KeywordValueToBcp47Extension(const char* aKeyword, + int32_t aLength) { + if (aKeyword == nullptr) { + return Err(InternalError{}); + } + return MakeStringSpan(uloc_toUnicodeLocaleType("co", aKeyword)); +} + +} // namespace mozilla::intl diff --git a/intl/components/src/Collator.h b/intl/components/src/Collator.h new file mode 100644 index 0000000000..dcb5a12a4f --- /dev/null +++ b/intl/components/src/Collator.h @@ -0,0 +1,322 @@ +/* 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_Collator_h_ +#define intl_components_Collator_h_ + +#ifndef JS_STANDALONE +# include "gtest/MozGtestFriend.h" +#endif + +#include "unicode/ucol.h" + +#include "mozilla/intl/ICU4CGlue.h" +#include "mozilla/intl/ICUError.h" +#include "mozilla/Result.h" +#include "mozilla/Span.h" + +namespace mozilla::intl { + +class Collator final { + public: + /** + * Construct from a raw UCollator. This is public so that the UniquePtr can + * access it. + */ + explicit Collator(UCollator* aCollator); + + // Do not allow copy as this class owns the ICU resource. Move is not + // currently implemented, but a custom move operator could be created if + // needed. + Collator(const Collator&) = delete; + Collator& operator=(const Collator&) = delete; + + /** + * Attempt to initialize a new collator. + */ + static Result, ICUError> TryCreate(const char* aLocale); + + ~Collator(); + + /** + * Get a sort key with the provided UTF-16 string, and store the sort key into + * the provided buffer of byte array. + * Every sort key ends with 0x00, and the terminating 0x00 byte is counted + * when calculating the length of buffer. For the purpose of other byte + * values, check the "Special Byte Values" document from ICU. + * + * https://icu.unicode.org/design/collation/bytes + */ + template + ICUResult GetSortKey(Span aString, B& aBuffer) const { + return FillBufferWithICUCall( + aBuffer, + [this, aString](uint8_t* target, int32_t length, UErrorCode* status) { + // ucol_getSortKey doesn't use the error code to report + // U_BUFFER_OVERFLOW_ERROR, instead it uses the return value to + // indicate the desired length to store the key. So we update the + // UErrorCode accordingly to let FillBufferWithICUCall resize the + // buffer. + int32_t len = ucol_getSortKey(mCollator.GetConst(), aString.data(), + static_cast(aString.size()), + target, length); + if (len == 0) { + // Returns 0 means there's an internal error. + *status = U_INTERNAL_PROGRAM_ERROR; + } else if (len > length) { + *status = U_BUFFER_OVERFLOW_ERROR; + } else { + *status = U_ZERO_ERROR; + } + return len; + }); + } + + int32_t CompareStrings(Span aSource, + Span aTarget) const; + + int32_t CompareSortKeys(Span aKey1, + Span aKey2) const; + + /** + * Determine how casing affects sorting. These options map to ECMA 402 + * collator options. + * + * https://tc39.es/ecma402/#sec-initializecollator + */ + enum class CaseFirst { + // Sort upper case first. + Upper, + // Sort lower case first. + Lower, + // Orders upper and lower case letters in accordance to their tertiary + // weights. + False, + }; + + /** + * Which differences in the strings should lead to differences in collation + * comparisons. + * + * This setting needs to be ECMA 402 compliant. + * https://tc39.es/ecma402/#sec-collator-comparestrings + */ + enum class Sensitivity { + // Only strings that differ in base letters compare as unequal. + // Examples: a ≠ b, a = á, a = A. + Base, + // Only strings that differ in base letters or accents and other diacritic + // marks compare as unequal. + // Examples: a ≠ b, a ≠ á, a = A. + Accent, + // Only strings that differ in base letters or case compare as unequal. + // Examples: a ≠ b, a = á, a ≠ A. + Case, + // Strings that differ in base letters, accents and other diacritic marks, + // or case compare as unequal. Other differences may also be taken into + // consideration. + // Examples: a ≠ b, a ≠ á, a ≠ A. + Variant, + }; + + /** + * These options map to ECMA 402 collator options. Make sure the defaults map + * to the default initialized values of ECMA 402. + * + * https://tc39.es/ecma402/#sec-initializecollator + */ + struct Options { + Sensitivity sensitivity = Sensitivity::Variant; + CaseFirst caseFirst = CaseFirst::False; + bool ignorePunctuation = false; + bool numeric = false; + }; + + /** + * Change the configuraton of the options. + */ + ICUResult SetOptions(const Options& aOptions, + const Maybe aPrevOptions = Nothing()); + + /** + * Return the case first option of this collator. + */ + Result GetCaseFirst() const; + + /** + * Map keywords to their BCP 47 equivalents. + */ + static SpanResult KeywordValueToBcp47Extension(const char* aKeyword, + int32_t aLength); + + enum class CommonlyUsed : bool { + /** + * Select all possible values, even when not commonly used by a locale. + */ + No, + + /** + * Only select the values which are commonly used by a locale. + */ + Yes, + }; + + using Bcp47ExtEnumeration = + Enumeration, + Collator::KeywordValueToBcp47Extension>; + + /** + * Returns an iterator of collator locale extensions in the preferred order. + * These extensions can be used in BCP 47 locales. For instance this + * iterator could return "phonebk" and could be appled to the German locale + * "de" as "de-co-phonebk" for a phonebook-style collation. + * + * The collation extensions can be found here: + * http://cldr.unicode.org/core-spec/#Key_Type_Definitions + */ + static Result GetBcp47KeywordValuesForLocale( + const char* aLocale, CommonlyUsed aCommonlyUsed = CommonlyUsed::No); + + /** + * Returns an iterator over all possible collator locale extensions. + * These extensions can be used in BCP 47 locales. For instance this + * iterator could return "phonebk" and could be appled to the German locale + * "de" as "de-co-phonebk" for a phonebook-style collation. + * + * The collation extensions can be found here: + * http://cldr.unicode.org/core-spec/#Key_Type_Definitions + */ + static Result GetBcp47KeywordValues(); + + /** + * Returns an iterator over all supported collator locales. + * + * The returned strings are ICU locale identifiers and NOT BCP 47 language + * tags. + * + * Also see . + */ + static auto GetAvailableLocales() { + return AvailableLocalesEnumeration(); + } + + private: + /** + * Toggle features, or use the default setting. + */ + enum class Feature { + // Turn the feature off. + On, + // Turn the feature off. + Off, + // Use the default setting for the feature. + Default, + }; + + /** + * Attribute for handling variable elements. + */ + enum class AlternateHandling { + // Treats all the codepoints with non-ignorable primary weights in the + // same way (default) + NonIgnorable, + // Causes codepoints with primary weights that are equal or below the + // variable top value to be ignored on primary level and moved to the + // quaternary level. + Shifted, + Default, + }; + + /** + * The strength attribute. + * + * The usual strength for most locales (except Japanese) is tertiary. + * + * Quaternary strength is useful when combined with shifted setting for + * alternate handling attribute and for JIS X 4061 collation, when it is used + * to distinguish between Katakana and Hiragana. Otherwise, quaternary level + * is affected only by the number of non-ignorable code points in the string. + * + * Identical strength is rarely useful, as it amounts to codepoints of the NFD + * form of the string. + */ + enum class Strength { + // Primary collation strength. + Primary, + // Secondary collation strength. + Secondary, + // Tertiary collation strength. + Tertiary, + // Quaternary collation strength. + Quaternary, + // Identical collation strength. + Identical, + Default, + }; + + /** + * Configure the Collation::Strength + */ + void SetStrength(Strength strength); + + /** + * Configure Collation::AlternateHandling. + */ + ICUResult SetAlternateHandling(AlternateHandling aAlternateHandling); + + /** + * Controls whether an extra case level (positioned before the third level) is + * generated or not. + * + * Contents of the case level are affected by the value of CaseFirst + * attribute. A simple way to ignore accent differences in a string is to set + * the strength to Primary and enable case level. + */ + ICUResult SetCaseLevel(Feature aFeature); + + /** + * When turned on, this attribute makes substrings of digits sort according to + * their numeric values. + * + * This is a way to get '100' to sort AFTER '2'. Note that the longest digit + * substring that can be treated as a single unit is 254 digits (not counting + * leading zeros). If a digit substring is longer than that, the digits beyond + * the limit will be treated as a separate digit substring. + * + * A "digit" in this sense is a code point with General_Category=Nd, which + * does not include circled numbers, roman numerals, etc. Only a contiguous + * digit substring is considered, that is, non-negative integers without + * separators. There is no support for plus/minus signs, decimals, exponents, + * etc. + */ + ICUResult SetNumericCollation(Feature aFeature); + + /** + * Controls whether the normalization check and necessary normalizations are + * performed. + * + * When off (default), no normalization check is performed. The correctness of + * the result is guaranteed only if the input data is in so-called FCD form + * When set to on, an incremental check is performed to see whether the input + * data is in the FCD form. If the data is not in the FCD form, incremental + * NFD normalization is performed. + */ + ICUResult SetNormalizationMode(Feature aFeature); + + /** + * Configure Collation::CaseFirst. + */ + ICUResult SetCaseFirst(CaseFirst aCaseFirst); + +#ifndef JS_STANDALONE + FRIEND_TEST(IntlCollator, SetAttributesInternal); +#endif + + ICUPointer mCollator = ICUPointer(nullptr); + Maybe mLastStrategy = Nothing(); +}; + +} // namespace mozilla::intl + +#endif diff --git a/intl/components/src/Currency.cpp b/intl/components/src/Currency.cpp new file mode 100644 index 0000000000..4db8e0919c --- /dev/null +++ b/intl/components/src/Currency.cpp @@ -0,0 +1,22 @@ +/* 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 "mozilla/intl/Currency.h" + +#include "unicode/ucurr.h" +#include "unicode/uenum.h" +#include "unicode/utypes.h" + +namespace mozilla::intl { + +Result, ICUError> Currency::GetISOCurrencies() { + UErrorCode status = U_ZERO_ERROR; + UEnumeration* enumeration = ucurr_openISOCurrencies(UCURR_ALL, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + return SpanEnumeration(enumeration); +} + +} // namespace mozilla::intl diff --git a/intl/components/src/Currency.h b/intl/components/src/Currency.h new file mode 100644 index 0000000000..d0f8eb6ee8 --- /dev/null +++ b/intl/components/src/Currency.h @@ -0,0 +1,30 @@ +/* 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_Currency_h_ +#define intl_components_Currency_h_ + +#include "mozilla/intl/ICU4CGlue.h" +#include "mozilla/intl/ICUError.h" +#include "mozilla/Result.h" + +namespace mozilla::intl { + +/** + * This component is a Mozilla-focused API for working with currencies in + * internationalization code. + */ +class Currency final { + public: + Currency() = delete; + + /** + * Returns an enumeration of all supported ISO currency codes. + */ + static Result, ICUError> GetISOCurrencies(); +}; + +} // namespace mozilla::intl + +#endif diff --git a/intl/components/src/DateIntervalFormat.cpp b/intl/components/src/DateIntervalFormat.cpp new file mode 100644 index 0000000000..0097668f8b --- /dev/null +++ b/intl/components/src/DateIntervalFormat.cpp @@ -0,0 +1,266 @@ +/* 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 "DateTimeFormat.h" // for DATE_TIME_FORMAT_REPLACE_SPECIAL_SPACES +#include "DateTimeFormatUtils.h" +#include "ScopedICUObject.h" + +#include "mozilla/intl/Calendar.h" +#include "mozilla/intl/DateIntervalFormat.h" + +namespace mozilla::intl { + +/** + * PartitionDateTimeRangePattern ( dateTimeFormat, x, y ), steps 9-11. + * + * Examine the formatted value to see if any interval span field is present. + * + * https://tc39.es/ecma402/#sec-partitiondatetimerangepattern + */ +static ICUResult DateFieldsPracticallyEqual( + const UFormattedValue* aFormattedValue, bool* aEqual) { + if (!aFormattedValue) { + return Err(ICUError::InternalError); + } + + MOZ_ASSERT(aEqual); + *aEqual = false; + UErrorCode status = U_ZERO_ERROR; + UConstrainedFieldPosition* fpos = ucfpos_open(&status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + ScopedICUObject toCloseFpos(fpos); + + // We're only interested in UFIELD_CATEGORY_DATE_INTERVAL_SPAN fields. + ucfpos_constrainCategory(fpos, UFIELD_CATEGORY_DATE_INTERVAL_SPAN, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + + bool hasSpan = ufmtval_nextPosition(aFormattedValue, fpos, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + + // When no date interval span field was found, both dates are "practically + // equal" per PartitionDateTimeRangePattern. + *aEqual = !hasSpan; + return Ok(); +} + +/* static */ +Result, ICUError> DateIntervalFormat::TryCreate( + Span aLocale, Span aSkeleton, + Span aTimeZone) { + UErrorCode status = U_ZERO_ERROR; + UDateIntervalFormat* dif = + udtitvfmt_open(IcuLocale(aLocale), aSkeleton.data(), + AssertedCast(aSkeleton.size()), aTimeZone.data(), + AssertedCast(aTimeZone.size()), &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + + return UniquePtr(new DateIntervalFormat(dif)); +} + +DateIntervalFormat::~DateIntervalFormat() { + MOZ_ASSERT(mDateIntervalFormat); + udtitvfmt_close(mDateIntervalFormat.GetMut()); +} + +#if DATE_TIME_FORMAT_REPLACE_SPECIAL_SPACES +// We reach inside the UFormattedValue and modify its internal string. (It's +// crucial that this is just an in-place replacement that doesn't alter any +// field positions, etc., ) +static void ReplaceSpecialSpaces(const UFormattedValue* aValue) { + UErrorCode status = U_ZERO_ERROR; + int32_t len; + const UChar* str = ufmtval_getString(aValue, &len, &status); + if (U_FAILURE(status)) { + return; + } + + for (const auto& c : Span(str, len)) { + if (IsSpecialSpace(c)) { + const_cast(c) = ' '; + } + } +} +#endif + +ICUResult DateIntervalFormat::TryFormatCalendar( + const Calendar& aStart, const Calendar& aEnd, + AutoFormattedDateInterval& aFormatted, bool* aPracticallyEqual) const { + MOZ_ASSERT(aFormatted.IsValid()); + + UErrorCode status = U_ZERO_ERROR; + udtitvfmt_formatCalendarToResult(mDateIntervalFormat.GetConst(), + aStart.GetUCalendar(), aEnd.GetUCalendar(), + aFormatted.GetFormatted(), &status); + + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + +#if DATE_TIME_FORMAT_REPLACE_SPECIAL_SPACES + ReplaceSpecialSpaces(aFormatted.Value()); +#endif + + MOZ_TRY(DateFieldsPracticallyEqual(aFormatted.Value(), aPracticallyEqual)); + return Ok(); +} + +ICUResult DateIntervalFormat::TryFormatDateTime( + double aStart, double aEnd, AutoFormattedDateInterval& aFormatted, + bool* aPracticallyEqual) const { + MOZ_ASSERT(aFormatted.IsValid()); + + UErrorCode status = U_ZERO_ERROR; + udtitvfmt_formatToResult(mDateIntervalFormat.GetConst(), aStart, aEnd, + aFormatted.GetFormatted(), &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + +#if DATE_TIME_FORMAT_REPLACE_SPECIAL_SPACES + ReplaceSpecialSpaces(aFormatted.Value()); +#endif + + MOZ_TRY(DateFieldsPracticallyEqual(aFormatted.Value(), aPracticallyEqual)); + return Ok(); +} + +ICUResult DateIntervalFormat::TryFormattedToParts( + const AutoFormattedDateInterval& aFormatted, + DateTimePartVector& aParts) const { + MOZ_ASSERT(aFormatted.IsValid()); + const UFormattedValue* value = aFormatted.Value(); + if (!value) { + return Err(ICUError::InternalError); + } + + size_t lastEndIndex = 0; + auto AppendPart = [&](DateTimePartType type, size_t endIndex, + DateTimePartSource source) { + if (!aParts.emplaceBack(type, endIndex, source)) { + return false; + } + + lastEndIndex = endIndex; + return true; + }; + + UErrorCode status = U_ZERO_ERROR; + UConstrainedFieldPosition* fpos = ucfpos_open(&status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + ScopedICUObject toCloseFpos(fpos); + + size_t categoryEndIndex = 0; + DateTimePartSource source = DateTimePartSource::Shared; + + while (true) { + bool hasMore = ufmtval_nextPosition(value, fpos, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + if (!hasMore) { + break; + } + + int32_t category = ucfpos_getCategory(fpos, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + + int32_t field = ucfpos_getField(fpos, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + + int32_t beginIndexInt, endIndexInt; + ucfpos_getIndexes(fpos, &beginIndexInt, &endIndexInt, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + + MOZ_ASSERT(beginIndexInt <= endIndexInt, + "field iterator returning invalid range"); + + size_t beginIndex = AssertedCast(beginIndexInt); + size_t endIndex = AssertedCast(endIndexInt); + + // Indices are guaranteed to be returned in order (from left to right). + MOZ_ASSERT(lastEndIndex <= beginIndex, + "field iteration didn't return fields in order start to " + "finish as expected"); + + if (category == UFIELD_CATEGORY_DATE_INTERVAL_SPAN) { + // Append any remaining literal parts before changing the source kind. + if (lastEndIndex < beginIndex) { + if (!AppendPart(DateTimePartType::Literal, beginIndex, source)) { + return Err(ICUError::InternalError); + } + } + + // The special field category UFIELD_CATEGORY_DATE_INTERVAL_SPAN has only + // two allowed values (0 or 1), indicating the begin of the start- resp. + // end-date. + MOZ_ASSERT(field == 0 || field == 1, + "span category has unexpected value"); + + source = field == 0 ? DateTimePartSource::StartRange + : DateTimePartSource::EndRange; + categoryEndIndex = endIndex; + continue; + } + + // Ignore categories other than UFIELD_CATEGORY_DATE. + if (category != UFIELD_CATEGORY_DATE) { + continue; + } + + DateTimePartType type = + ConvertUFormatFieldToPartType(static_cast(field)); + if (lastEndIndex < beginIndex) { + if (!AppendPart(DateTimePartType::Literal, beginIndex, source)) { + return Err(ICUError::InternalError); + } + } + + if (!AppendPart(type, endIndex, source)) { + return Err(ICUError::InternalError); + } + + if (endIndex == categoryEndIndex) { + // Append any remaining literal parts before changing the source kind. + if (lastEndIndex < endIndex) { + if (!AppendPart(DateTimePartType::Literal, endIndex, source)) { + return Err(ICUError::InternalError); + } + } + + source = DateTimePartSource::Shared; + } + } + + // Append any final literal. + auto spanResult = aFormatted.ToSpan(); + if (spanResult.isErr()) { + return spanResult.propagateErr(); + } + size_t formattedSize = spanResult.unwrap().size(); + if (lastEndIndex < formattedSize) { + if (!AppendPart(DateTimePartType::Literal, formattedSize, source)) { + return Err(ICUError::InternalError); + } + } + + return Ok(); +} + +} // namespace mozilla::intl diff --git a/intl/components/src/DateIntervalFormat.h b/intl/components/src/DateIntervalFormat.h new file mode 100644 index 0000000000..c4dbce807a --- /dev/null +++ b/intl/components/src/DateIntervalFormat.h @@ -0,0 +1,107 @@ +/* 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_DateIntervalFormat_h_ +#define intl_components_DateIntervalFormat_h_ + +#include "mozilla/intl/Calendar.h" +#include "mozilla/intl/DateTimePart.h" +#include "mozilla/intl/ICU4CGlue.h" +#include "mozilla/intl/ICUError.h" +#include "mozilla/Result.h" +#include "mozilla/Span.h" +#include "mozilla/UniquePtr.h" + +#include "unicode/udateintervalformat.h" +#include "unicode/utypes.h" + +namespace mozilla::intl { +class Calendar; + +using AutoFormattedDateInterval = + AutoFormattedResult; + +/** + * This component is a Mozilla-focused API for the date range formatting + * provided by ICU. This DateIntervalFormat class helps to format the range + * between two date-time values. + * + * https://tc39.es/ecma402/#sec-formatdatetimerange + * https://tc39.es/ecma402/#sec-formatdatetimerangetoparts + */ +class DateIntervalFormat final { + public: + /** + * Create a DateIntervalFormat object from locale, skeleton and time zone. + * The format of skeleton can be found in [1]. + * + * Note: Skeleton will be removed in the future. + * + * [1]: https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns + */ + static Result, ICUError> TryCreate( + Span aLocale, Span aSkeleton, + Span aTimeZone); + + ~DateIntervalFormat(); + + /** + * Format a date-time range between two Calendar objects. + * + * DateIntervalFormat cannot be changed to use a proleptic Gregorian + * calendar, so use this method if the start date is before the Gregorian + * calendar is introduced(October 15, 1582), otherwise use TryFormatDateTime + * instead. + * + * The result will be stored in aFormatted, caller can use + * AutoFormattedDateInterval::ToSpan() to get the formatted string, or pass + * the aFormatted to TryFormattedToParts to get the parts vector. + * + * aPracticallyEqual will be set to true if the date times of the two + * calendars are equal. + */ + ICUResult TryFormatCalendar(const Calendar& aStart, const Calendar& aEnd, + AutoFormattedDateInterval& aFormatted, + bool* aPracticallyEqual) const; + + /** + * Format a date-time range between two Unix epoch times in milliseconds. + * + * The result will be stored in aFormatted, caller can use + * AutoFormattedDateInterval::ToSpan() to get the formatted string, or pass + * the aFormatted to TryFormattedToParts to get the parts vector. + * + * aPracticallyEqual will be set to true if the date times of the two + * Unix epoch times are equal. + */ + ICUResult TryFormatDateTime(double aStart, double aEnd, + AutoFormattedDateInterval& aFormatted, + bool* aPracticallyEqual) const; + + /** + * Convert the formatted DateIntervalFormat into several parts. + * + * The caller get the formatted result from either TryFormatCalendar, or + * TryFormatDateTime methods, and instantiate the DateTimePartVector. This + * method will generate the parts and insert them into the vector. + * + * See: + * https://tc39.es/ecma402/#sec-formatdatetimerangetoparts + */ + ICUResult TryFormattedToParts(const AutoFormattedDateInterval& aFormatted, + DateTimePartVector& aParts) const; + + private: + DateIntervalFormat() = delete; + explicit DateIntervalFormat(UDateIntervalFormat* aDif) + : mDateIntervalFormat(aDif) {} + DateIntervalFormat(const DateIntervalFormat&) = delete; + DateIntervalFormat& operator=(const DateIntervalFormat&) = delete; + + ICUPointer mDateIntervalFormat = + ICUPointer(nullptr); +}; +} // namespace mozilla::intl + +#endif diff --git a/intl/components/src/DateTimeFormat.cpp b/intl/components/src/DateTimeFormat.cpp new file mode 100644 index 0000000000..5a6429e976 --- /dev/null +++ b/intl/components/src/DateTimeFormat.cpp @@ -0,0 +1,1140 @@ +/* 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 + +#include "unicode/ucal.h" +#include "unicode/udat.h" +#include "unicode/udatpg.h" +#include "unicode/ures.h" + +#include "DateTimeFormatUtils.h" +#include "ScopedICUObject.h" + +#include "mozilla/EnumSet.h" +#include "mozilla/intl/Calendar.h" +#include "mozilla/intl/DateTimeFormat.h" +#include "mozilla/intl/DateTimePatternGenerator.h" + +namespace mozilla::intl { + +DateTimeFormat::~DateTimeFormat() { + MOZ_ASSERT(mDateFormat); + udat_close(mDateFormat); +} + +static UDateFormatStyle ToUDateFormatStyle( + Maybe aLength) { + if (!aLength) { + return UDAT_NONE; + } + switch (*aLength) { + case DateTimeFormat::Style::Full: + return UDAT_FULL; + case DateTimeFormat::Style::Long: + return UDAT_LONG; + case DateTimeFormat::Style::Medium: + return UDAT_MEDIUM; + case DateTimeFormat::Style::Short: + return UDAT_SHORT; + } + MOZ_ASSERT_UNREACHABLE(); + // Do not use the default: branch so that the enum is exhaustively checked. + return UDAT_NONE; +} + +/** + * Parse a pattern according to the format specified in + * . + */ +template +class PatternIterator { + CharT* iter; + const CharT* const end; + + public: + explicit PatternIterator(mozilla::Span aPattern) + : iter(aPattern.data()), end(aPattern.data() + aPattern.size()) {} + + CharT* next() { + MOZ_ASSERT(iter != nullptr); + + bool inQuote = false; + while (iter < end) { + CharT* cur = iter++; + if (*cur == '\'') { + inQuote = !inQuote; + } else if (!inQuote) { + return cur; + } + } + + iter = nullptr; + return nullptr; + } +}; + +Maybe DateTimeFormat::HourCycleFromPattern( + Span aPattern) { + PatternIterator iter(aPattern); + while (const auto* ptr = iter.next()) { + switch (*ptr) { + case 'K': + return Some(DateTimeFormat::HourCycle::H11); + case 'h': + return Some(DateTimeFormat::HourCycle::H12); + case 'H': + return Some(DateTimeFormat::HourCycle::H23); + case 'k': + return Some(DateTimeFormat::HourCycle::H24); + } + } + return Nothing(); +} + +static bool IsHour12(DateTimeFormat::HourCycle aHourCycle) { + return aHourCycle == DateTimeFormat::HourCycle::H11 || + aHourCycle == DateTimeFormat::HourCycle::H12; +} + +static char16_t HourSymbol(DateTimeFormat::HourCycle aHourCycle) { + switch (aHourCycle) { + case DateTimeFormat::HourCycle::H11: + return 'K'; + case DateTimeFormat::HourCycle::H12: + return 'h'; + case DateTimeFormat::HourCycle::H23: + return 'H'; + case DateTimeFormat::HourCycle::H24: + return 'k'; + } + MOZ_CRASH("unexpected hour cycle"); +} + +enum class PatternField { Hour, Minute, Second, Other }; + +template +static PatternField ToPatternField(CharT aCh) { + if (aCh == 'K' || aCh == 'h' || aCh == 'H' || aCh == 'k' || aCh == 'j') { + return PatternField::Hour; + } + if (aCh == 'm') { + return PatternField::Minute; + } + if (aCh == 's') { + return PatternField::Second; + } + return PatternField::Other; +} + +/** + * Replaces all hour pattern characters in |patternOrSkeleton| to use the + * matching hour representation for |hourCycle|. + */ +/* static */ +void DateTimeFormat::ReplaceHourSymbol( + mozilla::Span aPatternOrSkeleton, + DateTimeFormat::HourCycle aHourCycle) { + char16_t replacement = HourSymbol(aHourCycle); + PatternIterator iter(aPatternOrSkeleton); + while (auto* ptr = iter.next()) { + auto field = ToPatternField(*ptr); + if (field == PatternField::Hour) { + *ptr = replacement; + } + } +} + +/** + * Find a matching pattern using the requested hour-12 options. + * + * This function is needed to work around the following two issues. + * - https://unicode-org.atlassian.net/browse/ICU-21023 + * - https://unicode-org.atlassian.net/browse/CLDR-13425 + * + * We're currently using a relatively simple workaround, which doesn't give the + * most accurate results. For example: + * + * ``` + * var dtf = new Intl.DateTimeFormat("en", { + * timeZone: "UTC", + * dateStyle: "long", + * timeStyle: "long", + * hourCycle: "h12", + * }); + * print(dtf.format(new Date("2020-01-01T00:00Z"))); + * ``` + * + * Returns the pattern "MMMM d, y 'at' h:mm:ss a z", but when going through + * |DateTimePatternGenerator::GetSkeleton| and then + * |DateTimePatternGenerator::GetBestPattern| to find an equivalent pattern for + * "h23", we'll end up with the pattern "MMMM d, y, HH:mm:ss z", so the + * combinator element " 'at' " was lost in the process. + */ +/* static */ +ICUResult DateTimeFormat::FindPatternWithHourCycle( + DateTimePatternGenerator& aDateTimePatternGenerator, + DateTimeFormat::PatternVector& aPattern, bool aHour12, + DateTimeFormat::SkeletonVector& aSkeleton) { + MOZ_TRY(mozilla::intl::DateTimePatternGenerator::GetSkeleton(aPattern, + aSkeleton)); + + // Input skeletons don't differentiate between "K" and "h" resp. "k" and "H". + DateTimeFormat::ReplaceHourSymbol(aSkeleton, + aHour12 ? DateTimeFormat::HourCycle::H12 + : DateTimeFormat::HourCycle::H23); + + MOZ_TRY(aDateTimePatternGenerator.GetBestPattern(aSkeleton, aPattern)); + + return Ok(); +} + +static auto PatternMatchOptions(mozilla::Span aSkeleton) { + // Values for hour, minute, and second are: + // - absent: 0 + // - numeric: 1 + // - 2-digit: 2 + int32_t hour = 0; + int32_t minute = 0; + int32_t second = 0; + + PatternIterator iter(aSkeleton); + while (const auto* ptr = iter.next()) { + switch (ToPatternField(*ptr)) { + case PatternField::Hour: + MOZ_ASSERT(hour < 2); + hour += 1; + break; + case PatternField::Minute: + MOZ_ASSERT(minute < 2); + minute += 1; + break; + case PatternField::Second: + MOZ_ASSERT(second < 2); + second += 1; + break; + case PatternField::Other: + break; + } + } + + // Adjust the field length when the user requested '2-digit' representation. + // + // We can't just always adjust the field length, because + // 1. The default value for hour, minute, and second fields is 'numeric'. If + // the length is always adjusted, |date.toLocaleTime()| will start to + // return strings like "1:5:9 AM" instead of "1:05:09 AM". + // 2. ICU doesn't support to adjust the field length to 'numeric' in certain + // cases. For example when the locale is "de" (German): + // a. hour='numeric' and minute='2-digit' will return "1:05". + // b. whereas hour='numeric' and minute='numeric' will return "01:05". + // + // Therefore we only support adjusting the field length when the user + // explicitly requested the '2-digit' representation. + + using PatternMatchOption = + mozilla::intl::DateTimePatternGenerator::PatternMatchOption; + mozilla::EnumSet options; + if (hour == 2) { + options += PatternMatchOption::HourField; + } + if (minute == 2) { + options += PatternMatchOption::MinuteField; + } + if (second == 2) { + options += PatternMatchOption::SecondField; + } + return options; +} + +/* static */ +Result, ICUError> DateTimeFormat::TryCreateFromStyle( + Span aLocale, const StyleBag& aStyleBag, + DateTimePatternGenerator* aDateTimePatternGenerator, + Maybe> aTimeZoneOverride) { + auto dateStyle = ToUDateFormatStyle(aStyleBag.date); + auto timeStyle = ToUDateFormatStyle(aStyleBag.time); + + if (dateStyle == UDAT_NONE && timeStyle == UDAT_NONE) { + dateStyle = UDAT_DEFAULT; + timeStyle = UDAT_DEFAULT; + } + + // The time zone is optional. + int32_t tzIDLength = -1; + const UChar* tzID = nullptr; + if (aTimeZoneOverride) { + tzIDLength = static_cast(aTimeZoneOverride->size()); + tzID = aTimeZoneOverride->Elements(); + } + + UErrorCode status = U_ZERO_ERROR; + UDateFormat* dateFormat = + udat_open(timeStyle, dateStyle, IcuLocale(aLocale), tzID, tzIDLength, + /* pattern */ nullptr, /* pattern length */ -1, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + + auto df = UniquePtr(new DateTimeFormat(dateFormat)); + + if (aStyleBag.time && (aStyleBag.hour12 || aStyleBag.hourCycle)) { + // Only adjust the style pattern for time if there is an override. + // Extract the pattern and adjust it for the preferred hour cycle. + DateTimeFormat::PatternVector pattern{}; + + VectorToBufferAdaptor buffer(pattern); + MOZ_TRY(df->GetPattern(buffer)); + + Maybe hcPattern = HourCycleFromPattern(pattern); + DateTimeFormat::SkeletonVector skeleton{}; + + if (hcPattern) { + bool wantHour12 = + aStyleBag.hour12 ? *aStyleBag.hour12 : IsHour12(*aStyleBag.hourCycle); + if (wantHour12 == IsHour12(*hcPattern)) { + // Return the date-time format when its hour-cycle settings match the + // requested options. + if (aStyleBag.hour12 || *hcPattern == *aStyleBag.hourCycle) { + return df; + } + } else { + MOZ_ASSERT(aDateTimePatternGenerator); + MOZ_TRY(DateTimeFormat::FindPatternWithHourCycle( + *aDateTimePatternGenerator, pattern, wantHour12, skeleton)); + } + // Replace the hourCycle, if present, in the pattern string. But only do + // this if no hour12 option is present, because the latter takes + // precedence over hourCycle. + if (!aStyleBag.hour12) { + DateTimeFormat::ReplaceHourSymbol(pattern, *aStyleBag.hourCycle); + } + + auto result = DateTimeFormat::TryCreateFromPattern(aLocale, pattern, + aTimeZoneOverride); + if (result.isErr()) { + return Err(result.unwrapErr()); + } + auto dateTimeFormat = result.unwrap(); + MOZ_TRY(dateTimeFormat->CacheSkeleton(skeleton)); + return dateTimeFormat; + } + } + + return df; +} + +DateTimeFormat::DateTimeFormat(UDateFormat* aDateFormat) { + MOZ_RELEASE_ASSERT(aDateFormat, "Expected aDateFormat to not be a nullptr."); + mDateFormat = aDateFormat; +} + +// A helper to ergonomically push a string onto a string vector. +template +static ICUResult PushString(V& aVec, const char16_t (&aString)[N]) { + if (!aVec.append(aString, N - 1)) { + return Err(ICUError::OutOfMemory); + } + return Ok(); +} + +// A helper to ergonomically push a char onto a string vector. +template +static ICUResult PushChar(V& aVec, char16_t aCh) { + if (!aVec.append(aCh)) { + return Err(ICUError::OutOfMemory); + } + return Ok(); +} + +/** + * Returns an ICU skeleton string representing the specified options. + * http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table + */ +ICUResult ToICUSkeleton(const DateTimeFormat::ComponentsBag& aBag, + DateTimeFormat::SkeletonVector& aSkeleton) { + // Create an ICU skeleton representing the specified aBag. See + if (aBag.weekday) { + switch (*aBag.weekday) { + case DateTimeFormat::Text::Narrow: + MOZ_TRY(PushString(aSkeleton, u"EEEEE")); + break; + case DateTimeFormat::Text::Short: + MOZ_TRY(PushString(aSkeleton, u"E")); + break; + case DateTimeFormat::Text::Long: + MOZ_TRY(PushString(aSkeleton, u"EEEE")); + } + } + if (aBag.era) { + switch (*aBag.era) { + case DateTimeFormat::Text::Narrow: + MOZ_TRY(PushString(aSkeleton, u"GGGGG")); + break; + case DateTimeFormat::Text::Short: + // Use "GGG" instead of "G" to return the same results as other + // browsers. This is exploiting the following ICU bug + // . As soon as that + // bug has been fixed, we can change this back to "G". + // + // In practice the bug only affects "G", so we only apply it for "G" + // and not for other symbols like "B" or "z". + MOZ_TRY(PushString(aSkeleton, u"GGG")); + break; + case DateTimeFormat::Text::Long: + MOZ_TRY(PushString(aSkeleton, u"GGGG")); + break; + } + } + if (aBag.year) { + switch (*aBag.year) { + case DateTimeFormat::Numeric::TwoDigit: + MOZ_TRY(PushString(aSkeleton, u"yy")); + break; + case DateTimeFormat::Numeric::Numeric: + MOZ_TRY(PushString(aSkeleton, u"y")); + break; + } + } + if (aBag.month) { + switch (*aBag.month) { + case DateTimeFormat::Month::TwoDigit: + MOZ_TRY(PushString(aSkeleton, u"MM")); + break; + case DateTimeFormat::Month::Numeric: + MOZ_TRY(PushString(aSkeleton, u"M")); + break; + case DateTimeFormat::Month::Narrow: + MOZ_TRY(PushString(aSkeleton, u"MMMMM")); + break; + case DateTimeFormat::Month::Short: + MOZ_TRY(PushString(aSkeleton, u"MMM")); + break; + case DateTimeFormat::Month::Long: + MOZ_TRY(PushString(aSkeleton, u"MMMM")); + break; + } + } + if (aBag.day) { + switch (*aBag.day) { + case DateTimeFormat::Numeric::TwoDigit: + MOZ_TRY(PushString(aSkeleton, u"dd")); + break; + case DateTimeFormat::Numeric::Numeric: + MOZ_TRY(PushString(aSkeleton, u"d")); + break; + } + } + + // If hour12 and hourCycle are both present, hour12 takes precedence. + char16_t hourSkeletonChar = 'j'; + if (aBag.hour12) { + if (*aBag.hour12) { + hourSkeletonChar = 'h'; + } else { + hourSkeletonChar = 'H'; + } + } else if (aBag.hourCycle) { + switch (*aBag.hourCycle) { + case DateTimeFormat::HourCycle::H11: + case DateTimeFormat::HourCycle::H12: + hourSkeletonChar = 'h'; + break; + case DateTimeFormat::HourCycle::H23: + case DateTimeFormat::HourCycle::H24: + hourSkeletonChar = 'H'; + break; + } + } + if (aBag.hour) { + switch (*aBag.hour) { + case DateTimeFormat::Numeric::TwoDigit: + MOZ_TRY(PushChar(aSkeleton, hourSkeletonChar)); + MOZ_TRY(PushChar(aSkeleton, hourSkeletonChar)); + break; + case DateTimeFormat::Numeric::Numeric: + MOZ_TRY(PushChar(aSkeleton, hourSkeletonChar)); + break; + } + } + // ICU requires that "B" is set after the "j" hour skeleton symbol. + // https://unicode-org.atlassian.net/browse/ICU-20731 + if (aBag.dayPeriod) { + switch (*aBag.dayPeriod) { + case DateTimeFormat::Text::Narrow: + MOZ_TRY(PushString(aSkeleton, u"BBBBB")); + break; + case DateTimeFormat::Text::Short: + MOZ_TRY(PushString(aSkeleton, u"B")); + break; + case DateTimeFormat::Text::Long: + MOZ_TRY(PushString(aSkeleton, u"BBBB")); + break; + } + } + if (aBag.minute) { + switch (*aBag.minute) { + case DateTimeFormat::Numeric::TwoDigit: + MOZ_TRY(PushString(aSkeleton, u"mm")); + break; + case DateTimeFormat::Numeric::Numeric: + MOZ_TRY(PushString(aSkeleton, u"m")); + break; + } + } + if (aBag.second) { + switch (*aBag.second) { + case DateTimeFormat::Numeric::TwoDigit: + MOZ_TRY(PushString(aSkeleton, u"ss")); + break; + case DateTimeFormat::Numeric::Numeric: + MOZ_TRY(PushString(aSkeleton, u"s")); + break; + } + } + if (aBag.fractionalSecondDigits) { + switch (*aBag.fractionalSecondDigits) { + case 1: + MOZ_TRY(PushString(aSkeleton, u"S")); + break; + case 2: + MOZ_TRY(PushString(aSkeleton, u"SS")); + break; + default: + MOZ_TRY(PushString(aSkeleton, u"SSS")); + break; + } + } + if (aBag.timeZoneName) { + switch (*aBag.timeZoneName) { + case DateTimeFormat::TimeZoneName::Short: + MOZ_TRY(PushString(aSkeleton, u"z")); + break; + case DateTimeFormat::TimeZoneName::Long: + MOZ_TRY(PushString(aSkeleton, u"zzzz")); + break; + case DateTimeFormat::TimeZoneName::ShortOffset: + MOZ_TRY(PushString(aSkeleton, u"O")); + break; + case DateTimeFormat::TimeZoneName::LongOffset: + MOZ_TRY(PushString(aSkeleton, u"OOOO")); + break; + case DateTimeFormat::TimeZoneName::ShortGeneric: + MOZ_TRY(PushString(aSkeleton, u"v")); + break; + case DateTimeFormat::TimeZoneName::LongGeneric: + MOZ_TRY(PushString(aSkeleton, u"vvvv")); + break; + } + } + return Ok(); +} + +/* static */ +Result, ICUError> +DateTimeFormat::TryCreateFromComponents( + Span aLocale, const DateTimeFormat::ComponentsBag& aBag, + DateTimePatternGenerator* aDateTimePatternGenerator, + Maybe> aTimeZoneOverride) { + DateTimeFormat::SkeletonVector skeleton; + MOZ_TRY(ToICUSkeleton(aBag, skeleton)); + return TryCreateFromSkeleton(aLocale, skeleton, aDateTimePatternGenerator, + aBag.hourCycle, aTimeZoneOverride); +} + +/* static */ +Result, ICUError> +DateTimeFormat::TryCreateFromPattern( + Span aLocale, Span aPattern, + Maybe> aTimeZoneOverride) { + UErrorCode status = U_ZERO_ERROR; + + // The time zone is optional. + int32_t tzIDLength = -1; + const UChar* tzID = nullptr; + if (aTimeZoneOverride) { + tzIDLength = static_cast(aTimeZoneOverride->size()); + tzID = aTimeZoneOverride->data(); + } + + // Create the date formatter. + UDateFormat* dateFormat = udat_open( + UDAT_PATTERN, UDAT_PATTERN, IcuLocale(aLocale), tzID, tzIDLength, + aPattern.data(), static_cast(aPattern.size()), &status); + + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + + // The DateTimeFormat wrapper will control the life cycle of the ICU + // dateFormat object. + return UniquePtr(new DateTimeFormat(dateFormat)); +} + +/* static */ +Result, ICUError> +DateTimeFormat::TryCreateFromSkeleton( + Span aLocale, Span aSkeleton, + DateTimePatternGenerator* aDateTimePatternGenerator, + Maybe aHourCycle, + Maybe> aTimeZoneOverride) { + if (!aDateTimePatternGenerator) { + return Err(ICUError::InternalError); + } + + // Compute the best pattern for the skeleton. + DateTimeFormat::PatternVector pattern; + auto options = PatternMatchOptions(aSkeleton); + MOZ_TRY( + aDateTimePatternGenerator->GetBestPattern(aSkeleton, pattern, options)); + + if (aHourCycle) { + DateTimeFormat::ReplaceHourSymbol(pattern, *aHourCycle); + } + + auto result = + DateTimeFormat::TryCreateFromPattern(aLocale, pattern, aTimeZoneOverride); + if (result.isErr()) { + return Err(result.unwrapErr()); + } + auto dateTimeFormat = result.unwrap(); + MOZ_TRY(dateTimeFormat->CacheSkeleton(aSkeleton)); + return dateTimeFormat; +} + +ICUResult DateTimeFormat::CacheSkeleton(Span aSkeleton) { + if (mOriginalSkeleton.append(aSkeleton.Elements(), aSkeleton.Length())) { + return Ok(); + } + return Err(ICUError::OutOfMemory); +} + +void DateTimeFormat::SetStartTimeIfGregorian(double aTime) { + UErrorCode status = U_ZERO_ERROR; + UCalendar* cal = const_cast(udat_getCalendar(mDateFormat)); + ucal_setGregorianChange(cal, aTime, &status); + // An error here means the calendar is not Gregorian, and can be ignored. +} + +/* static */ +Result, ICUError> DateTimeFormat::CloneCalendar( + double aUnixEpoch) const { + UErrorCode status = U_ZERO_ERROR; + UCalendar* calendarRaw = ucal_clone(udat_getCalendar(mDateFormat), &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + auto calendar = MakeUnique(calendarRaw); + + MOZ_TRY(calendar->SetTimeInMs(aUnixEpoch)); + + return calendar; +} + +/** + * ICU locale identifier consisting of a language and a region subtag. + */ +class LanguageRegionLocaleId { + // unicode_language_subtag = alpha{2,3} | alpha{5,8} ; + static constexpr size_t LanguageLength = 8; + + // unicode_region_subtag = (alpha{2} | digit{3}) ; + static constexpr size_t RegionLength = 3; + + // Add +1 to account for the separator. + static constexpr size_t LRLength = LanguageLength + RegionLength + 1; + + // Add +1 to zero terminate the string. + char mLocale[LRLength + 1] = {}; + + // Pointer to the start of the region subtag within |locale_|. + char* mRegion = nullptr; + + public: + LanguageRegionLocaleId(Span aLanguage, + Maybe> aRegion); + + const char* languageRegion() const { return mLocale; } + const char* region() const { return mRegion; } +}; + +LanguageRegionLocaleId::LanguageRegionLocaleId( + Span aLanguage, Maybe> aRegion) { + MOZ_RELEASE_ASSERT(aLanguage.Length() <= LanguageLength); + MOZ_RELEASE_ASSERT(!aRegion || aRegion->Length() <= RegionLength); + + size_t languageLength = aLanguage.Length(); + + std::memcpy(mLocale, aLanguage.Elements(), languageLength); + + // ICU locale identifiers are separated by underscores. + mLocale[languageLength] = '_'; + + mRegion = mLocale + languageLength + 1; + if (aRegion) { + std::memcpy(mRegion, aRegion->Elements(), aRegion->Length()); + } else { + // Use "001" (UN M.49 code for the World) as the fallback to match ICU. + std::strcpy(mRegion, "001"); + } +} + +/* static */ +Result +DateTimeFormat::GetAllowedHourCycles(Span aLanguage, + Maybe> aRegion) { + // ICU doesn't expose a public API to retrieve the hour cyles for a locale, so + // we have to reconstruct |DateTimePatternGenerator::getAllowedHourFormats()| + // using the public UResourceBundle API. + // + // The time data format is specified in UTS 35 at [1] and the data itself is + // located at [2]. + // + // [1] https://unicode.org/reports/tr35/tr35-dates.html#Time_Data + // [2] + // https://github.com/unicode-org/cldr/blob/master/common/supplemental/supplementalData.xml + + HourCyclesVector result; + + // Reserve space for the maximum number of hour cycles. This call always + // succeeds because it matches the inline capacity. We can now infallibly + // append all hour cycles to the vector. + MOZ_ALWAYS_TRUE(result.reserve(HourCyclesVector::InlineLength)); + + LanguageRegionLocaleId localeId(aLanguage, aRegion); + + // First open the "supplementalData" resource bundle. + UErrorCode status = U_ZERO_ERROR; + UResourceBundle* res = ures_openDirect(nullptr, "supplementalData", &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + ScopedICUObject closeRes(res); + MOZ_ASSERT(ures_getType(res) == URES_TABLE); + + // Locate "timeDate" within the "supplementalData" resource bundle. + UResourceBundle* timeData = ures_getByKey(res, "timeData", nullptr, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + ScopedICUObject closeTimeData(timeData); + MOZ_ASSERT(ures_getType(timeData) == URES_TABLE); + + // Try to find a matching resource within "timeData". The two possible keys + // into the "timeData" resource bundle are `language_region` and `region`. + // Prefer `language_region` and otherwise fallback to `region`. + UResourceBundle* hclocale = + ures_getByKey(timeData, localeId.languageRegion(), nullptr, &status); + if (status == U_MISSING_RESOURCE_ERROR) { + status = U_ZERO_ERROR; + hclocale = ures_getByKey(timeData, localeId.region(), nullptr, &status); + } + if (status == U_MISSING_RESOURCE_ERROR) { + // Default to "h23" if no resource was found at all. This matches ICU. + result.infallibleAppend(HourCycle::H23); + return result; + } + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + ScopedICUObject closeHcLocale(hclocale); + MOZ_ASSERT(ures_getType(hclocale) == URES_TABLE); + + EnumSet added{}; + + auto addToResult = [&](const UChar* str, int32_t len) { + // An hour cycle strings is one of "K", "h", "H", or "k"; optionally + // followed by the suffix "b" or "B". We ignore the suffix because day + // periods can't be expressed in the "hc" Unicode extension. + MOZ_ASSERT(len == 1 || len == 2); + + // Default to "h23" for unsupported hour cycle strings. + HourCycle hc = HourCycle::H23; + switch (str[0]) { + case 'K': + hc = HourCycle::H11; + break; + case 'h': + hc = HourCycle::H12; + break; + case 'H': + hc = HourCycle::H23; + break; + case 'k': + hc = HourCycle::H24; + break; + } + + // Add each unique hour cycle to the result array. + if (!added.contains(hc)) { + added += hc; + + result.infallibleAppend(hc); + } + }; + + // Determine the preferred hour cycle for the locale. + int32_t len = 0; + const UChar* hc = ures_getStringByKey(hclocale, "preferred", &len, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + addToResult(hc, len); + + // Find any additionally allowed hour cycles of the locale. + UResourceBundle* allowed = + ures_getByKey(hclocale, "allowed", nullptr, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + ScopedICUObject closeAllowed(allowed); + MOZ_ASSERT(ures_getType(allowed) == URES_ARRAY || + ures_getType(allowed) == URES_STRING); + + while (ures_hasNext(allowed)) { + int32_t len = 0; + const UChar* hc = ures_getNextString(allowed, &len, nullptr, &status); + if (U_FAILURE(status)) { + return Err(ToICUError(status)); + } + addToResult(hc, len); + } + + return result; +} + +Result +DateTimeFormat::ResolveComponents() { + // Maps an ICU pattern string to a corresponding set of date-time components + // and their values, and adds properties for these components to the result + // object, which will be returned by the resolvedOptions method. For the + // interpretation of ICU pattern characters, see + // http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table + + DateTimeFormat::PatternVector pattern{}; + VectorToBufferAdaptor buffer(pattern); + MOZ_TRY(GetPattern(buffer)); + + DateTimeFormat::ComponentsBag bag{}; + + using Text = DateTimeFormat::Text; + using HourCycle = DateTimeFormat::HourCycle; + using Numeric = DateTimeFormat::Numeric; + using Month = DateTimeFormat::Month; + + auto text = Text::Long; + auto numeric = Numeric::Numeric; + auto month = Month::Long; + uint8_t fractionalSecondDigits = 0; + + for (size_t i = 0, len = pattern.length(); i < len;) { + char16_t c = pattern[i++]; + if (c == u'\'') { + // Skip past string literals. + while (i < len && pattern[i] != u'\'') { + i++; + } + i++; + continue; + } + + // Count how many times the character is repeated. + size_t count = 1; + while (i < len && pattern[i] == c) { + i++; + count++; + } + + // Determine the enum case of the field. + switch (c) { + // "text" cases + case u'G': + case u'E': + case u'c': + case u'B': + case u'z': + case u'O': + case u'v': + case u'V': + if (count <= 3) { + text = Text::Short; + } else if (count == 4) { + text = Text::Long; + } else { + text = Text::Narrow; + } + break; + // "number" cases + case u'y': + case u'd': + case u'h': + case u'H': + case u'm': + case u's': + case u'k': + case u'K': + if (count == 2) { + numeric = Numeric::TwoDigit; + } else { + numeric = Numeric::Numeric; + } + break; + // "text & number" cases + case u'M': + case u'L': + if (count == 1) { + month = Month::Numeric; + } else if (count == 2) { + month = Month::TwoDigit; + } else if (count == 3) { + month = Month::Short; + } else if (count == 4) { + month = Month::Long; + } else { + month = Month::Narrow; + } + break; + case u'S': + fractionalSecondDigits = count; + break; + default: { + // skip other pattern characters and literal text + } + } + + // Map ICU pattern characters back to the corresponding date-time + // components of DateTimeFormat. See + // http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table + switch (c) { + case u'E': + case u'c': + bag.weekday = Some(text); + break; + case u'G': + bag.era = Some(text); + break; + case u'y': + bag.year = Some(numeric); + break; + case u'M': + case u'L': + bag.month = Some(month); + break; + case u'd': + bag.day = Some(numeric); + break; + case u'B': + bag.dayPeriod = Some(text); + break; + case u'K': + bag.hourCycle = Some(HourCycle::H11); + bag.hour = Some(numeric); + bag.hour12 = Some(true); + break; + case u'h': + bag.hourCycle = Some(HourCycle::H12); + bag.hour = Some(numeric); + bag.hour12 = Some(true); + break; + case u'H': + bag.hourCycle = Some(HourCycle::H23); + bag.hour = Some(numeric); + bag.hour12 = Some(false); + break; + case u'k': + bag.hourCycle = Some(HourCycle::H24); + bag.hour = Some(numeric); + bag.hour12 = Some(false); + break; + case u'm': + bag.minute = Some(numeric); + break; + case u's': + bag.second = Some(numeric); + break; + case u'S': + bag.fractionalSecondDigits = Some(fractionalSecondDigits); + break; + case u'z': + switch (text) { + case Text::Long: + bag.timeZoneName = Some(TimeZoneName::Long); + break; + case Text::Short: + case Text::Narrow: + bag.timeZoneName = Some(TimeZoneName::Short); + break; + } + break; + case u'O': + switch (text) { + case Text::Long: + bag.timeZoneName = Some(TimeZoneName::LongOffset); + break; + case Text::Short: + case Text::Narrow: + bag.timeZoneName = Some(TimeZoneName::ShortOffset); + break; + } + break; + case u'v': + case u'V': + switch (text) { + case Text::Long: + bag.timeZoneName = Some(TimeZoneName::LongGeneric); + break; + case Text::Short: + case Text::Narrow: + bag.timeZoneName = Some(TimeZoneName::ShortGeneric); + break; + } + break; + } + } + return bag; +} + +const char* DateTimeFormat::ToString( + DateTimeFormat::TimeZoneName aTimeZoneName) { + switch (aTimeZoneName) { + case TimeZoneName::Long: + return "long"; + case TimeZoneName::Short: + return "short"; + case TimeZoneName::ShortOffset: + return "shortOffset"; + case TimeZoneName::LongOffset: + return "longOffset"; + case TimeZoneName::ShortGeneric: + return "shortGeneric"; + case TimeZoneName::LongGeneric: + return "longGeneric"; + } + MOZ_CRASH("Unexpected DateTimeFormat::TimeZoneName"); +} + +const char* DateTimeFormat::ToString(DateTimeFormat::Month aMonth) { + switch (aMonth) { + case Month::Numeric: + return "numeric"; + case Month::TwoDigit: + return "2-digit"; + case Month::Long: + return "long"; + case Month::Short: + return "short"; + case Month::Narrow: + return "narrow"; + } + MOZ_CRASH("Unexpected DateTimeFormat::Month"); +} + +const char* DateTimeFormat::ToString(DateTimeFormat::Text aText) { + switch (aText) { + case Text::Long: + return "long"; + case Text::Short: + return "short"; + case Text::Narrow: + return "narrow"; + } + MOZ_CRASH("Unexpected DateTimeFormat::Text"); +} + +const char* DateTimeFormat::ToString(DateTimeFormat::Numeric aNumeric) { + switch (aNumeric) { + case Numeric::Numeric: + return "numeric"; + case Numeric::TwoDigit: + return "2-digit"; + } + MOZ_CRASH("Unexpected DateTimeFormat::Numeric"); +} + +const char* DateTimeFormat::ToString(DateTimeFormat::Style aStyle) { + switch (aStyle) { + case Style::Full: + return "full"; + case Style::Long: + return "long"; + case Style::Medium: + return "medium"; + case Style::Short: + return "short"; + } + MOZ_CRASH("Unexpected DateTimeFormat::Style"); +} + +const char* DateTimeFormat::ToString(DateTimeFormat::HourCycle aHourCycle) { + switch (aHourCycle) { + case HourCycle::H11: + return "h11"; + case HourCycle::H12: + return "h12"; + case HourCycle::H23: + return "h23"; + case HourCycle::H24: + return "h24"; + } + MOZ_CRASH("Unexpected DateTimeFormat::HourCycle"); +} + +ICUResult DateTimeFormat::TryFormatToParts( + UFieldPositionIterator* aFieldPositionIterator, size_t aSpanSize, + DateTimePartVector& aParts) const { + ScopedICUObject toClose( + aFieldPositionIterator); + + size_t lastEndIndex = 0; + auto AppendPart = [&](DateTimePartType type, size_t endIndex) { + // For the part defined in FormatDateTimeToParts, it doesn't have ||Source|| + // property, we store Shared for simplicity, + if (!aParts.emplaceBack(type, endIndex, DateTimePartSource::Shared)) { + return false; + } + + lastEndIndex = endIndex; + return true; + }; + + int32_t fieldInt, beginIndexInt, endIndexInt; + while ((fieldInt = ufieldpositer_next(aFieldPositionIterator, &beginIndexInt, + &endIndexInt)) >= 0) { + MOZ_ASSERT(beginIndexInt <= endIndexInt, + "field iterator returning invalid range"); + + size_t beginIndex = AssertedCast(beginIndexInt); + size_t endIndex = AssertedCast(endIndexInt); + + // Technically this isn't guaranteed. But it appears true in pratice, + // and http://bugs.icu-project.org/trac/ticket/12024 is expected to + // correct the documentation lapse. + MOZ_ASSERT(lastEndIndex <= beginIndex, + "field iteration didn't return fields in order start to " + "finish as expected"); + + DateTimePartType type = + ConvertUFormatFieldToPartType(static_cast(fieldInt)); + if (lastEndIndex < beginIndex) { + if (!AppendPart(DateTimePartType::Literal, beginIndex)) { + return Err(ICUError::InternalError); + } + } + + if (!AppendPart(type, endIndex)) { + return Err(ICUError::InternalError); + } + } + + // Append any final literal. + if (lastEndIndex < aSpanSize) { + if (!AppendPart(DateTimePartType::Literal, aSpanSize)) { + return Err(ICUError::InternalError); + } + } + + return Ok(); +} + +} // namespace mozilla::intl diff --git a/intl/components/src/DateTimeFormat.h b/intl/components/src/DateTimeFormat.h new file mode 100644 index 0000000000..b3e32cd276 --- /dev/null +++ b/intl/components/src/DateTimeFormat.h @@ -0,0 +1,593 @@ +/* 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_DateTimeFormat_h_ +#define intl_components_DateTimeFormat_h_ +#include +#include "unicode/udat.h" + +#include "mozilla/Assertions.h" +#include "mozilla/intl/ICU4CGlue.h" +#include "mozilla/intl/ICUError.h" + +#include "mozilla/intl/DateTimePart.h" +#include "mozilla/intl/DateTimePatternGenerator.h" +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "mozilla/Span.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Utf8.h" +#include "mozilla/Variant.h" +#include "mozilla/Vector.h" + +/* + * To work around webcompat problems caused by Narrow No-Break Space in + * formatted date/time output, where existing code on the web naively + * assumes there will be a normal Space, we replace any occurrences of + * U+202F in the formatted results with U+0020. + * + * The intention is to undo this hack once other major browsers are also + * ready to ship with the updated (ICU72) i18n data that uses NNBSP. + * + * See https://bugzilla.mozilla.org/show_bug.cgi?id=1806042 for details, + * and see DateIntervalFormat.cpp for the other piece of this hack. + */ +#define DATE_TIME_FORMAT_REPLACE_SPECIAL_SPACES 1 + +namespace mozilla::intl { + +#if DATE_TIME_FORMAT_REPLACE_SPECIAL_SPACES +static inline bool IsSpecialSpace(char16_t c) { + // NARROW NO-BREAK SPACE and THIN SPACE + return c == 0x202F || c == 0x2009; +} +#endif + +class Calendar; + +/** + * Intro to mozilla::intl::DateTimeFormat + * ====================================== + * + * This component is a Mozilla-focused API for the date formatting provided by + * ICU. The methods internally call out to ICU4C. This is responsible for and + * owns any resources opened through ICU, through RAII. + * + * The construction of a DateTimeFormat contains the majority of the cost + * of the DateTimeFormat operation. DateTimeFormat::TryFormat should be + * relatively inexpensive after the initial construction. + * + * This class supports creating from Styles (a fixed set of options) and from a + * components bag (a list of components and their lengths). + * + * This API serves to back the ECMA-402 Intl.DateTimeFormat API. + * https://tc39.es/ecma402/#datetimeformat-objects + * + * + * ECMA-402 Intl.DateTimeFormat API and implementation details with ICU + * skeletons and patterns. + * ==================================================================== + * + * Different locales have different ways to display dates using the same + * basic components. For example, en-US might use "Sept. 24, 2012" while + * fr-FR might use "24 Sept. 2012". The intent of Intl.DateTimeFormat is to + * permit production of a format for the locale that best matches the + * set of date-time components and their desired representation as specified + * by the API client. + * + * ICU4C supports specification of date and time formats in three ways: + * + * 1) A style is just one of the identifiers FULL, LONG, MEDIUM, or SHORT. + * The date-time components included in each style and their representation + * are defined by ICU using CLDR locale data (CLDR is the Unicode + * Consortium's Common Locale Data Repository). + * + * 2) A skeleton is a string specifying which date-time components to include, + * and which representations to use for them. For example, "yyyyMMMMdd" + * specifies a year with at least four digits, a full month name, and a + * two-digit day. It does not specify in which order the components appear, + * how they are separated, the localized strings for textual components + * (such as weekday or month), whether the month is in format or + * stand-alone form¹, or the numbering system used for numeric components. + * All that information is filled in by ICU using CLDR locale data. + * ¹ The format form is the one used in formatted strings that include a + * day; the stand-alone form is used when not including days, e.g., in + * calendar headers. The two forms differ at least in some Slavic languages, + * e.g. Russian: "22 марта 2013 г." vs. "Март 2013". + * + * 3) A pattern is a string specifying which date-time components to include, + * in which order, with which separators, in which grammatical case. For + * example, "EEEE, d MMMM y" specifies the full localized weekday name, + * followed by comma and space, followed by the day, followed by space, + * followed by the full month name in format form, followed by space, + * followed by the full year. It + * still does not specify localized strings for textual components and the + * numbering system - these are determined by ICU using CLDR locale data or + * possibly API parameters. + * + * All actual formatting in ICU4C is done with patterns; styles and skeletons + * have to be mapped to patterns before processing. + * + * The options of Intl.DateTimeFormat most closely correspond to ICU skeletons. + * This implementation therefore converts DateTimeFormat options to ICU + * skeletons, and then lets ICU map skeletons to actual ICU patterns. The + * pattern may not directly correspond to what the skeleton requests, as the + * mapper (UDateTimePatternGenerator) is constrained by the available locale + * data for the locale. + * + * An ICU pattern represents the information of the following DateTimeFormat + * internal properties described in the specification, which therefore don't + * exist separately in the implementation: + * - [[weekday]], [[era]], [[year]], [[month]], [[day]], [[hour]], [[minute]], + * [[second]], [[timeZoneName]] + * - [[hour12]] + * - [[hourCycle]] + * - [[hourNo0]] + * When needed for the resolvedOptions method, the resolveICUPattern function + * queries the UDateFormat's internal pattern and then maps the it back to the + * specified properties of the object returned by resolvedOptions. + * + * ICU date-time skeletons and patterns aren't fully documented in the ICU + * documentation (see http://bugs.icu-project.org/trac/ticket/9627). The best + * documentation at this point is in UTR 35: + * http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns + * + * Future support for ICU4X + * ======================== + * This implementation exposes a components bag, and internally handles the + * complexity of working with skeletons and patterns to generate the correct + * results. In the future, if and when we switch to ICU4X, the complexities of + * manipulating patterns will be able to be removed, as ICU4X will directly know + * how to apply the components bag. + */ +class DateTimeFormat final { + public: + /** + * The hour cycle for components. + */ + enum class HourCycle { + H11, + H12, + H23, + H24, + }; + + /** + * The style for dates or times. + */ + enum class Style { + Full, + Long, + Medium, + Short, + }; + + /** + * A bag of options to determine the length of the time and date styles. The + * hour cycle can be overridden. + */ + struct StyleBag { + Maybe