/* 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