<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd">
<script:module xmlns:script="http://openoffice.org/2000/script" script:name="SF_Array" script:language="StarBasic" script:moduleType="normal">REM =======================================================================================================================
REM ===			The ScriptForge library and its associated libraries are part of the LibreOffice project.				===
REM ===					Full documentation is available on https://help.libreoffice.org/								===
REM =======================================================================================================================

Option Compatible
Option Explicit

&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;
&apos;&apos;&apos;	SF_Array
&apos;&apos;&apos;	========
&apos;&apos;&apos;		Singleton class implementing the &quot;ScriptForge.Array&quot; service
&apos;&apos;&apos;		Implemented as a usual Basic module
&apos;&apos;&apos;		Only 1D or 2D arrays are considered. Arrays with more than 2 dimensions are rejected
&apos;&apos;&apos;			With the noticeable exception of the CountDims method (&gt;2 dims allowed)
&apos;&apos;&apos;		The first argument of almost every method is the array to consider
&apos;&apos;&apos;			It is always passed by reference and left unchanged
&apos;&apos;&apos;
&apos;&apos;&apos;		Detailed user documentation:
&apos;&apos;&apos;			https://help.libreoffice.org/latest/en-US/text/sbasic/shared/03/sf_array.html?DbPAR=BASIC
&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;&apos;

REM ================================================================== EXCEPTIONS

Const ARRAYSEQUENCEERROR		=	&quot;ARRAYSEQUENCEERROR&quot;		&apos;	Incoherent arguments
Const ARRAYINSERTERROR			=	&quot;ARRAYINSERTERROR&quot;			&apos;	Matrix and vector have incompatible sizes
Const ARRAYINDEX1ERROR			=	&quot;ARRAYINDEX1ERROR&quot;			&apos;	Given index does not fit in array bounds
Const ARRAYINDEX2ERROR			=	&quot;ARRAYINDEX2ERROR&quot;			&apos;	Given indexes do not fit in array bounds
Const CSVPARSINGERROR			=	&quot;CSVPARSINGERROR&quot;			&apos;	Parsing error detected while parsing a csv file
Const CSVOVERFLOWWARNING		=	&quot;CSVOVERFLOWWARNING&quot;		&apos;	Array becoming too big, import process of csv file is interrupted

REM ============================================================ MODULE CONSTANTS

Const MAXREPR					= 50	&apos;	Maximum length to represent an array in the console

REM ===================================================== CONSTRUCTOR/DESTRUCTOR

REM -----------------------------------------------------------------------------
Public Function Dispose() As Variant
	Set Dispose = Nothing
End Function	&apos;	ScriptForge.SF_Array Explicit destructor

REM ================================================================== PROPERTIES

REM -----------------------------------------------------------------------------
Property Get ObjectType As String
&apos;&apos;&apos;	Only to enable object representation
	ObjectType = &quot;SF_Array&quot;
End Property	&apos;	ScriptForge.SF_Array.ObjectType

REM -----------------------------------------------------------------------------
Property Get ServiceName As String
&apos;&apos;&apos;	Internal use
	ServiceName = &quot;ScriptForge.Array&quot;
End Property	&apos;	ScriptForge.SF_Array.ServiceName

REM ============================================================== PUBLIC METHODS

REM -----------------------------------------------------------------------------
Public Function Append(Optional ByRef Array_1D As Variant _
						, ParamArray pvArgs() As Variant _
						) As Variant
&apos;&apos;&apos;	Append at the end of the input array the items listed as arguments
&apos;&apos;&apos;		Arguments are appended blindly
&apos;&apos;&apos;			each of them might be a scalar of any type or a subarray
&apos;&apos;&apos;	Args
&apos;&apos;&apos;		Array_1D: the pre-existing array, may be empty
&apos;&apos;&apos;		pvArgs: a list of items to append to Array_1D
&apos;&apos;&apos;	Return:
&apos;&apos;&apos;		the new extended array. Its LBound is identical to that of Array_1D
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		SF_Array.Append(Array(1, 2, 3), 4, 5) returns (1, 2, 3, 4, 5)

Dim vAppend As Variant		&apos;	Return value
Dim lNbArgs As Long			&apos;	Number of elements to append
Dim lMax As Long			&apos;	UBound of input array
Dim i As Long
Const cstThisSub = &quot;Array.Append&quot;
Const cstSubArgs = &quot;Array_1D, arg0[, arg1] ...&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vAppend = Array()

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_1D, &quot;Array_1D&quot;, 1) Then GoTo Finally
	End If

Try:
	lMax = UBound(Array_1D)
	lNbArgs = UBound(pvArgs) + 1	&apos;	pvArgs is always zero-based
	If lMax &lt; LBound(Array_1D) Then	&apos; Initial array is empty
		If lNbArgs &gt; 0 Then
			ReDim vAppend(0 To lNbArgs - 1)
		End If
	Else
		vAppend() = Array_1D()
		If lNbArgs &gt; 0 Then
			ReDim Preserve vAppend(LBound(Array_1D) To lMax + lNbArgs)
		End If
	End If
	For i = 1 To lNbArgs
		vAppend(lMax + i) = pvArgs(i - 1)
	Next i

Finally:
	Append = vAppend()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.Append

REM -----------------------------------------------------------------------------
Public Function AppendColumn(Optional ByRef Array_2D As Variant _
								, Optional ByRef Column As Variant _
								) As Variant
&apos;&apos;&apos;	AppendColumn appends to the right side of a 2D array a new Column
&apos;&apos;&apos;	Args
&apos;&apos;&apos;		Array_2D: the pre-existing array, may be empty
&apos;&apos;&apos;			If the array has 1 dimension, it is considered as the 1st Column of the resulting 2D array
&apos;&apos;&apos;		Column: a 1D array with as many items as there are rows in Array_2D
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		the new extended array. Its LBounds are identical to that of Array_2D
&apos;&apos;&apos;	Exceptions:
&apos;&apos;&apos;		ARRAYINSERTERROR
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		SF_Array.AppendColumn(Array(1, 2, 3), Array(4, 5, 6)) returns ((1, 4), (2, 5), (3, 6))
&apos;&apos;&apos;		x = SF_Array.AppendColumn(Array(), Array(1, 2, 3)) =&gt; ∀ i ∈ {0 ≤ i ≤ 2} : x(0, i) ≡ i

Dim vAppendColumn As Variant	&apos;	Return value
Dim iDims As Integer			&apos;	Dimensions of Array_2D
Dim lMin1 As Long				&apos;	LBound1 of input array
Dim lMax1 As Long				&apos;	UBound1 of input array
Dim lMin2 As Long				&apos;	LBound2 of input array
Dim lMax2 As Long				&apos;	UBound2 of input array
Dim lMin As Long				&apos;	LBound of Column array
Dim lMax As Long				&apos;	UBound of Column array
Dim i As Long
Dim j As Long
Const cstThisSub = &quot;Array.AppendColumn&quot;
Const cstSubArgs = &quot;Array_2D, Column&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vAppendColumn = Array()

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_2D, &quot;Array_2D&quot;) Then GoTo Finally	&apos;Initial check: not missing and array
		If Not SF_Utils._ValidateArray(Column, &quot;Column&quot;, 1) Then GoTo Finally
	End If
	iDims = SF_Array.CountDims(Array_2D)
	If iDims &gt; 2 Then
		If Not SF_Utils._ValidateArray(Array_2D, &quot;Array_2D&quot;, 2) Then GoTo Finally	&apos;2nd check to manage error
	End If

Try:
	lMin = LBound(Column)
	lMax = UBound(Column)

	&apos;	Compute future dimensions of output array
	Select Case iDims
		Case 0		:	lMin1 = lMin					:	lMax1 = lMax
						lMin2 = 0						:	lMax2 = -1
		Case 1		:	lMin1 = LBound(Array_2D, 1)		:	lMax1 = UBound(Array_2D, 1)
						lMin2 = 0						:	lMax2 = 0
		Case 2		:	lMin1 = LBound(Array_2D, 1)		:	lMax1 = UBound(Array_2D, 1)
						lMin2 = LBound(Array_2D, 2)		:	lMax2 = UBound(Array_2D, 2)
	End Select
	If iDims &gt; 0 And lMax - lMin &lt;&gt; lMax1 - lMin1 Then GoTo CatchColumn
	ReDim vAppendColumn(lMin1 To lMax1, lMin2 To lMax2 + 1)

	&apos;	Copy input array to output array
	For i = lMin1 To lMax1
		For j = lMin2 To lMax2
			If iDims = 2 Then vAppendColumn(i, j) = Array_2D(i, j) Else vAppendColumn(i, j) = Array_2D(i)
		Next j
	Next i
	&apos;	Copy new Column
	For i = lMin1 To lMax1
		vAppendColumn(i, lMax2 + 1) = Column(i)
	Next i

Finally:
	AppendColumn = vAppendColumn()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
CatchColumn:
	SF_Exception.RaiseFatal(ARRAYINSERTERROR, &quot;Column&quot;, SF_Array._Repr(Array_2D), SF_Utils._Repr(Column, MAXREPR))
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.AppendColumn

REM -----------------------------------------------------------------------------
Public Function AppendRow(Optional ByRef Array_2D As Variant _
							, Optional ByRef Row As Variant _
							) As Variant
&apos;&apos;&apos;	AppendRow appends below a 2D array a new row
&apos;&apos;&apos;	Args
&apos;&apos;&apos;		Array_2D: the pre-existing array, may be empty
&apos;&apos;&apos;			If the array has 1 dimension, it is considered as the 1st row of the resulting 2D array
&apos;&apos;&apos;		Row: a 1D array with as many items as there are columns in Array_2D
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		the new extended array. Its LBounds are identical to that of Array_2D
&apos;&apos;&apos;	Exceptions:
&apos;&apos;&apos;		ARRAYINSERTERROR
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		SF_Array.AppendRow(Array(1, 2, 3), Array(4, 5, 6)) returns ((1, 2, 3), (4, 5, 6))
&apos;&apos;&apos;		x = SF_Array.AppendRow(Array(), Array(1, 2, 3)) =&gt; ∀ i ∈ {0 ≤ i ≤ 2} : x(i, 0) ≡ i

Dim vAppendRow As Variant	&apos;	Return value
Dim iDims As Integer		&apos;	Dimensions of Array_2D
Dim lMin1 As Long			&apos;	LBound1 of input array
Dim lMax1 As Long			&apos;	UBound1 of input array
Dim lMin2 As Long			&apos;	LBound2 of input array
Dim lMax2 As Long			&apos;	UBound2 of input array
Dim lMin As Long			&apos;	LBound of row array
Dim lMax As Long			&apos;	UBound of row array
Dim i As Long
Dim j As Long
Const cstThisSub = &quot;Array.AppendRow&quot;
Const cstSubArgs = &quot;Array_2D, Row&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vAppendRow = Array()

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_2D, &quot;Array_2D&quot;) Then GoTo Finally	&apos;Initial check: not missing and array
		If Not SF_Utils._ValidateArray(Row, &quot;Row&quot;, 1) Then GoTo Finally
	End If
	iDims = SF_Array.CountDims(Array_2D)
	If iDims &gt; 2 Then
		If Not SF_Utils._ValidateArray(Array_2D, &quot;Array_2D&quot;, 2) Then GoTo Finally	&apos;2nd check to manage error
	End If

Try:
	lMin = LBound(Row)
	lMax = UBound(Row)

	&apos;	Compute future dimensions of output array
	Select Case iDims
		Case 0		:	lMin1 = 0						:	lMax1 = -1
						lMin2 = lMin					:	lMax2 = lMax
		Case 1		:	lMin1 = 0						:	lMax1 = 0
						lMin2 = LBound(Array_2D, 1)		:	lMax2 = UBound(Array_2D, 1)
		Case 2		:	lMin1 = LBound(Array_2D, 1)		:	lMax1 = UBound(Array_2D, 1)
						lMin2 = LBound(Array_2D, 2)		:	lMax2 = UBound(Array_2D, 2)
	End Select
	If iDims &gt; 0 And lMax - lMin &lt;&gt; lMax2 - lMin2 Then GoTo CatchRow
	ReDim vAppendRow(lMin1 To lMax1 + 1, lMin2 To lMax2)

	&apos;	Copy input array to output array
	For i = lMin1 To lMax1
		For j = lMin2 To lMax2
			If iDims = 2 Then vAppendRow(i, j) = Array_2D(i, j) Else vAppendRow(i, j) = Array_2D(j)
		Next j
	Next i
	&apos;	Copy new row
	For j = lMin2 To lMax2
		vAppendRow(lMax1 + 1, j) = Row(j)
	Next j

Finally:
	AppendRow = vAppendRow()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
CatchRow:
	SF_Exception.RaiseFatal(ARRAYINSERTERROR, &quot;Row&quot;, SF_Array._Repr(Array_2D), SF_Utils._Repr(Row, MAXREPR))
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.AppendRow

REM -----------------------------------------------------------------------------
Public Function Contains(Optional ByRef Array_1D As Variant _
							, Optional ByVal ToFind As Variant _
							, Optional ByVal CaseSensitive As Variant _
							, Optional ByVal SortOrder As Variant _
							) As Boolean
&apos;&apos;&apos;	Check if a 1D array contains the ToFind number, string or date
&apos;&apos;&apos;	The comparison between strings can be done case-sensitive or not
&apos;&apos;&apos;	If the array is sorted then
&apos;&apos;&apos;		the array must be filled homogeneously, i.e. all items must be of the same type
&apos;&apos;&apos;		Empty and Null items are forbidden
&apos;&apos;&apos;		a binary search is done
&apos;&apos;&apos;	Otherwise the array is scanned from top. Null or Empty items are simply ignored
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array_1D: the array to scan
&apos;&apos;&apos;		ToFind: a number, a date or a string to find
&apos;&apos;&apos;		CaseSensitive: Only for string comparisons, default = False
&apos;&apos;&apos;		SortOrder: &quot;ASC&quot;, &quot;DESC&quot; or &quot;&quot; (= not sorted, default)
&apos;&apos;&apos;	Return: True when found
&apos;&apos;&apos;		Result is unpredictable when array is announced sorted and is in reality not
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		SF_Array.Contains(Array(&quot;A&quot;,&quot;B&quot;,&quot;c&quot;,&quot;D&quot;), &quot;C&quot;, SortOrder := &quot;ASC&quot;) returns True
&apos;&apos;&apos;		SF_Array.Contains(Array(&quot;A&quot;,&quot;B&quot;,&quot;c&quot;,&quot;D&quot;), &quot;C&quot;, CaseSensitive := True) returns False

Dim bContains As Boolean			&apos;	Return value
Dim iToFindType As Integer			&apos;	VarType of ToFind
Const cstThisSub = &quot;Array.Contains&quot;
Const cstSubArgs = &quot;Array_1D, ToFind, [CaseSensitive=False], [SortOrder=&quot;&quot;&quot;&quot;|&quot;&quot;ASC&quot;&quot;|&quot;&quot;DESC&quot;&quot;]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch

	bContains = False

Check:
	If IsMissing(CaseSensitive) Or IsEmpty(CaseSensitive) Then CaseSensitive = False
	If IsMissing(SortOrder) Or IsEmpty(SortOrder) Then SortOrder = &quot;&quot;
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._Validate(SortOrder, &quot;SortOrder&quot;, V_STRING, Array(&quot;ASC&quot;, &quot;DESC&quot;, &quot;&quot;)) Then GoTo Finally
		If Not SF_Utils._Validate(ToFind, &quot;ToFind&quot;, Array(V_STRING, V_DATE, V_NUMERIC)) Then GoTo Finally
		iToFindType = SF_Utils._VarTypeExt(ToFind)
		If SortOrder &lt;&gt; &quot;&quot; Then
			If Not SF_Utils._ValidateArray(Array_1D, &quot;Array_1D&quot;, 1, iToFindType) Then GoTo Finally
		Else
			If Not SF_Utils._ValidateArray(Array_1D, &quot;Array_1D&quot;, 1) Then GoTo Finally
		End If
		If Not SF_Utils._Validate(CaseSensitive, &quot;CaseSensitive&quot;, V_BOOLEAN) Then GoTo Finally
	End If

Try:
	bContains = SF_Array._FindItem(Array_1D, ToFind, CaseSensitive, UCase(SortOrder))(0)

Finally:
	Contains = bContains
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.Contains

REM -----------------------------------------------------------------------------
Public Function ConvertFromDataArray(Optional ByRef DataArray As Variant _
										, Optional ByVal IsRange As Variant _
										, Optional ByVal FillValue As Variant _
										) As Variant
