String.ahk, Array.ahk, Misc.ahk

Post your working scripts, libraries and tools.
Descolada
Posts: 1138
Joined: 23 Dec 2021, 02:30

String.ahk, Array.ahk, Misc.ahk

Post by Descolada » 09 Sep 2022, 13:19

All libraries also available at GitHub

String.ahk
Heavily based on String Things by tidbit.

A compilation of useful string methods. Also lets strings be treated as objects.

Examples:

Code: Select all

"test".Length ; returns 4

str := "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."

MsgBox("First character: " str[1])
MsgBox("Substring for characters 2-7: " str[2,7])
MsgBox("Substring starting from character 10: " str[10,0])

MsgBox("Reversed: " str.Reverse())
MsgBox("Word wrapped: `n" str.WordWrap())
MsgBox("Concatenate: " "; ".Concat("First", "second", 123))
Library:

Code: Select all

/*
	Name: String.ahk
	Version 0.14 (23.03.2023)
	Created: 27.08.22
	Author: Descolada
	Credit:
	tidbit		--- Author of "String Things - Common String & Array Functions", from which
					I copied/based a lot of methods
	Contributors: Axlefublr, neogna2
	Contributors to "String Things": AfterLemon, Bon, Lexikos, MasterFocus, Rseding91, Verdlin

	Description:
	A compilation of useful string methods. Also lets strings be treated as objects.

	These methods cannot be used as stand-alone. To do that, you must add another argument
	'string' to the function and replace all occurrences of 'this' with 'string'.
	.-==========================================================================-.
	| Properties                                                                 |
	|============================================================================|
	| String.Length                                                              |
	|       .IsDigit                                                             |
	|       .IsXDigit                                                            |
	|       .IsAlpha                                                             |
	|       .IsUpper                                                             |
	|       .IsLower                                                             |
	|       .IsAlnum                                                             |
	|       .IsSpace                                                             |
	|       .IsTime                                                              |
	|============================================================================|
	| Methods                                                                    |
	|============================================================================|
	| Native functions as methods:                                               |
	| String.ToUpper()                                                           |
	|       .ToLower()                                                           |
	|       .ToTitle()                                                           |
	|       .Split([Delimiters, OmitChars, MaxParts])                            |
	|       .Replace(Needle [, ReplaceText, CaseSense, &OutputVarCount, Limit])  |
	|       .Trim([OmitChars])                                                   |
	|       .LTrim([OmitChars])                                                  |
	|       .RTrim([OmitChars])                                                  |
	|       .Compare(comparison [, CaseSense])                                   |
	|       .Sort([, Options, Function])                                         |
	|       .Format([Values...])                                                 |
	|       .Find(Needle [, CaseSense, StartingPos, Occurrence])                 |
	|       .SplitPath() => returns object {FileName, Dir, Ext, NameNoExt, Drive}|                                                       |
	|		.RegExMatch(needleRegex, &match?, startingPos?)                      |
	|       .RegExMatchAll(needleRegex, startingPos?)                            |
	|		.RegExReplace(needle, replacement?, &count?, limit?, startingPos?)   |
	|                                                                            |
	| String[n] => gets nth character                                            |
	| String[i,j] => substring from i to j                                       |
	| for [index,] char in String => loops over the characters in String         |
	| String.Length                                                              |
	| String.Count(searchFor)                                                    |
	| String.Insert(insert, into [, pos])                                        |
	| String.Delete(string [, start, length])                                    |
	| String.Overwrite(overwrite, into [, pos])                                  |
	| String.Repeat(count)                                                       |
	| Delimeter.Concat(words*)                                                   |
	|                                                                            |
	| String.LineWrap([column:=56, indentChar:=""])                              |
	| String.WordWrap([column:=56, indentChar:=""])                              |
	| String.ReadLine(line [, delim:="`n", exclude:="`r"])                       |
	| String.DeleteLine(line [, delim:="`n", exclude:="`r"])                     |
	| String.InsertLine(insert, into, line [, delim:="`n", exclude:="`r"])       |
	|                                                                            |
	| String.Reverse()                                                           |
	| String.Contains(needle1 [, needle2, needle3...])                           |
	| String.RemoveDuplicates([delim:="`n"])                                     |
	| String.LPad(count)                                                         |
	| String.RPad(count)                                                         |
	|                                                                            |
	| String.Center([fill:=" ", symFill:=0, delim:="`n", exclude:="`r", width])  |
	| String.Right([fill:=" ", delim:="`n", exclude:="`r"])                      |
	'-==========================================================================-'
*/
Class String2 {
	static __New() {
		; Add String2 methods and properties into String object
		__ObjDefineProp := Object.Prototype.DefineProp
		for __String2_Prop in String2.OwnProps()
			if SubStr(__String2_Prop, 1, 2) != "__"
				__ObjDefineProp(String.Prototype, __String2_Prop, String2.GetOwnPropDesc(__String2_Prop))
		__ObjDefineProp(String.Prototype, "__Item", {get:(args*)=>String2.__Item[args*]})
		__ObjDefineProp(String.Prototype, "__Enum", {call:String2.__Enum})
	}

	static __Item[args*] {
		get {
			if args.length = 2
				return SubStr(args[1], args[2], 1)
			else {
				len := StrLen(args[1])
				if args[2] < 0
					args[2] := len+args[2]+1
				if args[3] < 0
					args[3] := len+args[3]+1
				if args[3] >= args[2]
					return SubStr(args[1], args[2], args[3]-args[2]+1)
				else
					return SubStr(args[1], args[3], args[2]-args[3]+1).Reverse()
			}
		}
	}

	static __Enum(varCount) {
		pos := 0, len := StrLen(this)
		EnumElements(&char) {
			char := StrGet(StrPtr(this) + 2*pos, 1)
			return ++pos <= len
		}
		
		EnumIndexAndElements(&index, &char) {
			char := StrGet(StrPtr(this) + 2*pos, 1), index := ++pos
			return pos <= len
		}

		return varCount = 1 ? EnumElements : EnumIndexAndElements
	}
	; Native functions implemented as methods for the String object
	static Length    	  => StrLen(this)
	static WLength        => (RegExReplace(this, "s).", "", &i), i)
	static ULength        => StrLen(RegExReplace(this, "s)((?>\P{M}(\p{M}|\x{200D}))+\P{M})|\X", "_"))
	static IsDigit		  => IsDigit(this)
	static IsXDigit		  => IsXDigit(this)
	static IsAlpha		  => IsAlpha(this)
	static IsUpper		  => IsUpper(this)
	static IsLower		  => IsLower(this)
	static IsAlnum		  => IsAlnum(this)
	static IsSpace		  => IsSpace(this)
	static IsTime		  => IsTime(this)
	static ToUpper()      => StrUpper(this)
	static ToLower()      => StrLower(this)
	static ToTitle()      => StrTitle(this)
	static Split(args*)   => StrSplit(this, args*)
	static Replace(args*) => StrReplace(this, args*)
	static Trim(args*)    => Trim(this, args*)
	static LTrim(args*)   => LTrim(this, args*)
	static RTrim(args*)   => RTrim(this, args*)
	static Compare(args*) => StrCompare(this, args*)
	static Sort(args*)    => Sort(this, args*)
	static Format(args*)  => Format(this, args*)
	static Find(args*)    => InStr(this, args*)
	static SplitPath() 	  => (SplitPath(this, &a1, &a2, &a3, &a4, &a5), {FileName: a1, Dir: a2, Ext: a3, NameNoExt: a4, Drive: a5})
	/**
	 * Returns the match object
	 * @param needleRegex *String* What pattern to match
	 * @param startingPos *Integer* Specify a number to start matching at. By default, starts matching at the beginning of the string
	 * @returns {Object}
	 */
	static RegExMatch(needleRegex, &match?, startingPos?) => (RegExMatch(this, needleRegex, &match, startingPos?), match)
	/**
	* Returns all RegExMatch results in an array: [RegExMatchInfo1, RegExMatchInfo2, ...]
	* @param needleRegEx *String* The RegEx pattern to search for.
	* @param startingPosition *Integer* If StartingPos is omitted, it defaults to 1 (the beginning of haystack).
	* @returns {Array}
	*/
	static RegExMatchAll(needleRegEx, startingPosition := 1) {
		out := []
		While startingPosition := RegExMatch(this, needleRegEx, &outputVar, startingPosition)
			out.Push(outputVar), startingPosition += outputVar[0] ? StrLen(outputVar[0]) : 1
		return out
	}
	 /**
	  * Uses regex to perform a replacement, returns the changed string
	  * @param needleRegex *String* What pattern to match.
	  * 	This can also be a Array of needles (and replacement a corresponding array of replacement values), 
	  * 	in which case all of the pairs will be searched for and replaced with the corresponding replacement. 
	  * 	replacement should be left empty, outputVarCount will be set to the total number of replacements, limit is the maximum
	  * 	number of replacements for each needle-replacement pair.
	  * @param replacement *String* What to replace that match into
	  * @param outputVarCount *VarRef* Specify a variable with a `&` before it to assign it to the amount of replacements that have occured
	  * @param limit *Integer* The maximum amount of replacements that can happen. Unlimited by default
	  * @param startingPos *Integer* Specify a number to start matching at. By default, starts matching at the beginning of the string
	  * @returns {String} The changed string
	  */
	static RegExReplace(needleRegex, replacement?, &outputVarCount?, limit?, startingPos?) {
		if IsObject(needleRegex) {
			out := this, count := 0
			for i, needle in needleRegex {
				out := RegExReplace(out, needle, IsSet(replacement) ? replacement[i] : unset, &count, limit?, startingPos?)
				if IsSet(outputVarCount)
					outputVarCount += count
			}
			return out
		}
		return RegExReplace(this, needleRegex, replacement?, &outputVarCount?, limit?, startingPos?)
	}
	/**
	 * Add character(s) to left side of the input string.
	 * example: "aaa".LPad("+", 5)
	 * output: +++++aaa
	 * @param padding Text you want to add
	 * @param count How many times do you want to repeat adding to the left side.
	 * @returns {String}
	 */
	static LPad(padding, count:=1) {
		str := this
		if (count>0) {
			Loop count
				str := padding str
		}
		return str
	}

	/**
	 * Add character(s) to right side of the input string.
	 * example: "aaa".RPad("+", 5)
	 * output: aaa+++++
	 * @param padding Text you want to add
	 * @param count How many times do you want to repeat adding to the left side.
	 * @returns {String}
	 */
	static RPad(padding, count:=1) {
		str := this
		if (count>0) {
			Loop count
				str := str padding
		}
		return str
	}

	/**
	 * Count the number of occurrences of needle in the string
	 * input: "12234".Count("2")
	 * output: 2
	 * @param needle Text to search for
	 * @param caseSensitive
	 * @returns {Integer}
	 */
	static Count(needle, caseSensitive:=False) {
		StrReplace(this, needle,, caseSensitive, &count)
		return count
	}

	/**
	 * Duplicate the string 'count' times.
	 * input: "abc".Repeat(3)
	 * output: "abcabcabc"
	 * @param count *Integer*
	 * @returns {String}
	 */
	static Repeat(count) => StrReplace(Format("{:" count "}",""), " ", this)

	/**
	 * Reverse the string.
	 * @returns {String}
	 */
	static Reverse() {
		DllCall("msvcrt\_wcsrev", "str", str := this, "CDecl str")
		return str
	}
	static WReverse() {
		str := this, out := "", m := ""
		While str && (m := Chr(Ord(str))) && (out := m . out)
			str := SubStr(str,StrLen(m)+1)
		return out
	}

	/**
	 * Insert the string inside 'insert' into position 'pos'
	 * input: "abc".Insert("d", 2)
	 * output: "adbc"
	 * @param insert The text to insert
	 * @param pos *Integer*
	 * @returns {String}
	 */
	static Insert(insert, pos:=1) {
		Length := StrLen(this)
		((pos > 0)
			? pos2 := pos - 1
			: (pos = 0
				? (pos2 := StrLen(this), Length := 0)
				: pos2 := pos
				)
		)
		output := SubStr(this, 1, pos2) . insert . SubStr(this, pos, Length)
		if (StrLen(output) > StrLen(this) + StrLen(insert))
			((Abs(pos) <= StrLen(this)/2)
				? (output := SubStr(output, 1, pos2 - 1)
					. SubStr(output, pos + 1, StrLen(this))
				)
				: (output := SubStr(output, 1, pos2 - StrLen(insert) - 2)
					. SubStr(output, pos - StrLen(insert), StrLen(this))
				)
			)
		return output
	}

	/**
	 * Replace part of the string with the string in 'overwrite' starting from position 'pos'
	 * input: "aaabbbccc".Overwrite("zzz", 4)
	 * output: "aaazzzccc"
	 * @param overwrite Text to insert.
	 * @param pos The position where to begin overwriting. 0 may be used to overwrite at the very end, -1 will offset 1 from the end, and so on.
	 * @returns {String}
	 */
	static Overwrite(overwrite, pos:=1) {
		if (Abs(pos) > StrLen(this))
			return ""
		else if (pos>0)
			return SubStr(this, 1, pos-1) . overwrite . SubStr(this, pos+StrLen(overwrite))
		else if (pos<0)
			return SubStr(this, 1, pos) . overwrite . SubStr(this " ",(Abs(pos) > StrLen(overwrite) ? pos+StrLen(overwrite) : 0), Abs(pos+StrLen(overwrite)))
		else if (pos=0)
			return this . overwrite
	}

	/**
	 * Delete a range of characters from the specified string.
	 * input: "aaabbbccc".Delete(4, 3)
	 * output: "aaaccc"
	 * @param start The position where to start deleting.
	 * @param length How many characters to delete.
	 * @returns {String}
	 */
	static Delete(start:=1, length:=1) {
		if (Abs(start) > StrLen(this))
			return ""
		if (start>0)
			return SubStr(this, 1, start-1) . SubStr(this, start + length)
		else if (start<=0)
			return SubStr(this " ", 1, start-1) SubStr(this " ", ((start<0) ? start-1+length : 0), -1)
	}

	/**
	 * Wrap the string so each line is never more than a specified length.
	 * input: "Apples are a round fruit, usually red".LineWrap(20, "---")
	 * output: "Apples are a round f
	 *          ---ruit, usually red"
	 * @param column Specify a maximum length per line
	 * @param indentChar Choose a character to indent the following lines with
	 * @returns {String}
	 */
	static LineWrap(column:=56, indentChar:="") {
		CharLength := StrLen(indentChar)
		, columnSpan := column - CharLength
		, Ptr := A_PtrSize ? "Ptr" : "UInt"
		, UnicodeModifier := 2
		, VarSetStrCapacity(&out, (finalLength := (StrLen(this) + (Ceil(StrLen(this) / columnSpan) * (column + CharLength + 1))))*2)
		, A := StrPtr(out)

		Loop parse, this, "`n", "`r" {
			if ((FieldLength := StrLen(ALoopField := A_LoopField)) > column) {
				DllCall("RtlMoveMemory", "Ptr", A, "ptr", StrPtr(ALoopField), "UInt", column * UnicodeModifier)
				, A += column * UnicodeModifier
				, NumPut("UShort", 10, A)
				, A += UnicodeModifier
				, Pos := column

				While (Pos < FieldLength) {
					if CharLength
						DllCall("RtlMoveMemory", "Ptr", A, "ptr", StrPtr(indentChar), "UInt", CharLength * UnicodeModifier)
						, A += CharLength * UnicodeModifier

					if (Pos + columnSpan > FieldLength)
						DllCall("RtlMoveMemory", "Ptr", A, "ptr", StrPtr(ALoopField) + (Pos * UnicodeModifier), "UInt", (FieldLength - Pos) * UnicodeModifier)
						, A += (FieldLength - Pos) * UnicodeModifier
						, Pos += FieldLength - Pos
					else
						DllCall("RtlMoveMemory", "Ptr", A, "ptr", StrPtr(ALoopField) + (Pos * UnicodeModifier), "UInt", columnSpan * UnicodeModifier)
						, A += columnSpan * UnicodeModifier
						, Pos += columnSpan

					NumPut("UShort", 10, A)
					, A += UnicodeModifier
				}
			} else
				DllCall("RtlMoveMemory", "Ptr", A, "ptr", StrPtr(ALoopField), "UInt", FieldLength * UnicodeModifier)
				, A += FieldLength * UnicodeModifier
				, NumPut("UShort", 10, A)
				, A += UnicodeModifier
		}
		NumPut("UShort", 0, A)
		VarSetStrCapacity(&out, -1)
		return SubStr(out,1, -1)
	}

	/**
	 * Wrap the string so each line is never more than a specified length.
	 * Unlike LineWrap(), this method takes into account words separated by a space.
	 * input: "Apples are a round fruit, usually red.".WordWrap(20, "---")
	 * output: "Apples are a round
	 *          ---fruit, usually
	 *          ---red."
	 * @param column Specify a maximum length per line
	 * @param indentChar Choose a character to indent the following lines with
	 * @returns {String}
	 */
	static WordWrap(column:=56, indentChar:="") {
		if !IsInteger(column)
			throw TypeError("WordWrap: argument 'column' must be an integer", -1)
		out := ""
		indentLength := StrLen(indentChar)

		Loop parse, this, "`n", "`r" {
			if (StrLen(A_LoopField) > column) {
				pos := 1
				Loop parse, A_LoopField, " "
					if (pos + (LoopLength := StrLen(A_LoopField)) <= column)
						out .= (A_Index = 1 ? "" : " ") A_LoopField
						, pos += LoopLength + 1
					else
						pos := LoopLength + 1 + indentLength
						, out .= "`n" indentChar A_LoopField

				out .= "`n"
			} else
				out .= A_LoopField "`n"
		}
		return SubStr(out, 1, -1)
	}

	/**
	* Insert a line of text at the specified line number.
	* The line you specify is pushed down 1 and your text is inserted at its
	* position. A "line" can be determined by the delimiter parameter. Not
	* necessarily just a `r or `n. But perhaps you want a | as your "line".
	* input: "aaa|ccc|ddd".InsertLine("bbb", 2, "|")
	* output: "aaa|bbb|ccc|ddd"
	* @param insert Text you want to insert.
	* @param line What line number to insert at. Use a 0 or negative to start inserting from the end.
	* @param delim The character which defines a "line".
	* @param exclude The text you want to ignore when defining a line.
	* @returns {String}
	 */
	static InsertLine(insert, line, delim:="`n", exclude:="`r") {
		if StrLen(delim) != 1
			throw ValueError("InsertLine: Delimiter can only be a single character", -1)
		into := this, new := ""
		count := into.Count(delim)+1

		; Create any lines that don't exist yet, if the Line is less than the total line count.
		if (line<0 && Abs(line)>count) {
			Loop Abs(line)-count
				into := delim into
			line:=1
		}
		if (line == 0)
			line:=Count+1
		if (line<0)
			line:=count+line+1
		; Create any lines that don't exist yet. Otherwise the Insert doesn't work.
		if (count<line)
			Loop line-count
				into.=delim

		Loop parse, into, delim, exclude
			new.=((a_index==line) ? insert . delim . A_LoopField . delim : A_LoopField . delim)

		return SubStr(new, 1, -(line > count ? 2 : 1))
	}

	/**
	 * Delete a line of text at the specified line number.
	 * The line you specify is deleted and all lines below it are shifted up.
	 * A "line" can be determined by the delimiter parameter. Not necessarily
	 * just a `r or `n. But perhaps you want a | as your "line".
	 * input: "aaa|bbb|777|ccc".DeleteLine(3, "|")
	 * output: "aaa|bbb|ccc"
	 * @param string Text you want to delete the line from.
	 * @param line What line to delete. You may use -1 for the last line and a negative an offset from the last. -2 would be the second to the last.
	 * @param delim The character which defines a "line".
	 * @param exclude The text you want to ignore when defining a line.
	 * @returns {String}
	 */
	static DeleteLine(line, delim:="`n", exclude:="`r") {
		if StrLen(delim) != 1
			throw ValueError("DeleteLine: Delimiter can only be a single character", -1)
		new := ""
		; checks to see if we are trying to delete a non-existing line.
		count:=this.Count(delim)+1
		if (abs(line)>Count)
			throw ValueError("DeleteLine: the line number cannot be greater than the number of lines", -1)
		if (line<0)
			line:=count+line+1
		else if (line=0)
			throw ValueError("DeleteLine: line number cannot be 0", -1)

		Loop parse, this, delim, exclude {
			if (a_index==line) {
				Continue
			} else
				(new .= A_LoopField . delim)
		}

		return SubStr(new,1,-1)
	}

	/**
	 * Read the content of the specified line in a string. A "line" can be
	 * determined by the delimiter parameter. Not necessarily just a `r or `n.
	 * But perhaps you want a | as your "line".
	 * input: "aaa|bbb|ccc|ddd|eee|fff".ReadLine(4, "|")
	 * output: "ddd"
	 * @param line What line to read*. "L" = The last line. "R" = A random line. Otherwise specify a number to get that line. You may specify a negative number to get the line starting from the end. -1 is the same as "L", the last. -2 would be the second to the last, and so on.
	 * @param delim The character which defines a "line".
	 * @param exclude The text you want to ignore when defining a line.
	 * @returns {String}
	 */
	static ReadLine(line, delim:="`n", exclude:="`r") {
		out := "", count:=this.Count(delim)+1

		if (line="R")
			line := Random(1, count)
		else if (line="L")
			line := count
		else if abs(line)>Count
			throw ValueError("ReadLine: the line number cannot be greater than the number of lines", -1)
		else if (line<0)
			line:=count+line+1
		else if (line=0)
			throw ValueError("ReadLine: line number cannot be 0", -1)

		Loop parse, this, delim, exclude {
			if A_Index = line
				return A_LoopField
		}
		throw Error("ReadLine: something went wrong, the line was not found", -1)
	}

	/**
	 * Replace all consecutive occurrences of 'delim' with only one occurrence.
	 * input: "aaa|bbb|||ccc||ddd".RemoveDuplicates("|")
	 * output: "aaa|bbb|ccc|ddd"
	 * @param delim *String*
	 */
	static RemoveDuplicates(delim:="`n") => RegExReplace(this, "(\Q" delim "\E)+", "$1")

	/**
	 * Checks whether the string contains any of the needles provided.
	 * input: "aaa|bbb|ccc|ddd".Contains("eee", "aaa")
	 * output: 1 (although the string doesn't contain "eee", it DOES contain "aaa")
	 * @param needles
	 * @returns {Boolean}
	 */
	static Contains(needles*) {
		for needle in needles
			if InStr(this, needle)
				return 1
		return 0
	}

	/**
	 * Centers a block of text to the longest item in the string.
	 * example: "aaa`na`naaaaaaaa".Center()
	 * output: "aaa
	 *           a
	 *       aaaaaaaa"
	 * @param text The text you would like to center.
	 * @param fill A single character to use as the padding to center text.
	 * @param symFill 0: Just fill in the left half. 1: Fill in both sides.
	 * @param delim The character which defines a "line".
	 * @param exclude The text you want to ignore when defining a line.
	 * @param width Can be specified to add extra padding to the sides
	 * @returns {String}
	 */
	static Center(fill:=" ", symFill:=0, delim:="`n", exclude:="`r", width?) {
		fill:=SubStr(fill,1,1), longest := 0, new := ""
		Loop parse, this, delim, exclude
			if (StrLen(A_LoopField)>longest)
				longest := StrLen(A_LoopField)
		if IsSet(width)
			longest := Max(longest, width)
		Loop parse this, delim, exclude 
		{
			filled:="", len := StrLen(A_LoopField)
			Loop (longest-len)//2
				filled.=fill
			new .= filled A_LoopField ((symFill=1) ? filled (2*StrLen(filled)+len = longest ? "" : fill) : "") "`n"
		}
		return RTrim(new,"`r`n")
	}

	/**
	 * Align a block of text to the right side.
	 * input: "aaa`na`naaaaaaaa".Right()
	 * output: "     aaa
	 *                 a
	 *          aaaaaaaa"
	 * @param fill A single character to use as to push the text to the right.
	 * @param delim The character which defines a "line".
	 * @param exclude The text you want to ignore when defining a line.
	 * @returns {String}
	 */
	static Right(fill:=" ", delim:="`n", exclude:="`r") {
		fill:=SubStr(fill,1,1), longest := 0, new := ""
		Loop parse, this, delim, exclude
			if (StrLen(A_LoopField)>longest)
				longest:=StrLen(A_LoopField)
		Loop parse, this, delim, exclude {
			filled:=""
			Loop Abs(longest-StrLen(A_LoopField))
				filled.=fill
			new.= filled A_LoopField "`n"
		}
		return RTrim(new,"`r`n")
	}

	/**
	 * Join a list of strings together to form a string separated by delimiter this was called with.
	 * input: "|".Concat("111", "222", "333", "abc")
	 * output: "111|222|333|abc"
	 * @param words A list of strings separated by a comma.
	 * @returns {String}
	 */
	static Concat(words*) {
		delim := this, s := ""
		for v in words
			s .= v . delim
		return SubStr(s,1,-StrLen(this))
	}
}
Array.ahk

