/* 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/DateTimePatternGenerator.h" #include "mozilla/Span.h" #include "TestBuffer.h" 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, Skeleton_enUS_utf8_in) { UniquePtr gen = nullptr; auto dateTimePatternGenerator = DateTimePatternGenerator::TryCreate("en").unwrap(); UniquePtr dtFormat = DateTimeFormat::TryCreateFromSkeleton( "en-US", MakeStringSpan("yMdhhmmss"), dateTimePatternGenerator.get(), Nothing(), Some(MakeStringSpan("GMT+3"))) .unwrap(); TestBuffer buffer; dtFormat->TryFormat(DATE, buffer).unwrap(); ASSERT_TRUE(buffer.verboseMatches("9/23/2002, 08:07:30 PM")); } TEST(IntlDateTimeFormat, Skeleton_enUS_utf16_in) { UniquePtr gen = nullptr; auto dateTimePatternGenerator = DateTimePatternGenerator::TryCreate("en").unwrap(); UniquePtr dtFormat = DateTimeFormat::TryCreateFromSkeleton( "en-US", MakeStringSpan(u"yMdhhmmss"), dateTimePatternGenerator.get(), Nothing(), Some(MakeStringSpan(u"GMT+3"))) .unwrap(); TestBuffer buffer; dtFormat->TryFormat(DATE, buffer).unwrap(); ASSERT_TRUE(buffer.verboseMatches("9/23/2002, 08: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")); } // A utility function to help test the DateTimeFormat::ComponentsBag. [[nodiscard]] bool FormatComponents(TestBuffer& aBuffer, DateTimeFormat::ComponentsBag& aComponents, Span aLocale = "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, "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 = "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")); } } // namespace mozilla::intl