简体   繁体   中英

How to split a string into substrings of equal length

So

split("There are fourty-eight characters in this string", 20)

should return

["There are fourty-eig", "ht characters in thi","s string"]

If I make currentIndex = string.startIndex and then try to advance() it further than a string.endIndex, I get "fatal error: can not increment endIndex" before I check if my currentIndex < string.endIndex so the code below doesn't work

var string = "12345"
var currentIndex = string.startIndex
currentIndex = advance(currentIndex, 6)
if currentIndex > string.endIndex {currentIndex = string.endIndex}

I just answered a similar question on SO and thought I can provide a more concise solution:

Swift 2

func split(str: String, _ count: Int) -> [String] {
    return 0.stride(to: str.characters.count, by: count).map { i -> String in
        let startIndex = str.startIndex.advancedBy(i)
        let endIndex   = startIndex.advancedBy(count, limit: str.endIndex)
        return str[startIndex..<endIndex]
    }
}

Swift 3

func split(_ str: String, _ count: Int) -> [String] {
    return stride(from: 0, to: str.characters.count, by: count).map { i -> String in
        let startIndex = str.index(str.startIndex, offsetBy: i)
        let endIndex   = str.index(startIndex, offsetBy: count, limitedBy: str.endIndex) ?? str.endIndex
        return str[startIndex..<endIndex]
    }
}

Swift 4

Changed to a while loop for better efficiency and made into a String's extension by popular request:

