How to add a String to duplicate element in Array in Powershell - arrays

I have following array:
$array = 'A', 'B', 'C', 'A'
I want to identify the duplicate and add to the duplicate a string, for example, $string='Part 2'
so that I have
$array = 'A', 'B', 'C', 'A Part 2'
How do I do this in PowerShell?

Use a hashtable to keep track of strings you've already seen, then loop through all items in the array - if we've already seen the same, modify the item, otherwise leave it as is:
$array = 'A','B','C','A'
# Create hashtable to keep track of strings we've already encountered
$stringsSeen = #{}
# Now iterate over the array
$modifiedArray = $array |ForEach-Object {
if(!$stringsSeen.ContainsKey($_)){
# First encounter, add to hashtable
$stringsSeen[$_] = 1
}
else {
# We've seen it before! Time to update value and modify `$_`
$number = ++$stringsSeen[$_]
$_ += " Part $number"
}
# finally output `$_`, regardless of whether we modified it or not
$_
}
$modifiedArray now holds the new array of (partially) modified strings:
PS ~> $modifiedArray
A
B
C
A Part 2

Mathias R. Jessen's helpful answer shows an effective, readable solution.
The following solution streamlines the approach to make it more concise (though thereby potentially more obscure) and more efficient:
The .ForEach() array method is used to process the input array, as a faster alternative to the ForEach-Object cmdlet. (A foreach statement would be even faster, but is a bit more verbose).
It relies on the (non-obvious) fact that ++ applied to a non-existent hashtable entry implicitly creates the entry with value 1.
$array = 'A', 'B', 'C', 'A'
$ht = #{}
$newArray = $array.ForEach(
{ if (($count = ++$ht[$_]) -eq 1) { $_ } else { "$_ Part $count" } }
)
Note: $newArray is technically not an array, but of type [System.Collections.ObjectModel.Collection[psobject]], but that usually won't make a difference.
In PowerShell (Core) 7+ an even more concise solution is possible, using ?:, the ternary conditional operator:
$array = 'A', 'B', 'C', 'A'
$ht = #{}
$newArray = $array.ForEach({ ($count = ++$ht[$_]) -eq 1 ? $_ : "$_ Part $count" })

Related

Powershell Loop that will assign a value to a variable once the number chosen by a client is reached

There's 2 things I'm trying to get accomplished.
I'm attempting to list all the values of an array to a user while having that list numbered.
I'm attempting to assign a value to a variable, depending on what
the user chooses.
# List that prints array items for a user to choose from
$Array = #('A', 'B', 'C', 'D', 'E', 'F')
foreach($i in $Array)
{
Write-Host "|" $i #this | character should be a list from 1-6, listing all the groups in the array
}
Write-Output "Please enter which group:"
$Array = Read-Host -Prompt " "
# Assign value to the variable
$x = Read-Host "Please enter a number"
$Array = #('A', 'B', 'C', 'D', 'E', 'F')
$i=0
while ($i -lt $x) {
$i++
if ($i -eq $x) { continue }
}
$x = $Array[$i]
Write-Host "You're assigned to group " $x
I've figured this dirty way of doing it, but I was trying to see if there was a simpler way of doing this (someone mentioned multidimmensional arrays). The 2 pieces of code should work together, explaining the same variable assignment.
Thanks!
EDIT 1: I want the user to not have to input "0" for the A group.
I think I understood you correctly...
In this situation, I would use a for loop as it's meant for these kind of scenarios:
$Array = #('A', 'B', 'C', 'D', 'E', 'F')
for ($i = 0; $i -lt $Array.Count; $i++)
{
$j = $i + 1
"{0}: {1}" -f $j, $Array[$i]
}
$selection = (Read-Host -Prompt "Enter Number(s)").Split(',').Trim()
foreach ($selected in $selection)
{
$Array[$selected - 1]
}
#Outputs:
<#
1: A
2: B
3: C
4: D
5: E
6: F
Enter Number(s): 1, 2, 3
A
B
C
#>
By adding 1 to $i (and assigning it to $j), we start at 1 for our numerical index assignment to the current iteration in your $Array. This way, we could still use $i to refer to the actual index value and have it display along side it. Using the -f format operator, this allows for "cleaner" code; where $j is 1, and $Array[$i] is 0 so A.
Finally, using a foreach loop, we can make a multiple choice selection if you pass more than one number separated by a comma. The only thing that's left to do is subtract 1 from the selected number(s): $Array[$selected - 1], and it will give you the correct value in your array.
To complement Abraham's helpful answer with a very similar approach but implementing input validation. This one does not accept multiple inputs.
$Array = 'A', 'B', 'C', 'D', 'E', 'F'
$index = 1
foreach($item in $Array) {
"{0}. $item" -f $index++
}
while($true) {
$choice = Read-Host "Please enter which Group"
if($choice -notmatch "^[1-9]\d*$" -or -not ($value = $Array[$choice-1])) {
"Incorrect choice, no element with index $choice"
continue
}
break
}
"Choice was $value"

Conditionally modify array element without resorting to a temporary array

Given an array like this
$array = #('A', 'B', 'C', 'D', 'E')
I can append something to the end of each item in the array like so
$array = $array | Foreach-Object {"$_ *"}
And if I want to append something DIFFERENT to each member I can do so like this
$tempArray = #()
$count = 1
foreach ($item in $array) {
$tempArray += "$item $count"
$count ++
}
$array = $tempArray
But that use of a temporary array seems... clumsy. I feel like I am missing something that would allow conditionally changing the values of the original array instead. Not adding new items, just changing the value of existing items.
you have two options here
1: set the result of the foreach to the variable you are iterating. this will iterate the array and set the result to the same variable.
$count = 1
$array = foreach ($item in $array) {
"$item $count"
$count ++
}
2: set an array element like so (note array elements begin by 0 not 1)
$array[2] = 'foo'
or in a loop like like so. this will set each variable at iteration.
$append = 'a'
for($i=0;$i -lt $array.count;$i++){
$array[$i] = $array[$i],$append -join ' '
$append += 'a'
}
You don't need to add #(), it's already an array as
$array = 'A', 'B', 'C', 'D', 'E'
If you simply want to append to the string and store back into the array, this is how I would do it. I'm taking advantage of the -Begin, -Process, and -OutVariable parameters of Foreach-Object.
$array | ForEach{$count = 1}{$_ + $count++} -OutVariable array
Output (also stored back in $array)
A1
B2
C3
D4
E5
Now if you did actually want to append a space before the number, then simply add that to the item like this
$array | ForEach{$count = 1}{"$_ " + $count++} -OutVariable array
Output
A 1
B 2
C 3
D 4
E 5
If you need to control the data flow more explicitly I would still lean towards not using for loops. This is equivalent.
$array = 'A', 'B', 'C', 'D', 'E'
$array = 1..$array.Count | ForEach{$array[$_-1] + " $_"}
or you could also write it as
$array = 'A', 'B', 'C', 'D', 'E'
$array = 1..$array.Count | ForEach{"$($array[$_-1]) $_"}
Something like this seems to be what you're after:
$array = #('A','B', 'C', 'D', 'E')
for ($c=1; $c -le $array.count; $c++) {
Write-host "$($array[$c-1]) $c"
}

Appending a string to each item of an array

I have an array and when I try to append a string to it the array converts to a single string.
I have the following data in an array:
$Str
451 CAR,-3 ,7 ,10 ,0 ,3 , 20 ,Over: 41
452 DEN «,40.5,0,7,0,14, 21 ,  Cover: 4
And I want to append the week of the game in this instance like this:
$Str = "Week"+$Week+$Str
I get a single string:
Week16101,NYG,42.5 ,3 ,10 ,3 ,3 , 19 ,Over 43 102,PHI,- 1,14,7,0,3, 24 ,  Cover 4 103,
Of course I'd like the append to occur on each row.
Instead of a for loop you could also use the Foreach-Object cmdlet (if you prefer using the pipeline):
$str = "apple","lemon","toast"
$str = $str | ForEach-Object {"Week$_"}
Output:
Weekapple
Weeklemon
Weektoast
Another option for PowerShell v4+
$str = $str.ForEach({ "Week" + $Week + $_ })
Something like this will work for prepending/appending text to each line in an array.
Set array $str:
$str = "apple","lemon","toast"
$str
apple
lemon
toast
Prepend text now:
for ($i=0; $i -lt $Str.Count; $i++) {
$str[$i] = "yogurt" + $str[$i]
}
$str
yogurtapple
yogurtlemon
yogurttoast
This works for prepending/appending static text to each line. If you need to insert a changing variable this may require some modification. I would need to see more code in order to recommend something.
Another solution, which is fast and concise, albeit a bit obscure.
It uses the regex-based -replace operator with regex '^' which matches the position at the start of each input string and therefore effectively prepends the replacement string to each array element (analogously, you could use '$' to append):
# Sample array.
$array = 'one', 'two', 'three'
# Prepend 'Week ' to each element and create a new array.
$newArray = $array -replace '^', 'Week '
$newArray then contains 'Week one', 'Week two', 'Week three'
To show an equivalent foreach solution, which is syntactically simpler than a for solution (but, like the -replace solution above, invariably creates a new array):
[array] $newArray = foreach ($element in $array) { 'Week ' + $element }
Note: The [array] cast is needed to ensure that the result is always an array; without it, if the input array happens to contain just one element, PowerShell would assign the modified copy of that element as-is to $newArray; that is, no array would be created.
As for what you tried:
"Week"+$Week+$Str
Because the LHS of the + operation is a single string, simple string concatenation takes place, which means that the array in $str is stringified, which by default concatenates the (stringified) elements with a space character.
A simplified example:
PS> 'foo: ' + ('bar', 'baz')
foo: bar baz
Solution options:
For per-element operations on an array, you need one of the following:
A loop statement, such as foreach or for.
Michael Timmerman's answer shows a for solution, which - while syntactically more cumbersome than a foreach solution - has the advantage of updating the array in place.
A pipeline that performs per-element processing via the ForEach-Object cmdlet, as shown in Martin Brandl's answer.
An expression that uses the .ForEach() array method, as shown in Patrick Meinecke's answer.
An expression that uses an operator that accepts arrays as its LHS operand and then operates on each element, such as the -replace solution shown above.
Tradeoffs:
Speed:
An operator-based solution is fastest, followed by for / foreach, .ForEach(), and, the slowest option, ForEach-Object.
Memory use:
Only the for option with indexed access to the array elements allows in-place updating of the input array; all other methods create a new array.[1]
[1] Strictly speaking, what .ForEach() returns isn't a .NET array, but a collection of type [System.Collections.ObjectModel.Collection[psobject]], but the difference usually doesn't matter in PowerShell.

PowerShell dictionary of arrays?

In VBScript I can do something like this
Set objDict = CreateObject("Scripting.Dictionary")
objDict.Add "item1", Array("data1", "data2")
objDict.Add "item2", Array("data3", "data4")
Then I can do a lookup using something like this
dataArray = objDict.Item("item2")
elem1 = dataArray(0)
elem2 = dataArray(1)
the result is elem1 contains "data3" and elem2 contains "data4"
I am not sure how to replicate this in PowerShell. Anyone help?
Dictionaries are called hashtables in PowerShell. When PowerShell first came out Microsoft also released a VBScript-to-PowerShell Conversion Guide, covering dictionaries and arrays among other things.
You define hashtables (dictionaries) like this:
$d = #{
'foo' = 'something'
'bar' = 42
}
Alternatively –for instance if you need to populate the hashtable dynamically– you can create an empty hashtable and add elements to it, like this:
$d = #{}
$d.Add('foo', 'something')
$d.Add('bar', 42)
or like this:
$d = #{}
$d['foo'] = 'something'
$d['bar'] = 42
Usually I prefer the latter, because it replaces existing keys instead of throwing an error.
$d1 = #{}
$d1.Add('foo', 23)
$d1.Add('foo', 42) # can't add another key with same name => error
$d2 = #{}
$d2['foo'] = 23 # key is automatically added
$d2['foo'] = 42 # replaces value of existing key
Arrays are defined as a simple comma-separated list:
$a = 'a', 'b', 'c'
Optionally you can also use the array subexpression operator:
$a = #('a', 'b', 'c')
That's useful if you need an array result, but are unsure of how many results your expression will produce. Without the operator you'd get $null, a single value, or an array, depending on the result of the expression. By using the operator you always get an array, with zero, one, or more elements.
Both array and hashtable elements can be accessed via the index operator ([]):
$d['foo'] # output: "something"
$a[1] # output: "b"
The elements of hashtables can also be accessed via dot-notation:
$d.foo # output: "something"
Of course you can nest arrays in hashtables and vice versa, just like you can do in VBScript. Your example would look somewhat like this in PowerShell
$dict = #{
'item1' = 'data1', 'data2'
'item2' = 'data3', 'data4'
}
$dataArray = $dict['item2']
$elem1 = $dataArray[0]
$elem2 = $dataArray[1]
PowerShell has learned a thing or two from other scripting languages, though. For instance you can assign arrays to a list of variables, so the above assignment of array elements to individual variables could be simplified to this:
$elem1, $elem2 = $dict['item2']
If the array has more elements than the number of variables on the left side of the assignment, the last variable takes the remainder of the array:
$x, $y, $z = 'a', 'b', 'c', 'd'
# $x -> 'a'
# $y -> 'b'
# $z -> #('c', 'd')
$x, $y, $z = 'a', 'b'
# $x -> 'a'
# $y -> 'b'
# $z -> $null
The index operator allows access to multiple elements:
$a = 'a', 'b', 'c', 'd', 'e'
$a[1,3,4] # output: "b", "d", "e"
as well as elements from the end of the array:
$a = 'a', 'b', 'c', 'd', 'e'
$a[-1] # output: "e"
$a[-2] # output: "d"
And the range operator (..) allows you to get a slice from an array by just specifying the first and last index. It produces a list of numbers starting with the first operand and ending with the second operand (e.g. 2..5→2,3,4,5).
$a = 'a', 'b', 'c', 'd', 'e'
$a[1..3] # equivalent to $a[1,2,3], output: "b", "c", "d"
$a[-1..-3] # equivalent to $a[-1,-2,-3], output: "e", "d", "c"
Another solution, you can use a System.Collections.Generic.Dictionary like this:
#for create your dictionary
$mydico = New-Object 'System.Collections.Generic.Dictionary[string,string]'
#For add element into your dictionary
$mydico.Add("mykey1", "myvalue1")
$mydico.Add("mykey2", "myvalue2")
#for get value by key
$value = $mydico["mykey1"] # $value has myvalue1 like value

