swahili
(Swahili)
1
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!
Nevin
2
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].
swahili
(Swahili)
3
@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)
}
Nevin
4
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])
swahili
(Swahili)
6
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)
}
}
Nevin
7
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