extension String {
    func split(by length: Int) -> [String] {
        var startIndex = self.startIndex
        var results = [Substring]()

        while startIndex < self.endIndex {
            let endIndex = self.index(startIndex, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex
            results.append(self[startIndex..<endIndex])
            startIndex = endIndex
        }

        return results.map { String($0) }
    }
}

This problem could be easily solved with just one pass through the characters sequence:

Swift 2.2

extension String {
    func splitByLength(length: Int) -> [String] {
        var result = [String]()
        var collectedCharacters = [Character]()
        collectedCharacters.reserveCapacity(length)
        var count = 0
        
        for character in self.characters {
            collectedCharacters.append(character)
            count += 1
            if (count == length) {
                // Reached the desired length
                count = 0
                result.append(String(collectedCharacters))
                collectedCharacters.removeAll(keepCapacity: true)
            }
        }
        
        // Append the remainder
        if !collectedCharacters.isEmpty {
            result.append(String(collectedCharacters))
        }
        
        return result
    }
}

let foo = "There are fourty-eight characters in this string"
foo.splitByLength(20)

Swift 3.0

extension String {
    func splitByLength(_ length: Int) -> [String] {
        var result = [String]()
        var collectedCharacters = [Character]()
        collectedCharacters.reserveCapacity(length)
        var count = 0
        
        for character in self.characters {
            collectedCharacters.append(character)
            count += 1
            if (count == length) {
                // Reached the desired length
                count = 0
                result.append(String(collectedCharacters))
                collectedCharacters.removeAll(keepingCapacity: true)
            }
        }
        
        // Append the remainder
        if !collectedCharacters.isEmpty {
            result.append(String(collectedCharacters))
        }
        
        return result
    }
}

let foo = "There are fourty-eight characters in this string"
foo.splitByLength(20)

Since String is a pretty complicated type, ranges and indexes could have different computational costs depending on the view . These details are still evolving, thus the above one-pass solution might be a safer choice.

Hope this helps

String extension based on "Code Different" answer:

Swift 3/4/5

extension String {
    func components(withLength length: Int) -> [String] {
        return stride(from: 0, to: self.characters.count, by: length).map {
            let start = self.index(self.startIndex, offsetBy: $0)
            let end = self.index(start, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex
            return self[start..<end]
        }
    }
}

Usage

let str = "There are fourty-eight characters in this string"
let components = str.components(withLength: 20)

Swift 5 , based on @Ondrej Stocek solution

extension String {
    func components(withMaxLength length: Int) -> [String] {
        return stride(from: 0, to: self.count, by: length).map {
            let start = self.index(self.startIndex, offsetBy: $0)
            let end = self.index(start, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex
            return String(self[start..<end])
        }
    }
}

Here is a string extension you can use if you want to split a String at a certain length, but also take into account words:

Swift 4:

func splitByLength(_ length: Int, seperator: String) -> [String] {
    var result = [String]()
    var collectedWords = [String]()
    collectedWords.reserveCapacity(length)
    var count = 0
    let words = self.components(separatedBy: " ")

    for word in words {
        count += word.count + 1 //add 1 to include space
        if (count > length) {
            // Reached the desired length

            result.append(collectedWords.map { String($0) }.joined(separator: seperator) )
            collectedWords.removeAll(keepingCapacity: true)

            count = word.count
            collectedWords.append(word)
        } else {
            collectedWords.append(word)
        }
    }

    // Append the remainder
    if !collectedWords.isEmpty {
        result.append(collectedWords.map { String($0) }.joined(separator: seperator))
    }

    return result
}

This is a modification of Matteo Piombo 's answer above.

Usage

let message = "Here is a string that I want to split."
let message_lines = message.splitByLength(18, separator: " ")

//output: [ "Here is a string", "that I want to", "split." ]

You must not use range that exceeds the string size. The following method will demonstrates how to do it:

extension String {
    func split(len: Int) -> [String] {
        var currentIndex = 0
        var array = [String]()
        let length = self.characters.count
        while currentIndex < length {
            let startIndex = self.startIndex.advancedBy(currentIndex)
            let endIndex = startIndex.advancedBy(len, limit: self.endIndex)
            let substr = self.substringWithRange(Range(start: startIndex, end: endIndex))
            array.append(substr)
            currentIndex += len
        }
        return array
    }
}

Usage:

"There are fourty-eight characters in this string".split(20)
//output: ["There are fourty-eig", "ht characters in thi", "s string"]

or

"😀😁😂😃😄😅😆⛵".split(3)
//output: ["😀😁😂", "😃😄😅", "😆⛵"]

Edit: Updated the answer to work with Xcode 7 beta 6. The advance method is gone, replaced by advancedBy instance methods of Index . The advancedBy:limit: version is especially useful in this case.

My solution with an array of characters:

func split(text: String, count: Int) -> [String] {
    let chars = Array(text)
    return stride(from: 0, to: chars.count, by: count)
        .map { chars[$0 ..< min($0 + count, chars.count)] }
        .map { String($0) }
}

Or you can use more optimised variant for large strings with Substring :

func split(text: String, length: Int) -> [Substring] {
    return stride(from: 0, to: text.count, by: length)
        .map { text[text.index(text.startIndex, offsetBy: $0)..<text.index(text.startIndex, offsetBy: min($0 + length, text.count))] }
}

endIndex is not a valid index; it is one more than the valid range.

Here is a version, that works in the following situations:

  • the given row length is 0 or smaller
  • the input is empty
  • the last word of a line does not fit: the word is wrapped into a new line
  • the last word of a line is longer than the row length: the word is cut and wrapped partially
  • the last word of a line is longer than multiple lines: the word is cut and wrapped multiple times.
extension String {

    func ls_wrap(maxWidth: Int) -> [String] {
        guard maxWidth > 0 else {
            Logger.logError("wrap: maxWidth too small")
            return []
        }
        let addWord: (String, String) -> String = { (line: String, word: String) in
            line.isEmpty
                ? word
                : "\(line) \(word)"
        }
        let handleWord: (([String], String), String) -> ([String], String) = { (arg1: ([String], String), word: String) in
            let (acc, line): ([String], String) = arg1
            let lineWithWord: String = addWord(line, word)
            if lineWithWord.count <= maxWidth { // 'word' fits fine; append to 'line' and continue.
                return (acc, lineWithWord)
            } else if word.count > maxWidth { // 'word' doesn't fit in any way; split awkwardly.
                let splitted: [String] = lineWithWord.ls_chunks(of: maxWidth)
                let (intermediateLines, lastLine) = (splitted.ls_init, splitted.last!)
                return (acc + intermediateLines, lastLine)
            } else { // 'line' is full; start with 'word' and continue.
                return (acc + [line], word)
            }
        }
        let (accLines, lastLine) = ls_words().reduce(([],""), handleWord)
        return accLines + [lastLine]
    }
    