A compilation of useful array methods.

Examples:

Code: Select all

#include ..\Lib\Misc.ahk
#include ..\Lib\Array.ahk

Print([4,3,5,1,2].Sort(), MsgBox)
Print(["a", 2, 1.2, 1.22, 1.20].Sort("C")) ; Default is numeric sort, so specify case-sensitivity if the array contains strings.

myImmovables:=[]
myImmovables.push({town: "New York", size: "60", price: 400000, balcony: 1})
myImmovables.push({town: "Berlin", size: "45", price: 230000, balcony: 1})
myImmovables.push({town: "Moscow", size: "80", price: 350000, balcony: 0})
myImmovables.push({town: "Tokyo", size: "90", price: 600000, balcony: 2})
myImmovables.push({town: "Palma de Mallorca", size: "250", price: 1100000, balcony: 3})
Print(myImmovables.Sort("N R", "size")) ; Sort by the key 'size', treating the values as numbers, and sorting in reverse

Print(["dog", "cat", "mouse"].Map((v) => "a " v))

partial := [1,2,3,4,5].Map((a,b) => a+b)
Print("Array: [1,2,3,4,5]`n`nAdd 3 to first element: " partial[1](3) "`nSubtract 1 from third element: " partial[3](-1))

Print("Filter integer values from [1,'two','three',4,5]: " ([1,'two','three',4,5].Filter(IsInteger).Join()))

