33

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}
yshilov
  • 774
  • 1
  • 8
  • 11
  • possible duplicate of [How do you use String.substringWithRange? (or, how do Ranges work in Swift?)](http://stackoverflow.com/questions/24044851/how-do-you-use-string-substringwithrange-or-how-do-ranges-work-in-swift) – Palu Macil Aug 25 '15 at 19:34
  • updated with an issue – yshilov Aug 25 '15 at 19:52
  • Here the three-parameter version of advance() comes in handy, compare http://stackoverflow.com/questions/30128282/how-to-use-advance-function-in-swift-with-three-parameters. – Martin R Aug 25 '15 at 19:58
  • You can generate the substrings lazily using this https://stackoverflow.com/a/48089097/2303865. It also works on substrings – Leo Dabus Jan 20 '22 at 13:43

13 Answers13

39

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) }
    }
}
Code Different
  • 90,614
  • 16
  • 144
  • 163
  • 1
    Thanks! Real swifty solution, probably better to add this to extension String – Igor Palaguta Oct 28 '16 at 13:07
  • Really, best swift solution, here is an extension: extension String { func split(_ count: Int) -> [String] { return stride(from: 0, to: self.characters.count, by: count).map { i -> String in let startIndex = self.index(self.startIndex, offsetBy: i) let endIndex = self.index(startIndex, offsetBy: count, limitedBy: self.endIndex) ?? self.endIndex return self[startIndex.. – Nik Nov 29 '17 at 11:28
18

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])
        }
    }
}
Wujo
  • 1,845
  • 2
  • 25
  • 33
  • 1
    This optimizes the offset of the upperbound of each substring but the lowerbound is always unnecessarily offset all the way from the start index. You should keep the last in upperbound to offset from there – Leo Dabus Jan 20 '22 at 13:52
11

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

Community
  • 1
  • 1
Matteo Piombo
  • 6,688
  • 2
  • 25
  • 25
10

String extension based on "Code Different" answer:

Swift 5

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

Usage

let str = "There are fourty-eight characters in this string"
let components = str.components(withLength: 20)
Ondrej Stocek
  • 2,102
  • 1
  • 18
  • 13
5

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." ]
Caleb L
  • 91
  • 1
  • 4
4

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))] }
}
iUrii
  • 11,742
  • 1
  • 33
  • 48
3

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.

Adam
  • 26,549
  • 8
  • 62
  • 79
  • 1
    seems to be a working variant, thank you. p.s. I'd change length to str.characters.count – yshilov Aug 25 '15 at 20:10
  • This solution is already outdated with respect to Swift 2.0. The use of UTF8 view could lead to strange behaviours when our beloved emoticons are present. – Matteo Piombo Aug 25 '15 at 21:36
  • @yshilov That makes perfect sense, I have updated the answer. – Adam Aug 26 '15 at 05:34
  • @MatteoPiombo This answer is not "outdated with respect to Swift 2.0". In fact, it was written using Xcode7 and Swift2. The UTF8 issue has nothing to do with version of Swift. – Adam Aug 26 '15 at 05:36
  • @Adam I should have more specific about Swift 2.0. I refer to the latest Xcode 7 Beta 6. In this beta there are significant changes with regard to indexes. `substringWithRange` is no more present. – Matteo Piombo Aug 26 '15 at 06:03
  • @MatteoPiombo Updated the answer for the new Xcode. `substringWithRange ` works fine, but `advance` function was replaced. – Adam Aug 26 '15 at 10:10
2

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

GoZoner
  • 67,920
  • 20
  • 95
  • 145
  • yes and I do not even try to call any string method with this index, I just advance my variable currentIndex with arbitrary shift and get an error before validating this new index – yshilov Aug 25 '15 at 19:59
  • You compared `currentIndex > endIndex` but `currentIndex` won't ever be more than `endIndex` - an exception is thrown before getting there. – GoZoner Aug 25 '15 at 20:06
2

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))
vadian
  • 274,689
  • 30
  • 353
  • 361
1

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"] 
eastriver lee
  • 173
  • 2
  • 9
0

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])
    }
}
thetrutz
  • 1,395
  • 13
  • 12
0

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"] )

}

0
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)) }
    }
}