Generate a Secret Santa List with PowerShell

It's supposed to be the most wonderful time of the year and while you might buy multiple Christmas gifts for everyone in your immediate family, often times buying for everyone in your extended family or for all of your co-workers is cost prohibitive.

I originally started out with a simple idea to create a PowerShell script to take a list of names and generate a second random list of names based off of the first one while making sure the corresponding name on the second list doesn't match the first one. Sounds simple enough, right?

Everything seemed well and fine until I figured out there was a problem with my logic because if the last entry in both lists are the same, there's no way to prevent a duplicate other than not performing a match at all which means someone would be left out.

 1$Users = Get-ADUser -Filter * -SearchBase 'OU=AdventureWorks Users,OU=Users,OU=Test,DC=mikefrobbins,DC=com' |
 2Select-Object -First 10 -ExpandProperty Name
 3
 4$Match = $Users
 5
 6foreach($User in $Users) {
 7
 8    $Result = $Match.Where({$_ -ne $User}) | Get-Random
 9
10    [pscustomobject]@{
11        Name = $User
12        Match = $Result
13    }
14
15    $Match = $Match.Where({$_ -ne $Result})
16
17}

secret-santa1a.jpg

It took running the code a number of times for the problem to occur. As you can see in the previous example, Alan Brewer doesn't have a match because the only one left in the second list was himself.

I decided to take a different approach while at the same time checking to see if the last person in the two lists matched and just regenerate the second list if they did.

 1$Gifters = Get-ADUser -Filter * -SearchBase 'OU=AdventureWorks Users,OU=Users,OU=Test,DC=mikefrobbins,DC=com' |
 2Select-Object -First 10 -ExpandProperty Name
 3
 4do {
 5    $Giftees = $Gifters | Sort-Object {Get-Random}
 6}
 7while ($Gifters[$Gifters.Length -1] -eq $Giftees[$Giftees.Length -1] )
 8
 9for ($i = 0; $i -lt ($Gifters.Length); $i += 1) {
10    [pscustomobject]@{
11        Gifter = $Gifters[$i]
12        Giftee = $Giftees[$i]
13    }
14}

secret-santa2a.jpg

The problem with my second approach is that it didn't prevent a person from being matched to themselves in the middle of the list.

Clearly this was going to be a little more difficult than I initially thought. If I'm going to put more effort into this, I'll just create a function for it.

 1#Requires -Version 3.0
 2function Get-MrSecretSantaList {
 3<#
 4.SYNOPSIS
 5    Generates a unique list of gift givers and gift receivers based on a single list of names.
 6.DESCRIPTION
 7    Get-MrSecretSantaList is an advanced function that generates a unique list of gift givers
 8    and gift receivers based on a single list of names.
 9.PARAMETER Name
10    The name of the person. A minimum of 2 names must be provided.
11.EXAMPLE
12    Get-MrSecretSantaList -Name 'Mike Robbins', 'Joe Doe'
13 .EXAMPLE
14    Get-MrSecretSantaList -Name (Get-ADUser -Filter "Enabled -eq '$true'" | Select-Object -ExpandProperty Name)
15.INPUTS
16    None
17.OUTPUTS
18    PSCustomObject
19.NOTES
20    Author:  Mike F Robbins
21    Website: http://mikefrobbins.com
22    Twitter: @mikefrobbins
23#>
24    [CmdletBinding()]
25    param (
26        [Parameter(Mandatory)]
27        [ValidateCount(2,32768)]
28        [string[]]$Name
29    )
30    if ($Name.Length % 2 -ne 0) {
31        Throw 'An even number of Names must be specified in order for matching to occur.'
32    }
33    do {
34        $Giftee = $Name | Sort-Object {Get-Random}
35    }
36    while (
37        $(for ($i = 0; $i -lt ($Name.Length); $i += 1) {
38            if ($Name[$i] -eq $Giftee[$i]) {
39                Write-Verbose -Message "A duplicate has occured in loop $i Name: $($Name[$i]) cannot match Giftee: $($Giftee[$i])"
40                $true
41                break
42            }
43        })
44    )
45    for ($i = 0; $i -lt ($Name.Length); $i += 1) {
46        [pscustomobject]@{
47            Gifter = $Name[$i]
48            Giftee = $Giftee[$i]
49        }
50    }
51}

I attempted to use ValidateScript to make sure there were an even number of items provided, but it appears that you're unable to retrieve the count or length property in the param block.

I nested a for loop inside the do/while loop to check each entry and if it matches itself, the break statement causes it to immediately exit the loop, regenerate the second list, and start over.

The function generates two lists with the same names that are matched randomly while making sure no one is matched to themselves.

1$Gifters = Get-ADUser -Filter * -SearchBase 'OU=AdventureWorks Users,OU=Users,OU=Test,DC=mikefrobbins,DC=com' |
2Select-Object -First 10 -ExpandProperty Name
3
4Get-MrSecretSantaList -Name $Gifters

secret-santa3a.jpg

If less than two names are provided or if an odd number of names is provided, an error is generated.

1Get-MrSecretSantaList -Name 'Mike Robbins', 'John Doe', 'Jane Doe'

secret-santa4a.jpg

While duplicates don't seem to occur that often, they do occur. The verbose parameter can be used to see the duplicates.

1$null = Get-MrSecretSantaList -Name (1..32768) -Verbose

secret-santa5a.jpg

You could query Active Directory similarly to what I've done in the examples shown in this blog article and add their email address. Then the Send-MailMessage cmdlet could be used to automatically email each of the users with the name of the person they're buying a gift for.

The other possibility that I thought about is simply generating a random offset less than the number of names in the list and offsetting the matching list by that number, but it wouldn't purely random like the examples shown in this blog article. I'd love to hear your thoughts and know if there's a simpler way to accomplish this task?

µ