Print("Sum of elements in [1,2,3,4,5]: " ([1,2,3,4,5].Reduce((a,b) => (a+b))))

Print("First value in [1,2,3,4,5] that is an even number: " ([1,2,3,4,5].Find((v) => (Mod(v,2) == 0))))
Library:

Code: Select all

/*
	Name: Array.ahk
	Version 0.3 (24.03.23)
	Created: 27.08.22
	Author: Descolada

	Description:
	A compilation of useful array methods.

    Array.Slice(start:=1, end:=0, step:=1)  => Returns a section of the array from 'start' to 'end', 
        optionally skipping elements with 'step'.
    Array.Swap(a, b)                        => Swaps elements at indexes a and b.
    Array.Map(func, arrays*)                => Applies a function to each element in the array.
    Array.ForEach(func)                     => Calls a function for each element in the array.
    Array.Filter(func)                      => Keeps only values that satisfy the provided function
    Array.Reduce(func, initialValue?)       => Applies a function cumulatively to all the values in 
        the array, with an optional initial value.
    Array.IndexOf(value, start:=1)          => Finds a value in the array and returns its index.
    Array.Find(func, &match?, start:=1)     => Finds a value satisfying the provided function and returns the index.
        match will be set to the found value. 
    Array.Reverse()                         => Reverses the array.
    Array.Count(value)                      => Counts the number of occurrences of a value.
    Array.Sort(OptionsOrCallback?, Key?)    => Sorts an array, optionally by object values.
    Array.Shuffle()                         => Randomizes the array.
    Array.Join(delim:=",")                  => Joins all the elements to a string using the provided delimiter.
    Array.Flat()                            => Turns a nested array into a one-level array.
    Array.Extend(arr)                       => Adds the contents of another array to the end of this one.
*/

