Add random users to groups from CSV file - powershell

I have a question, i'm adding users from a csv file to some groups, in this ex. it's MAX. 3 users/group.But what I want is, that I have 10 groups and then adding the random users from the csv file to the groups, then maybe i got 7 groups with 4 users and then last 3 with 3 users and thats OK.
But how do I changes this script, from just adding 3 users/group, to adding users to the "hardcoded" 10 groups, from the csv file?
ATM. I got this:
$deltager = Import-Csv C:\Users\Desktop\liste.csv
$holdstr = 3
$maxTeams = [math]::ceiling($deltager.Count/$holdstr)
$teams = #{}
$shuffled = $deltager | Get-Random -Count $deltager.Count
$shuffled | ForEach-Object { $i = 1 }{
$teams["$([Math]::Floor($i / $holdstr))"] += #($_.Navn)
$i++
}
$Grupper = $teams | Out-String
Write-Host $Grupper

Use the remainder/modulo operator % to "wrap around" the group index, then simply add one user at a time for optimum distribution:
# define number of groups
$numberOfTeams = 10
# read participant records from csv
$participants = Import-Csv C:\Users\Desktop\liste.csv
# create jagged array for the team rosters
$teams = ,#() * $numberOfTeams
# go through participant list, add to "next" group in line
$index = 0
$participants |%{
$teams[$index++ % $numberOfTeams] += #($_.Navn)
}
If you want to randomize the order of participants, simply sort the list randomly before populating the groups:
$participants = Import-Csv C:\Users\Desktop\liste.csv |Sort-Object {Get-Random}
Each item in $teams will be another array of names, so to enumerate them:
0..$teams.Length |%{
Write-Host "Team $($_+1):" $($teams[$_] -join ', ')
}

Related

Send 1 email to each person with all their codes

