PowerShell dictionary of arrays? - 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

Related

Powershell join array values

I'm looking to cross combine values of arrays to create new array. example,
$a = #('Blue','red','green')
$b = #('car', 'bike')
to something like
('blue car','red car','green car') and ('blue bike','red bike','green bike')
PS: this is not simple concatenate function I'm looking.
Thanks,
Nilay
$a = #('Blue', 'red', 'green')
$b = #('car', 'bike')
$outArrays = #(), #() # Initialize a 2-element array.
$i = 0
foreach ($elB in $b) {
$outArrays[$i++] = foreach ($elA in $a) { "$elA $elB" }
}
$outArrays[0] now contains 'blue car', 'red car', 'green car'
$outArrays[1] now contains 'blue bike', 'red bike', 'green bike'
The above takes advantage of PowerShell's ability to use loop statements such as foreach as expressions, with PowerShell implicitly collecting all outputs from the loop in an array ([object[]]; assuming two or more outputs).
General information about array concatentation:
To create a flat array, by concatenation, simply use +:
# Creates a flat array with 5 elements:
# #('Blue', 'red', 'green', 'car', 'bike')
$a + $b
To create a two-element nested array, use the unary form of ,, the array constructor operator:
# -> Nested 2-element array:
# * Element 0 contains #('Blue', 'red', 'green')
# * Element 1 contains: #('car', 'bike')
, $a + , $b
As an aside: note that you don't strictly need #(...), the array-subexpression operator to create array literals; e.g.,
$a = 'Blue', 'red', 'green' will do.
You can create a matrix with this approach
# your input
$a = #('Blue','red','green')
$b = #('car', 'bike')
# create an empty result array
$result = #()
# iterate the input arrays and add the items to the result
$b | ForEach-Object { $x=$_; $a | ForEach-Object { $result += "$_ $x" } }
The += operator is not very efficient. It will create a new instance of the result for each iteration. This might work for small input arrays.

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

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" })

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"
}

Perl: slicing an array of hashes

The output of the code below is always empty. Not sure what I am doing wrong and would appreciate any help. How do I get to the values of a key in a specific hash in an array of hashes?
use strict;
use warnings;
my %dot1 = ('a'=>1,'b'=>2);
my %dot2 = ('a'=>3,'b'=>4);
my %dot3 = ('a'=>5,'b'=>6);
my %dot4 = ('a'=>7,'b'=>8);
my #array = (%dot1,%dot2,%dot3,%dot4);
my %x = $array[2];
my $y = $x->{'a'};
print "$y \n";
You don't have an array of hashes. You have an array that looks like a hash, where the keys a and b will be there four times each, in relatively random order.
print Dumper \#array;
$VAR1 = [
'a',
1,
'b',
2,
'a',
3,
'b',
4,
'a',
5,
'b',
6,
'a',
7,
'b',
8
];
Afterwards, you are using $x->{a}, which is the syntax to take the key a from the hashref $x, but you only ever declared a hash %a. That in turn breaks, because you give it an odd-sized list of one value.
Instead, add references to the hashes to your array. That way you will get a multi-level data structure instead of a flat list. Then make the x variable a scalar $x.
my %dot1 = ('a'=>1,'b'=>2);
my %dot2 = ('a'=>3,'b'=>4);
my %dot3 = ('a'=>5,'b'=>6);
my %dot4 = ('a'=>7,'b'=>8);
my #array = (\%dot1,\%dot2,\%dot3,\%dot4); # here
my $x = $array[2]; # here
my $y = $x->{'a'};
print "$y \n";
This will print 5.
You should read up on data structures in perlref and perlreftut.
If you want an array of hash references, you need to say so explicitly.
my #array = (\%dot1, \%dot2, \%dot3, \%dot4);
my %x = %{$array[2]};
my $y = $x{a};
print "$y\n";
What you want to do is add references to your hashes to your #array, otherwise perl will evaluate the hashes in list context.
my #array = (\%dot1,\%dot2,\%dot3,\%dot4);
my $x = $array[2];
my $y = $x->{'a'};
print "$y \n";

Handle range operator with positive and negative range?

I want to store the second index to fifth element from last index from splitting using range operator.
my $s = "a-b-c-d-e-f-g-f-e-c-a-v-a-t-a-v";
my #ar = (split "-",$s)[2..-5];
print #ar;
Expected output is c-d-e-f-g-f-e-c-a-v
As like below I want to store
a-b-c-d-e-f-g-f-e-c-a-v-a-t-a-v
| |
2 |
-5
__________________
How to store these element
The easiest way is to put it in an array and then slice it:
my $s = 'a-b-c-d-e-f-g-f-e-c-a-v-a-t-a-v';
my #arr1 = split /-/, $s;
my #arr2 = #arr1[2..#arr1-5];
or
my #arr = do { my #arr = split /-/, $s; #arr[2..#arr-5] };
or
my #arr = map #$_[2..$#$_-5], [ split /-/, $s ];
To do it all at once without an extra variable, you would need to pick an arbitrary size bigger than the array would be and do two slices:
my #arr = grep defined, (grep defined, (split /-/, $s)[2..999])[-999..-5];
(which is much less efficient than just using a separate array).
Explanation: assuming $s is 'a-b-c-d-e-f-g-f-e-c-a-v-a-t-a-v':
The first slice (2..999) will produce a list of 'c', 'd', 'e', ..., 't', 'a', 'v' followed by 984 undef values, since the slice asks for that many elements that don't exist. The inner grep defined removes those undef values, leaving the 14 element c through v list.
The second slice (-999..-5) produces a list beginning with 985 undef elements (since indexes -999 through -15 don't exist) followed by 'c', 'd', 'e', ..., 'c', 'a', 'v'. The outer grep defined removes those undef values, leaving the desired 10 element list.
You can't do that directly. 2..-5 is an empty list because 2 is greater than -5.
You'd have to use an array:
my $s = "a-b-c-d-e-f-g-f-e-c-a-v-a-t-a-v";
my #ar = split /-/, $s;
print join("-", #ar[2 .. $#ar-4]), "\n";
$#ar is the last index in #ar, which makes the range computation work.

Resources