Array.Prototype.base := Array2

class Array2 {
    /**
     * Returns a section of the array from 'start' to 'end', optionally skipping elements with 'step'.
     * Modifies the original array.
     * @param start Optional: index to start from. Default is 1.
     * @param end Optional: index to end at. Can be negative. Default is 0 (includes the last element).
     * @param step Optional: an integer specifying the incrementation. Default is 1.
     * @returns {Array}
     */
    static Slice(start:=1, end:=0, step:=1) {
        len := this.Length, i := start < 1 ? len + start : start, j := Min(end < 1 ? len + end : end, len), r := [], reverse := False
        if len = 0
            return []
        if i < 1
            i := 1
        if step = 0
            Throw Error("Slice: step cannot be 0",-1)
        else if step < 0 {
            while i >= j {
                r.Push(this[i])
                i += step
            }
        } else {
            while i <= j {
                r.Push(this[i])
                i += step
            }
        }
        return this := r
    }
    /**
     * Swaps elements at indexes a and b
     * @param a First elements index to swap
     * @param b Second elements index to swap
     * @returns {Array}
     */
    static Swap(a, b) {
        temp := this[b]
        this[b] := this[a]
        this[a] := temp
        return this
    }
    /**
     * Applies a function to each element in the array (mutates the array).
     * @param func The mapping function that accepts one argument.
     * @param arrays Additional arrays to be accepted in the mapping function
     * @returns {Array}
     */
    static Map(func, arrays*) {
        if !HasMethod(func)
            throw ValueError("Map: func must be a function", -1)
        for i, v in this {
            bf := func.Bind(v?)
            for _, vv in arrays
                bf := bf.Bind(vv.Has(i) ? vv[i] : unset)
            try bf := bf()
            this[i] := bf
        }
        return this
    }
    /**
     * Applies a function to each element in the array.
     * @param func The callback function with arguments Callback(value[, index, array]).
     * @returns {Array}
     */
    static ForEach(func) {
        if !HasMethod(func)
            throw ValueError("ForEach: func must be a function", -1)
        for i, v in this
            func(v, i, this)
        return this
    }
    /**
     * Keeps only values that satisfy the provided function
     * @param func The filter function that accepts one argument.
     * @returns {Array}
     */
    static Filter(func) {
        if !HasMethod(func)
            throw ValueError("Filter: func must be a function", -1)
        r := []
        for v in this
            if func(v)
                r.Push(v)
        return this := r
    }
    /**
     * Applies a function cumulatively to all the values in the array, with an optional initial value.
     * @param func The function that accepts two arguments and returns one value
     * @param initialValue Optional: the starting value. If omitted, the first value in the array is used.
     * @returns {func return type}
     * @example
     * [1,2,3,4,5].Reduce((a,b) => (a+b)) ; returns 15 (the sum of all the numbers)
     */
    static Reduce(func, initialValue?) {
        if !HasMethod(func)
            throw ValueError("Reduce: func must be a function", -1)
        len := this.Length + 1
        if len = 1
            return initialValue ?? ""
        if IsSet(initialValue)
            out := initialValue, i := 0
        else
            out := this[1], i := 1
        while ++i < len {
            out := func(out, this[i])
        }
        return out
    }
    /**
     * Finds a value in the array and returns its index.
     * @param value The value to search for.
     * @param start Optional: the index to start the search from. Default is 1.
     */
    static IndexOf(value, start:=1) {
        if !IsInteger(start)
            throw ValueError("IndexOf: start value must be an integer")
        for i, v in this {
            if i < start
                continue
            if v == value
                return i
        }
        return 0
    }
    /**
     * Finds a value satisfying the provided function and returns its index.
     * @param func The condition function that accepts one argument.
     * @param match Optional: is set to the found value
     * @param start Optional: the index to start the search from. Default is 1.
     * @example
     * [1,2,3,4,5].Find((v) => (Mod(v,2) == 0)) ; returns 2
     */
    static Find(func, &match?, start:=1) {
        if !HasMethod(func)
            throw ValueError("Find: func must be a function", -1)
        for i, v in this {
            if i < start
                continue
            if func(v) {
                match := v
                return i
            }
        }
        return 0
    }
    /**
     * Reverses the array.
     * @example
     * [1,2,3].Reverse() ; returns [3,2,1]
     */
    static Reverse() {
        len := this.Length + 1, max := (len // 2), i := 0
        while ++i <= max
            this.Swap(i, len - i)
        return this
    }
    /**
     * Counts the number of occurrences of a value
     * @param value The value to count. Can also be a function.
     */
    static Count(value) {
        count := 0
        if HasMethod(value) {
            for v in this
                if value(v?)
                    count++
        } else
            for v in this
                if v == value
                    count++
        return count
    }
    /**
     * Sorts an array, optionally by object keys
     * @param OptionsOrCallback Optional: either a callback function, or one of the following:
     * 
     *     N => array is considered to consist of only numeric values. This is the default option.
     *     C, C1 or COn => case-sensitive sort of strings
     *     C0 or COff => case-insensitive sort of strings
     * 
     *     The callback function should accept two parameters elem1 and elem2 and return an integer:
     *     Return integer < 0 if elem1 less than elem2
     *     Return 0 is elem1 is equal to elem2
     *     Return > 0 if elem1 greater than elem2
     * @param Key Optional: Omit it if you want to sort a array of primitive values (strings, numbers etc).
     *     If you have an array of objects, specify here the key by which contents the object will be sorted.
     * @returns {Array}
     */
    static Sort(optionsOrCallback:="N", key?) {
        static sizeofFieldType := A_PtrSize * 2
        if HasMethod(optionsOrCallback)
            pCallback := CallbackCreate(CustomCompare.Bind(optionsOrCallback), "F", 2), optionsOrCallback := ""
        else {
            if InStr(optionsOrCallback, "N")
                pCallback := CallbackCreate(IsSet(key) ? NumericCompareKey.Bind(key) : NumericCompare, "F", 2)
            if RegExMatch(optionsOrCallback, "i)C(?!0)|C1|COn")
                pCallback := CallbackCreate(IsSet(key) ? StringCompareKey.Bind(key,,True) : StringCompare.Bind(,,True), "F", 2)
            if RegExMatch(optionsOrCallback, "i)C0|COff")
                pCallback := CallbackCreate(IsSet(key) ? StringCompareKey.Bind(key) : StringCompare, "F", 2)
            if InStr(optionsOrCallback, "Random")
                pCallback := CallbackCreate(RandomCompare, "F", 2)
            if !IsSet(pCallback)
                throw ValueError("No valid options provided!", -1)
        }
        mFields := NumGet(ObjPtr(this) + (4 * A_PtrSize), "Ptr") ; 0 is VTable. 2 is mBase, 4 is FlatVector, 5 is mLength and 6 is mCapacity
        DllCall("msvcrt.dll\qsort", "Ptr", mFields, "Int", this.Length, "Int", sizeofFieldType, "Ptr", pCallback)
        CallbackFree(pCallback)
        if RegExMatch(optionsOrCallback, "i)R(?!a)")
            this.Reverse()
        if InStr(optionsOrCallback, "U")
            this := this.Unique()
        return this

        CustomCompare(compareFunc, pFieldType1, pFieldType2) => (ValueFromFieldType(pFieldType1, &fieldValue1), ValueFromFieldType(pFieldType2, &fieldValue2), compareFunc(fieldValue1, fieldValue2))
        NumericCompare(pFieldType1, pFieldType2) => (ValueFromFieldType(pFieldType1, &fieldValue1), ValueFromFieldType(pFieldType2, &fieldValue2), fieldValue1 - fieldValue2)
        NumericCompareKey(key, pFieldType1, pFieldType2) => (ValueFromFieldType(pFieldType1, &fieldValue1), ValueFromFieldType(pFieldType2, &fieldValue2), fieldValue1.%key% - fieldValue2.%key%)
        StringCompare(pFieldType1, pFieldType2, casesense := False) => (ValueFromFieldType(pFieldType1, &fieldValue1), ValueFromFieldType(pFieldType2, &fieldValue2), StrCompare(fieldValue1 "", fieldValue2 "", casesense))
        StringCompareKey(key, pFieldType1, pFieldType2, casesense := False) => (ValueFromFieldType(pFieldType1, &fieldValue1), ValueFromFieldType(pFieldType2, &fieldValue2), StrCompare(fieldValue1.%key% "", fieldValue2.%key% "", casesense))
        RandomCompare(pFieldType1, pFieldType2) => (Random(0, 1) ? 1 : -1)

        ValueFromFieldType(pFieldType, &fieldValue?) {
            static SYM_STRING := 0, PURE_INTEGER := 1, PURE_FLOAT := 2, SYM_MISSING := 3, SYM_OBJECT := 5
            switch SymbolType := NumGet(pFieldType + A_PtrSize, "Int") {
                case PURE_INTEGER: fieldValue := NumGet(pFieldType, "Int64") 
                case PURE_FLOAT: fieldValue := NumGet(pFieldType, "Double") 
                case SYM_STRING: fieldValue := StrGet(NumGet(pFieldType, "Ptr")+2*A_PtrSize)
                case SYM_OBJECT: fieldValue := ObjFromPtrAddRef(NumGet(pFieldType, "Ptr")) 
                case SYM_MISSING: return		
            }
        }
    }
    /**
     * Randomizes the array. Slightly faster than Array.Sort(,"Random N")
     * @returns {Array}
     */
    static Shuffle() {
        len := this.Length
        Loop len-1
            this.Swap(A_index, Random(A_index, len))
        return this
    }
    /**
     * 
     */
    static Unique() {
        unique := Map()
        for v in this
            unique[v] := 1
        return [unique*]
    }
    /**
     * Joins all the elements to a string using the provided delimiter.
     * @param delim Optional: the delimiter to use. Default is comma.
     * @returns {String}
     */
	static Join(delim:=",") {
		result := ""
		for v in this
			result .= v delim
		return (len := StrLen(delim)) ? SubStr(result, 1, -len) : result
	}
    /**
     * Turns a nested array into a one-level array
     * @returns {Array}
     * @example
     * [1,[2,[3]]].Flat() ; returns [1,2,3]
     */
    static Flat() {
        r := []
        for v in this {
            if Type(v) = "Array"
                r.Extend(v.Flat())
            else
                r.Push(v)
        }
        return this := r
    }
    /**
     * Adds the contents of another array to the end of this one.
     * @param arr The array that is used to extend this one.
     * @returns {Array}
     */
    static Extend(arr) {
        if !HasMethod(arr, "__Enum")
            throw ValueError("Extend: arr must be an iterable")
        for v in arr
            this.Push(v)
        return this
    }
}
Misc.ahk

Some useful functions that can separately be copied into your scripts.

Examples:

Code: Select all

#include ..\Lib\Misc.ahk

; ----------------- Print ----------------------
Print("Hello") ; Uses OutputDebug to print the string 'Hello'

Print({example:"Object", key:"value"},MsgBox) ; Next calls of Print will use MsgBox to display the value
; ----------------- Range ----------------------

; Loop forwards, equivalent to Loop 10
result := ""
for v in Range(10)
    result .= v "`n"
Print(result)

; Loop backwards, equivalent to Range(1,10,-1)
Print(Range(10,1).ToArray())

; Loop forwards, step 2
Print(Range(-10,10,2).ToArray())

; Nested looping
result := ""
for v in Range(3)
    for k in Range(5,1)
        result .= v " " k "`n"
Print(result)

; ----------------- RegExMatchAll ----------------------

result := ""
matches := RegExMatchAll("a,bb,ccc", "\w+")
for i, match in matches
    result .= "Match " i ": " match[] "`n"
Print(result)
Library:

Code: Select all

/*
	Name: Misc.ahk
	Version 0.2 (15.10.22)
	Created: 26.08.22
	Author: Descolada (https://www.autohotkey.com/boards/viewtopic.php?f=83&t=107759)
    Credit: Coco

	Range(stop)						=> Returns an iterable to count from 1..stop
	Range(start, stop [, step])		=> Returns an iterable to count from start to stop with step
	Swap(&a, &b)					=> Swaps the values of a and b
	Print(value?, func?, newline?) 	=> Prints the formatted value of a variable (number, string, array, map, object)
	RegExMatchAll(haystack, needleRegEx [, startingPosition := 1])
	    Returns all RegExMatch results (RegExMatchInfo objects) for needleRegEx in haystack 
		in an array: [RegExMatchInfo1, RegExMatchInfo2, ...]
	Highlight(x?, y?, w?, h?, showTime:=0, color:="Red", d:=2)
		Highlights an area with a colorful border.
	MouseTip(x?, y?, color1:="red", color2:="blue", d:=4)
		Flashes a colorful highlight at a point for 2 seconds.
	WindowFromPoint(X, Y) 			=> Returns the window ID at screen coordinates X and Y.
	ConvertWinPos(X, Y, &outX, &outY, relativeFrom:=A_CoordModeMouse, relativeTo:="screen", winTitle?, winText?, excludeTitle?, excludeText?)
		Converts coordinates between screen, window and client.
*/

/**
 * Returns a sequence of numbers, starting from 1 by default, 
 * and increments by step 1 (by default), 
 * and stops at a specified end number.
 * Can be converted to an array with the method ToArray()
 * @param start The number to start with, or if 'end' is omitted then the number to end with
 * @param end The number to end with
 * @param step Optional: a number specifying the incrementation. Default is 1.
 * @returns {Iterable}
 * @example 
 * for v in Range(5)
 *     Print(v) ; Outputs "1 2 3 4 5"
 */
class Range {
	__New(start, end?, step:=1) {
		if !step
			throw TypeError("Invalid 'step' parameter")
		if !IsSet(end)
			end := start, start := 1
		if (end < start) && (step > 0)
			step := -step
		this.start := start, this.end := end, this.step := step
	}
	__Enum(varCount) {
		start := this.start - this.step, end := this.end, step := this.step, counter := 0
		EnumElements(&element) {
			start := start + step
			if ((step > 0) && (start > end)) || ((step < 0) && (start < end))
				return false
			element := start
			return true
		}
		EnumIndexAndElements(&index, &element) {
			start := start + step
			if ((step > 0) && (start > end)) || ((step < 0) && (start < end))
				return false
			index := ++counter
			element := start
			return true
		}
		return (varCount = 1) ? EnumElements : EnumIndexAndElements
	}
	/**
	 * Converts the iterable into an array.
	 * @returns {Array}
	 * @example
	 * Range(3).ToArray() ; returns [1,2,3]
	 */
	ToArray() {
		r := []
		for v in this
			r.Push(v)
		return r
	}
}

/**
 * Swaps the values of two variables
 * @param a First variable
 * @param b Second variable
 */
Swap(&a, &b) {
	temp := a
	a := b
	b := temp
}

/**
 * Prints the formatted value of a variable (number, string, object).
 * Leaving all parameters empty will return the current function and newline in an Array: [func, newline]
 * @param value Optional: the variable to print. 
 *     If omitted then new settings (output function and newline) will be set.
 *     If value is an object/class that has a ToString() method, then the result of that will be printed.
 * @param func Optional: the print function to use. Default is OutputDebug.
 *     Not providing a function will cause the Print output to simply be returned as a string.
 * @param newline Optional: the newline character to use (applied to the end of the value). 
 *     Default is newline (`n).
 */
Print(value?, func?, newline?) {
	static p := OutputDebug, nl := "`n"
	if IsSet(func)
		p := func
	if IsSet(newline)
		nl := newline
	if IsSet(value) {
		val := IsObject(value) ? ToString(value) nl : value nl
		return HasMethod(p) ? p(val) : val
	}
	return [p, nl]
}

/**
 * Converts a value (number, array, object) to a string.
 * Leaving all parameters empty will return the current function and newline in an Array: [func, newline]
 * @param value Optional: the value to convert. 
 * @returns {String}
 */
ToString(val?) {
    if !IsSet(val)
        return "unset"
    valType := Type(val)
    switch valType, 0 {
        case "String":
            return "'" val "'"
        case "Integer", "Float":
            return val
        default:
            self := "", iter := "", out := ""
            try self := ToString(val.ToString()) ; if the object has ToString available, print it
            if valType != "Array" { ; enumerate object with key and value pair, except for array
                try {
                    enum := val.__Enum(2) 
                    while (enum.Call(&val1, &val2))
                        iter .= ToString(val1) ":" ToString(val2?) ", "
                }
            }
            if !IsSet(enum) { ; if enumerating with key and value failed, try again with only value
                try {
                    enum := val.__Enum(1)
                    while (enum.Call(&enumVal))
                        iter .= ToString(enumVal?) ", "
                }
            }
            if !IsSet(enum) && (valType = "Object") && !self { ; if everything failed, enumerate Object props
                for k, v in val.OwnProps()
                    iter .= SubStr(ToString(k), 2, -1) ":" ToString(v?) ", "
            }
            iter := SubStr(iter, 1, StrLen(iter)-2)
            if !self && !iter && !((valType = "Array" && val.Length = 0) || (valType = "Map" && val.Count = 0) || (valType = "Object" && ObjOwnPropCount(val) = 0))
                return valType ; if no additional info is available, only print out the type
            else if self && iter
                out .= "value:" self ", iter:[" iter "]"
            else
                out .= self iter
            return (valType = "Object") ? "{" out "}" : (valType = "Array") ? "[" out "]" : valType "(" out ")"
    }
}

/**
 * Returns all RegExMatch results in an array: [RegExMatchInfo1, RegExMatchInfo2, ...]
 * @param haystack The string whose content is searched.
 * @param needleRegEx The RegEx pattern to search for.
 * @param startingPosition If StartingPos is omitted, it defaults to 1 (the beginning of haystack).
 * @returns {Array}
 */
RegExMatchAll(haystack, needleRegEx, startingPosition := 1) {
	out := []
	While startingPosition := RegExMatch(haystack, needleRegEx, &outputVar, startingPosition) {
		out.Push(outputVar), startingPosition += outputVar[0] ? StrLen(outputVar[0]) : 1
	}
	return out
}

/**
 * Highlights an area with a colorful border.
 * @param x Screen X-coordinate of the top left corner of the highlight
 * @param y Screen Y-coordinate of the top left corner of the highlight
 * @param w Width of the highlight
 * @param h Height of the highlight
 * @param showTime Can be one of the following:
 *     0 - removes the highlighting
 *     Positive integer (eg 2000) - will highlight and pause for the specified amount of time in ms
 *     Negative integer - will highlight for the specified amount of time in ms, but script execution will continue
 * @param color The color of the highlighting. Default is red.
 * @param d The border thickness of the highlighting in pixels. Default is 2.
 */
Highlight(x?, y?, w?, h?, showTime:=0, color:="Red", d:=2) {
	static guis := []
	for _, r in guis
		r.Destroy()
	guis := []
	if !IsSet(x)
		return
	Loop 4
		guis.Push(Gui("+AlwaysOnTop -Caption +ToolWindow -DPIScale +E0x08000000"))
	Loop 4 {
		i:=A_Index
		, x1:=(i=2 ? x+w : x-d)
		, y1:=(i=3 ? y+h : y-d)
		, w1:=(i=1 or i=3 ? w+2*d : d)
		, h1:=(i=2 or i=4 ? h+2*d : d)
		guis[i].BackColor := color
		guis[i].Show("NA x" . x1 . " y" . y1 . " w" . w1 . " h" . h1)
	}
	if showTime > 0 {
		Sleep(showTime)
		Highlight()
	} else if showTime < 0
		SetTimer(Highlight, -Abs(showTime))
}

/**
 * Flashes a colorful highlight at a point for 2 seconds.
 * @param x Screen X-coordinate for the highlight
 *     Omit x or y to highlight the current cursor position.
 * @param y Screen Y-coordinate for the highlight
 * @param color1 First color for the highlight. Default is red.
 * @param color2 Second color for the highlight. Default is blue.
 * @param d The border thickness of the highlighting in pixels. Default is 2.
 */
MouseTip(x?, y?, color1:="red", color2:="blue", d:=4) {
	If !(IsSet(x) && IsSet(y))
		MouseGetPos(&x, &y)
	Loop 2 {
		Highlight(x-10, y-10, 20, 20, 500, color1, d)
		Highlight(x-10, y-10, 20, 20, 500, color2, d)
	}
	Highlight()
}

/**
 * Returns the window ID at screen coordinates X and Y. 
 * @param X Screen X-coordinate of the point
 * @param Y Screen Y-coordinate of the point
 */
WindowFromPoint(X, Y) { ; by SKAN and Linear Spoon
	return DllCall("GetAncestor", "UInt", DllCall("user32.dll\WindowFromPoint", "Int64", Y << 32 | X), "UInt", 2)
}

/**
 * Converts coordinates between screen, window and client.
 * @param X X-coordinate to convert
 * @param Y Y-coordinate to convert
 * @param outX Variable where to store the converted X-coordinate
 * @param outY Variable where to store the converted Y-coordinate
 * @param relativeFrom CoordMode where to convert from. Default is A_CoordModeMouse.
 * @param relativeTo CoordMode where to convert to. Default is Screen.
 * @param winTitle A window title or other criteria identifying the target window. 
 * @param winText If present, this parameter must be a substring from a single text element of the target window.
 * @param excludeTitle Windows whose titles include this value will not be considered.
 * @param excludeText Windows whose text include this value will not be considered.
 */
ConvertWinPos(X, Y, &outX, &outY, relativeFrom:="", relativeTo:="screen", winTitle?, winText?, excludeTitle?, excludeText?) {
	relativeFrom := (relativeFrom == "") ? A_CoordModeMouse : relativeFrom
	if relativeFrom = relativeTo {
		outX := X, outY := Y
		return
	}
	hWnd := WinExist(winTitle?, winText?, excludeTitle?, excludeText?)

	switch relativeFrom, 0 {
		case "screen", "s":
			if relativeTo = "window" || relativeTo = "w" {
				DllCall("user32\GetWindowRect", "Int", hWnd, "Ptr", RECT := Buffer(16))
				outX := X-NumGet(RECT, 0, "Int"), outY := Y-NumGet(RECT, 4, "Int")
			} else { 
				; screen to client
				pt := Buffer(8), NumPut("int",X,pt), NumPut("int",Y,pt,4)
				DllCall("ScreenToClient", "Int", hWnd, "Ptr", pt)
				outX := NumGet(pt,0,"int"), outY := NumGet(pt,4,"int")
			}
		case "window", "w":
			; window to screen
			WinGetPos(&outX, &outY,,,hWnd)
			outX += X, outY += Y
			if relativeTo = "client" || relativeTo = "c" {
				; screen to client
				pt := Buffer(8), NumPut("int",outX,pt), NumPut("int",outY,pt,4)
				DllCall("ScreenToClient", "Int", hWnd, "Ptr", pt)
				outX := NumGet(pt,0,"int"), outY := NumGet(pt,4,"int")
			}
		case "client", "c":
			; client to screen
			pt := Buffer(8), NumPut("int",X,pt), NumPut("int",Y,pt,4)
			DllCall("ClientToScreen", "Int", hWnd, "Ptr", pt)
			outX := NumGet(pt,0,"int"), outY := NumGet(pt,4,"int")
			if relativeTo = "window" || relativeTo = "w" { ; screen to window
				DllCall("user32\GetWindowRect", "Int", hWnd, "Ptr", RECT := Buffer(16))
				outX -= NumGet(RECT, 0, "Int"), outY -= NumGet(RECT, 4, "Int")
			}
	}
}

/**
 * Gets the position of the caret with UIA, Acc or CaretGetPos.
 * Credit: plankoe (https://www.reddit.com/r/AutoHotkey/comments/ysuawq/get_the_caret_location_in_any_program/)
 * @param X Value is set to the screen X-coordinate of the caret
 * @param Y Value is set to the screen Y-coordinate of the caret
 * @param W Value is set to the width of the caret
 * @param H Value is set to the height of the caret
 */
GetCaretPos(&X?, &Y?, &W?, &H?) {
    ; UIA2 caret
    static IUIA := ComObject("{e22ad333-b25f-460c-83d0-0581107395c9}", "{34723aff-0c9d-49d0-9896-7ab52df8cd8a}")
    try {
        ComCall(8, IUIA, "ptr*", &FocusedEl:=0) ; GetFocusedElement
        ComCall(16, FocusedEl, "int", 10024, "ptr*", &patternObject:=0), ObjRelease(FocusedEl) ; GetCurrentPattern. TextPatternElement2 = 10024
        if patternObject {
            ComCall(10, patternObject, "int*", &IsActive:=1, "ptr*", &caretRange:=0), ObjRelease(patternObject) ; GetCaretRange
            ComCall(10, caretRange, "ptr*", &boundingRects:=0), ObjRelease(caretRange) ; GetBoundingRectangles
            if (Rect := ComValue(0x2005, boundingRects)).MaxIndex() = 3 { ; VT_ARRAY | VT_R8
                X:=Round(Rect[0]), Y:=Round(Rect[1]), W:=Round(Rect[2]), H:=Round(Rect[3])
                return
            }
        }
    }

    ; Acc caret
    static _ := DllCall("LoadLibrary", "Str","oleacc", "Ptr")
    try {
        idObject := 0xFFFFFFF8 ; OBJID_CARET
        if DllCall("oleacc\AccessibleObjectFromWindow", "ptr", WinExist("A"), "uint",idObject &= 0xFFFFFFFF
            , "ptr",-16 + NumPut("int64", idObject == 0xFFFFFFF0 ? 0x46000000000000C0 : 0x719B3800AA000C81, NumPut("int64", idObject == 0xFFFFFFF0 ? 0x0000000000020400 : 0x11CF3C3D618736E0, IID := Buffer(16)))
            , "ptr*", oAcc := ComValue(9,0)) = 0 {
            x:=Buffer(4), y:=Buffer(4), w:=Buffer(4), h:=Buffer(4)
            oAcc.accLocation(ComValue(0x4003, x.ptr, 1), ComValue(0x4003, y.ptr, 1), ComValue(0x4003, w.ptr, 1), ComValue(0x4003, h.ptr, 1), 0)
            X:=NumGet(x,0,"int"), Y:=NumGet(y,0,"int"), W:=NumGet(w,0,"int"), H:=NumGet(h,0,"int")
            if (X | Y) != 0
                return
        }
    }

    ; Default caret
    savedCaret := A_CoordModeCaret, W := 4, H := 20
    CoordMode "Caret", "Screen"
    CaretGetPos(&X, &Y)
    CoordMode "Caret", savedCaret
}

/**
 * Checks whether two rectangles intersect and if they do, then returns an object containing the
 * rectangle of the intersection: {l:left, t:top, r:right, b:bottom}
 * Note 1: Overlapping area must be at least 1 unit. 
 * Note 2: Second rectangle starting at the edge of the first doesn't count as intersecting:
 *     {l:100, t:100, r:200, b:200} does not intersect {l:200, t:100, 400, 400}
 * @param l1 x-coordinate of the upper-left corner of the first rectangle
 * @param t1 y-coordinate of the upper-left corner of the first rectangle
 * @param r1 x-coordinate of the lower-right corner of the first rectangle
 * @param b1 y-coordinate of the lower-right corner of the first rectangle
 * @param l2 x-coordinate of the upper-left corner of the second rectangle
 * @param t2 y-coordinate of the upper-left corner of the second rectangle
 * @param r2 x-coordinate of the lower-right corner of the second rectangle
 * @param b2 y-coordinate of the lower-right corner of the second rectangle
 * @returns {Object}
 */
IntersectRect(l1, t1, r1, b1, l2, t2, r2, b2) {
	rect1 := Buffer(16), rect2 := Buffer(16), rectOut := Buffer(16)
	NumPut("int", l1, "int", t1, "int", r1, "int", b1, rect1)
	NumPut("int", l2, "int", t2, "int", r2, "int", b2, rect2)
	if DllCall("user32\IntersectRect", "Ptr", rectOut, "Ptr", rect1, "Ptr", rect2)
		return {l:NumGet(rectOut, 0, "Int"), t:NumGet(rectOut, 4, "Int"), r:NumGet(rectOut, 8, "Int"), b:NumGet(rectOut, 12, "Int")}
}
Version history

Code: Select all

09.09.22: First post for String.ahk
07.10.22: Added Array.ahk, Misc.ahk
12.10.22: A ton of String.ahk bugfixes. Added RegExMatch and RegExReplace (thanks Axlefublr!)
15.10.22: String.ahk: improved WReverse speed. Added SplitPath() (thanks neogna2!). Array.ahk: combined Find and FindIndex into one. Misc.ahk: added RegExMatchAll.
01.03.23: String.ahk: added __Enum (thanks aliztori!)
24.03.23: String.ahk: added Format(). Bug fixes. Array.ahk: added ForEach(). Improved Sort() (introduces breaking change!). Misc.ahk: added GetCaretPos(), IntersectRect().
Last edited by Descolada on 24 Mar 2023, 13:43, edited 7 times in total.

guest3456
Posts: 3463
Joined: 09 Oct 2013, 10:31

Re: String.ahk

Post by guest3456 » 13 Sep 2022, 01:49

nice


AHK_user
Posts: 515
Joined: 04 Dec 2015, 14:52
Location: Belgium

Re: String.ahk

Post by AHK_user » 13 Sep 2022, 14:48

Great work :bravo:

This feels like programming magic :shock: :shock: :shock:

stretch65
Posts: 21
Joined: 16 Jun 2015, 23:49

Re: String.ahk

Post by stretch65 » 23 Sep 2022, 00:15

Seems like a great example for me to learn from. Thanks very much for this!

User avatar
hyaray
Posts: 85
Joined: 20 Jun 2015, 01:37
Contact:

Re: String.ahk

Post by hyaray » 30 Sep 2022, 02:38

great, BTW, is anywhere has "array.ahk", "object.ahk", "number.ahk"??

Descolada
Posts: 1138
Joined: 23 Dec 2021, 02:30

Re: String.ahk

Post by Descolada » 30 Sep 2022, 10:30

@hyaray, as of this moment I am unaware of such libraries. I did have plans of writing "Array.ahk", but since I haven't personally needed any of these beyond Sort and got busy with other projects, it kinda fell through. Perhaps somebody is interested in porting @Chunjee's array.ahk?

Descolada
Posts: 1138
Joined: 23 Dec 2021, 02:30

Re: String.ahk, Array.ahk, Misc.ahk

Post by Descolada » 07 Oct 2022, 09:00

@hyaray, I've updated the post with Array.ahk, please report any bugs that you find either here or at GitHub. Also it isn't as complete as Chunjee's, but it should have pretty much all the essentials.

Also thought about Object.ahk, but would there be any more methods to add beside Keys() and Values()? And I have no good ideas on what to do with Number.ahk. :/

User avatar
Chunjee
Posts: 1421
Joined: 18 Apr 2014, 19:05
Contact:

Re: String.ahk, Array.ahk, Misc.ahk

Post by Chunjee » 07 Oct 2022, 11:28

These are great! I love seeing those arrow function expressions too :bravo:

User avatar
thqby
Posts: 407
Joined: 16 Apr 2021, 11:18
Contact:

Re: String.ahk, Array.ahk, Misc.ahk

Post by thqby » 13 Oct 2022, 03:40

Code: Select all

bf := func.Bind(v)
for _, vv in arrays
    bf := bf.Bind(vv.Has(i) ? vv[i] : "")
Is that what you want?

Code: Select all

bf := func.bind(v, arrays*)

Descolada
Posts: 1138
Joined: 23 Dec 2021, 02:30

Re: String.ahk, Array.ahk, Misc.ahk

Post by Descolada » 13 Oct 2022, 05:08

@thqby, no, I wanted [1,2,3,4,5].Map((a,b) => a+b, [0,1,3,5,7]) to return [1, 3, 6, 9, 12], similar to Python's map function. Of course it would make more sense for it to be Map((a,b) => a+b, [1,2,3,4,5], [0,1,3,5,7]), but since I wanted it to be applicable as a method then this seemed the second best way.
Some more design choices I am not sure about: whether it should bind "unset" instead of "" if the array is shorter than the main array; or perhaps the function should stop once the shortest array has been applied.

neogna2
Posts: 591
Joined: 15 Sep 2016, 15:44

Re: String.ahk, Array.ahk, Misc.ahk

Post by neogna2 » 14 Oct 2022, 18:11

@Descolada :thumbup:
String.ahk could include SplitPath. These method names are inspired by the documentation VarRef example names but shorter.

Code: Select all

