In recent months I have been trying to figure out how in the world one can mimic the functionality of Excel's New Dynamic Arrays
exclusively in VBA
. There are tricky ways to do this using the window's API, (see this link), and I have also found that one can utilize ADO with Querytables
(see this link), which IMO, is a more stable implementation than using Win 32
timers. However, both of these require external functions/libraries, and I wanted to find another way. I knew that spooky things happen when using VBA's Evaluate
function, so I started piddling around with it here and there. I had some free time over the weekend and decided that I would test a bunch different ideas using Evaluate
, when all of the sudden, it happened...it worked!
What I have below is by no means the final product, nor is it pretty, but I was too excited to Not post it.
EDIT: @JonPeltier pointed out that GetDynamicArray1D
was removing the last element of the array when sending it to the sheet, so I updated the ArrayToSheet1D
to the following:
Private Sub ArrayToSheet1D(rngOut As Range, ByVal boolToRow As Boolean)
'if zero Based
If LBound(arryVariant, 1) = 0 Then
If boolToRow Then
'1-D Arry to 1 Row
rngOut.Resize(1, UBound(arryVariant, 1) + 1).Value2 = _
Application.Transpose(Application.Transpose(arryVariant))
Else
'1-D Arry to 1 column
rngOut.Resize(UBound(arryVariant, 1) + 1).Value2 = _
Application.Transpose(arryVariant)
End If
Else
If boolToRow Then
'1-D Arry to 1 Row
rngOut.Resize(1, UBound(arryVariant, 1)).Value2 = _
Application.Transpose(Application.Transpose(arryVariant))
Else
'1-D Arry to 1 column
rngOut.Resize(UBound(arryVariant)).Value2 = _
Application.Transpose(arryVariant)
End If
End If
End Sub
Everything else in the original code should stay the same.
Original Code:
Option Explicit
Private arryVariant As Variant
Private Const ERROR_SPILL As String = "#SPILL"
Public Function GetDynamicArray1D(ParamArray arrIn() As Variant) As Variant
Dim strRangeFormulaOut As String, strRangeAddress As String
If RngHasData(Application.Caller.Address, UBound(arrIn) + 1) Then GetDynamicArray1D = ERROR_SPILL: Exit Function
arryVariant = CVar(arrIn)
'Remove the first value
arryVariant = Filter(arryVariant, arryVariant(0), False)
strRangeAddress = Application.Caller.Offset(1, 0).Address(False, False)
strRangeFormulaOut = "ArrayToSheet1D(" & strRangeAddress & "," & False & ")"
Evaluate strRangeFormulaOut
GetDynamicArray1D = arrIn(0)
End Function
Public Function SortValues(ByVal rngIn As Range, ByVal lngColIndex As Long, _
Optional boolAscending As Boolean = True) As Variant
Dim strRngAddressBelow As String, strRngAddressToRight As String
Dim varValue As Variant, arryTopRow As Variant
If RngHasData(Application.Caller.Address, rngIn.Rows.Count) Then SortValues = ERROR_SPILL: Exit Function
arryVariant = rngIn.Value
QuickSortArrAscDesc arryVariant, lngColIndex, , , boolAscending
arryTopRow = Application.Index(arryVariant, 1, 0)
'get first value after sorting
varValue = arryVariant(1, 1)
arryTopRow = RemoveElementFromArray1D(arryTopRow, 1)
arryVariant = DeleteRowFromArray(arryVariant, 1)
strRngAddressBelow = Application.Caller.Offset(1, 0).Address(False, False)
strRngAddressToRight = Application.Caller.Offset(0, 1).Address(False, False)
Evaluate "ArrayToSheet2D(" & strRngAddressBelow & ")"
arryVariant = arryTopRow
Evaluate "ArrayToSheet1D(" & strRngAddressToRight & "," & True & ")"
SortValues = varValue
End Function
'Helper Functions
Private Sub ArrayToSheet1D(rngOut As Range, ByVal boolToRow As Boolean)
If boolToRow Then
rngOut.Resize(1, UBound(arryVariant)).Value2 = Application.Transpose(Application.Transpose(arryVariant))
Else
rngOut.Resize(UBound(arryVariant)).Value2 = Application.Transpose(arryVariant)
End If
End Sub
Private Sub ArrayToSheet2D(rngOut As Range)
rngOut.Resize(UBound(arryVariant, 1), UBound(arryVariant, 2)).Value2 = arryVariant
End Sub
Private Function StripText(ByVal strIn As String) As Long
With CreateObject("vbscript.regexp")
.Global = True
.Pattern = "[^\d]+"
StripText = CLng(.Replace(strIn, vbNullString))
End With
End Function
Private Function StripNumbers(ByVal strInPut As String, Optional ByVal strReplacementVal As String) As String
With CreateObject("VBScript.RegExp")
.Global = True
.Pattern = "\d+"
StripNumbers = .Replace(strInPut, strReplacementVal)
End With
End Function
Private Function RemoveElementFromArray1D(ByRef arryIn As Variant, _
ByVal lngIndex As Long) As Variant
Dim i As Long, k As Long
Dim arryOut As Variant
ReDim arryOut(LBound(arryIn) To (UBound(arryIn, 1) - 1))
For i = LBound(arryIn) To UBound(arryIn)
If i <> lngIndex Then
k = k + 1
arryOut(k) = arryIn(i)
End If
Next i
RemoveElementFromArray1D = arryOut
End Function
Private Function DeleteRowFromArray(ByRef arryIn As Variant, _
ByVal lngRowIndex As Long) As Variant
Dim i As Long, j As Long, k As Long
Dim arryOut As Variant
ReDim arryOut(LBound(arryIn, 1) To (UBound(arryIn, 1) - 1), _
LBound(arryIn, 2) To UBound(arryIn, 2))
For i = LBound(arryIn, 1) To UBound(arryIn, 1)
If i <> lngRowIndex Then
k = k + 1
For j = LBound(arryIn, 2) To UBound(arryIn, 2)
arryOut(k, j) = arryIn(i, j)
Next j
End If
Next i
DeleteRowFromArray = arryOut
End Function
Private Function RngHasData(ByVal strCallerAddress As String, ByVal lngRowCount As Long) As Boolean
Dim strSpillRng As String
If lngRowCount = 1 Then Exit Function 'don't need to check
strSpillRng = GetSpillRange(strCallerAddress, lngRowCount)
If Application.CountA(ActiveSheet.Range(strSpillRng)) > 0 Then RngHasData = True
End Function
Private Function GetSpillRange(ByVal strCallAddress As String, ByVal lngRowCount As Long) As String
Dim strRangeBegin As String
Dim lngStartRowBelow As Long, lngEndRowBelow As Long
strRangeBegin = StripNumbers(CStr(Split(strCallAddress, ":")(0)))
lngStartRowBelow = StripText(CStr(Split(strCallAddress, ":")(0))) + 1
lngEndRowBelow = lngStartRowBelow + lngRowCount - 2
GetSpillRange = strRangeBegin & CStr(lngStartRowBelow) & ":" & strRangeBegin & CStr(lngEndRowBelow)
End Function
'Adapted From Nigel Heffernan's Post
'https://stackoverflow.com/questions/4873182/sorting-a-multidimensionnal-array-in-vba
Public Sub QuickSortArrAscDesc(ByRef arrySource As Variant, ByVal lngSortCol As Long, _
Optional lngMin As Long = -1, _
Optional lngMax As Long = -1, _
Optional boolAscending As Boolean = True)
Dim varPivot As Variant, i As Long, j As Long, lngColTemp As Long
Dim arrRowTemp As Variant
If IsEmpty(arrySource) Then Exit Sub
If InStr(TypeName(arrySource), "()") < 1 Then Exit Sub
If lngMin = -1 Then lngMin = LBound(arrySource, 1)
If lngMax = -1 Then lngMax = UBound(arrySource, 1)
If lngMin >= lngMax Then Exit Sub
i = lngMin
j = lngMax
varPivot = Empty
varPivot = arrySource(Int((lngMin + lngMax) / 2), lngSortCol)
Do While i <= j
If boolAscending Then
Do While arrySource(i, lngSortCol) < varPivot
i = i + 1
Loop
Else
Do While arrySource(i, lngSortCol) > varPivot
i = i + 1
Loop
End If
If boolAscending Then
Do While arrySource(j, lngSortCol) > varPivot
j = j - 1
Loop
Else
Do While arrySource(j, lngSortCol) < varPivot
j = j - 1
Loop
End If
If i <= j Then
For lngColTemp = LBound(arrySource, 2) To UBound(arrySource, 2)
arrRowTemp = arrySource(i, lngColTemp)
arrySource(i, lngColTemp) = arrySource(j, lngColTemp)
arrySource(j, lngColTemp) = arrRowTemp
Next
arrRowTemp = Empty
i = i + 1
j = j - 1
End If
Loop
If lngMin < j Then QuickSortArrAscDesc arrySource, lngSortCol, lngMin, j, boolAscending
If i < lngMax Then QuickSortArrAscDesc arrySource, lngSortCol, lngMax, j, boolAscending
End Sub
SortValues Example:
GetDynamicArray1D Example:
#SPILL
string looks like the thing, I would recommend outputting an actual existing/supportedError
type (e.g.CVErr(xlErrValue)
), so that native functions likeIsError
still work correctly. \$\endgroup\$