Aggregating tasks by duration for each day in a week/month? (PoSH) - powershell

I am parsing JSON from a web service to get my tasks (using a TimeFlip). Right now, I get back each task, when it occurred, and duration, so the data looks like this:
(taskname, start, durationinSec)
TaskA,"6/5/2018 12:16:36 PM",312
TaskB,"6/5/2018 12:30:36 PM",200
TaskA,"6/6/2018 08:00:00 AM",150
TaskA,"6/6/2018 03:00:00 PM",150
(etc etc)
I would like to generate a rollup report, showing by day which tasks had how much time.
While the data will span weeks, I'm just trying to do a weekly report that I can easily transcribe into our time app (since they won't give me an API key). So I'll do something like where {$_.start -gt (? {$_.start -gt (get-date -Hour 0 -Minute 00 -Second 00).adddays(-7)} first.
6/5/2018 6/6/2018
TaskA 312 300
TaskB 200
How can I do that? I assume group-object, but unclear how you'd do either the pivot or even the grouping.

The following doesn't output a pivot table, but performs the desired grouping and aggregation:
$rows = #'
taskname,start,durationinSec
TaskA,"6/5/2018 12:16:36 PM",312
TaskB,"6/5/2018 12:30:36 PM",200
TaskA,"6/6/2018 08:00:00 AM",150
TaskA,"6/6/2018 03:00:00 PM",150
'# | ConvertFrom-Csv
$rows | Group-Object { (-split $_.start)[0] }, taskname | ForEach-Object {
$_ | Select-Object #{ n='Date'; e={$_.Values[0]} },
#{ n='Task'; e={$_.Values[1]} },
#{ n='Duration'; e={ ($_.Group | Measure-Object durationInSec -Sum).Sum } }
}
(-split $_.start)[0] splits each start value by whitespace and returns the first token ([0]), which is the date portion of the time stamp; e.g., 6/5/2018 is returned for 6/5/2018 12:16:36 PM; passing this operation as a script block ({ ... }) to Group-Object means that grouping happens by date only, not also time (in addition to grouping by taskname).
This yields:
Date Task Duration
---- ---- --------
6/5/2018 TaskA 312
6/5/2018 TaskB 200
6/6/2018 TaskA 300
To construct pivot-table-like output requires substantially more effort, and it won't be fast:
Assume that $objs contains the objects created above ($objs = $rows | Group-Object ...).
# Get all distinct dates.
$dates = $objs | Select-Object -Unique -ExpandProperty Date
# Get all distinct tasks.
$tasks = $objs | Select-Object -Unique -ExpandProperty Task
# Create an ordered hashtable that contains an entry for each task that
# holds a nested hashtable with (empty-for-now) entries for all dates.
$ohtPivot = [ordered] #{}
$tasks | ForEach-Object {
$ohtDates = [ordered] #{}
$dates | ForEach-Object { $ohtDates[$_] = $null }
$ohtPivot[$_] = $ohtDates
}
# Fill the hashtable from the grouped objects with the task- and
# date-specific durations.
$objs | ForEach-Object { $ohtPivot[$_.Task][$_.Date] = $_.Duration }
# Output the resulting hashtable in pivot-table-like form by transforming
# each entry into a custom object
$ohtPivot.GetEnumerator() | ForEach-Object {
[pscustomobject] #{ Task = $_.Key } | Add-Member -PassThru -NotePropertyMembers $_.Value
}
The above yields:
Task 6/5/2018 6/6/2018
---- -------- --------
TaskA 312 300
TaskB 200