&apos;&apos;&apos;	Convert a data array to a scalar, a vector or a 2D array
&apos;&apos;&apos;	A &quot;data array&quot; is an array of subarrays. The array and the subarrays must be zero-based.
&apos;&apos;&apos;		The individual items may be of any type.
&apos;&apos;&apos;	On request, the individual items are reduced to strings or doubles only.
&apos;&apos;&apos;		Typically it represents
&apos;&apos;&apos;			- the content of a range of Calc cells returned by the UNO XCellRange.getDataArray()
&apos;&apos;&apos;			  or XCellRange.getFormulaArray() methods
&apos;&apos;&apos;			- the output of the SF_Session.ExecuteCalcFunction() method
&apos;&apos;&apos;			- a tuple of (sub)tuples returned by a Python script.
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		DataArray: an array of subarrays. Array and subarrays must be zero-based.
&apos;&apos;&apos;		IsRange: When True (default = False), the items are converted to strings or doubles.
&apos;&apos;&apos;		FillValue: Default value of missing items. Default = the empty string
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		A scalar, a zero-based 1D array or a zero-based 2D array.
&apos;&apos;&apos;		The number of columns in the resulting 2D array = the number of items in the 1st subarray.
&apos;&apos;&apos;		If next subarrays have a different size, next rows can be complemented with FillValue or truncated.
&apos;&apos;&apos;	Example:
&apos;&apos;&apos;		vDataArray = Array(Array(&quot;Col1&quot;, &quot;Col2&quot;, &quot;Col3&quot;), Array(10, 20, 30))
&apos;&apos;&apos;		Array2D = SF_Array.ConvertFromDataArray(vDataArray)
&apos;&apos;&apos;		MsgBox Array2D(1, 2)	&apos;	30

Dim vArray As Variant				&apos;	Return value
Dim lMax1 As Long					&apos;	UBound of DataArray
Dim lMax2 As Long					&apos;	UBound of the 1st row of DataArray
Dim lMax As Long					&apos;	UBound of a subarray
Dim i As Long
Dim j As Long

Const cstThisSub = &quot;Array.ConvertFromDataArray&quot;
Const cstSubArgs = &quot;DataArray, [IsRange=False], [FillValue=&quot;&quot;&quot;&quot;]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vArray = Array()

Check:
	If IsMissing(IsRange) Or IsEmpty(IsRange) Then IsRange = False
	If IsMissing(FillValue) Or IsEmpty(FillValue) Then FillValue = &quot;&quot;
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(DataArray, &quot;DataArray&quot;, 1) Then GoTo Finally
		For i = 0 To UBound(DataArray)
			If Not SF_Utils._ValidateArray(DataArray(i), &quot;DataArray(&quot; &amp; i &amp; &quot;)&quot;, 1) Then GoTo Finally
		Next i
	End If

Try:
	&apos;	Convert the data array to scalar, vector or array
	lMax1 = UBound(DataArray)
	If lMax1 &gt;= 0 Then
		lMax2 = UBound(DataArray(0))
		If lMax2 &gt;= 0 Then
			If lMax1 + lMax2 &gt; 0 Then vArray = Array()
			Select Case True
				Case lMax1 = 0 And lMax2 = 0			&apos;	Scalar
					vArray = _ConvertToCellValue(DataArray(0)(0), pbIsCell := IsRange)
				Case lMax1 &gt; 0 And lMax2 = 0			&apos;	Vertical vector
					ReDim vArray(0 To lMax1)
					For i = 0 To lMax1
						vArray(i) = _ConvertToCellValue(DataArray(i)(0), pbIsCell := IsRange)
					Next i
				Case lMax1 = 0 And lMax2 &gt; 0			&apos;	Horizontal vector
					ReDim vArray(0 To lMax2)
					For j = 0 To lMax2
						vArray(j) = _ConvertToCellValue(DataArray(0)(j), pbIsCell := IsRange)
					Next j
				Case Else								&apos;	Array
					ReDim vArray(0 To lMax1, 0 To lMax2)
					For i = 0 To lMax1
						lMax = UBound(DataArray(i))
						For j = 0 To lMax2
							If j &lt;= lMax Then vArray(i, j) = _ConvertToCellValue(DataArray(i)(j), pbIsCell := IsRange) Else vArray(i, j) = FillValue
						Next j
					Next i
			End Select
		End If
	End If

Finally:
	ConvertFromDataArray = vArray
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.ConvertFromDataArray

REM -----------------------------------------------------------------------------
Public Function ConvertToDataArray(Optional ByRef Data As Variant _
								, Optional ByVal IsRange As Variant _
								, Optional ByVal Rows As Variant _
								, Optional ByVal Columns As Variant _
								) As Variant
&apos;&apos;&apos;	Create a data array from a scalar, a 1D array or a 2D array
&apos;&apos;&apos;	The returned &quot;data array&quot; is an array of subarrays.
&apos;&apos;&apos;	On request, the individual items are reduced to strings or doubles only.
&apos;&apos;&apos;	Typically it represents
&apos;&apos;&apos;		- the content of a range of Calc cells used to apply the UNO XCellRange.setDataArray()
&apos;&apos;&apos;		  or XCellRange.setFormulaArray() methods
&apos;&apos;&apos;		- a tuple of (sub)tuples passed to a Python script.
&apos;&apos;&apos;	Input argument may already be a data array, typically when call is issued by a Python script.
&apos;&apos;&apos;	When IsRange = True then the individual items are converted to (possibly empty) strings or doubles.
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Data: the input scalar or array. If array, must be 1D or 2D otherwise it is ignored.
&apos;&apos;&apos;		IsRange: When True (default = False), the items are converted to strings or doubles.
&apos;&apos;&apos;		Rows, Columns: the number of rows or columns of the returned data array.
&apos;&apos;&apos;			If bigger than Data, fill with zero-length strings
&apos;&apos;&apos;			If smaller than Data, truncate
&apos;&apos;&apos;			If Rows = 1 and the input array is a vector, the data array is aligned horizontally
&apos;&apos;&apos;				By default, vectors are aligned vertically.
&apos;&apos;&apos;			When absent, the size of the output is determined by the input array.
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		The output is always an array of nested arrays. Array and nested arrays are zero-based.
&apos;&apos;&apos;		It is compatible with the XCellRange.DataArray property when IsRange = True.

Dim vDataArray() As Variant			&apos;	Return value
Dim vVector() As Variant			&apos;	A temporary 1D array
Dim vItem As Variant				&apos;	A single input item
Dim iDims As Integer				&apos;	Number of dimensions of the input argument
Dim lMin1 As Long					&apos;	Lower bound (1) of input array
Dim lMax1 As Long					&apos;	Upper bound (1)
Dim lMin2 As Long					&apos;	Lower bound (2)
Dim lMax2 As Long					&apos;	Upper bound (2)
Dim lRows As Long					&apos;	Upper bound of vDataArray
Dim lCols As Long					&apos;	Upper bound of vVector
Dim bHorizontal As Boolean			&apos;	Horizontal vectoriDims &lt;&gt; 0 =&gt; Data has at least 1 item
Dim bDataArray As Boolean			&apos;	Input array is already an array of arrays
Dim i As Long
Dim j As Long
Const cstEmpty = &quot;&quot;		&apos;	Empty cell

Const cstThisSub = &quot;Array.ConvertToDataArray&quot;
Const cstSubArgs = &quot;Data, [IsRange=False], [Rows=0], [Columns=0]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vDataArray = Array()

Check:
	If IsMissing(IsRange) Or IsEmpty(IsRange) Then IsRange = False
	If IsMissing(Rows) Or IsEmpty(Rows) Then Rows = 0
	If IsMissing(Columns) Or IsEmpty(Columns) Then Columns = 0
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._Validate(IsRange, &quot;IsRange&quot;, V_BOOLEAN) Then GoTo Finally
		If Not SF_Utils._Validate(Rows, &quot;Rows&quot;, V_NUMERIC) Then GoTo Finally
		If Not SF_Utils._Validate(Columns, &quot;Columns&quot;, V_NUMERIC) Then GoTo Finally
	End If

Try:
	&apos;	Examine the input argument and determine its boundaries
	&apos;	Set the output dimensions accordingly
	iDims = SF_Array.CountDims(Data)
	If iDims = 0 Or iDims &gt; 2 Then Exit Function
	lMin1 = 0		:	lMax1 = 0	&apos;	Default values
	lMin2 = 0		:	lMax2 = 0
	lRows = 0		:	lCols = 0
	Select Case iDims
		Case -1													&apos;	Scalar value
		Case 1													&apos;	1D Array
			bHorizontal = ( Rows = 1 )
			bDataArray = IsArray(Data(LBound(Data)))
			If Not bDataArray Then
				If Not bHorizontal Then
					lMin1 = LBound(Data)	:	lMax1 = UBound(Data)
					lRows = lMax1 - lMin1
				Else
					lMin2 = LBound(Data)	:	lMax2 = UBound(Data)
					lCols = lMax2 - lMin2
				End If
			Else
				iDims = 2
				lMin1 = LBound(Data)	:	lMax1 = UBound(Data)
				lMin2 = LBound(Data(0))	:	lMax2 = UBound(Data(0))
				lRows = lMax1 - lMin1
				lCols = lMax2 - lMin2
			End If
		Case 2													&apos;	2D Array
			lMin1 = LBound(Data, 1)	:	lMax1 = UBound(Data, 1)
			lMin2 = LBound(Data, 2)	:	lMax2 = UBound(Data, 2)
			lRows = lMax1 - lMin1
			lCols = lMax2 - lMin2
	End Select

	&apos;	Size the future data array to the output dimensions
	&apos;	Are the dimensions imposed ?
	If Rows &gt;= 1 Then lRows = Rows - 1
	If Columns &gt;= 1 Then lCols = Columns - 1
	ReDim vDataArray(0 To lRows)

	&apos;	Feed the output array row by row, each row being a vector
	For i = 0 To lRows
		ReDim vVector(0 To lCols)
		For j = 0 To lCols
			If i &gt; lMax1 - lMin1 Then
				vVector(j) = cstEmpty
			ElseIf j &gt; lMax2 - lMin2 Then
				vVector(j) = cstEmpty
			Else
				Select Case iDims
					Case -1		:	vItem = _ConvertToCellValue(Data, IsRange)
					Case 1
						If bHorizontal Then
							vItem = _ConvertToCellValue(Data(j + lMin2), IsRange)
						Else
							vItem = _ConvertToCellValue(Data(i + lMin1), IsRange)
						End If
					Case 2
						If bDataArray Then
							vItem = _ConvertToCellValue(Data(i + lMin1)(j + lMin2), IsRange)
						Else
							vItem = _ConvertToCellValue(Data(i + lMin1, j + lMin2), IsRange)
						End If
				End Select
				vVector(j) = vItem
			End If
			vDataArray(i) = vVector
		Next j
	Next i

Finally:
	ConvertToDataArray = vDataArray
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.ConvertToDataArray

REM -----------------------------------------------------------------------------
Public Function ConvertToDictionary(Optional ByRef Array_2D As Variant) As Variant
&apos;&apos;&apos;	Store the content of a 2-columns array into a dictionary with case-sensitive comparison of keys
&apos;&apos;&apos;	Key found in 1st column, Item found in 2nd
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array_2D: 1st column must contain exclusively non zero-length strings
&apos;&apos;&apos;		1st column may not be sorted
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		a ScriptForge dictionary object
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		

Dim oDict As Variant		&apos;	Return value
Dim i As Long
Const cstThisSub = &quot;Array.ConvertToDictionary&quot;
Const cstSubArgs = &quot;Array_2D&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_2D, &quot;Array_2D&quot;, 2, V_STRING, True) Then GoTo Finally
	End If

Try:
	Set oDict = SF_Services.CreateScriptService(&quot;Dictionary&quot;, True)
	For i = LBound(Array_2D, 1) To UBound(Array_2D, 1)
		oDict.Add(Array_2D(i, 0), Array_2D(i, 1))
	Next i
		
	ConvertToDictionary = oDict

Finally:
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.ConvertToDictionary

REM -----------------------------------------------------------------------------
Public Function ConvertToRange(Optional ByRef Data As Variant _
								, Optional ByVal Direction As Variant _
								) As Variant
&apos;&apos;&apos;	Create a valid cells range content from a scalar, a 1D array, a 2D array or a data array
&apos;&apos;&apos;	(a &quot;data array&quot; is an array of subarrays). Lower bounds are preserved.
&apos;&apos;&apos;	The individual items are always reduced to strings or doubles. Including booleans or dates.
&apos;&apos;&apos;	The returned scalar or array is a valid argument of the SF_Session.ExecuteCalcFunction() method.
&apos;&apos;&apos;	Input argument may be a data array, typically when returned by a Python script.
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Data: the input scalar, array or data array. If array, must be 1D or 2D, otherwise 0 is returned.
&apos;&apos;&apos;		Direction: the alignment of the range when it is a vector, either &quot;V&quot;[ertical, default] or &quot;H&quot;[orizontal].
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		A scalar or a 2D array compatible with the format of the arguments expected by
&apos;&apos;&apos;		- the SF_Session.ExecuteCalcFunction() method, in the context of [complex] array functions
&apos;&apos;&apos;		- a Calc user-defined function when called by a macro
&apos;&apos;&apos;	Example:
&apos;&apos;&apos;		arr = CreateScriptService(&quot;Array&quot;)
&apos;&apos;&apos;		sess = CreateScriptService(&quot;Session&quot;)
&apos;&apos;&apos;		matrix = Array(Array(-1, 2, 3), Array(4, -5, 6), Array(7, 8, -9))	&apos;	Input = a data array
&apos;&apos;&apos;		result = sess.ExecuteCalcFunction(&quot;ABS&quot;, arr.ConvertToRange(matrix))
&apos;&apos;&apos;				&apos;	result is a data array; result(2)(2) = 9

Dim vRange As Variant				&apos;	Return value
Dim iDims As Integer				&apos;	Number of dimensions of Data
Dim vDataArray As Variant			&apos;	External array of a data array
Dim vVector As Variant				&apos;	Internal array of a data array
Dim lMin1 As Long					&apos;	Lower bound #1
Dim lMax1 As Long					&apos;	Upper bound #1
Dim lMin2 As Long					&apos;	Lower bound #2
Dim lMax2 As Long					&apos;	Upper bound #2
Dim lMin As Long					&apos;	Lower bound
Dim lMax As Long					&apos;	Upper bound
Dim i As Long
Dim j As Long

Const cstThisSub = &quot;Array.ConvertToRange&quot;
Const cstSubArgs = &quot;Data, [Direction=&quot;&quot;&quot;&quot;|&quot;&quot;V&quot;&quot;|&quot;&quot;H&quot;&quot;]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vRange = 0

Check:
	If IsMissing(Direction) Or IsEmpty(Direction) Then Direction = &quot;V&quot;
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If IsMissing(Data) Or IsEmpty(Data) Then
			If Not SF_Utils._Validate(Data, &quot;Data&quot;) Then GoTo Finally
		End If
		If Not SF_Utils._Validate(Direction, &quot;Direction&quot;, V_STRING, Array(&quot;&quot;, &quot;V&quot;, &quot;H&quot;)) Then GoTo Finally
	End If

Try:
	iDims = SF_Array.CountDims(Data)
	If iDims = 0 Or iDims &gt; 2 Then Exit Function

	Select Case iDims
		&apos;	All input items are converted on-the-fly to an acceptable Calc cell value
		Case -1					&apos;	Scalar
			vRange = _ConvertToCellValue(Data, pbIsCell := True)
		Case 1					&apos;	Vector or data array
			&apos;	iDims &lt;&gt; 0 =&gt; Data has at least 1 item
			lMin1 = LBound(Data)	:	lMax1 = UBound(Data)
			If IsArray(Data(lMin1)) Then	&apos;	Data array
				&apos;	Design a 2D array of the appropriate size
				lMin2 = LBound(Data(lMin1))	:	lMax2 = UBound(Data(lMin1))
				vRange = Array()
				ReDim vRange(lMin1 To lMax1, lMin2 To lMax2)
				For i = lMin1 To lMax1
					If Not IsArray(Data(i)) Then
						lMin = -1	:	lMax = -1
					Else
						lMin = LBound(Data(i))
						lMax = UBound(Data(i))
					End If
					For j = lMin2 To lMax2
						If j &lt; lMin Or j &gt; lMax Then vRange(i, j) = &quot;&quot; Else vRange(i, j) = _ConvertToCellValue(Data(i)(j), pbIsCell := True)
					Next j
				Next i
			Else						&apos;	Vector
				vRange = Array()
				If UCase(Direction) = &quot;V&quot; Then		&apos;	Vertical
					ReDim vRange(lMin1 To lMax1, 0 To 0)
					For i = lMin1 To lMax1
						vRange(i, 0) = _ConvertToCellValue(Data(i), pbIsCell := True)
					Next i
				Else								&apos;	Horizontal
					ReDim vRange(0 To 0, lMin1 To lMax1)
					For j = lMin1 To lMax1
						vRange(0, j) = _ConvertToCellValue(Data(j), pbIsCell := True)
					Next j
				End If
			End If
		Case 2
			&apos;	Copy all array items
			vRange = Array()
			lMin1 = LBound(Data, 1)	:	lMax1 = UBound(Data, 1)
			lMin2 = LBound(Data, 2)	:	lMax2 = UBound(Data, 2)
			ReDim vRange(lMin1 To lMax1, lMin2 To lMax2)
			For i = lMin1 To lMax1
				For j = lMin2 To lMax2
					vRange(i, j) = _ConvertToCellValue(Data(i, j), pbIsCell := True)
				Next j
			Next i
		Case Else
	End Select

Finally:
	ConvertToRange = vRange
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.ConvertToRange

