9

Given [Int?], need to build string from it.

This code snippet works

    let optionalInt1: Int? = 1
    let optionalInt2: Int? = nil

    let unwrappedStrings = [optionalInt1, optionalInt2].flatMap({ $0 }).map({ String($0) })
    let string = unwrappedStrings.joined(separator: ",")

But I don't like flatMap followed by map. Is there any better solution?

6 Answers 6

6

Here's another approach:

[optionalInt1, optionalInt2].flatMap { $0 == nil ? nil : String($0!) }

Edit: You probably shouldn't do this. These approaches are better, to avoid the !

[optionalInt1, optionalInt2].flatMap {
    guard let num = $0 else { return nil }
    return String(num)
}

or:

[optionalInt1, optionalInt2].flatMap { $0.map(String.init) }
Sign up to request clarification or add additional context in comments.

6 Comments

This solution will also work where the generic type of the array doesn't conform to CustomStringConvertible or doesn't have a description property.
@YakivKovalskiy in this case it's very apparent that $0 is .some(...) if the 2nd conditional target of the ternary operator is entered; a special case where we truly may use ! safely and at the same time showing, semantically, that we know what we're doing.
@YakivKovalskiy Forced unwrapping isn't necessarily bad. It's way overused and abused, but it's perfectly justifiable in a case like this
@Alexander you may be right. In my opinion, one should never ever use force unwrap in Swift code, but this topic is too controversial to discuss here.
flatMap has been replaced with compactMap.
|
4

You can make use of the map method of Optional within a flatMap closure applied to the array, making use of the fact that the former will return nil without entering the supplied closure in case the optional itself is nil:

let unwrappedStrings = [optionalInt1, optionalInt2]
    .flatMap { $0.map(String.init) }

Also, if you don't wrap the trailing closures (of flatMap, map) in paranthesis and furthermore make use of the fact that the initializer reference String.init will (in this case) non-ambigously resolve to the correct String initializer (as used above), a chained flatMap and map needn't look "bloated", and is also a fully valid approach here (the chained flatMap and map also hold value for semantics).

let unwrappedStrings = [optionalInt1, optionalInt2]
    .flatMap{ $0 }.map(String.init) 

3 Comments

But I'm trying to avoid use of map and flatMap methods here because loop through array twice can't be good.
@YakivKovalskiy note that the first solution above use the map operation of the optional type (which is a single call to an instance method of the optional element), not of the array, so there will only be one pass over the array in that one. The first method above is almost equivalent to @Alexander's solution below. Nonetheless, a chained flatMap and map is still O(n), and I don't see how this could ever be "can't be good" unless you're working with some extremely heavy HPC application. If not, put likewise focus also on semantics.
@YakivKovalsky If you want to avoid repeated looping, use .lazy. E.g. array.map(fn1).map(fn2) will only read the elements once, applying each function in succession directly, without copying/storing intermediate results into intermediate arrays.
3

If you don't like the flatMap and the map together you can replace this

[optionalInt1, optionalInt2].flatMap({ $0 }).map({ String($0) })

with this

[optionalInt1, optionalInt2].flatMap { $0?.description }

Wrap up

let optionalInt1: Int? = 1
let optionalInt2: Int? = nil

let result = [optionalInt1, optionalInt2]
    .flatMap { $0?.description }
    .joined(separator: ",")

Update

Since:

  • as @Hamish pointed out direct access to description is discouraged by the Swift team
  • and the OP wants to avoid the flatMap + map concatenation because of the double loop

I propose another solution

let result = [optionalInt1, optionalInt2].flatMap {
        guard let num = $0 else { return nil }
        return String(num)
    }.joined(separator: ",")

3 Comments

Nice catch! I forgot about .description trying to make things work with String(describing:), String()
Note that the Swift team discourages you from directly accessing the description property (last paragraph of the Overview in the docs) – although I admit it allows for a nice looking call.
@Hamish: I didn't know that. Thank you very much for the information.
2

The other answers use flatMap to eliminate the nil elements and require a call to joined() to combine the elements and add the commas.

Using joined() to add the commas is of course a second pass through the array. Here's a way to use reduce to do this in one pass:

let arr: [Int?] = [nil, nil, 1, 2, 3, nil, 4, nil, 5, nil]

let result = arr.reduce("") { $1 == nil ? $0 : $0.isEmpty ? "\($1!)" : $0 + ",\($1!)" }

print(result)

Output:

1,2,3,4,5

Explanation

This example uses reduce to construct the result string element by element.

reduce is a method called on a sequence. It takes an initial value ("" in this example) and a closure that combines the next element in the sequence with the partial result to create the next partial result.

The closure is called on each element in the sequence (i.e. array arr). The closure first checks if the element is nil and if it is, it returns the partial result unmodified. If the element is not nil, it then checks if the partial result is still the empty string. If it is empty, this is the first element in the result so it returns a string made up of just the element. If the partial result is not empty, it adds the new element preceded by a , to the partial result.


Use reduce(into:):

Rewriting this to use reduce(into:) and append results in an implementation that is twice as fast as the other answers when generating the final comma separated string.

let result = arr.reduce(into: "") { (string, elem) in
    guard let elem = elem else { return }
    if string.isEmpty {
        string = String(elem)
    } else {
        string.append(",\(elem)")
    }
}

5 Comments

(I'm aware this is an old answer) This has O(n^2) time complexity (each append would cause a string duplication), which isn't great. Plus, since the code is more complex than flatMap + joined(separator: " "), I would strongly advise against it. See github.com/amomchilov/Blog/blob/master/…
@Alexander-ReinstateMonica, in a Release build a 10,000 element array with 10% nils took 0.01 seconds both with my answer and your latest one with joined(separator: ","). Rewriting to use reduce(into:) and append ran twice as fast. So your complaint about the complexity of the code is valid, but the speed concern is unwarranted.
My performance concerns these days usually are less about time (except for blocking network calls, those are still a big no-no), but more about battery life. I wouldn't put much time into it, but if it's trivial (e.g. put a .lazy before a chain of calls), I'd do it up-front, just to keep the device from needing to use its higher-clocked cores and such.
@Alexander-ReinstateMonica, adding .lazy to make let result = arr.lazy.compactMap { $0.map(String.init) }.joined(separator: ",") slowed it down from 0.01068 seconds to 0.01469 seconds.
@Alexander-ReinstateMonica, yes, it is a release build.
1

Swift 3.1:

[1,2,3].flatMap({String(describing: $0)}).joined(separator: " ")

// "1 2 3" (without quotes sure)

1 Comment

This produces string values of Optional(1) and nil, which is almost certainly not intended. And even if it was intended, that's probably worth strong reconsideration.
0

Avoiding flatMap and map altogether. However creating a local var.

let optionalInt1: Int? = 1
let optionalInt2: Int? = nil
let optionalInts = [optionalInt1, optionalInt2]

var strings = [String]()
for optInt in optionalInts {
    if let integer = optInt {
        strings.append(String(integer))
    }
}
let string = strings.joined(separator: ",")

3 Comments

"Avoiding flatMap and map altogether" don't do that.
@Alexander-ReinstateMonica May I know why?
Your entire for loop is just a really long way of saying flatMap (now called compactMap, for this variant). Why reinvent the wheel?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.