	static Filename()     => (SplitPath(this, &Out), Out)
	static Dir()          => (SplitPath(this, , &Out), Out)
	static Ext()          => (SplitPath(this, , , &Out), Out)
	static NameNoExt()    => (SplitPath(this, , , , &Out), Out)
	static Drive()        => (SplitPath(this, , , , , &Out), Out)
edit: or as object properties :think:

Code: Select all

static Path()        => (SplitPath(this, &a1, &a2, &a3, &a4, &a5), {Filename: a1, Dir: a2, Ext: a3, NameNoExt: a4, Drive: a5} )

iseahound
Posts: 1445
Joined: 13 Aug 2016, 21:04
Contact:

Re: String.ahk, Array.ahk, Misc.ahk

Post by iseahound » 14 Oct 2022, 19:54

For the string reversal algorithm, you're better off using Ord to tokenize your string and create a new one. For example, Ord("hi") gives you "h" (after using Chr()).

Descolada
Posts: 1138
Joined: 23 Dec 2021, 02:30

Re: String.ahk, Array.ahk, Misc.ahk

Post by Descolada » 15 Oct 2022, 11:40

@neogna2, good idea, I added the second one to the library :)

@iseahound, that does seem to be ~30% faster than the RegEx version for Unicode reversal, but for normal use-cases _wcsrev blows them out of the water completely:

Code: Select all

startTime := A_TickCount
Loop 100000
    RegExReverse("💩1c`nba")
OutputDebug("RegExReverse: " A_TickCount-startTime " ms`n")
startTime := A_TickCount
Loop 100000
    OrdReverse("💩1c`nba")
OutputDebug("OrdReverse: " A_TickCount-startTime " ms`n")
startTime := A_TickCount
Loop 100000
    WcsReverse("💩1c`nba")
OutputDebug("WcsReverse: " A_TickCount-startTime " ms`n")

RegExReverse(str) {
    out := "", m := ""
    While RegexMatch(str, "s).", &m) && out := m[] out
        str := RegExReplace(str, "s).",,,1)
    return out
}

OrdReverse(str) {
    out := "", m := ""
    While str && (m := Chr(Ord(str))) && (out := m . out)
        str := SubStr(str,StrLen(m)+1)
    return out
}

WcsReverse(str) {
    DllCall("msvcrt\_wcsrev", "str", str, "CDecl str")
    return str
}
I just assumed that Chr would return just half on an Unicode character (like SubStr and whatnot), but it doesn't. Thanks for the tip :)

Fun facts:

Code: Select all

MsgBox(StrLen(Chr(Ord("")))) ; returns 1

if m := Chr(Ord("")) ; evaluates to True
    MsgBox('m := Chr(Ord(""))') 