REM -----------------------------------------------------------------------------
Public Function Copy(Optional ByRef Array_ND As Variant) As Variant
&apos;&apos;&apos;	Duplicate a 1D or 2D array
&apos;&apos;&apos;	A usual assignment copies an array by reference, i.e. shares the same memory location
&apos;&apos;&apos;		Dim a, b
&apos;&apos;&apos;		a = Array(1, 2, 3)
&apos;&apos;&apos;		b = a
&apos;&apos;&apos;		a(2) = 30
&apos;&apos;&apos;		MsgBox b(2)		&apos;	30
&apos;&apos;&apos;	Args
&apos;&apos;&apos;		Array_ND: the array to copy, may be empty
&apos;&apos;&apos;	Return:
&apos;&apos;&apos;		the copied array. Subarrays however still remain assigned by reference
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		SF_Array.Copy(Array(1, 2, 3)) returns (1, 2, 3)

Dim vCopy As Variant		&apos;	Return value
Dim iDims As Integer		&apos;	Number of dimensions of the input array
Const cstThisSub = &quot;Array.Copy&quot;
Const cstSubArgs = &quot;Array_ND&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vCopy = Array()

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_ND, &quot;Array_ND&quot;) Then GoTo Finally
		iDims = SF_Array.CountDims(Array_ND)
		If iDims &gt; 2 Then
			If Not SF_Utils._ValidateArray(Array_ND, &quot;Array_ND&quot;, 2) Then GoTo Finally
		End If
	End If

Try:
	Select Case iDims
		Case 0
		Case 1
			vCopy = Array_ND
			ReDim Preserve vCopy(LBound(Array_ND) To UBound(Array_ND))
		Case 2
			vCopy = Array_ND
			ReDim Preserve vCopy(LBound(Array_ND, 1) To UBound(Array_ND, 1), LBound(Array_ND, 2) To UBound(Array_ND, 2))
	End Select

Finally:
	Copy = vCopy()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.Copy

REM -----------------------------------------------------------------------------
Public Function CountDims(Optional ByRef Array_ND As Variant) As Integer
&apos;&apos;&apos;	Count the number of dimensions of an array - may be &gt; 2
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array_ND: the array to be examined
&apos;&apos;&apos;	Return: the number of dimensions: -1 = not array, 0 = uninitialized array, else &gt;= 1
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		Dim a(1 To 10, -3 To 12, 5)
&apos;&apos;&apos;		CountDims(a) returns 3

Dim iDims As Integer	&apos;	Return value
Dim lMax As Long		&apos;	Storage for UBound of each dimension
Const cstThisSub = &quot;Array.CountDims&quot;
Const cstSubArgs = &quot;Array_ND&quot;

Check:
	iDims = -1
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If IsMissing(Array_ND) Then		&apos;	To have missing exception processed
			If Not SF_Utils._ValidateArray(Array_ND, &quot;Array_ND&quot;) Then GoTo Finally
		End If
	End If

Try:
	On Local Error Goto ErrHandler
	&apos;	Loop, increasing the dimension index (i) until an error occurs.
	&apos;	An error will occur when i exceeds the number of dimensions in the array. Returns i - 1.
	iDims = 0
	If Not IsArray(Array_ND) Then
	Else
		Do
			iDims = iDims + 1
			lMax = UBound(Array_ND, iDims)
		Loop Until (Err &lt;&gt; 0)
	End If
	
	ErrHandler:
		On Local Error GoTo 0
	
	iDims = iDims - 1
	If iDims = 1 Then
		If LBound(Array_ND, 1) &gt; UBound(Array_ND, 1) Then iDims = 0
	End If

Finally:
	CountDims = iDims
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
End Function	&apos;	ScriptForge.SF_Array.CountDims

REM -----------------------------------------------------------------------------
Public Function Difference(Optional ByRef Array1_1D As Variant _
								, Optional ByRef Array2_1D As Variant _
								, Optional ByVal CaseSensitive As Variant _
								) As Variant
&apos;&apos;&apos;	Build a set being the Difference of the two input arrays, i.e. items are contained in 1st array and NOT in 2nd
&apos;&apos;&apos;		both input arrays must be filled homogeneously, i.e. all items must be of the same type
&apos;&apos;&apos;		Empty and Null items are forbidden
&apos;&apos;&apos;		The comparison between strings is case sensitive or not
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array1_1D: a 1st input array
&apos;&apos;&apos;		Array2_1D: a 2nd input array
&apos;&apos;&apos;		CaseSensitive: default = False
&apos;&apos;&apos;	Returns: a zero-based array containing unique items from the 1st array not present in the 2nd
&apos;&apos;&apos;		The output array is sorted in ascending order
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		SF_Array.Difference(Array(&quot;A&quot;, &quot;C&quot;, &quot;A&quot;, &quot;b&quot;, &quot;B&quot;), Array(&quot;C&quot;, &quot;Z&quot;, &quot;b&quot;), True) returns (&quot;A&quot;, &quot;B&quot;)

Dim vDifference() As Variant	&apos;	Return value
Dim vSorted() As Variant		&apos;	The 2nd input array after sort
Dim iType As Integer			&apos;	VarType of elements in input arrays
Dim lMin1 As Long				&apos;	LBound of 1st input array
Dim lMax1 As Long				&apos;	UBound of 1st input array
Dim lMin2 As Long				&apos;	LBound of 2nd input array
Dim lMax2 As Long				&apos;	UBound of 2nd input array
Dim lSize As Long				&apos;	Number of Difference items
Dim vItem As Variant			&apos;	One single item in the array
Dim i As Long
Const cstThisSub = &quot;Array.Difference&quot;
Const cstSubArgs = &quot;Array1_1D, Array2_1D, [CaseSensitive=False]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vDifference = Array()

Check:
	If IsMissing(CaseSensitive) Then CaseSensitive = False
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array1_1D, &quot;Array1_1D&quot;, 1, 0, True) Then GoTo Finally
		iType = SF_Utils._VarTypeExt(Array1_1D(LBound(Array1_1D)))
		If Not SF_Utils._ValidateArray(Array2_1D, &quot;Array2_1D&quot;, 1, iType, True) Then GoTo Finally
		If Not SF_Utils._Validate(CaseSensitive, &quot;CaseSensitive&quot;, V_BOOLEAN) Then GoTo Finally
	End If

Try:
	lMin1 = LBound(Array1_1D)	:	lMax1 = UBound(Array1_1D)
	lMin2 = LBound(Array2_1D)	:	lMax2 = UBound(Array2_1D)

	&apos;	If 1st array is empty, do nothing
	If lMax1 &lt; lMin1 Then
	ElseIf lMax2 &lt; lMin2 Then	&apos;	only 2nd array is empty
		vUnion = SF_Array.Unique(Array1_1D, CaseSensitive)
	Else

		&apos;	First sort the 2nd array
		vSorted = SF_Array.Sort(Array2_1D, &quot;ASC&quot;, CaseSensitive)

		&apos;	Resize the output array to the size of the 1st array
		ReDim vDifference(0 To (lMax1 - lMin1))
		lSize = -1

		&apos;	Fill vDifference one by one with items present only in 1st set
		For i = lMin1 To lMax1
			vItem = Array1_1D(i)
			If Not SF_Array.Contains(vSorted, vItem, CaseSensitive, &quot;ASC&quot;) Then
				lSize = lSize + 1
				vDifference(lSize) = vItem
			End If
		Next i

		&apos;	Remove unfilled entries and duplicates
		If lSize &gt;= 0 Then
			ReDim Preserve vDifference(0 To lSize)
			vDifference() = SF_Array.Unique(vDifference, CaseSensitive)
		Else
			vDifference = Array()
		End If
	End If

Finally:
	Difference = vDifference()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.Difference

REM -----------------------------------------------------------------------------
Public Function ExportToTextFile(Optional ByRef Array_1D As Variant _
									, Optional ByVal FileName As Variant _
									, Optional ByVal Encoding As Variant _
									) As Boolean
&apos;&apos;&apos;	Write all items of the array sequentially to a text file
&apos;&apos;&apos;	If the file exists already, it will be overwritten without warning
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array_1D: the array to export
&apos;&apos;&apos;		FileName: the full name (path + file) in SF_FileSystem.FileNaming notation
&apos;&apos;&apos;		Encoding: The character set that should be used
&apos;&apos;&apos;				Use one of the Names listed in https://www.iana.org/assignments/character-sets/character-sets.xhtml
&apos;&apos;&apos;				Note that LibreOffice does not implement all existing sets
&apos;&apos;&apos;				Default = UTF-8
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		True if successful
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		SF_Array.ExportToTextFile(Array(&quot;A&quot;,&quot;B&quot;,&quot;C&quot;,&quot;D&quot;), &quot;C:\Temp\A short file.txt&quot;)

Dim bExport As Boolean			&apos;	Return value
Dim oFile As Object				&apos;	Output file handler
Dim sLine As String				&apos;	A single line
Const cstThisSub = &quot;Array.ExportToTextFile&quot;
Const cstSubArgs = &quot;Array_1D, FileName&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	bExport = False

Check:
	If IsMissing(Encoding) Or IsEmpty(Encoding) Then Encoding = &quot;UTF-8&quot;
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_1D, &quot;Array_1D&quot;, 1, V_STRING, True) Then GoTo Finally
		If Not SF_Utils._ValidateFile(FileName, &quot;FileName&quot;) Then GoTo Finally
		If Not SF_Utils._Validate(Encoding, &quot;Encoding&quot;, V_STRING) Then GoTo Finally
	End If

Try:
	Set oFile = SF_FileSystem.CreateTextFile(FileName, Overwrite := True, Encoding := Encoding)
	If Not IsNull(oFile) Then
		With oFile
			For Each sLine In Array_1D
				.WriteLine(sLine)
			Next sLine
			.CloseFile()
		End With
	End If

	bExport = True

Finally:
	If Not IsNull(oFile) Then Set oFile = oFile.Dispose()
	ExportToTextFile = bExport
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.ExportToTextFile

REM -----------------------------------------------------------------------------
Public Function ExtractColumn(Optional ByRef Array_2D As Variant _
								, Optional ByVal ColumnIndex As Variant _
								) As Variant
&apos;&apos;&apos;	ExtractColumn extracts from a 2D array a specific column
&apos;&apos;&apos;	Args
&apos;&apos;&apos;		Array_2D: the array from which to extract
&apos;&apos;&apos;		ColumnIndex: the column to extract - must be in the interval [LBound, UBound]
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		the extracted column. Its LBound and UBound are identical to that of the 1st dimension of Array_2D
&apos;&apos;&apos;	Exceptions:
&apos;&apos;&apos;		ARRAYINDEX1ERROR
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;								|1, 2, 3|
&apos;&apos;&apos;		SF_Array.ExtractColumn(	|4, 5, 6|, 2) returns (3, 6, 9)
&apos;&apos;&apos;								|7, 8, 9|

Dim vExtractColumn As Variant	&apos;	Return value
Dim lMin1 As Long			&apos;	LBound1 of input array
Dim lMax1 As Long			&apos;	UBound1 of input array
Dim lMin2 As Long			&apos;	LBound1 of input array
Dim lMax2 As Long			&apos;	UBound1 of input array
Dim i As Long
Const cstThisSub = &quot;Array.ExtractColumn&quot;
Const cstSubArgs = &quot;Array_2D, ColumnIndex&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vExtractColumn = Array()

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_2D, &quot;Array_2D&quot;, 2) Then GoTo Finally
		If Not SF_Utils._Validate(ColumnIndex, &quot;ColumnIndex&quot;, V_NUMERIC) Then GoTo Finally
	End If

Try:
	&apos;	Compute future dimensions of output array
	lMin2 = LBound(Array_2D, 2)		:	lMax2 = UBound(Array_2D, 2)
	If ColumnIndex &lt; lMin2 Or ColumnIndex &gt; lMax2 Then GoTo CatchIndex
	lMin1 = LBound(Array_2D, 1)		:	lMax1 = UBound(Array_2D, 1)
	ReDim vExtractColumn(lMin1 To lMax1)

	&apos;	Copy Column of input array to output array
	For i = lMin1 To lMax1
		vExtractColumn(i) = Array_2D(i, ColumnIndex)
	Next i

Finally:
	ExtractColumn = vExtractColumn()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
CatchIndex:
	SF_Exception.RaiseFatal(ARRAYINDEX1ERROR, &quot;ColumnIndex&quot;, SF_Array._Repr(Array_2D), ColumnIndex)
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.ExtractColumn

REM -----------------------------------------------------------------------------
Public Function ExtractRow(Optional ByRef Array_2D As Variant _
							, Optional ByVal RowIndex As Variant _
							) As Variant
&apos;&apos;&apos;	ExtractRow extracts from a 2D array a specific row
&apos;&apos;&apos;	Args
&apos;&apos;&apos;		Array_2D: the array from which to extract
&apos;&apos;&apos;		RowIndex: the row to extract - must be in the interval [LBound, UBound]
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		the extracted row. Its LBound and UBound are identical to that of the 2nd dimension of Array_2D
&apos;&apos;&apos;	Exceptions:
&apos;&apos;&apos;		ARRAYINDEX1ERROR
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;							|1, 2, 3|
&apos;&apos;&apos;		SF_Array.ExtractRow(|4, 5, 6|, 2) returns (7, 8, 9)
&apos;&apos;&apos;							|7, 8, 9|

Dim vExtractRow As Variant	&apos;	Return value
Dim lMin1 As Long			&apos;	LBound1 of input array
Dim lMax1 As Long			&apos;	UBound1 of input array
Dim lMin2 As Long			&apos;	LBound1 of input array
Dim lMax2 As Long			&apos;	UBound1 of input array
Dim i As Long
Const cstThisSub = &quot;Array.ExtractRow&quot;
Const cstSubArgs = &quot;Array_2D, RowIndex&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vExtractRow = Array()

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_2D, &quot;Array_2D&quot;, 2) Then GoTo Finally
		If Not SF_Utils._Validate(RowIndex, &quot;RowIndex&quot;, V_NUMERIC) Then GoTo Finally
	End If

Try:
	&apos;	Compute future dimensions of output array
	lMin1 = LBound(Array_2D, 1)		:	lMax1 = UBound(Array_2D, 1)
	If RowIndex &lt; lMin1 Or RowIndex &gt; lMax1 Then GoTo CatchIndex
	lMin2 = LBound(Array_2D, 2)		:	lMax2 = UBound(Array_2D, 2)
	ReDim vExtractRow(lMin2 To lMax2)

	&apos;	Copy row of input array to output array
	For i = lMin2 To lMax2
		vExtractRow(i) = Array_2D(RowIndex, i)
	Next i

Finally:
	ExtractRow = vExtractRow()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
CatchIndex:
	SF_Exception.RaiseFatal(ARRAYINDEX1ERROR, &quot;RowIndex&quot;, SF_Array._Repr(Array_2D), RowIndex)
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.ExtractRow

REM -----------------------------------------------------------------------------
Public Function Flatten(Optional ByRef Array_1D As Variant) As Variant
&apos;&apos;&apos;	Stack all items and all items in subarrays into one array without subarrays
&apos;&apos;&apos;	Args
&apos;&apos;&apos;		Array_1D: the pre-existing array, may be empty
&apos;&apos;&apos;	Return:
&apos;&apos;&apos;		The new flattened array. Its LBound is identical to that of Array_1D
&apos;&apos;&apos;		If one of the subarrays has a number of dimensions &gt; 1 Then that subarray is left unchanged
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		SF_Array.Flatten(Array(1, 2, Array(3, 4, 5)) returns (1, 2, 3, 4, 5)

Dim vFlatten As Variant		&apos;	Return value
Dim lMin As Long			&apos;	LBound of input array
Dim lMax As Long			&apos;	UBound of input array
Dim lIndex As Long			&apos;	Index in output array
Dim vItem As Variant		&apos;	Array single item
Dim iDims As Integer		&apos;	Array number of dimensions
Dim lEmpty As Long			&apos;	Number of empty subarrays
Dim i As Long
Dim j As Long
Const cstThisSub = &quot;Array.Flatten&quot;
Const cstSubArgs = &quot;Array_1D&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vFlatten = Array()

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_1D, &quot;Array_1D&quot;, 1) Then GoTo Finally
	End If

Try:
	If UBound(Array_1D) &gt;= LBound(Array_1D) Then
		lMin = LBound(Array_1D)		:	lMax = UBound(Array_1D)
		ReDim vFlatten(lMin To lMax)	&apos;	Initial minimal sizing
		lEmpty = 0
		lIndex = lMin - 1
		For i = lMin To lMax
			vItem = Array_1D(i)
			If IsArray(vItem) Then
				iDims = SF_Array.CountDims(vItem)
				Select Case iDims
					Case 0			&apos;	Empty arrays are ignored
						lEmpty = lEmpty + 1
					Case 1			&apos;	Only 1D subarrays are flattened
						ReDim Preserve vFlatten(lMin To UBound(vFlatten) + UBound(vItem) - LBound(vItem))
						For j = LBound(vItem) To UBound(vItem)
							lIndex = lIndex + 1
							vFlatten(lIndex) = vItem(j)
						Next j
					Case &gt; 1		&apos;	Other arrays are left unchanged
						lIndex = lIndex + 1
						vFlatten(lIndex) = vItem
				End Select
			Else
				lIndex = lIndex + 1
			 	vFlatten(lIndex) = vItem
			End If
		Next i
	End If
	&apos;	Reduce size of output if Array_1D is populated with some empty arrays
	If lEmpty &gt; 0 Then
		If lIndex - lEmpty &lt; lMin Then
			vFlatten = Array()
		Else
			ReDim Preserve vFlatten(lMin To UBound(vFlatten) - lEmpty)
		End If
	End If

