Yakiv Kovalskyi
Yakiv Kovalskyi

Reputation: 1757

How to join array of optional integers to string?

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?

Upvotes: 9

Views: 4605

Answers (6)

Maxim Firsoff
Maxim Firsoff

Reputation: 2296

Swift 3.1:

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

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

Upvotes: 1

vacawama
vacawama

Reputation: 154731

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)")
    }
}

Upvotes: 2

Alexander
Alexander

Reputation: 63399

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) }

Upvotes: 6

user12345625
user12345625

Reputation: 398

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: ",")

Upvotes: 0

Luca Angeletti
Luca Angeletti

Reputation: 59536

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: ",")

Upvotes: 3

dfrib
dfrib

Reputation: 73236

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) 

Upvotes: 4

Related Questions