if Chr(Ord("")) ; evaluates to False
    MsgBox('Chr(Ord(""))')

iseahound
Posts: 1445
Joined: 13 Aug 2016, 21:04
Contact:

Re: String.ahk, Array.ahk, Misc.ahk

Post by iseahound » 16 Oct 2022, 01:05

I think you should do pointer arithmetic instead with StrPtr and Ord > 65536. SubStr is highly ineffiencent, there is no reason to create a copy of a string when an offset may be enough. (Which may lead to a review of your current code base)

StringTrimLeft from v1 was much faster than SubStr, and pointers should be faster still.

aliztori
Posts: 119
Joined: 19 Jul 2022, 12:44

Re: String.ahk, Array.ahk, Misc.ahk

Post by aliztori » 01 Mar 2023, 07:59

@Descolada

String.ahk could include __Enum like python
we can iter every character of string

Code: Select all

	for char in "ali"
		MsgBox char ;=> "a", "l", "i"
and with Index:

Code: Select all

	for index, char in "ali"
		MsgBox index ": " char ;=> 1: "a", 2: "l", 3: "i"
Enumerate the characters of a string.

fat arrow syntax
Spoiler

Code: Select all


static __Enum(varCount) {

	pos := 1, counter := 0

	enumerate(&char) {
		char := SubStr(this, pos, 1)
		pos++
		return pos-1 <= StrLen(this)
	}
	
	EnumIndexAndElements(&index, &char) {

		char := SubStr(this, pos, 1)
		pos++, index := ++counter
		return pos-1 <= StrLen(this)
	}

	return varCount = 1 ? enumerate : EnumIndexAndElements
}