Finally:
	Flatten = vFlatten()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.Flatten

REM -----------------------------------------------------------------------------
Public Function GetProperty(Optional ByVal PropertyName As Variant) As Variant
&apos;&apos;&apos;	Return the actual value of the given property
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		PropertyName: the name of the property as a string
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		The actual value of the property
&apos;&apos;&apos;	Exceptions
&apos;&apos;&apos;		ARGUMENTERROR		The property does not exist

Const cstThisSub = &quot;Array.GetProperty&quot;
Const cstSubArgs = &quot;PropertyName&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	GetProperty = Null

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._Validate(PropertyName, &quot;PropertyName&quot;, V_STRING, Properties()) Then GoTo Catch
	End If

Try:
	Select Case UCase(PropertyName)
		Case Else
	End Select

Finally:
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.GetProperty

REM -----------------------------------------------------------------------------
Public Function ImportFromCSVFile(Optional ByRef FileName As Variant _
									, Optional ByVal Delimiter As Variant _
									, Optional ByVal DateFormat As Variant _
									) As Variant
&apos;&apos;&apos;	Import the data contained in a comma-separated values (CSV) file
&apos;&apos;&apos;	The comma may be replaced by any character
&apos;&apos;&apos;	Each line in the file contains a full record
&apos;&apos;&apos;		Line splitting is not allowed)
&apos;&apos;&apos;		However sequences like \n, \t, ... are left unchanged. Use SF_String.Unescape() to manage them
&apos;&apos;&apos;	A special mechanism is implemented to load dates
&apos;&apos;&apos;	The applicable CSV format is described in https://tools.ietf.org/html/rfc4180
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		FileName: the name of the text file containing the data expressed as given by the current FileNaming
&apos;&apos;&apos;			property of the SF_FileSystem service. Default = both URL format or native format
&apos;&apos;&apos;		Delimiter: 	Default = &quot;,&quot;. Other usual options are &quot;;&quot; and the tab character
&apos;&apos;&apos;		DateFormat: either YYYY-MM-DD, DD-MM-YYYY or MM-DD-YYYY
&apos;&apos;&apos;			The dash (-) may be replaced by a dot (.), a slash (/) or a space
&apos;&apos;&apos;			Other date formats will be ignored
&apos;&apos;&apos;			If &quot;&quot; (default), dates will be considered as strings
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		A 2D-array with each row corresponding with a single record read in the file
&apos;&apos;&apos;		and each column corresponding with a field of the record
&apos;&apos;&apos;		No check is made about the coherence of the field types across columns
&apos;&apos;&apos;			A best guess will be made to identify numeric and date types
&apos;&apos;&apos;		If a line contains less or more fields than the first line in the file,
&apos;&apos;&apos;			an exception will be raised. Empty lines however are simply ignored
&apos;&apos;&apos;		If the size of the file exceeds the number of items limit, a warning is raised
&apos;&apos;&apos;			and the array is truncated
&apos;&apos;&apos;	Exceptions:
&apos;&apos;&apos;		CSVPARSINGERROR		Given file is not formatted as a csv file
&apos;&apos;&apos;		CSVOVERFLOWWARNING	Maximum number of allowed items exceeded

Dim vArray As Variant			&apos;	Returned array
Dim lCol As Long				&apos;	Index of last column of vArray
Dim lRow As Long				&apos;	Index of current row of vArray
Dim lFileSize As Long			&apos;	Number of records found in the file
Dim vCsv As Object				&apos;	CSV file handler
Dim sLine As String				&apos;	Last read line
Dim vLine As Variant			&apos;	Array of fields of last read line
Dim sItem As String				&apos;	Individual item in the file
Dim vItem As Variant			&apos;	Individual item in the output array
Dim iPosition As Integer		&apos;	Date position in individual item
Dim iYear As Integer, iMonth As Integer, iDay As Integer
								&apos;	Date components
Dim bIsoDate As Boolean			&apos;	When True, do not convert dates to Date variables
Dim i As Long

Const cstItemsLimit = 250000	&apos;	Maximum number of admitted items
Const cstThisSub = &quot;Array.ImportFromCSVFile&quot;
Const cstSubArgs = &quot;FileName, [Delimiter=&quot;&quot;,&quot;&quot;], [DateFormat=&quot;&quot;&quot;&quot;]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vArray = Array()

Check:
	If IsMissing(Delimiter) Or IsEmpty(Delimiter) Then Delimiter = &quot;,&quot;
	If IsMissing(DateFormat) Or IsEmpty(DateFormat) Then DateFormat = &quot;&quot;
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateFile(FileName, &quot;FileName&quot;) Then GoTo Finally
		If Not SF_Utils._Validate(Delimiter, &quot;Delimiter&quot;, V_STRING) Then GoTo Finally
		If Not SF_Utils._Validate(DateFormat, &quot;DateFormat&quot;, V_STRING) Then GoTo Finally
	End If
	If Len(Delimiter) = 0 Then Delimiter = &quot;,&quot;

Try:
	bIsoDate = _SF_.TriggeredByPython	&apos;	Dates are not converted
	&apos;	Counts the lines present in the file to size the final array
	&apos;		Very beneficial for large files, better than multiple ReDims
	&apos;		Small overhead for small files
	lFileSize = SF_FileSystem._CountTextLines(FileName, False)
	If lFileSize &lt;= 0 Then GoTo Finally

	&apos;	Reread file line by line
	Set vCsv = SF_FileSystem.OpenTextFile(FileName, IOMode := SF_FileSystem.ForReading)
	If IsNull(vCsv) Then GoTo Finally	&apos;	Open error
	lRow = -1
	With vCsv
		Do While Not .AtEndOfStream
			sLine = .ReadLine()
			If Len(sLine) &gt; 0 Then		&apos;	Ignore empty lines
				If InStr(sLine, &quot;&quot;&quot;&quot;) &gt; 0 Then vLine = SF_String.SplitNotQuoted(sLine, Delimiter) Else vLine = Split(sLine, Delimiter)	&apos; Simple split when relevant
				lRow = lRow + 1
				If lRow = 0 Then		&apos;	Initial sizing of output array
					lCol = UBound(vLine)
					ReDim vArray(0 To lFileSize - 1, 0 To lCol)
				ElseIf UBound(vLine) &lt;&gt; lCol Then
					GoTo CatchCSVFormat
				End If
				&apos;	Check type and copy all items of the line
				For i = 0 To lCol
					If Left(vLine(i), 1) = &quot;&quot;&quot;&quot; Then sItem = SF_String.Unquote(vLine(i)) Else sItem = vLine(i)	&apos; Unquote only when useful
					&apos;	Interpret the individual line item
					Select Case True
						Case IsNumeric(sItem)
							If InStr(sItem, &quot;.&quot;) + InStr(1, sItem, &quot;e&quot;, 1) &gt; 0 Then vItem = Val(sItem) Else vItem = CLng(sItem)
						Case DateFormat &lt;&gt; &quot;&quot; And Len(sItem) = Len(DateFormat)
							If SF_String.IsADate(sItem, DateFormat) Then
								iPosition = InStr(DateFormat, &quot;YYYY&quot;)	:	iYear = CInt(Mid(sItem, iPosition, 4))
								iPosition = InStr(DateFormat, &quot;MM&quot;)		:	iMonth = CInt(Mid(sItem, iPosition, 2))
								iPosition = InStr(DateFormat, &quot;DD&quot;)		:	iDay = CInt(Mid(sItem, iPosition, 2))
								vItem = DateSerial(iYear, iMonth, iDay)
								If bIsoDate Then vItem = SF_Utils._CDateToIso(vItem)	&apos;	Called from Python
							Else
								vItem = sItem
							End If
						Case Else		:	vItem = sItem
					End Select
					vArray(lRow, i) = vItem
				Next i
			End If
			&apos;	Provision to avoid very large arrays and their sometimes erratic behaviour
			If (lRow + 2) * (lCol + 1) &gt; cstItemsLimit Then
				ReDim Preserve vArray(0 To lRow, 0 To lCol)
				GoTo CatchOverflow
			End If
		Loop
	End With

Finally:
	If Not IsNull(vCsv) Then
		vCsv.CloseFile()
		Set vCsv = vCsv.Dispose()
	End If
	ImportFromCSVFile = vArray
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
CatchCSVFormat:
	SF_Exception.RaiseFatal(CSVPARSINGERROR, FileName, vCsv.Line, sLine)
	GoTo Finally
CatchOverflow:
	&apos;TODO SF_Exception.RaiseWarning(SF_Exception.CSVOVERFLOWWARNING, cstThisSub)
	&apos;MsgBox &quot;TOO MUCH LINES !!&quot;
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.ImportFromCSVFile

REM -----------------------------------------------------------------------------
Public Function IndexOf(Optional ByRef Array_1D As Variant _
							, Optional ByVal ToFind As Variant _
							, Optional ByVal CaseSensitive As Variant _
							, Optional ByVal SortOrder As Variant _
							) As Long
&apos;&apos;&apos;	Finds in a 1D array the ToFind number, string or date
&apos;&apos;&apos;	ToFind must exist within the array.
&apos;&apos;&apos;	The comparison between strings can be done case-sensitively or not
&apos;&apos;&apos;	If the array is sorted then
&apos;&apos;&apos;		the array must be filled homogeneously, i.e. all items must be of the same type
&apos;&apos;&apos;		Empty and Null items are forbidden
&apos;&apos;&apos;		a binary search is done
&apos;&apos;&apos;	Otherwise the array is scanned from top. Null or Empty items are simply ignored
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array_1D: the array to scan
&apos;&apos;&apos;		ToFind: a number, a date or a string to find
&apos;&apos;&apos;		CaseSensitive: Only for string comparisons, default = False
&apos;&apos;&apos;		SortOrder: &quot;ASC&quot;, &quot;DESC&quot; or &quot;&quot; (= not sorted, default)
&apos;&apos;&apos;	Return: the index of the found item, LBound - 1 if not found
&apos;&apos;&apos;		Result is unpredictable when array is announced sorted and is in reality not
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		SF_Array.IndexOf(Array(&quot;A&quot;,&quot;B&quot;,&quot;c&quot;,&quot;D&quot;), &quot;C&quot;, SortOrder := &quot;ASC&quot;) returns 2
&apos;&apos;&apos;		SF_Array.IndexOf(Array(&quot;A&quot;,&quot;B&quot;,&quot;c&quot;,&quot;D&quot;), &quot;C&quot;, CaseSensitive := True) returns -1

Dim vFindItem As Variant			&apos;	2-items array (0) = True if found, (1) = Index where found
Dim lIndex As Long					&apos;	Return value
Dim iToFindType As Integer			&apos;	VarType of ToFind
Const cstThisSub = &quot;Array.IndexOf&quot;
Const cstSubArgs = &quot;Array_1D, ToFind, [CaseSensitive=False], [SortOrder=&quot;&quot;&quot;&quot;|&quot;&quot;ASC&quot;&quot;|&quot;&quot;DESC&quot;&quot;]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch

	lIndex = -1

Check:
	If IsMissing(CaseSensitive) Or IsEmpty(CaseSensitive) Then CaseSensitive = False
	If IsMissing(SortOrder) Or IsEmpty(SortOrder) Then SortOrder = &quot;&quot;
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._Validate(SortOrder, &quot;SortOrder&quot;, V_STRING, Array(&quot;ASC&quot;, &quot;DESC&quot;, &quot;&quot;)) Then GoTo Finally
		If Not SF_Utils._Validate(ToFind, &quot;ToFind&quot;, Array(V_STRING, V_DATE, V_NUMERIC)) Then GoTo Finally
		iToFindType = SF_Utils._VarTypeExt(ToFind)
		If SortOrder &lt;&gt; &quot;&quot; Then
			If Not SF_Utils._ValidateArray(Array_1D, &quot;Array&quot;, 1, iToFindType) Then GoTo Finally
		Else
			If Not SF_Utils._ValidateArray(Array_1D, &quot;Array&quot;, 1) Then GoTo Finally
		End If
		If Not SF_Utils._Validate(CaseSensitive, &quot;CaseSensitive&quot;, V_BOOLEAN) Then GoTo Finally
	End If

Try:
	vFindItem = SF_Array._FindItem(Array_1D, ToFind, CaseSensitive, UCase(SortOrder))
	If vFindItem(0) = True Then lIndex = vFindItem(1) Else lIndex = LBound(Array_1D) - 1

Finally:
	IndexOf = lIndex
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.IndexOf

REM -----------------------------------------------------------------------------
Public Function Insert(Optional ByRef Array_1D As Variant _
						, Optional ByVal Before As Variant _
						, ParamArray pvArgs() As Variant _
						) As Variant
&apos;&apos;&apos;	Insert before the index Before of the input array the items listed as arguments
&apos;&apos;&apos;		Arguments are inserted blindly
&apos;&apos;&apos;			each of them might be a scalar of any type or a subarray
&apos;&apos;&apos;	Args
&apos;&apos;&apos;		Array_1D: the pre-existing array, may be empty
&apos;&apos;&apos;		Before: the index before which to insert; must be in the interval [LBound, UBound + 1]
&apos;&apos;&apos;		pvArgs: a list of items to Insert inside Array_1D
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		the new rxtended array. Its LBound is identical to that of Array_1D
&apos;&apos;&apos;	Exceptions:
&apos;&apos;&apos;		ARRAYINSERTERROR
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		SF_Array.Insert(Array(1, 2, 3), 2, 4, 5) returns (1, 2, 4, 5, 3)

Dim vInsert As Variant		&apos;	Return value
Dim lNbArgs As Long			&apos;	Number of elements to Insert
Dim lMin As Long			&apos;	LBound of input array
Dim lMax As Long			&apos;	UBound of input array
Dim i As Long
Const cstThisSub = &quot;Array.Insert&quot;
Const cstSubArgs = &quot;Array_1D, Before, arg0[, arg1] ...&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vInsert = Array()

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_1D, &quot;Array_1D&quot;, 1) Then GoTo Finally
		If Not SF_Utils._Validate(Before, &quot;Before&quot;, V_NUMERIC) Then GoTo Finally
		If Before &lt; LBound(Array_1D) Or Before &gt; UBound(Array_1D) + 1 Then GoTo CatchArgument
	End If

Try:
	lNbArgs = UBound(pvArgs) + 1	&apos;	pvArgs is always zero-based
	lMin = LBound(Array_1D)			&apos;	= LBound(vInsert)
	lMax = UBound(Array_1D)			&apos;	&lt;&gt; UBound(vInsert)
	If lNbArgs &gt; 0 Then
		ReDim vInsert(lMin To lMax + lNbArgs)
		For i = lMin To UBound(vInsert)
			If i &lt; Before Then
				vInsert(i) = Array_1D(i)
			ElseIf i &lt; Before + lNbArgs Then
				vInsert(i) = pvArgs(i - Before)
			Else
				vInsert(i) = Array_1D(i - lNbArgs)
			End If
		Next i
	Else
		vInsert() = Array_1D()
	End If

Finally:
	Insert = vInsert()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
CatchArgument:
	&apos;TODO SF_Exception.RaiseFatal(ARRAYINSERTERROR, cstThisSub)
	MsgBox &quot;INVALID ARGUMENT VALUE !!&quot;
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.Insert

REM -----------------------------------------------------------------------------
Public Function InsertSorted(Optional ByRef Array_1D As Variant _
						, Optional ByVal Item As Variant _
						, Optional ByVal SortOrder As Variant _
						, Optional ByVal CaseSensitive As Variant _
						) As Variant
&apos;&apos;&apos;	Insert in a sorted array a new item on its place
&apos;&apos;&apos;		the array must be filled homogeneously, i.e. all items must be of the same type
&apos;&apos;&apos;		Empty and Null items are forbidden
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array_1D: the array to sort
&apos;&apos;&apos;		Item: the scalar value to insert, same type as the existing array items
&apos;&apos;&apos;		SortOrder: &quot;ASC&quot; (default) or &quot;DESC&quot;
&apos;&apos;&apos;		CaseSensitive: Default = False
&apos;&apos;&apos;	Returns: the extended sorted array with same LBound as input array
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		InsertSorted(Array(&quot;A&quot;, &quot;C&quot;, &quot;a&quot;, &quot;b&quot;), &quot;B&quot;, CaseSensitive := True) returns (&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;a&quot;, &quot;b&quot;)

Dim vSorted() As Variant	&apos;	Return value
Dim iType As Integer		&apos;	VarType of elements in input array
Dim lMin As Long			&apos;	LBound of input array
Dim lMax As Long			&apos;	UBound of input array
Dim lIndex As Long			&apos;	Place where to insert new item
Const cstThisSub = &quot;Array.InsertSorted&quot;
Const cstSubArgs = &quot;Array_1D, Item, [SortOrder=&quot;&quot;ASC&quot;&quot;|&quot;&quot;DESC&quot;&quot;], [CaseSensitive=False]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vSorted = Array()

