what bad consequence could using an array have 'in this situation'? - arrays

My question is two fold I'm playing around with PS and trying new stuff, I wrote a small script that generates full names by exporting first names from a txt.file and last names from another txt.file and finally randomly combines the 2 to create a full name, this is the script:
$Firstnames = Get-Content -Path 'C:\Users\user\Desktop\Firstname.txt'
$Lastnames = Get-Content -Path 'C:\Users\user\Desktop\Lastnames.txt'
$feminine = $firstnames | ?{$_ -like "*;frau"} | %{$_.Split(';')[0]}
Write-Host "Random full name generator"
$numberofNames = 1..(Read-Host 'how many combination do you want to generate?')
$numberofNames | foreach {
$f = $feminine[ (Get-Random $feminine.count)]
$l = $Lastnames[ (Get-Random $Lastnames.count)]
$full = $f+" "+$l
Write-output $full
}
1- now $numberofNames is an array 'a range operator', I would like to know what bad consequences could use this method have? is this the best approach for the users input?
2- what is the key difference between using for example $a = 100 and $a = 1..100?
in case you need to know:
$firstnames looks something like this:
Linda;frau
Aline;frau
Lisa;frau
Katja;frau
Karen;frau
and $lastnames:
smith
anderson
müller
klein
Rex
thank you

1- now $numberofNames is an array 'a range operator', i would like to know what bad consequences could using this method have? is this the best approach for the users input?
It's valid. Even if the user adds e.g. a string as input Powershell will throw an error that it could not cast the input to an int:
1 .. "test" | % {Write-Host $_ }
Cannot convert value "test" to type "System.Int32". Error: "Input string was not in a correct format."
At line:1 char:1
+ 1 .. "test" | % {Write-Host $_ }
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvalidCastFromStringToInteger
Even a neg. int shouldn't be a problem since you're not using the array content for any indexing operations.
2- what is the key difference between using for example $a = 100 and $a = 1..100?
$a = 100 points to integer with the value 100.
$a = 1..100 is an array with 100 entries.

Related

How to declare a multidimensional 2D array in Powershell? [duplicate]

