1

I've created a class which contains all optional properties. I'm trying to create a computed var that returns all non-nil properties of the object.

[<array of stuff>].flatMap{ $0 } seemed like the obvious choice, but when I tinker with it in Playground, it still returns an array containing nil values.

Here are the various iterations of what I've tried to get an array of non-nil properties of my class:

Let's say I declare my object like so:

let lastSearch = LastSearch(startDate: startDate, endDate: endDate, minMagnitude: 1.0, maxMagnitude: 5.0, minLongitude: nil, maxLongitude: nil, minLatitude: nil, maxLatitude: nil, minDepth: nil, maxDepth: nil)

Attempt #1: Within my class, I'm trying to create a nonNilProperties computed variable:

var nonNilProperties: Any {
    return [startDate, endDate, minMagnitude, maxMagnitude, minLongitude, maxLongitude, minLatitude, maxLatitude, minDepth , maxDepth].flatMap{ $0 } as Any
}

This is what prints in the console when I print lastSearch.nonNilProperties:

[Optional(2015-10-01 13:23:32 +0000), Optional(2015-10-07 01:43:59 +0000), Optional(1.0), Optional(5.0), nil, nil, nil, nil, nil, nil]

Attempt #2:

If I tack on as Any after each property, it quells compiler warnings and the populated values don't print with "Optional" in front of them, but it still has null values:

var nonNilProperties: Any {
    return [startDate as Any, endDate as Any, minMagnitude as Any, maxMagnitude as Any, minLongitude as Any, maxLongitude as Any, minLatitude as Any, maxLatitude as Any, minDepth as Any, maxDepth as Any].flatMap{ $0 } as [AnyObject]
}

This is what prints in the console when I print it:

[2015-10-01 13:23:32 +0000, 2015-10-07 01:43:59 +0000, 1, 5, <null>, <null>, <null>, <null>, <null>, <null>]

Thank you for reading. I welcome your suggestions. Here's what the class looks like:

class LastSearch {

  private var startDate: Date?
  private var endDate: Date?
  private var minMagnitude: Double?
  private var maxMagnitude: Double?
  private var minLongitude: Double?
  private var maxLongitude: Double?
  private var minLatitude: Double?
  private var maxLatitude: Double?
  private var minDepth: Double?
  private var maxDepth: Double?

  private var nonNilProperties: Any {
    return [startDate as Any, endDate as Any, minMagnitude as Any, maxMagnitude as Any, minLongitude as Any, maxLongitude as Any, minLatitude as Any, maxLatitude as Any, minDepth as Any, maxDepth as Any].flatMap{ $0 } as Any
  }

  init(startDate: Date?, endDate: Date?,
       minMagnitude: Double?, maxMagnitude: Double?,
       minLongitude: Double?, maxLongitude: Double?,
       minLatitude: Double?, maxLatitude: Double?,
       minDepth: Double?, maxDepth: Double?) {
    // Dates
    self.startDate = startDate
    self.endDate = endDate

    // Magnitude Values
    self.minMagnitude = minMagnitude
    self.maxMagnitude = maxMagnitude

    // Geographic Coordinates
    self.minLongitude = minLongitude
    self.maxLongitude = maxLongitude
    self.minLatitude = minLatitude
    self.maxLatitude = maxLatitude

    // Depth Values
    self.minDepth = minDepth
    self.maxDepth = maxDepth
  }      
}
Adrian
  • 16,233
  • 18
  • 112
  • 180
  • 1
    Why would you create a class where all properties may be nil? Are you sure you need to make all of them optionals? – Leo Dabus Nov 26 '17 at 01:38
  • The class will never be initialized without one of the properties initialized. I'm using the class to do book keeping between classes. It's got a few methods that I haven't posted here. – Adrian Nov 26 '17 at 01:47
  • 3
    Btw How would you know which elements in the array to use and how are you gonna identify which ones are they? I think this is not useful. – Leo Dabus Nov 26 '17 at 02:01
  • @LeoDabus I figured a way around that, thought it's outside the scope of this question. – Adrian Nov 26 '17 at 02:10
  • another approach `return ((Mirror(reflecting: self).children.map { $0.value } as [Any]) as [Any?]).flatMap { $0 }` – Leo Dabus Nov 26 '17 at 04:10

