The String API seems unnecessarily obtuse

I recently needed to get the the suffix of a String after a specific delimiter. It turns out that there is no obvious way to do this in the string API.

The eventual solution (which I admit I found on SO as I would never have guessed it myself), is this:

guard let delimiterRange = aString.range(of: delimeter) else { return }
let suffix = aString[delimeterRange.upperBound...]

Seriously? It's the first time I have ever had to use upperBound and an open range.

I had previously tried aString.components(separatedBy:).last or aString.split(separator:).last but both of these return the whole string if the delimiter isn't found, so are not a solution.

In Kotlin, the equivalent would be aString.substringAfter(delimiter: delimiter, missingDelimiterValue: "")

It seems to me that the String API is in many cases unnecessarily obtuse or difficult to work with, although I do realise that the complexity comes from Swift handling strings (especially unicode strings) "correctly". IMO there is scope for some additions to the API to cover some of the gaps, like this one.

3 Likes

While I agree that the general idea here is reasonable, there's a pretty straightforward way to resolve that problem: check whether the split results in only one entry in the resulting collection. If it does, the delimiter was not present. Otherwise, it was, and you can return last.

which would be,

let components = aString.split(separator: delimiter, omittingEmptySubsequences: false)
guard components.count > 1 else {
    return
}
let suffix = components.last!

and while I'm at it,

guard let delimiterIndex = aString.lastIndex(of: delimiter) else {
    return
}
let suffix = aString[aString.index(after: delimiterIndex)...]

Sure, but none of these is particularly ergonomic. I previously had:

guard let delimiterIndex = aString.lastIndex(of: delimiter) else {
    return
}
let suffix = aString.suffix(from: aString.index(after: delimiterIndex))

All (including my solution using ranges) require a guard and referencing the string at least twice.

Repeating my response from Twitter here for visibility:

let suffix = aString.split(separator: delimiter, maxSplits: 1, omittingEmptySubsequences: false).dropFirst().last

This doesn't strike me as an issue related to Swift's insistence on correct unicode handling, TBH. It's just an API that doesn't exist in its most direct form on String. Could be added by a sufficiently motivated evolution proposal! :man_shrugging:

2 Likes

This doesn't handle the edge case where the delimiters are only at the leading or trailing positions of the string. For example, suppose the delimiter is ";", then the following strings containing the delimiter result in collections of 1 element when split:

";;;foo"
"foo;;;"
";foo;;"

I think a better solution is to find the exact position of the delimiter since we know which delimiter we're taking the suffix from:

// If the delimiter doesn't exist, then the endIndex of the string is returned.
// This way the suffix would be empty if there is no delimiter.
// Althernatively, use startIndex if the suffix should be the entire string if without the delimiter.
let indexOfFinalDelimiter = aString.lastIndex(of: delimiter) ?? aString.endIndex
let startIndexOfSuffix = aString[indexOfFinalDelimiter...].firstIndex(where: { $0 != delimiter } ) ?? aString.endIndex
let suffix = aString[startIndexOfSuffix...]

That's definitely an improvement :+1: You'd still need to guard against nil in the case of the delimiter not being in the string, but it's much better. Still IMO a long way from what is typically offered for string manipulation in other popular languages.

Set omittingEmptySubsequences to false and it works great.

I didn't know that. Thanks!

I don't think anyone is disputing that the API could be added: indeed, it seems like a reasonable addition generically on Collection.

1 Like

IMO nil is the most correct return value in such a case, but you can always slap on a ?? "" if you want to explicitly ignore the "missing delimiter" case.

1 Like
Terms of Service

Privacy Policy

Cookie Policy