Powershell populate word table from an array - arrays

I have a PS script that will import a csv into several arrays and I need it to populate a table in word. I am able to get the data into the arrays, and create a table with headers and the correct number of rows, but cannot get the data from the arrays into the table. Doing lots of google searches led me to the following code. Any help is greatly appreciated.
Sample of My_File.txt
Number of rows will vary, but the header row is always there.
component,id,iType,
VCT,AD-1234,Story,
VCT,Ad-4567,DR,
$component = #()
$id = #()
$iType =#()
$vFile = Import-CSV ("H:\My_file.txt")
$word = New-Object -ComObject "Word.Application"
$vFile | ForEach-Object {
$component += $_.components
$id += $_.id
$iType +=_.iType
}
$template = $word.Documents.Open ("H:\Test.docx")
$template = $word.Document.Add()
$word.Visible = $True
$Number_rows = ($vFile.count +1)
$Number_cols = 3
$range = $template.range()
$template.Tables.add($range, $Number_rows, $Number_cols) | out-null
$table = $template.Tables.Item(1)
$table.cell(1,1).Range.Text = "Component"
$table.cell(1,2).Range.Text = "ID"
$table.cell(1,3).Range.text = "Type"
for ($i=0; $i -lt; $vFile.count+2, $i++){
$table.cell(($i+2),1).Range.Text = $component[$i].components
$table.cell(($i+2),2).Range.Text = $id[$i].id
$table.cell(($i+2),3).Range.Text = $iType[$i].iType
}
$Table.Style = "Medium Shading 1 - Accent 1"
$template.SaveAs("H:\New_Doc.docx")

Don't separate the rows in the parsed CSV object array into three arrays, but leave the collection as-is and use the data to fill the table using the properties of that object array directly.
I took the liberty of renaming your variable $vFile into $data as to me at least this is more descriptive of what is in there.
Try
$data = Import-Csv -Path "H:\My_file.txt"
$word = New-Object -ComObject "Word.Application"
$word.Visible = $True
$template = $word.Documents.Open("H:\Test.docx")
$Number_rows = $data.Count +1 # +1 for the header
$Number_cols = 3
$range = $template.Range()
[void]$template.Tables.Add($range, $Number_rows, $Number_cols)
$table = $template.Tables.Item(1)
$table.Style = "Medium Shading 1 - Accent 1"
# write the headers
$table.cell(1,1).Range.Text = "Component"
$table.cell(1,2).Range.Text = "ID"
$table.cell(1,3).Range.text = "Type"
# next, add the data rows
for ($i=0; $i -lt $data.Count; $i++){
$table.cell(($i+2),1).Range.Text = $data[$i].component
$table.cell(($i+2),2).Range.Text = $data[$i].id
$table.cell(($i+2),3).Range.Text = $data[$i].iType
}
$template.SaveAs("H:\New_Doc.docx")
When done, do not forget to close the document, quit word and clean up the used COM objects:
$template.Close()
$word.Quit()
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($template)
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($word)
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()

Related

Adding values from two arrays into a PSCustomObject