Googling for PowerShell and Pivot I found this gist.github.com with a more universal way to create the PivotTable.
To transpose (swap x,y) you simply change the variables $rotate, $keep
It has the additional benefit of calculating a row Total
## Q:\Test\2018\06\09\PivotTable.ps1
## Source https://gist.github.com/andyoakley/1651859
# #############################################################################
# Rotates a vertical set similar to an Excel PivotTable
# #############################################################################
$OutputFile = "MyPivot.csv"
$data = #'
taskname,start,duration
TaskA,"6/5/2018 12:16:36 PM",312
TaskB,"6/5/2018 12:30:36 PM",200
TaskA,"6/6/2018 08:00:00 AM",150
TaskA,"6/6/2018 03:00:00 PM",150
'# | ConvertFrom-Csv |Select-Object taskname, duration, #{n='start';e={($_.start -split ' ')[0]}}
# Fields of interest
$rotate = "taskname" # Bits along the top
$keep = "start" # Those along the side
$value = "duration" # What to total
#-------------------- No need to change anything below ------------------------
# Creatre variable to store the output
$rows = #()
# Find the unique "Rotate" [top row of the pivot] values and sort ascending
$pivots = $data | select -unique $rotate | foreach { $_.$rotate} | Sort-Object
# Step through the original data...
# for each of the "Keep" [left hand side] find the Sum of the "Value" for each "Rotate"
$data |
group $keep |
foreach {
$group = $_.Group
# Create the data row and name it as per the "Keep"
$row = new-object psobject
$row | add-member NoteProperty $keep $_.Name
# Cycle through the unique "Rotate" values and get the sum
foreach ($pivot in $pivots) {
$row | add-member NoteProperty $pivot ($group | where { $_.$rotate -eq $pivot } | measure -sum $value).Sum
}
# Add the total to the row
$row | add-member NoteProperty Total ($group | measure -sum $value).Sum
# Add the row to the collection
$rows += $row
}
# Do something with the pivot rows
$rows | Format-Table
$rows | Export-Csv $OutputFile -NoTypeInformation
Sample output:
start TaskA TaskB Total
----- ----- ----- -----
6/5/2018 312 200 512
6/6/2018 300 300
Or x/y swapped
taskname 6/5/2018 6/6/2018 Total
-------- -------- -------- -----
TaskA 312 300 612
TaskB 200 200

Related

Powershell - Group and count from CSV file

In my CSV file, I have two columns with header Start_date and Status. I am trying to find out the success percentage for each Start_date
Start_date Status
------------------------------------------
02-03-2022 Completed
02-03-2022 Completed
03-03-2022 Failed
03-03-2022 Completed
I am looking for a final output like below which I export CSV
Start_date Total Completed Failed Success %
02-03-2022 2 2 0 100
03-03-2022 2 1 1 50
As a first step, I am trying to get the count of each day job using below code.
$data = Import-Csv "C:\file.csv"
$data | group {$_.Start_date} | Sort-Object {$_.Start_date} | Select-Object {$_.Status}, Count
Above code will give me output like
$_.Status Count
--------- -----
1
1
it is not showing the date value. what will be the correct approach for this issue ?
You can use Group-Object to group the objects by the date column, then it's just math:
$csv = #'
Start_date,Status
02-03-2022,Completed
02-03-2022,Completed
03-03-2022,Failed
03-03-2022,Completed
'# | ConvertFrom-Csv
# This from your side should be:
# Import-Csv path/to/csv.csv | Group-Object ....
$csv | Group-Object Start_date | ForEach-Object {
$completed, $failed = $_.Group.Status.where({ $_ -eq 'Completed' }, 'Split')
$totalc = $_.Group.Count
$complc = $completed.Count
$failc = $failed.Count
$success = $complc / $totalc
[pscustomobject]#{
Start_Date = $_.Name
Total = $totalc
Completed = $complc
Failed = $failc
Success = $success.ToString('P0')
}
}
Here's another one:
$csv = Import-Csv C:\Temp\tmp.csv
$Results = #()
foreach ($group in $csv | Group Start_date)
{
$Completed = ($group.Group | group status | ? Name -eq Completed).Count
$Failed = ($group.Group | group status | ? Name -eq Failed).Count
$row = "" | Select Start_date,Total,Completed,Failed,"Success %"
$row.Start_date = $group.Name
$row.Total = $group.Count
$row.Completed = $Completed
$row.Failed = $Failed
$row."Success %" = $Completed / 2 * 100
$Results += $row
}
$results
Start_date Total Completed Failed Success %
---------- ----- --------- ------ ---------
02-03-2022 2 2 0 100
03-03-2022 2 1 1 50

Count the number of columns in an array with PowerShell

