DateIntervalFormat.cpp (10671B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 #include "DateTimeFormatUtils.h" 6 #include "ScopedICUObject.h" 7 8 #include "mozilla/intl/Calendar.h" 9 #include "mozilla/intl/DateIntervalFormat.h" 10 #include "mozilla/intl/DateTimeFormat.h" 11 12 #if !MOZ_SYSTEM_ICU 13 # include "unicode/calendar.h" 14 # include "unicode/datefmt.h" 15 # include "unicode/dtitvfmt.h" 16 #endif 17 18 namespace mozilla::intl { 19 20 /** 21 * PartitionDateTimeRangePattern ( dateTimeFormat, x, y ), steps 9-11. 22 * 23 * Examine the formatted value to see if any interval span field is present. 24 * 25 * https://tc39.es/ecma402/#sec-partitiondatetimerangepattern 26 */ 27 static ICUResult DateFieldsPracticallyEqual( 28 const UFormattedValue* aFormattedValue, bool* aEqual) { 29 if (!aFormattedValue) { 30 return Err(ICUError::InternalError); 31 } 32 33 MOZ_ASSERT(aEqual); 34 *aEqual = false; 35 UErrorCode status = U_ZERO_ERROR; 36 UConstrainedFieldPosition* fpos = ucfpos_open(&status); 37 if (U_FAILURE(status)) { 38 return Err(ToICUError(status)); 39 } 40 ScopedICUObject<UConstrainedFieldPosition, ucfpos_close> toCloseFpos(fpos); 41 42 // We're only interested in UFIELD_CATEGORY_DATE_INTERVAL_SPAN fields. 43 ucfpos_constrainCategory(fpos, UFIELD_CATEGORY_DATE_INTERVAL_SPAN, &status); 44 if (U_FAILURE(status)) { 45 return Err(ToICUError(status)); 46 } 47 48 bool hasSpan = ufmtval_nextPosition(aFormattedValue, fpos, &status); 49 if (U_FAILURE(status)) { 50 return Err(ToICUError(status)); 51 } 52 53 // When no date interval span field was found, both dates are "practically 54 // equal" per PartitionDateTimeRangePattern. 55 *aEqual = !hasSpan; 56 return Ok(); 57 } 58 59 /* static */ 60 Result<UniquePtr<DateIntervalFormat>, ICUError> DateIntervalFormat::TryCreate( 61 Span<const char> aLocale, Span<const char16_t> aSkeleton, 62 Span<const char16_t> aTimeZone) { 63 UErrorCode status = U_ZERO_ERROR; 64 UDateIntervalFormat* dif = 65 udtitvfmt_open(IcuLocale(aLocale), aSkeleton.data(), 66 AssertedCast<int32_t>(aSkeleton.size()), aTimeZone.data(), 67 AssertedCast<int32_t>(aTimeZone.size()), &status); 68 if (U_FAILURE(status)) { 69 return Err(ToICUError(status)); 70 } 71 72 auto result = UniquePtr<DateIntervalFormat>(new DateIntervalFormat(dif)); 73 74 #if !MOZ_SYSTEM_ICU 75 auto* dtif = reinterpret_cast<icu::DateIntervalFormat*>(dif); 76 const icu::Calendar* calendar = dtif->getDateFormat()->getCalendar(); 77 78 auto replacement = CreateCalendarOverride(calendar); 79 if (replacement.isErr()) { 80 return replacement.propagateErr(); 81 } 82 83 if (auto newCalendar = replacement.unwrap()) { 84 dtif->adoptCalendar(newCalendar.release()); 85 } 86 #endif 87 88 return result; 89 } 90 91 DateIntervalFormat::~DateIntervalFormat() { 92 MOZ_ASSERT(mDateIntervalFormat); 93 udtitvfmt_close(mDateIntervalFormat.GetMut()); 94 } 95 96 #if DATE_TIME_FORMAT_REPLACE_SPECIAL_SPACES 97 // We reach inside the UFormattedValue and modify its internal string. (It's 98 // crucial that this is just an in-place replacement that doesn't alter any 99 // field positions, etc., ) 100 static void ReplaceSpecialSpaces(const UFormattedValue* aValue) { 101 UErrorCode status = U_ZERO_ERROR; 102 int32_t len; 103 const UChar* str = ufmtval_getString(aValue, &len, &status); 104 if (U_FAILURE(status)) { 105 return; 106 } 107 108 for (const auto& c : Span(str, len)) { 109 if (IsSpecialSpace(c)) { 110 const_cast<UChar&>(c) = ' '; 111 } 112 } 113 } 114 #endif 115 116 ICUResult DateIntervalFormat::TryFormatCalendar( 117 const Calendar& aStart, const Calendar& aEnd, 118 AutoFormattedDateInterval& aFormatted, bool* aPracticallyEqual) const { 119 MOZ_ASSERT(aFormatted.IsValid()); 120 121 UErrorCode status = U_ZERO_ERROR; 122 udtitvfmt_formatCalendarToResult(mDateIntervalFormat.GetConst(), 123 aStart.GetUCalendar(), aEnd.GetUCalendar(), 124 aFormatted.GetFormatted(), &status); 125 126 if (U_FAILURE(status)) { 127 return Err(ToICUError(status)); 128 } 129 130 #if DATE_TIME_FORMAT_REPLACE_SPECIAL_SPACES 131 ReplaceSpecialSpaces(aFormatted.Value()); 132 #endif 133 134 MOZ_TRY(DateFieldsPracticallyEqual(aFormatted.Value(), aPracticallyEqual)); 135 return Ok(); 136 } 137 138 ICUResult DateIntervalFormat::TryFormatDateTime( 139 double aStart, double aEnd, AutoFormattedDateInterval& aFormatted, 140 bool* aPracticallyEqual) const { 141 MOZ_ASSERT(aFormatted.IsValid()); 142 143 UErrorCode status = U_ZERO_ERROR; 144 udtitvfmt_formatToResult(mDateIntervalFormat.GetConst(), aStart, aEnd, 145 aFormatted.GetFormatted(), &status); 146 if (U_FAILURE(status)) { 147 return Err(ToICUError(status)); 148 } 149 150 #if DATE_TIME_FORMAT_REPLACE_SPECIAL_SPACES 151 ReplaceSpecialSpaces(aFormatted.Value()); 152 #endif 153 154 MOZ_TRY(DateFieldsPracticallyEqual(aFormatted.Value(), aPracticallyEqual)); 155 return Ok(); 156 } 157 158 ICUResult DateIntervalFormat::TryFormatDateTime( 159 double aStart, double aEnd, const DateTimeFormat* aDateTimeFormat, 160 AutoFormattedDateInterval& aFormatted, bool* aPracticallyEqual) const { 161 #if MOZ_SYSTEM_ICU 162 // We can't access the calendar used by UDateIntervalFormat to change it to a 163 // proleptic Gregorian calendar. Instead we need to call a different formatter 164 // function which accepts UCalendar instead of UDate. 165 // But creating new UCalendar objects for each call is slow, so when we can 166 // ensure that the input dates are later than the Gregorian change date, 167 // directly call the formatter functions taking UDate. 168 169 constexpr int32_t msPerDay = 24 * 60 * 60 * 1000; 170 171 // The Gregorian change date "1582-10-15T00:00:00.000Z". 172 constexpr double GregorianChangeDate = -12219292800000.0; 173 174 // Add a full day to account for time zone offsets. 175 constexpr double GregorianChangeDatePlusOneDay = 176 GregorianChangeDate + msPerDay; 177 178 if (aStart < GregorianChangeDatePlusOneDay || 179 aEnd < GregorianChangeDatePlusOneDay) { 180 // Create calendar objects for the start and end date by cloning the date 181 // formatter calendar. The date formatter calendar already has the correct 182 // time zone set and was changed to use a proleptic Gregorian calendar. 183 auto startCal = aDateTimeFormat->CloneCalendar(aStart); 184 if (startCal.isErr()) { 185 return startCal.propagateErr(); 186 } 187 188 auto endCal = aDateTimeFormat->CloneCalendar(aEnd); 189 if (endCal.isErr()) { 190 return endCal.propagateErr(); 191 } 192 193 return TryFormatCalendar(*startCal.unwrap(), *endCal.unwrap(), aFormatted, 194 aPracticallyEqual); 195 } 196 #endif 197 198 // The common fast path which doesn't require creating calendar objects. 199 return TryFormatDateTime(aStart, aEnd, aFormatted, aPracticallyEqual); 200 } 201 202 ICUResult DateIntervalFormat::TryFormattedToParts( 203 const AutoFormattedDateInterval& aFormatted, 204 DateTimePartVector& aParts) const { 205 MOZ_ASSERT(aFormatted.IsValid()); 206 const UFormattedValue* value = aFormatted.Value(); 207 if (!value) { 208 return Err(ICUError::InternalError); 209 } 210 211 size_t lastEndIndex = 0; 212 auto AppendPart = [&](DateTimePartType type, size_t endIndex, 213 DateTimePartSource source) { 214 if (!aParts.emplaceBack(type, endIndex, source)) { 215 return false; 216 } 217 218 lastEndIndex = endIndex; 219 return true; 220 }; 221 222 UErrorCode status = U_ZERO_ERROR; 223 UConstrainedFieldPosition* fpos = ucfpos_open(&status); 224 if (U_FAILURE(status)) { 225 return Err(ToICUError(status)); 226 } 227 ScopedICUObject<UConstrainedFieldPosition, ucfpos_close> toCloseFpos(fpos); 228 229 size_t categoryEndIndex = 0; 230 DateTimePartSource source = DateTimePartSource::Shared; 231 232 while (true) { 233 bool hasMore = ufmtval_nextPosition(value, fpos, &status); 234 if (U_FAILURE(status)) { 235 return Err(ToICUError(status)); 236 } 237 if (!hasMore) { 238 break; 239 } 240 241 int32_t category = ucfpos_getCategory(fpos, &status); 242 if (U_FAILURE(status)) { 243 return Err(ToICUError(status)); 244 } 245 246 int32_t field = ucfpos_getField(fpos, &status); 247 if (U_FAILURE(status)) { 248 return Err(ToICUError(status)); 249 } 250 251 int32_t beginIndexInt, endIndexInt; 252 ucfpos_getIndexes(fpos, &beginIndexInt, &endIndexInt, &status); 253 if (U_FAILURE(status)) { 254 return Err(ToICUError(status)); 255 } 256 257 MOZ_ASSERT(beginIndexInt <= endIndexInt, 258 "field iterator returning invalid range"); 259 260 size_t beginIndex = AssertedCast<size_t>(beginIndexInt); 261 size_t endIndex = AssertedCast<size_t>(endIndexInt); 262 263 // Indices are guaranteed to be returned in order (from left to right). 264 MOZ_ASSERT(lastEndIndex <= beginIndex, 265 "field iteration didn't return fields in order start to " 266 "finish as expected"); 267 268 if (category == UFIELD_CATEGORY_DATE_INTERVAL_SPAN) { 269 // Append any remaining literal parts before changing the source kind. 270 if (lastEndIndex < beginIndex) { 271 if (!AppendPart(DateTimePartType::Literal, beginIndex, source)) { 272 return Err(ICUError::InternalError); 273 } 274 } 275 276 // The special field category UFIELD_CATEGORY_DATE_INTERVAL_SPAN has only 277 // two allowed values (0 or 1), indicating the begin of the start- resp. 278 // end-date. 279 MOZ_ASSERT(field == 0 || field == 1, 280 "span category has unexpected value"); 281 282 source = field == 0 ? DateTimePartSource::StartRange 283 : DateTimePartSource::EndRange; 284 categoryEndIndex = endIndex; 285 continue; 286 } 287 288 // Ignore categories other than UFIELD_CATEGORY_DATE. 289 if (category != UFIELD_CATEGORY_DATE) { 290 continue; 291 } 292 293 DateTimePartType type = 294 ConvertUFormatFieldToPartType(static_cast<UDateFormatField>(field)); 295 if (lastEndIndex < beginIndex) { 296 if (!AppendPart(DateTimePartType::Literal, beginIndex, source)) { 297 return Err(ICUError::InternalError); 298 } 299 } 300 301 if (!AppendPart(type, endIndex, source)) { 302 return Err(ICUError::InternalError); 303 } 304 305 if (endIndex == categoryEndIndex) { 306 // Append any remaining literal parts before changing the source kind. 307 if (lastEndIndex < endIndex) { 308 if (!AppendPart(DateTimePartType::Literal, endIndex, source)) { 309 return Err(ICUError::InternalError); 310 } 311 } 312 313 source = DateTimePartSource::Shared; 314 } 315 } 316 317 // Append any final literal. 318 auto spanResult = aFormatted.ToSpan(); 319 if (spanResult.isErr()) { 320 return spanResult.propagateErr(); 321 } 322 size_t formattedSize = spanResult.unwrap().size(); 323 if (lastEndIndex < formattedSize) { 324 if (!AppendPart(DateTimePartType::Literal, formattedSize, source)) { 325 return Err(ICUError::InternalError); 326 } 327 } 328 329 return Ok(); 330 } 331 332 } // namespace mozilla::intl