I am trying to combine values from two arrays into a table using a PSCustomObject.
One array has the passwords and one array has the users.
When I try to combine them in to a PSCustomObject array it only list the last value in the array, not a list.
I have tried a few different versions:
for ($i = 0; $i -lt $users.Length; $i++) {$myObject = [PSCustomObject] #{name = $users.name[$i]; User = $users.samaccountname[$i]; Mail = $users.mail[$i]; Password = $passwords[$i]}}
and
foreach ($psw in $passwords) {$users | % {$myObject = [PSCustomObject] #{name = $PSItem.name; User = $PSItem.samaccountname; Mail = $PSItem.mail; Password = $psw}}}
When I try to use += on $myobject it gives the error:
Method invocation failed because [System.Management.Automation.PSObject] does not contain a method named 'op_Addition'.
Any ideas what I am doing wrong?
Instead of using +=, simply assign all the output from the loop to a variable (also note that you'll probably want to index into $users rather than the synthetic collection(s) you get from $users.name etc. - otherwise it'll break if any property contains multiple values):
$myObjects =
for ($i = 0; $i -lt $users.Length; $i++) {
[PSCustomObject] #{
Name = $users[$i].name
User = $users[$i].samaccountname
Mail = $users[$i].mail
Password = $passwords[$i]
}
}
The error you get when using += on $myobject is caused because $myobject is of a custom type (without the 'op_Addition' method implemented).
You can use an object with this method implemented, for example ArrayList, like that:
$myObject = New-Object -TypeName "System.Collections.ArrayList"
for ($i = 0; $i -lt $users.Length; $i++) {
$myObject += [PSCustomObject] #{
Name = $users[$i].name
User = $users[$i].samaccountname
Mail = $users[$i].mail
Password = $passwords[$i]
}
}

To change Column value of an array to Column header

I am extracting some data from an API which I have finally stored in the form of two strings which looks something like this:
$String1:
Client 1
Client 2
Client 3
$String2:
Product a
Product b
Product c
The above is a long list in reality.
I also have fetched a different set of data from a different API which is stored in an array.
$Array:
Server State
abcd.com Normal
defg.com Dead
fguy.com Normal
Note, the way I have fetched $String1 and $String2 are values of foreach of Server from a different API output extracted as findstr Client and findstr Product.
Now, what I want to achieve from these 3 $s is a final table which will look something like this:
$Final:
Server State Client Product
abcd.com Normal 1 a
defg.com Dead 2 b
fguy.com Normal 3 c
So what I first tried to do is create and intermediate table which might look as
Client Product
1 a
2 b
3 c
and then merge with the $Array into the Final table.
And currently I am going nowhere with this, I have tried a lot of different ways which look stupid and getting me nowhere.
Since it looks like $Array3 is an array of objects with two properties: Server and State, I think this could help you out:
$Array1 = 'Client 1','Client 2','Client 3'
$Array2 = 'Product a','Product b','Product c'
$Array3 = #(
[PsCustomObject]#{'Server' = 'abcd.com'; 'State' = 'Normal'},
[PsCustomObject]#{'Server' = 'defg.com'; 'State' = 'Dead'},
[PsCustomObject]#{'Server' = 'fguy.com'; 'State' = 'Normal'}
)
for ($i = 0; $i -lt $Array3.Count; $i++) {
$Array3[$i] | Select-Object *,
#{Name = 'Client'; Expression = { $Array1[$i] -replace '^Client\s*'}},
#{Name = 'Product'; Expression = { $Array2[$i] -replace '^Product\s*'}}
}
Output:
Server State Client Product
------ ----- ------ -------
abcd.com Normal 1 a
defg.com Dead 2 b
fguy.com Normal 3 c
If you want to, you can capture the result of the for(..) loop and save that as CSV file somewhere. In that case just do
$result = for ($i = 0; $i -lt $Array3.Count; $i++) {
$Array3[$i] | Select-Object *,
#{Name = 'Client'; Expression = { $Array1[$i] -replace '^Client\s*'}},
#{Name = 'Product'; Expression = { $Array2[$i] -replace '^Product\s*'}}
}
$result | Export-Csv -Path 'D:\serverresult.csv' -NoTypeInformation
Update
Apparently the arrays 1 and 2 you mention are not arrays at all, but (multiline) strings.
In that case, split the lines inside these strings so you will end up with true arrays:
$string1 = #"
Client 1
Client 2
Client 3
"#
$string2 = #"
Product a
Product b
Product c
"#
# split the multiline strings (examples) on the Newline character into arrays
$Array1 = $string1 -split '\r?\n'
$Array2 = $string2 -split '\r?\n'
# now, both arrays will have 3 elements:
# $Array1 = 'Client 1','Client 2','Client 3'
# $Array2 = 'Product a','Product b','Product c'
# array no. 3 is an array of objects as we saw earlier
$Array3 = #(
[PsCustomObject]#{'Server' = 'abcd.com'; 'State' = 'Normal'},
[PsCustomObject]#{'Server' = 'defg.com'; 'State' = 'Dead'},
[PsCustomObject]#{'Server' = 'fguy.com'; 'State' = 'Normal'}
)
# Finally, you can use the `for(..)` loop unaltered
$result = for ($i = 0; $i -lt $Array3.Count; $i++) {
$Array3[$i] |
Select-Object *,
#{Name = 'Client'; Expression = { $Array1[$i] -replace '^Client\s*'}},
#{Name = 'Product'; Expression = { $Array2[$i] -replace '^Product\s*'}}
}
# output on console
$result
# output to CSV file
$result | Export-Csv -Path 'D:\serverresult.csv' -NoTypeInformation
This should work:
# Example-Arrays
$Array1 = #( 'Client 1', 'Client 2', 'Client 3' )
$Array2 = #( 'Product a', 'Product b', 'Product c' )
$Array3 = #( [PsCustomObject]#{'Server' = 'abcd.com'; 'State' = 'Normal'},
[PsCustomObject]#{'Server' = 'defg.com'; 'State' = 'Dead'},
[PsCustomObject]#{'Server' = 'fguy.com'; 'State' = 'Normal'} )
# Create datatable
$dt = New-Object system.Data.DataTable
[void]$dt.Columns.Add('Server',[string]::empty.GetType() )
[void]$dt.Columns.Add('State',[string]::empty.GetType() )
[void]$dt.Columns.Add('Client',[string]::empty.GetType() )
[void]$dt.Columns.Add('Product',[string]::empty.GetType() )
for( $counter = 0; $counter -lt $Array1.Count; $counter++ ) {
# Add new rows:
$newRow = $dt.NewRow()
$newRow.Server = $Array3.Server[$counter]
$newRow.State = $Array3.State[$counter]
$newRow.Client = $Array1[$counter] -replace '^.+(\d+)$', '$1'
$newRow.Product = $Array2[$counter] -replace '^.+[ \n\t\r]+(.*)$', '$1'
[void]$dt.Rows.Add( $newRow )
}
# Output-Datatable
$dt
# To File
$dt | Out-File 'test.txt'
Two cmdlets I wrote might be help for this:
ConvertFrom-SourceTable
This cmdlet is able to restore objects from a fixed width table.
For tables that do not contain a header line (as in your case for $String1 and $String2), you can separately define it with the -Header parameter where the header has basically to functions: it defines the property names and the column alignment (along with a possible ruler and the data contained by the table).
$Client = ConvertFrom-SourceTable $String1 -Header 'Name Client'
$Product = ConvertFrom-SourceTable $String2 -Header 'Name Product'
It is not clear whether the $Array is also a string or an object list. Presuming it is a string, you simply might restore the object list as follows:
$Server = ConvertFrom-SourceTable $Array
Join-Object
The other cmdlet is initially written to join objects on an common property relation. Nevertheless, if you omit the -On parameter (which defines the relation), it will simply join the objects based on the line index. The -Property parameter will just select the properties you need (and skip the Name property, defined in the header of the $String1 and $String2):
$Server | Join $Client | Join $Product -Property Server, State, Client, Product
(See also: In Powershell, what's the best way to join two tables into one?)

Processing large arrays in PowerShell

I am having a difficult time understanding the most efficient to process large datasets/arrays in PowerShell. I have arrays that have several million items that I need to process and group. This list is always different in size meaning it could be 3.5 million items or 10 million items.
Example: 3.5 million items they group by "4's" like the following:
Items 0,1,2,3 Group together 4,5,6,7 Group Together and so on.
I have tried processing the array using a single thread by looping through the list and assigning to a pscustomobject which works it just takes 45-50+ minutes to complete.
I have also attempted to break up the array into smaller arrays but that causes the process to run even longer.
$i=0
$d_array = #()
$item_array # Large dataset
While ($i -lt $item_array.length){
$o = "Test"
$oo = "Test"
$n = $item_array[$i];$i++
$id = $item_array[$i];$i++
$ir = $item_array[$i];$i++
$cs = $item_array[$i];$i++
$items = [PSCustomObject]#{
'field1' = $o
'field2' = $oo
'field3' = $n
'field4' = $id
'field5' = $ir
'field6'= $cs
}
$d_array += $items
}
I would imagine if I applied a job scheduler that would allow me to run the multiple jobs would cut the process time down by a significant amount, but I wanted to get others takes on a quick effective way to tackle this.
If you are working with large data, using C# is also effective.
Add-Type -TypeDefinition #"
using System.Collections.Generic;
public static class Test
{
public static List<object> Convert(object[] src)
{
var result = new List<object>();
for(var i = 0; i <= src.Length - 4; i+=4)
{
result.Add( new {
field1 = "Test",
field2 = "Test",
field3 = src[i + 0],
field4 = src[i + 1],
field5 = src[i + 2],
field6 = src[i + 3]
});
}
return result;
}
}
"#
$item_array = 1..10000000
$result = [Test]::Convert($item_array)
While rokumarus version is unsurpassed, here my try with my local measurements from js2010
Same $item_array = 1..100000 applied to all versions
> .\SO_56406847.ps1
measuring...BDups
measuring...LotPings
measuring...Theo
measuring...js2010
measuring...rokumaru
BDups = 75,9949897 TotalSeconds
LotPings = 2,3663763 TotalSeconds
Theo = 2,4469917 TotalSeconds
js2010 = 2,9198114 TotalSeconds
rokumaru = 0,0109287 TotalSeconds
## Q:\Test\2019\06\01\SO_56406847.ps1
$i=0
$item_array = 1..100000 # Large dataset
'measuring...LotPings'
$LotPings = measure-command {
$d_array = for($i=0;$i -lt $item_array.length;$i+=4){
[PSCustomObject]#{
'field1' = "Test"
'field2' = "Test"
'field3' = $item_array[$i]
'field4' = $item_array[$i+1]
'field5' = $item_array[$i+2]
'field6' = $item_array[$i+3]
}
}
} # measure-command
How's this? 32.5x faster. Making arrays with += kills puppies. It copies the whole array every time.
$i=0
$item_array = 1..100000 # Large dataset
'measuring...'
# original 1 min 5 sec
# mine 2 sec
# other answer, 2 or 3 sec
# c# version 0.029 sec, 2241x faster!
measure-command {
$d_array =
While ($i -lt $item_array.length){
$o = "Test"
$oo = "Test"
$n = $item_array[$i];$i++
$id = $item_array[$i];$i++
$ir = $item_array[$i];$i++
$cs = $item_array[$i];$i++
# $items =
[PSCustomObject]#{
'field1' = $o
'field2' = $oo
'field3' = $n
'field4' = $id
'field5' = $ir
'field6'= $cs
}
# $d_array += $items
}
}
You could optimize this somewhat using an ArrayList, or perhaps even better by using a strongly typed List but going through millions of elements in an array will still take time..
As for your code: there is no need to capture the array item values in a variable first and use that later to add to the PSCustomObject.
$item_array = 'a','b','c','d','e','f','g','h' # Large dataset
$result = New-Object System.Collections.Generic.List[PSCustomObject]
# or use an ArrayList: $result = New-Object System.Collections.ArrayList
$i = 0
While ($i -lt $item_array.Count) {
[void]$result.Add(
[PSCustomObject]#{
'field1' = "Test" # $o
'field2' = "Test" # $oo
'field3' = $item_array[$i++] #$n
'field4' = $item_array[$i++] #$id
'field5' = $item_array[$i++] #$ir
'field6' = $item_array[$i++] #$cs
}
)
}
# save to a CSV file maybe ?
$result | Export-Csv 'D:\blah.csv' -NoTypeInformation
If you need the result to become a 'normal' array again, use $result.ToArray()