Check:
	If IsMissing(SortOrder) Or IsEmpty(SortOrder) Then SortOrder = &quot;ASC&quot;
	If IsMissing(CaseSensitive) Or IsEmpty(CaseSensitive) Then CaseSensitive = False
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_1D, &quot;Array_1D&quot;, 1, 0) Then GoTo Finally
		If LBound(Array_1D) &lt;= UBound(Array_1D) Then
			iType = SF_Utils._VarTypeExt(Array_1D(LBound(Array_1D)))
			If Not SF_Utils._Validate(Item, &quot;Item&quot;, iType) Then GoTo Finally
		Else
			If Not SF_Utils._Validate(Item, &quot;Item&quot;, Array(V_STRING, V_DATE, V_NUMERIC)) Then GoTo Finally
		End If
		If Not SF_Utils._Validate(SortOrder, &quot;SortOrder&quot;, V_STRING, Array(&quot;ASC&quot;,&quot;DESC&quot;)) Then GoTo Finally
		If Not SF_Utils._Validate(CaseSensitive, &quot;CaseSensitive&quot;, V_BOOLEAN) Then GoTo Finally
	End If

Try:
	lMin = LBound(Array_1D)
	lMax = UBound(Array_1D)
	lIndex = SF_Array._FindItem(Array_1D, Item, CaseSensitive, UCase(SortOrder))(1)
	vSorted = SF_Array.Insert(Array_1D, lIndex, Item)

Finally:
	InsertSorted = vSorted()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.InsertSorted

REM -----------------------------------------------------------------------------
Public Function Intersection(Optional ByRef Array1_1D As Variant _
								, Optional ByRef Array2_1D As Variant _
								, Optional ByVal CaseSensitive As Variant _
								) As Variant
&apos;&apos;&apos;	Build a set being the intersection of the two input arrays, i.e. items are contained in both arrays
&apos;&apos;&apos;		both input arrays must be filled homogeneously, i.e. all items must be of the same type
&apos;&apos;&apos;		Empty and Null items are forbidden
&apos;&apos;&apos;		The comparison between strings is case sensitive or not
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array1_1D: a 1st input array
&apos;&apos;&apos;		Array2_1D: a 2nd input array
&apos;&apos;&apos;		CaseSensitive: default = False
&apos;&apos;&apos;	Returns: a zero-based array containing unique items stored in both input arrays
&apos;&apos;&apos;		The output array is sorted in ascending order
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		Intersection(Array(&quot;A&quot;, &quot;C&quot;, &quot;A&quot;, &quot;b&quot;, &quot;B&quot;), Array(&quot;C&quot;, &quot;Z&quot;, &quot;b&quot;), True) returns (&quot;C&quot;, &quot;b&quot;)

Dim vIntersection() As Variant	&apos;	Return value
Dim vSorted() As Variant		&apos;	The shortest input array after sort
Dim iType As Integer			&apos;	VarType of elements in input arrays
Dim lMin1 As Long				&apos;	LBound of 1st input array
Dim lMax1 As Long				&apos;	UBound of 1st input array
Dim lMin2 As Long				&apos;	LBound of 2nd input array
Dim lMax2 As Long				&apos;	UBound of 2nd input array
Dim lMin As Long				&apos;	LBound of unsorted array
Dim lMax As Long				&apos;	UBound of unsorted array
Dim iShortest As Integer		&apos;	1 or 2 depending on shortest input array
Dim lSize As Long				&apos;	Number of Intersection items
Dim vItem As Variant			&apos;	One single item in the array
Dim i As Long
Const cstThisSub = &quot;Array.Intersection&quot;
Const cstSubArgs = &quot;Array1_1D, Array2_1D, [CaseSensitive=False]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vIntersection = Array()

Check:
	If IsMissing(CaseSensitive) Then CaseSensitive = False
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array1_1D, &quot;Array1_1D&quot;, 1, 0, True) Then GoTo Finally
		iType = SF_Utils._VarTypeExt(Array1_1D(LBound(Array1_1D)))
		If Not SF_Utils._ValidateArray(Array2_1D, &quot;Array2_1D&quot;, 1, iType, True) Then GoTo Finally
		If Not SF_Utils._Validate(CaseSensitive, &quot;CaseSensitive&quot;, V_BOOLEAN) Then GoTo Finally
	End If

Try:
	lMin1 = LBound(Array1_1D)	:	lMax1 = UBound(Array1_1D)
	lMin2 = LBound(Array2_1D)	:	lMax2 = UBound(Array2_1D)

	&apos;	If one of both arrays is empty, do nothing
	If lMax1 &gt;= lMin1 And lMax2 &gt;= lMin2 Then

		&apos;	First sort the shortest array
		If lMax1 - lMin1 &lt;= lMax2 - lMin2 Then
			iShortest = 1
			vSorted = SF_Array.Sort(Array1_1D, &quot;ASC&quot;, CaseSensitive)
			lMin = lMin2	:	lMax = lMax2		&apos;	Bounds of unsorted array
		Else
			iShortest = 2
			vSorted = SF_Array.Sort(Array2_1D, &quot;ASC&quot;, CaseSensitive)
			lMin = lMin1	:	lMax = lMax1		&apos;	Bounds of unsorted array
		End If

		&apos;	Resize the output array to the size of the shortest array
		ReDim vIntersection(0 To (lMax - lMin))
		lSize = -1

		&apos;	Fill vIntersection one by one only with items present in both sets
		For i = lMin To lMax
			If iShortest = 1 Then vItem = Array2_1D(i) Else vItem = Array1_1D(i)	&apos;	Pick in unsorted array
			If SF_Array.Contains(vSorted, vItem, CaseSensitive, &quot;ASC&quot;) Then
				lSize = lSize + 1
				vIntersection(lSize) = vItem
			End If
		Next i

		&apos;	Remove unfilled entries and duplicates
		If lSize &gt;= 0 Then
			ReDim Preserve vIntersection(0 To lSize)
			vIntersection() = SF_Array.Unique(vIntersection, CaseSensitive)
		Else
			vIntersection = Array()
		End If
	End If

Finally:
	Intersection = vIntersection()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.Intersection

REM -----------------------------------------------------------------------------
Public Function Join2D(Optional ByRef Array_2D As Variant _
							, Optional ByVal ColumnDelimiter As Variant _
							, Optional ByVal RowDelimiter As Variant _
							, Optional ByVal Quote As Variant _
							) As String
&apos;&apos;&apos;	Join a two-dimensional array with two delimiters, one for columns, one for rows
&apos;&apos;&apos;	Args: 
&apos;&apos;&apos;		Array_2D: each item must be either a String, a number, a Date or a Boolean
&apos;&apos;&apos;		ColumnDelimiter: delimits each column (default = Tab/Chr(9))
&apos;&apos;&apos;		RowDelimiter: delimits each row (default = LineFeed/Chr(10))
&apos;&apos;&apos;		Quote: if True, protect strings with double quotes (default = False)
&apos;&apos;&apos;	Return:
&apos;&apos;&apos;		A string after conversion of numbers and dates
&apos;&apos;&apos;		Invalid items are replaced by a zero-length string
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;							| 1, 2, &quot;A&quot;, [2020-02-29], 5      |
&apos;&apos;&apos;		SF_Array.Join_2D(	| 6, 7, &quot;this is a string&quot;, 9, 10 |	, &quot;,&quot;, &quot;/&quot;)
&apos;&apos;&apos;					&apos; &quot;1,2,A,2020-02-29 00:00:00,5/6,7,this is a string,9,10&quot;

Dim sJoin As String				&apos;	The return value
Dim sItem As String				&apos;	The string representation of a single item
Dim vItem As Variant			&apos;	Single item
Dim lMin1 As Long					&apos;	LBound1 of input array
Dim lMax1 As Long					&apos;	UBound1 of input array
Dim lMin2 As Long					&apos;	LBound2 of input array
Dim lMax2 As Long					&apos;	UBound2 of input array
Dim i As Long
Dim j As Long
Const cstThisSub = &quot;Array.Join2D&quot;
Const cstSubArgs = &quot;Array_2D, [ColumnDelimiter=Chr(9)], [RowDelimiter=Chr(10)], [Quote=False]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	sJoin = &quot;&quot;

Check:
	If IsMissing(ColumnDelimiter) Or IsEmpty(ColumnDelimiter) Then ColumnDelimiter = Chr(9)
	If IsMissing(RowDelimiter) Or IsEmpty(RowDelimiter) Then RowDelimiter = Chr(10)
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_2D, &quot;Array_2D&quot;, 2) Then GoTo Finally
		If Not SF_Utils._Validate(ColumnDelimiter, &quot;ColumnDelimiter&quot;, V_STRING) Then GoTo Finally
		If Not SF_Utils._Validate(RowDelimiter, &quot;RowDelimiter&quot;, V_STRING) Then GoTo Finally
		If Not SF_Utils._Validate(Quote, &quot;Quote&quot;, V_BOOLEAN) Then GoTo Finally
	End If

Try:
	lMin1 = LBound(Array_2D, 1)			:	lMax1 = UBound(Array_2D, 1)
	lMin2 = LBound(Array_2D, 2)			:	lMax2 = UBound(Array_2D, 2)
	If lMin1 &lt;= lMax1 Then
		For i = lMin1 To lMax1
			For j = lMin2 To lMax2
				vItem = Array_2D(i, j)
				Select Case SF_Utils._VarTypeExt(vItem)
					Case V_STRING			:	If Quote Then sItem = SF_String.Quote(vItem) Else sItem = vItem
					Case V_NUMERIC, V_DATE	:	sItem = SF_Utils._Repr(vItem)
					Case V_BOOLEAN			:	sItem = Iif(vItem, &quot;True&quot;, &quot;False&quot;)	&apos;TODO: L10N
					Case Else		:	sItem = &quot;&quot;
				End Select
				sJoin = sJoin &amp; sItem &amp; Iif(j &lt; lMax2, ColumnDelimiter, &quot;&quot;)
			Next j
			sJoin = sJoin &amp; Iif(i &lt; lMax1, RowDelimiter, &quot;&quot;)
		Next i
	End If

Finally:
	Join2D = sJoin
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.Join2D

REM -----------------------------------------------------------------------------
Public Function Methods() As Variant
&apos;&apos;&apos;	Return the list of public methods of the Array service as an array

	Methods = Array( _
					&quot;Append&quot; _
					, &quot;AppendColumn&quot; _
					, &quot;AppendRow&quot; _
					, &quot;Contains&quot; _
					, &quot;ConvertFromDataArray&quot; _
					, &quot;ConvertToDataArray&quot; _
					, &quot;ConvertToDictionary&quot; _
					, &quot;ConvertToRange&quot; _
					, &quot;Copy&quot; _
					, &quot;CountDims&quot; _
					, &quot;Difference&quot; _
					, &quot;ExportToTextFile&quot; _
					, &quot;ExtractColumn&quot; _
					, &quot;ExtractRow&quot; _
					, &quot;Flatten&quot; _
					, &quot;ImportFromCSVFile&quot; _
					, &quot;IndexOf&quot; _
					, &quot;Insert&quot; _
					, &quot;InsertSorted&quot; _
					, &quot;Intersection&quot; _
					, &quot;Join2D&quot; _
					, &quot;Prepend&quot; _
					, &quot;PrependColumn&quot; _
					, &quot;PrependRow&quot; _
					, &quot;RangeInit&quot; _
					, &quot;Reverse&quot; _
					, &quot;Shuffle&quot; _
					, &quot;Slice&quot; _
					, &quot;Sort&quot; _
					, &quot;SortColumns&quot; _
					, &quot;SortRows&quot; _
					, &quot;Transpose&quot; _
					, &quot;TrimArray&quot; _
					, &quot;Union&quot; _
					, &quot;Unique&quot; _
					)

End Function	&apos;	ScriptForge.SF_Array.Methods

REM -----------------------------------------------------------------------------
Public Function Prepend(Optional ByRef Array_1D As Variant _
						, ParamArray pvArgs() As Variant _
						) As Variant
&apos;&apos;&apos;	Prepend at the beginning of the input array the items listed as arguments
&apos;&apos;&apos;		Arguments are Prepended blindly
&apos;&apos;&apos;			each of them might be a scalar of any type or a subarray
&apos;&apos;&apos;	Args
&apos;&apos;&apos;		Array_1D: the pre-existing array, may be empty
&apos;&apos;&apos;		pvArgs: a list of items to Prepend to Array_1D
&apos;&apos;&apos;	Return: the new rxtended array. Its LBound is identical to that of Array_1D
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		SF_Array.Prepend(Array(1, 2, 3), 4, 5) returns (4, 5, 1, 2, 3)

Dim vPrepend As Variant		&apos;	Return value
Dim lNbArgs As Long			&apos;	Number of elements to Prepend
Dim lMin As Long			&apos;	LBound of input array
Dim lMax As Long			&apos;	UBound of input array
Dim i As Long
Const cstThisSub = &quot;Array.Prepend&quot;
Const cstSubArgs = &quot;Array_1D, arg0[, arg1] ...&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vPrepend = Array()

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_1D, &quot;Array_1D&quot;, 1) Then GoTo Finally
	End If

Try:
	lNbArgs = UBound(pvArgs) + 1	&apos;	pvArgs is always zero-based
	lMin = LBound(Array_1D)			&apos;	= LBound(vPrepend)
	lMax = UBound(Array_1D)			&apos;	&lt;&gt; UBound(vPrepend)
	If lMax &lt; LBound(Array_1D) And lNbArgs &gt; 0 Then	&apos;	Initial array is empty
		ReDim vPrepend(0 To lNbArgs - 1)
	Else
		ReDim vPrepend(lMin To lMax + lNbArgs)
	End If
	For i = lMin To UBound(vPrepend)
		If i &lt; lMin + lNbArgs Then vPrepend(i) = pvArgs(i - lMin) Else vPrepend(i) = Array_1D(i - lNbArgs)
	Next i

Finally:
	Prepend = vPrepend
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.Prepend

REM -----------------------------------------------------------------------------
Public Function PrependColumn(Optional ByRef Array_2D As Variant _
								, Optional ByRef Column As Variant _
								) As Variant
&apos;&apos;&apos;	PrependColumn prepends to the left side of a 2D array a new Column
&apos;&apos;&apos;	Args
&apos;&apos;&apos;		Array_2D: the pre-existing array, may be empty
&apos;&apos;&apos;			If the array has 1 dimension, it is considered as the last Column of the resulting 2D array
&apos;&apos;&apos;		Column: a 1D array with as many items as there are rows in Array_2D
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		the new rxtended array. Its LBounds are identical to that of Array_2D
&apos;&apos;&apos;	Exceptions:
&apos;&apos;&apos;		ARRAYINSERTERROR
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		SF_Array.PrependColumn(Array(1, 2, 3), Array(4, 5, 6)) returns ((4, 1), (5, 2), (6, 3))
&apos;&apos;&apos;		x = SF_Array.PrependColumn(Array(), Array(1, 2, 3)) =&gt; ∀ i ∈ {0 ≤ i ≤ 2} : x(0, i) ≡ i

Dim vPrependColumn As Variant	&apos;	Return value
Dim iDims As Integer			&apos;	Dimensions of Array_2D
Dim lMin1 As Long				&apos;	LBound1 of input array
Dim lMax1 As Long				&apos;	UBound1 of input array
Dim lMin2 As Long				&apos;	LBound2 of input array
Dim lMax2 As Long				&apos;	UBound2 of input array
Dim lMin As Long				&apos;	LBound of Column array
Dim lMax As Long				&apos;	UBound of Column array
Dim i As Long
Dim j As Long
Const cstThisSub = &quot;Array.PrependColumn&quot;
Const cstSubArgs = &quot;Array_2D, Column&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vPrependColumn = Array()

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_2D, &quot;Array_2D&quot;) Then GoTo Finally	&apos;Initial check: not missing and array
		If Not SF_Utils._ValidateArray(Column, &quot;Column&quot;, 1) Then GoTo Finally
	End If
	iDims = SF_Array.CountDims(Array_2D)
	If iDims &gt; 2 Then
		If Not SF_Utils._ValidateArray(Array_2D, &quot;Array_2D&quot;, 2) Then GoTo Finally	&apos;2nd check to manage error
	End If

Try:
	lMin = LBound(Column)
	lMax = UBound(Column)

	&apos;	Compute future dimensions of output array
	Select Case iDims
		Case 0		:	lMin1 = lMin					:	lMax1 = lMax
						lMin2 = 0						:	lMax2 = -1
		Case 1		:	lMin1 = LBound(Array_2D, 1)		:	lMax1 = UBound(Array_2D, 1)
						lMin2 = 0						:	lMax2 = 0
		Case 2		:	lMin1 = LBound(Array_2D, 1)		:	lMax1 = UBound(Array_2D, 1)
						lMin2 = LBound(Array_2D, 2)		:	lMax2 = UBound(Array_2D, 2)
	End Select
	If iDims &gt; 0 And lMax - lMin &lt;&gt; lMax1 - lMin1 Then GoTo CatchColumn
	ReDim vPrependColumn(lMin1 To lMax1, lMin2 To lMax2 + 1)

	&apos;	Copy input array to output array
	For i = lMin1 To lMax1
		For j = lMin2 + 1 To lMax2 + 1
			If iDims = 2 Then vPrependColumn(i, j) = Array_2D(i, j - 1) Else vPrependColumn(i, j) = Array_2D(i)
		Next j
	Next i
	&apos;	Copy new Column
	For i = lMin1 To lMax1
		vPrependColumn(i, lMin2) = Column(i)
	Next i

