AutoHotKey Adding to Array within function call - arrays

I know the newer version is better, but company does not allows me to. So the question is related to AutoHotKey, ver 1.0.47.06.
I am trying to refactor my 400 lines program, by separating them into functions.
CaseNumberArray := "" ; The array to store all the case numbers
CaseNumberArrayCount := 0
; Helper function to load the case number into the array
ReadInputFile() {
Loop, Read, U:\case.txt
{
global CaseNumberArrayCount
CaseNumberArrayCount += 1 ; Increment the ArrayCount
CaseNumberArray%CaseNumberArrayCount% := A_LoopReadLine
current := CaseNumberArray%CaseNumberArrayCount%
}
}
CreateOutputHeader()
ReadInputFile()
MsgBox, There are %CaseNumberArrayCount% case(s) in the file.
Loop, %CaseNumberArrayCount%
{
case_number := CaseNumberArray%A_Index%
MsgBox, %case_number%
}
The last part of the code is testing if I can retrieve the case numbers I loaded into the array named CaseNumberArray, but it is currently all blank.
I studied this question, the author user1944441 wrote:
Important: YourArray must not be global and the counter in
YourArray%counter% must not be global, the rest doesn't matter.
I experimented by placing the global variables in different location, but it still does not work. I know the CaseArrayCount is correctly stored, and the Read Loop is working as well (When it is outside of a function). Is it possible to separate the code into a function?

