Handle range operator with positive and negative range? - arrays

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.

Related

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

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

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

Using an array to index into another array

I have two arrays, let's call them #a1 and #a2. What I'm trying to do is obtain elements from #a2 using the values in #a1 as indices. My current attempt doesn't work properly.
foreach (#a1) {
print $a2[$_] . "at" . $_;
}
This only prints $_ but not $a2[$_].
I sense there is a trivial solution to this, but I just can't find it.
There is nothing wrong with the code you have. I have tested a small script and it works as expected. Asi i suggested in my comment, try using something like Data::Dumper to see whats in the arrays before the loop.
use strict;
use warnings;
use Data::Dumper;
my #a1 = (0..4);
my #a2 = ("a".."e");
print Dumper \#a1, \#a2;
foreach (#a1){
print $a2[$_]." at ".$_."\n";
}
OUTPUT
$VAR1 = [
0,
1,
2,
3,
4
];
$VAR2 = [
'a',
'b',
'c',
'd',
'e'
];
a at 0
b at 1
c at 2
d at 3
e at 4
there's no reason your code shouldn't work as long as the values of the first array are valid addresses in the second array. but if all you really want to do is just get the values and address of the second array, you could just do:
for my $i (0..$#a2) {
print "$i: $a2[$i]","\n";
}
$#a2 is the last element address of the array.

Two-dimensional array access in Perl