Finally:
	PrependColumn = vPrependColumn()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
CatchColumn:
	SF_Exception.RaiseFatal(ARRAYINSERTERROR, &quot;Column&quot;, SF_Array._Repr(Array_2D), SF_Utils._Repr(Column, MAXREPR))
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.PrependColumn

REM -----------------------------------------------------------------------------
Public Function PrependRow(Optional ByRef Array_2D As Variant _
							, Optional ByRef Row As Variant _
							) As Variant
&apos;&apos;&apos;	PrependRow prepends on top of a 2D array a new row
&apos;&apos;&apos;	Args
&apos;&apos;&apos;		Array_2D: the pre-existing array, may be empty
&apos;&apos;&apos;			If the array has 1 dimension, it is considered as the last row of the resulting 2D array
&apos;&apos;&apos;		Row: a 1D array with as many items as there are columns in Array_2D
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		the new rxtended array. Its LBounds are identical to that of Array_2D
&apos;&apos;&apos;	Exceptions:
&apos;&apos;&apos;		ARRAYINSERTERROR
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		SF_Array.PrependRow(Array(1, 2, 3), Array(4, 5, 6)) returns ((4, 5, 6), (1, 2, 3))
&apos;&apos;&apos;		x = SF_Array.PrependColumn(Array(), Array(1, 2, 3) =&gt; ∀ i ∈ {0 ≤ i ≤ 2} : x(i, 0) ≡ i

Dim vPrependRow As Variant	&apos;	Return value
Dim iDims As Integer		&apos;	Dimensions of Array_2D
Dim lMin1 As Long			&apos;	LBound1 of input array
Dim lMax1 As Long			&apos;	UBound1 of input array
Dim lMin2 As Long			&apos;	LBound2 of input array
Dim lMax2 As Long			&apos;	UBound2 of input array
Dim lMin As Long			&apos;	LBound of row array
Dim lMax As Long			&apos;	UBound of row array
Dim i As Long
Dim j As Long
Const cstThisSub = &quot;Array.PrependRow&quot;
Const cstSubArgs = &quot;Array_2D, Row&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vPrependRow = Array()

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_2D, &quot;Array_2D&quot;) Then GoTo Finally	&apos;Initial check: not missing and array
		If Not SF_Utils._ValidateArray(Row, &quot;Row&quot;, 1) Then GoTo Finally
	End If
	iDims = SF_Array.CountDims(Array_2D)
	If iDims &gt; 2 Then
		If Not SF_Utils._ValidateArray(Array_2D, &quot;Array_2D&quot;, 2) Then GoTo Finally	&apos;2nd check to manage error
	End If

Try:
	lMin = LBound(Row)
	lMax = UBound(Row)

	&apos;	Compute future dimensions of output array
	Select Case iDims
		Case 0		:	lMin1 = 0						:	lMax1 = -1
						lMin2 = lMin					:	lMax2 = lMax
		Case 1		:	lMin1 = 0						:	lMax1 = 0
						lMin2 = LBound(Array_2D, 1)		:	lMax2 = UBound(Array_2D, 1)
		Case 2		:	lMin1 = LBound(Array_2D, 1)		:	lMax1 = UBound(Array_2D, 1)
						lMin2 = LBound(Array_2D, 2)		:	lMax2 = UBound(Array_2D, 2)
	End Select
	If iDims &gt; 0 And lMax - lMin &lt;&gt; lMax2 - lMin2 Then GoTo CatchRow
	ReDim vPrependRow(lMin1 To lMax1 + 1, lMin2 To lMax2)

	&apos;	Copy input array to output array
	For i = lMin1 + 1 To lMax1 + 1
		For j = lMin2 To lMax2
			If iDims = 2 Then vPrependRow(i, j) = Array_2D(i - 1, j) Else vPrependRow(i, j) = Array_2D(j)
		Next j
	Next i
	&apos;	Copy new row
	For j = lMin2 To lMax2
		vPrependRow(lMin1, j) = Row(j)
	Next j

Finally:
	PrependRow = vPrependRow()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
CatchRow:
	SF_Exception.RaiseFatal(ARRAYINSERTERROR, &quot;Row&quot;, SF_Array._Repr(Array_2D), SF_Utils._Repr(Row, MAXREPR))
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.PrependRow

REM -----------------------------------------------------------------------------
Public Function Properties() As Variant
&apos;&apos;&apos;	Return the list or properties as an array

	Properties = Array( _
					)

End Function	&apos;	ScriptForge.SF_Array.Properties

REM -----------------------------------------------------------------------------
Public Function RangeInit(Optional ByVal From As Variant _
							, Optional ByVal UpTo As Variant _
							, Optional ByVal ByStep As Variant _
							) As Variant
&apos;&apos;&apos;	Initialize a new zero-based array with numeric values
&apos;&apos;&apos;	Args: all numeric
&apos;&apos;&apos;		From: value of first item
&apos;&apos;&apos;		UpTo: last item should not exceed UpTo
&apos;&apos;&apos;		ByStep: difference between 2 successive items
&apos;&apos;&apos;	Return: the new array
&apos;&apos;&apos;	Exceptions:
&apos;&apos;&apos;		ARRAYSEQUENCEERROR	Wrong arguments, f.i. UpTo &lt; From with ByStep &gt; 0
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		SF_Array.RangeInit(10, 1, -1) returns (10, 9, 8, 7, 6, 5, 4, 3, 2, 1)

Dim lIndex As Long			&apos;	Index of array
Dim lSize As Long			&apos;	UBound of resulting array
Dim vCurrentItem As Variant	&apos;	Last stored item
Dim vArray()				&apos;	The return value
Const cstThisSub = &quot;Array.RangeInit&quot;
Const cstSubArgs = &quot;From, UpTo, [ByStep = 1]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vArray = Array()

Check:
	If IsMissing(ByStep) Or IsEmpty(ByStep) Then ByStep = 1
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._Validate(From, &quot;From&quot;, V_NUMERIC) Then GoTo Finally
		If Not SF_Utils._Validate(UpTo, &quot;UpTo&quot;, V_NUMERIC) Then GoTo Finally
		If Not SF_Utils._Validate(ByStep, &quot;ByStep&quot;, V_NUMERIC) Then GoTo Finally
	End If
	If (From &lt; UpTo And ByStep &lt;= 0) Or (From &gt; UpTo And ByStep &gt;= 0) Then GoTo CatchSequence

Try:
	lSize = CLng(Abs((UpTo - From) / ByStep))
	ReDim vArray(0 To lSize)
	For lIndex = 0 To lSize
		vArray(lIndex) = From + lIndex * ByStep
	Next lIndex

Finally:
	RangeInit = vArray
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
CatchSequence:
	SF_Exception.RaiseFatal(ARRAYSEQUENCEERROR, From, UpTo, ByStep)
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.RangeInit

REM -----------------------------------------------------------------------------
Public Function Reverse(Optional ByRef Array_1D As Variant) As Variant
&apos;&apos;&apos;	Return the reversed 1D input array
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array_1D: the array to reverse
&apos;&apos;&apos;	Returns: the reversed array
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		SF_Array.Reverse(Array(1, 2, 3, 4)) returns (4, 3, 2, 1)

Dim vReverse() As Variant	&apos;	Return value
Dim lHalf As Long			&apos;	Middle of array
Dim lMin As Long			&apos;	LBound of input array
Dim lMax As Long			&apos;	UBound of input array
Dim i As Long, j As Long
Const cstThisSub = &quot;Array.Reverse&quot;
Const cstSubArgs = &quot;Array_1D&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vReverse = Array()

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_1D, &quot;Array_1D&quot;, 1) Then GoTo Finally
	End If

Try:
	lMin = LBound(Array_1D)
	lMax = UBound(Array_1D)
	ReDim vReverse(lMin To lMax)
	lHalf = Int((lMax + lMin) / 2)
	j = lMax
	For i = lMin To lHalf
		vReverse(i) = Array_1D(j)
		vReverse(j) = Array_1D(i)
		j = j - 1
	Next i
	&apos;	Odd number of items
	If IsEmpty(vReverse(lHalf + 1)) Then vReverse(lHalf + 1) = Array_1D(lHalf + 1)

Finally:
	Reverse = vReverse()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.Reverse

REM -----------------------------------------------------------------------------
Public Function SetProperty(Optional ByVal PropertyName As Variant _
								, Optional ByRef Value As Variant _
								) As Boolean
&apos;&apos;&apos;	Set a new value to the given property
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		PropertyName: the name of the property as a string
&apos;&apos;&apos;		Value: its new value
&apos;&apos;&apos;	Exceptions
&apos;&apos;&apos;		ARGUMENTERROR		The property does not exist

Const cstThisSub = &quot;Array.SetProperty&quot;
Const cstSubArgs = &quot;PropertyName, Value&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	SetProperty = False

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._Validate(PropertyName, &quot;PropertyName&quot;, V_STRING, Properties()) Then GoTo Catch
	End If

Try:
	Select Case UCase(PropertyName)
		Case Else
	End Select

Finally:
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.SetProperty

REM -----------------------------------------------------------------------------
Public Function Shuffle(Optional ByRef Array_1D As Variant) As Variant
&apos;&apos;&apos;	Returns a random permutation of a 1D array
&apos;&apos;&apos;		https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array_1D: the array to shuffle
&apos;&apos;&apos;	Returns: the shuffled array

Dim vShuffle() As Variant	&apos;	Return value
Dim vSwapValue As Variant	&apos;	Intermediate value during swap
Dim lMin As Long			&apos;	LBound of Array_1D
Dim lCurrentIndex As Long	&apos;	Decremented from UBount to LBound
Dim lRandomIndex As Long	&apos;	Random between LBound and lCurrentIndex
Dim i As Long
Const cstThisSub = &quot;Array.Shuffle&quot;
Const cstSubArgs = &quot;Array_1D&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vShuffle = Array()

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_1D, &quot;Array_1D&quot;, 1) Then GoTo Finally
	End If

Try:
	lMin = LBound(Array_1D)
	lCurrentIndex = UBound(array_1D)
	&apos;	Initialize the output array
	ReDim vShuffle(lMin To lCurrentIndex)
	For i = lMin To lCurrentIndex
		vShuffle(i) = Array_1D(i)
	Next i
	&apos;	Now ... shuffle !
	Do While lCurrentIndex &gt; lMin
		lRandomIndex = Int(Rnd * (lCurrentIndex - lMin + 1)) + lMin
		vSwapValue = vShuffle(lCurrentIndex)
		vShuffle(lCurrentIndex) = vShuffle(lRandomIndex)
		vShuffle(lRandomIndex) = vSwapValue
		lCurrentIndex = lCurrentIndex - 1
	Loop

Finally:
	Shuffle = vShuffle()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.Shuffle

REM -----------------------------------------------------------------------------
Public Function Slice(Optional ByRef Array_1D As Variant _
						, Optional ByVal From As Variant _
						, Optional ByVal UpTo As Variant _
						) As Variant
&apos;&apos;&apos;	Returns a subset of a 1D array
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array_1D: the array to slice
&apos;&apos;&apos;		From: the lower index of the subarray to extract (included)
&apos;&apos;&apos;		UpTo: the upper index of the subarray to extract (included). Default = the last item of Array_1D
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		The selected subarray with the same LBound as the input array.
&apos;&apos;&apos;		If UpTo &lt; From then the returned array is empty
&apos;&apos;&apos;	Exceptions:
&apos;&apos;&apos;		ARRAYINDEX2ERROR		Wrong values for From and/or UpTo
&apos;&apos;&apos;	Example:
&apos;&apos;&apos;		SF_Array.Slice(Array(1, 2, 3, 4, 5), 1, 3) returns (2, 3, 4)

Dim vSlice() As Variant		&apos;	Return value
Dim lMin As Long			&apos;	LBound of Array_1D
Dim lIndex As Long			&apos;	Current index in output array
Dim i As Long
Const cstThisSub = &quot;Array.Slice&quot;
Const cstSubArgs = &quot;Array_1D, From, [UpTo = UBound(Array_1D)]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vSlice = Array()

Check:
	If IsMissing(UpTo) Or IsEmpty(UpTo) Then UpTo = -1
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_1D, &quot;Array_1D&quot;, 1) Then GoTo Finally
		If Not SF_Utils._Validate(From, &quot;From&quot;, V_NUMERIC) Then GoTo Finally
		If Not SF_Utils._Validate(UpTo, &quot;UpTo&quot;, V_NUMERIC) Then GoTo Finally
	End If
	If UpTo = -1 Then UpTo = UBound(Array_1D)
	If From &lt; LBound(Array_1D) Or From &gt; UBound(Array_1D) _
		Or From &gt; UpTo Or UpTo &gt; UBound(Array_1D) Then GoTo CatchIndex

Try:
	If UpTo &gt;= From Then
		lMin = LBound(Array_1D)
		&apos;	Initialize the output array
		ReDim vSlice(lMin To lMin + UpTo - From)
		lIndex = lMin - 1
		For i = From To UpTo
			lIndex = lIndex + 1
			vSlice(lIndex) = Array_1D(i)
		Next i
	End If

Finally:
	Slice = vSlice()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
CatchIndex:
	SF_Exception.RaiseFatal(ARRAYINDEX2ERROR, SF_Array._Repr(Array_1D), From, UpTo)
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.Slice

REM -----------------------------------------------------------------------------
Public Function Sort(Optional ByRef Array_1D As Variant _
						, Optional ByVal SortOrder As Variant _
						, Optional ByVal CaseSensitive As Variant _
						) As Variant
&apos;&apos;&apos;	Sort a 1D array in ascending or descending order. String comparisons can be case-sensitive or not
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array_1D: the array to sort
&apos;&apos;&apos;			must be filled homogeneously by either strings, dates or numbers
&apos;&apos;&apos;			Null and Empty values are allowed
&apos;&apos;&apos;		SortOrder: &quot;ASC&quot; (default) or &quot;DESC&quot;
&apos;&apos;&apos;		CaseSensitive: Default = False
&apos;&apos;&apos;	Returns: the sorted array
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		Sort(Array(&quot;a&quot;, &quot;A&quot;, &quot;b&quot;, &quot;B&quot;, &quot;C&quot;), CaseSensitive := True) returns (&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;a&quot;, &quot;b&quot;)

Dim vSort() As Variant		&apos;	Return value
Dim vIndexes() As Variant	&apos;	Indexes of sorted items
Dim lMin As Long			&apos;	LBound of input array
Dim lMax As Long			&apos;	UBound of input array
Dim i As Long
Const cstThisSub = &quot;Array.Sort&quot;
Const cstSubArgs = &quot;Array_1D, [SortOrder=&quot;&quot;&quot;&quot;|&quot;&quot;ASC&quot;&quot;|&quot;&quot;DESC&quot;&quot;], [CaseSensitive=False]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vSort = Array()

Check:
	If IsMissing(SortOrder) Or IsEmpty(SortOrder) Then SortOrder = &quot;ASC&quot;
	If IsMissing(CaseSensitive) Or IsEmpty(CaseSensitive) Then CaseSensitive = False
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_1D, &quot;Array_1D&quot;, 1, 0) Then GoTo Finally
		If Not SF_Utils._Validate(SortOrder, &quot;SortOrder&quot;, V_STRING, Array(&quot;ASC&quot;,&quot;DESC&quot;)) Then GoTo Finally
		If Not SF_Utils._Validate(CaseSensitive, &quot;CaseSensitive&quot;, V_BOOLEAN) Then GoTo Finally
	End If

Try:
	lMin = LBound(Array_1D)
	lMax = UBound(Array_1D)
	If lMax &lt; lMin Then GoTo Finally	&apos;	Do nothing if the input array is empty
	vIndexes() = SF_Array._HeapSort(Array_1D, ( UCase(SortOrder) = &quot;ASC&quot; ), CaseSensitive)

	&apos;	Load output array
	ReDim vSort(lMin To lMax)
	For i = lMin To lMax
		vSort(i) = Array_1D(vIndexes(i))
	Next i

Finally:
	Sort = vSort()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.Sort

REM -----------------------------------------------------------------------------
Public Function SortColumns(Optional ByRef Array_2D As Variant _
								, Optional ByVal RowIndex As Variant _
								, Optional ByVal SortOrder As Variant _
								, Optional ByVal CaseSensitive As Variant _
								) As Variant
&apos;&apos;&apos;	Returns a permutation of the columns of a 2D array, sorted on the values of a given row
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array_2D: the input array
&apos;&apos;&apos;		RowIndex: the index of the row to sort the columns on
&apos;&apos;&apos;			the row must be filled homogeneously by either strings, dates or numbers
&apos;&apos;&apos;			Null and Empty values are allowed
&apos;&apos;&apos;		SortOrder: &quot;ASC&quot; (default) or &quot;DESC&quot;
&apos;&apos;&apos;		CaseSensitive: Default = False
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		the array with permuted columns, LBounds and UBounds are unchanged
&apos;&apos;&apos;	Exceptions:
&apos;&apos;&apos;		ARRAYINDEXERROR
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;								| 5, 7, 3 |						| 7, 5, 3 |
&apos;&apos;&apos;		SF_Array.SortColumns(	| 1, 9, 5 |, 2, &quot;ASC&quot;) returns	| 9, 1, 5 |
&apos;&apos;&apos;								| 6, 1, 8 |						| 1, 6, 8 |

