Skip to content

Handle ICU failures better #625

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1959,7 +1959,11 @@ internal final class _CalendarICU: _CalendarProtocol, @unchecked Sendable {
@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *)
extension Calendar {
private func symbols(for key: UDateFormatSymbolType) -> [String] {
ICUDateFormatter.cachedFormatter(for: self).symbols(for: key)
guard let fmt = ICUDateFormatter.cachedFormatter(for: self) else {
return []
}

return fmt.symbols(for: key)
}

/// A list of eras in this calendar, localized to the Calendar's `locale`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,13 +259,19 @@ private func localizedParens(locale: Locale) -> (String, String) {
let ulocdata = locale.identifier.withCString {
ulocdata_open($0, &status)
}
try! status.checkSuccess()
defer { ulocdata_close(ulocdata) }

guard status.checkSuccessAndLogError("ulocdata_open failed.") else {
return (" (", ")")
}

let exemplars = ulocdata_getExemplarSet(ulocdata, nil, 0, .punctuation, &status)
try! status.checkSuccess()
defer { uset_close(exemplars) }

guard status.checkSuccessAndLogError("ulocdata_getExemplarSet failed.") else {
return (" (", ")")
}

let fullwidthLeftParenUTF32 = 0x0000FF08 as Int32
let containsFullWidth = uset_contains(exemplars!, fullwidthLeftParenUTF32).boolValue

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ extension Date {
// MARK: - FormatStyle conformance

public func format(_ v: Range<Date>) -> String {
ICUDateIntervalFormatter.formatter(for: self).string(from: v)
guard let formatter = ICUDateIntervalFormatter.formatter(for: self), let result = formatter.string(from: v) else {
return "\(v.lowerBound.description) - \(v.upperBound.description)"
}
return result
}

public func locale(_ locale: Locale) -> Self {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ extension Date {
}

public func format(_ value: Date) -> String {
return ICUDateFormatter.cachedFormatter(for: self).format(value) ?? value.description
guard let fm = ICUDateFormatter.cachedFormatter(for: self), let result = fm.format(value) else {
return value.description
}

return result
}

public func locale(_ locale: Locale) -> Date.VerbatimFormatStyle {
Expand Down Expand Up @@ -101,16 +105,11 @@ extension Date.VerbatimFormatStyle {
}

public func format(_ value: Date) -> AttributedString {
let fm = ICUDateFormatter.cachedFormatter(for: base)

var result: AttributedString
if let (str, attributes) = fm.attributedFormat(value) {
result = str._attributedStringFromPositions(attributes)
} else {
result = AttributedString(value.description)
guard let fm = ICUDateFormatter.cachedFormatter(for: base), let (str, attributes) = fm.attributedFormat(value) else {
return AttributedString(value.description)
}

return result
return str._attributedStringFromPositions(attributes)
}

public func locale(_ locale: Locale) -> Self {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,22 +306,19 @@ extension Date {

/// Returns an attributed string with `AttributeScopes.FoundationAttributes.DateFieldAttribute`
public func format(_ value: Date) -> AttributedString {
let fm: ICUDateFormatter
let fm: ICUDateFormatter?
switch innerStyle {
case .formatStyle(let formatStyle):
fm = ICUDateFormatter.cachedFormatter(for: formatStyle)
case .verbatimFormatStyle(let verbatimFormatStyle):
fm = ICUDateFormatter.cachedFormatter(for: verbatimFormatStyle)
}

var result: AttributedString
if let (str, attributes) = fm.attributedFormat(value) {
result = str._attributedStringFromPositions(attributes)
} else {
result = AttributedString("")
guard let fm, let (str, attributes) = fm.attributedFormat(value) else {
return AttributedString("")
}

return result
return str._attributedStringFromPositions(attributes)
}

public func locale(_ locale: Locale) -> Self {
Expand Down Expand Up @@ -376,16 +373,10 @@ extension Date.FormatStyle {
}

public func format(_ value: Date) -> AttributedString {
let fm = ICUDateFormatter.cachedFormatter(for: base)

var result: AttributedString
if let (str, attributes) = fm.attributedFormat(value) {
result = str._attributedStringFromPositions(attributes)
} else {
result = AttributedString("")
guard let fm = ICUDateFormatter.cachedFormatter(for: base), let (str, attributes) = fm.attributedFormat(value) else {
return AttributedString("")
}

return result
return str._attributedStringFromPositions(attributes)
}

public func locale(_ locale: Locale) -> Self {
Expand Down Expand Up @@ -691,8 +682,10 @@ extension Date.FormatStyle.Attributed {
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
extension Date.FormatStyle : FormatStyle {
public func format(_ value: Date) -> String {
let fm = ICUDateFormatter.cachedFormatter(for: self)
return fm.format(value) ?? ""
guard let fm = ICUDateFormatter.cachedFormatter(for: self), let result = fm.format(value) else {
return ""
}
return result
}

public func locale(_ locale: Locale) -> Self {
Expand All @@ -707,7 +700,10 @@ extension Date.FormatStyle : FormatStyle {
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
extension Date.FormatStyle : ParseStrategy {
public func parse(_ value: String) throws -> Date {
let fm = ICUDateFormatter.cachedFormatter(for: self)
guard let fm = ICUDateFormatter.cachedFormatter(for: self) else {
throw CocoaError(CocoaError.formatting, userInfo: [ NSDebugDescriptionErrorKey: "Error creating icu date formatter" ])
}

guard let date = fm.parse(value) else {
throw parseError(value, exampleFormattedString: fm.format(Date.now))
}
Expand Down Expand Up @@ -1093,7 +1089,10 @@ extension Date.FormatStyle : CustomConsumingRegexComponent {
guard index < bounds.upperBound else {
return nil
}
return ICUDateFormatter.cachedFormatter(for: self).parse(input, in: index..<bounds.upperBound)
guard let fmt = ICUDateFormatter.cachedFormatter(for: self) else {
return nil
}
return fmt.parse(input, in: index..<bounds.upperBound)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ extension Date {
self.twoDigitStartDate = twoDigitStartDate
}

private var formatter: ICUDateFormatter {
private var formatter: ICUDateFormatter? {
let dateFormatInfo = ICUDateFormatter.DateFormatInfo(localeIdentifier: locale?.identifier, timeZoneIdentifier: timeZone.identifier, calendarIdentifier: calendar.identifier, firstWeekday: calendar.firstWeekday, minimumDaysInFirstWeek: calendar.minimumDaysInFirstWeek, capitalizationContext: .unknown, pattern: format, parseLenient: isLenient, parseTwoDigitStartDate: twoDigitStartDate)
return ICUDateFormatter.cachedFormatter(for: dateFormatInfo)
}
Expand All @@ -77,6 +77,10 @@ extension Date.ParseStrategy : ParseStrategy {
/// - Throws: Throws `NSFormattingError` if the string cannot be parsed.
/// - Returns: A `Date` represented by `value`.
public func parse(_ value: String) throws -> Date {
guard let formatter = self.formatter else {
throw CocoaError(CocoaError.formatting, userInfo: [ NSDebugDescriptionErrorKey: "Error creating icu date formatter" ])
}

guard let date = formatter.parse(value) else {
throw parseError(value, exampleFormattedString: formatter.format(Date.now))
}
Expand All @@ -101,7 +105,10 @@ extension Date.ParseStrategy : CustomConsumingRegexComponent {
guard index < bounds.upperBound else {
return nil
}
return formatter.parse(input, in: index..<bounds.upperBound)
guard let fmt = self.formatter else {
return nil
}
return fmt.parse(input, in: index..<bounds.upperBound)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ final class ICUDateFormatter {
var udateFormat: UnsafeMutablePointer<UDateFormat?>
var lenientParsing: Bool

private init(localeIdentifier: String, timeZoneIdentifier: String, calendarIdentifier: Calendar.Identifier, firstWeekday: Int, minimumDaysInFirstWeek: Int, capitalizationContext: FormatStyleCapitalizationContext, pattern: String, twoDigitStartDate: Date, lenientParsing: Bool) {
private init?(localeIdentifier: String, timeZoneIdentifier: String, calendarIdentifier: Calendar.Identifier, firstWeekday: Int, minimumDaysInFirstWeek: Int, capitalizationContext: FormatStyleCapitalizationContext, pattern: String, twoDigitStartDate: Date, lenientParsing: Bool) {
self.lenientParsing = lenientParsing

// We failed to construct a locale with the given calendar; fall back to locale's identifier
Expand All @@ -39,34 +39,44 @@ final class ICUDateFormatter {
let pt = Array(pattern.utf16)

var status = U_ZERO_ERROR
udateFormat = udat_open(UDAT_PATTERN, UDAT_PATTERN, localeIdentifierWithCalendar, tz, Int32(tz.count), pt, Int32(pt.count), &status)!
try! status.checkSuccess()
let udat = udat_open(UDAT_PATTERN, UDAT_PATTERN, localeIdentifierWithCalendar, tz, Int32(tz.count), pt, Int32(pt.count), &status)

guard status.checkSuccessAndLogError("udat_open failed."), let udat else {
if (udat != nil) {
udat_close(udat)
}
return nil
}

udateFormat = udat

udat_setContext(udateFormat, capitalizationContext.icuContext, &status)
try! status.checkSuccess()
_ = status.checkSuccessAndLogError("udat_setContext failed.")

if lenientParsing {
udat_setLenient(udateFormat, UBool.true)
} else {
udat_setLenient(udateFormat, UBool.false)

udat_setBooleanAttribute(udateFormat, UDAT_PARSE_ALLOW_WHITESPACE, UBool.false, &status)
try! status.checkSuccess()
_ = status.checkSuccessAndLogError("Cannot set UDAT_PARSE_ALLOW_WHITESPACE.")

udat_setBooleanAttribute(udateFormat, UDAT_PARSE_ALLOW_NUMERIC, UBool.false, &status)
try! status.checkSuccess()
_ = status.checkSuccessAndLogError("Cannot set UDAT_PARSE_ALLOW_NUMERIC.")

udat_setBooleanAttribute(udateFormat, UDAT_PARSE_PARTIAL_LITERAL_MATCH, UBool.false, &status)
try! status.checkSuccess()
_ = status.checkSuccessAndLogError("Cannot set UDAT_PARSE_PARTIAL_LITERAL_MATCH.")

udat_setBooleanAttribute(udateFormat, UDAT_PARSE_MULTIPLE_PATTERNS_FOR_MATCH, UBool.false, &status)
try! status.checkSuccess()
_ = status.checkSuccessAndLogError("Cannot set UDAT_PARSE_MULTIPLE_PATTERNS_FOR_MATCH.")
}

let udatCalendar = udat_getCalendar(udateFormat)
let ucal = ucal_clone(udatCalendar, &status)
defer { ucal_close(ucal) }
try! status.checkSuccess()
guard status.checkSuccessAndLogError("ucal_clone failed."), let ucal else {
return
}

ucal_clear(ucal)
ucal_setAttribute(ucal, .firstDayOfWeek, Int32(firstWeekday))
Expand Down Expand Up @@ -227,10 +237,6 @@ final class ICUDateFormatter {
var parseLenient: Bool
var parseTwoDigitStartDate: Date

func createICUDateFormatter() -> ICUDateFormatter {
ICUDateFormatter(localeIdentifier: localeIdentifier, timeZoneIdentifier: timeZoneIdentifier, calendarIdentifier: calendarIdentifier, firstWeekday: firstWeekday, minimumDaysInFirstWeek: minimumDaysInFirstWeek, capitalizationContext: capitalizationContext, pattern: pattern, twoDigitStartDate: parseTwoDigitStartDate, lenientParsing: parseLenient)
}

init(localeIdentifier: String?, timeZoneIdentifier: String, calendarIdentifier: Calendar.Identifier, firstWeekday: Int, minimumDaysInFirstWeek: Int, capitalizationContext: FormatStyleCapitalizationContext, pattern: String, parseLenient: Bool = true, parseTwoDigitStartDate: Date = Date(timeIntervalSince1970: 0)) {
if let localeIdentifier {
self.localeIdentifier = localeIdentifier
Expand All @@ -250,11 +256,13 @@ final class ICUDateFormatter {
}
}

static let formatterCache = FormatterCache<DateFormatInfo, ICUDateFormatter>()
static let formatterCache = FormatterCache<DateFormatInfo, ICUDateFormatter?>()
static var patternCache = LockedState<[PatternCacheKey : String]>(initialState: [:])

static func cachedFormatter(for dateFormatInfo: DateFormatInfo) -> ICUDateFormatter {
return Self.formatterCache.formatter(for: dateFormatInfo, creator: dateFormatInfo.createICUDateFormatter)
static func cachedFormatter(for dateFormatInfo: DateFormatInfo) -> ICUDateFormatter? {
return Self.formatterCache.formatter(for: dateFormatInfo) {
ICUDateFormatter(localeIdentifier: dateFormatInfo.localeIdentifier, timeZoneIdentifier: dateFormatInfo.timeZoneIdentifier, calendarIdentifier: dateFormatInfo.calendarIdentifier, firstWeekday: dateFormatInfo.firstWeekday, minimumDaysInFirstWeek: dateFormatInfo.minimumDaysInFirstWeek, capitalizationContext: dateFormatInfo.capitalizationContext, pattern: dateFormatInfo.pattern, twoDigitStartDate: dateFormatInfo.parseTwoDigitStartDate, lenientParsing: dateFormatInfo.parseLenient)
}
}

struct PatternCacheKey : Hashable {
Expand All @@ -264,17 +272,17 @@ final class ICUDateFormatter {
var datePatternOverride: String?
}

static func cachedFormatter(for format: Date.FormatStyle) -> ICUDateFormatter {
return cachedFormatter(for: .init(format))
static func cachedFormatter(for format: Date.FormatStyle) -> ICUDateFormatter? {
cachedFormatter(for: .init(format))
}

static func cachedFormatter(for format: Date.VerbatimFormatStyle) -> ICUDateFormatter {
return cachedFormatter(for: .init(format))
static func cachedFormatter(for format: Date.VerbatimFormatStyle) -> ICUDateFormatter? {
cachedFormatter(for: .init(format))
}

// Returns a formatter to retrieve localized calendar symbols
static func cachedFormatter(for calendar: Calendar) -> ICUDateFormatter {
return cachedFormatter(for: .init(calendar))
static func cachedFormatter(for calendar: Calendar) -> ICUDateFormatter? {
cachedFormatter(for: .init(calendar))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ final class ICUDateIntervalFormatter {
let dateTemplate: String
}

internal static let cache = FormatterCache<Signature, ICUDateIntervalFormatter>()
internal static let cache = FormatterCache<Signature, ICUDateIntervalFormatter?>()

let uformatter: OpaquePointer // UDateIntervalFormat

private init(signature: Signature) {
private init?(signature: Signature) {
var comps = signature.localeComponents
comps.calendar = signature.calendarIdentifier
let id = comps.icuIdentifier
Expand All @@ -37,36 +37,39 @@ final class ICUDateIntervalFormatter {
let dateTemplate16 = Array(signature.dateTemplate.utf16)

var status = U_ZERO_ERROR
uformatter = tz16.withUnsafeBufferPointer { tz in
let formatter = tz16.withUnsafeBufferPointer { tz in
dateTemplate16.withUnsafeBufferPointer { template in
udtitvfmt_open(id, template.baseAddress, Int32(template.count), tz.baseAddress, Int32(tz.count), &status)
}
}

try! status.checkSuccess()
guard status.checkSuccessAndLogError("udtitvfmt_open failed."), let formatter else {
if (formatter != nil) {
udtitvfmt_close(formatter)
}
return nil
}

udtitvfmt_setAttribute(uformatter, UDTITVFMT_MINIMIZE_TYPE, UDTITVFMT_MINIMIZE_NONE, &status)
uformatter = formatter

try! status.checkSuccess()
udtitvfmt_setAttribute(uformatter, UDTITVFMT_MINIMIZE_TYPE, UDTITVFMT_MINIMIZE_NONE, &status)
_ = status.checkSuccessAndLogError("udtitvfmt_setAttribute failed.")
}

deinit {
udtitvfmt_close(uformatter)
}

func string(from: Range<Date>) -> String {
func string(from: Range<Date>) -> String? {
let fromUDate = from.lowerBound.udate
let toUDate = from.upperBound.udate

let result = _withResizingUCharBuffer { buffer, size, status in
return _withResizingUCharBuffer { buffer, size, status in
udtitvfmt_format(uformatter, fromUDate, toUDate, buffer, size, nil /* position */, &status)
}

if let result { return result }
return ""
}

internal static func formatter(for style: Date.IntervalFormatStyle) -> ICUDateIntervalFormatter {
internal static func formatter(for style: Date.IntervalFormatStyle) -> ICUDateIntervalFormatter? {
var template = style.symbols.formatterTemplate(overridingDayPeriodWithLocale: style.locale)

if template.isEmpty {
Expand All @@ -80,9 +83,8 @@ final class ICUDateIntervalFormatter {
let comps = Locale.Components(locale: style.locale)
let signature = Signature(localeComponents: comps, calendarIdentifier: style.calendar.identifier, timeZoneIdentifier: style.timeZone.identifier, dateTemplate: template)

let formatter = Self.cache.formatter(for: signature) {
return Self.cache.formatter(for: signature) {
ICUDateIntervalFormatter(signature: signature)
}
return formatter
}
}
Loading