I have the following code:
$array = Get-Process
Now, I want to count the number of columns. I have tried:
$array.Columns.Count
$array.Columns
And all without luck.
What should I do to get the correct number of columns? That should be 8.
<<Added on January, 27th, 2021: >>
I have modified the code to:
$Processes = #(Get-Process | Select-Object -Property Name, Description, ProductVersion, #{Name="Application";Expression={$_.Description + " " + $_.ProductVersion}},#{Name="Executable";Expression={(($_."Path").split("\"))[-1]}} |Where {$_.Executable})
$Array = #()
$Record = [ordered] #{"Name" = "";
"Application" = ""}
ForEach ($ProcesName in $Processes)
{
$Record."Name" = $ProcesName.Name
$Record."Application" = $ProcesName.Application
$objRecord = New-Object PSObject -Property $Record
$Array += $objRecord
}
Clear-Host
Write-Host "Number of columns: $($Array.Column.Count)"
Write-Host "Number rows: $($Array.Count)"
The result is the same: both the column and row are the same. And that is not correct. I want to use the technique in a new script with much more columns.
<<End part that has been added on January, 27th, 2021 >>
Feedback is appreciated.
With kind regards,
TheStingPilot
$array is an array of objects of type System.Diagnostics.Process. There aren't columns as such. The objects have properties which you may choose to display in columns using $array | ft or as a list $array | fl *. You can count the properties like this:
$array | Get-Member -MemberType Property | measure
If you'd like the answer to be 8 then you can do this:
$array | select Handles, NPM, PM, WS, CPU, Id, SI, Name | Get-Member -MemberType NoteProperty | measure

Select Fields From Duplicate Records for an Array