Working two Array PowerShell

I have two arrays: array1 [POP1, POP2, POP3 .... POP30] and array2 [61,61,62 ... 61]. I need to create a new object with value 62 and its POP.
In this example:
POP3 62.
I am simplifying the explanation because I've already been able to get the value from the database.
Can someone help me?
Code:
$target = #( )
$ini = 0 | foreach {
$apiurl = "http://xxxxxxxxx:8080/fxxxxp/events_xxxx.xml"
[xml]$ini = (New-Object System.Net.WebClient).downloadstring($apiurl)
$target = $ini.events.event.name
$nodename = $target
$target = $ini.events.event.statuscode
$statuscode = $target
}
$column1 = #($nodename)
$column2 = #($statuscode)
$i = 0
($column1,$column2)[0] | foreach {
New-Object PSObject -Property #{
POP = $Column1[$i]
Status = $column2[$i++]
} | ft -AutoSize
I really couldn't figure out what you were trying to do, but you definitely over complicated it. Here is what I thought of your code:
# Here you have an empty array
$target = #( )
# Here you set call a Foreach, but you don't even need it
$ini = 0 | foreach {
$apiurl = "http://xxxxxxxxx:8080/fxxxxp/events_xxxx.xml"
[xml]$ini = (new-object System.Net.WebClient).downloadstring($apiurl)
# You duplicated variables here. Just set $nodename = $ini.events.event.name
$target = $ini.events.event.name
$nodename = $target
# You duplicate variables here. Just set $statuscode = $ini.events.event.name
$target = $ini.events.event.statuscode
$statuscode = $target
}
# You should already have arrays, so now you're making making more arrays duplicating variables again
$column1 = #($nodename)
$column2 = #($statuscode)
# counter, but you won't need it
$i = 0
# So here, youre making a new array again, but this contains two nested arrays. I don't get it.
($column1,$column2)[0] | foreach {
New-Object PSObject -Property #{
POP = $Column1[$i]
Status = $column2[$i++]
} | ft -AutoSize
} # You were missing a closing bracket for your foreach loop
Here is a solution that should probable work for you:
# Download the file
$apiurl = "http://xxxxxxxxx:8080/fxxxxp/events_xxxx.xml"
[xml]$ini = (New-Object System.Net.WebClient).DownloadString($apiurl)
# Set arrays
$nodename = $ini.events.event.name
$statuscode = $ini.events.event.statuscode
# Create $TableValues by looping through one array
$TableValues = foreach ( $node in $nodename )
{
[pscustomobject] #{
# The current node
POP = $node
# use the array method IndexOf
# This should return the position of the current node
# Then use that index to get the matching value of $statuscode
Status = $statuscode[$nodename.IndexOf($node)]
}
}
# Add a custom value
$TableValues += [pscustomobject] #{
POP = 'POP100'
Status = 100
}
$TableValues | Format-Table -AutoSize
Assuming that your intent is to create an array of custom objects constructed from the pairs of corresponding elements of 2 arrays of the same size:
A concise pipeline-based solution (PSv3+; a for / foreach solution would be faster):
$arr1 = 'one', 'two', 'three'
$arr2 = 1, 2, 3
0..$($arr1.Count-1) | % { [pscustomobject] #{ POP = $arr1[$_]; Status = $arr2[$_] } }
This yields:
POP Status
--- ------
one 1
two 2
three 3

Email Formatted Array

I got a script that creates two arrays (each has 1 column and variable number of lines). I want to format these two arrays and e-mail it to an Outlook account. Code and sample data below.
$Values2 = #(Get-Content *\IdealOutput.csv)
$OutputLookUp2 = #()
foreach ($Value in $Values2) {
$OutputLookUp2 += $Excel.WorksheetFunction.VLookup($Value,$range4,3,$false)
}
$Excel.Workbooks.Close()
$Excel.Quit()
$EmailFrom = "sample#sample.com"
$EmailTo = "sample#sample.com"
$EmailBody = "$Values2 $OutputLookup2"
$EmailSubject = "Test"
$Username = "sample"
$Password = "sample"
$Message = New-Object Net.Mail.MailMessage `
($EmailFrom, $EmailTo, $EmailSubject, $EmailBody)
$SMTPClient = New-Object Net.Mail.SmtpClient `
("smtp.outlook.com", portnumber) #Port can be changed
$SMTPClient.EnableSsl = $true
$SMTPClient.Credentials = New-Object System.Net.NetworkCredential `
($Username, $Password);
$SMTPClient.Send($Message)
Both $OutputLookUp2 and $Values2 are one column with variable number of lines.
Example:
$Outputlookup2 =
X1
X2
$Values2 =
Y1
Y2
I would like the output to the body of the e-mail to be:
X1 Y1
X2 Y2
And I would like to avoid HTML as it will be sent via text as well.
Assuming my interpretation is correct this seems simple enough. For every $Values2, which is just a line from a text file, find its similar value in the open spreadsheet. You are have the loop that you need. Problem is you are building the item lists independent of each other.
$Values2 = #(Get-Content *\IdealOutput.csv)
$OutputLookUp2 = #()
foreach ($Value in $Values2){
$OutputLookUp2 += "$Value $($Excel.WorksheetFunction.VLookup($Value,$range4,3,$false))"
}
Now $OutputLookUp2 should contain your expected output in array form.
If the array does not work you could also just declare it as a string and the add newlines as you are building it. You will notice the "`r`n" at the end of the string.
$Values2 = #(Get-Content *\IdealOutput.csv)
$OutputLookUp2 = ""
foreach ($Value in $Values2){
$OutputLookUp2 += "$Value $($Excel.WorksheetFunction.VLookup($Value,$range4,3,$false))`r`n"
}
In both example you can just flip the order of the $value and the lookup easy. If you need a header you can add that when you declare $OutputLookUp2.
There is always room for improvement
If you want to take this a little further in the direction that Ansgar Wiechers was eluding to...
$OutputLookUp2 = Get-Content *\IdealOutput.csv | ForEach-Object{
"$_ $($Excel.WorksheetFunction.VLookup($_,$range4,3,$false))"
} | Out-String

Resources