Reputation: 794
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}
Upvotes: 35
Views: 22388
Reputation: 2262
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)
Upvotes: 12
Reputation: 1
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)) }
}
}
Upvotes: 0
Reputation: 285160
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))
Upvotes: 3
Reputation: 498
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])
}
}
}
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.let splittedHangeul = "체르노빌같던후쿠시마원전폭발".split(by: 3)
let splittedEnglish = "THEQUICKBROWNFOXJUMPSOVERTHELAZYDOG".split(by: 6)
print(splittedHangeul)
print(splittedEnglish)
//["체르노", "빌같던", "후쿠시", "마원전", "폭발"]
//["THEQUI", "CKBROW", "NFOXJU", "MPSOVE", "RTHELA", "ZYDOG"]
Upvotes: 1
Reputation: 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"] )
}
Upvotes: 0
Reputation: 1555
Here is a version, that works in the following situations:
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])
}
}
Upvotes: 0
Reputation: 13818
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))] }
}
Upvotes: 4
Reputation: 1867
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])
}
}
}
Upvotes: 18
Reputation: 93181
I just answered a similar question on SO and thought I can provide a more concise solution:
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]
}
}
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]
}
}
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) }
}
}
Upvotes: 40
Reputation: 91
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." ]
Upvotes: 5
Reputation: 6726
This problem could be easily solved with just one pass through the characters sequence:
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)
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
Upvotes: 11
Reputation: 26917
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.
Upvotes: 3
Reputation: 70185
endIndex
is not a valid index; it is one more than the valid range.
Upvotes: 2