I've got a CSV file that is imported that looks like this:
Customer ID Contract Start Contract End Region Customer
2-213456 2/20/2018 1/1/2030 NA Acme
2-213456 6/18/2019 6/17/2020 NA Acme
2-213456 6/18/2020 6/30/2021 NA Acme
3-213458 6/27/2019 6/26/2020 CAN Acme Shipping
2-123456 6/27/2020 6/27/2021 AUS Acme Manufacturing
5-123576 6/29/2019 6/28/2020 AUS Acme Storage
Which I'm trying to build an array that only has the unique values (Customer ID) but, would like to include the earliest Contract Start date and the latest Contract End date to get a result like:
Customer ID Contract Start Contract End Region Customer
2-213456 2/20/2018 6/30/2021 NA Acme
3-213458 6/27/2019 6/26/2020 CAN Acme Shipping
2-123456 6/27/2020 6/27/2021 AUS Acme Manufacturing
5-123576 6/29/2019 6/28/2020 AUS Acme Storage
This is what I have but, I keep getting a System.Object[] for the dates
$Data = import-csv -path "C:\Customers.csv"
$Final = #()
$N = 0
$count = $Data.count
foreach ($record in $Data)
{
Write-Host "Record " $N " of " $Count
$Rec = New-Object System.Object
$Rec | Add-Member -type NoteProperty -name "Customer ID" -value $record.'Customer ID'
$StartDate = $Data | Foreach-Object {$_.'Contract Start' = [DateTime]$_.'Contract Start'; $_} | Group-Object 'Customer ID' | Foreach-Object {$_.Group | Sort-Object 'Contract Start' | Select-Object -Property $record.'Contract Start' -first 1}
$Rec | Add-Member -type NoteProperty -name "Contract Start" -value $StartDate
$EndDate = $Data | Foreach-Object {$_.'Contract End' = [DateTime]$_.'Contract End'; $_} | Group-Object 'Customer ID' | Foreach-Object {$_.Group | Sort-Object 'Contract End' | Select-Object -Property $record.'Contract End' -Last 1}
$Rec | Add-Member -type NoteProperty -name "Contract End" -value $EndDate
$Rec | Add-Member -type NoteProperty -name "Region" -value $record.'Region'
$Rec | Add-Member -type NoteProperty -name "Customer" -value $record.'Customer'
$Final += $Rec
$N++
}
I got a lot of errors about Datetime trying to replicate what you have posted above. You've tried to do a lot in one place when setting and sorting the start and end dates, so our first task is to simplify that. Knowing that you could potentially have a lot of customer data, I thought it best to group the customers by their ID in a hashtable. That way we can call the customer ID and immediately just have their records. PowerShell classes allow us to create a couple of methods to import the data in to the hashtable, parse the dates as part of the import. The final method exports your data picking the earliest start date, and the latest end date. Fully tested solution below.
class Customers {
[hashtable]$Accounts
# Constructor
Customers() {
$this.Accounts = #{}
}
# Methods
[void]AddCustomerData([psobject[]]$Records) {
foreach ($Record in $Records) {
# Convert the dates to datetime objects so we can sort them later
$Record = $this.ParseDates($Record)
$ID = $Record."Customer ID"
# If the hashtable already contains the customer ID, we need to add the new record to their existing ones.
if ($this.Accounts.Contains($ID)) {
$CustomerRecords = $this.Accounts[$ID]
$CustomerRecords += $Record
$this.Accounts[$ID] = $CustomerRecords
}
# If it doesn't we create a new record with the value as an array.
else {
$this.Accounts[$ID] = #(,$Record)
}
}
}
[psobject]ParseDates([psobject]$Row) {
# Your dates appear to be US format, so I've kept them that way, change the culture from 'en-US' if you need to.
$Row."Contract Start" = [Datetime]::Parse($Row."Contract Start",[cultureinfo]::new("en-US",$false))
$Row."Contract End" = [Datetime]::Parse($Row."Contract End",[cultureinfo]::new("en-US",$false))
return $Row
}
[psobject[]]PrintCustomerData() {
$CustomerData = #()
# Loop through the hashtable
$this.Accounts.GetEnumerator() | ForEach-Object {
$Contracts = $_.Value
# Find the earliest start date for the current customer by sorting in ascending order
$StartDate = $Contracts."Contract Start" | Sort-Object | Select-Object -First 1
# Find the latest end date for the current customer by sorting in descending order
$EndDate = $Contracts."Contract End" | Sort-Object -Descending | Select-Object -First 1
# Create a new PSObject for each customer, selecting a Unique value for Region and Customer as it should be the same across records
$CustomerData += [PSCustomObject] #{
"Customer ID" = $_.Key
"Contract Start" = $StartDate
"Contract End" = $EndDate
Region = $($Contracts | Select-Object -Unique -ExpandProperty Region)
Customer = $($Contracts | Select-Object -Unique -ExpandProperty Customer)
}
}
return $CustomerData
}
}
Usage:
$csv = Import-Csv -Path .\Desktop\test.csv
# Create a new instance of the class
$customers = [Customers]::new()
# Add the CSV data to a the Accounts hashtable
$customers.AddCustomerData($csv)
# Print out the data from the hashtable in the desired format.
$customers.PrintCustomerData() | Format-Table -AutoSize
Customer ID Contract Start Contract End Region Customer
----------- -------------- ------------ ------ --------
2-213456 20/02/2018 00:00:00 01/01/2030 00:00:00 NA Acme
2-123456 27/06/2020 00:00:00 27/06/2021 00:00:00 AUS Acme Manufacturing
3-213458 27/06/2019 00:00:00 26/06/2020 00:00:00 CAN Acme Shipping
5-123576 29/06/2019 00:00:00 28/06/2020 00:00:00 AUS Acme Storage
And now you have your records in a hashtable, you can do other awesome stuff like look up the records for a particular customer.
$customers.Accounts['2-213456'] | Format-Table -AutoSize
Customer ID Contract Start Contract End Region Customer
----------- -------------- ------------ ------ --------
2-213456 20/02/2018 00:00:00 01/01/2030 00:00:00 NA Acme
2-213456 18/06/2019 00:00:00 17/06/2020 00:00:00 NA Acme
2-213456 18/06/2020 00:00:00 30/06/2021 00:00:00 NA Acme
Using this data.csv as an example input:
Customer ID,Contract Start,Contract End,Region,Customer
2-213456,2/20/2018,1/1/2030,NA,Acme
2-213456,6/18/2019,6/17/2020,NA,Acme
2-213456,6/18/2020,6/30/2021,NA,Acme
3-213458,6/27/2019,6/26/2020,CAN,Acme Shipping
2-123456,6/27/2020,6/27/2021,AUS,Acme Manufacturing
5-123576,6/29/2019,6/28/2020,AUS,Acme Storage
We can use Group-Object to group by Customer ID and use Sort-Object to sort by datetime versions of Contract Start and Contract End. Then we can construct a new System.Management.Automation.PSCustomObject for each compressed record, and format the System.Object[] array with Format-Table.
$array = Import-Csv -Path .\data.csv | Group-Object -Property "Customer ID" | ForEach-Object {
$contractStart = $_.Group |
Sort-Object -Property #{Expression = {[datetime]$_."Contract Start"}} |
Select-Object -First 1
$contractEnd = $_.Group |
Sort-Object -Property #{Expression = {[datetime]$_."Contract End"}} |
Select-Object -Last 1
[PSCustomObject]#{
"Customer ID" = $_.Name
"Contract Start" = $contractStart."Contract Start"
"Contract End" = $contractEnd."Contract End"
"Region" = $contractStart.Region
"Customer" = $contractStart.Customer
}
}
$array.GetType().FullName
$array | Format-Table -AutoSize
Which results in the following table result:
System.Object[]
Customer ID Contract Start Contract End Region Customer
----------- -------------- ------------ ------ --------
2-123456 6/27/2020 6/27/2021 AUS Acme Manufacturing
2-213456 2/20/2018 1/1/2030 NA Acme
3-213458 6/27/2019 6/26/2020 CAN Acme Shipping
5-123576 6/29/2019 6/28/2020 AUS Acme Storage

