From 6380b60bb041bd94b09e0c151037508b93155fb4 Mon Sep 17 00:00:00 2001 From: woxtu Date: Tue, 11 Jul 2023 00:13:52 +0000 Subject: [PATCH 1/3] Export CoreFoundation functions --- CoreFoundation/Base.subproj/ForSwiftFoundationOnly.h | 1 + CoreFoundation/Locale.subproj/CFListFormatter.h | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CoreFoundation/Base.subproj/ForSwiftFoundationOnly.h b/CoreFoundation/Base.subproj/ForSwiftFoundationOnly.h index 642151ab34..1c5da5db03 100644 --- a/CoreFoundation/Base.subproj/ForSwiftFoundationOnly.h +++ b/CoreFoundation/Base.subproj/ForSwiftFoundationOnly.h @@ -30,6 +30,7 @@ #include #include #include +#include #if TARGET_OS_WIN32 #define NOMINMAX diff --git a/CoreFoundation/Locale.subproj/CFListFormatter.h b/CoreFoundation/Locale.subproj/CFListFormatter.h index fffa436eaa..5a4f5296ff 100644 --- a/CoreFoundation/Locale.subproj/CFListFormatter.h +++ b/CoreFoundation/Locale.subproj/CFListFormatter.h @@ -22,7 +22,10 @@ typedef struct CF_BRIDGED_TYPE(id) __CFListFormatter *CFListFormatterRef; CF_EXPORT CFTypeID _CFListFormatterGetTypeID(void); +CF_EXPORT CFListFormatterRef _Nullable _CFListFormatterCreate(CFAllocatorRef allocator, CFLocaleRef locale); + +CF_EXPORT CFStringRef _Nullable _CFListFormatterCreateStringByJoiningStrings(CFAllocatorRef allocator, CFListFormatterRef formatter, const CFArrayRef strings); CF_ASSUME_NONNULL_END From c7a846b724e02d18996c000332f3a84947edee43 Mon Sep 17 00:00:00 2001 From: woxtu Date: Tue, 11 Jul 2023 00:34:30 +0000 Subject: [PATCH 2/3] Add `ListFormatter` --- Foundation.xcodeproj/project.pbxproj | 8 ++ Sources/Foundation/CMakeLists.txt | 1 + Sources/Foundation/ListFormatter.swift | 94 ++++++++++++++++++ Tests/Foundation/CMakeLists.txt | 1 + .../Foundation/Tests/TestListFormatter.swift | 97 +++++++++++++++++++ Tests/Foundation/main.swift | 1 + 6 files changed, 202 insertions(+) create mode 100644 Sources/Foundation/ListFormatter.swift create mode 100644 Tests/Foundation/Tests/TestListFormatter.swift diff --git a/Foundation.xcodeproj/project.pbxproj b/Foundation.xcodeproj/project.pbxproj index 4f3e98bd42..029fcf1912 100644 --- a/Foundation.xcodeproj/project.pbxproj +++ b/Foundation.xcodeproj/project.pbxproj @@ -404,6 +404,8 @@ 90E645DF1E4C89A400D0D47C /* TestNSCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90E645DE1E4C89A400D0D47C /* TestNSCache.swift */; }; 91B668A32252B3C5001487A1 /* FileManager+POSIX.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B668A22252B3C5001487A1 /* FileManager+POSIX.swift */; }; 91B668A52252B3E7001487A1 /* FileManager+Win32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91B668A42252B3E7001487A1 /* FileManager+Win32.swift */; }; + 9ED688592A5CD6F000CEBE96 /* ListFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ED688582A5CD6F000CEBE96 /* ListFormatter.swift */; }; + 9ED6885B2A5CD71400CEBE96 /* TestListFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ED6885A2A5CD71400CEBE96 /* TestListFormatter.swift */; }; 9F0DD3521ECD73D000F68030 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0041781ECD5962004138BD /* main.swift */; }; 9F0DD3571ECD783500F68030 /* SwiftFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B5D885D1BBC938800234F36 /* SwiftFoundation.framework */; }; A058C2021E529CF100B07AA1 /* TestMassFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A058C2011E529CF100B07AA1 /* TestMassFormatter.swift */; }; @@ -1134,6 +1136,8 @@ 90E645DE1E4C89A400D0D47C /* TestNSCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSCache.swift; sourceTree = ""; }; 91B668A22252B3C5001487A1 /* FileManager+POSIX.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+POSIX.swift"; sourceTree = ""; }; 91B668A42252B3E7001487A1 /* FileManager+Win32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Win32.swift"; sourceTree = ""; }; + 9ED688582A5CD6F000CEBE96 /* ListFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListFormatter.swift; sourceTree = ""; }; + 9ED6885A2A5CD71400CEBE96 /* TestListFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestListFormatter.swift; sourceTree = ""; }; 9F0041781ECD5962004138BD /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 9F0DD33F1ECD734200F68030 /* xdgTestHelper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = xdgTestHelper.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9F0DD34F1ECD737B00F68030 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1894,6 +1898,7 @@ 3EA9D66F1EF0532D00B362D6 /* TestJSONEncoder.swift */, 5EB6A15C1C188FC40037DCB8 /* TestJSONSerialization.swift */, BD8042151E09857800487EB8 /* TestLengthFormatter.swift */, + 9ED6885A2A5CD71400CEBE96 /* TestListFormatter.swift */, A058C2011E529CF100B07AA1 /* TestMassFormatter.swift */, 7D8BD738225ED1480057CF37 /* TestMeasurement.swift */, BF8E65301DC3B3CB005AB5C3 /* TestNotification.swift */, @@ -2161,6 +2166,7 @@ EADE0B641BD15DFF00C49C64 /* JSONSerialization.swift */, 49D55FA025E84FE5007BD3B3 /* JSONSerialization+Parser.swift */, EADE0B661BD15DFF00C49C64 /* LengthFormatter.swift */, + 9ED688582A5CD6F000CEBE96 /* ListFormatter.swift */, 5BD70FB11D3D4CDC003B9BF8 /* Locale.swift */, EADE0B681BD15DFF00C49C64 /* MassFormatter.swift */, 5BECBA371D1CAD7000B39B1F /* Measurement.swift */, @@ -2993,6 +2999,7 @@ AA9E0E0B21FA6C5600963F4C /* PropertyListEncoder.swift in Sources */, 5BD70FB41D3D4F8B003B9BF8 /* Calendar.swift in Sources */, 5BA9BEBD1CF4F3B8009DBD6C /* Notification.swift in Sources */, + 9ED6885B2A5CD71400CEBE96 /* TestListFormatter.swift in Sources */, 5BD70FB21D3D4CDC003B9BF8 /* Locale.swift in Sources */, EADE0BB71BD15E0000C49C64 /* Stream.swift in Sources */, 5BF7AEBF1BCD51F9008F214A /* NSURL.swift in Sources */, @@ -3015,6 +3022,7 @@ EADE0BB31BD15E0000C49C64 /* NSRegularExpression.swift in Sources */, EADE0BA41BD15E0000C49C64 /* LengthFormatter.swift in Sources */, 5BDC3FCA1BCF176100ED97BB /* NSCFArray.swift in Sources */, + 9ED688592A5CD6F000CEBE96 /* ListFormatter.swift in Sources */, EADE0BB21BD15E0000C49C64 /* Progress.swift in Sources */, EADE0B961BD15DFF00C49C64 /* DateIntervalFormatter.swift in Sources */, 5B5BFEAC1E6CC0C200AC8D9E /* NSCFBoolean.swift in Sources */, diff --git a/Sources/Foundation/CMakeLists.txt b/Sources/Foundation/CMakeLists.txt index 398fbfc5e9..2039e65736 100644 --- a/Sources/Foundation/CMakeLists.txt +++ b/Sources/Foundation/CMakeLists.txt @@ -47,6 +47,7 @@ add_library(Foundation JSONSerialization.swift JSONSerialization+Parser.swift LengthFormatter.swift + ListFormatter.swift Locale.swift MassFormatter.swift Measurement.swift diff --git a/Sources/Foundation/ListFormatter.swift b/Sources/Foundation/ListFormatter.swift new file mode 100644 index 0000000000..83cf55ab86 --- /dev/null +++ b/Sources/Foundation/ListFormatter.swift @@ -0,0 +1,94 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// + +@_implementationOnly import CoreFoundation + +/* NSListFormatter provides locale-correct formatting of a list of items using the appropriate separator and conjunction. Note that the list formatter is unaware of the context where the joined string will be used, e.g., in the beginning of the sentence or used as a standalone string in the UI, so it will not provide any sort of capitalization customization on the given items, but merely join them as-is. The string joined this way may not be grammatically correct when placed in a sentence, and it should only be used in a standalone manner. +*/ +open class ListFormatter: Formatter { + private let cfFormatter: CFListFormatter + + /* Specifies the locale to format the items. Defaults to autoupdatingCurrentLocale. Also resets to autoupdatingCurrentLocale on assignment of nil. + */ + open var locale: Locale! = .autoupdatingCurrent + + /* Specifies how each object should be formatted. If not set, the object is formatted using its instance method in the following order: -descriptionWithLocale:, -localizedDescription, and -description. + */ + /*@NSCopying*/ open var itemFormatter: Formatter? + + public override init() { + self.cfFormatter = _CFListFormatterCreate(kCFAllocatorSystemDefault, CFLocaleCopyCurrent())! + super.init() + } + + public required init?(coder: NSCoder) { + self.cfFormatter = _CFListFormatterCreate(kCFAllocatorSystemDefault, CFLocaleCopyCurrent())! + super.init(coder: coder) + } + + open override func copy(with zone: NSZone? = nil) -> Any { + let copied = ListFormatter() + copied.locale = locale + copied.itemFormatter = itemFormatter?.copy(with: zone) as? Formatter + return copied + } + + /* Convenience method to return a string constructed from an array of strings using the list format specific to the current locale. It is recommended to join only disjointed strings that are ready to display in a bullet-point list. Sentences, phrases with punctuations, and appositions may not work well when joined together. + */ + open class func localizedString(byJoining strings: [String]) -> String { + let formatter = ListFormatter() + return formatter.string(from: strings)! + } + + /* Convenience method for -stringForObjectValue:. Returns a string constructed from an array in the locale-aware format. Each item is formatted using the itemFormatter. If the itemFormatter does not apply to a particular item, the method will fall back to the item's -descriptionWithLocale: or -localizedDescription if implemented, or -description if not. + + Returns nil if `items` is nil or if the list formatter cannot generate a string representation for all items in the array. + */ + open func string(from items: [Any]) -> String? { + let strings = items.map { item in + if let string = itemFormatter?.string(for: item) { + return string + } + + // Use the item’s `description(withLocale:)` if implemented + if let item = item as? NSArray { + return item.description(withLocale: locale) + } else if let item = item as? NSDecimalNumber { + return item.description(withLocale: locale) + } else if let item = item as? NSDictionary { + return item.description(withLocale: locale) + } else if let item = item as? NSNumber { + return item.description(withLocale: locale) + } else if let item = item as? NSOrderedSet { + return item.description(withLocale: locale) + } else if let item = item as? NSSet { + return item.description(withLocale: locale) + } + + // Use the item’s `localizedDescription` if implemented + if let item = item as? Error { + return item.localizedDescription + } + + return String(describing: item) + } + + return _CFListFormatterCreateStringByJoiningStrings(kCFAllocatorSystemDefault, cfFormatter, strings._cfObject)?._swiftObject + } + + /* Inherited from NSFormatter. `obj` must be an instance of NSArray. Returns nil if `obj` is nil, not an instance of NSArray, or if the list formatter cannot generate a string representation for all objects in the array. + */ + open override func string(for obj: Any?) -> String? { + guard let list = obj as? [Any] else { + return nil + } + + return string(from: list) + } +} diff --git a/Tests/Foundation/CMakeLists.txt b/Tests/Foundation/CMakeLists.txt index abe9b8b48a..6755fefcda 100644 --- a/Tests/Foundation/CMakeLists.txt +++ b/Tests/Foundation/CMakeLists.txt @@ -42,6 +42,7 @@ target_sources(TestFoundation PRIVATE Tests/TestJSONEncoder.swift Tests/TestJSONSerialization.swift Tests/TestLengthFormatter.swift + Tests/TestListFormatter.swift Tests/TestMassFormatter.swift Tests/TestMeasurement.swift Tests/TestNotificationCenter.swift diff --git a/Tests/Foundation/Tests/TestListFormatter.swift b/Tests/Foundation/Tests/TestListFormatter.swift new file mode 100644 index 0000000000..7a05794987 --- /dev/null +++ b/Tests/Foundation/Tests/TestListFormatter.swift @@ -0,0 +1,97 @@ +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See http://swift.org/LICENSE.txt for license information +// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// + +#if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT + #if canImport(SwiftFoundation) && !DEPLOYMENT_RUNTIME_OBJC + @testable import SwiftFoundation + #else + @testable import Foundation + #endif +#endif + +class TestListFormatter: XCTestCase { + private var formatter: ListFormatter! + + override func setUp() { + super.setUp() + + formatter = ListFormatter() + } + + override func tearDown() { + formatter = nil + + super.tearDown() + } + + func test_copy() throws { + formatter.itemFormatter = NumberFormatter() + + let copied = try XCTUnwrap(formatter.copy() as? ListFormatter) + XCTAssertEqual(formatter.locale, copied.locale) + XCTAssert(copied.itemFormatter is NumberFormatter) + + copied.locale = Locale(identifier: "en_US_POSIX") + copied.itemFormatter = DateFormatter() + XCTAssertNotEqual(formatter.locale, copied.locale) + XCTAssert(formatter.itemFormatter is NumberFormatter) + XCTAssertFalse(copied.itemFormatter is NumberFormatter) + } + + func test_stringFromItemsWithItemFormatter() throws { + let numberFormatter = NumberFormatter() + numberFormatter.locale = Locale(identifier: "en_US_POSIX") + numberFormatter.numberStyle = .percent + + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.itemFormatter = numberFormatter + XCTAssertEqual(formatter.string(from: [1, 2, 3]), "100%, 200%, and 300%") + } + + func test_stringFromDescriptionsWithLocale() throws { + formatter.locale = Locale(identifier: "en_US") + XCTAssertEqual(formatter.string(from: [1000, 2000, 3000]), "1,000, 2,000, and 3,000") + } + + func test_stringFromLocalizedDescriptions() throws { + struct Item: LocalizedError { + let errorDescription: String? = "item" + } + + formatter.locale = Locale(identifier: "en_US_POSIX") + XCTAssertEqual(formatter.string(from: [Item(), Item(), Item()]), "item, item, and item") + } + + func test_stringFromItems() throws { + struct Item {} + + formatter.locale = Locale(identifier: "en_US_POSIX") + XCTAssertEqual(formatter.string(from: [Item(), Item(), Item()]), "Item(), Item(), and Item()") + } + + func test_stringForList() throws { + XCTAssertEqual(formatter.string(for: [42]), "42") + } + + func test_stringForNonList() throws { + XCTAssertNil(formatter.string(for: 42)) + } + + static var allTests: [(String, (TestListFormatter) -> () throws -> Void)] { + return [ + ("test_copy", test_copy), + ("test_stringFromItemsWithItemFormatter", test_stringFromItemsWithItemFormatter), + ("test_stringFromDescriptionsWithLocale", test_stringFromDescriptionsWithLocale), + ("test_stringFromLocalizedDescriptions", test_stringFromLocalizedDescriptions), + ("test_stringFromItems", test_stringFromItems), + ("test_stringForList", test_stringForList), + ("test_stringForNonList", test_stringForNonList), + ] + } +} diff --git a/Tests/Foundation/main.swift b/Tests/Foundation/main.swift index e6c7b35bc1..ad126b8172 100644 --- a/Tests/Foundation/main.swift +++ b/Tests/Foundation/main.swift @@ -60,6 +60,7 @@ var allTestCases = [ testCase(TestNSKeyedArchiver.allTests), testCase(TestNSKeyedUnarchiver.allTests), testCase(TestLengthFormatter.allTests), + testCase(TestListFormatter.allTests), testCase(TestNSLocale.allTests), testCase(TestNotificationCenter.allTests), testCase(TestNotificationQueue.allTests), From 13542f757032c9bdf664ad0595cee50e1c0afff1 Mon Sep 17 00:00:00 2001 From: woxtu Date: Wed, 12 Jul 2023 11:51:44 +0000 Subject: [PATCH 3/3] Respect the locale --- .../Locale.subproj/CFListFormatter.c | 27 +++++++++++++++++++ .../Locale.subproj/CFListFormatter.h | 3 +++ Sources/Foundation/ListFormatter.swift | 7 ++++- .../Foundation/Tests/TestListFormatter.swift | 11 ++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/CoreFoundation/Locale.subproj/CFListFormatter.c b/CoreFoundation/Locale.subproj/CFListFormatter.c index 83366dda96..319f234bca 100644 --- a/CoreFoundation/Locale.subproj/CFListFormatter.c +++ b/CoreFoundation/Locale.subproj/CFListFormatter.c @@ -17,9 +17,22 @@ #define BUFFER_SIZE 256 #define RESULT_BUFFER_SIZE 768 +#if TARGET_OS_WASI +#define LOCK() do {} while (0) +#define UNLOCK() do {} while (0) +#else +#include + +#define LOCK() do { dispatch_semaphore_wait(formatter->_lock, DISPATCH_TIME_FOREVER); } while(0) +#define UNLOCK() do { dispatch_semaphore_signal(formatter->_lock); } while(0) +#endif + struct __CFListFormatter { CFRuntimeBase _base; CFLocaleRef _locale; +#if !TARGET_OS_WASI + dispatch_semaphore_t _lock; +#endif }; static void __CFListFormatterDeallocate(CFTypeRef cf) { @@ -64,10 +77,24 @@ CFListFormatterRef _CFListFormatterCreate(CFAllocatorRef allocator, CFLocaleRef } memory->_locale = CFRetain(locale); +#if !TARGET_OS_WASI + memory->_lock = dispatch_semaphore_create(1); +#endif return memory; } +void _CFListFormatterSetLocale(CFListFormatterRef formatter, CFLocaleRef locale) { + assert(locale != NULL); + + LOCK(); + if (locale != formatter->_locale) { + CFRelease(formatter->_locale); + formatter->_locale = CFLocaleCreateCopy(kCFAllocatorSystemDefault, locale); + } + UNLOCK(); +} + CFStringRef _CFListFormatterCreateStringByJoiningStrings(CFAllocatorRef allocator, const CFListFormatterRef formatter, const CFArrayRef strings) { CFAssert1(strings != NULL, __kCFLogAssertion, "%s(): strings should not be NULL", __PRETTY_FUNCTION__); if (strings == NULL) { diff --git a/CoreFoundation/Locale.subproj/CFListFormatter.h b/CoreFoundation/Locale.subproj/CFListFormatter.h index 5a4f5296ff..94f46d3b3c 100644 --- a/CoreFoundation/Locale.subproj/CFListFormatter.h +++ b/CoreFoundation/Locale.subproj/CFListFormatter.h @@ -25,6 +25,9 @@ CFTypeID _CFListFormatterGetTypeID(void); CF_EXPORT CFListFormatterRef _Nullable _CFListFormatterCreate(CFAllocatorRef allocator, CFLocaleRef locale); +CF_EXPORT +void _CFListFormatterSetLocale(CFListFormatterRef formatter, CFLocaleRef locale); + CF_EXPORT CFStringRef _Nullable _CFListFormatterCreateStringByJoiningStrings(CFAllocatorRef allocator, CFListFormatterRef formatter, const CFArrayRef strings); diff --git a/Sources/Foundation/ListFormatter.swift b/Sources/Foundation/ListFormatter.swift index 83cf55ab86..fe2ad878f0 100644 --- a/Sources/Foundation/ListFormatter.swift +++ b/Sources/Foundation/ListFormatter.swift @@ -16,7 +16,11 @@ open class ListFormatter: Formatter { /* Specifies the locale to format the items. Defaults to autoupdatingCurrentLocale. Also resets to autoupdatingCurrentLocale on assignment of nil. */ - open var locale: Locale! = .autoupdatingCurrent + private var _locale: Locale = .autoupdatingCurrent + open var locale: Locale! { + get { return _locale } + set { _locale = newValue ?? .autoupdatingCurrent } + } /* Specifies how each object should be formatted. If not set, the object is formatted using its instance method in the following order: -descriptionWithLocale:, -localizedDescription, and -description. */ @@ -79,6 +83,7 @@ open class ListFormatter: Formatter { return String(describing: item) } + _CFListFormatterSetLocale(cfFormatter, locale._cfObject) return _CFListFormatterCreateStringByJoiningStrings(kCFAllocatorSystemDefault, cfFormatter, strings._cfObject)?._swiftObject } diff --git a/Tests/Foundation/Tests/TestListFormatter.swift b/Tests/Foundation/Tests/TestListFormatter.swift index 7a05794987..bdb4102e6e 100644 --- a/Tests/Foundation/Tests/TestListFormatter.swift +++ b/Tests/Foundation/Tests/TestListFormatter.swift @@ -30,6 +30,16 @@ class TestListFormatter: XCTestCase { super.tearDown() } + func test_locale() throws { + XCTAssertEqual(formatter.locale, Locale.autoupdatingCurrent) + + formatter.locale = Locale(identifier: "en_US_POSIX") + XCTAssertEqual(formatter.locale, Locale(identifier: "en_US_POSIX")) + + formatter.locale = nil + XCTAssertEqual(formatter.locale, Locale.autoupdatingCurrent) + } + func test_copy() throws { formatter.itemFormatter = NumberFormatter() @@ -85,6 +95,7 @@ class TestListFormatter: XCTestCase { static var allTests: [(String, (TestListFormatter) -> () throws -> Void)] { return [ + ("test_locale", test_locale), ("test_copy", test_copy), ("test_stringFromItemsWithItemFormatter", test_stringFromItemsWithItemFormatter), ("test_stringFromDescriptionsWithLocale", test_stringFromDescriptionsWithLocale),