    // stolen from https://stackoverflow.com/questions/32212220/how-to-split-a-string-into-substrings-of-equal-length
    func ls_chunks(of length: Int) -> [String] {
        var startIndex = self.startIndex
        var results = [Substring]()
        while startIndex < self.endIndex {
            let endIndex = self.index(startIndex, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex
            results.append(self[startIndex..<endIndex])
            startIndex = endIndex
        }
        return results.map { String($0) }
    }
    
    // could be improved to split on whiteSpace instead of only " " and "\n"
    func ls_words() -> [String] {
        return split(separator: " ")
            .flatMap{ $0.split(separator: "\n") }
            .map{ String($0) }
    }
}

extension Array {
    
    var ls_init: [Element] {
        return isEmpty
            ? self
            : Array(self[0..<count-1])
    }
}

The solution with a while loop is actually a bit more flexible than the one with the stride. Here is a slight update (Swift 5) of Adam's answer:

extension String {

func split(len: Int) -> [String] {
    
    var currentIndex = 0
    var array = [String]()
    let length = self.count
    
    while currentIndex < length {
        let startIndex = index(self.startIndex, offsetBy: currentIndex)
        let endIndex = index(startIndex, offsetBy: len, limitedBy: self.endIndex) ?? self.endIndex
        let substr = String( self[startIndex...endIndex] )
        array.append(substr)
        currentIndex += len
    }
    
    return array
    
}

}

We can generalise it to take a an array of Ints instead of a single Int. So that we can split a string into substrings of various lengths like so:

extension String {
func split(len: [Int]) -> [String] {
    
    var currentIndex = 0
    var array = [String]()
    let length = self.count
    var i = 0
    
    while currentIndex < length {
        let startIndex = index(self.startIndex, offsetBy: currentIndex)
        let endIndex = index(startIndex, offsetBy: len[i], limitedBy: self.endIndex) ?? self.endIndex
        let substr = String( self[startIndex..<endIndex] )
        array.append(substr)
        currentIndex += len[i]
        i += 1
    }
    
    return array
    
}

}

Usage:

func testSplitString() throws {
var retVal = "Hello, World!".split(len: [6, 1, 6])
XCTAssert( retVal == ["Hello,", " ", "World!"] )
                      
retVal = "Hello, World!".split(len: [5, 2, 5, 1])
XCTAssert( retVal == ["Hello", ", ", "World", "!"] )

retVal = "hereyouare".split(len: [4, 3, 3])
XCTAssert( retVal == ["here", "you", "are"] )

}

code

People shouldn't use stride() for this.
An ordinary Range<Int> is enough.

This is a simple, yet optimized Swift5 solution:

extension String {
    func split(by length: Int) -> [String] {
        guard length > 0 else { return [] }
        var start: Index!
        var end = startIndex
        return (0...count/length).map { _ in
            start = end
            end = index(start, offsetBy: length, limitedBy: endIndex) ?? endIndex
            return String(self[start..<end])
        }
    }
}
  • Since start and end indices are being tracked in map function, it does not repeat itself.
  • count/length takes care of when length exceeds count .
  • guard is needed for the length <= 0 case.

usage

let splittedHangeul = "체르노빌같던후쿠시마원전폭발".split(by: 3)
let splittedEnglish = "THEQUICKBROWNFOXJUMPSOVERTHELAZYDOG".split(by: 6)

print(splittedHangeul)
print(splittedEnglish)
//["체르노", "빌같던", "후쿠시", "마원전", "폭발"]
//["THEQUI", "CKBROW", "NFOXJU", "MPSOVE", "RTHELA", "ZYDOG"] 

A modern (2021+) solution is Chunked of the Swift Algorithms package

let string = "There are fourty-eight characters in this string"
let chunked = string.chunks(ofCount: 20)
print(Array(chunked))
extension String {
    func inserting(separator: String, every n: Int) -> String {
        enumerated().reduce("") { $0 + ((($1.offset + 1) % n == 0) ? String($1.element) + separator : String($1.element)) }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM