Reputation: 941
I'm trying to write an extension method for [String]
.
It seems you can't extend [String]
directly ("Type 'Element' constrained to non-protocol type 'String'"), though I came across this trick:
protocol StringType { }
extension String: StringType { }
But I still can't quite make the Swift type system happy with this:
extension Array where Element: StringType {
// ["a","b","c","d","e"] -> "a, b, c, d, or e".
func joinWithCommas() -> String {
switch count {
case 0, 1, 2:
return joinWithSeparator(" or ")
default:
return dropLast(1).joinWithSeparator(", ") + ", or " + last!
}
}
}
The joinWithSeparator
calls are "Ambiguous". I've tried everything I could think of, like using (self as! [String])
(and a bunch of similar variants), but nothing seems to work.
How can I make the Swift compiler happy with this?
Upvotes: 9
Views: 4847
Reputation: 236360
edit/update
Swift 4 or later it is better to constrain the collection elements to StringProtocol which will cover Substrings as well.
extension BidirectionalCollection where Element: StringProtocol {
var joinedWithCommas: String {
guard let last = last else { return "" }
return count > 2 ? dropLast().joined(separator: ", ") + ", or " + last : joined(separator: " or ")
}
}
And if all elements are just Characters we can simply extend StringProtocol:
extension StringProtocol {
func joined(with separator: String = ",", conector: String = "") -> String {
guard let last = last else { return "" }
if count > 2 {
return dropLast().map(String.init).joined(separator: separator + " ") + separator + " " + conector + " " + String(last)
}
return map(String.init).joined(separator: " " + conector + " ")
}
}
let elements = "abc"
let elementsJoined = elements.joined() // "a, b, c"
let elementsSeparated = elements.joined(conector: "or") // "a, b, or c"
let elementsConected = elements.joined(conector: "and") // "a, b, and c"
Original answer
In Swift 3.1 (Xcode 8.3.2) you can simply extend Array constraining element type equal to String
extension Array where Element == String {
var joinedWithCommas: String {
guard let last = last else { return "" }
return count > 2 ? dropLast().joined(separator: ", ") + ", or " + last : joined(separator: " or ")
}
}
["a","b","c"].joinedWithCommas // "a, b, or c"
Upvotes: 12
Reputation: 523304
You could follow the declaration of joinWithSeparator
(Cmd-click on it) and find that it is defined as an extension of the protocol SequenceType
instead of the type Array
.
// swift 2:
extension SequenceType where Generator.Element == String {
public func joinWithSeparator(separator: String) -> String
}
(Note: In Xcode 8 / Swift 3 if you Cmd-click on join(separator:)
you will land on Array
even if it is still implemented inside Sequence
, but that won't invalidate the idea below)
We could do the same with your function, where we extend a protocol adopted by Array instead of Array itself:
// swift 2:
extension CollectionType where
Generator.Element == String,
SubSequence.Generator.Element == String,
Index: BidirectionalIndexType
{
func joinWithCommas() -> String {
switch count {
case 0, 1, 2:
return joinWithSeparator(" or ")
default:
return dropLast(1).joinWithSeparator(", ") + ", or " + last!
}
}
}
// swift 3:
extension BidirectionalCollection where
Iterator.Element == String,
SubSequence.Iterator.Element == String
{
func joinWithCommas() -> String {
switch count {
case 0, 1, 2:
return joined(separator: " or ")
default:
return dropLast().joined(separator: ", ") + ", or " + last!
}
}
}
Note:
CollectionType
to be able to use count
Generator.Element == String
to use joinWithSeparator
SubSequence.Generator.Element == String
to ensure dropLast(1)
can use joinWithSeparator
. dropLast(1)
returns the associated type SubSequence
.Index: BidirectionalIndexType
to use last
.Upvotes: 7