Powershell Get-Random with Constraints

I'm currently using the Get-Random function of Powershell to randomly pull a set number of rows from a csv. I need to create a constraint that says if one id is pulled, find the other ids that match it and pull their value.
Here is what I currently have:
$chosenOnes = Import-CSV C:\Temp\pk2.csv | sort{Get-Random} | Select -first 6
$i = 1
$count = $chosenOnes | Group-Object householdID
foreach ($row in $count)
{
if ($row.count -gt 1)
{
$students = $row.Group.Student
foreach ($student in $students)
{
$name = $student.tostring()
#...do something
$i = $i + 1
}
}
else
{
$name = $row.Group.Student
if($i -le 5)
{
#...do something
}
else
{
#...do something
}
$i = $i + 1
}
}
Example dataset
ID,name
165,Ernest Hemingway
1204,Mark Twain
1578,Stephen King
1634,Charles Dickens
1726,George Orwell
7751,John Doe
7751,Tim Doe
In this example, there are 7 rows but I'm randomly selecting 6 in my code. What needs to happen is when ID=7751 then I must return both rows where ID=7751. The IDs cannot not be statically set in the code.
Use Get-Random directly, with -Count, to extract a given number of random elements from a collection.
$allRows = Import-CSV C:\Temp\pk2.csv
$chosenHouseholdIDs = ($allRows | Get-Random -Count 6).householdID
Then filter all rows by whether their householdID column contains one of the 6 randomly selected rows' householdID values (PSv3+ syntax), using the -in array-containment operator:
$allRows | Where-Object householdID -in $chosenHouseholdIDs
Optional reading: performance considerations:
$allRows | Get-Random -Count 6 is not only conceptually simpler, but also much faster than $allRows | Sort-Object { Get-Random } | Select-Object -First 6
Using the Time-Command function to compare the performance of two approaches, using a 1000-row test file with 10 columns yields the following sample timings on my Windows 10 VM in Windows PowerShell - note that the Sort-Object { Get-Random }-based solution is more than 15(!) times slower:
Factor Secs (100-run avg.) Command TimeSpan
------ ------------------- ------- --------
1.00 0.007 $allRows | Get-Random -Count 6 00:00:00.0072520
15.65 0.113 $allRows | Sort-Object { Get-Random } | Select-Object -First 6 00:00:00.1134909
Similarly, a single pass through all rows to find matching IDs via array-containment operator -in performs much better than looping over the randomly selected IDs and searching all rows for each.
I tried sticking with your beginning and came up with this.
$Array = Import-CSV C:\test\StudtentTest.csv
$Array | Sort{Get-Random} | select -first 2 | %{
$id = $_.id
$Array | ?{$_.id -eq $id} | %{
$_
}
}
$Array will be your parsed CSV
We pipe in and sort by random select -first 2 (in this case)
Save the ID of the object into $id and then search the array for that ID and dispaly each that matches
If same ID does match you end up with something like
ID name
-- ----
7751 John Doe
7751 Tim Doe
1634 Charles Dickens

How do you export objects with a varying amount of properties?