Array.Find and IndexOf for multiple elements that are exactly the same object

I have trouble of getting index of the current element for multiple elements that are exactly the same object:
$b = "A","D","B","D","C","E","D","F"
$b | ? { $_ -contains "D" }
Alternative version:
$b = "A","D","B","D","C","E","D","F"
[Array]::FindAll($b, [Predicate[String]]{ $args[0] -contains "D" })
This will return:
D
D
D
But this code:
$b | % { $b.IndexOf("D") }
Alternative version:
[Array]::FindAll($b, [Predicate[String]]{ $args[0] -contains "D" }) | % { $b.IndexOf($_) }
Returns:
1
1
1
so it's pointing at the index of the first element. How to get indexes of the other elements?
You can do this:
$b = "A","D","B","D","C","E","D","F"
(0..($b.Count-1)) | where {$b[$_] -eq 'D'}
1
3
6
mjolinor's answer is conceptually elegant, but slow with large arrays, presumably due to having to build a parallel array of indices first (which is also memory-inefficient).
It is conceptually similar to the following LINQ-based solution (PSv3+), which is more memory-efficient and about twice as fast, but still slow:
$arr = 'A','D','B','D','C','E','D','F'
[Linq.Enumerable]::Where(
[Linq.Enumerable]::Range(0, $arr.Length),
[Func[int, bool]] { param($i) $arr[$i] -eq 'D' }
)
While any PowerShell looping solution is ultimately slow compared to a compiled language, the following alternative, while more verbose, is still much faster with large arrays:
PS C:\> & { param($arr, $val)
$i = 0
foreach ($el in $arr) { if ($el -eq $val) { $i } ++$i }
} ('A','D','B','D','C','E','D','F') 'D'
1
3
6
Note:
Perhaps surprisingly, this solution is even faster than Matt's solution, which calls [array]::IndexOf() in a loop instead of enumerating all elements.
Use of a script block (invoked with call operator & and arguments), while not strictly necessary, is used to prevent polluting the enclosing scope with helper variable $i.
The foreach statement is faster than the Foreach-Object cmdlet (whose built-in aliases are % and, confusingly, also foreach).
Simply (implicitly) outputting $i for each match makes PowerShell collect multiple results in an array.
If only one index is found, you'll get a scalar [int] instance instead; wrap the whole command in #(...) to ensure that you always get an array.
While $i by itself outputs the value of $i, ++$i by design does NOT (though you could use (++$i) to achieve that, if needed).
Unlike Array.IndexOf(), PowerShell's -eq operator is case-insensitive by default; for case-sensitivity, use -ceq instead.
It's easy to turn the above into a (simple) function (note that the parameters are purposely untyped, for flexibility):
function get-IndicesOf($Array, $Value) {
$i = 0
foreach ($el in $Array) {
if ($el -eq $Value) { $i }
++$i
}
}
# Sample call
PS C:\> get-IndicesOf ('A','D','B','D','C','E','D','F') 'D'
1
3
6
You would still need to loop with the static methods from [array] but if you are still curious something like this would work.
$b = "A","D","B","D","C","E","D","F"
$results = #()
$singleIndex = -1
Do{
$singleIndex = [array]::IndexOf($b,"D",$singleIndex + 1)
If($singleIndex -ge 0){$results += $singleIndex}
}While($singleIndex -ge 0)
$results
1
3
6
Loop until a match is not found. Assume the match at first by assigning the $singleIndex to -1 ( Which is what a non match would return). When a match is found add the index to a results array.

Resources