From 430a5c1ced7666d33095c0e67cf36c761384ca6b Mon Sep 17 00:00:00 2001 From: Lily Vulcano Date: Mon, 22 Jul 2019 10:04:09 -0700 Subject: [PATCH] =?UTF-8?q?Parity:=20NSString.enumerateSubstrings(?= =?UTF-8?q?=E2=80=A6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement those flags that do not require Darwin frameworks, and diagnose use of the rest. --- Foundation/NSString.swift | 94 +++++++++++- TestFoundation/TestNSString.swift | 230 ++++++++++++++++++++---------- 2 files changed, 251 insertions(+), 73 deletions(-) diff --git a/Foundation/NSString.swift b/Foundation/NSString.swift index 65bec45c97..4afcea51da 100644 --- a/Foundation/NSString.swift +++ b/Foundation/NSString.swift @@ -74,8 +74,13 @@ extension NSString { public static let byLines = EnumerationOptions(rawValue: 0) public static let byParagraphs = EnumerationOptions(rawValue: 1) public static let byComposedCharacterSequences = EnumerationOptions(rawValue: 2) + + @available(*, unavailable, message: "Enumeration by words isn't supported in swift-corelibs-foundation") public static let byWords = EnumerationOptions(rawValue: 3) + + @available(*, unavailable, message: "Enumeration by sentences isn't supported in swift-corelibs-foundation") public static let bySentences = EnumerationOptions(rawValue: 4) + public static let reverse = EnumerationOptions(rawValue: 1 << 8) public static let substringNotRequired = EnumerationOptions(rawValue: 1 << 9) public static let localized = EnumerationOptions(rawValue: 1 << 10) @@ -825,8 +830,95 @@ extension NSString { return NSRange(location: start, length: parEnd - start) } + private enum EnumerateBy { + case lines + case paragraphs + case composedCharacterSequences + + init?(parsing options: EnumerationOptions) { + var me: EnumerateBy? + + // We don't test for .byLines because .byLines.rawValue == 0, which unfortunately means _every_ NSString.EnumerationOptions contains .byLines. + // Instead, we just default to .lines below. + + if options.contains(.byParagraphs) { + guard me == nil else { return nil } + me = .paragraphs + } + + if options.contains(.byComposedCharacterSequences) { + guard me == nil else { return nil } + me = .composedCharacterSequences + } + + self = me ?? .lines + } + } + public func enumerateSubstrings(in range: NSRange, options opts: EnumerationOptions = [], using block: (String?, NSRange, NSRange, UnsafeMutablePointer) -> Void) { - NSUnimplemented() + guard let enumerateBy = EnumerateBy(parsing: opts) else { + fatalError("You must specify only one of the .by… enumeration options.") + } + + // We do not heed the .localized flag because it affects only by-words and by-sentences enumeration, which we do not support in s-c-f. + + var currentIndex = opts.contains(.reverse) ? length - 1 : 0 + + func shouldContinue() -> Bool { + opts.contains(.reverse) ? currentIndex >= 0 : currentIndex < length + } + + let reverse = opts.contains(.reverse) + + func nextIndex(after fullRange: NSRange, compensatingForLengthDifferenceFrom oldLength: Int) -> Int { + var index = reverse ? fullRange.location - 1 : fullRange.location + fullRange.length + + if !reverse { + index += (oldLength - length) + } + + return index + } + + while shouldContinue() { + var range = NSRange(location: currentIndex, length: 0) + var fullRange = range + + switch enumerateBy { + case .lines: + var start = 0, end = 0, contentsEnd = 0 + getLineStart(&start, end: &end, contentsEnd: &contentsEnd, for: range) + range.location = start + range.length = contentsEnd - start + fullRange.location = start + fullRange.length = end - start + case .paragraphs: + var start = 0, end = 0, contentsEnd = 0 + getParagraphStart(&start, end: &end, contentsEnd: &contentsEnd, for: range) + range.location = start + range.length = contentsEnd - start + fullRange.location = start + fullRange.length = end - start + case .composedCharacterSequences: + range = rangeOfComposedCharacterSequences(for: range) + fullRange = range + } + + var substring: String? + if !opts.contains(.substringNotRequired) { + substring = self.substring(with: range) + } + + let oldLength = length + + var stop: ObjCBool = false + block(substring, range, fullRange, &stop) + if stop.boolValue { + return + } + + currentIndex = nextIndex(after: fullRange, compensatingForLengthDifferenceFrom: oldLength) + } } public func enumerateLines(_ block: (String, UnsafeMutablePointer) -> Void) { diff --git a/TestFoundation/TestNSString.swift b/TestFoundation/TestNSString.swift index 70372b7a6a..1c3c94c2fc 100755 --- a/TestFoundation/TestNSString.swift +++ b/TestFoundation/TestNSString.swift @@ -28,78 +28,6 @@ internal let kCFStringEncodingUTF32LE = CFStringBuiltInEncodings.UTF32LE.rawVal class TestNSString: LoopbackServerTest { - - static var allTests: [(String, (TestNSString) -> () throws -> Void)] { - return [ - ("test_initData", test_initData), - ("test_boolValue", test_boolValue ), - ("test_BridgeConstruction", test_BridgeConstruction ), - ("test_integerValue", test_integerValue ), - ("test_intValue", test_intValue ), - ("test_doubleValue", test_doubleValue), - ("test_isEqualToStringWithSwiftString", test_isEqualToStringWithSwiftString ), - ("test_isEqualToObjectWithNSString", test_isEqualToObjectWithNSString ), - ("test_isNotEqualToObjectWithNSNumber", test_isNotEqualToObjectWithNSNumber ), - ("test_FromASCIIData", test_FromASCIIData ), - ("test_FromUTF8Data", test_FromUTF8Data ), - ("test_FromMalformedUTF8Data", test_FromMalformedUTF8Data ), - ("test_FromASCIINSData", test_FromASCIINSData ), - ("test_FromUTF8NSData", test_FromUTF8NSData ), - ("test_FromMalformedUTF8NSData", test_FromMalformedUTF8NSData ), - ("test_FromNullTerminatedCStringInASCII", test_FromNullTerminatedCStringInASCII ), - ("test_FromNullTerminatedCStringInUTF8", test_FromNullTerminatedCStringInUTF8 ), - ("test_FromMalformedNullTerminatedCStringInUTF8", test_FromMalformedNullTerminatedCStringInUTF8 ), - ("test_uppercaseString", test_uppercaseString ), - ("test_lowercaseString", test_lowercaseString ), - ("test_capitalizedString", test_capitalizedString ), - ("test_longLongValue", test_longLongValue ), - ("test_rangeOfCharacterFromSet", test_rangeOfCharacterFromSet ), - ("test_CFStringCreateMutableCopy", test_CFStringCreateMutableCopy), - /* ⚠️ */ ("test_FromContentsOfURL", testExpectedToFail(test_FromContentsOfURL, - /* ⚠️ */ "test_FromContentsOfURL is flaky on CI, with unclear causes. https://bugs.swift.org/browse/SR-10514")), - ("test_FromContentOfFileUsedEncodingIgnored", test_FromContentOfFileUsedEncodingIgnored), - ("test_FromContentOfFileUsedEncodingUTF8", test_FromContentOfFileUsedEncodingUTF8), - ("test_FromContentsOfURLUsedEncodingUTF16BE", test_FromContentsOfURLUsedEncodingUTF16BE), - ("test_FromContentsOfURLUsedEncodingUTF16LE", test_FromContentsOfURLUsedEncodingUTF16LE), - ("test_FromContentsOfURLUsedEncodingUTF32BE", test_FromContentsOfURLUsedEncodingUTF32BE), - ("test_FromContentsOfURLUsedEncodingUTF32LE", test_FromContentsOfURLUsedEncodingUTF32LE), - ("test_FromContentOfFile",test_FromContentOfFile), - ("test_swiftStringUTF16", test_swiftStringUTF16), - // This test takes forever on build servers; it has been seen up to 1852.084 seconds -// ("test_completePathIntoString", test_completePathIntoString), - ("test_stringByTrimmingCharactersInSet", test_stringByTrimmingCharactersInSet), - ("test_initializeWithFormat", test_initializeWithFormat), - ("test_initializeWithFormat2", test_initializeWithFormat2), - ("test_initializeWithFormat3", test_initializeWithFormat3), - ("test_appendingPathComponent", test_appendingPathComponent), - ("test_deletingLastPathComponent", test_deletingLastPathComponent), - ("test_getCString_simple", test_getCString_simple), - ("test_getCString_nonASCII_withASCIIAccessor", test_getCString_nonASCII_withASCIIAccessor), - ("test_NSHomeDirectoryForUser", test_NSHomeDirectoryForUser), - ("test_resolvingSymlinksInPath", test_resolvingSymlinksInPath), - ("test_expandingTildeInPath", test_expandingTildeInPath), - ("test_standardizingPath", test_standardizingPath), - ("test_addingPercentEncoding", test_addingPercentEncoding), - ("test_removingPercentEncodingInLatin", test_removingPercentEncodingInLatin), - ("test_removingPercentEncodingInNonLatin", test_removingPercentEncodingInNonLatin), - ("test_removingPersentEncodingWithoutEncoding", test_removingPersentEncodingWithoutEncoding), - ("test_addingPercentEncodingAndBack", test_addingPercentEncodingAndBack), - ("test_stringByAppendingPathExtension", test_stringByAppendingPathExtension), - ("test_deletingPathExtension", test_deletingPathExtension), - ("test_ExternalRepresentation", test_ExternalRepresentation), - ("test_mutableStringConstructor", test_mutableStringConstructor), - ("test_emptyStringPrefixAndSuffix",test_emptyStringPrefixAndSuffix), - ("test_reflection", { _ in test_reflection }), - ("test_replacingOccurrences", test_replacingOccurrences), - ("test_getLineStart", test_getLineStart), - ("test_substringWithRange", test_substringWithRange), - ("test_substringFromCFString", test_substringFromCFString), - ("test_createCopy", test_createCopy), - ("test_commonPrefix", test_commonPrefix), - ("test_lineRangeFor", test_lineRangeFor), - ("test_fileSystemRepresentation", test_fileSystemRepresentation), - ] - } func test_initData() { let testString = "\u{00} This is a test string" @@ -1424,4 +1352,162 @@ extension TestNSString { result.deallocate() #endif } + + func test_enumerateSubstrings() { + // http://www.gutenberg.org/ebooks/12389, with a prefix addition by me. + // U+0300 COMBINING ACUTE ACCENT; + // U+0085 NEXT LINE (creates a new line that's not a new paragraph) + let text = "Questo e\u{0300} un poema.\n---\nCyprus, Paphos, or Panormus\u{0085}May detain thee with their splendour\u{0085}Of oblations on thine altars,\u{0085}O imperial Aphrodite.\n\nYet do thou regard, with pity\u{0085}For a nameless child of passion,\u{0085}This small unfrequented valley\u{0085}By the sea, O sea-born mother.\u{0085}" + let nstext = text as NSString + + let graphemes = ["Q", "u", "e", "s", "t", "o", " ", "e\u{0300}", " ", "u", "n", " ", "p", "o", "e", "m", "a", ".", "\n", "-", "-", "-", "\n", "C", "y", "p", "r", "u", "s", ",", " ", "P", "a", "p", "h", "o", "s", ",", " ", "o", "r", " ", "P", "a", "n", "o", "r", "m", "u", "s", "\u{0085}", "M", "a", "y", " ", "d", "e", "t", "a", "i", "n", " ", "t", "h", "e", "e", " ", "w", "i", "t", "h", " ", "t", "h", "e", "i", "r", " ", "s", "p", "l", "e", "n", "d", "o", "u", "r", "\u{0085}", "O", "f", " ", "o", "b", "l", "a", "t", "i", "o", "n", "s", " ", "o", "n", " ", "t", "h", "i", "n", "e", " ", "a", "l", "t", "a", "r", "s", ",", "\u{0085}", "O", " ", "i", "m", "p", "e", "r", "i", "a", "l", " ", "A", "p", "h", "r", "o", "d", "i", "t", "e", ".", "\n", "\n", "Y", "e", "t", " ", "d", "o", " ", "t", "h", "o", "u", " ", "r", "e", "g", "a", "r", "d", ",", " ", "w", "i", "t", "h", " ", "p", "i", "t", "y", "\u{0085}", "F", "o", "r", " ", "a", " ", "n", "a", "m", "e", "l", "e", "s", "s", " ", "c", "h", "i", "l", "d", " ", "o", "f", " ", "p", "a", "s", "s", "i", "o", "n", ",", "\u{0085}", "T", "h", "i", "s", " ", "s", "m", "a", "l", "l", " ", "u", "n", "f", "r", "e", "q", "u", "e", "n", "t", "e", "d", " ", "v", "a", "l", "l", "e", "y", "\u{0085}", "B", "y", " ", "t", "h", "e", " ", "s", "e", "a", ",", " ", "O", " ", "s", "e", "a", "-", "b", "o", "r", "n", " ", "m", "o", "t", "h", "e", "r", ".", "\u{0085}"] + + let lines = ["Questo e\u{0300} un poema.", "---", "Cyprus, Paphos, or Panormus", "May detain thee with their splendour", "Of oblations on thine altars,", "O imperial Aphrodite.", "", "Yet do thou regard, with pity", "For a nameless child of passion,", "This small unfrequented valley", "By the sea, O sea-born mother."] + + let paragraphs = ["Questo è un poema.", "---", "Cyprus, Paphos, or Panormus\u{0085}May detain thee with their splendour\u{0085}Of oblations on thine altars,\u{0085}O imperial Aphrodite.", "", "Yet do thou regard, with pity\u{0085}For a nameless child of passion,\u{0085}This small unfrequented valley\u{0085}By the sea, O sea-born mother.\u{0085}"] + + enum Result { + case substrings([String]) + case count(Int) + } + + let expectations: [(options: NSString.EnumerationOptions, result: Result)] = [ + (options: [.byComposedCharacterSequences], + result: .substrings(graphemes)), + (options: [.byComposedCharacterSequences, .reverse], + result: .substrings(graphemes.reversed())), + (options: [.byComposedCharacterSequences, .substringNotRequired], + result: .count(graphemes.count)), + (options: [.byComposedCharacterSequences, .substringNotRequired, .reverse], + result: .count(graphemes.count)), + (options: [.byLines], + result: .substrings(lines)), + (options: [.byLines, .reverse], + result: .substrings(lines.reversed())), + (options: [.byLines, .substringNotRequired], + result: .count(lines.count)), + (options: [.byLines, .substringNotRequired, .reverse], + result: .count(lines.count)), + (options: [.byParagraphs], + result: .substrings(paragraphs)), + (options: [.byParagraphs, .reverse], + result: .substrings(paragraphs.reversed())), + (options: [.byParagraphs, .substringNotRequired], + result: .count(paragraphs.count)), + (options: [.byParagraphs, .substringNotRequired, .reverse], + result: .count(paragraphs.count)), + ] + + for expectation in expectations { + var substrings: [String] = [] + let requiresSubstrings = !expectation.options.contains(.substringNotRequired) + + var hasFailedSubstringPresence = false + var count = 0 + + nstext.enumerateSubstrings(in: NSMakeRange(0, nstext.length), options: expectation.options) { (substring, range, fullRange, stop) in + // TODO: range, fullRange + + count += 1 + + if requiresSubstrings { + XCTAssertNotNil(substring, "Testing with options: \(expectation.options)") + if let substring = substring { + substrings.append(substring) + } else { + hasFailedSubstringPresence = true + } + } else { + XCTAssertNil(substring, "Testing with options: \(expectation.options)") + if substring != nil { + hasFailedSubstringPresence = true + } + } + } + + if !hasFailedSubstringPresence { + switch expectation.result { + case .count(let expectedCount): + XCTAssertEqual(count, expectedCount, "Testing with options: \(expectation.options)") + case .substrings(let expectedSubstrings): + XCTAssertEqual(substrings, expectedSubstrings, "Testing with options: \(expectation.options)") + } + } + } + } + + static var allTests: [(String, (TestNSString) -> () throws -> Void)] { + return [ + ("test_initData", test_initData), + ("test_boolValue", test_boolValue ), + ("test_BridgeConstruction", test_BridgeConstruction ), + ("test_integerValue", test_integerValue ), + ("test_intValue", test_intValue ), + ("test_doubleValue", test_doubleValue), + ("test_isEqualToStringWithSwiftString", test_isEqualToStringWithSwiftString ), + ("test_isEqualToObjectWithNSString", test_isEqualToObjectWithNSString ), + ("test_isNotEqualToObjectWithNSNumber", test_isNotEqualToObjectWithNSNumber ), + ("test_FromASCIIData", test_FromASCIIData ), + ("test_FromUTF8Data", test_FromUTF8Data ), + ("test_FromMalformedUTF8Data", test_FromMalformedUTF8Data ), + ("test_FromASCIINSData", test_FromASCIINSData ), + ("test_FromUTF8NSData", test_FromUTF8NSData ), + ("test_FromMalformedUTF8NSData", test_FromMalformedUTF8NSData ), + ("test_FromNullTerminatedCStringInASCII", test_FromNullTerminatedCStringInASCII ), + ("test_FromNullTerminatedCStringInUTF8", test_FromNullTerminatedCStringInUTF8 ), + ("test_FromMalformedNullTerminatedCStringInUTF8", test_FromMalformedNullTerminatedCStringInUTF8 ), + ("test_uppercaseString", test_uppercaseString ), + ("test_lowercaseString", test_lowercaseString ), + ("test_capitalizedString", test_capitalizedString ), + ("test_longLongValue", test_longLongValue ), + ("test_rangeOfCharacterFromSet", test_rangeOfCharacterFromSet ), + ("test_CFStringCreateMutableCopy", test_CFStringCreateMutableCopy), + + /* ⚠️ */ ("test_FromContentsOfURL", testExpectedToFail(test_FromContentsOfURL, + /* ⚠️ */ "test_FromContentsOfURL is flaky on CI, with unclear causes. https://bugs.swift.org/browse/SR-10514")), + + ("test_FromContentOfFileUsedEncodingIgnored", test_FromContentOfFileUsedEncodingIgnored), + ("test_FromContentOfFileUsedEncodingUTF8", test_FromContentOfFileUsedEncodingUTF8), + ("test_FromContentsOfURLUsedEncodingUTF16BE", test_FromContentsOfURLUsedEncodingUTF16BE), + ("test_FromContentsOfURLUsedEncodingUTF16LE", test_FromContentsOfURLUsedEncodingUTF16LE), + ("test_FromContentsOfURLUsedEncodingUTF32BE", test_FromContentsOfURLUsedEncodingUTF32BE), + ("test_FromContentsOfURLUsedEncodingUTF32LE", test_FromContentsOfURLUsedEncodingUTF32LE), + ("test_FromContentOfFile",test_FromContentOfFile), + ("test_swiftStringUTF16", test_swiftStringUTF16), + // This test takes forever on build servers; it has been seen up to 1852.084 seconds + // ("test_completePathIntoString", test_completePathIntoString), + ("test_stringByTrimmingCharactersInSet", test_stringByTrimmingCharactersInSet), + ("test_initializeWithFormat", test_initializeWithFormat), + ("test_initializeWithFormat2", test_initializeWithFormat2), + ("test_initializeWithFormat3", test_initializeWithFormat3), + ("test_appendingPathComponent", test_appendingPathComponent), + ("test_deletingLastPathComponent", test_deletingLastPathComponent), + ("test_getCString_simple", test_getCString_simple), + ("test_getCString_nonASCII_withASCIIAccessor", test_getCString_nonASCII_withASCIIAccessor), + ("test_NSHomeDirectoryForUser", test_NSHomeDirectoryForUser), + ("test_resolvingSymlinksInPath", test_resolvingSymlinksInPath), + ("test_expandingTildeInPath", test_expandingTildeInPath), + ("test_standardizingPath", test_standardizingPath), + ("test_addingPercentEncoding", test_addingPercentEncoding), + ("test_removingPercentEncodingInLatin", test_removingPercentEncodingInLatin), + ("test_removingPercentEncodingInNonLatin", test_removingPercentEncodingInNonLatin), + ("test_removingPersentEncodingWithoutEncoding", test_removingPersentEncodingWithoutEncoding), + ("test_addingPercentEncodingAndBack", test_addingPercentEncodingAndBack), + ("test_stringByAppendingPathExtension", test_stringByAppendingPathExtension), + ("test_deletingPathExtension", test_deletingPathExtension), + ("test_ExternalRepresentation", test_ExternalRepresentation), + ("test_mutableStringConstructor", test_mutableStringConstructor), + ("test_emptyStringPrefixAndSuffix",test_emptyStringPrefixAndSuffix), + ("test_reflection", { _ in test_reflection }), + ("test_replacingOccurrences", test_replacingOccurrences), + ("test_getLineStart", test_getLineStart), + ("test_substringWithRange", test_substringWithRange), + ("test_substringFromCFString", test_substringFromCFString), + ("test_createCopy", test_createCopy), + ("test_commonPrefix", test_commonPrefix), + ("test_lineRangeFor", test_lineRangeFor), + ("test_fileSystemRepresentation", test_fileSystemRepresentation), + ("test_enumerateSubstrings", test_enumerateSubstrings), + ] + } }