Only trigger a function in the last iteration of a forEach loop

Get help with using AutoHotkey and its commands and hotkeys
majstang
Posts: 89
Joined: 11 Feb 2018, 03:39

Only trigger a function in the last iteration of a forEach loop

26 Aug 2020, 13:55

After spending hours trying to do this and with nothing to show I ask about it instead. For Each loops an array containing an object. How in the world do you trigger a function ONLY in the last iteration?
User avatar
Relayer
Posts: 138
Joined: 30 Sep 2013, 13:09
Location: Delaware, USA

Re: Only trigger a function in the last iteration of a forEach loop

26 Aug 2020, 14:03

Depending on they type of object something like this will work"

Code: Select all

for k, v in object
{
	do something
	if (A_Index = object.Length())
		call function
}
Relayer
User avatar
boiler
Posts: 6589
Joined: 21 Dec 2014, 02:44

Re: Only trigger a function in the last iteration of a forEach loop

26 Aug 2020, 14:13

Use .Count() so it works with non-integer keys:

Code: Select all

Car := {make: "Toyota", model: "Celica", style: "coupe", color: "grey"}
for k, v in Car {
	MsgBox, % k " - " v
	if (A_Index = Car.Count())
		LastItem()
}
return

LastItem() {
	MsgBox, This is the last item
}
A_AhkUser
Posts: 1147
Joined: 06 Mar 2017, 16:18
GitHub: AAhkUser
Location: France

Re: Only trigger a function in the last iteration of a forEach loop

26 Aug 2020, 15:02

I should mention that, generally, length() and count() have nothing 'loopy'; thus, it might be relevant to put them outside the body of the loop if the loop's target object is not intented to be modified during the time of the loop execution.
For the same reason, and unlike for the first one I wonder why it is relevant to handle the last iteration from within the body of the loop. If anyone know any use case I'd be interested in as I failed to figure it out one. Thanks.

Example using Continue instead:

Code: Select all

Car := {make: "Toyota", model: "Celica", style: "coupe", color: "grey"}
for k, v in Car, count := Car.count() {
	MsgBox, 64,, % k "," v
	if (--count)
		continue
	f("last", k, v)
}
f(which, a, b) {
MsgBox % "After the sorting of the target object it appears that '" a "' is the " which " element (its value is '" b "')."
}
Btw, one possible option to handle the first iteration:

Code: Select all

enum := Car._NewEnum()
if (enum.next(k, v)) {
	f("first", k, v)
}
while(enum.next(k, v)) {
	MsgBox, 64,, % k "," v
}
f(which, a, b) {
MsgBox % "After the sorting of the target object it appears that '" a "' is the " which " element (its value is '" b "')."
}

A_AhkUser
my scripts
User avatar
boiler
Posts: 6589
Joined: 21 Dec 2014, 02:44

Re: Only trigger a function in the last iteration of a forEach loop

26 Aug 2020, 15:21

A_AhkUser wrote:
26 Aug 2020, 15:02
I should mention that, generally, length() and count() have nothing 'loopy'; thus, it might be relevant to put them outside the body of the loop if the loop's target object is not intented to be modified during the time of the loop execution.
For the same reason, and unlike for the first one I wonder why it is relevant to handle the last iteration from within the body of the loop. If anyone know any use case I'd be interested in as I failed to figure it out one. Thanks.
Here's a simple example where you would use the fact that it's the last iteration of the loop while you're in the loop instead of afterwards: You are asking a user a series of questions, and within the loop, you have the code that asks the question, evaluates the answer, etc. On asking the last question, you want to say, "And one last question..." That would need to be handled within the loop, not afterwards.

In general, it seems very reasonable that there would be things that you would do differently during the last iteration of the loop, not afterward.
majstang
Posts: 89
Joined: 11 Feb 2018, 03:39

Re: Only trigger a function in the last iteration of a forEach loop

26 Aug 2020, 15:57

Sorry guys that does not work. So I tried to identify which kind of array/object im dealing with and it looks like to be a "multi-dimensional" array which is storing arrays (or objects) inside other arrays. I think it is commonly called "nested arrays"!?
Using boilers example upon it A_Index and Car.Count() never matches and the condition fails accordingly.
Last edited by majstang on 26 Aug 2020, 16:29, edited 1 time in total.
User avatar
boiler
Posts: 6589
Joined: 21 Dec 2014, 02:44

