From 357befc6ef4e53098056a3a61be3a8b775b5fb22 Mon Sep 17 00:00:00 2001 From: Seyed Mojtaba Hosseini Zeidabadi Date: Tue, 11 Nov 2025 20:09:57 +0330 Subject: [PATCH 1/3] Fix: attributed string typos (#1588) * fix: discontiguous attributed string * fix: not divisable * fix: item delimiter * fix: contiguous sequence --- .../AttributedString/AttributedString+IndexValidity.swift | 4 ++-- .../AttributedString/AttributedString+_InternalRuns.swift | 2 +- .../AttributedString/FoundationAttributes.swift | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/FoundationEssentials/AttributedString/AttributedString+IndexValidity.swift b/Sources/FoundationEssentials/AttributedString/AttributedString+IndexValidity.swift index 8b617aaab..13b604fb0 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributedString+IndexValidity.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributedString+IndexValidity.swift @@ -56,7 +56,7 @@ extension AttributedString.Index { /// Indicates whether the index is valid for use with the provided discontiguous attributed string. /// - Parameter text: A discontiguous attributed string used to validate the index. - /// - Returns: `true` when the index is valid for use with the provided discontiguous attributed string; otherwise, false. An index is valid if it is both within the bounds of the discontigous attributed string and was produced from the provided string without any intermediate mutations. + /// - Returns: `true` when the index is valid for use with the provided discontiguous attributed string; otherwise, false. An index is valid if it is both within the bounds of the discontiguous attributed string and was produced from the provided string without any intermediate mutations. public func isValid(within text: DiscontiguousAttributedSubstring) -> Bool { self._version == text._guts.version && text._indices.contains(self._value) @@ -100,7 +100,7 @@ extension RangeSet { } /// Indicates whether the range set is valid for use with the provided discontiguous attributed string. - /// - Parameter text: A discontigious attributed string used to validate the range set. + /// - Parameter text: A discontiguous attributed string used to validate the range set. /// - Returns: `true` when the range set is valid for use with the provided discontiguous attributed string; otherwise, false. A range set is valid if each of its ranges are valid in the discontiguous attributed string. public func isValid(within text: DiscontiguousAttributedSubstring) -> Bool { self.ranges.allSatisfy { diff --git a/Sources/FoundationEssentials/AttributedString/AttributedString+_InternalRuns.swift b/Sources/FoundationEssentials/AttributedString/AttributedString+_InternalRuns.swift index 0c0e3c6fe..0fa265828 100644 --- a/Sources/FoundationEssentials/AttributedString/AttributedString+_InternalRuns.swift +++ b/Sources/FoundationEssentials/AttributedString/AttributedString+_InternalRuns.swift @@ -44,7 +44,7 @@ extension AttributedString { extension AttributedString._InternalRuns { /// A metric that assigns each run a size of 1; i.e., the metric corresponding to run offsets. /// - /// Runs are not divisable under this metric. + /// Runs are not divisible under this metric. struct RunMetric: RopeMetric { typealias Element = AttributedString._InternalRun typealias Summary = Element.Summary diff --git a/Sources/FoundationEssentials/AttributedString/FoundationAttributes.swift b/Sources/FoundationEssentials/AttributedString/FoundationAttributes.swift index 2799e00bd..7af1c3c46 100644 --- a/Sources/FoundationEssentials/AttributedString/FoundationAttributes.swift +++ b/Sources/FoundationEssentials/AttributedString/FoundationAttributes.swift @@ -470,7 +470,7 @@ extension AttributeScopes.FoundationAttributes { guard text.count == 1 else { throw DecodingError.dataCorrupted(DecodingError.Context( codingPath: container.codingPath, - debugDescription: "List item delimeter encoded value must contain only one character / grapheme cluster" + debugDescription: "List item delimiter encoded value must contain only one character / grapheme cluster" )) } return text[text.startIndex] @@ -902,7 +902,7 @@ extension AttributedString { /// The writing direction of a piece of text. /// /// Writing direction defines the base direction in which bidirectional text - /// lays out its directional runs. A directional run is a contigous sequence + /// lays out its directional runs. A directional run is a contiguous sequence /// of characters that all have the same effective directionality, which can /// be determined using the Unicode BiDi algorithm. The ``leftToRight`` /// writing direction puts the directional run that is placed first in the From b7be5355eb8a3198055c5f5ff0c0c17f7111fc81 Mon Sep 17 00:00:00 2001 From: Tina L <49205802+itingliu@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:32:10 -0800 Subject: [PATCH 2/3] Revert "128-bit Date (#3475)" (#3549) (#1581) Reverting this from main for now to figure out test breakages --- Sources/FoundationEssentials/CMakeLists.txt | 1 - .../Calendar/Calendar_Gregorian.swift | 29 +-- .../Calendar/Calendar_Recurrence.swift | 17 +- Sources/FoundationEssentials/Date.swift | 97 ++------- .../FoundationEssentials/DateInterval.swift | 49 ++--- .../FoundationEssentials/DoubleDouble.swift | 186 ------------------ .../GregorianCalendarTests.swift | 27 +-- .../CalendarTests.swift | 2 +- .../ParseStrategy+RegexComponentTests.swift | 5 +- ...ianCalendarInternationalizationTests.swift | 76 +++---- 10 files changed, 97 insertions(+), 392 deletions(-) delete mode 100644 Sources/FoundationEssentials/DoubleDouble.swift diff --git a/Sources/FoundationEssentials/CMakeLists.txt b/Sources/FoundationEssentials/CMakeLists.txt index 00d41bbfe..d009cde11 100644 --- a/Sources/FoundationEssentials/CMakeLists.txt +++ b/Sources/FoundationEssentials/CMakeLists.txt @@ -21,7 +21,6 @@ add_library(FoundationEssentials ComparisonResult.swift Date.swift DateInterval.swift - DoubleDouble.swift FoundationEssentials.swift IndexPath.swift LockedState.swift diff --git a/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift b/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift index 4f5cc4885..b7809b9b0 100644 --- a/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift +++ b/Sources/FoundationEssentials/Calendar/Calendar_Gregorian.swift @@ -1448,39 +1448,30 @@ internal final class _CalendarGregorian: _CalendarProtocol, @unchecked Sendable } func dateInterval(of component: Calendar.Component, for date: Date) -> DateInterval? { - let approximateTime = date._time.head + let time = date.timeIntervalSinceReferenceDate var effectiveUnit = component switch effectiveUnit { case .calendar, .timeZone, .isLeapMonth, .isRepeatedDay: return nil case .era: - if approximateTime < -63113904000.0 { + if time < -63113904000.0 { return DateInterval(start: Date(timeIntervalSinceReferenceDate: -63113904000.0 - inf_ti), duration: inf_ti) } else { return DateInterval(start: Date(timeIntervalSinceReferenceDate: -63113904000.0), duration: inf_ti) } case .hour: - // Local hours may not be aligned to GMT hours, so we have to apply - // the time zone adjustment before rounding down, then unapply it. - let offset = Double(timeZone.secondsFromGMT(for: date)) - let start = ((date._time + offset)/3600).floor() * 3600 - offset - return DateInterval( - start: Date(start), - duration: 3600 - ) + let ti = Double(timeZone.secondsFromGMT(for: date)) + var fixedTime = time + ti // compute local time + fixedTime = floor(fixedTime / 3600.0) * 3600.0 + fixedTime = fixedTime - ti // compute GMT + return DateInterval(start: Date(timeIntervalSinceReferenceDate: fixedTime), duration: 3600.0) case .minute: - return DateInterval( - start: Date((date._time/60).floor() * 60), - duration: 60 - ) + return DateInterval(start: Date(timeIntervalSinceReferenceDate: floor(time / 60.0) * 60.0), duration: 60.0) case .second: - return DateInterval(start: Date(date._time.floor()), duration: 1) + return DateInterval(start: Date(timeIntervalSinceReferenceDate: floor(time)), duration: 1.0) case .nanosecond: - return DateInterval( - start: Date((date._time*1e9).floor() / 1e9), - duration: 1e-9 - ) + return DateInterval(start: Date(timeIntervalSinceReferenceDate: floor(time * 1.0e+9) * 1.0e-9), duration: 1.0e-9) case .year, .yearForWeekOfYear, .quarter, .month, .day, .dayOfYear, .weekOfMonth, .weekOfYear: // Continue to below break diff --git a/Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift b/Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift index 8d9a32abc..dd8740236 100644 --- a/Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift +++ b/Sources/FoundationEssentials/Calendar/Calendar_Recurrence.swift @@ -93,8 +93,8 @@ extension Calendar { /// value is used as a lower bound for ``nextBaseRecurrenceDate()``. let rangeLowerBound: Date? - /// The start date's fractional seconds component - let fractionalSeconds: TimeInterval + /// The start date's nanoseconds component + let startDateNanoseconds: TimeInterval /// How many occurrences have been found so far var resultsFound = 0 @@ -131,10 +131,7 @@ extension Calendar { } self.recurrence = recurrence - // round start down to whole seconds, set aside fraction. - let wholeSeconds = start._time.floor() - fractionalSeconds = (start._time - wholeSeconds).head - self.start = Date(wholeSeconds) + self.start = start self.range = range let frequency = recurrence.frequency @@ -236,7 +233,9 @@ extension Calendar { case .monthly: [.second, .minute, .hour, .day] case .yearly: [.second, .minute, .hour, .day, .month, .isLeapMonth] } - var componentsForEnumerating = recurrence.calendar._dateComponents(components, from: start) + var componentsForEnumerating = recurrence.calendar._dateComponents(components, from: start) + + startDateNanoseconds = start.timeIntervalSinceReferenceDate.truncatingRemainder(dividingBy: 1) let expansionChangesDay = dayOfYearAction == .expand || dayOfMonthAction == .expand || weekAction == .expand || weekdayAction == .expand let expansionChangesMonth = dayOfYearAction == .expand || monthAction == .expand || weekAction == .expand @@ -428,11 +427,11 @@ extension Calendar { recurrence._limitTimeComponent(.second, dates: &dates, anchor: anchor) } - if fractionalSeconds != 0 { + if startDateNanoseconds > 0 { // `_dates(startingAfter:)` above returns whole-second dates, // so we need to restore the nanoseconds value present in the original start date. for idx in dates.indices { - dates[idx] += fractionalSeconds + dates[idx] += startDateNanoseconds } } dates = dates.filter { $0 >= self.start } diff --git a/Sources/FoundationEssentials/Date.swift b/Sources/FoundationEssentials/Date.swift index 2fefd10fc..df240ed2f 100644 --- a/Sources/FoundationEssentials/Date.swift +++ b/Sources/FoundationEssentials/Date.swift @@ -34,64 +34,12 @@ public typealias TimeInterval = Double A `Date` is independent of a particular calendar or time zone. To represent a `Date` to a user, you must interpret it in the context of a `Calendar`. */ @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) -public struct Date: Comparable, Hashable, Equatable, Sendable { - /* Date is internally represented as a sum of two Doubles. - - Previously Date was backed by a single Double measuring time since - Jan 1 2001 in seconds. Because Double's precision is non-uniform, this - means that times within a hundred days of the epoch are represented - with approximately nanosecond precision, but as you get farther away - from that date the precision decreases. For times close to the time - at which this comment was written, accuracy has been reduced to about - 100ns. - - The obvious thing would be to adopt an integer-based representation - similar to C's timespec (32b nanoseconds, 64b seconds) or Swift's - Duration (128b attoseconds). These representations suffer from a few - difficulties: - - - Existing API on Date takes and produces `TimeInterval` (aka Double). - Making Date use an integer representation internally would mean that - existing users of the public API would suddently start getting - different results for computations that were previously exact; even - though we could add new API, and the overall system would be more - precise, this would be a surprisingly subtle change for users to - navigate. - - - We have been told that some software interprets the raw bytes of Date - as a Double for the purposes of fast serialization. These packages - formally violate Foundation's API boundaries, but that doesn't help - users of those packages who would abruptly be broken by switching to - an integer representation. - - Using DoubleDouble instead navigates these problems fairly elegantly. - - - Because DoubleDouble is still a floating-point type, it still suffers - from non-uniform precision. However, because DoubleDouble is so - fantastically precise, it can represent dates out to ±2.5 quadrillion - years at ~nanosecond or better precision, so in practice this won't - be much of an issue. - - - Existing API on Date will produce exactly the same result as it did - previously in cases where those results were exact, minimizing - surprises. In cases where the existing API was not exact, it will - produce much more accurate results, even if users do not adopt new - API, because its internal calculations are now more precise. - - - Software that (incorrectly) interprets the raw bytes of Date as a - Double will get at least as accurate of a value as it did previously - (and often a more accurate value). */ - internal var _time: DoubleDouble - - internal init(_ time: DoubleDouble) { - self._time = time - } -} +public struct Date : Comparable, Hashable, Equatable, Sendable { + + internal var _time : TimeInterval -@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) -extension Date { /// The number of seconds from 1 January 1970 to the reference date, 1 January 2001. - public static let timeIntervalBetween1970AndReferenceDate: TimeInterval = 978307200.0 + public static let timeIntervalBetween1970AndReferenceDate : TimeInterval = 978307200.0 /// The number of seconds from 1 January 1601 to the reference date, 1 January 2001. internal static let timeIntervalBetween1601AndReferenceDate: TimeInterval = 12622780800.0 @@ -103,23 +51,17 @@ extension Date { /// Returns a `Date` initialized to the current date and time. public init() { - _time = .init(uncheckedHead: Self.getCurrentAbsoluteTime(), tail: 0) + _time = Self.getCurrentAbsoluteTime() } /// Returns a `Date` initialized relative to the current date and time by a given number of seconds. public init(timeIntervalSinceNow: TimeInterval) { - self.init(.sum( - Self.getCurrentAbsoluteTime(), - timeIntervalSinceNow - )) + self.init(timeIntervalSinceReferenceDate: timeIntervalSinceNow + Self.getCurrentAbsoluteTime()) } /// Returns a `Date` initialized relative to 00:00:00 UTC on 1 January 1970 by a given number of seconds. public init(timeIntervalSince1970: TimeInterval) { - self.init(.sum( - timeIntervalSince1970, - -Date.timeIntervalBetween1970AndReferenceDate - )) + self.init(timeIntervalSinceReferenceDate: timeIntervalSince1970 - Date.timeIntervalBetween1970AndReferenceDate) } /** @@ -129,12 +71,12 @@ extension Date { - Parameter date: The reference date. */ public init(timeInterval: TimeInterval, since date: Date) { - self.init(date._time + timeInterval) + self.init(timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate + timeInterval) } /// Returns a `Date` initialized relative to 00:00:00 UTC on 1 January 2001 by a given number of seconds. public init(timeIntervalSinceReferenceDate ti: TimeInterval) { - _time = .init(uncheckedHead: ti, tail: 0) + _time = ti } /** @@ -143,7 +85,7 @@ extension Date { This property's value is negative if the date object is earlier than the system's absolute reference date (00:00:00 UTC on 1 January 2001). */ public var timeIntervalSinceReferenceDate: TimeInterval { - return _time.head + return _time } /** @@ -158,7 +100,7 @@ extension Date { - SeeAlso: `timeIntervalSinceReferenceDate` */ public func timeIntervalSince(_ date: Date) -> TimeInterval { - return (self._time - date._time).head + return self.timeIntervalSinceReferenceDate - date.timeIntervalSinceReferenceDate } /** @@ -231,9 +173,9 @@ extension Date { /// Compare two `Date` values. public func compare(_ other: Date) -> ComparisonResult { - if _time < other._time { + if _time < other.timeIntervalSinceReferenceDate { return .orderedAscending - } else if _time > other._time { + } else if _time > other.timeIntervalSinceReferenceDate { return .orderedDescending } else { return .orderedSame @@ -242,27 +184,27 @@ extension Date { /// Returns true if the two `Date` values represent the same point in time. public static func ==(lhs: Date, rhs: Date) -> Bool { - return lhs._time == rhs._time + return lhs.timeIntervalSinceReferenceDate == rhs.timeIntervalSinceReferenceDate } /// Returns true if the left hand `Date` is earlier in time than the right hand `Date`. public static func <(lhs: Date, rhs: Date) -> Bool { - return lhs._time < rhs._time + return lhs.timeIntervalSinceReferenceDate < rhs.timeIntervalSinceReferenceDate } /// Returns true if the left hand `Date` is later in time than the right hand `Date`. public static func >(lhs: Date, rhs: Date) -> Bool { - return lhs._time > rhs._time + return lhs.timeIntervalSinceReferenceDate > rhs.timeIntervalSinceReferenceDate } /// Returns a `Date` with a specified amount of time added to it. public static func +(lhs: Date, rhs: TimeInterval) -> Date { - return Date(lhs._time + rhs) + return Date(timeIntervalSinceReferenceDate: lhs.timeIntervalSinceReferenceDate + rhs) } /// Returns a `Date` with a specified amount of time subtracted from it. public static func -(lhs: Date, rhs: TimeInterval) -> Date { - return Date(lhs._time - rhs) + return Date(timeIntervalSinceReferenceDate: lhs.timeIntervalSinceReferenceDate - rhs) } /// Add a `TimeInterval` to a `Date`. @@ -278,6 +220,7 @@ extension Date { public static func -=(lhs: inout Date, rhs: TimeInterval) { lhs = lhs - rhs } + } @available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *) @@ -395,7 +338,7 @@ extension Date : ReferenceConvertible, _ObjectiveCBridgeable { @_semantics("convertToObjectiveC") public func _bridgeToObjectiveC() -> NSDate { - return NSDate(timeIntervalSinceReferenceDate: _time.head) + return NSDate(timeIntervalSinceReferenceDate: _time) } public static func _forceBridgeFromObjectiveC(_ x: NSDate, result: inout Date?) { diff --git a/Sources/FoundationEssentials/DateInterval.swift b/Sources/FoundationEssentials/DateInterval.swift index 835157808..04d2c55d1 100644 --- a/Sources/FoundationEssentials/DateInterval.swift +++ b/Sources/FoundationEssentials/DateInterval.swift @@ -12,37 +12,30 @@ /// DateInterval represents a closed date interval in the form of [startDate, endDate]. It is possible for the start and end dates to be the same with a duration of 0. DateInterval does not support reverse intervals i.e. intervals where the duration is less than 0 and the end date occurs earlier in time than the start date. @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) -public struct DateInterval: Comparable, Hashable, Codable, Sendable { +public struct DateInterval : Comparable, Hashable, Codable, Sendable { /// The start date. - public var start: Date - - /// Underlying storage for `duration` - internal var _duration: DoubleDouble + public var start : Date /// The end date. /// /// - precondition: `end >= start` - public var end: Date { + public var end : Date { get { - return Date(start._time + _duration) + return start + duration } set { precondition(newValue >= start, "Reverse intervals are not allowed") - _duration = (newValue._time - start._time) + duration = newValue.timeIntervalSinceReferenceDate - start.timeIntervalSinceReferenceDate } } - - /// The duration + + /// The duration. /// /// - precondition: `duration >= 0` - public var duration: TimeInterval { - get { - _duration.head - } - set { + public var duration : TimeInterval { + willSet { precondition(newValue >= 0, "Negative durations are not allowed") - _duration = DoubleDouble(uncheckedHead: newValue, tail: 0) } } @@ -50,7 +43,7 @@ public struct DateInterval: Comparable, Hashable, Codable, Sendable { public init() { let d = Date() start = d - _duration = .zero + duration = 0 } /// Initialize a `DateInterval` with the specified start and end date. @@ -59,7 +52,7 @@ public struct DateInterval: Comparable, Hashable, Codable, Sendable { public init(start: Date, end: Date) { precondition(end >= start, "Reverse intervals are not allowed") self.start = start - _duration = end._time - start._time + duration = end.timeIntervalSince(start) } /// Initialize a `DateInterval` with the specified start date and duration. @@ -68,7 +61,7 @@ public struct DateInterval: Comparable, Hashable, Codable, Sendable { public init(start: Date, duration: TimeInterval) { precondition(duration >= 0, "Negative durations are not allowed") self.start = start - _duration = DoubleDouble(uncheckedHead: duration, tail: 0) + self.duration = duration } /** @@ -169,24 +162,6 @@ public struct DateInterval: Comparable, Hashable, Codable, Sendable { public static func <(lhs: DateInterval, rhs: DateInterval) -> Bool { return lhs.compare(rhs) == .orderedAscending } - - enum CodingKeys: String, CodingKey { - case start = "start" - case duration = "duration" - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let start = try container.decode(Date.self, forKey: .start) - let duration = try container.decode(TimeInterval.self, forKey: .duration) - self.init(start: start, duration: duration) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(start, forKey: .start) - try container.encode(duration, forKey: .duration) - } } @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) diff --git a/Sources/FoundationEssentials/DoubleDouble.swift b/Sources/FoundationEssentials/DoubleDouble.swift deleted file mode 100644 index b8d58ddf4..000000000 --- a/Sources/FoundationEssentials/DoubleDouble.swift +++ /dev/null @@ -1,186 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// - -/// A numeric type that uses two Double values as its representation, providing -/// about 106 bits of precision with the same exponent range as Double. -/// -/// This type conforms to AdditiveArithmetic, Hashable and Comparable, but does -/// not conform to FloatingPoint or Numeric; it implements only the API surface -/// that is necessary to serve as an internal implementation detail of Date. -internal struct DoubleDouble { - - private let storage: (Double, Double) - - /// A double-double value constructed by specifying the head and tail. - /// - /// This is an unchecked operation because it does not enforce the - /// invariant that head + tail == head in release builds, which is - /// necessary for subsequent arithmetic operations to behave correctly. - @_transparent - init(uncheckedHead head: Double, tail: Double) { - assert(!head.isFinite || head + tail == head) - storage = (head, tail) - } - - /// The high-order Double. - /// - /// This property does not have a setter because `head` should pretty much - /// never be set independently of `tail`, so as to maintain the invariant - /// that `head + tail == head`. You can use `init(uncheckedHead:tail:)` - /// to directly construct DoubleDouble values, which will enforce the - /// invariant in debug builds. - @_transparent - var head: Double { storage.0 } - - /// The low-order Double. - /// - /// This property does not have a setter because `tail` should pretty much - /// never be set independently of `head`, so as to maintain the invariant - /// that `head + tail == head`. You can use `init(uncheckedHead:tail:)` - /// to directly construct DoubleDouble values, which will enforce the - /// invariant in debug builds. - @_transparent - var tail: Double { storage.1 } - - /// `a + b` represented as a normalized DoubleDouble. - /// - /// Computed via the [2Sum algorithm](https://en.wikipedia.org/wiki/2Sum). - @inlinable - static func sum(_ a: Double, _ b: Double) -> DoubleDouble { - let head = a + b - let x = head - b - let y = head - x - let tail = (a - x) + (b - y) - return DoubleDouble(uncheckedHead: head, tail: tail) - } - - /// `a + b` represented as a normalized DoubleDouble. - /// - /// Computed via the [Fast2Sum algorithm](https://en.wikipedia.org/wiki/2Sum). - /// - /// - Precondition: - /// `large` and `small` must be such that `sum(large:small:)` - /// produces the same result as `sum(_:_:)` would. A sufficient condition - /// is that `|large| >= |small|`, but this is not necessary, so we do not - /// enforce it via an assert. Instead this function asserts that the result - /// is the same as that produced by `sum(_:_:)` in Debug builds. This is - /// unchecked in Release. - @inlinable - static func sum(large a: Double, small b: Double) -> DoubleDouble { - let head = a + b - let tail = a - head + b - let result = DoubleDouble(uncheckedHead: head, tail: tail) - assert(!head.isFinite || result == sum(a, b)) - return result - } - - /// `a * b` represented as a normalized DoubleDouble. - @inlinable - static func product(_ a: Double, _ b: Double) -> DoubleDouble { - let head = a * b - let tail = (-head).addingProduct(a, b) - return DoubleDouble(uncheckedHead: head, tail: tail) - } -} - -extension DoubleDouble: Comparable { - @_transparent - static func ==(a: Self, b: Self) -> Bool { - a.head == b.head && a.tail == b.tail - } - - @_transparent - static func <(a: Self, b: Self) -> Bool { - a.head < b.head || a.head == b.head && a.tail < b.tail - } -} - -extension DoubleDouble: Hashable { - @_transparent - func hash(into hasher: inout Hasher) { - hasher.combine(head) - hasher.combine(tail) - } -} - -extension DoubleDouble: AdditiveArithmetic { - @inlinable - static var zero: DoubleDouble { - Self(uncheckedHead: 0, tail: 0) - } - - @inlinable - static func +(a: DoubleDouble, b: DoubleDouble) -> DoubleDouble { - let heads = sum(a.head, b.head) - let tails = sum(a.tail, b.tail) - let first = sum(large: heads.head, small: heads.tail + tails.head) - return sum(large: first.head, small: first.tail + tails.tail) - } - - /// Equivalent to `a + DoubleDouble(uncheckedHead: b, tail: 0)` but - /// computed more efficiently. - @inlinable - static func +(a: DoubleDouble, b: Double) -> DoubleDouble { - let heads = sum(a.head, b) - let first = sum(large: heads.head, small: heads.tail + a.tail) - return sum(large: first.head, small: first.tail) - } - - @inlinable - prefix static func -(a: DoubleDouble) -> DoubleDouble { - DoubleDouble(uncheckedHead: -a.head, tail: -a.tail) - } - - @inlinable - static func -(a: DoubleDouble, b: DoubleDouble) -> DoubleDouble { - a + (-b) - } - - /// Equivalent to `a - DoubleDouble(uncheckedHead: b, tail: 0)` but - /// computed more efficiently. - @inlinable - static func -(a: DoubleDouble, b: Double) -> DoubleDouble { - a + (-b) - } -} - -extension DoubleDouble { - @inlinable - static func *(a: DoubleDouble, b: Double) -> DoubleDouble { - let tmp = product(a.head, b) - return DoubleDouble( - uncheckedHead: tmp.head, - tail: tmp.tail.addingProduct(a.tail, b) - ) - } - - @inlinable - static func /(a: DoubleDouble, b: Double) -> DoubleDouble { - let head = a.head/b - let residual = a.head.addingProduct(-head, b) + a.tail - return .sum(large: head, small: residual/b) - } -} - -extension DoubleDouble { - // This value rounded down to an integer. - @inlinable - func floor() -> DoubleDouble { - let approx = head.rounded(.down) - // If head was already an integer, round tail down and renormalize. - if approx == head { - return .sum(large: head, small: tail.rounded(.down)) - } - // Head was not an integer; we can simply discard tail. - return DoubleDouble(uncheckedHead: approx, tail: 0) - } -} diff --git a/Tests/FoundationEssentialsTests/GregorianCalendarTests.swift b/Tests/FoundationEssentialsTests/GregorianCalendarTests.swift index 31377c924..9e3d3c05f 100644 --- a/Tests/FoundationEssentialsTests/GregorianCalendarTests.swift +++ b/Tests/FoundationEssentialsTests/GregorianCalendarTests.swift @@ -277,13 +277,7 @@ private struct GregorianCalendarTests { func test(addField field: Calendar.Component, value: Int, to addingToDate: Date, wrap: Bool, expectedDate: Date, sourceLocation: SourceLocation = #_sourceLocation) { let components = DateComponents(component: field, value: value)! let result = gregorianCalendar.date(byAdding: components, to: addingToDate, wrappingComponents: wrap)! - // These tests were written when Date used a 64b representation; - // we'll add new tests that validate the low-word of the 128b - // Date, but these old tests should continue passing if we only - // look at the high word as vended by tISRD. - #expect(result.timeIntervalSinceReferenceDate == - expectedDate.timeIntervalSinceReferenceDate, - sourceLocation: sourceLocation) + #expect(result == expectedDate, sourceLocation: sourceLocation) } date = Date(timeIntervalSince1970: 825723300.0) @@ -405,13 +399,7 @@ private struct GregorianCalendarTests { func test(addField field: Calendar.Component, value: Int, to addingToDate: Date, wrap: Bool, expectedDate: Date, sourceLocation: SourceLocation = #_sourceLocation) { let components = DateComponents(component: field, value: value)! let result = gregorianCalendar.date(byAdding: components, to: addingToDate, wrappingComponents: wrap)! - // These tests were written when Date used a 64b representation; - // we'll add new tests that validate the low-word of the 128b - // Date, but these old tests should continue passing if we only - // look at the high word as vended by tISRD. - #expect(result.timeIntervalSinceReferenceDate == - expectedDate.timeIntervalSinceReferenceDate, - sourceLocation: sourceLocation) + #expect(result == expectedDate, sourceLocation: sourceLocation) } date = Date(timeIntervalSince1970: 62135596800.0) // 3939-01-01 @@ -838,11 +826,7 @@ private struct GregorianCalendarTests { let new_end = new?.end #expect(new_start == start, "interval start did not match", sourceLocation: sourceLocation) - // These tests were written when Date used a 64b representation; - // we'll add new tests that validate the low-word of the 128b - // Date, but these old tests should continue passing if we only - // look at the high word as vended by tISRD. - #expect(new_end?.timeIntervalSinceReferenceDate == end?.timeIntervalSinceReferenceDate, "interval end did not match", sourceLocation: sourceLocation) + #expect(new_end == end, "interval end did not match", sourceLocation: sourceLocation) } var date: Date @@ -854,10 +838,7 @@ private struct GregorianCalendarTests { test(.hour, date, expectedStart: Date(timeIntervalSince1970: 820454400.0), end: Date(timeIntervalSince1970: 820458000.0)) test(.minute, date, expectedStart: Date(timeIntervalSince1970: 820454400.0), end: Date(timeIntervalSince1970: 820454460.0)) test(.second, date, expectedStart: Date(timeIntervalSince1970: 820454400.0), end: Date(timeIntervalSince1970: 820454401.0)) - // Legacy test from 64b Date; expected end is the same as start due to rounding. test(.nanosecond, date, expectedStart: Date(timeIntervalSince1970: 820454400.0), end: Date(timeIntervalSince1970: 820454400.0)) - // Updated test for 128b Date to benefit from improved accuracy: - #expect(calendar.dateInterval(of: .nanosecond, for: date)?.end == Date(timeInterval: 1e-9, since: date)) test(.weekday, date, expectedStart: Date(timeIntervalSince1970: 820396800.0), end: Date(timeIntervalSince1970: 820483200.0)) test(.weekdayOrdinal, date, expectedStart: Date(timeIntervalSince1970: 820396800.0), end: Date(timeIntervalSince1970: 820483200.0)) test(.quarter, date, expectedStart: Date(timeIntervalSince1970: 812534400.0), end: Date(timeIntervalSince1970: 820483200.0)) @@ -888,7 +869,7 @@ private struct GregorianCalendarTests { test(.hour, date, expectedStart: Date(timeIntervalSince1970: -62135769600.0), end: Date(timeIntervalSince1970: -62135766000.0)) test(.minute, date, expectedStart: Date(timeIntervalSince1970: -62135769600.0), end: Date(timeIntervalSince1970: -62135769540.0)) test(.second, date, expectedStart: Date(timeIntervalSince1970: -62135769600.0), end: Date(timeIntervalSince1970: -62135769599.0)) - test(.nanosecond, date, expectedStart: date, end: Date(timeInterval: 1e-9, since: date)) + test(.nanosecond, date, expectedStart: Date(timeIntervalSince1970: -62135769600.00001), end: Date(timeIntervalSince1970: -62135769600.00001)) test(.weekday, date, expectedStart: Date(timeIntervalSince1970: -62135827200.0), end: Date(timeIntervalSince1970: -62135740800.0)) test(.weekdayOrdinal, date, expectedStart: Date(timeIntervalSince1970: -62135827200.0), end: Date(timeIntervalSince1970: -62135740800.0)) test(.quarter, date, expectedStart: Date(timeIntervalSince1970: -62143689600.0), end: Date(timeIntervalSince1970: -62135740800.0)) diff --git a/Tests/FoundationInternationalizationTests/CalendarTests.swift b/Tests/FoundationInternationalizationTests/CalendarTests.swift index 6ce160945..8602f9a26 100644 --- a/Tests/FoundationInternationalizationTests/CalendarTests.swift +++ b/Tests/FoundationInternationalizationTests/CalendarTests.swift @@ -1420,7 +1420,7 @@ private struct CalendarTests { func test(_ start: Date, _ end: Date) throws { let components = c.dateComponents([.year, .month, .day, .hour, .minute, .second, .nanosecond, .weekOfMonth], from: start, to: end) let added = try #require(c.date(byAdding: components, to: start)) - #expect(added.timeIntervalSinceReferenceDate == end.timeIntervalSinceReferenceDate, "actual: \(s.format(added)), expected: \(s.format(end))") + #expect(added == end, "actual: \(s.format(added)), expected: \(s.format(end))") } // 2024-03-09T02:34:36-0800, 2024-03-17T03:34:36-0700, 10:34:36 UTC diff --git a/Tests/FoundationInternationalizationTests/Formatting/ParseStrategy+RegexComponentTests.swift b/Tests/FoundationInternationalizationTests/Formatting/ParseStrategy+RegexComponentTests.swift index f9370c88d..6e684a957 100644 --- a/Tests/FoundationInternationalizationTests/Formatting/ParseStrategy+RegexComponentTests.swift +++ b/Tests/FoundationInternationalizationTests/Formatting/ParseStrategy+RegexComponentTests.swift @@ -62,6 +62,8 @@ private struct ParseStrategyMatchTests { #expect(res.output.1 == expectedDate) } +// https://github.com/apple/swift-foundation/issues/60 +#if FOUNDATION_FRAMEWORK @Test func apiStatement() { let statement = """ @@ -211,8 +213,9 @@ DEBIT Mar 31/20 March Payment to BoA -USD 52,249.98 #expect(match.output.1 == "MergeableSetTests") #expect(match.output.2 == "started") // dateFormatter.date(from: "2021-07-08 10:19:35.418")! - #expect(match.output.3.timeIntervalSinceReferenceDate == 647432375.418) + #expect(match.output.3 == Date(timeIntervalSinceReferenceDate: 647432375.418)) } +#endif @Test func variousDatesAndTimes() { func verify(_ str: String, _ strategy: Date.ParseStrategy, _ expected: String?, sourceLocation: SourceLocation = #_sourceLocation) { diff --git a/Tests/FoundationInternationalizationTests/GegorianCalendarInternationalizationTests.swift b/Tests/FoundationInternationalizationTests/GegorianCalendarInternationalizationTests.swift index 3d389281c..195c18912 100644 --- a/Tests/FoundationInternationalizationTests/GegorianCalendarInternationalizationTests.swift +++ b/Tests/FoundationInternationalizationTests/GegorianCalendarInternationalizationTests.swift @@ -1461,8 +1461,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828262987.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 829468987.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828262987.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828867787.0)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828867787.0)) date = Date(timeIntervalSince1970: 828871387.0) // 1996-04-07T03:03:07-0700 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828871387.0)) @@ -1489,8 +1489,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828270187.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 829476187.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828270187.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828871387.0)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828871387.0)) date = Date(timeIntervalSince1970: 828874987.0) // 1996-04-07T04:03:07-0700 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828874987.0)) @@ -1517,8 +1517,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828273787.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 829479787.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828273787.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828874987.0)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828874987.0)) date = Date(timeIntervalSince1970: 846403387.0) // 1996-10-27T01:03:07-0700 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846403387.0)) @@ -1547,8 +1547,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845798587.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 847011787.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845798587.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846403387.0)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 846403387.0)) date = Date(timeIntervalSince1970: 846406987.0) // 1996-10-27T01:03:07-0800 // Previously this returns 1996-10-27T01:03:07-0700 @@ -1578,8 +1578,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845798587.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 847011787.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845798587.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846406987.0)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 846406987.0)) date = Date(timeIntervalSince1970: 846410587.0) // 1996-10-27T02:03:07-0800 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846410587.0)) @@ -1606,8 +1606,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845802187.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 847015387.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845802187.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846410587.0)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 846410587.0)) date = Date(timeIntervalSince1970: 846414187.0) // 1996-10-27T03:03:07-0800 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846414187.0)) @@ -1634,8 +1634,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845805787.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 847018987.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845805787.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846414187.0)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 846414187.0)) date = Date(timeIntervalSince1970: 814953787.0) // 1995-10-29T01:03:07-0700 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 814953787.0)) @@ -1664,8 +1664,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814348987.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 815562187.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814348987.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 814953787.0)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814953787.0)) date = Date(timeIntervalSince1970: 814957387.0) // 1995-10-29T01:03:07-0800 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 814957387.0)) @@ -1692,8 +1692,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814348987.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 815562187.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814348987.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 814957387.0)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814957387.0)) date = Date(timeIntervalSince1970: 814960987.0) // 1995-10-29T02:03:07-0800 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 814960987.0)) @@ -1720,8 +1720,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814352587.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 815565787.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814352587.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 814960987.0)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814960987.0)) date = Date(timeIntervalSince1970: 814964587.0) // 1995-10-29T03:03:07-0800 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 814964587.0)) @@ -1748,8 +1748,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814356187.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 815569387.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814356187.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 814964587.0)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 814964587.0)) } @Test func add_Wrap_DST() { @@ -1789,8 +1789,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828262987.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 829468987.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 830851387.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828867787.0)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828867787.0)) date = Date(timeIntervalSince1970: 828871387.0) // 1996-04-07T03:03:07-0700 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828871387.0)) @@ -1817,8 +1817,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828270187.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 829476187.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 830858587.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828871387.0)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828871387.0)) date = Date(timeIntervalSince1970: 828874987.0) // 1996-04-07T04:03:07-0700 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828874987.0)) @@ -1845,8 +1845,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828273787.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 829479787.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 830862187.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 828874987.0)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 828874987.0)) date = Date(timeIntervalSince1970: 846406987.0) // 1996-10-27T01:03:07-0800 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846406987.0)) @@ -1873,8 +1873,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845798587.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846752587.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845798587.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846406987.0)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 846406987.0)) date = Date(timeIntervalSince1970: 846410587.0) // 1996-10-27T02:03:07-0800 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846410587.0)) @@ -1901,8 +1901,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845802187.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846756187.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845802187.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846410587.0)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 846410587.0)) date = Date(timeIntervalSince1970: 846414187.0) // 1996-10-27T03:03:07-0800 test(addField: .era, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846414187.0)) @@ -1929,8 +1929,8 @@ private struct GregorianCalendarInternationalizationTests { test(addField: .weekOfYear, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845805787.0)) test(addField: .weekOfMonth, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846759787.0)) test(addField: .weekOfMonth, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 845805787.0)) - test(addField: .nanosecond, value: 1, to: date, expectedDate: date.addingTimeInterval(1e-9)) - test(addField: .nanosecond, value: -1, to: date, expectedDate: date.addingTimeInterval(-1e-9)) + test(addField: .nanosecond, value: 1, to: date, expectedDate: Date(timeIntervalSince1970: 846414187.0)) + test(addField: .nanosecond, value: -1, to: date, expectedDate: Date(timeIntervalSince1970: 846414187.0)) } @Test func ordinality_DST() { @@ -2448,21 +2448,21 @@ private struct GregorianCalendarInternationalizationTests { dc_customCalendarAndTimeZone.timeZone = .init(secondsFromGMT: 28800) // calendar.timeZone = UTC+0, dc.calendar.timeZone = UTC-7, dc.timeZone = UTC+8 // expect local time in dc.timeZone (UTC+8) - #expect(gregorianCalendar.date(from: dc_customCalendarAndTimeZone)!.timeIntervalSinceReferenceDate == 679024975.891) // 2022-07-09T02:02:55Z + #expect(gregorianCalendar.date(from: dc_customCalendarAndTimeZone)! == Date(timeIntervalSinceReferenceDate: 679024975.891)) // 2022-07-09T02:02:55Z var dc_customCalendar = dc dc_customCalendar.calendar = dcCalendar dc_customCalendar.timeZone = nil // calendar.timeZone = UTC+0, dc.calendar.timeZone = UTC-7, dc.timeZone = nil // expect local time in calendar.timeZone (UTC+0) - #expect(gregorianCalendar.date(from: dc_customCalendar)!.timeIntervalSinceReferenceDate == 679053775.891) // 2022-07-09T10:02:55Z + #expect(gregorianCalendar.date(from: dc_customCalendar)! == Date(timeIntervalSinceReferenceDate: 679053775.891)) // 2022-07-09T10:02:55Z var dc_customTimeZone = dc_customCalendarAndTimeZone dc_customTimeZone.calendar = nil dc_customTimeZone.timeZone = .init(secondsFromGMT: 28800) // calendar.timeZone = UTC+0, dc.calendar = nil, dc.timeZone = UTC+8 // expect local time in dc.timeZone (UTC+8) - #expect(gregorianCalendar.date(from: dc_customTimeZone)!.timeIntervalSinceReferenceDate == 679024975.891) // 2022-07-09T02:02:55Z + #expect(gregorianCalendar.date(from: dc_customTimeZone)! == Date(timeIntervalSinceReferenceDate: 679024975.891)) // 2022-07-09T02:02:55Z let dcCalendar_noTimeZone = Calendar(identifier: .japanese, locale: Locale(identifier: ""), timeZone: .gmt, firstWeekday: 1, minimumDaysInFirstWeek: 1, gregorianStartDate: nil) var dc_customCalendarNoTimeZone_customTimeZone = dc @@ -2470,7 +2470,7 @@ private struct GregorianCalendarInternationalizationTests { dc_customCalendarNoTimeZone_customTimeZone.timeZone = .init(secondsFromGMT: 28800) // calendar.timeZone = UTC+0, dc.calendar.timeZone = nil, dc.timeZone = UTC+8 // expect local time in dc.timeZone (UTC+8) - #expect(gregorianCalendar.date(from: dc_customCalendarNoTimeZone_customTimeZone)!.timeIntervalSinceReferenceDate == 679024975.891) // 2022-07-09T02:02:55Z + #expect(gregorianCalendar.date(from: dc_customCalendarNoTimeZone_customTimeZone)! == Date(timeIntervalSinceReferenceDate: 679024975.891)) // 2022-07-09T02:02:55Z } @Test func dateFromComponents_componentsTimeZoneConversion() { From d8533a2e24a7684fa8c0ed846439aa4a62b4c80d Mon Sep 17 00:00:00 2001 From: YOCKOW Date: Wed, 12 Nov 2025 08:39:18 +0900 Subject: [PATCH 3/3] SF-0033: Implement `String.Encoding.ianaName` and `String.Encoding(ianaName:)`. (#1286) * Import implementation for String Encoding Names from other repo. - source: https://github.com/YOCKOW/SF-StringEncodingNameImpl * Import tests for String Encoding Names from other repo. - source: https://github.com/YOCKOW/SF-StringEncodingNameImpl/blob/0.4.0/Tests/StringEncodingNameImplTests/StringEncodingNameParserTests.swift * Remove dead code in terms of the current proposal. * Use `Testing` for String Encoding Names tests. * NFC: Fix indentation in "String+Encoding+Names.swift". * SF-0033: Adjust comments/attributes to match the accepted proposal. * Auto-generate Swift source code for IANA Charset names. * Remove unnecessary `@inlinable`. * Simplify `String.init(ianaName:)`. * Add new files related to SF-0033 to CMakeLists.txt. * Rewrite script in Swift instead of Python. In response to: https://github.com/swiftlang/swift-foundation/pull/1286#discussion_r2437753438 * Simplify logic to parse IANA Charset names. In response to: - https://github.com/swiftlang/swift-foundation/pull/1286#discussion_r2441497400 - https://github.com/swiftlang/swift-foundation/pull/1286#discussion_r2441505001 - https://github.com/swiftlang/swift-foundation/pull/1286#discussion_r2441546727 * Fix spelling of functions for "case-insensitively". In response to: - https://github.com/swiftlang/swift-foundation/pull/1286#discussion_r2457914338 * Remove redundant nested function in `String.Encoding(ianaName:)`. In response to: - https://github.com/swiftlang/swift-foundation/pull/1286#discussion_r2457939067 --- .../String/CMakeLists.txt | 2 + .../String/IANACharsetNames.swift | 213 ++++++++++++++++++ .../String/String+Encoding+Names.swift | 155 +++++++++++++ .../StringTests.swift | 63 ++++++ utils/update-iana-charset-names | 62 +++++ utils/update-iana-charset-names-impl.swift | 195 ++++++++++++++++ 6 files changed, 690 insertions(+) create mode 100644 Sources/FoundationEssentials/String/IANACharsetNames.swift create mode 100644 Sources/FoundationEssentials/String/String+Encoding+Names.swift create mode 100755 utils/update-iana-charset-names create mode 100755 utils/update-iana-charset-names-impl.swift diff --git a/Sources/FoundationEssentials/String/CMakeLists.txt b/Sources/FoundationEssentials/String/CMakeLists.txt index 720eb218d..6cc7994d7 100644 --- a/Sources/FoundationEssentials/String/CMakeLists.txt +++ b/Sources/FoundationEssentials/String/CMakeLists.txt @@ -15,10 +15,12 @@ target_sources(FoundationEssentials PRIVATE BidirectionalCollection.swift BuiltInUnicodeScalarSet.swift + IANACharsetNames.swift RegexPatternCache.swift String+Bridging.swift String+Comparison.swift String+Encoding.swift + String+Encoding+Names.swift String+EndianAdaptorSequence.swift String+Essentials.swift String+IO.swift diff --git a/Sources/FoundationEssentials/String/IANACharsetNames.swift b/Sources/FoundationEssentials/String/IANACharsetNames.swift new file mode 100644 index 000000000..8f3e88f09 --- /dev/null +++ b/Sources/FoundationEssentials/String/IANACharsetNames.swift @@ -0,0 +1,213 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + + +// WARNING: DO NOT EDIT THIS FILE DIRECTLY. +// This is auto-generated by `update-iana-charset-names`. + + +extension IANACharset { + /// IANA Charset `US-ASCII`. + static let usASCII = IANACharset( + preferredMIMEName: "US-ASCII", + name: "US-ASCII", + aliases: [ + "iso-ir-6", + "ANSI_X3.4-1968", + "ANSI_X3.4-1986", + "ISO_646.irv:1991", + "ISO646-US", + "US-ASCII", + "us", + "IBM367", + "cp367", + "csASCII", + ] + ) + + /// IANA Charset `ISO-8859-1`. + static let iso8859_1 = IANACharset( + preferredMIMEName: "ISO-8859-1", + name: "ISO_8859-1:1987", + aliases: [ + "iso-ir-100", + "ISO_8859-1", + "ISO-8859-1", + "latin1", + "l1", + "IBM819", + "CP819", + "csISOLatin1", + ] + ) + + /// IANA Charset `ISO-8859-2`. + static let iso8859_2 = IANACharset( + preferredMIMEName: "ISO-8859-2", + name: "ISO_8859-2:1987", + aliases: [ + "iso-ir-101", + "ISO_8859-2", + "ISO-8859-2", + "latin2", + "l2", + "csISOLatin2", + ] + ) + + /// IANA Charset `Shift_JIS`. + static let shiftJIS = IANACharset( + preferredMIMEName: "Shift_JIS", + name: "Shift_JIS", + aliases: [ + "MS_Kanji", + "csShiftJIS", + ] + ) + + /// IANA Charset `EUC-JP`. + static let eucJP = IANACharset( + preferredMIMEName: "EUC-JP", + name: "Extended_UNIX_Code_Packed_Format_for_Japanese", + aliases: [ + "csEUCPkdFmtJapanese", + "EUC-JP", + ] + ) + + /// IANA Charset `ISO-2022-JP`. + static let iso2022JP = IANACharset( + preferredMIMEName: "ISO-2022-JP", + name: "ISO-2022-JP", + aliases: [ + "csISO2022JP", + ] + ) + + /// IANA Charset `UTF-8`. + static let utf8 = IANACharset( + preferredMIMEName: nil, + name: "UTF-8", + aliases: [ + "csUTF8", + ] + ) + + /// IANA Charset `UTF-16BE`. + static let utf16BE = IANACharset( + preferredMIMEName: nil, + name: "UTF-16BE", + aliases: [ + "csUTF16BE", + ] + ) + + /// IANA Charset `UTF-16LE`. + static let utf16LE = IANACharset( + preferredMIMEName: nil, + name: "UTF-16LE", + aliases: [ + "csUTF16LE", + ] + ) + + /// IANA Charset `UTF-16`. + static let utf16 = IANACharset( + preferredMIMEName: nil, + name: "UTF-16", + aliases: [ + "csUTF16", + ] + ) + + /// IANA Charset `UTF-32`. + static let utf32 = IANACharset( + preferredMIMEName: nil, + name: "UTF-32", + aliases: [ + "csUTF32", + ] + ) + + /// IANA Charset `UTF-32BE`. + static let utf32BE = IANACharset( + preferredMIMEName: nil, + name: "UTF-32BE", + aliases: [ + "csUTF32BE", + ] + ) + + /// IANA Charset `UTF-32LE`. + static let utf32LE = IANACharset( + preferredMIMEName: nil, + name: "UTF-32LE", + aliases: [ + "csUTF32LE", + ] + ) + + /// IANA Charset `macintosh`. + static let macintosh = IANACharset( + preferredMIMEName: nil, + name: "macintosh", + aliases: [ + "mac", + "csMacintosh", + ] + ) + + /// IANA Charset `windows-1250`. + static let windows1250 = IANACharset( + preferredMIMEName: nil, + name: "windows-1250", + aliases: [ + "cswindows1250", + ] + ) + + /// IANA Charset `windows-1251`. + static let windows1251 = IANACharset( + preferredMIMEName: nil, + name: "windows-1251", + aliases: [ + "cswindows1251", + ] + ) + + /// IANA Charset `windows-1252`. + static let windows1252 = IANACharset( + preferredMIMEName: nil, + name: "windows-1252", + aliases: [ + "cswindows1252", + ] + ) + + /// IANA Charset `windows-1253`. + static let windows1253 = IANACharset( + preferredMIMEName: nil, + name: "windows-1253", + aliases: [ + "cswindows1253", + ] + ) + + /// IANA Charset `windows-1254`. + static let windows1254 = IANACharset( + preferredMIMEName: nil, + name: "windows-1254", + aliases: [ + "cswindows1254", + ] + ) +} diff --git a/Sources/FoundationEssentials/String/String+Encoding+Names.swift b/Sources/FoundationEssentials/String/String+Encoding+Names.swift new file mode 100644 index 000000000..1407e5ae1 --- /dev/null +++ b/Sources/FoundationEssentials/String/String+Encoding+Names.swift @@ -0,0 +1,155 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + + +// MARK: - Private extensions for parsing encoding names + +private extension UTF8.CodeUnit { + func _isASCIICaseInsensitivelyEqual(to other: UTF8.CodeUnit) -> Bool { + return switch self { + case other, other._uppercased, other._lowercased: true + default: false + } + } +} + +private extension String { + func _isASCIICaseInsensitivelyEqual(to other: String) -> Bool { + let (myUTF8, otherUTF8) = (self.utf8, other.utf8) + var (myIndex, otherIndex) = (myUTF8.startIndex, otherUTF8.startIndex) + while myIndex < myUTF8.endIndex && otherIndex < otherUTF8.endIndex { + guard myUTF8[myIndex]._isASCIICaseInsensitivelyEqual(to: otherUTF8[otherIndex]) else { + return false + } + + myUTF8.formIndex(after: &myIndex) + otherUTF8.formIndex(after: &otherIndex) + } + return myIndex == myUTF8.endIndex && otherIndex == otherUTF8.endIndex + } +} + + +// MARK: - IANA Charset Names + +/// Info about IANA Charset. +internal struct IANACharset { + /// Preferred MIME Name + let preferredMIMEName: String? + + /// The name of this charset + let name: String + + /// The aliases of this charset + let aliases: Array + + var representativeName: String { + return preferredMIMEName ?? name + } + + init(preferredMIMEName: String?, name: String, aliases: Array) { + self.preferredMIMEName = preferredMIMEName + self.name = name + self.aliases = aliases + } + + func matches(_ string: String) -> Bool { + if let preferredMIMEName = self.preferredMIMEName, + preferredMIMEName._isASCIICaseInsensitivelyEqual(to: string) { + return true + } + if name._isASCIICaseInsensitivelyEqual(to: string) { + return true + } + for alias in aliases { + if alias._isASCIICaseInsensitivelyEqual(to: string) { + return true + } + } + return false + } +} + + +// MARK: - `String.Encoding` Names + +extension String.Encoding { + private var _ianaCharset: IANACharset? { + switch self { + case .utf8: .utf8 + case .ascii: .usASCII + case .japaneseEUC: .eucJP + case .isoLatin1: .iso8859_1 + case .shiftJIS: .shiftJIS + case .isoLatin2: .iso8859_2 + case .unicode: .utf16 + case .windowsCP1251: .windows1251 + case .windowsCP1252: .windows1252 + case .windowsCP1253: .windows1253 + case .windowsCP1254: .windows1254 + case .windowsCP1250: .windows1250 + case .iso2022JP: .iso2022JP + case .macOSRoman: .macintosh + case .utf16BigEndian: .utf16BE + case .utf16LittleEndian: .utf16LE + case .utf32: .utf32 + case .utf32BigEndian: .utf32BE + case .utf32LittleEndian: .utf32LE + default: nil + } + } + + /// The name of this encoding that is compatible with the one of the IANA registry "charset". + @available(FoundationPreview 6.3, *) + public var ianaName: String? { + return _ianaCharset?.representativeName + } + + /// Creates an instance from the name of the IANA registry "charset". + /// + /// - Note: The given name is compared to each IANA "charset" name + /// with ASCII case-insensitive collation + /// to determine which encoding is suitable. + @available(FoundationPreview 6.3, *) + public init?(ianaName charsetName: String) { + let possibilities: [String.Encoding] = [ + .utf8, + .ascii, + .japaneseEUC, + .isoLatin1, + .shiftJIS, + .isoLatin2, + .unicode, // .utf16 + .windowsCP1251, + .windowsCP1252, + .windowsCP1253, + .windowsCP1254, + .windowsCP1250, + .iso2022JP, + .macOSRoman, + .utf16BigEndian, + .utf16LittleEndian, + .utf32, + .utf32BigEndian, + .utf32LittleEndian, + ] + + for encoding in possibilities { + if encoding._ianaCharset!.matches(charsetName) { + self = encoding + return + } + } + return nil + } +} + diff --git a/Tests/FoundationEssentialsTests/StringTests.swift b/Tests/FoundationEssentialsTests/StringTests.swift index 26286be15..6a6781874 100644 --- a/Tests/FoundationEssentialsTests/StringTests.swift +++ b/Tests/FoundationEssentialsTests/StringTests.swift @@ -1397,6 +1397,69 @@ private struct StringTests { "abcd🎺efgh" ]) } + + @Test func encodingNames() { + // Encoding to Name + #expect(String.Encoding.ascii.ianaName == "US-ASCII") + #expect(String.Encoding.nextstep.ianaName == nil) + #expect(String.Encoding.japaneseEUC.ianaName == "EUC-JP") + #expect(String.Encoding.utf8.ianaName == "UTF-8") + #expect(String.Encoding.isoLatin1.ianaName == "ISO-8859-1") + #expect(String.Encoding.symbol.ianaName == nil) + #expect(String.Encoding.nonLossyASCII.ianaName == nil) + #expect(String.Encoding.shiftJIS.ianaName == "Shift_JIS") + #expect(String.Encoding.isoLatin2.ianaName == "ISO-8859-2") + #expect(String.Encoding.unicode.ianaName == "UTF-16") + #expect(String.Encoding.windowsCP1251.ianaName == "windows-1251") + #expect(String.Encoding.windowsCP1252.ianaName == "windows-1252") + #expect(String.Encoding.windowsCP1253.ianaName == "windows-1253") + #expect(String.Encoding.windowsCP1254.ianaName == "windows-1254") + #expect(String.Encoding.windowsCP1250.ianaName == "windows-1250") + #expect(String.Encoding.iso2022JP.ianaName == "ISO-2022-JP") + #expect(String.Encoding.macOSRoman.ianaName == "macintosh") + #expect(String.Encoding.utf16BigEndian.ianaName == "UTF-16BE") + #expect(String.Encoding.utf16LittleEndian.ianaName == "UTF-16LE") + #expect(String.Encoding.utf32.ianaName == "UTF-32") + #expect(String.Encoding.utf32BigEndian.ianaName == "UTF-32BE") + #expect(String.Encoding.utf32LittleEndian.ianaName == "UTF-32LE") + #expect(String.Encoding(rawValue: .max).ianaName == nil) + + // Name to Encoding + #expect(String.Encoding(ianaName: "us-ascii") == .ascii) + #expect(String.Encoding(ianaName: "iso-ir-2") == nil) + #expect(String.Encoding(ianaName: "x-nextstep") == nil) + #expect(String.Encoding(ianaName: "euc-jp") == .japaneseEUC) + #expect(String.Encoding(ianaName: "CP51932") == nil) + #expect(String.Encoding(ianaName: "utf-8") == .utf8) + #expect(String.Encoding(ianaName: "iso_8859-1") == .isoLatin1) + #expect(String.Encoding(ianaName: "x-mac-symbol") == nil) + #expect(String.Encoding(ianaName: "Adobe-symbol-encoding") == nil) + #expect(String.Encoding(ianaName: "cp932") == nil) + #expect(String.Encoding(ianaName: "shift_jis") == .shiftJIS) + #expect(String.Encoding(ianaName: "windows-31j") == nil) + #expect(String.Encoding(ianaName: "iso_8859-2") == .isoLatin2) + #expect(String.Encoding(ianaName: "utf-16") == .utf16) + #expect(String.Encoding(ianaName: "iso-10646-ucs-2") == nil) + #expect(String.Encoding(ianaName: "unicode-1-1") == nil) + #expect(String.Encoding(ianaName: "windows-1251") == .windowsCP1251) + #expect(String.Encoding(ianaName: "windows-1252") == .windowsCP1252) + #expect(String.Encoding(ianaName: "ISO-8859-1-Windows-3.0-Latin-1") == nil) + #expect(String.Encoding(ianaName: "ISO-8859-1-Windows-3.1-Latin-1") == nil) + #expect(String.Encoding(ianaName: "windows-1253") == .windowsCP1253) + #expect(String.Encoding(ianaName: "windows-1254") == .windowsCP1254) + #expect(String.Encoding(ianaName: "iso-8859-9-windows-Latin-5") == nil) + #expect(String.Encoding(ianaName: "windows-1250") == .windowsCP1250) + #expect(String.Encoding(ianaName: "iso-8859-2-windows-Latin-2") == nil) + #expect(String.Encoding(ianaName: "iso-2022-jp") == .iso2022JP) + #expect(String.Encoding(ianaName: "macintosh") == .macOSRoman) + #expect(String.Encoding(ianaName: "utf-16be") == .utf16BigEndian) + #expect(String.Encoding(ianaName: "utf-16le") == .utf16LittleEndian) + #expect(String.Encoding(ianaName: "utf-32") == .utf32) + #expect(String.Encoding(ianaName: "iso-10646-ucs-4") == nil) + #expect(String.Encoding(ianaName: "utf-32be") == .utf32BigEndian) + #expect(String.Encoding(ianaName: "utf-32le") == .utf32LittleEndian) + #expect(String.Encoding(ianaName: "foo-bar-baz") == nil) + } } // MARK: - Helper functions diff --git a/utils/update-iana-charset-names b/utils/update-iana-charset-names new file mode 100755 index 000000000..e56e972b1 --- /dev/null +++ b/utils/update-iana-charset-names @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the Swift.org open source project +## +## Copyright (c) 2025 Apple Inc. and the Swift project authors +## Licensed under Apache License v2.0 with Runtime Library Exception +## +## See https://swift.org/LICENSE.txt for license information +## See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +## +##===----------------------------------------------------------------------===## + +# This is a shell script that generates a Swift source code file which contains +# the list of IANA "Character Sets". + +set -eu + +declare -r commandName="$(basename "$0")" +declare -r utilsDir="$(cd "$(dirname "$0")" && pwd)" +declare -r foundationRepoDir="$(cd "${utilsDir}/.." && pwd)" +declare -r targetSwiftFileRelativePath="Sources/FoundationEssentials/String/IANACharsetNames.swift" + +declare -r copyrightYear=$( + currentYear=$(date +%Y) + if [[ $currentYear -eq 2025 ]]; then + echo 2025 + else + echo 2025-${currentYear} + fi +) +declare -r swiftLicenseHeader=" +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) ${copyrightYear} Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +" +declare -r warningComment=" +// WARNING: DO NOT EDIT THIS FILE DIRECTLY. +// This is auto-generated by \`${commandName}\`. + +" + +echo "Generating Swift source code..." 1>&2 +declare generatedCode +generatedCode=$( + echo "${swiftLicenseHeader##$'\n'}" + echo "$warningComment" + swift -D PRINT_CODE "${utilsDir}/${commandName}-impl.swift" +) + +echo "Writing the code to '${targetSwiftFileRelativePath}'..." 1>&2 +echo "$generatedCode" >"${foundationRepoDir}/${targetSwiftFileRelativePath}" + +echo "Done." 1>&2 diff --git a/utils/update-iana-charset-names-impl.swift b/utils/update-iana-charset-names-impl.swift new file mode 100755 index 000000000..c7e83e0d8 --- /dev/null +++ b/utils/update-iana-charset-names-impl.swift @@ -0,0 +1,195 @@ +#!/usr/bin/env swift -D PRINT_CODE +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/* + +This is a Swift script that converts an XML file containing the list of IANA +"Character Sets" to Swift source code. +This script generates minimum code and is intended to be executed by other shell +script. + + */ + +import Foundation +#if canImport(FoundationXML) +import FoundationXML +#endif + +// MARK: - Constants + +let requiredCharsetNames = [ + "UTF-8", + "US-ASCII", + "EUC-JP", + "ISO-8859-1", + "Shift_JIS", + "ISO-8859-2", + "UTF-16", + "windows-1251", + "windows-1252", + "windows-1253", + "windows-1254", + "windows-1250", + "ISO-2022-JP", + "macintosh", + "UTF-16BE", + "UTF-16LE", + "UTF-32", + "UTF-32BE", + "UTF-32LE", +] +let charsetsXMLURL = URL( + string: "https://www.iana.org/assignments/character-sets/character-sets.xml" +)! +let charsetsXMLNamespace = "http://www.iana.org/assignments" +let swiftCodeIndent = " " + + +// MARK: - Implementation + +enum CodeGenerationError: Swift.Error { + case missingName + case missingAliasValue + case noRootElement +} + +/// Representation of element in 'character-sets.xml' +/// +/// The structure of element is as blow: +/// ```xml +/// +/// US-ASCII +/// +/// 3 +/// ANSI X3.4-1986 +/// iso-ir-6 +/// ANSI_X3.4-1968 +/// ANSI_X3.4-1986 +/// ISO_646.irv:1991 +/// ISO646-US +/// US-ASCII +/// us +/// IBM367 +/// cp367 +/// csASCII +/// US-ASCII +/// +/// ``` +struct IANACharsetNameRecord { + /// Preferred MIME Name + let preferredMIMEName: String? + + /// The name of this charset + let name: String + + /// The aliases of this charset + let aliases: Array + + var representativeName: String { + return preferredMIMEName ?? name + } + + var swiftCodeLines: [String] { + var lines: [String] = [] + lines.append("/// IANA Charset `\(representativeName)`.") + lines.append("static let \(representativeName._camelcased()) = IANACharset(") + lines.append("\(swiftCodeIndent)preferredMIMEName: \(preferredMIMEName.map { #""\#($0)""# } ?? "nil"),") + lines.append("\(swiftCodeIndent)name: \"\(name)\",") + lines.append("\(swiftCodeIndent)aliases: [") + for alias in aliases { + lines.append("\(swiftCodeIndent)\(swiftCodeIndent)\"\(alias)\",") + } + lines.append("\(swiftCodeIndent)]") + lines.append(")") + return lines + } + + init(_ node: XMLNode) throws { + guard let name = try node.nodes(forXPath: "./name").first?.stringValue else { + throw CodeGenerationError.missingName + } + self.name = name + self.preferredMIMEName = try node.nodes(forXPath: "./preferred_alias").first?.stringValue + self.aliases = try node.nodes(forXPath: "./alias").map { + guard let alias = $0.stringValue else { + throw CodeGenerationError.missingAliasValue + } + return alias + } + } +} + +func generateSwiftCode() throws -> String { + let charsetsXMLDocument = try XMLDocument(contentsOf: charsetsXMLURL) + guard let charsetsXMLRoot = charsetsXMLDocument.rootElement() else { + throw CodeGenerationError.noRootElement + } + let charsetsXMLRecordElements = try charsetsXMLRoot.nodes(forXPath: "./registry/record") + + var result = "extension IANACharset {" + + for record in try charsetsXMLRecordElements.map({ + try IANACharsetNameRecord($0) + }) where requiredCharsetNames.contains(record.representativeName) { + result += "\n" + result += record.swiftCodeLines.map({ swiftCodeIndent + $0 }).joined(separator: "\n") + result += "\n" + } + + result += "}\n" + return result +} + +#if PRINT_CODE +print(try generateSwiftCode()) +#endif + +// MARK: - Extensions + +extension UTF8.CodeUnit { + var _isASCIINumeric: Bool { (0x30...0x39).contains(self) } + var _isASCIIUppercase: Bool { (0x41...0x5A).contains(self) } + var _isASCIILowercase: Bool { (0x61...0x7A).contains(self) } +} + +extension String { + func _camelcased() -> String { + var result = "" + var previousWord: Substring.UTF8View? = nil + for wordUTF8 in self.utf8.split(whereSeparator: { + !$0._isASCIINumeric && + !$0._isASCIIUppercase && + !$0._isASCIILowercase + }) { + defer { + previousWord = wordUTF8 + } + let word = String(Substring(wordUTF8)) + guard let previousWord else { + result += word.lowercased() + continue + } + if previousWord.last!._isASCIINumeric && wordUTF8.first!._isASCIINumeric { + result += "_" + } + if let firstNonNumericIndex = wordUTF8.firstIndex(where: { !$0._isASCIINumeric }), + wordUTF8[firstNonNumericIndex...].allSatisfy({ $0._isASCIIUppercase }) { + result += word + } else { + result += word.capitalized(with: nil) + } + + } + return result + } +}