patternswiftMinor
Secret Santa with Groups on Swift
Viewed 0 times
swiftgroupswithsecretsanta
Problem
I am working on an special version of Secret Santa with internal sub-groups.
Typical Secret Santa (SS) requirements:
Special requirements; there are subgroups that should be taken into consideration:
Implementation
I am new to Swift, I am working on a playground. Let's first consider the problems to solve:
Problems to consider
For one group (normal SS)
Let's assume a 3 people group
In that case there is no possible person for 3 to give to.
For multiple groups
Assume these 3 groups
This could be assigned like this
Which is not a desired outcome.
Algorithm
Pick random participant
Now remove
Code
```
class Participant: CustomStringConvertible {
var name = "noname"
var contact = "nocontact"
var giveTo:Participant?
init(name:String,contact:String){
self.name = name
self.contact = contact
}
var description: String {return name + ((giveTo==nil) ?"":"->\(giveTo!.name)")}
}
class Group: Cu
Typical Secret Santa (SS) requirements:
- Cannot be assigned to themselves
- Should be random
- Every participant should be assigned to someone
- Every participant should have someone assigned to
Special requirements; there are subgroups that should be taken into consideration:
- Reduce participants assigned to someone into their same group as much as possible (none if possible)
Implementation
I am new to Swift, I am working on a playground. Let's first consider the problems to solve:
Problems to consider
For one group (normal SS)
Let's assume a 3 people group
1->2
2->1
3->?In that case there is no possible person for 3 to give to.
For multiple groups
Assume these 3 groups
group-participant:[1-1,1-2]
[2-1]
[3-1]This could be assigned like this
2-1 -> 3-1
3-1 -> 2-1
1-1 -> 1->2
1-2 -> 1->1Which is not a desired outcome.
Algorithm
Pick random participant
p from all the possible participants. Also save the first p apart.if p is not from the largest group
r is a random element from the largest group
else if p is from the largest group and there are multiple non-empty groups
r is a random element from the any other group
else if p is not from the largest group and is from the only remaining group
r is a random element from its group that it is not p
p gives present to rNow remove
p from the groups and repeat the same but make p=r until it is there are no more participants. The last one gives to the first one and the shuffle is complete.Code
```
class Participant: CustomStringConvertible {
var name = "noname"
var contact = "nocontact"
var giveTo:Participant?
init(name:String,contact:String){
self.name = name
self.contact = contact
}
var description: String {return name + ((giveTo==nil) ?"":"->\(giveTo!.name)")}
}
class Group: Cu
Solution
Your code works – as far as I can see – correctly. I cannot judge
the fairness of the algorithm itself, but I have some suggestions
concerning the implementation.
My first point of criticism is the use of the static
is not obvious to the user of your code. The call
creates a group and discards the result (which gives a compiler warning),
so the initializer is called purely for its side effect.
set of groups.
I would suggest to build an array of groups explicitly:
Similarly, the static
consecutive names to the each created group. This is acceptable, but why
not create each group with an explicit name (as you already do for the
participants)?
There is too much logic in the
are used for the "Secret Santa" algorithm, and do not use any state of
the
class for that purpose:
Now lets have a look at the
default initial values "noname", "nocontact" because both properties are
assigned to in the init method. Moreover, name and contact do never change,
so they can be constant properties with
For better structuring of the code, protocol implementations such as
Testing against
of optional binding:
or more compactly using the
In the
to identify it in an array. This is error-prone because it relies on unique
names. A better solution is to implement the
participants can be compared with
i.e. a reference type, this can be done with the "identical-to"
operator
Now a simple
in an array.
The same suggestions apply to the
static properties and methods, this is what it could look like:
be used. In addition, a precondition is added to detect programming errors.
participant is present in the array. Instead of filtering the array,
The remaining logic is m
the fairness of the algorithm itself, but I have some suggestions
concerning the implementation.
My first point of criticism is the use of the static
Group propertyallGroups:- Each created
Groupinstance is implicitly appended toallGroupswhich
is not obvious to the user of your code. The call
Group(participants:[...])creates a group and discards the result (which gives a compiler warning),
so the initializer is called purely for its side effect.
- It makes the code less reusable. You cannot create an independent second
set of groups.
I would suggest to build an array of groups explicitly:
let g1 = Group(participants:[...])
let g2 = Group(participants:[...])
let g3 = Group(participants:[...])
let allGroups = [g1, g2, g3]Similarly, the static
Group property counter is used to assignconsecutive names to the each created group. This is acceptable, but why
not create each group with an explicit name (as you already do for the
participants)?
let g1 = Group(name: "G1", participants:[...])
let g2 = Group(name: "G2", participants:[...])
let g3 = Group(name: "G3", participants:[...])There is too much logic in the
Group class. In fact all the static methodsare used for the "Secret Santa" algorithm, and do not use any state of
the
Group class. I suggest to create a separateclass for that purpose:
let g1 = Group(name: "G1", participants:[...])
let g2 = Group(name: "G2", participants:[...])
let g3 = Group(name: "G3", participants:[...])
let santa = SecretSanta(groups: [g1, g2, g3])
let pairs = santa.assignPairs()
print(pairs)Now lets have a look at the
Participant class. There is no need to assigndefault initial values "noname", "nocontact" because both properties are
assigned to in the init method. Moreover, name and contact do never change,
so they can be constant properties with
let:class Participant {
let name : String
let contact : String
var giveTo : Participant?
init(name: String, contact: String) {
self.name = name
self.contact = contact
}
}For better structuring of the code, protocol implementations such as
CustomStringConvertible can be written as an extension.Testing against
nil and implicit unwrapping should be avoided in favorof optional binding:
extension Participant: CustomStringConvertible {
var description: String {
var desc = name
if let receiver = giveTo {
desc += " -> " + receiver.name
}
return desc
}
}or more compactly using the
Optional.map() method:extension Participant: CustomStringConvertible {
var description : String {
return name + (giveTo.map { " -> " + $0.name } ?? "")
}
}In the
remove() method of Group the name of the participant is usedto identify it in an array. This is error-prone because it relies on unique
names. A better solution is to implement the
Equatable protocol, so thatparticipants can be compared with
==. Since Participant is a class,i.e. a reference type, this can be done with the "identical-to"
operator
===, i.e. only identical instances are considered equal:extension Participant : Equatable { }
func ==(lhs : Participant, rhs : Participant) -> Bool {
return lhs === rhs
}Now a simple
participants.indexOf(p) can be used to find a participantin an array.
The same suggestions apply to the
Group class. If we remove thestatic properties and methods, this is what it could look like:
class Group {
let name : String
var participants: [Participant]
init(name: String, participants: [Participant]) {
self.name = name
self.participants = participants
}
var size : Int {
return participants.count
}
func randomParticipant() -> Participant {
precondition(participants.count > 0, "participants array is empty")
return participants[Int(arc4random_uniform(UInt32(size)))]
}
func removeParticipant(participant : Participant) {
guard let index = participants.indexOf(participant) else {
fatalError("participant not found in group")
}
participants.removeAtIndex(index)
}
}
extension Group : CustomStringConvertible {
var description : String { return name }
}
extension Group : Equatable { }
func ==(lhs : Group, rhs : Group) -> Bool {
return lhs === rhs
}- I have made
sizea computed property instead of a function, similar to
count for arrays or length for strings.- In
getRandParticipant()I have removed the "get" prefix which should not
be used. In addition, a precondition is added to detect programming errors.
remove()is renamed toremoveParticipant()and expects that the
participant is present in the array. Instead of filtering the array,
indexOf and removeAtIndex are used as a small optimization.The remaining logic is m
Code Snippets
let g1 = Group(participants:[...])
let g2 = Group(participants:[...])
let g3 = Group(participants:[...])
let allGroups = [g1, g2, g3]let g1 = Group(name: "G1", participants:[...])
let g2 = Group(name: "G2", participants:[...])
let g3 = Group(name: "G3", participants:[...])let g1 = Group(name: "G1", participants:[...])
let g2 = Group(name: "G2", participants:[...])
let g3 = Group(name: "G3", participants:[...])
let santa = SecretSanta(groups: [g1, g2, g3])
let pairs = santa.assignPairs()
print(pairs)class Participant {
let name : String
let contact : String
var giveTo : Participant?
init(name: String, contact: String) {
self.name = name
self.contact = contact
}
}extension Participant: CustomStringConvertible {
var description: String {
var desc = name
if let receiver = giveTo {
desc += " -> " + receiver.name
}
return desc
}
}Context
StackExchange Code Review Q#114480, answer score: 4
Revisions (0)
No revisions yet.