Dim vSort() As Variant		&apos;	Return value
Dim vRow() As Variant		&apos;	The row on which to sort the array
Dim vIndexes() As Variant	&apos;	Indexes of sorted row
Dim lMin1 As Long			&apos;	LBound1 of input array
Dim lMax1 As Long			&apos;	UBound1 of input array
Dim lMin2 As Long			&apos;	LBound2 of input array
Dim lMax2 As Long			&apos;	UBound2 of input array
Dim i As Long, j As Long
Const cstThisSub = &quot;Array.SortColumn&quot;
Const cstSubArgs = &quot;Array_2D, RowIndex, [SortOrder=&quot;&quot;&quot;&quot;|&quot;&quot;ASC&quot;&quot;|&quot;&quot;DESC&quot;&quot;], [CaseSensitive=False]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vSort = Array()

Check:
	If IsMissing(SortOrder) Or IsEmpty(SortOrder) Then SortOrder = &quot;ASC&quot;
	If IsMissing(CaseSensitive) Or IsEmpty(CaseSensitive) Then CaseSensitive = False
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_2D, &quot;Array_2D&quot;, 2) Then GoTo Finally
		If Not SF_Utils._Validate(RowIndex, &quot;RowIndex&quot;, V_NUMERIC) Then GoTo Finally
		If Not SF_Utils._Validate(SortOrder, &quot;SortOrder&quot;, V_STRING, Array(&quot;ASC&quot;,&quot;DESC&quot;)) Then GoTo Finally
		If Not SF_Utils._Validate(CaseSensitive, &quot;CaseSensitive&quot;, V_BOOLEAN) Then GoTo Finally
	End If

Try:
	lMin1 = LBound(Array_2D, 1)		:	lMax1 = UBound(Array_2D, 1)
	If RowIndex &lt; lMin1 Or RowIndex &gt; lMax1 Then GoTo CatchIndex
	lMin2 = LBound(Array_2D, 2)		:	lMax2 = UBound(Array_2D, 2)

	&apos;	Extract and sort the RowIndex-th row
	vRow = SF_Array.ExtractRow(Array_2D, RowIndex)
	If Not SF_Utils._ValidateArray(vRow, &quot;Row #&quot; &amp; CStr(RowIndex), 1, 0) Then GoTo Finally
	vIndexes() = SF_Array._HeapSort(vRow, ( UCase(SortOrder) = &quot;ASC&quot; ), CaseSensitive)

	&apos;	Load output array
	ReDim vSort(lMin1 To lMax1, lMin2 To lMax2)
	For i = lMin1 To lMax1
		For j = lMin2 To lMax2
			vSort(i, j) = Array_2D(i, vIndexes(j))
		Next j
	Next i

Finally:
	SortColumns = vSort()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
CatchIndex:
	&apos;TODO SF_Exception.RaiseFatal(ARRAYINDEXERROR, cstThisSub)
	MsgBox &quot;INVALID INDEX VALUE !!&quot;
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.SortColumns

REM -----------------------------------------------------------------------------
Public Function SortRows(Optional ByRef Array_2D As Variant _
								, Optional ByVal ColumnIndex As Variant _
								, Optional ByVal SortOrder As Variant _
								, Optional ByVal CaseSensitive As Variant _
								) As Variant
&apos;&apos;&apos;	Returns a permutation of the rows of a 2D array, sorted on the values of a given column
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array_2D: the input array
&apos;&apos;&apos;		ColumnIndex: the index of the column to sort the rows on
&apos;&apos;&apos;			the column must be filled homogeneously by either strings, dates or numbers
&apos;&apos;&apos;			Null and Empty values are allowed
&apos;&apos;&apos;		SortOrder: &quot;ASC&quot; (default) or &quot;DESC&quot;
&apos;&apos;&apos;		CaseSensitive: Default = False
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		the array with permuted Rows, LBounds and UBounds are unchanged
&apos;&apos;&apos;	Exceptions:
&apos;&apos;&apos;		ARRAYINDEXERROR
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;							| 5, 7, 3 |						| 1, 9, 5 |
&apos;&apos;&apos;		SF_Array.SortRows(	| 1, 9, 5 |, 0, &quot;ASC&quot;) returns	| 5, 7, 3 |
&apos;&apos;&apos;							| 6, 1, 8 |						| 6, 1, 8 |

Dim vSort() As Variant		&apos;	Return value
Dim vCol() As Variant		&apos;	The column on which to sort the array
Dim vIndexes() As Variant	&apos;	Indexes of sorted row
Dim lMin1 As Long			&apos;	LBound1 of input array
Dim lMax1 As Long			&apos;	UBound1 of input array
Dim lMin2 As Long			&apos;	LBound2 of input array
Dim lMax2 As Long			&apos;	UBound2 of input array
Dim i As Long, j As Long
Const cstThisSub = &quot;Array.SortRow&quot;
Const cstSubArgs = &quot;Array_2D, ColumnIndex, [SortOrder=&quot;&quot;&quot;&quot;|&quot;&quot;ASC&quot;&quot;|&quot;&quot;DESC&quot;&quot;], [CaseSensitive=False]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vSort = Array()

Check:
	If IsMissing(SortOrder) Or IsEmpty(SortOrder) Then SortOrder = &quot;ASC&quot;
	If IsMissing(CaseSensitive) Or IsEmpty(CaseSensitive) Then CaseSensitive = False
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_2D, &quot;Array_2D&quot;, 2) Then GoTo Finally
		If Not SF_Utils._Validate(ColumnIndex, &quot;ColumnIndex&quot;, V_NUMERIC) Then GoTo Finally
		If Not SF_Utils._Validate(SortOrder, &quot;SortOrder&quot;, V_STRING, Array(&quot;ASC&quot;,&quot;DESC&quot;)) Then GoTo Finally
		If Not SF_Utils._Validate(CaseSensitive, &quot;CaseSensitive&quot;, V_BOOLEAN) Then GoTo Finally
	End If

Try:
	lMin2 = LBound(Array_2D, 2)		:	lMax2 = UBound(Array_2D, 2)
	If ColumnIndex &lt; lMin2 Or ColumnIndex &gt; lMax2 Then GoTo CatchIndex
	lMin1 = LBound(Array_2D, 1)		:	lMax1 = UBound(Array_2D, 1)

	&apos;	Extract and sort the ColumnIndex-th column
	vCol = SF_Array.ExtractColumn(Array_2D, ColumnIndex)
	If Not SF_Utils._ValidateArray(vCol, &quot;Column #&quot; &amp; CStr(ColumnIndex), 1, 0) Then GoTo Finally
	vIndexes() = SF_Array._HeapSort(vCol, ( UCase(SortOrder) = &quot;ASC&quot; ), CaseSensitive)

	&apos;	Load output array
	ReDim vSort(lMin1 To lMax1, lMin2 To lMax2)
	For i = lMin1 To lMax1
		For j = lMin2 To lMax2
			vSort(i, j) = Array_2D(vIndexes(i), j)
		Next j
	Next i

Finally:
	SortRows = vSort()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
CatchIndex:
	&apos;TODO SF_Exception.RaiseFatal(ARRAYINDEXERROR, cstThisSub)
	MsgBox &quot;INVALID INDEX VALUE !!&quot;
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.SortRows

REM -----------------------------------------------------------------------------
Public Function Transpose(Optional ByRef Array_2D As Variant) As Variant
&apos;&apos;&apos;	Swaps rows and columns in a 2D array
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array_2D: the array to transpose
&apos;&apos;&apos;	Returns:
&apos;&apos;&apos;		The transposed array
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;							| 1, 2 |			| 1, 3, 5 |
&apos;&apos;&apos;		SF_Array.Transpose(	| 3, 4 | ) returns	| 2, 4, 6 |
&apos;&apos;&apos;							| 5, 6 |

Dim vTranspose As Variant			&apos;	Return value
Dim lIndex As Long					&apos;	vTranspose index
Dim lMin1 As Long					&apos;	LBound1 of input array
Dim lMax1 As Long					&apos;	UBound1 of input array
Dim lMin2 As Long					&apos;	LBound2 of input array
Dim lMax2 As Long					&apos;	UBound2 of input array
Dim i As Long, j As Long
Const cstThisSub = &quot;Array.Transpose&quot;
Const cstSubArgs = &quot;Array_2D&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vTranspose = Array()

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_2D, &quot;Array_2D&quot;, 2) Then GoTo Finally
	End If

Try:
	&apos;	Resize the output array
	lMin1 = LBound(Array_2D, 1)			:	lMax1 = UBound(Array_2D, 1)
	lMin2 = LBound(Array_2D, 2)			:	lMax2 = UBound(Array_2D, 2)
	If lMin1 &lt;= lMax1 Then
		ReDim vTranspose(lMin2 To lMax2, lMin1 To lMax1)
	End If

	&apos;	Transpose items
	For i = lMin1 To lMax1
		For j = lMin2 To lMax2
			vTranspose(j, i) = Array_2D(i, j)
		Next j
	Next i

Finally:
	Transpose = vTranspose
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.Transpose

REM -----------------------------------------------------------------------------
Public Function TrimArray(Optional ByRef Array_1D As Variant) As Variant
&apos;&apos;&apos;	Remove from a 1D array all Null, Empty and zero-length entries
&apos;&apos;&apos;	Strings are trimmed as well
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array_1D: the array to scan
&apos;&apos;&apos;	Return: The trimmed array
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		SF_Array.TrimArray(Array(&quot;A&quot;,&quot;B&quot;,Null,&quot; D &quot;)) returns (&quot;A&quot;,&quot;B&quot;,&quot;D&quot;)

Dim vTrimArray As Variant			&apos;	Return value
Dim lIndex As Long					&apos;	vTrimArray index
Dim lMin As Long					&apos;	LBound of input array
Dim lMax As Long					&apos;	UBound of input array
Dim vItem As Variant				&apos;	Single array item
Dim i As Long
Const cstThisSub = &quot;Array.TrimArray&quot;
Const cstSubArgs = &quot;Array_1D&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vTrimArray = Array()

Check:
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_1D, &quot;Array_1D&quot;, 1) Then GoTo Finally
	End If

Try:
	lMin = LBound(Array_1D)
	lMax = UBound(Array_1D)
	If lMin &lt;= lMax Then
		ReDim vTrimArray(lMin To lMax)
	End If
	lIndex = lMin - 1

	&apos;	Load only valid items from Array_1D to vTrimArray
	For i = lMin To lMax
		vItem = Array_1D(i)
		Select Case VarType(vItem)
			Case V_EMPTY
			Case V_NULL		:	vItem = Empty
			Case V_STRING
				vItem = Trim(vItem)
				If Len(vItem) = 0 Then vItem = Empty
			Case Else
		End Select
		If Not IsEmpty(vItem) Then
			lIndex = lIndex + 1
			vTrimArray(lIndex) = vItem
		End If
	Next i

	&apos;Keep valid entries
	If lMin &lt;= lIndex Then
		ReDim Preserve vTrimArray(lMin To lIndex)
	Else
		vTrimArray = Array()
	End If

Finally:
	TrimArray = vTrimArray
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.TrimArray

REM -----------------------------------------------------------------------------
Public Function Union(Optional ByRef Array1_1D As Variant _
								, Optional ByRef Array2_1D As Variant _
								, Optional ByVal CaseSensitive As Variant _
								) As Variant
&apos;&apos;&apos;	Build a set being the Union of the two input arrays, i.e. items are contained in any of both arrays
&apos;&apos;&apos;		both input arrays must be filled homogeneously, i.e. all items must be of the same type
&apos;&apos;&apos;		Empty and Null items are forbidden
&apos;&apos;&apos;		The comparison between strings is case sensitive or not
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array1_1D: a 1st input array
&apos;&apos;&apos;		Array2_1D: a 2nd input array
&apos;&apos;&apos;		CaseSensitive: default = False
&apos;&apos;&apos;	Returns: a zero-based array containing unique items stored in any of both input arrays
&apos;&apos;&apos;		The output array is sorted in ascending order
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		SF_Array.Union(Array(&quot;A&quot;, &quot;C&quot;, &quot;A&quot;, &quot;b&quot;, &quot;B&quot;), Array(&quot;C&quot;, &quot;Z&quot;, &quot;b&quot;), True) returns (&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;Z&quot;, &quot;b&quot;)

Dim vUnion() As Variant			&apos;	Return value
Dim iType As Integer			&apos;	VarType of elements in input arrays
Dim lMin1 As Long				&apos;	LBound of 1st input array
Dim lMax1 As Long				&apos;	UBound of 1st input array
Dim lMin2 As Long				&apos;	LBound of 2nd input array
Dim lMax2 As Long				&apos;	UBound of 2nd input array
Dim lSize As Long				&apos;	Number of Union items
Dim i As Long
Const cstThisSub = &quot;Array.Union&quot;
Const cstSubArgs = &quot;Array1_1D, Array2_1D, [CaseSensitive=False]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vUnion = Array()

Check:
	If IsMissing(CaseSensitive) Then CaseSensitive = False
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array1_1D, &quot;Array1_1D&quot;, 1, 0, True) Then GoTo Finally
		iType = SF_Utils._VarTypeExt(Array1_1D(LBound(Array1_1D)))
		If Not SF_Utils._ValidateArray(Array2_1D, &quot;Array2_1D&quot;, 1, iType, True) Then GoTo Finally
		If Not SF_Utils._Validate(CaseSensitive, &quot;CaseSensitive&quot;, V_BOOLEAN) Then GoTo Finally
	End If

Try:
	lMin1 = LBound(Array1_1D)	:	lMax1 = UBound(Array1_1D)
	lMin2 = LBound(Array2_1D)	:	lMax2 = UBound(Array2_1D)

	&apos;	If both arrays are empty, do nothing
	If lMax1 &lt; lMin1 And lMax2 &lt; lMin2 Then
	ElseIf lMax1 &lt; lMin1 Then	&apos;	only 1st array is empty
		vUnion = SF_Array.Unique(Array2_1D, CaseSensitive)
	ElseIf lMax2 &lt; lMin2 Then	&apos;	only 2nd array is empty
		vUnion = SF_Array.Unique(Array1_1D, CaseSensitive)
	Else

		&apos;	Build union of both arrays
		ReDim vUnion(0 To (lMax1 - lMin1) + (lMax2 - lMin2) + 1)
		lSize = -1

		&apos;	Fill vUnion one by one only with items present in any set
		For i = lMin1 To lMax1
			lSize = lSize + 1
			vUnion(lSize) = Array1_1D(i)
		Next i
		For i = lMin2 To lMax2
			lSize = lSize + 1
			vUnion(lSize) = Array2_1D(i)
		Next i

		&apos;	Remove duplicates
		vUnion() = SF_Array.Unique(vUnion, CaseSensitive)
	End If

Finally:
	Union = vUnion()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.Union

REM -----------------------------------------------------------------------------
Public Function Unique(Optional ByRef Array_1D As Variant _
							, Optional ByVal CaseSensitive As Variant _
							) As Variant
&apos;&apos;&apos;	Build a set of unique values derived from the input array
&apos;&apos;&apos;		the input array must be filled homogeneously, i.e. all items must be of the same type
&apos;&apos;&apos;		Empty and Null items are forbidden
&apos;&apos;&apos;		The comparison between strings is case sensitive or not
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		Array_1D: the input array with potential duplicates
&apos;&apos;&apos;		CaseSensitive: default = False
&apos;&apos;&apos;	Returns: the array without duplicates with same LBound as input array
&apos;&apos;&apos;		The output array is sorted in ascending order
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		Unique(Array(&quot;A&quot;, &quot;C&quot;, &quot;A&quot;, &quot;b&quot;, &quot;B&quot;), True) returns (&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;b&quot;)

Dim vUnique() As Variant	&apos;	Return value
Dim vSorted() As Variant	&apos;	The input array after sort
Dim lMin As Long			&apos;	LBound of input array
Dim lMax As Long			&apos;	UBound of input array
Dim lUnique As Long			&apos;	Number of unique items
Dim vIndex As Variant		&apos;	Output of _FindItem() method
Dim vItem As Variant		&apos;	One single item in the array
Dim i As Long
Const cstThisSub = &quot;Array.Unique&quot;
Const cstSubArgs = &quot;Array_1D, [CaseSensitive=False]&quot;

	If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch
	vUnique = Array()

Check:
	If IsMissing(CaseSensitive) Then CaseSensitive = False
	If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then
		If Not SF_Utils._ValidateArray(Array_1D, &quot;Array_1D&quot;, 1, 0, True) Then GoTo Finally
		If Not SF_Utils._Validate(CaseSensitive, &quot;CaseSensitive&quot;, V_BOOLEAN) Then GoTo Finally
	End If

Try:
	lMin = LBound(Array_1D)
	lMax = UBound(Array_1D)
	If lMax &gt;= lMin Then
		&apos;	First sort the array
		vSorted = SF_Array.Sort(Array_1D, &quot;ASC&quot;, CaseSensitive)
		ReDim vUnique(lMin To lMax)
		lUnique = lMin
		&apos;	Fill vUnique one by one ignoring duplicates
		For i = lMin To lMax
			vItem = vSorted(i)
			If i = lMin Then
				vUnique(i) = vItem
			Else
				If SF_Array._ValCompare(vItem, vSorted(i - 1), CaseSensitive) = 0 Then	&apos;	Ignore item
				Else
					lUnique = lUnique + 1
					vUnique(lUnique) = vItem
				End If
			End If
		Next i
		&apos;	Remove unfilled entries
		ReDim Preserve vUnique(lMin To lUnique)
	End If

