How to create a Dictionary dynamically?

Hi,

I'd like to generate a Dictionary dynamically, but I tried to do it as a struct property unsuccessfully, where I got the error Value of type 'SpaceAge' has no subscripts in self["on\(planet)"].

Here's the source code that I'm working on:

struct SpaceAge {
    var seconds: Double
    var onEarth: Double
    var earthYearInSeconds: Double = 31557600
    
    private let orbitalPeriods: [String: Double] = [
        "Earth": 1,
        "Mercury": 0.240846,
        "Venus": 0.61519726,
        "Mars": 1.8808158,
        "Jupiter": 11.862615,
        "Saturn": 29.447498,
        "Uranus": 84.016846,
        "Neptune": 164.79132
    ]
    
    init(_ seconds: Double) {
        self.seconds = seconds
        self.onEarth = self.seconds / self.earthYearInSeconds
        for planet in orbitalPeriods {
            self["on\(planet)"] = (self.seconds / self.earthYearInSeconds) * self.orbitalPeriods[planet]
        }
    }
}

Any tips on how to do this properly in Swift?

Thank you!

If you want to store the dictionary as a member of the struct, then you need to declare it on the struct.

However, I personally would use a different approach:

  • Declare a Planet struct with name and orbitalPeriod properties.
  • Give the Planet struct static members for each planet: .mercury, .venus, etc.
  • Make SpaceAge store only the seconds property.
  • Give SpaceAge a subscript so you can write myAge[on: .jupiter].

@Nevin, thanks! I'll read about "subscript".

Regarding the subscript, I'll need to have the property onPlanet accessible, as in myAge.onMars or myAge.onNeptune since I have unit tests that expect to compute the values. Are the subscripts ok for that?

Example:

func testAgeInMercuryYears() {
    let age = SpaceAge(2_134_835_688)
    XCTAssertEqual(67.65, age.onEarth, accuracy: 0.01)
    XCTAssertEqual(280.88, age.onMercury, accuracy: 0.01)
}

I would change the tests to use age[on: .earth], etc.

If you must have properties, you can implement dynamic member lookup, but that is a much more advanced topic and almost certainly overkill here.

Or you could manually implement var onEarth: Double { self[on: .earth] }. But again, it’s probably better not to have onEarth at all, and just write age[on: .earth].

Doing it the way I describe makes it easy to introduce new planets:

let krypton = Planet(name: "Krypton", orbitalPeriod: 1.37)
print(myAge[on: krypton])

Ok cool! thanks @Nevin

1 Like

Here's the best I came up with, considering the basic knowledge I have of Swift this far, where I've used computed properties:

enum OrbitalEarth: Double {
    case earth = 1
    case mercury = 0.240846
    case venus = 0.61519726
    case mars = 1.8808158
    case jupiter = 11.862615
    case saturn = 29.447498
    case uranus = 84.016846
    case neptune = 164.79132
}

struct SpaceAge {
    var seconds: Double
    private let earthYearInSeconds: Double = 31557600
    
    var onEarth: Double {
        self.calc(.earth)
    }
    var onMercury: Double {
        self.calc(.mercury)
    }
    var onVenus: Double {
        self.calc(.venus)
    }
    var onMars: Double {
        self.calc(.mars)
    }
    var onJupiter: Double {
        self.calc(.jupiter)
    }
    var onSaturn: Double {
        self.calc(.saturn)
    }
    var onUranus: Double {
        self.calc(.uranus)
    }
    var onNeptune: Double {
        self.calc(.neptune)
    }
    
    init(_ seconds: Double) {
        self.seconds = seconds
    }
    
    func calc (_ planet: OrbitalEarth) -> Double {
        return self.seconds / (self.earthYearInSeconds * planet.rawValue)
    }
}

You can certainly use an enum here, but I suggested a struct because I believe it is a better fit.

I already mentioned how using a struct makes it easy to create new planets (even dynamically at runtime), and to include their names.

But also, if you decide you want to calculate days on a planet as well, that’s much easier with a struct too:

struct Planet {
  var name: String
  var orbitalPeriod: Double
  var rotationPeriod: Double
}

Perhaps later you decide to calculate how much things weigh on planets. Well, that only needs a couple more properties in the Planet struct:

var mass: Double
var radius: Double

If you stick with an enum, these sorts of additions become much more difficult.

2 Likes