Usually, global/local declarations are placed right below the method header, not somewhere in some subsequent code block. After all, these declarations apply only to the entire function.
You have to distinguish between simple loop counter variables and variables holding the actual size of the array. In your code, CaseNumberArrayCount describes the size of CaseNumberArray whereas in the answer to which you're referring, it's a counter only used to iterate over the array, which might as well be local.
But you don't have to use two "variables" anyway. Your pseudo array (which can be accessed like CaseNumberArray1, CaseNumberArray2, CaseNumberArray2, ...) has an unused CaseNumberArray0, why not not store the size there?
A pseudo array is actually a collection of sequentially numbered variables. global CaseNumberArray (which by the way you didn't seem to try) will only allow access to the variable named CaseNumberArray, but not CaseNumberArray1 or CaseNumberArray2 and so on.
One solution would be to use Assume-global mode which makes every global variable accessible by default:
; Now, CaseNumberArray0 will hold the array length,
; rendering CaseNumberArrayCount unnecessary
CaseNumberArray0 := 0
; Helper function to load the case number into the array
ReadInputFile() {
; We want to access every global variable we have,
; beware of name conflicts within your function!
global
Loop, Read, test.txt
{
CaseNumberArray0 += 1
CaseNumberArray%CaseNumberArray0% := A_LoopReadLine
}
}
; Here's an alternative: Let AHK build the pseudo array!
ReadInputFileAlternative() {
global caseAlt0
FileRead, fileCont, test.txt
StringSplit, caseAlt, fileCont, `n, `r
}
ReadInputFile()
out := ""
Loop, %CaseNumberArray0%
{
out .= CaseNumberArray%A_Index% "`n"
}
MsgBox, There are %CaseNumberArray0% case(s) in the file:`n`n%out%
; Now, let's test the alternative!
ReadInputFileAlternative()
out := ""
Loop, %caseAlt0%
{
out .= caseAlt%A_Index% "`n"
}
MsgBox, There are %caseAlt0% case(s) in the alternative pseudo-array:`n`n%out%
Edit: "Real Arrays"
As suggested in the comments, here's what I would do instead: I would convince my boss to allow the use of an up-to-date version of AHK and then work with real arrays. This comes with several benefits:
Real arrays are fully managed by AHK, which means that things like inserting, removing, iterating and indexing can all automagically be done by AHK.
A real array resides in one real variable, meaning that you can pass it along functions and anywhere you want, without having to worry about the current scope and whether you can access it in the first place.
The array syntax is very similar to most other languages, making your code intuitive and easier to read. And maybe it helps you in the future when dealing with another language.
Primitive n-dimensional arrays (and primitive AHK objects in general) can be expressed using JSON. This provides you with an easy way to (de-)serialize AHK objects.
The following code snippet shows the two methods used above (reading loop and splitting), but with real arrays. You will notice that we don't need any global declarations anymore, since we now can declare the array inside our function, and simply pass it back to the caller. In my opinion, this is what functions should really look like: A "black box" that doesn't affect its surroundings.
; Method 1: Line by line
ReadLineByLine(file) {
out := []
Loop, Read, % file
{
out.Insert(A_LoopReadLine)
}
return out
}
; Method 2: StrSplit
ReadAndSplit(file) {
FileRead, fileCont, % file
return StrSplit(fileCont, "`n", "`r")
}
caseNumbers := ReadLineByLine("test.txt")
out := "ReadLineByLine() yields " caseNumbers.MaxIndex() " entries:`n`n"
; using the for loop
for idx, caseNumber in caseNumbers
{
out .= caseNumber "`n"
}
MsgBox % out
caseNumbers := ReadAndSplit("test.txt")
out := "ReadAndSplit() yields " caseNumbers.MaxIndex() " entries:`n`n"
; using the normal loop
Loop % caseNumbers.MaxIndex()
{
out .= caseNumbers[A_Index] "`n"
}
MsgBox % out
MsgBox % "The second item is " caseNumbers[2]

Related

Creating a multidimensional array in AutoHotKey that is updated according to user inputs

General problem
In AutoHotKey, I'm having a hard time appending values to an Array object that starts off empty and then gets filled up with values as the user performs certain actions.
The script
Copied below is a small reproducible version of my script. In it, the goal is for the NumpadAdd key to add the current mouse position to the Array called list_of_mouse_positions. After adding a few points, the user can use the Numpad0 key to look at the list of positions stored thus far.
;---------------;
; GENERAL SETUP ;
;---------------;
coordmode Mouse, screen
; Creates an empty array. This will store the mouse positions.
list_of_mouse_positions := Array()
;-----------;
; FUNCTIONS ;
;-----------;
; Function used to add the current mouse position to the global
; list of mouse positions. The `x` parameter serves no purpose.
AddLocation(x)
{
MouseGetPos, xpos, ypos
mouse_pos := Array()
mouse_pos[1] := xpos
mouse_pos[2] := ypos
list_of_mouse_positions.Push(mouse_pos)
}
;-------------;
; KEYBINDINGS ;
;-------------;
; Adds the current mouse position to the
NumpadAdd::
AddLocation(0)
return
; Checking the mouse positions stored so far
NumPad0::
MsgBox, %list_of_mouse_positions%
return
Expected Output vs Actual Output
After running the script and hitting the NumpadAdd key to capture a couple of coordinates, I hit the Numpad0 key. What I expected to show up was a Message Box containing all of the coordinates. However, what showed up instead was just an empty message box.
Specific question
How can I update an Array object (in my case, the list_of_mouse_positions variable) by appending new rows to it, each one of which stores two new values?
Note about the expected dimensions of list_of_mouse_positions
I expect the list_of_mouse_positions variable to be a multidimensional array of dimensions (n x 2), where the first column contains all of the x values, and the second column contains all of the y values.
For example, suppose the user "captured" the coordinates of the following mouse positions:
(100, 200)
(400, 100)
(600, 150)
In this case, the structure of the list_of_mouse_positions would be as follows:
list_of_mouse_positions[1,1] = 100
list_of_mouse_positions[1,2] = 200
list_of_mouse_positions[2,1] = 400
list_of_mouse_positions[2,2] = 100
list_of_mouse_positions[3,1] = 600
list_of_mouse_positions[3,2] = 150
Corrected code to match your new code, but with super-global edit (see doc quote below)
Super-global variables [v1.1.05+]: If a global declaration appears outside of any function, it takes effect for all functions by default (excluding force-local functions). This avoids the need to redeclare the variable in each function. However, if a function parameter or local variable with the same name is declared, it takes precedence over the global variable. Variables created by the class keyword are also super-global.
with some included comments on your code with regards to simplicity you may want to do, or not as you desire:
;---------------;
; GENERAL SETUP ;
;---------------;
; Setting the coordinate mode
coordmode Mouse, screen
; This variable will store the mouse positions.
; The command below establishes that it is a global variable.
; Initializes the global variables
Global list_of_mouse_positions := Array()
Return ; end auto-execute section of script
;-----------;
; FUNCTIONS ;
;-----------;
; Adds the current mouse position to an array of mouse positions
AddLocation()
{
; global list_of_mouse_positions ; not needed, declared super-global above.
MouseGetPos, xpos, ypos
mouse_pos := Array() ; not needed see below
mouse_pos.Push(xpos) ; not needed see below
mouse_pos.Push(ypos) ; not needed see below
list_of_mouse_positions.Push(mouse_pos)
; instead of making 3 additonal lines you can just do 'list_of_mouse_positions.Push(Array(xpos, ypos))'
return
}
; Prints 2D arrays
print2DArr(byRef Arr, setDelim="`r`n", valDelim=", ")
{
printText := ""
For _, Row in Arr
{
printText .= (printText ? setDelim : "") . print1DArr(Row, valDelim)
}
Return printText
}
; Prints 1D arrays
print1DArr(byRef Array, valDelim=", ")
{
printText := ""
For _, Val in Array
{
printText .= (printText ? valDelim : "") . Val
}
Return printText
}
;-------------;
; KEYBINDINGS ;
;-------------;
; Adds new positions to the list of mouse positions
NumpadAdd::
AddLocation()
return
; Checking the mouse positions stored so far
NumPad0::
global list_of_mouse_positions
MsgBox % print2DArr(list_of_mouse_positions)
return

Passing array arguments to Perl subroutine

I am new to perl programming and I am trying to build a script using several subroutines on it. For a start I am trying to run a short mocke script to work out subroutines behaviour, but I don't get to understand the input.
Here is my code:
sub prueba{
my (#array1, #array2)=#_;
if (scalar(#array1)<scalar(#array2)) {
print #array1,"\n";
} elsif (scalar(#array1)>scalar(#array2)){
print #array2,"\n";
}
};
my #primero=(1,5,9);
my #segundo=(1,7,8,9,6,5,6,9);
prueba(#primero,#segundo);
I am passing two arrays and I want the subroutine to retrieve the answer according to those arrays, but when I run it I get no output, not even warning errors messages... I already tried using the refference to the array, but still not working:
sub prueba{
my (#array1, #array2)=#_;
if (scalar(#array1)<scalar(#array2)) {
print #array1,"\n";
} elsif (scalar(#array1)>scalar(#array2)){
print #array2,"\n";
}
};
my #primero=(1,5,9);
my #segundo=(1,7,8,9,6,5,6,9);
prueba(\#primero,\#segundo);
You can't pass arrays to subs (and they can't return them). You can only pass a number of scalars. What you are doing is equivalent to the following:
prueba(1,5,9,1,7,8,9,6,5,6,9);
All of the arguments end up in #array1. What we do is pass references to arrays.
prueba(\#primero,\#segundo);
But that also requires changing the sub. Without change, all of the arguments still end up in #array1. See perlreftut for a start on working with references. You can use
sub prueba{
my ($array1, $array2)=#_;
if (scalar(#$array1)<scalar(#$array2)) {
print "#$array1\n";
} elsif (scalar(#$array1)>scalar(#$array2)){
print "#$array2\n";
}
}
or just
sub prueba {
my ($array1, $array2) = #_;
if (#$array1 < #$array2) { say "#$array1"; }
elsif (#$array1 > #$array2) { say "#$array2"; }
}
< and > expect a number, so they already impose scalar context. And might as well use say, though that requires use feature qw( say ); (or something like use 5.014; which does the trick as well).
You can use prototypes to make it look like you're passing multiple arrays, and have perl turn them automatically into references:
sub prueba :prototype(\#\#) {
my ($array1, $array2) = #_;
if (#$array1 < #$array2) {
print #$array1,"\n";
} elsif (#$array1 > #$array2){
print #$array2,"\n";
}
}
my #primero=(1,5,9);
my #segundo=(1,7,8,9,6,5,6,9);
prueba(#primero, #segundo);
But read the documentation carefully to understand the cases where the subroutine can be called without enforcing the prototype.
Thanks all I just figured out what I wanted. I found that I can actually pass an array to perl, however maybe I am not explaning mysfel properly.The thing is to load the arrays as follow, inside the subroutine.
my #primero=#{$_[0]};
my #segundo=#{$_[1]};
This means we are using the reference. Ehen running the function, we must write the \ before each input:
prueba(\#primero,\#segundo);

Reading data from file into an array to manipulate within Perl script

New to Perl. I need to figure out how to read from a file, separated by (:), into an array. Then I can manipulate the data.
Here is a sample of the file 'serverFile.txt' (Just threw in random #'s)
The fields are Name : CPU Utilization: avgMemory Usage : disk free
Server1:8:6:2225410
Server2:75:68:64392
Server3:95:90:12806
Server4:14:7:1548700
I would like to figure out how to get each field into its appropriate array to then perform functions on. For instance, find the server with the least amount of free disk space.
The way I have it set up now, I do not think will work. So how do I put each element in each line into an array?
#!usr/bin/perl
use warnings;
use diagnostics;
use v5.26.1;
#Opens serverFile.txt or reports and error
open (my $fh, "<", "/root//Perl/serverFile.txt")
or die "System cannot find the file specified. $!";
#Prints out the details of the file format
sub header(){
print "Server ** CPU Util% ** Avg Mem Usage ** Free Disk\n";
print "-------------------------------------------------\n";
}
# Creates our variables
my ($name, $cpuUtil, $avgMemUsage, $diskFree);
my $count = 0;
my $totalMem = 0;
header();
# Loops through the program looking to see if CPU Utilization is greater than 90%
# If it is, it will print out the Server details
while(<$fh>) {
# Puts the file contents into the variables
($name, $cpuUtil, $avgMemUsage, $diskFree) = split(":", $_);
print "$name ** $cpuUtil% ** $avgMemUsage% ** $diskFree% ", "\n\n", if $cpuUtil > 90;
$totalMem = $avgMemUsage + $totalMem;
$count++;
}
print "The average memory usage for all servers is: ", $totalMem / $count. "%\n";
# Closes the file
close $fh;
For this use case, a hash is much better than an array.
#!/usr/bin/perl
use strict;
use feature qw{ say };
use warnings;
use List::Util qw{ min };
my %server;
while (<>) {
chomp;
my ($name, $cpu_utilization, $avg_memory, $disk_free)
= split /:/;
#{ $server{$name} }{qw{ cpu_utilization avg_memory disk_free }}
= ($cpu_utilization, $avg_memory, $disk_free);
}
my $least_disk = min(map $server{$_}{disk_free}, keys %server);
say for grep $server{$_}{disk_free} == $least_disk, keys %server;
choroba's answer
is ideal, but I think your own code could be improved
Don't use v5.26.1 unless you need a specific feature that is available only in the given version of Perl. Note that it also enables use strict, which should be at the top of every Perl program you write
die "System cannot find the file specified. $!" is wrong: there are multiple reasons why an open may fail, beyond that it "cannot be found". Your die string should include the path to the file you're trying to open; the reason for the failure is in $!
Don't use subroutine prototypes: they don't do what you think they do. sub header() { ... } should be just sub header { ... }
There's no point in declaring a subroutine only to call it a few lines later. Put your code for header in line
You have clearly come from another language. Declare your variables with my as late as possible. In this case only $count and $totalMem must be declared outside the while loop
perl will close all open file handles when the program exits. There is rarely a need for an explicit close call, which just makes your code more noisy
$totalMem = $avgMemUsage + $totalMem is commonly written $totalMem += $avgMemUsage
I hope that helps
To your original question about how to store the data in an array...
First, initialize an empty array outside the file read loop:
my #servers = ();
Then, within the loop, after you have your data pieces parsed out, you can store them in your array as sub-arrays (the resulting data structure is a two dimensional array):
$servers[$count] = [ $name, $cpuUtil, $avgMemUsage, $diskFree ];
Note, the square brackets on the right create the sub-array for the server's data pieces and return a reference to this new array. Also, on the left side we just use the current value of $count as an index within the #servers array and as the value increases, the size of the #servers array will grow automatically (this is called autovivification of new elements). Alternatively, you can push new elements onto the #servers array inside the loop, like this:
push #servers, [ $name, $cpuUtil, $avgMemUsage, $diskFree ];
This way, you explicitly ask for a new element to be added to the array and the square brackets still do the same creation of the sub-array.
In any case, the end result is that after you are finished with the file read loop, you now have a 2D array where you can access the first server and its disk free field (the 4-th field at index 3) like this:
my $df = $servers[0][3];
Or inspect all the servers in a loop to find the minimum disk free:
my $min_s = 0;
for ( my $s = 0; $s < #servers; $s++ ) {
$min_s = $s if ( $servers[$s][3] < $servers[$min_s][3] );
}
print "Server $min_s has least disk free: $servers[$min_s][3]\n";
Like #choroba suggested, you can store the server data pieces/fields in hashes, so that your code will be more readable. You can still store your list of servers in an array but the second dimension can be hash:
$servers[$count] = {
name => $name,
cpu_util => $cpuUtil,
avg_mem_usage => $avgMemUsage,
disk_free => $diskFree
};
So, your resulting structure will be an array of hashes. Here, the curly braces on the right create a new hash and return the reference to it. So, you can later refer to:
my $df = $servers[0]{disk_free};

AutoHotKey - Declaring global array does not work

; Give user the opportunity to choose his own hotkey
Gui, Add, Hotkey, x21 y234 w240 h30 vPanicKey gRunPanicKey,^F12
global myList := ["foo"]
RunPanicKey:
if(PanicKey != "") { ; Make sure the hotkey chosen by the user isn't empty.
myList.Insert("bar") ; Insert new string into the array.
myList2 := myList[2] ; Get 2nd index value and store it in myList2
MsgBox,0,My Array, The 2nd value of myList is: %myList2%
}
return
It appears that myList.Insert() does not work here, because the script cannot find the array, therefore myList2 is empty. But how come? I thought I made the array global?
GuiClose:
ExitApp
return
global myList := ["foo"]
As seen from your comment, the problem lies not within the global keyword (which you actually do not need at all here). The reason your program is not working is that return statement. Everything after the first return in a script does not get executed automatically on script start. See https://autohotkey.com/docs/Scripts.htm#auto for details. So, simply move the variable initialization at the very beginning of your file.

Iterating over array references causes infinite looping

I have the following method:
sub CleanErrorLog {
my ($actnList, $cmplist) = #_;
print "\n" . ("-" x 100) . "\n";
print "\t\t---->> Begin Clean Error Output <<----";
for my $comp (#$cmplist)
{
for my $action (#$actnList)
{
Build($comp, $action);
}
}
}
This is called by:
CleanErrorLog(\#actionList, \#failedComponents) if #failedComponents;
However, the loop never ends - it continuously attempt to Build($comp, $action) over and over. This is the first time I've used \# for parameters, so I could be doing something wrong?
Your Build function probably modified the #actionList or #failedComponents arrays. As you passed your arrays by reference, these modifications could lead to infinite looping. As a guide line, never modify the array or hash you are iterating over. Always do a copy first. For example, you could pass copies to CleanErrorLog:
CleanErrorLog([#actionList], [#failedComponents]) if #failedComponents;
The better solutution would be to rework Build so that it doesn't modify these variables.

Resources