Select Fields From Duplicate Records for an Array - powershell

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

Related

How would I use powershell to pull data from two different csv files and then use that data to create a new csv file with the data I've pulled?

I'm not sure if I'm on the right track for this or not. So I've been given an assignment where I have two reports on two different cvs files that hold the information below:
One csv holds payment information under these headers:
Payments.csv:
id,employee_id,payment,code
The other holds employee information using the following headers:
Employee.csv:
id,first_name,last_name,email,city,ip_address
The primary key here is id in the employee.csv file while the foreign key is employee_id in the payments.csv file. This means that id on the employee.csv file should match up with employee_id on the payment.csv.
With this information I am supposed to create 2 classes. One will be an employee class that creates objects using the information from the employee.csv file. The other will be a payments class that creates objects using the payments.csv.
I will then need to compare both sets of objects where id on employee.csv equals employee_id on payments.csv. Then I'd like to use this data to create a new csv that consolidates the data on both csv files into one file where employees on employee.csv are linked to their payments on payments.csv.
Any assistance or guidance is appreciated! This is what I've go so far. I might be way off so please no judgement. Just trying to learn. I've hit a road block on exactly what to do after being able to create employee and payment objects.
#Class that creates employee object
class Employee {
[Int]$id
[String]$first_name
[String]$last_name
[String]$email
[String]$city
[String]$ip_address
Employee ([Int]$id,[String]$first_name,[String]$last_name,[String]$email,[String]$city,[String]$ip_address){
$This.id = $id
$This.first_name = $first_name
$This.last_name = $last_name
$This.email = $email
$This.city = $city
$This.ip_address = $ip_address
}
}
#Class that creates payment object
class Payment{
[Int]$id
[Int]$employee_id
[String]$payment
[String]$code
Payment ([Int]$id,[Int]$employee_id,[String]$payment,[String]$code){
$This.id = $id
$This.employee_id = $employee_id
$This.payment = $payment
$This.code = $code
}
}
#Importing spreadsheets w/ data being used
$ImportedEmployees = Import-Csv ".\Employee.csv"
$ImportedPayments = Import-Csv ".\Payment.csv"
$FinalEmployeeReport = #{}
#Calling [Employee] to create new objects using the employee.csv
Foreach ($Employee in $ImportedEmployees){
$NewEmployeeEntry = [Employee]::new([Int]$Employee.id,[String]$Employee.first_name,[String]$Employee.last_name,[String]$Employee.email,[String]$Employee.city,[String]$Employee.ip_address)
#Adding object to $FinalEmployeeReport
$FinalEmployeeReport.Add([String]$NewEmployeeEntry.last_name,[Int]$NewEmployeeEntry.id)
}
Foreach ($Payment in $ImportedPayments)
{
$NewPayment = [Payment]::new([Int]$Payment.id,[Int]$Payment.employee_id,[String]$Payment.payment,[String]$Payment.code)
$FinalEmployeeReport.Add[Int]$Payment.employee_id,[String]$Payment.payment))
}
Foreach($Payment in $ImportedPayments){
$NewPayment = [Payment]::new([Int]$Payment.id,[Int]$Payment.employee_id,[String]$Payment.payment,[String]$Payment.code)
Foreach($NewEmployeeEntry in $Payment){
if($NewPayment.employee_id -eq $NewEmployeeEntry.id ){
$NewEmployeeEntry.Add($NewPayment)
}
}
}
$FinalEmployeeReport.Add($NewEmployeeEntry)
One way of achieving this is by using a simple loop and inside find matching records based on the employee id.
If these are your input fies
payments.csv
id,employee_id,payment,code
123,8765,1500,abc123
456,9007,100,xyz456
999,9007,200,def666
employee.csv
id,first_name,last_name,email,city,ip_address
9007,John,Doe,jdoe#yourcompany.com,Miami,10.10.10.10
8765,Joe,Bloggs,jbloggs#somewhere.org,Salem,10.11.12.13
Then try
# load both csv files
$payments = Import-Csv -Path 'D:\Test\payments.csv'
$employees = Import-Csv -Path 'D:\Test\employee.csv'
# loop through the employees records
$result = foreach ($emp in $employees){
# find a record in the payments.csv where the .employee_id is equal to the .id in the employee.csv
$payments | Where-Object { $_.employee_id -eq $emp.id } | ForEach-Object {
# create an object with properties from both csv files combined
$obj = $emp | Select-Object #{Name = 'employee_id'; Expression = {$_.id}}, * -ExcludeProperty id
# add the details from $payments to this
$obj | Add-Member -MemberType NoteProperty -Name 'payment_id' -Value $_.id
$obj | Add-Member -MemberType NoteProperty -Name 'payment' -Value $_.payment
$obj | Add-Member -MemberType NoteProperty -Name 'payment_code' -Value $_.code
# output the combined object to be collected in variable $result
$obj
}
}
# now you can show the results in the console
$result | Format-Table -AutoSize
# and save as new csv file
$result | Export-Csv -Path 'D:\Test\EmployeePayments.csv' -NoTypeInformation
Output on screen:
employee_id first_name last_name email city ip_address payment_id payment payment_code
----------- ---------- --------- ----- ---- ---------- ---------- ------- ------------
9007 John Doe jdoe#yourcompany.com Miami 10.10.10.10 456 100 xyz456
9007 John Doe jdoe#yourcompany.com Miami 10.10.10.10 999 200 def666
8765 Joe Bloggs jbloggs#somewhere.org Salem 10.11.12.13 123 1500 abc123

How to retrieve associated value from second CSV in Foreach

