J. Cocoe
J. Cocoe

Reputation: 941

Swift extension for [String]?

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

Answers (2)

Leo Dabus
Leo Dabus

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

kennytm
kennytm

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:

  • we extend CollectionType to be able to use count
  • we constraint Generator.Element == String to use joinWithSeparator
  • we constraint SubSequence.Generator.Element == String to ensure dropLast(1) can use joinWithSeparator. dropLast(1) returns the associated type SubSequence.
  • we constraint Index: BidirectionalIndexType to use last.

Upvotes: 7

Related Questions