3 Answers3

1

One solution is to explicitly create a new array of type [Any] and only add the property to the array if it is not nil.

public var nonNilProperties: [Any] {
    let allProperties: [Any?] = [startDate, endDate, minMagnitude, maxMagnitude, minLongitude, maxLongitude, minLatitude, maxLatitude, minDepth, maxDepth]
    var output = [Any]()
    for property in allProperties {
        if let nonNilProperty = property {
            output.append(nonNilProperty)
        }
    }
    return output
}

Or you can use flatMap which is closer to your original solution (credit to @Leo Dabus)

public var nonNilProperties: [Any] {
     return ([startDate, endDate, minMagnitude, maxMagnitude, minLongitude, maxLongitude, minLatitude, maxLatitude, minDepth, maxDepth] as [Any?]).flatMap { $0 }
}

Test case:

let lastSearch = LastSearch(startDate: Date(), endDate: Date(), minMagnitude: 1.0, maxMagnitude: 5.0, minLongitude: nil, maxLongitude: nil, minLatitude: nil, maxLatitude: nil, minDepth: nil, maxDepth: nil)
print(lastSearch.nonNilProperties)

Output:

[2017-11-26 02:00:13 +0000, 2017-11-26 02:00:13 +0000, 1.0, 5.0]

This works, but it's a little awkward. Depending on your exact situation, there is probably a better way to structure your data.

nathangitter
  • 9,607
  • 3
  • 33
  • 42
  • still returning optionals – Leo Dabus Nov 26 '17 at 01:52
  • @LeoDabus I see what you are saying. I'll edit my answer real quick. – nathangitter Nov 26 '17 at 01:53
  • 1
    instead of casting you can explicitly set the array type to `[Any?]`. `let allProperties: [Any?] = [startDate, endDate, minMagnitude, maxMagnitude, minLongitude, maxLongitude, minLatitude, maxLatitude, minDepth, maxDepth]` – Leo Dabus Nov 26 '17 at 02:05
  • 2
    btw `return ([startDate, endDate, minMagnitude, maxMagnitude, minLongitude, maxLongitude, minLatitude, maxLatitude, minDepth, maxDepth] as [Any?]).flatMap{$0}` – Leo Dabus Nov 26 '17 at 02:07
0

First nonNilProperties is not Any, its [Any] (array of any). And you don't need to cast every property to any, it will do that automatically. Second, flatMap is not what you need. Use map. flatMap does not iterate over elements, but operates on whole array. Here are some examples: http://sketchytech.blogspot.ru/2015/06/swift-what-do-map-and-flatmap-really-do.html

0

As Leo Dabus pointed out in comments, having the non-nil values by themselves is useless if I don't know what they are. That said, here's what I ultimately did:

In Charles Prince's answer, I found a method to unwrap optionals.

I added this method to my class:

func unwrap<T>(_ any: T) -> Any {
  let mirror = Mirror(reflecting: any)
  guard mirror.displayStyle == .optional, let first = mirror.children.first else {
    return any
  }
  return unwrap(first.value)
}

Then, I created a computed var to return a Dictionary of non-nil values, grabbing keys and values using Mirror.

var nonNilPropertyDict: [String: Any] {

    let keys = Mirror(reflecting: self).children.flatMap { $0.label }
    let values = Mirror(reflecting: self).children.flatMap { unwrap($0.value) as! AnyObject }

    var dict = Dictionary(uniqueKeysWithValues: zip(keys, values))

    var keysToRemove = dict.keys.filter{ dict[$0] is NSNull }
    for key in keysToRemove {
      dict.removeValue(forKey: key)
    }

    return dict
}
Adrian
  • 16,233
  • 18
  • 112
  • 180