Descolada
Posts: 1138
Joined: 23 Dec 2021, 02:30

Re: String.ahk, Array.ahk, Misc.ahk

Post by Descolada » 01 Mar 2023, 09:02

@aliztori, I added __Enum, albeit in a modified form. I think calling SubStr on every call of __Enum might be slow, so I opted to using StrSplit instead.

iseahound
Posts: 1445
Joined: 13 Aug 2016, 21:04
Contact:

Re: String.ahk, Array.ahk, Misc.ahk

Post by iseahound » 01 Mar 2023, 15:59

I wasn't sure if you understood my previous post but I'd suggested using a pointer offset like this:

Code: Select all

#Requires AutoHotkey v2.0

str(s) {
   pos := 1
   enumerate(&char) {
      char := StrGet(StrPtr(s) + 2*(pos-1), 1) ; yield statement
      pos++                     ; do block
      return pos-1 <= StrLen(s) ; continue?
   }
   return enumerate
}
for char in str("kangaroo")
   MsgBox char
which is fine because StrSplit doesn't handle unicode extended characters either:

Code: Select all

for char in strsplit("🧝‍♀️💖🟡")
   msgbox char
; Returns gibberish
which leads to understanding how strings are passed in lower level programming. I think Lexikos posted a great reference: https://github.com/chakra-core/ChakraCore/wiki/String-Internals
  1. To read a string, just get its pointer via StrPtr. Increment the pointer by 2 bytes as this uses UTF-16 by default. To access the second character of "icy" use StrGet(StrPtr("icy") + 2, 1) (Note: This is unsafe because "icy" is being garbage collected, so assign it to a variable first)
  2. To copy a string, use SubStr to create a copy. I'd imagine SubStr just calls memcpy with a start and end pointer.
My previous comment of checking Ord(s) > 65536 still stand I think. Also someone wrote GraphemeSplit: viewtopic.php?f=83&t=113294

Descolada
Posts: 1138
Joined: 23 Dec 2021, 02:30

Re: String.ahk, Array.ahk, Misc.ahk

Post by Descolada » 02 Mar 2023, 01:47

@iseahound, ah, I had used StrPtr method before as well, but it was slower than other methods so I didn't try it. When I optimized your proposed code a bit, it did get significantly faster and I'll update the library soon.

Code: Select all

#include String.ahk

str(s) {
    pos := 0, len := StrLen(s)
    enumerate(&char) {
       char := StrGet(StrPtr(s) + 2*pos, 1) ; yield statement
       return ++pos <= len
    }
    return enumerate
}

loopCount := 100000
startTime := A_TickCount
Loop loopCount {
    res := ""
    for char in str("kangaroo")
        res := char
}
OutputDebug "StrPtr: " (A_TickCount-startTime) " ms`n"

startTime := A_TickCount
Loop loopCount {
    res := ""
    for char in "kangaroo"
        res := char
}
OutputDebug "StrSplit: " (A_TickCount-startTime) " ms`n"
GraphemeSplit looks useful! I've looked into parsing Unicode properly and decided that it's a can of worms I'd rather put off dealing with at the moment :D Namely it would make the behavior of String.ahk inconsisent with AHK overall. For example, it would get confusing if StrLen("💩") outputs 2 but "💩".Length outputted 1 - you would have to constantly remind yourself which style to use. The same goes for StrSplit, StrReverse, __Enum etc. Using Unicode style in __Enum would also lead to some interesting results: "`r`n" would be enumerated as one character, because in Unicode they are considered one CRLF. This is why I opted to keeping to one single style in String.ahk, at least for now.

I see two possible solutions for this problem in the future. One is that AHK v2.1 (or v3?) embraces Unicode and natively solves the afore-mentioned problems. This I think is fairly unlikely. Second is that I (or someone else who is interested in solving this problem) decides to write a MCode solution for parsing Unicode characters. I think that a bare minimum would be an Unicode StrSplitToChars, and the other ones can be easily generated in AHK from the output of that. I need to learn a bit more C++ before attempting that :)

Some other good references for reading: Mathias on Javascript Unicode, ruleset for Unicode segmentation.

aliztori
Posts: 119
Joined: 19 Jul 2022, 12:44

Re: String.ahk, Array.ahk, Misc.ahk

Post by aliztori » 02 Mar 2023, 05:08

Descolada wrote:
01 Mar 2023, 09:02
@aliztori, I added __Enum, albeit in a modified form. I think calling SubStr on every call of __Enum might be slow, so I opted to using StrSplit instead.
Oh, my last review was on your GitHub

iseahound
Posts: 1445
Joined: 13 Aug 2016, 21:04
Contact:

Re: String.ahk, Array.ahk, Misc.ahk

Post by iseahound » 02 Mar 2023, 11:32

Yep I ran a benchmark too, and using StrPtr is about 2x faster than StrSplit

Code: Select all

#Requires AutoHotkey v2.0-beta.3+
#include *i String.ahk
; SetBatchLines -1

f := 100000, a := b := c := d := 0
MsgBox "Ready?"

s := "I was temporarily forced to zig-zag and quiver furiously around big junky xylophones."

DllCall("QueryPerformanceFrequency", "int64*", &frequency:=0)
loop f {

   ; ((Test 1))
   DllCall("QueryPerformanceCounter", "int64*", &start:=0)
   res := ""
   for char in str(s)
      res .= char
   DllCall("QueryPerformanceCounter", "int64*", &end:=0)
   a += end - start

   ; ((Test 2))
   DllCall("QueryPerformanceCounter", "int64*", &start:=0)
   res := ""
   for char in StrSplit(s)
      res .= char
   DllCall("QueryPerformanceCounter", "int64*", &end:=0)
   b += end - start

   ; ((Test 3))
   DllCall("QueryPerformanceCounter", "int64*", &start:=0)
   res := ""
   for char in str2(s)
      res .= char
   DllCall("QueryPerformanceCounter", "int64*", &end:=0)
   c += end - start

   ; ((Test 4))
   DllCall("QueryPerformanceCounter", "int64*", &start:=0)
   DllCall("QueryPerformanceCounter", "int64*", &end:=0)
   d += end - start

}

str(s) {
    pos := 0, len := 2*StrLen(s)
    enumerate(&char) {
       char := StrGet(StrPtr(s) + pos, 1) ; yield statement
       return (pos+=2) <= len
    }
    return enumerate
}

str2(s) {
    pos := 0, len := 2*StrLen(s), ptr := StrPtr(s)
    enumerate(&char) {
       char := StrGet(ptr + pos, 1) ; yield statement
       return (pos+=2) <= len
    }
    return enumerate
}

a := a / frequency
a := f / a
b := b / frequency
b := f / b
c := c / frequency
c := f / c
d := d / frequency
d := f / d


MsgBox   Round(a, 2)  " fps"
  . "`n" Round(b, 2)  " fps"
  . "`n" Round(c, 2)  " fps"
  . "`n" Round(d, 2)  " fps"

Post Reply

Return to “Scripts and Functions (v2)”