Finally:
	Unique = vUnique()
	SF_Utils._ExitFunction(cstThisSub)
	Exit Function
Catch:
	GoTo Finally
End Function	&apos;	ScriptForge.SF_Array.Unique

REM ============================================================= PRIVATE METHODS

REM -----------------------------------------------------------------------------
Private Function _ConvertToCellValue(ByVal pvItem As Variant _
										, ByVal pbIsCell As Boolean _
										) As Variant
&apos;&apos;&apos;	Convert the argument to a valid Calc cell content, i.e. a string or a double.
&apos;&apos;&apos;	When the argument is not immediately convertible, either
&apos;&apos;&apos;		- the cell range is returned unchanged if the argument is a UNO com.sun.star.table.XCellRange object
&apos;&apos;&apos;		- the date is converted to a double when the argument is a UNO com.sun.star.util.DateTime object
&apos;&apos;&apos;		- the zero-length string is returned (pbIsCell = True)
&apos;&apos;&apos;		- the argument is returned unchanged (pbIsCell = False)

Dim vCell As Variant			&apos;	Return value
Dim oObjectDescriptor As Object	&apos;	Object descriptor (see SF_Utils)

Try:
	&apos;	Conversion takes place only when pbIsCell = True
	If pbIsCell Then
		Select Case SF_Utils._VarTypeExt(pvItem)
			Case V_STRING					:		vCell = pvItem
			Case V_DATE						:		vCell = CDbl(pvItem)
			Case V_NUMERIC					:		vCell = CDbl(pvItem)
			Case V_BOOLEAN					:		vCell = CDbl(Iif(pvItem, 1, 0))
			Case V_OBJECT
				Set oObjectDescriptor = SF_Utils._VarTypeObj(pvItem)
				Select Case oObjectDescriptor.sObjectType
					Case &quot;ScCellRangeObj&quot;				:	vCell = pvItem
					Case &quot;com.sun.star.util.DateTime&quot;	:	vCell = CDbl(CDateFromUnoDateTime(pvItem))	&apos;	Python date
					Case Else							:	vCell = &quot;&quot;
				End Select
			Case Else						:		vCell = &quot;&quot;
		End Select
	Else		&apos;	Return the input item unchanged
		vCell = pvItem
	End If

Finally:
	_ConvertToCellValue = vCell
	Exit Function
End Function	&apos;	ScriptForge.SF_Array._ConvertToCellValue

REM -----------------------------------------------------------------------------
Public Function _FindItem(ByRef pvArray_1D As Variant _
							, ByVal pvToFind As Variant _
							, ByVal pbCaseSensitive As Boolean _
							, ByVal psSortOrder As String _
							) As Variant
&apos;&apos;&apos;	Check if a 1D array contains the ToFind number, string or date and return its index
&apos;&apos;&apos;	The comparison between strings can be done case-sensitively or not
&apos;&apos;&apos;	If the array is sorted then a binary search is done
&apos;&apos;&apos;	Otherwise the array is scanned from top. Null or Empty items are simply ignored
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		pvArray_1D: the array to scan
&apos;&apos;&apos;		pvToFind: a number, a date or a string to find
&apos;&apos;&apos;		pbCaseSensitive: Only for string comparisons, default = False
&apos;&apos;&apos;		psSortOrder: &quot;ASC&quot;, &quot;DESC&quot; or &quot;&quot; (= not sorted, default)
&apos;&apos;&apos;	Return: a (0:1) array
&apos;&apos;&apos;		(0) = True when found
&apos;&apos;&apos;		(1) = if found: index of item
&apos;&apos;&apos;			  if not found:	if sorted, index of next item in the array (might be = UBound + 1)
&apos;&apos;&apos;							if not sorted, meaningless
&apos;&apos;&apos;		Result is unpredictable when array is announced sorted and is in reality not
&apos;&apos;&apos;	Called by Contains, IndexOf and InsertSorted. Also called by SF_Dictionary

Dim bContains As Boolean			&apos;	True if match found
Dim iToFindType As Integer			&apos;	VarType of pvToFind
Dim lTop As Long, lBottom As Long	&apos;	Interval in scope of binary search
Dim lIndex As Long					&apos;	Index used in search
Dim iCompare As Integer				&apos;	Output of _ValCompare function
Dim lLoops As Long					&apos;	Count binary searches
Dim lMaxLoops As Long				&apos;	Max number of loops during binary search: to avoid infinite loops if array not sorted
Dim vFound(1) As Variant			&apos;	Returned array (Contains, Index)

	bContains = False

	If LBound(pvArray_1D) &gt; UBound(pvArray_1D) Then		&apos;	Empty array, do nothing
	Else
		&apos;	Search sequentially
		If Len(psSortOrder) = 0 Then
			For lIndex = LBound(pvArray_1D) To UBound(pvArray_1D)
				bContains = ( SF_Array._ValCompare(pvToFind, pvArray_1D(lIndex), pbCaseSensitive) = 0 )
				If bContains Then Exit For
			Next lIndex
		Else
		&apos;	Binary search
			If psSortOrder = &quot;ASC&quot; Then
				lTop = UBound(pvArray_1D)
				lBottom = lBound(pvArray_1D)
			Else
				lBottom = UBound(pvArray_1D)
				lTop = lBound(pvArray_1D)
			End If
			lLoops = 0
			lMaxLoops = CLng((Log(UBound(pvArray_1D) - LBound(pvArray_1D) + 1.0) / Log(2.0))) + 1
			Do
				lLoops = lLoops + 1
				lIndex = (lTop + lBottom) / 2
				iCompare = SF_Array._ValCompare(pvToFind, pvArray_1D(lIndex), pbCaseSensitive)
				Select Case True
					Case iCompare = 0		:	bContains = True
					Case iCompare &lt; 0 And psSortOrder = &quot;ASC&quot;
						lTop = lIndex - 1
					Case  iCompare &gt; 0 And psSortOrder = &quot;DESC&quot;
						lBottom = lIndex - 1
					Case iCompare &gt; 0 And psSortOrder = &quot;ASC&quot;
						lBottom = lIndex + 1
					Case iCompare &lt; 0 And psSortOrder = &quot;DESC&quot;
						lTop = lIndex + 1
				End Select
			Loop Until ( bContains ) Or ( lBottom &gt; lTop And psSortOrder = &quot;ASC&quot; ) Or (lBottom &lt; lTop And psSortOrder = &quot;DESC&quot; ) Or lLoops &gt; lMaxLoops
			&apos;	Flag first next non-matching element
			If Not bContains Then lIndex = Iif(psSortOrder = &quot;ASC&quot;, lBottom, lTop)
		End If
	End If

	&apos;	Build output array
	vFound(0) = bContains
	vFound(1) = lIndex
	_FindItem = vFound

End Function	&apos;	ScriptForge.SF_Array._FindItem

REM -----------------------------------------------------------------------------
Private Function _HeapSort(ByRef pvArray As Variant _
							, Optional ByVal pbAscending As Boolean _
							, Optional ByVal pbCaseSensitive As Boolean _
							) As Variant
&apos;&apos;&apos;	Sort an array: items are presumed all strings, all dates or all numeric
&apos;&apos;&apos;		Null or Empty are allowed and are considered smaller than other items
&apos;&apos;&apos;		https://en.wikipedia.org/wiki/Heapsort
&apos;&apos;&apos;		http://www.vbforums.com/showthread.php?473677-VB6-Sorting-algorithms-(sort-array-sorting-arrays)&amp;p=2909250#post2909250
&apos;&apos;&apos;		HeapSort preferred to QuickSort because not recursive (this routine returns an array of indexes !!)
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		pvArray: a 1D array
&apos;&apos;&apos;		pbAscending: default = True
&apos;&apos;&apos;		pbCaseSensitive: default = False
&apos;&apos;&apos;	Returns
&apos;&apos;&apos;		An array of Longs of same dimensions as the input array listing the indexes of the sorted items
&apos;&apos;&apos;		An empty array if the sort failed
&apos;&apos;&apos;	Examples:
&apos;&apos;&apos;		_HeapSort(Array(4, 2, 6, 1) returns (3, 1, 0, 2)

Dim vIndexes As Variant			&apos;	Return value
Dim i As Long
Dim lMin As Long, lMax As Long	&apos;	Array bounds
Dim lSwap As Long				&apos;	For index swaps

	If IsMissing(pbAscending) Then pbAscending = True
	If IsMissing(pbCaseSensitive) Then pbCaseSensitive = False
	vIndexes = Array()
	lMin = LBound(pvArray, 1)
	lMax = UBound(pvArray, 1)

	&apos;	Initialize output array
	ReDim vIndexes(lMin To lMax)
	For i = lMin To lMax
		vIndexes(i) = i
	Next i

    &apos;	Initial heapify
	For i = (lMax + lMin) \ 2 To lMin Step -1
		SF_Array._HeapSort1(pvArray, vIndexes, i, lMin, lMax, pbCaseSensitive)
	Next i
	&apos;	Next heapify
	For i = lMax To lMin + 1 Step -1
		&apos;	Only indexes as swapped, not the array items themselves
		lSwap = vIndexes(i)
		vIndexes(i) = vIndexes(lMin)
		vIndexes(lMin) = lSwap
		SF_Array._HeapSort1(pvArray, vIndexes, lMin, lMin, i - 1, pbCaseSensitive)
	Next i
	
	If pbAscending Then _HeapSort = vIndexes() Else _HeapSort = SF_Array.Reverse(vIndexes())

End Function	&apos;	ScriptForge.SF_Array._HeapSort
 
REM -----------------------------------------------------------------------------
Private Sub _HeapSort1(ByRef pvArray As Variant _
							, ByRef pvIndexes As Variant _
							, ByVal plIndex As Long _
							, ByVal plMin As Long _
							, ByVal plMax As Long _
							, ByVal pbCaseSensitive As Boolean _
							)
&apos;&apos;&apos;	Sub called by _HeapSort only

	Dim lLeaf As Long
	Dim lSwap As Long
    
	Do
		lLeaf = plIndex + plIndex - (plMin - 1)
		Select Case lLeaf
			Case Is &gt; plMax: Exit Do
			Case Is &lt; plMax
				If SF_Array._ValCompare(pvArray(pvIndexes(lLeaf + 1)), pvArray(pvIndexes(lLeaf)), pbCaseSensitive) &gt; 0 Then lLeaf = lLeaf + 1
		End Select
		If SF_Array._ValCompare(pvArray(pvIndexes(plIndex)), pvArray(pvIndexes(lLeaf)), pbCaseSensitive) &gt; 0 Then Exit Do
		&apos;	Only indexes as swapped, not the array items themselves
		lSwap = pvIndexes(plIndex)
		pvIndexes(plIndex) = pvIndexes(lLeaf)
		pvIndexes(lLeaf) = lSwap
		plIndex = lLeaf
	Loop

End Sub			&apos;	ScriptForge.SF_Array._HeapSort1

REM -----------------------------------------------------------------------------
Private Function _Repr(ByRef pvArray As Variant) As String
&apos;&apos;&apos;	Convert array to a readable string, typically for debugging purposes (DebugPrint ...)
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		pvArray: the array to convert, individual items may be of any type, including arrays
&apos;&apos;&apos;	Return:
&apos;&apos;&apos;		&quot;[ARRAY] (L:U[, L:U]...)&quot; if # of Dims &gt; 1
&apos;&apos;&apos;		&quot;[ARRAY] (L:U) (item1,item2, ...)&quot; if 1D array

Dim iDims As Integer	&apos;	Number of dimensions of the array
Dim sArray As String	&apos;	Return value
Dim i As Long
Const cstArrayEmpty = &quot;[ARRAY] ()&quot;
Const cstArray = &quot;[ARRAY]&quot;
Const cstMaxLength = 50	&apos;	Maximum length for items
Const cstSeparator = &quot;, &quot;

	_Repr = &quot;&quot;
	iDims = SF_Array.CountDims(pvArray)

	Select Case iDims
		Case -1			:	Exit Function		&apos;	Not an array
		Case 0			:	sArray = cstArrayEmpty
		Case Else
			sArray = cstArray
			For i = 1 To iDims
				sArray = sArray &amp; Iif(i = 1, &quot; (&quot;, &quot;, &quot;) &amp; CStr(LBound(pvArray, i)) &amp; &quot;:&quot; &amp; CStr(UBound(pvArray, i))
			Next i
			sArray = sArray &amp; &quot;)&quot;
			&apos;	List individual items of 1D arrays
			If iDims = 1 Then
				sArray = sArray &amp; &quot; (&quot;
				For i = LBound(pvArray) To UBound(pvArray)
					sArray = sArray &amp; SF_Utils._Repr(pvArray(i), cstMaxLength) &amp; cstSeparator	&apos;	Recursive call
				Next i
				sArray = Left(sArray, Len(sArray) - Len(cstSeparator))	&apos;	Suppress last comma
				sArray = sArray &amp; &quot;)&quot;
			End If
	End Select

	_Repr = sArray

End Function	&apos;	ScriptForge.SF_Array._Repr

REM -----------------------------------------------------------------------------
Public Function _StaticType(ByRef pvArray As Variant) As Integer
&apos;&apos;&apos;	If array is static, return its type
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		pvArray: array to examine
&apos;&apos;&apos;	Return:
&apos;&apos;&apos;		array type, -1 if not identified
&apos;&apos;&apos;		All numeric types are aggregated into V_NUMERIC

Dim iArrayType As Integer	&apos;	VarType of array
Dim iType As Integer		&apos;	VarType of items

	iArrayType = VarType(pvArray)
	iType = iArrayType - V_ARRAY
	Select Case iType
		Case V_INTEGER, V_LONG, V_SINGLE, V_DOUBLE, V_CURRENCY, V_BIGINT, V_DECIMAL, V_BOOLEAN
			_StaticType = V_NUMERIC
		Case V_STRING, V_DATE
			_StaticType = iType
		Case Else
			_StaticType = -1
	End Select

End Function	&apos;	ScriptForge.SF_Utils._StaticType

REM -----------------------------------------------------------------------------
Private Function _ValCompare(ByVal pvValue1 As Variant _
								, pvValue2 As Variant _
								, Optional ByVal pbCaseSensitive As Boolean _
								) As Integer
&apos;&apos;&apos;	Compare 2 values : equality, greater than or smaller than
&apos;&apos;&apos;	Args:
&apos;&apos;&apos;		pvValue1 and pvValue2: values to compare. pvValues must be String, Number, Date, Empty or Null
&apos;&apos;&apos;		By convention: Empty &lt; Null &lt; string, number or date
&apos;&apos;&apos;		pbCaseSensitive: ignored when not String comparison
&apos;&apos;&apos;	Return:	-1	when pvValue1 &lt; pvValue2
&apos;&apos;&apos;			+1	when pvValue1 &gt; pvValue2
&apos;&apos;&apos;			0	when pvValue1 = pvValue2
&apos;&apos;&apos;			-2	when comparison is nonsense

Dim iCompare As Integer, iVarType1 As Integer, iVarType2 As Integer

	If IsMissing(pbCaseSensitive) Then pbCaseSensitive = False
	iVarType1 = SF_Utils._VarTypeExt(pvValue1)
	iVarType2 = SF_Utils._VarTypeExt(pvValue2)

	iCompare = -2
	If iVarType1 = V_OBJECT Or iVarType1 = V_BYTE Or iVarType1 &gt;= V_ARRAY Then			&apos;	Nonsense
	ElseIf iVarType2 = V_OBJECT Or iVarType2 = V_BYTE Or iVarType2 &gt;= V_ARRAY Then		&apos;	Nonsense
	ElseIf iVarType1 = V_STRING And iVarType2 = V_STRING Then
			iCompare = StrComp(pvValue1, pvValue2, Iif(pbCaseSensitive, 1, 0))
	ElseIf iVarType1 = V_NULL Or iVarType1 = V_EMPTY Or iVarType2 = V_NULL Or iVarType2 = V_EMPTY Then
		Select Case True
			Case pvValue1 = pvValue2						:	iCompare = 0
			Case iVarType1 = V_NULL And iVarType2 = V_EMPTY	:	iCompare = +1
			Case iVarType1 = V_EMPTY And iVarType2 = V_NULL	:	iCompare = -1
			Case iVarType1 = V_NULL Or iVarType1 = V_EMPTY	:	iCompare = -1
			Case iVarType2 = V_NULL Or iVarType2 = V_EMPTY	:	iCompare = +1
		End Select
	ElseIf iVarType1 = iVarType2 Then
		Select Case True
			Case pvValue1 &lt; pvValue2						:	iCompare = -1
			Case pvValue1 = pvValue2						:	iCompare = 0
			Case pvValue1 &gt; pvValue2						:	iCompare = +1
		End Select
	End If

	_ValCompare = iCompare

End Function	&apos;	ScriptForge.SF_Array._ValCompare

REM ================================================= END OF SCRIPTFORGE.SF_ARRAY
</script:module>