I need to write a script that sends a single email to each person.
That email will have unique codes for the person.
Each person can get any number of codes.
The data looks like this
Email,Codes
Email#dom.com,213
Email#dom.com,999
Email2#dom.com,111
Email2#dom.com,123
Email2#dom.com,643
Email2#dom.com,809
Email2#dom.com,722
Email3#dom.com,013
I think the script will go something like this.
#get the data
$PeopleAndCodes = Import-Csv C:\temp\PeopleCodes.csv
#Count how many groups of unique people
[array]$CountOfPeople = $PeopleAndCodes.email | Group-Object -noelement | Select-Object Count
#Loop through unique people
$Index = 0;
while ($Index -lt $CountOfPeople.count) {
$Index++
#THIS BELOW EXITS BEFORE GETTING THROUGH ALL THE CODES FOR ONE PERSON
[int]$DCodes = 0
foreach ($DCodes in [int]$CountOfPeople[$DCodes].count) {
$DCodes++
Write-Host $DCodes DCODES
Write-Host $CountOfPeople[$DCodes].count CountOfPeople
Write-Host $PeopleAndCodes[$DCodes].codes
}
}
The problem is that my 2nd loop stops as soon as number of unique people is reached and then moves to the next person.
I don't understand why the 2nd loop is not going through the codes and then to the next person?
You are very close, I would make some small tweaks to your code and we can get it there.
First, instead of indexing through the array, let's select all of the unique e-mails from the list of codes.
$uniqueUsers = $PeopleAndCodes | select -Unique Email
Next, we can foreach loop our way through the list of $uniqueUsers, and for each, find any matching codes for them.
foreach($uniqueUser in $uniqueUsers){
$thisEmail = $uniqueUser.Email
$matchingCodes = $PeopleAndCodes | Where Email -Match $uniqueUser.Email |
Select-Object -ExpandProperty Codes
Now we have within this loop a $thisEmail var that holds the email address of the user, and then an array of all of the matching codes for the user, called $matchingCodes. We don't need to index through these either. In fact that second loop was likely causing the issue, as there are more items in the list than unique users.
Your limiting condition was the number of unique users, not the number of items in the list.
So, to avoid confusion and get the desired output, just remove that second loop entirely since it isn't helping us.
Write-Host "Person - $thisEmail"
Write-Host "Person has $($matchingCodes.Count) codes"
$matchingCodes -join ","
Gives an output of
Person - Email#dom.com
Person has 2 codes
213,999
--------------------
Person - Email2#dom.com
Person has 5 codes
111,123,643,809,722
Completed Code
$PeopleAndCodes = Import-Csv C:\temp\PeopleCodes.csv
$uniqueUsers = $PeopleAndCodes | select -Unique Email
foreach($uniqueUser in $uniqueUsers){
$thisEmail = $uniqueUser.Email
$matchingCodes = $PeopleAndCodes | where Email -Match $uniqueUser.Email | Select-Object -ExpandProperty Codes
Write-Host "Person - $thisEmail"
Write-Host "Person has $($matchingCodes.Count) codes"
$matchingCodes -join ","
Write-Host "--------------------"
}
I want to point out that while above is the best answer. In the comments is another answer that works with my original code as well.
#get the data
$PeopleAndCodes = Import-Csv C:\temp\PeopleCodes.csv
#Count how many groups of unique people
[array]$CountOfPeople = $PeopleAndCodes.email | Group-Object -noelement | Select-Object Count
#Loop through unique people
$Index = 0;
while ($Index -lt $CountOfPeople.count) {
$Index++
#This loops through each person and gets their code(s)
[int]$DCodes = 0
foreach ($DCodes in 0..[int]$CountOfPeople[$DCodes].count) {
Write-Host $PeopleAndCodes[$DCodes].email
Write-Host $PeopleAndCodes[$DCodes].codes
$DCodes++
}
}

PowerShell: Find unique values from multiple CSV files

let's say that I have several CSV files and I need to check a specific column and find values that exist in one file, but not in any of the others. I'm having a bit of trouble coming up with the best way to go about it as I wanted to use Compare-Object and possibly keep all columns and not just the one that contains the values I'm checking.
So I do indeed have several CSV files and they all have a Service Code column, and I'm trying to create a list for each Service Code that only appears in one file. So I would have "Service Codes only in CSV1", "Service Codes only in CSV2", etc.
Based on some testing and a semi-related question, I've come up with a workable solution, but with all of the nesting and For loops, I'm wondering if there is a more elegant method out there.
Here's what I do have:
$files = Get-ChildItem -LiteralPath "C:\temp\ItemCompare" -Include "*.csv"
$HashList = [System.Collections.Generic.List[System.Collections.Generic.HashSet[String]]]::New()
For ($i = 0; $i -lt $files.Count; $i++){
$TempHashSet = [System.Collections.Generic.HashSet[String]]::New([String[]](Import-Csv $files[$i])."Service Code")
$HashList.Add($TempHashSet)
}
$FinalHashList = [System.Collections.Generic.List[System.Collections.Generic.HashSet[String]]]::New()
For ($i = 0; $i -lt $HashList.Count; $i++){
$UniqueHS = [System.Collections.Generic.HashSet[String]]::New($HashList[$i])
For ($j = 0; $j -lt $HashList.Count; $j++){
#Skip the check when the HashSet would be compared to itself
If ($j -eq $i){Continue}
$UniqueHS.ExceptWith($HashList[$j])
}
$FinalHashList.Add($UniqueHS)
}
It seems a bit messy to me using so many different .NET references, and I know I could make it cleaner with a tag to say using namespace System.Collections.Generic, but I'm wondering if there is a way to make it work using Compare-Object which was my first attempt, or even just a simpler/more efficient method to filter each file.
I believe I found an "elegant" solution based on Group-Object, using only a single pipeline:
# Import all CSV files.
Get-ChildItem $PSScriptRoot\csv\*.csv -File -PipelineVariable file | Import-Csv |
# Add new column "FileName" to distinguish the files.
Select-Object *, #{ label = 'FileName'; expression = { $file.Name } } |
# Group by ServiceCode to get a list of files per distinct value.
Group-Object ServiceCode |
# Filter by ServiceCode values that exist only in a single file.
# Sort-Object -Unique takes care of possible duplicates within a single file.
Where-Object { ( $_.Group.FileName | Sort-Object -Unique ).Count -eq 1 } |
# Expand the groups so we get the original object structure back.
ForEach-Object Group |
# Format-Table requires sorting by FileName, for -GroupBy.
Sort-Object FileName |
# Finally pretty-print the result.
Format-Table -Property ServiceCode, Foo -GroupBy FileName
Test Input
a.csv:
ServiceCode,Foo
1,fop
2,fip
3,fap
b.csv:
ServiceCode,Foo
6,bar
6,baz
3,bam
2,bir
4,biz
c.csv:
ServiceCode,Foo
2,bla
5,blu
1,bli
Output
FileName: b.csv
ServiceCode Foo
----------- ---
4 biz
6 bar
6 baz
FileName: c.csv
ServiceCode Foo
----------- ---
5 blu
Looks correct to me. The values 1, 2 and 3 are duplicated between multiple files, so they are excluded. 4, 5 and 6 exist only in single files, while 6 is a duplicate value only within a single file.
Understanding the code
Maybe it is easier to understand how this code works, by looking at the intermediate output of the pipeline produced by the Group-Object line:
Count Name Group
----- ---- -----
2 1 {#{ServiceCode=1; Foo=fop; FileName=a.csv}, #{ServiceCode=1; Foo=bli; FileName=c.csv}}
3 2 {#{ServiceCode=2; Foo=fip; FileName=a.csv}, #{ServiceCode=2; Foo=bir; FileName=b.csv}, #{ServiceCode=2; Foo=bla; FileName=c.csv}}
2 3 {#{ServiceCode=3; Foo=fap; FileName=a.csv}, #{ServiceCode=3; Foo=bam; FileName=b.csv}}
1 4 {#{ServiceCode=4; Foo=biz; FileName=b.csv}}
1 5 {#{ServiceCode=5; Foo=blu; FileName=c.csv}}
2 6 {#{ServiceCode=6; Foo=bar; FileName=b.csv}, #{ServiceCode=6; Foo=baz; FileName=b.csv}}
Here the Name contains the unique ServiceCode values, while Group "links" the data to the files.
From here it should already be clear how to find values that exist only in single files. If duplicate ServiceCode values within a single file wouldn't be allowed, we could even simplify the filter to Where-Object Count -eq 1. Since it was stated that dupes within single files may exist, we need the Sort-Object -Unique to count multiple equal file names within a group as only one.
It is not completely clear what you expect as an output.
If this is just the ServiceCodes that intersect then this is actually a duplicate with:
Comparing two arrays & get the values which are not common
Union and Intersection in PowerShell?
But taking that you actually want the related object and files, you might use this approach:
$HashTable = #{}
ForEach ($File in Get-ChildItem .\*.csv) {
ForEach ($Object in (Import-Csv $File)) {
$HashTable[$Object.ServiceCode] = $Object |Select-Object *,
#{ n='File'; e={ $File.Name } },
#{ n='Count'; e={ $HashTable[$Object.ServiceCode].Count + 1 } }
}
}
$HashTable.Values |Where-Object Count -eq 1
Here is my take on this fun exercise, I'm using a similar approach as yours with the HashSet but adding [System.StringComparer]::OrdinalIgnoreCase to leverage the .Contains(..) method:
using namespace System.Collections.Generic
# Generate Random CSVs:
$charset = 'abABcdCD0123xXyYzZ'
$ran = [random]::new()
$csvs = #{}
foreach($i in 1..50) # Create 50 CSVs for testing
{
$csvs["csv$i"] = foreach($z in 1..50) # With 50 Rows
{
$index = (0..2).ForEach({ $ran.Next($charset.Length) })
[pscustomobject]#{
ServiceCode = [string]::new($charset[$index])
Data = $ran.Next()
}
}
}
# Get Unique 'ServiceCode' per CSV:
$result = #{}
foreach($key in $csvs.Keys)
{
# Get all unique `ServiceCode` from the other CSVs
$tempHash = [HashSet[string]]::new(
[string[]]($csvs[$csvs.Keys -ne $key].ServiceCode),
[System.StringComparer]::OrdinalIgnoreCase
)
# Filter the unique `ServiceCode`
$result[$key] = foreach($line in $csvs[$key])
{
if(-not $tempHash.Contains($line.ServiceCode))
{
$line
}
}
}
# Test if the code worked,
# If something is returned from here means it didn't work
foreach($key in $result.Keys)
{
$tmp = $result[$result.Keys -ne $key].ServiceCode
foreach($val in $result[$key])
{
if($val.ServiceCode -in $tmp)
{
$val
}
}
}
i was able to get unique items as follow
# Get all items of CSVs in a single variable with adding the file name at the last column
$CSVs = Get-ChildItem "C:\temp\ItemCompare\*.csv" | ForEach-Object {
$CSV = Import-CSV -Path $_.FullName
$FileName = $_.Name
$CSV | Select-Object *,#{N='Filename';E={$FileName}}
}
Foreach($line in $CSVs){
$ServiceCode = $line.ServiceCode
$file = $line.Filename
if (!($CSVs | where {$_.ServiceCode -eq $ServiceCode -and $_.filename -ne $file})){
$line
}
}

Powershell - group array objects by properties and sum

I am working on getting some data out of CSV file with a script and have no idea to solve the most important part - I have an array with few hundred lines, there are about 50 Ids in those lines, and each Id has a few different services attached to it. Each line has a price attached.
I want to group lines by ID and Service and I want each of those groups in some sort of variable so I can sum the prices. I filter out unique IDs and Services earlier in a script because they are different all the time.
Some example data:
$data = #(
[pscustomobject]#{Id='1';Service='Service1';Propertyx=1;Price='5'}
[pscustomobject]#{Id='1';Service='Service2';Propertyx=1;Price='4'}
[pscustomobject]#{Id='2';Service='Service1';Propertyx=1;Price='17'}
[pscustomobject]#{Id='3';Service='Service1';Propertyx=1;Price='3'}
[pscustomobject]#{Id='2';Service='Service2';Propertyx=1;Price='11'}
[pscustomobject]#{Id='4';Service='Service1';Propertyx=1;Price='7'}
[pscustomobject]#{Id='2';Service='Service3';Propertyx=1;Price='5'}
[pscustomobject]#{Id='3';Service='Service2';Propertyx=1;Price='4'}
[pscustomobject]#{Id='4';Service='Service2';Propertyx=1;Price='12'}
[pscustomobject]#{Id='1';Service='Service3';Propertyx=1;Price='8'})
$ident = $data.Id | select -unique | sort
$Serv = $data.Service | select -unique | sort
All help will be appreciated!
Use Group-Object to group objects by common values across one or more properties.
For example, to calculate the sum per Id, do:
$data |Group-Object Id |ForEach-Object {
[pscustomobject]#{
Id = $_.Name
Sum = $_.Group |Measure-Object Price -Sum |ForEach-Object Sum
}
}
Which should yield output like:
Id Sum
-- ---
1 17
2 33
3 7
4 19

Edit CSV without EXCEL

I need to edit a .CSV file by shifting the first column of data down 1 row. then taking the last value in the first column and move it to the top. Any idea how I can do this without using
$objExcel = New-Object -ComObject Excel.Application
Although I wouldn't know why you want to rotate the values in one of the columns, here is how you can do that without the need for Excel.
From your comments, I gather the CSV file has no headers and contains only data rows.
Because of that, the following adds headers when importing the data.
Suppose your csv file looks like this:
Clothing Rental,Chicago Illinois,1,25
Clothing Purchase,Dallas Texas,2,35
Clothing Free of Charge,Phoenix Arizona,3,45
Then the following should do what you want:
$data = Import-Csv -Path 'D:\yourdata.csv' -Header 'Stuff','City','Number','InStock' # or add whatever headers you like
# get the first column as array of values
$column1 = $data.Stuff
# rotate the array values
switch ($column1.Count) {
1 { Write-Host "Nothing to do here. There is only one row of data.."; break}
2 {
# swap the values
$data[0].Stuff,$data[1].Stuff = $data[1].Stuff,$data[0].Stuff
break
}
default {
$newColumn1 = #($column1[-1]; $column1[0..($column1.Count -2)])
# re-write the first column in the data
for ($i = 0; $i -lt $newColumn1.Count; $i++) {
$data[$i].Stuff = $newColumn1[$i]
}
}
}
# output on screen
$data
# output to new CSV file WITH headers
$data | Export-Csv -Path 'D:\your_rotated_data.csv' -NoTypeInformation -Force
# output to new CSV file WITHOUT headers
$data | ConvertTo-Csv -NoTypeInformation | Select-Object -Skip 1 | Set-Content -Path 'D:\your_rotated_data.csv' -Force
The output on screen after running this looks like
Stuff City Number InStock
----- ---- ------ -------
Clothing Free of Charge Chicago Illinois 1 25
Clothing Rental Dallas Texas 2 35
Clothing Purchase Phoenix Arizona 3 45
and you can see all values in the first column ("Stuff") have been rotated, i.e. the last value is now on top and the other values have moved down.

re-arrange and combine powershell custom objects

I have a system that currently reads data from a CSV file produced by a separate system that is going to be replaced.
The imported CSV file looks like this
PS> Import-Csv .\SalesValues.csv
Sale Values AA BB
----------- -- --
10 6 5
5 3 4
3 1 9
To replace this process I hope to produce an object that looks identical to the CSV above, but I do not want to continue to use a CSV file.
I already have a script that reads data in from our database and extracts the data that I need to use. I'll not detail the fairly long script that preceeds this point but in effect it looks like this:
$SQLData = Custom-SQLFunction "SELECT * FROM SALES_DATA WHERE LIST_ID = $LISTID"
$SQLData will contain ~5000+ DataRow objects that I need to query.
One of those DataRow object looks something like this:
lead_id : 123456789
entry_date : 26/10/2018 16:51:16
modify_date : 01/11/2018 01:00:02
status : WRONG
user : mrexample
vendor_lead_code : TH1S15L0NGC0D3
source_id : A543212
list_id : 333004
list_name : AA Some Text
gmt_offset_now : 0.00
SaleValue : 10
list_name is going to be prefixed with AA or BB.
SaleValue can be any integer 3 and up, however realistically extremely unlikely to be higher than 100 (as this is a monthly donation) and will be one of 3,5,10 in the vast majority of occurrences.
I already have script that takes the content of list_name, creates and populates the data I need to use into two separate psobjects ($AASalesValues and $BBSalesValues) that collates the total numbers of 'SaleValue' across the data set.
Because I cannot reliably anticipate the value of any SaleValue I have to dynamically create the psobjects properties like this
foreach ($record in $SQLData) {
if ($record.list_name -match "BB") {
if ($record.SaleValue -gt 0) {
if ($BBSalesValues | Get-Member -Name $($record.SaleValue) -MemberType Properties) {
$BBSalesValues.$($record.SaleValue) = $BBSalesValues.$($record.SaleValue)+1
} else {
$BBSalesValues | Add-Member -Name $($record.SaleValue) -MemberType NoteProperty -Value 1
}
}
}
}
The two resultant objects look like this:
PS> $AASalesValues
10 5 3 50
-- - - --
17 14 3 1
PS> $BBSalesvalues
3 10 5 4
- -- - -
36 12 11 1
I now have the data that I need, however I need to format it in a way that replicates the format of the CSV so I can pass it directly to another existing powershell script that is configured to expect the data in the format that the CSV is in, but I do not want to write the data to a file.
I'd prefer to pass this directly to the next part of the script.
Ultimately what I want to do is to produce a new object/some output that looks like the output from Import-Csv command at the top of this post.
I'd like a new object, say $OverallSalesValues, to look like this:
PS>$overallSalesValues
Sale Values AA BB
50 1 0
10 17 12
5 14 11
4 0 1
3 3 36
In the above example the values from $AASalesValues is listed under the AA column, the values from $BBSalesValues is listed under the BB column, with the rows matching the headers of the two original objects.
I did try this with hashtables but I was unable to work out how to both create them from dynamic values and format them to how I needed them to look.
Finally got there.
$TotalList = #()
foreach($n in 3..200){
if($AASalesValues.$n -or $BBSalesValues.$n){
$AACount = $AASalesValues.$n
$BBcount = $BBSalesValues.$n
$values = [PSCustomObject]#{
'Sale Value'= $n
AA = $AACount
BB = $BBcount
}
$TotalList += $values
}
}
$TotalList
produces an output of
Sale Value AA BB
---------- -- --
3 3 36
4 2
5 14 11
10 18 12
50 1
Just need to add a bit to include '0' values instead of $null.
I'm going to assume that $record contains a list of the database results for either $AASalesValues or $BBSalesValues, not both, otherwise you'd need some kind of selector to avoid counting records of one group with the other group.
Group the records by their SaleValue property as LotPings suggested:
$BBSalesValues = $record | Group-Object SaleValue -NoElement
That will give you a list of the SaleValue values with their respective count.
PS> $BBSalesValues
Count Name
----- ----
36 3
12 10
11 5
1 4
You can then update your CSV data with these values like this:
$file = 'C:\path\to\data.csv'
# read CSV into a hashtable mapping the sale value to the complete record
# (so that we can lookup the record by sale value)
$csv = #{}
Import-Csv $file | ForEach-Object {
$csv[$_.'Sale Values'] = $_
}
# Add records for missing sale values
$($AASalesValues; $BBSalesValues) | Select-Object -Expand Name -Unique | ForEach-Object {
if (-not $csv.ContainsKey($_)) {
$csv[$_] = New-Object -Type PSObject -Property #{
'Sale Values' = $_
'AA' = 0
'BB' = 0
}
}
}
# update records with values from $AASalesValues
$AASalesValues | ForEach-Object {
[int]$csv[$_.Name].AA += $_.Count
}
# update records with values from $BBSalesValues
$BBSalesValues | ForEach-Object {
[int]$csv[$_.Name].BB += $_.Count
}
# write updated records back to file
$csv.Values | Export-Csv $file -NoType
Even with your updated question the approach would be pretty much the same, you'd just add another level of grouping for collecting the sales numbers:
$sales = #{}
$record | Group-Object {$_.list_name.Split()[0]} | ForEach-Object {
$sales[$_.Name] = $_.Group | Group-Object SaleValue -NoElement
}
and then adjust the merging to something like this:
$file = 'C:\path\to\data.csv'
# read CSV into a hashtable mapping the sale value to the complete record
# (so that we can lookup the record by sale value)
$csv = #{}
Import-Csv $file | ForEach-Object {
$csv[$_.'Sale Values'] = $_
}
# Add records for missing sale values
$sales.Values | Select-Object -Expand Name -Unique | ForEach-Object {
if (-not $csv.ContainsKey($_)) {
$prop = #{'Sale Values' = $_}
$sales.Keys | ForEach-Object {
$prop[$_] = 0
}
$csv[$_] = New-Object -Type PSObject -Property $prop
}
}
# update records with values from $sales
$sales.GetEnumerator() | ForEach-Object {
$name = $_.Key
$_.Value | ForEach-Object {
[int]$csv[$_.Name].$name += $_.Count
}
}
# write updated records back to file
$csv.Values | Export-Csv $file -NoType