I have an array populated with cities. I want to pass the array by reference to a sub routine and print each city to output. However, I have the following problems:
I can access each element before my while loop in the subroutine. But I cannot access the elements within my while loop. I get the error message:
...
Use of uninitialized value in print at line 44, line 997 (#1)
Use of uninitialized value in print at line 44, line 998 (#1)
...
The following is some code. I have commented what prints and what doesn't (I tried to cut out code that is not needed for my explanation...):
#cities;
# Assume cities is loaded successfully
&loadCities(getFileHandle('cities.txt'), $NUM_CITIES, \#cities);
&printElements(getFileHandle('names.txt'), \#cities);
sub printElements{
my $counter = 0;
my $arraySize = scalar $_[1];
# Prints fine!!!
print #{$_[1][($counter)%$arraySize];
while ((my $line = $_[0]->getline()) && $counter < 1000){
# Doesn't print. Generates the above error
print #{$_[1][($counter)%$arraySize];
$counter += 1;
}
}
The Perl syntax has me super confused. I do not understand what is going on with #{$_[1]}[0]. I am trying to work it out.
$_[1], treat the value at this location as scalar value (memory
address of the array)
#{...}, interpret what is stored at this
memory address as an array
#{...} [x], access the element at index x
Am I on the right track?
My first tip is that you should put use strict; and use warnings; at the top of your script. This generally reveals quite a few things.
This line: print #{$_[1][($counter)%$arraySize]; doesn't have a closing }. You also don't need the parenthesis around $counter.
Like you mentioned, the best/most clear way to get the length of an array is my $arraySize = scalar #{$_[1]};.
You can check out the documentation here for working with references. I'll give you a quick overview.
You can declare an array as normal
my #array = (1, 2, 3);
Then you can reference it using a backslash.
my $array_ref = \#array;
If you want to use the reference, use #{...}. This is just like using a regular array.
print #{$array_ref};
You could also declare it as a reference to begin with using square braces.
my $array_ref = [1, 2, 3];
print #{$array_ref}; # prints 123
In Perl, a 2-dimensional array is actually an array of array references. Here is an example:
my #array = ( ['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i'] );
print #{$array[1]}; # prints def
Now let's try passing in an array reference to a subroutine.
my #array = ( ['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i'] );
example(\#array); # pass in an array reference
sub example {
my #arr = #{$_[0]}; # use the array reference to assign a new array
print #{$arr[1]};
print #{$_[0][1]}; # using the array reference works too!
}
Now let's put it together and print the whole 2-d array.
my #array = ( ['a', 'b', 'c'], ['d', 'e', 'f'], ['g', 'h', 'i'] );
example(\#array);
sub example {
my #arr = #{$_[0]};
for my $ref (#arr) {
print #{$ref};
}
} # prints abcdefghi
You could adapt this example pretty easily if you wanted to use it for your printElements subroutine.
Another note about printing the elements in an array. Let's take this line from the last example:
print #{$ref};
Since we are calling it every time through the loop, we may want to print a new line at the end of it.
print #{$ref} . "\n";
What does this print? Try it! Does it work?
This is where the built-in subroutine join comes in handy.
print join(" ", #{$ref}) . "\n";
For loops are generally the best way to iterate through an array. My answer here talks a little about doing it with a while loop: https://stackoverflow.com/a/21950936/2534803
You can also check out this question: Best way to iterate through a Perl array
To make references a bit easier to understand, I prefer the -> syntax instead of the munge-it-all-together syntax.
Instead of:
#{$_[1]}[0].
Try
$_[1]->[0];
It means the same thing. It's easier, and it's cleaner to see. You can see that $_[1] is an array reference, and that you're referencing the first element in that array reference.
However, an even better way is to simply set variables for your various element in #_. You have to type a few more letters, but your code is much easier to understand, and is a lot easier to debug.
sub print_elements {
my $file_handle = shift; # This isn't a "reference", but an actual file handle
my $cities_array_ref = shift; # This is a reference to your array
my #cities = #{ $cities_array_ref }; # Dereferencing makes it easier to do your program
Now, your subroutine is dealing with variables which have names, and your array reference is an array which makes things cleaner. Also, you cannot accidentally affect the values in your main program. When you use #_, it's a direct link to the values you pass to it. Modifying #_ modifies the value in your main program which is probably not what you want to do.
So, going through your subroutine:
sub printElements {
my file_handle = shift;
my $cities_array_ref = shift;
my #cities = #{ $cities_array_ref };
my $counter;
my $array_size = #cities; # No need for scalar. This is automatic
while ( my $line = $file_handle->getline and $counter < 1000 ) {
chomp $line;
my $city_number = $counter % $array_size;
print $cities[$city_number]. "\n";
$counter += 1;
}
}
Note, how much easier it is to see what's going on by simply assigning a few variables instead of trying to cram everything together. I can easily see what your parameters to your subroutine are suppose to be. If you called the subroutine with the incorrect parameter order, you could easily spot it. Also notice I broke out $counter % $array_size and assigned that to a variable too. Suddenly, it's obvious what I'm trying to get out of it.
However, I can't see where you're using the $line you're getting with getline. Did I miss something?
By the way, I could have done this without referencing the array in the while loop too:
sub printElements {
my file_handle = shift;
my $cities = shift; # This is an array reference!
my $counter;
my $array_size = #{ $cities }; # I need to deref to get an array
while ( my $line = $file_handle->getline and $counter < 1000 ) {
chomp $line;
my $city_number = $counter % $array_size;
print $cities->[$city_number]. "\n"; # That's it!
$counter += 1;
}
}
See how that -> syntax makes it easy to see that $cities is a reference that points to an array? A lot cleaner and easier to understand than ${$cities}[$city_number].
This code wouldn't actually compile.
print #{$_[1][($counter)%$arraySize];
It probably wants to be
print $_[1]->[($counter)%$arraySize];
after you fix arraySize.
If the result is somehow a pointer to an array then
print "#{$_[1]->[($counter)%$arraySize]}";
I figured how to solve my #1 problem (still looking for help on my #2 if anyone can).
I changed
my $arraySize = scalar $_[1];
to
my $arraySize = #{$_[1]};
And my second print statement is printing the way I want.
It seems that scalar $_[1] was taking the memory address of the array and I was moding against this allowing my $counter to go way beyond the number of elements in the array.
References confuse me too! I always like to dereference them as soon as possible. This works for me:
sub printElements{
my $counter = 0;
my $fh = $_[0];
my #array = #{$_[1]};
my $arraySize = scalar #array;
# Prints fine!!!
print #array[($counter)%$arraySize];
while ((my $line = $fh->getline()) && $counter < 1000){
#Doesn't print. Generates the above error
print #array[($counter)%$arraySize];
$counter += 1;
}
}
I'm sure someone else could explain in the comments why they think working with the reference is a better way (please do), but under the mantra of "keep it simple", I don't like working with them. Probably because I was never a C programmer...

Resources