How to split a string into substrings of equal length
Asked Answered
P

13

35

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}
Priscillaprise answered 25/8, 2015 at 19:16 Comment(4)
possible duplicate of How do you use String.substringWithRange? (or, how do Ranges work in Swift?)Microeconomics
updated with an issuePriscillaprise
Here the three-parameter version of advance() comes in handy, compare #30128782.Pedantry
You can generate the substrings lazily using this https://mcmap.net/q/103220/-how-can-split-from-string-to-array-by-chunks-of-given-size. It also works on substringsSezen
M
40

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) }
    }
}
Marguritemargy answered 16/8, 2016 at 16:40 Comment(2)
Thanks! Real swifty solution, probably better to add this to extension StringHimelman
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..<endIndex] } } }Ardeb
A
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])
        }
    }
}
Alesiaalessandra answered 31/7, 2019 at 11:1 Comment(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 thereSezen
J
12

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)
Johannisberger answered 22/11, 2016 at 12:12 Comment(0)
P
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

Pyrophosphate answered 25/8, 2015 at 21:18 Comment(0)
A
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." ]
Admonish answered 16/4, 2018 at 14:31 Comment(0)
S
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))] }
}
Statutable answered 28/2, 2020 at 14:54 Comment(1)
This unnecessarily offsets all indices from the start indexSezen
R
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.

Ringsmuth answered 25/8, 2015 at 20:5 Comment(6)
seems to be a working variant, thank you. p.s. I'd change length to str.characters.countPriscillaprise
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.Pyrophosphate
@Priscillaprise That makes perfect sense, I have updated the answer.Ringsmuth
@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.Ringsmuth
@Ringsmuth 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.Pyrophosphate
@MatteoPiombo Updated the answer for the new Xcode. substringWithRange works fine, but advance function was replaced.Ringsmuth
D
3

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))
Distillate answered 1/10, 2022 at 14:25 Comment(0)
D
2

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

Disaster answered 25/8, 2015 at 19:52 Comment(2)
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 indexPriscillaprise
You compared currentIndex > endIndex but currentIndex won't ever be more than endIndex - an exception is thrown before getting there.Disaster
A
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"] 
Anachronistic answered 1/10, 2022 at 12:58 Comment(0)
K
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://mcmap.net/q/423602/-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])
    }
}
Kwangchowan answered 16/8, 2020 at 18:47 Comment(0)
S
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"] )

}

Sumikosumma answered 17/11, 2021 at 22:59 Comment(0)
M
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)) }
    }
}
Mccormac answered 15/11, 2022 at 3:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.