Search code examples
swiftstructjsondecoder

Swift compute field in struct on decode


I'm working on an app which does a lot of real time computing based on values loaded in a struct using JSONDecoder(). I need to minimise computations in run time so I'd like to do some calculation up front. For this question I've created a working example with som fictional names for example purpose only

This piece of code loads data from a json. My quest is to use data from one part and inject it in another part based on name similarity of these parts.

struct Structure: Decodable {
    
    static func withJSON(_ fileName: String) -> Structure? {
        guard let url = Bundle.main.url(forResource: fileName, withExtension: "json", subdirectory: "Structures") else { return nil }
        guard let data = try? Data(contentsOf: url) else { return nil }
        do {
            let decoded = try JSONDecoder().decode(Structure.self, from: data)
            return decoded
        }
        catch{
            print("Unexpected error Structure withJSON: \(error).")
        }
        return nil
    }
    
    private enum Keys: CodingKey {
        case id
        case rooms
        case lists
    }
    
    let id: Int
    let rooms: [Room]
    let lists: [List]
    
    init(from decoder: Decoder) throws {
        
        let container = try decoder.container(keyedBy: Keys.self)
        id = try container.decode(Int.self, forKey: .id)
        rooms = try container.decode([Room].self, forKey: .rooms)
        lists = try container.decode([List].self, forKey: .lists)
    }
}

extension Structure {
    
    struct Room: Decodable {
        
        private enum RoomKeys: CodingKey {
            case roomName
            case dimension
            case doors
        }
        
        let roomName: String
        let dimension: [Double]
        let doors: Int
        
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: RoomKeys.self)
            roomName = try container.decode(String.self, forKey: .roomName)
            dimension = try container.decode([Double].self, forKey: .dimension)
            doors = try container.decode(Int.self, forKey: .doors)
        }
    }
}

extension Structure {
    
    struct List: Decodable {
        
        private enum RoomKeys: CodingKey {
            case roomName
            case calculated
        }
        
        let roomName: String
        var calculated: Double?
        
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: RoomKeys.self)
            roomName = try container.decode(String.self, forKey: .roomName)
            calculated = 0 // This variable should be filled with the array sum of Structure.Room.dimensions bonus for minus the doors of that (room * 2)
        }
    }
}

In this over simplified example I'd like to compute Structure.Lists.calculated with the sum of the array which sits in rooms.dimension with both the SAME roomName. See this json example:

{
    "id": 1,
    "rooms": [
        {
            "roomName": "living",
            "dimension": [6,4,9],
            "doors": 2
        },
        {
            "roomName": "hall",
            "dimension": [2,4,4],
            "doors": 2
        },
        {
            "roomName": "bathroom",
            "dimension": [1,1,1],
            "doors": 2
        }
    ],
    "lists":  [
        {
            "roomName": "living"
        },
        {
            "roomName": "bathroom"
        }
    ]
}

In this example the optional Structure.List.calculated with the roomName "living" should be filled with the sum of dimension [6,4,9] thus 19 (bonus to subtract the doors * 2)

Can this be done on the fly while decoding? Or is this data not available yet since the struct is still loading? Then what approach should I take?


Solution

  • When decoding your "Structure", you can process your raw decoded Lists and store processed versions:

    init(from decoder: Decoder) throws {
    
        let container = try decoder.container(keyedBy: Keys.self)
        id = try container.decode(Int.self, forKey: .id)
        rooms = try container.decode([Room].self, forKey: .rooms)
        let rawLists = try container.decode([List].self, forKey: .lists)
    
        var listsWithSums: [List] = []
        for var list in rawLists {
            if let room = rooms.first(where: { $0.roomName == list.roomName }) {
                list.calculated = room.dimension.reduce(0, +)
                - Double(room.doors)
            }
            listsWithSums.append(list)
        }
    
        lists = listsWithSums
    }