I have a way of doing Arrays in other languagues like this:
$x = "David"
$arr = #()
$arr[$x]["TSHIRTS"]["SIZE"] = "M"
This generates an error.
You are trying to create an associative array (hash). Try out the following
sequence of commands
$arr=#{}
$arr["david"] = #{}
$arr["david"]["TSHIRTS"] = #{}
$arr["david"]["TSHIRTS"]["SIZE"] ="M"
$arr.david.tshirts.size
Note the difference between hashes and arrays
$a = #{} # hash
$a = #() # array
Arrays can only have non-negative integers as indexes
from powershell.com:
PowerShell supports two types of multi-dimensional arrays: jagged arrays and true multidimensional arrays.
Jagged arrays are normal PowerShell arrays that store arrays as elements. This is very cost-effective storage because dimensions can be of different size:
$array1 = 1,2,(1,2,3),3
$array1[0]
$array1[1]
$array1[2]
$array1[2][0]
$array1[2][1]
True multi-dimensional arrays always resemble a square matrix. To create such an array, you will need to access .NET. The next line creates a two-dimensional array with 10 and 20 elements resembling a 10x20 matrix:
$array2 = New-Object 'object[,]' 10,20
$array2[4,8] = 'Hello'
$array2[9,16] = 'Test'
$array2
for a 3-dimensioanl array 10*20*10
$array3 = New-Object 'object[,,]' 10,20,10
To extend on what manojlds said above is that you can nest Hashtables. It may not be a true multi-dimensional array but give you some ideas about how to structure the data. An example:
$hash = #{}
$computers | %{
$hash.Add(($_.Name),(#{
"Status" = ($_.Status)
"Date" = ($_.Date)
}))
}
What's cool about this is that you can reference things like:
($hash."Name1").Status
Also, it is far faster than arrays for finding stuff. I use this to compare data rather than use matching in Arrays.
$hash.ContainsKey("Name1")
Hope some of that helps!
-Adam
Knowing that PowerShell pipes objects between cmdlets, it is more common in PowerShell to use an array of PSCustomObjects:
$arr = #(
[PSCustomObject]#{Name = 'David'; Article = 'TShirt'; Size = 'M'}
[PSCustomObject]#{Name = 'Eduard'; Article = 'Trouwsers'; Size = 'S'}
)
Or for older PowerShell Versions (PSv2):
$arr = #(
New-Object PSObject -Property #{Name = 'David'; Article = 'TShirt'; Size = 'M'}
New-Object PSObject -Property #{Name = 'Eduard'; Article = 'Trouwsers'; Size = 'S'}
)
And grep your selection like:
$arr | Where {$_.Name -eq 'David' -and $_.Article -eq 'TShirt'} | Select Size
Or in newer PowerShell (Core) versions:
$arr | Where Name -eq 'David' | Where Article -eq 'TShirt' | Select Size
Or (just get the size):
$arr.Where{$_.Name -eq 'David' -and $_.Article -eq 'TShirt'}.Size
Addendum 2020-07-13
Syntax and readability
As mentioned in the comments, using an array of custom objects is straighter and saves typing, if you like to exhaust this further you might even use the ConvertForm-Csv (or the Import-Csv) cmdlet for building the array:
$arr = ConvertFrom-Csv #'
Name,Article,Size
David,TShirt,M
Eduard,Trouwsers,S
'#
Or more readable:
$arr = ConvertFrom-Csv #'
Name, Article, Size
David, TShirt, M
Eduard, Trouwsers, S
'#
Note: values that contain spaces or special characters need to be double quoted
Or use an external cmdlet like ConvertFrom-SourceTable which reads fixed width table formats:
$arr = ConvertFrom-SourceTable '
Name Article Size
David TShirt M
Eduard Trouwsers S
'
Indexing
The disadvantage of using an array of custom objects is that it is slower than a hash table which uses a binary search algorithm.
Note that the advantage of using an array of custom objects is that can easily search for anything else e.g. everybody that wears a TShirt with size M:
$arr | Where Article -eq 'TShirt' | Where Size -eq 'M' | Select Name
To build an binary search index from the array of objects:
$h = #{}
$arr | ForEach-Object {
If (!$h.ContainsKey($_.Name)) { $h[$_.Name] = #{} }
If (!$h[$_.Name].ContainsKey($_.Article)) { $h[$_.Name][$_.Article] = #{} }
$h[$_.Name][$_.Article] = $_ # Or: $h[$_.Name][$_.Article]['Size'] = $_.Size
}
$h.david.tshirt.size
M
Note: referencing a hash table key that doesn't exist in Set-StrictMode will cause an error:
Set-StrictMode -Version 2
$h.John.tshirt.size
PropertyNotFoundException: The property 'John' cannot be found on this object. Verify that the property exists.
Here is a simple multidimensional array of strings.
$psarray = #(
('Line' ,'One' ),
('Line' ,'Two')
)
foreach($item in $psarray)
{
$item[0]
$item[1]
}
Output:
Line
One
Line
Two
Two-dimensional arrays can be defined this way too as jagged array:
$array = New-Object system.Array[][] 5,5
This has the nice feature that
$array[0]
outputs a one-dimensional array, containing $array[0][0] to $array[0][4].
Depending on your situation you might prefer it over $array = New-Object 'object[,]' 5,5.
(I would have commented to CB above, but stackoverflow does not let me yet)
you could also uses System.Collections.ArrayList to make a and array of arrays or whatever you want.
Here is an example:
$resultsArray= New-Object System.Collections.ArrayList
[void] $resultsArray.Add(#(#('$hello'),2,0,0,0,0,0,0,1,1))
[void] $resultsArray.Add(#(#('$test', '$testagain'),3,0,0,1,0,0,0,1,2))
[void] $resultsArray.Add("ERROR")
[void] $resultsArray.Add(#(#('$var', '$result'),5,1,1,0,1,1,0,2,3))
[void] $resultsArray.Add(#(#('$num', '$number'),3,0,0,0,0,0,1,1,2))
One problem, if you would call it a problem, you cannot set a limit. Also, you need to use [void] or the script will get mad.
Using the .net syntax (like CB pointed above)
you also add coherence to your 'tabular' array...
if you define a array...
and you try to store diferent types
Powershell will 'alert' you:
$a = New-Object 'byte[,]' 4,4
$a[0,0] = 111; // OK
$a[0,1] = 1111; // Error
Of course Powershell will 'help' you
in the obvious conversions:
$a = New-Object 'string[,]' 2,2
$a[0,0] = "1111"; // OK
$a[0,1] = 111; // OK also
Another thread pointed here about how to add to a multidimensional array in Powershell. I don't know if there is some reason not to use this method, but it worked for my purposes.
$array = #()
$array += ,#( "1", "test1","a" )
$array += ,#( "2", "test2", "b" )
$array += ,#( "3", "test3", "c" )
Im found pretty cool solvation for making arrays in array.
$GroupArray = #()
foreach ( $Array in $ArrayList ){
$GroupArray += #($Array , $null)
}
$GroupArray = $GroupArray | Where-Object {$_ -ne $null}
Lent from above:
$arr = ConvertFrom-Csv #'
Name,Article,Size
David,TShirt,M
Eduard,Trouwsers,S
'#
Print the $arr:
$arr
Name Article Size
---- ------- ----
David TShirt M
Eduard Trouwsers S
Now select 'David'
$arr.Where({$_.Name -eq "david"})
Name Article Size
---- ------- ----
David TShirt M
Now if you want to know the Size of 'David'
$arr.Where({$_.Name -eq "david"}).size
M

Efficient way to remove duplicates from large 2D arrays in PowerShell

I have a large set of data roughly 10 million items that I need to process efficiently and quickly removing duplicate items based on two of the six column headers.
I have tried grouping and sorting items but it's horrendously slow.
$p1 = $test | Group-Object -Property ComputerSeriaID,ComputerID
$p2 = foreach ($object in $p1.group) {
$object | Sort-Object -Property FirstObserved | Select-Object -First 1
}
The goal would be to remove duplicates by assessing two columns while maintaining the oldest record based on first observed.
The data looks something like this:
LastObserved : 2019-06-05T15:40:37
FirstObserved : 2019-06-03T20:29:01
ComputerName : 1
ComputerID : 2
Virtual : 3
ComputerSerialID : 4
LastObserved : 2019-06-05T15:40:37
FirstObserved : 2019-06-03T20:29:01
ComputerName : 5
ComputerID : 6
Virtual : 7
ComputerSerialID : 8
LastObserved : 2019-06-05T15:40:37
FirstObserved : 2019-06-03T20:29:01
ComputerName : 9
ComputerID : 10
Virtual : 11
ComputerSerialID : 12
You might want to clean up your question a little bit, because it's a little bit hard to read, but I'll try to answer the best I can with what I can understand about what you're trying to do.
Unfortunately, with so much data there's no way to do this quickly. String Comparison and sorting are done by brute force; there is no way to reduce the complexity of comparing each character in one string against another any further than measuring them one at a time to see if they're the same.
(Honestly, if this were me, I'd just use export-csv $object and perform this operation in excel. The time tradeoff to scripting something like this only once just wouldn't be worth it.)
By "Items" I'm going to assume that you mean rows in your table, and that you're not trying to retrieve only the strings in the rows you're looking for. You've already got the basic idea of select-object down, you can do that for the whole table:
$outputFirstObserved = $inputData | Sort-Object -Property FirstObserved -Unique
$outputLastObserved = $inputData | Sort-Object -Property LastObserved -Unique
Now you have ~20 million rows in memory, but I guess that beats doing it by hand. All that's left is to join the two tables. You can download that Join-Object command from the powershell gallery with Install-Script -Name Join and use it in the way described. If you want to do this step yourself, the easiest way would be to squish the two tables together and sort them again:
$output = $outputFirstObserved + $outputLastObserved
$return = $output | Sort-Object | Get-Unique
Does this do it? It keeps the one it finds first.
$test | sort -u ComputerSeriaID, ComputerID
I created this function to de-duplicate my multi-dimensional arrays.
Basically, I concatenate the contents of the record, add this to a hash.
If the concatenate text already exists in the hash, don't add it to the array to be returned.
Function DeDupe_Array
{
param
(
$Data
)
$Return_Array = #()
$Check_Hash = #{}
Foreach($Line in $Data)
{
$Concatenated = ''
$Elements = ($Line | Get-Member -MemberType NoteProperty | % {"$($_.Name)"})
foreach($Element in $Elements)
{
$Concatenated += $line.$Element
}
If($Check_Hash.$Concatenated -ne 1)
{
$Check_Hash.add($Concatenated,1)
$Return_Array += $Line
}
}
return $Return_Array
}
Try the following script.
Should be as fast as possible due to avoiding any pipe'ing in PS.
$hashT = #{}
foreach ($item in $csvData) {
# Building hash table key
$key = '{0}###{1}' -f $item.ComputerSeriaID, $item.ComputerID
# if $key doesn't exist yet OR when $key exists and "FirstObserverd" is less than existing one in $hashT (only valid when date provided in sortable format / international format)
if ((! $hashT.ContainsKey($key)) -or ( $item.FirstObserved -lt $hashT[$key].FirstObserved )) {
$hashT[$key] = $item
}
}
$result = $hashT.Values

Powershell for loop throwing "the array index evaluated to null"

For starters, I'm on Fedora 30 using PSCore version 6.2.1. I've encountered this issue in GNOME Terminal and the vscode snap.
I'm on the first challenge of the PSKoans module and I'm stuck when trying to use a for loop. I am given an array of strings, each of which is a collection of strings separated by commas.
$StockData = #(
"Date,Open,High,Low,Close,Volume,Adj Close"
"2012-03-30,32.40,32.41,32.04,32.26,31749400,32.26"
"2012-03-29,32.06,32.19,31.81,32.12,37038500,32.12"
) # The array is much longer than that, but shortened for simplicity's sake
So, my idea is to build a hashtable out of each subsequent string line in the array by using the first string in the array as keys and each following line as a set of values. I'm using -split to split the values apart from within the strings. I want to use a for loop to iterate through the array and pull values, building a hastable in a file to be read later like so:
# Build the array of keys
[array]$keys = $StockData[0] -split ','
# Begin for loop, using $i as int
for ($i = 1, $StockData[$i], $i++) {
# Create a text file for each hastable
New-Item -Name "ht$i.txt" -ItemType File
# Split current string into values
$values = $StockData[$i] -split ','
# Set value int
$valuesInt = 0
foreach ($key in $keys) {
Add-Content -Path "./ht$i.txt" -Value "$key = $values[$valuesInt]"
$valuesInt++
}
}
As I run that, I get the following error:
Index operation failed; the array index evaluated to null.
At /home/user/PSKoans/Foundations/SolutionStockChallenge.ps1:28 char:6
+ for ($i = 1, $stockData[$i], $i++) {
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : NullArrayIndex
I've looked it up and I find all kinds of scenarios in which people get this error message. I didn't really find a solid explanation for the error message the might lead me to an answer.
Reading the error message, it doesn't make sense to me. the array index evaluated to null...but the array index in the first case is $StockData[1] which is a valid index and should return $true and continue with the loop. Am I missing something?
The syntax of your for loop is wrong. The for loop uses semi-colons as separators.
for ($i = 1, $StockData[$i], $i++) {
should be
for ($i = 1; $StockData[$i]; $i++) {
ConvertFrom-Json in PowerShell Core has the coolest switch - AsHashTable. Try this:
$StockData | convertfrom-csv | convertto-json | ConvertFrom-Json -AsHashtable

Powershell - how to declare an array for use with += operator

I can intuitively figure out most languages, but apparently not Powershell.
I want to create an array of arrays (this will contain disk directories and a count so later I can verify we have at least that many files).
From that array of arrays, I want to pull out a single array of just the directory names so I can pass it to Get-ChildItem.
$DirInfo = #('d:\Work',2),
('d:\Temp',3)
$DirNameArray=#() #declare empty array
foreach ($item in $DirInfo)
{
$DirNameArray += , $item[0] #tried with and without the comma here
Write-Host 'Loop1 ' $item[0]
}
write-host $DirNameArray.count
#Let's Verify what we got so we know how many items we have in our array
Write-Host "Verify with a loop"
foreach ($dir in $DirNameArray)
{
Write-Host 'Loop2:' $dir
}
Write-Host "Verify the other way"
Write-Host $DirNameArray
Actual Results:
Loop1 d:\Work
Loop1 d:\Temp
1
Verify with a loop
Loop2: d:\Workd:\Temp
Verify the other way
d:\Workd:\Temp
What I don't understand is why Loop2 didn't execute twice.
It looks like the =+ is just stringing together the values instead of adding a new item to my array called $DirNameArray.
I'm still utterly baffled, one file I created does this and gives me the expected results:
$a = "one","two"
Write-Host $a.count
$a += "three"
Write-Host $a.count
Results:
2
3
So if the above worked, why didn't my code work?
A second file I created does this - and I don't understand the results. I even made the variable name different so I wouldn't be dealing with any prior definition or values of that variable:
$DirNameArray5="abc","def"
write-host $DirNameArray5.count
$DirNameArray5 += "xyz"
write-host $DirNameArray5.count
$DirNameArray5 += #("opq")
write-host $DirNameArray5.count
Results:
1
1
1
$DirNameArray7="abc","def"
write-host $DirNameArray7.count
$DirNameArray7 += "xyz"
write-host $DirNameArray7.count
$DirNameArray7 += #("opq")
write-host $DirNameArray7.count
Results:
2
3
4
So apparently, if you once define a variable as a string, it's hard to get Powershell to redefine it as an array.
But I still have my original question. How to define an empty array so I can add to it in a loop using the += operator.
$DirNameArray=#()
I finally used the .GetType() method to see what my variables actually were:
PS C:\Users\nwalters> $DirNameArray5.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True String System.Object
PS C:\Users\nwalters> $DirNameArray7.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
Bottom line - this is what I want to do - in as few lines of code as possible with no loop:
[string]$x=#() # declare empty array
Write-Host $x.GetType()
$x += "one"
$x += "two"
Write-Host $x.count
Write-Host $x
Actual Results
System.String
1
onetwo
Desired Results:
object[] or string[]???
2
one two
Powershell will not let you create an empty array, or let you empty an array down to nothing (with one exception). There are two ways I have discovered to work around this issue:
Method 1: Use -OutVariable to a new variable name to create an array with your input
Example: gci C:\TestDir1 -OutVariable test
Using the .GetType() method returns:
$test.GetType()
Directory: C:\TestDir1
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 4/20/2020 11:36 AM 8015 Test1.xls
-a---- 6/26/2020 12:59 PM 0 test2.txt
Module : CommonLanguageRuntimeLibrary
Assembly : mscorlib, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
TypeHandle : System.RuntimeTypeHandle
DeclaringMethod :
BaseType : System.Object
UnderlyingSystemType : System.Collections.ArrayList
FullName : System.Collections.ArrayList
This will create a new variable named $test that is an array (since there are multiple items in it). $test.gettype() shows the object as an array.
Method 2: Explicitly declare an array with two dummy objects and then remove both dummy objects.
[System.Collections.ArrayList]$array = "value1", "value2"
$array.remove("value1")
$array.remove("value2")
Using the gettype method will still show $test is an array even though it is empty:
> $test.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True ArrayList System.Object
The array will now be completely empty and can be used to store any new input. This doesn't work unless you explicitly name the variable type like I did in my example (not sure why). Example shown below:
$test = "value1", "value2"
$test.Remove("value1")
Exception calling "Remove" with "1" argument(s): "Collection was of a fixed size."
At line:3 char:1
+ $test.Remove("value1")
+ ~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : NotSupportedException
P.S. I know this is an old thread, but it is unanswered and I just came upon this issue myself so I am answering it for anyone else that searches this issue.
The following code is tested on two different computers, both with PowerShell 2.0. Can you try this and post results.
# declare an empty array
$var = #()
write-host "var.count = '$($var.count)' var.type ='$($var.GetType())' var.type.BaseType = '$($var.GetType().BaseType)'"
# add a single item
$var += "single item"
write-host "var.count = '$($var.count)' var.type ='$($var.GetType())' var.type.BaseType = '$($var.GetType().BaseType)'"
# add an array
$var += , #("array 1 - item 1","array 1 - item 2")
write-host "var.count = '$($var.count)' var.type ='$($var.GetType())' var.type.BaseType = '$($var.GetType().BaseType)'"
# display the 'single item'
write-host "single item = '$($var[0])'"
# display first element of array item
write-host "first element of array item = '$($var[1][0])'"
gives me
var.count = '0' var.type ='System.Object[]' var.type.BaseType = 'array'
var.count = '1' var.type ='System.Object[]' var.type.BaseType = 'array'
var.count = '2' var.type ='System.Object[]' var.type.BaseType = 'array'
single item = 'single item'
first element of array item = 'array 1 - item 1'

How to store outlook email body in array - Powershell?

the script below reads my outlook emails but how do I access the output. I'm new too Powershell and I'm still getting used to certain things. I just want to get the body of 10 unread outlook emails and store them in an Array called $Body.
$olFolderInbox = 6
$outlook = new-object -com outlook.application;
$ns = $outlook.GetNameSpace("MAPI");
$inbox = $ns.GetDefaultFolder($olFolderInbox)
#checks 10 newest messages
$inbox.items | select -first 10 | foreach {
if($_.unread -eq $True) {
$mBody = $_.body
#Splits the line before any previous replies are loaded
$mBodySplit = $mBody -split "From:"
#Assigns only the first message in the chain
$mBodyLeft = $mbodySplit[0]
#build a string using the –f operator
$q = "From: " + $_.SenderName + ("`n") + " Message: " + $mBodyLeft
#create the COM object and invoke the Speak() method
(New-Object -ComObject SAPI.SPVoice).Speak($q) | Out-Null
}
}
This may not be a factor here, since you're looping through only ten elements, but using += to add elements to an array is very slow.
Another approach would be to output each element within the loop, and assign the results of the loop to $body. Here's a simplified example, assuming that you want $_.body:
$body = $inbox.items | select -first 10 | foreach {
if($_.unread -eq $True) {
$_.body
}
}
This works because anything that is output during the loop will be assigned to $body. And it can be much faster than using +=. You can verify this for yourself. Compare the two methods of creating an array with 10,000 elements:
Measure-Command {
$arr = #()
1..10000 | % {
$arr += $_
}
}
On my system, this takes just over 14 seconds.
Measure-Command {
$arr = 1..10000 | % {
$_
}
}
On my system, this takes 0.97 seconds, which makes it over 14 times faster. Again, probably not a factor if you are just looping through 10 items, but something to keep in mind if you ever need to create larger arrays.
define $body = #(); before your loop
Then just use += to add the elements
Here's another way:
$body = $inbox.Items.Restrict('[Unread]=true') | Select-Object -First 10 -ExpandProperty Body

Resources