Re: Only trigger a function in the last iteration of a forEach loop

26 Aug 2020, 16:20

It does work. You just have to implement it correctly for your specific situation. Example:

Code: Select all

Car := [{make: "Toyota", model: "Celica", style: "coupe", color: "grey"}
	, {make: "Mazda", model: "RX-7", style: "coupe", color: "black"}]
for k, v in Car {
	for i, e in v
		MsgBox, % i " - " e
	if (A_Index = Car.Count())
		LastItem()
}
return

LastItem() {
	MsgBox, That was the last item
}
User avatar
boiler
Posts: 6589
Joined: 21 Dec 2014, 02:44

Re: Only trigger a function in the last iteration of a forEach loop

26 Aug 2020, 16:24

You might say, "Well that wasn't inside the inner loop." See the following:

Code: Select all

Car := [{make: "Toyota", model: "Celica", style: "coupe", color: "grey"}
	, {make: "Mazda", model: "RX-7", style: "coupe", color: "black"}]
for k, v in Car {
	idx := A_Index
	for i, e in v {
		MsgBox, % i " - " e
		if (idx = Car.Count() && A_Index = v.Count())
			LastItem()
	}
}
return

LastItem() {
	MsgBox, That was the last item
}
majstang
Posts: 89
Joined: 11 Feb 2018, 03:39

Re: Only trigger a function in the last iteration of a forEach loop

26 Aug 2020, 16:39

Aha so that is how it should be done...using an inner loop. Im sure this will work when having more testing time available. Thanks for the lesson boiler :wave:
A_AhkUser
Posts: 1147
Joined: 06 Mar 2017, 16:18
GitHub: AAhkUser
Location: France

Re: Only trigger a function in the last iteration of a forEach loop

26 Aug 2020, 16:56

boiler wrote:
26 Aug 2020, 15:21
(...) In general, it seems very reasonable that there would be things that you would do differently during the last iteration of the loop, not afterward.
Ok I see - and I would be tempted to say that it is actually very reasonable, pretty obvious; I guess it is simply my narrow-mindedness and lack of experience that prevented me from seeing a possible pertinent difference between the handling of the last iteration of a loop as such vs an external handling, outside the loop. I simply kept in mind the handling of the first iteration shown above and try to apply it mutatis mutandis to the handling of any last iteration forgiving that it was built in a situation that rendered it pertinent.

I can say as well thanks for the lesson boiler :wave: (and allow me to take this opportunity to thank you for the constancy and quality of your help here on the forum).

boiler wrote: Car := [{make: "Toyota", model: "Celica", style: "coupe", color: "grey"}, {make: "Mazda", model: "RX-7", style: "coupe", color: "black"}]
It reminds me of the good old days: my (virtual) garage in Gran Turismo 2...

Cheers

A_AhkUser
my scripts
User avatar
boiler
Posts: 6589
Joined: 21 Dec 2014, 02:44

Re: Only trigger a function in the last iteration of a forEach loop

26 Aug 2020, 17:03

A_AhkUser wrote: I can say as well thanks for the lesson boiler :wave: (and allow me to take this opportunity to thank you for the constancy and quality of your help here on the forum).
Very kind of you to say. Thank you.
A_AhkUser wrote:
boiler wrote: Car := [{make: "Toyota", model: "Celica", style: "coupe", color: "grey"}, {make: "Mazda", model: "RX-7", style: "coupe", color: "black"}]
It reminds me of the good old days: my (virtual) garage in Gran Turismo 2...
My good old days too, as those were my first two (physical) cars. :)
majstang
Posts: 89
Joined: 11 Feb 2018, 03:39

Re: Only trigger a function in the last iteration of a forEach loop

26 Aug 2020, 18:12

Yeah, it kind of works, but not as i was imagine it. One more question boiler. The iteration of the nested array seems to always start with the last item e.g. color: "grey" and finish with the second to last e.g. model: "Celica". As it happens in my live nested array im planning on triggering the function after, as in your sample, color: "white" has been iterated and i will pass that value "white" to the function. Now the last iterated item is "Vito". Why does the iteration behave like that?

Code: Select all

Car := [{make: "Toyota", model: "Celica", color: "grey"}
	  , {make: "Mazda", model: "RX-7", color: "black"}
	  , {make: "Mercedes-Benz", model: "Vito", color: "white"}]