Warning - I've asked a similar question in the past but this is slightly different.
tl;dr; I want to export objects which have a varying number of properties. eg; object 1 may have 3 IP address and 2 NICs but object 2 has 7 IP addresses and 4 NICs (but not limited to this amount - it could be N properties).
I can happily capture and build objects that contain all the information I require. If I simply output my array to the console each object is shown with all its properties. If I want to out-file or export-csv I start hitting a problem surrounding the headings.
Previously JPBlanc recommended sorting the objects based on the amount of properties - ie, the object with the most properties would come first and hence the headings for the most amount of properties would be output.
Say I have built an object of servers which has varying properties based on IP addresses and NIC cards. For example;
ServerName: Mordor
IP1: 10.0.0.1
IP2: 10.0.0.2
NIC1: VMXNET
NIC2: Broadcom
ServerName: Rivendell
IP1: 10.1.1.1
IP2: 10.1.1.2
IP3: 10.1.1.3
IP4: 10.1.1.4
NIC1: VMXNET
Initially, if you were to export-csv an array of these objects the headers would be built upon the first object (aka, you would only get ServerName, IP1, IP2, NIC1 and NIC2) meaning for the second object you would lose any subsequent IPs (eg IP3 and IP4). To correct this, before an export I sort based on the number of IP properties - tada - the first object now has the most IPs in the array and hence none of the subsequent objects IPs are lost.
The downside is when you then have a second varying property - eg NICs. Once my sort is complete based on IP we then have the headings ServerName, IP1 - IP4 and NIC1. This means the subsequent object property of NIC2 is lost.
Is there a scalable way to ensure that you aren't losing data when exporting objects like this?
Try:
$o1 = New-Object psobject -Property #{
ServerName="Mordor"
IP1="10.0.0.1"
IP2="10.0.0.2"
NIC1="VMXNET"
NIC2="Broadcom"
}
$o2 = New-Object psobject -Property #{
ServerName="Rivendell"
IP1="10.1.1.1"
IP2="10.1.1.2"
IP3="10.1.1.3"
IP4="10.1.1.4"
NIC1="VMXNET"
}
$arr = #()
$arr += $o1
$arr += $o2
#Creating output
$prop = $arr | % { Get-Member -InputObject $_ -MemberType NoteProperty | Select -ExpandProperty Name } | Select -Unique | Sort-Object
$headers = #("ServerName")
$headers += $prop -notlike "ServerName"
$arr | ft -Property $headers
Output:
ServerName IP1 IP2 IP3 IP4 NIC1 NIC2
---------- --- --- --- --- ---- ----
Mordor 10.0.0.1 10.0.0.2 VMXNET Broadcom
Rivendell 10.1.1.1 10.1.1.2 10.1.1.3 10.1.1.4 VMXNET
If you know the types(NICS, IPS..), but not the count(ex. how many NICS) you could try:
#Creating output
$headers = $arr | % { Get-Member -InputObject $_ -MemberType NoteProperty | Select -ExpandProperty Name } | Select -Unique
$ipcount = ($headers -like "IP*").Count
$niccount = ($headers -like "NIC*").Count
$format = #("ServerName")
for ($i = 1; $i -le $ipcount; $i++) { $format += "IP$i" }
for ($i = 1; $i -le $niccount; $i++) { $format += "NIC$i" }
$arr | ft -Property $format
What about getting a list of all unique property headers and then doing a select on all the objects? When you do a select on an object for a nonexistent property it will create a blank one.
$allHeaders = $arrayOfObjects | % { Get-Member -inputobject $_ -membertype noteproperty | Select -expand Name } | Select -unique
$arrayOfObjects | Select $allHeaders
Granted you are looping through ever object to get the headers, so for a very large amount of objects it may take awhile.
Here's my attempt at a solution. I'm very tired now so hopefully it makes sense. Basically I'm calculating the largest amount of NIC and IP note properties, creating a place holder object that has those amounts of properties, adding it as the first item in a CSV, and then removing it from the CSV.
# Create example objects
$o1 = New-Object psobject -Property #{
ServerName="Mordor"
IP1="10.0.0.1"
IP2="10.0.0.2"
NIC1="VMXNET"
NIC2="Broadcom"
}
$o2 = New-Object psobject -Property #{
ServerName="Rivendell"
IP1="10.1.1.1"
IP2="10.1.1.2"
IP3="10.1.1.3"
IP4="10.1.1.4"
NIC1="VMXNET"
}
# Add to an array
$servers = #($o1, $o2)
# Calculate how many IP and NIC properties there are
$IPColSize = ($servers | Select IP* | %{($_ | gm -MemberType NoteProperty).Count} | Sort-Object -Descending)[0]
$NICColSize = ($servers | Select NIC* | %{($_ | gm -MemberType NoteProperty).Count} | Sort-Object -Descending)[0]
# Build a place holder object that will contain enough properties to cover all of the objects in the array.
$cmd = '$placeholder = "" | Select ServerName, {0}, {1}' -f (#(1..$IPColSize | %{"IP$_"}) -join ", "), (#(1..$NICColSize | %{"NIC$_"}) -join ", ")
Invoke-Expression $cmd
# Convert to CSV and remove the placeholder
$csv = $placeholder,$servers | %{$_ | Select *} | ConvertTo-Csv -NoTypeInformation
$csv | Select -First 1 -Last ($csv.Count-2) | ConvertFrom-Csv | Export-Csv Solution.csv -NoTypeInformation