How would I go about obtaining the value of a secondary field in a CSV, while foreaching results in another CSV?
Ex: List of computers, then search for serial in list, then get response.
CSV1 has list of computer names, serials ,and vendor IDs.
CSV2 has list of vendor IDS, and responsible party
Sample code:
$systems = import-csv -path "K:\systems.csv"
$vendors = import-csv -path "K:\vendors.csv"
$vendortable= #{}
$vendors | ForEach-Object {
$vendortable[$_.vendorid] = New-Object -Type PSCustomObject -Property #{
'ID' = $_.vendorid
'Managed' = $_.Managed
}
}
Foreach ($computer in $systems){
$host = $system.Host
if ($vendortable.key -match $system.vendorid){
$xvendor = $vendortable.Managed
}
$vendorobj = New-Object PSObject -property #{
'Host'=$System.host
'Mgr=$xvendor
}
$vendorobj |Select-object "Host","Mgr" |Export /////////
This returns an object of All values System.Object[] in the vendor table, not just one.
CSV1
Host,Serial,VendorID
A15,gtjk123,9001
C15,gtjk456,6402
T15,gtjk678,2301
S15,gtjk103,0101
CSV2
VendorID,Managed
9001,Trancom
6402,Stratus
2301,Psycorp
0101,Dell
Let's break this down into steps.
You want to iterate over the entire vendor list.
You want to retrieve the associated host from CSV1 and combine with manager (based on your select statement)
What you can do is make a lookup table of CSV1 using Group-Object and the -AsHashTable parameter specifying the "key" as vendorid.
$csv1 = #'
Host,Serial,VendorID
A15,gtjk123,9001
C15,gtjk456,6402
T15,gtjk678,2301
S15,gtjk103,0101
'# | ConvertFrom-Csv | Group-Object -Property vendorid -AsHashTable
Now you can run through each item of CSV2 and extract the required info.
$csv2 = #'
VendorID,Managed
9001,Trancom
6402,Stratus
2301,Psycorp
0101,Dell
'# | ConvertFrom-Csv
$csv2 | foreach {
[PSCustomObject]#{
Host = $csv1[$_.vendorid].host
Managed = $_.managed
}
}
Host Managed
---- -------
A15 Trancom
C15 Stratus
T15 Psycorp
S15 Dell
If you wanted to get all the properties
$csv2 | foreach {
[PSCustomObject]#{
Host = $csv1[$_.vendorid].host
Managed = $_.managed
VendorID = $_.vendorid
Serial = $csv1[$_.vendorid].serial
}
}
Host Managed VendorID Serial
---- ------- -------- ------
A15 Trancom 9001 gtjk123
C15 Stratus 6402 gtjk456
T15 Psycorp 2301 gtjk678
S15 Dell 0101 gtjk103

Filter results further based on pattern

Using PowerShell, we are able to successfully extract lines we need from doc.
Code:
Get-Content "C:\Contract.doc" |
Select-String -Pattern "in relation to any Facility" |
Select -Property #{Name = 'Name'; Expression = {$_.Line}}
Output:
Name
----
in relation to any Facility A Loan [2% ] per cent. per annum;
in relation to any Facility B Loan [ 5% ] per cent. per annum;
What we are looking for is extract 2% ... 5% from the above output
Code, we are trying is not working for us:
Get-Content "C:\Contract.doc" |
Select-String -Pattern "in relation to any Facility" |
Select -Property #{Name = 'Name'; Expression = {$_.Line}} |
Select-String '\[\?+([^?]+)\?+\]' |
ForEach-Object { $_.Matches.Groups[1].Value }
Can anyone please help how to extract like below:
"in relation to any Facility A Loan [2% ] per cent. per annum", "2%"
Part of Word Doc: Contract doc
"Margin" means:
(a) in relation to any Facility A Loan [2% ] per cent. per annum;
(b) in relation to any Facility B Loan [ 5% ] per cent. per annum;
(c) [in relation to any Incremental Facility Loan, the percentage rate per annum specified as such in the Incremental Facility Notice relating to the Incremental Facility under which that Incremental Facility Loan is made or is to be made;]
Check on the next snippet.
Get-Content C:\Contract.doc |
Select-String -Pattern #'
\b(in relation to any Facility [A-Z] Loan \[\s*(\d+%)\s*\] per cent. per annum);
'# |
Select-Object #{Name = 'Line'; Expression = {$_.Matches.Groups[1].Value}},
#{Name = 'Result'; Expression = {$_.Matches.Groups[2].Value}}
To get an explanation of the regex \b(in relation to any Facility [A-Z] Loan \[\s*(\d+%)\s*\] per cent. per annum);, please click here.
With a Regular Expression the solution is more efficient:
Get-Content .\contract.doc|
Where-Object {$_ -match 'in relation to any Facility.*\[([\d% ]+)\]'}|
ForEach-Object{
[PSCustomObject]#{
Name = $_
Value = $Matches[1].trim()
}
}
And I really should scroll down before posting a similar answer.
Try this:
EDIT:
$FinalTable = Get-Content .\Contract.doc |
select-string -pattern "in relation to any Facility" |
Select -Property #{Name = 'Name'; Expression = {$_.Line}} |
ForEach-Object {
$str = $_.Name
$start = $str.indexOf("[") + 1
$end = $str.indexOf("]", $start)
$length = $end - $start
$result = ($str.substring($start, $length)).trim()
#Creating a custom object to display in table format
$Obj = New-Object -TypeName PSCustomObject
Add-Member -InputObject $Obj -MemberType NoteProperty -Name Name -Value $str
Add-Member -InputObject $Obj -MemberType NoteProperty -Name Value -Value $result
$obj
}
$FinalTable

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

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

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