for k, v in Car {
    idx := A_Index
	for i, e in v {
      ;MsgBox, % i " - " e
	  if (idx = Car.Count() && A_Index = v.Count())
		MsgBox, % "The last iterated item was " e
		;LastItem()
    }
}
return

LastItem() {
	MsgBox, That was the last item
}
User avatar
boiler
Posts: 6589
Joined: 21 Dec 2014, 02:44

Re: Only trigger a function in the last iteration of a forEach loop

26 Aug 2020, 19:37

It's because the for loop takes the items in alphabetically (or alphanumerically) sorted order of the keys. So you could come up with different key names to have them presented in the order you would like, such as this:

Code: Select all

Car := [{1make: "Toyota", 2model: "Celica", 3color: "grey"}
	  , {1make: "Mazda", 2model: "RX-7", 3color: "black"}
	  , {1make: "Mercedes-Benz", 2model: "Vito", 3color: "white"}]
for k, v in Car {
    idx := A_Index
	for i, e in v {
      ;MsgBox, % i " - " e
	  if (idx = Car.Count() && A_Index = v.Count())
		MsgBox, % "The last iterated item was " e
		;LastItem()
    }
}
return

LastItem() {
	MsgBox, That was the last item
}
majstang
Posts: 89
Joined: 11 Feb 2018, 03:39

Re: Only trigger a function in the last iteration of a forEach loop

27 Aug 2020, 03:03

Aha, thanks boiler.
Unfortunately this idea does not work alongside just mes WatchFolder function https://www.autohotkey.com/boards/viewtopic.php?f=6&t=8384&hilit=WatchFolder

If im dragndrop moving two files into a watched folder, it looks like the Changes array is re-populated (not nested in first loop) before each for each loop starts, which renders a last iteration match for both files dropped. If dropping three files the first looped Changes is unnested containing the first file dropped, the second loop Changes is suddenly nested with second and third files dropped and in third loop it is a copy of the second loop. No wonder it gets problematic ;) One would of course prefer the Changes array was populated and nested with everything before entering the for each loop in ReportFunction().

So unfortunately the idea to trigger a function in last iteration becomes rather hard to accomplish, using your excellent approach, does not do what I intended namely not passing masses of file names as parameters to other scripts, but just pass the last one, avoiding the "Too long continuation section" error.

There are of course a number of workarounds to avoid that error, but it would be interesting and learning how to implement this approach using WatchFolder()

Here is a sample:

Code: Select all

#Persistent
WatchFolder("C:\AHK-Scripts\lib", "ReportFunction")

