Transforming booleans into comma-separated list of strings

I'm working on a project. In the code, there's the following configuration struct:

public class Config {
    static let shared = Config()
    
    private init() {}
    
    var isSeatmapActivated = true
    var isLoungeActivated = false
    var isBagActivated = true
    var isInsuranceActivated = true
    var isEnvironmentalActivated = true
}

Now for marketing purposes, I need to transform that into a semicolon-separated string. For example if all the above booleans are true, the semicolon-separated string is SEA;BAG;LOU;INS;ENV

I did the following:

var offeredProductGroups: [String] = []
if Config.shared.isSeatmapActivated {
    offeredProductGroups.append("SEA")
}
if Config.shared.isBagActivated {
    offeredProductGroups.append("BAG")
}
if Config.shared.isLoungeActivated {
    offeredProductGroups.append("LOU")
}
if Config.shared.isInsuranceActivated {
    offeredProductGroups.append("INS")
}
if Config.shared.isEnvironmentalActivated {
    offeredProductGroups.append("ENV")
}
print(offeredProductGroups.joined(separator: ";"))

That doesn't feel optimal to me :smiley: What is the shortest way to code the above?

I don’t know if there’s simpler way, but using KayPath seems to make life easier:

extension Config {
    func stringify() -> String {
        let mappings: [KeyPath<Config, Bool> : String] = [
            \.isSeatmapActivated : "SEA",
            \.isBagActivated : "BAG",
            \.isLoungeActivated : "LOU",
            \.isInsuranceActivated : "INS",
            \.isEnvironmentalActivated : "ENV"
        ]
        return mappings.compactMap {
            self[keyPath: $0] ? $1 : nil
        }.joined(separator: ";")
    }
}
extension Config {
    var marketingString: String {
        [
            isSeatmapActivated ? "SEA" : nil,
            isBagActivated ? "BAG" : nil,
            isLoungeActivated ? "LOU" : nil,
            isInsuranceActivated ? "INS" : nil,
            isEnvironmentalActivated ? "ENV" : nil
        ].compactMap { $0 }.joined(separator: ";")
    }
}
2 Likes
Seriously over-engineered approach 🤣
import Foundation

struct Config: Codable {
    var sea = true
    var lou = false
    var bag = true
    var ins = true
    var env = true
}

extension Config {
    var marketingString: String {
        let data = try! JSONEncoder().encode(self)
        let obj = try! JSONSerialization.jsonObject(with: data) as! [String: Bool]
        return obj
            .compactMapValues { $0 ? true : nil }
            .keys
            .sorted()
            .map { $0.uppercased() }
            .joined(separator: ";")
    }
}

func test() {
    print(Config().marketingString) // BAG;ENV;INS;SEA
}

test()

bending some of the input requirements, and outputting keys in a different order.

I think this is probably the most maintainable solution that fits your requirements. It doesn't need to be changed at all even if you add more configuration options (unless the abbreviations of two options happen to be the same, which is unlikely but not impossible, in which case you'd run into issues anyway).

var offeredProductGroups: [String] = []
for child in Mirror(reflecting: Config.shared).children {
  guard
    let label = child.label,
    child.value as? Bool == true
  else {
    continue
  }

  let abbreviation = label.dropFirst(2).prefix(3).uppercased()
  offeredProductGroups.append(abbreviation)
}

print(offeredProductGroups.joined(separator: ";"))

In case you want to learn more about this Swift feature, it's called reflection. Here's an article on it. I haven't read the whole article but it seems like it has all the basics.

I generally avoid reflection, but that's because I mostly work on performance-critical code. In your use case, the performance implications of this approach are negligible (unless you decide to run it thousands of times per second).

1 Like

Nice!

Same packaged in a more functional style.
extension Config {
    var marketingString: String {
        Mirror(reflecting: self).children.compactMap { child in
            guard let label = child.label, child.value as? Bool == true, label.count >= 5 else {
                return nil
            }
            return label.dropFirst(2).prefix(3).uppercased()
        }.joined(separator: ";")
    }
}
1 Like

Some seriously good replies here. Thanks people, I love this forum!

1 Like

Seems like OptionSet would be a good container for holding what is configured:
Apple Developer Documentation.

public struct Config: OptionSet {
    public let rawValue: Int
    
    public static let seatmap        = Config(rawValue: 1 << 0)
    public static let lounge         = Config(rawValue: 1 << 1)
    public static let bag            = Config(rawValue: 1 << 2)
    // etc...
    
    public init(rawValue: Int) {
        self.rawValue = rawValue
    }
    
    public static func marketingString(for options: [Config]) -> String {
        return options.reduce( .... ) //convert the option to a string value and append to the accumulator string; implementation left up to the user :)
    }
}
2 Likes

You could also use a Set<Option> and leverage a string-backed enum:

class Config {
  static let shared = Self()

  var options: Set<Option> = [.seatmap, .bag, .insurance, .environmental]

  enum Option: String {
    case bag = "BAG"
    case environmental = "ENV"
    case insurance = "INS"
    case lounge = "LOU"
    case seatmap = "SEA"
  }
}

Config.shared.options.map(\.rawValue).joined(separator: ";")
3 Likes