; WatchFolder - Reportfunctions
ReportFunction(Directory, Changes) { 
 For Each, Change In Changes {
       Action := Change.Action
       Name := Change.Name
       idx := A_Index
       for i, e in Change {
	   ; -------------------------------------------------------------------------------------------------------------------------
        ; Action 1 (added) = File gets added in the watched folder
	   If (Action = 1)  {
             if (idx = Changes.Count() && A_Index = Change.Count())
               MsgBox, % "The last iterated item was " e
             }
        }
  }
} 
; ===============================================================================================================================
; Function:       Notifies about changes within folders.
;                 This is a rewrite of HotKeyIt's WatchDirectory() released at
;                    http://www.autohotkey.com/board/topic/60125-ahk-lv2-watchdirectory-report-directory-changes/
; Tested with:    AHK 1.1.23.01 (A32/U32/U64)
; Tested on:      Win 10 Pro x64
; Usage:          WatchFolder(Folder, UserFunc[, SubTree := False[, Watch := 3]])
; Parameters:
;     Folder      -  The full qualified path of the folder to be watched.
;                    Pass the string "**PAUSE" and set UserFunc to either True or False to pause respectively resume watching.
;                    Pass the string "**END" and an arbitrary value in UserFunc to completely stop watching anytime.
;                    If not, it will be done internally on exit.
;     UserFunc    -  The name of a user-defined function to call on changes. The function must accept at least two parameters:
;                    1: The path of the affected folder. The final backslash is not included even if it is a drive's root
;                       directory (e.g. C:).
;                    2: An array of change notifications containing the following keys:
;                       Action:  One of the integer values specified as FILE_ACTION_... (see below).
;                                In case of renaming Action is set to FILE_ACTION_RENAMED (4).
;                       Name:    The full path of the changed file or folder.
;                       OldName: The previous path in case of renaming, otherwise not used.
;                       IsDir:   True if Name is a directory; otherwise False. In case of Action 2 (removed) IsDir is always False.
;                    Pass the string "**DEL" to remove the directory from the list of watched folders.
;     SubTree     -  Set to true if you want the whole subtree to be watched (i.e. the contents of all sub-folders).
;                    Default: False - sub-folders aren't watched.
;     Watch       -  The kind of changes to watch for. This can be one or any combination of the FILE_NOTIFY_CHANGES_...
;                    values specified below.
;                    Default: 0x03 - FILE_NOTIFY_CHANGE_FILE_NAME + FILE_NOTIFY_CHANGE_DIR_NAME
; Return values:
;     Returns True on success; otherwise False.
; Change history:
;     1.0.02.00/2016-11-30/just me        -  bug-fix for closing handles with the '**END' option.
;     1.0.01.00/2016-03-14/just me        -  bug-fix for multiple folders
;     1.0.00.00/2015-06-21/just me        -  initial release
; License:
;     The Unlicense -> http://unlicense.org/
; Remarks:
;     Due to the limits of the API function WaitForMultipleObjects() you cannot watch more than MAXIMUM_WAIT_OBJECTS (64)
;     folders simultaneously.
; MSDN:
;     ReadDirectoryChangesW          msdn.microsoft.com/en-us/library/aa365465(v=vs.85).aspx
;     FILE_NOTIFY_CHANGE_FILE_NAME   = 1   (0x00000001) : Notify about renaming, creating, or deleting a file.
;     FILE_NOTIFY_CHANGE_DIR_NAME    = 2   (0x00000002) : Notify about creating or deleting a directory.
;     FILE_NOTIFY_CHANGE_ATTRIBUTES  = 4   (0x00000004) : Notify about attribute changes.
;     FILE_NOTIFY_CHANGE_SIZE        = 8   (0x00000008) : Notify about any file-size change.
;     FILE_NOTIFY_CHANGE_LAST_WRITE  = 16  (0x00000010) : Notify about any change to the last write-time of files.
;     FILE_NOTIFY_CHANGE_LAST_ACCESS = 32  (0x00000020) : Notify about any change to the last access time of files.
;     FILE_NOTIFY_CHANGE_CREATION    = 64  (0x00000040) : Notify about any change to the creation time of files.
;     FILE_NOTIFY_CHANGE_SECURITY    = 256 (0x00000100) : Notify about any security-descriptor change.
;     FILE_NOTIFY_INFORMATION        msdn.microsoft.com/en-us/library/aa364391(v=vs.85).aspx
;     FILE_ACTION_ADDED              = 1   (0x00000001) : The file was added to the directory.
;     FILE_ACTION_REMOVED            = 2   (0x00000002) : The file was removed from the directory.
;     FILE_ACTION_MODIFIED           = 3   (0x00000003) : The file was modified.
;     FILE_ACTION_RENAMED            = 4   (0x00000004) : The file was renamed (not defined by Microsoft).
;     FILE_ACTION_RENAMED_OLD_NAME   = 4   (0x00000004) : The file was renamed and this is the old name.
;     FILE_ACTION_RENAMED_NEW_NAME   = 5   (0x00000005) : The file was renamed and this is the new name.
;     GetOverlappedResult            msdn.microsoft.com/en-us/library/ms683209(v=vs.85).aspx
;     CreateFile                     msdn.microsoft.com/en-us/library/aa363858(v=vs.85).aspx
;     FILE_FLAG_BACKUP_SEMANTICS     = 0x02000000
;     FILE_FLAG_OVERLAPPED           = 0x40000000
; ================================================================================================================================
WatchFolder(Folder, UserFunc, SubTree := False, Watch := 0x03) {
   Static DummyObject := {Base: {__Delete: Func("WatchFolder").Bind("**END", "")}}
   Static TimerID := "**" . A_TickCount
   Static TimerFunc := Func("WatchFolder").Bind(TimerID, "")
   Static MAXIMUM_WAIT_OBJECTS := 64
   Static MAX_DIR_PATH := 260 - 12 + 1
   Static SizeOfLongPath := MAX_DIR_PATH << !!A_IsUnicode
   Static SizeOfFNI := 0xFFFF ; size of the FILE_NOTIFY_INFORMATION structure buffer (64 KB)
   Static SizeOfOVL := 32     ; size of the OVERLAPPED structure (64-bit)
   Static WatchedFolders := {}
   Static EventArray := []
   Static HandleArray := []
   Static WaitObjects := 0
   Static BytesRead := 0
   Static Paused := False
   ; ===============================================================================================================================
   If (Folder = "")
      Return False
   SetTimer, % TimerFunc, Off
   RebuildWaitObjects := False
   ; ===============================================================================================================================
   If (Folder = TimerID) { ; called by timer
      If (ObjCount := EventArray.Length()) && !Paused {
         ObjIndex := DllCall("WaitForMultipleObjects", "UInt", ObjCount, "Ptr", &WaitObjects, "Int", 0, "UInt", 0, "UInt")
         While (ObjIndex >= 0) && (ObjIndex < ObjCount) {
            FolderName := WatchedFolders[ObjIndex + 1]
            D := WatchedFolders[FolderName]
            If DllCall("GetOverlappedResult", "Ptr", D.Handle, "Ptr", D.OVLAddr, "UIntP", BytesRead, "Int", True) {
               Changes := []
               FNIAddr := D.FNIAddr
               FNIMax := FNIAddr + BytesRead
               OffSet := 0
               PrevIndex := 0
               PrevAction := 0
               PrevName := ""
               Loop {
                  FNIAddr += Offset
                  OffSet := NumGet(FNIAddr + 0, "UInt")
                  Action := NumGet(FNIAddr + 4, "UInt")
                  Length := NumGet(FNIAddr + 8, "UInt") // 2
                  Name   := FolderName . "\" . StrGet(FNIAddr + 12, Length, "UTF-16")
                  IsDir  := InStr(FileExist(Name), "D") ? 1 : 0
                  If (Name = PrevName) {
                     If (Action = PrevAction)
                        Continue
                     If (Action = 1) && (PrevAction = 2) {
                        PrevAction := Action
                        Changes.RemoveAt(PrevIndex--)
                        Continue
                     }
                  }
                  If (Action = 4)
                     PrevIndex := Changes.Push({Action: Action, OldName: Name, IsDir: 0})
                  Else If (Action = 5) && (PrevAction = 4) {
                     Changes[PrevIndex, "Name"] := Name
                     Changes[PrevIndex, "IsDir"] := IsDir
                  }
                  Else
                     PrevIndex := Changes.Push({Action: Action, Name: Name, IsDir: IsDir})
                  PrevAction := Action
                  PrevName := Name
               } Until (Offset = 0) || ((FNIAddr + Offset) > FNIMax)
               If (Changes.Length() > 0)
                  D.Func.Call(FolderName, Changes)
               DllCall("ResetEvent", "Ptr", EventArray[D.Index])
               DllCall("ReadDirectoryChangesW", "Ptr", D.Handle, "Ptr", D.FNIAddr, "UInt", SizeOfFNI, "Int", D.SubTree
                                              , "UInt", D.Watch, "UInt", 0, "Ptr", D.OVLAddr, "Ptr", 0)
            }
            ObjIndex := DllCall("WaitForMultipleObjects", "UInt", ObjCount, "Ptr", &WaitObjects, "Int", 0, "UInt", 0, "UInt")
            Sleep, 0
         }
      }
   }
   ; ===============================================================================================================================
   Else If (Folder = "**PAUSE") { ; called to pause/resume watching
      Paused := !!UserFunc
      RebuildObjects := Paused
   }
   ; ===============================================================================================================================
   Else If (Folder = "**END") { ; called to stop watching
      For K, D In WatchedFolders
         If K Is Not Integer
            DllCall("CloseHandle", "Ptr", D.Handle)
      For Each, Event In EventArray
         DllCall("CloseHandle", "Ptr", Event)
      WatchedFolders := {}
      EventArray := []
      Paused := False
      Return True
   }
   ; ===============================================================================================================================
   Else { ; called to add, update, or remove folders
      Folder := RTrim(Folder, "\")
      VarSetCapacity(LongPath, SizeOfLongPath, 0)
      If !DllCall("GetLongPathName", "Str", Folder, "Ptr", &LongPath, "UInt", SizeOfLongPath)
         Return False
      VarSetCapacity(LongPath, -1)
      Folder := LongPath
      If (WatchedFolders[Folder]) { ; update or remove
         Handle := WatchedFolders[Folder, "Handle"]
         Index  := WatchedFolders[Folder, "Index"]
         DllCall("CloseHandle", "Ptr", Handle)
         DllCall("CloseHandle", "Ptr", EventArray[Index])
         EventArray.RemoveAt(Index)
         WatchedFolders.RemoveAt(Index)
         WatchedFolders.Delete(Folder)
         RebuildWaitObjects := True
      }
      If InStr(FileExist(Folder), "D") && (UserFunc <> "**DEL") && (EventArray.Length() < MAXIMUM_WAIT_OBJECTS) {
         If (IsFunc(UserFunc) && (UserFunc := Func(UserFunc)) && (UserFunc.MinParams >= 2)) && (Watch &= 0x017F) {
            Handle := DllCall("CreateFile", "Str", Folder . "\", "UInt", 0x01, "UInt", 0x07, "Ptr",0, "UInt", 0x03
                                          , "UInt", 0x42000000, "Ptr", 0, "UPtr")
            If (Handle > 0) {
               Event := DllCall("CreateEvent", "Ptr", 0, "Int", 1, "Int", 0, "Ptr", 0)
               Index := EventArray.Push(Event)
               WatchedFolders[Index] := Folder
               WatchedFolders[Folder] := {Func: UserFunc, Handle: Handle, Index: Index, SubTree: !!SubTree, Watch: Watch}
               WatchedFolders[Folder].SetCapacity("FNIBuff", SizeOfFNI)
               FNIAddr := WatchedFolders[Folder].GetAddress("FNIBuff")
               DllCall("RtlZeroMemory", "Ptr", FNIAddr, "Ptr", SizeOfFNI)
               WatchedFolders[Folder, "FNIAddr"] := FNIAddr
               WatchedFolders[Folder].SetCapacity("OVLBuff", SizeOfOVL)
               OVLAddr := WatchedFolders[Folder].GetAddress("OVLBuff")
               DllCall("RtlZeroMemory", "Ptr", OVLAddr, "Ptr", SizeOfOVL)
               NumPut(Event, OVLAddr + 8, A_PtrSize * 2, "Ptr")
               WatchedFolders[Folder, "OVLAddr"] := OVLAddr
               DllCall("ReadDirectoryChangesW", "Ptr", Handle, "Ptr", FNIAddr, "UInt", SizeOfFNI, "Int", SubTree
                                              , "UInt", Watch, "UInt", 0, "Ptr", OVLAddr, "Ptr", 0)
               RebuildWaitObjects := True
            }
         }
      }
      If (RebuildWaitObjects) {
         VarSetCapacity(WaitObjects, MAXIMUM_WAIT_OBJECTS * A_PtrSize, 0)
         OffSet := &WaitObjects
         For Index, Event In EventArray
            Offset := NumPut(Event, Offset + 0, 0, "Ptr")
      }
   }
   ; ===============================================================================================================================
   If (EventArray.Length() > 0)
      SetTimer, % TimerFunc, -100
   Return (RebuildWaitObjects) ; returns True on success, otherwise False
}
just me
Posts: 7402
Joined: 02 Oct 2013, 08:51
Location: Germany

Re: Only trigger a function in the last iteration of a forEach loop

27 Aug 2020, 10:00

The Changes object passed to the user function is a simple array containing associative arrays with exactly three keys: Action, Name, and IsDir.

Code: Select all

; WatchFolder - Reportfunctions
ReportFunction(Directory, Changes) { 
 For Each, Change In Changes {
       Action := Change.Action
       Name := Change.Name
       idx := A_Index
       for i, e in Change {   ; embedded For-Loop
	   ; -------------------------------------------------------------------------------------------------------------------------
        ; Action 1 (added) = File gets added in the watched folder
	   If (Action = 1)  {
             if (idx = Changes.Count() && A_Index = Change.Count())
               MsgBox, % "The last iterated item was " e
             }
        }
  }
}
Your embedded For-Loop doesn't make sense for me. What are you trying to achieve?
User avatar
boiler
Posts: 6589
Joined: 21 Dec 2014, 02:44

Re: Only trigger a function in the last iteration of a forEach loop

27 Aug 2020, 10:29

I think I understand the goal here and saw the same thing as I tried to reproduce it. If I drag a group of files into that directory, I want to capture or act on only the “last” of those files. However, I am seeing what majstang has described, which is that more than one event is triggered by WatchFolder when multiple files are dragged into the folder, thus ReportFunction() is called twice, each with its own object. It appears to be called one time with one file (the one Windows considers the selected file, perhaps?) and another time with the rest of the files that were also part of the overall group of dragged files.

Because two events are generated from a single action of dragging and dropping files into the directory, it’s not possible to only grab the “last” of the group of dragged files. You will get one from the first triggered event and another from the next triggered event.
majstang
Posts: 89
Joined: 11 Feb 2018, 03:39

Re: Only trigger a function in the last iteration of a forEach loop

27 Aug 2020, 12:01

Thanks boiler for translating the chain of events accurately into programmers lingo :thumbup: Im just a interested user and haven't got a wider understanding of AHK yet. As proof of fact I obviously failed to pinpoint the correct Object type :oops:
boiler wrote:
27 Aug 2020, 10:29
(the one Windows considers the selected file, perhaps?)
That's correct. The selected file in the selected group (the one the mousepointer is at when dragging) always gets it's own exclusive object.
majstang
Posts: 89
Joined: 11 Feb 2018, 03:39

Re: Only trigger a function in the last iteration of a forEach loop

28 Aug 2020, 06:21

A thought to prevent WatchFolder() from calling the user function twice with a different object each time...what if merging the multiple objects before user function is called!? Testing that thought failed right off the bat, because merging objects with same key names isn't that easy. Or did I do something wrong?

Code: Select all

Changes1 := [{Action: "1", Name: "C:\AHK-Scripts\lib\file1.txt", IsDir: "0"}]

Changes2 := [{Action: "1", Name: "C:\AHK-Scripts\lib\file2.txt", IsDir: "0"}
          , {Action: "1", Name: "C:\AHK-Scripts\lib\file3.txt", IsDir: "0"}
		, {Action: "1", Name: "C:\AHK-Scripts\lib\file4.txt", IsDir: "0"}]

MergeChanges := Changes1.Clone()
for x,y in Changes2
	MergeChanges[x] := y
dmp(Changes1, Changes2, MergeChanges)
swagfag
Posts: 4070
Joined: 11 Jan 2017, 17:59

Re: Only trigger a function in the last iteration of a forEach loop

28 Aug 2020, 06:59

pool callbacks arriving in a short time frame, defer their handling until later on

Code: Select all

ReportFunction(Directory, Changes) {
	static Events := {}

	if Events.HasKey(Directory)
		Events[Directory].Push(Changes*)
	else
		Events[Directory] := Changes

	SetTimer ProcessEvents, -100
	return

	ProcessEvents:
		for dir, Changes in Events
		{
			for idx, Change in Changes
			{
				Action := Change.Action
				Name := Change.Name
				IsDir := Change.IsDir

				if (idx = Changes.Count())
					MsgBox, % "The last iterated item was " Name
			}
		}

		Events := {}
	return
}
majstang
Posts: 89
Joined: 11 Feb 2018, 03:39

Re: Only trigger a function in the last iteration of a forEach loop

28 Aug 2020, 07:28

Not bad...that is working great for Action 1 (add files/folders) :D Thanks :) Do you think this object manipulation workaround might break functionallity if I use it for other Actions as well? Perhaps this is how the UserFunc should have looked like all along ;)

I added the Action 1 condition in your solution, which you did remove:

Code: Select all

; WatchFolder - Reportfunctions
ReportFunction(Directory, Changes) {
	static Events := {}
	
	if Events.HasKey(Directory)
		Events[Directory].Push(Changes*)
	else
		Events[Directory] := Changes
	
	SetTimer ProcessEvents, -100
	return
	
	ProcessEvents:
	for dir, Changes in Events
	{
		for idx, Change in Changes
		{
			Action := Change.Action
			Name := Change.Name
			IsDir := Change.IsDir
			
			; Action 1 (added) = File gets added in the watched folder
			If (Action = 1)  {
				if (idx = Changes.Count())
					MsgBox, % "The last iterated item was " Name
			}
		}
	}
	
	Events := {}
	return
}

Return to “Ask For Help”

Who is online

Users browsing this forum: Bing [Bot], c7aesa7r, Google [Bot], mcl, svArtist, zKade and 54 guests