Excel VBA -Sorting a 2-dimensional array - arrays

I want to alphabetically sort a 2-dimensional array results(lcol, 4) with VBA. This array contains 4 columns and variable number of rows, based on the values of the last column.
This is the code of how I populated the array :
ReDim results(lcol, 4)
For i = 1 To lcol
results(i, 1) = ThisWorkbook.Sheets(2).Range("B1").Offset(, i - 1).Value
results(i, 2) = "0"
results(i, 3) = ThisWorkbook.Sheets(3).Range("C2").Offset(i - 1, 0).Value
Next i
For Each of In ThisWorkbook.Sheets(1).Range("A1:C" & lrow2)
Set modele = of.Offset(, 1)
Set qte = of.Offset(, 2)
For Each modele2 In ThisWorkbook.Sheets(2).Range("A2:A481")
If modele2.Value = modele.Value Then
For i = 1 To lcol 'à modifier
results(i, 2) = results(i, 2) + qte.Value * modele2.Offset(, i).Value
If results(i, 2) <= results(i, 3) Then
results(i, 4) = "OK"
Else
results(i, 4) = "Rupture"
End If
Next i
Exit For
End If
Next modele2
Next of

This provides a basic (quiksort?) ascending sort on your populated array with the last column as the primary key.
dim i as long, j as long, tmp as variant
redim tmp(lbound(results, 1) to lbound(results, 1), lbound(results, 2) to ubound(results, 2))
for i = lbound(results, 1) to ubound(results, 1) - 1
if results(i, ubound(results, 2)) > results(i+1, ubound(results, 2)) or _
results(i, ubound(results, 2)) = vbnullstring then
for j = lbound(results, 2) to ubound(results, 2)
tmp(lbound(results, 1), j) = results(i, j)
next j
for j = lbound(results, 2) to ubound(results, 2)
results(i, j) = results(i+1, j)
next j
for j = lbound(results, 2) to ubound(results, 2)
results(i+1, j) = tmp(lbound(results, 1), j)
next j
end if
next i
Sorry for all the lbound and ubound but I had no idea if your array was zero-based of 1-based. The For i = 1 To lcol was not definitive. All evidence points to your arr being zero-based.

You could have SortedList object do the work
Assuming your results array is 1-based and with 4 columns, you could try the following code (UNTESTED):
Sub SortArray(results As Variant)
Dim i As Long, j As Long
With CreateObject("System.Collections.SortedList")
For i = 1 to UBound(results)
.Add results(i,4), Application.Index(result,i,0)
Next
For i = 1 To .Count
For j = 1 To 4
results(i, j) = .GetByIndex(i)(j)
Next
Next
End With
End Sub
Which you would call in your “main” sub as follows:
SortArray results

Related

The loop over two arrays take LONG

Thanks for your helps,
I have two arrays: A (100k row, 10 col) and B (100k row, 12 col)
The following code (thanks to BSALV) loop through A and B => It takes really long to finish. Is there any way to speedup.
ReDim Preserve B(1 To UBound(B), 1 To UBound(B, 2) + 4)
ReDim arr(1 To UBound(B), 1 To 2)
For i = 1 To UBound(B)
iSell = B(i, 3): mysold = 0
r = Application.Match(B(i, 2), Application.Index(A, 0, 2), 0)
If IsNumeric(r) Then
For i1 = r To UBound(A)
If A(i1, 2) = B(i, 2) And A(i1, 1) <= B(i, 1) Then
x = Application.Max(0, Application.Min(A(i1, 3), iSell))
If x > 0 Then
mysold = mysold + x
iSell = iSell - x
MyValueSold = MyValueSold + x * A(i1, 4)
A(i1, 3) = A(i1, 3) - x
If A(i1, 3) <= 0 Then A(i1, 2) = "~"
End If
If A(i1, 3) > 0 Then Exit For
End If
Next
End If
arr(i, 1) = mysold: arr(i, 2) = MyValueSold
Next
This operation is really slow when using larger arrays:
r = Application.Match(B(i, 2), Application.Index(A, 0, 2), 0)
You can get much better performance just by replacing the Index/Match line with a dictionary lookup.
To illustrate:
Sub Tester()
Const NROWS As Long = 100000
Dim i As Long, r, t
Dim dict As Object
Set dict = CreateObject("scripting.dictionary")
Dim A(1 To NROWS, 1 To 10)
'populate some dummy data
For i = 1 To UBound(A, 1)
A(i, 2) = Application.RandBetween(1, NROWS)
A(i, 3) = i
Next i
'First your existing row lookup...
t = Timer
For i = 1 To 100 'only testing 100 lookups (too slow for more!)
r = Application.Match(i, Application.Index(A, 0, 2), 0)
Next i
Debug.Print "Index/Match lookup", Timer - t, "*100* lookups"
'populate a dictionary for lookups...
t = Timer
For i = 1 To NROWS
dict(A(i, 2)) = i 'mapping second column first occurences to row #
Next i
Debug.Print "Mapping done", Timer - t
'Now the dictionary lookup
t = Timer
For i = 1 To NROWS
If dict.Exists(i) Then
r = dict(i)
End If
Next i
Debug.Print "Dictionary lookup", Timer - t, NROWS & " lookups"
End Sub
Output:
Index/Match lookup 9.62 *100* lookups '<<< slow slow!
Mapping done 0.12
Dictionary lookup 0.26 100000 lookups
EDIT: changes in your existing code
Dim rngMatch As Range '<<< added
'...
'...
Set lo = Sheets("Exc").ListObjects("TBL_Buy")
Set rngMatch = lo.DataBodyRange.Columns(2) '<<< lookup range
With lo.Range
.Sort .Range("B1"), xlAscending, , .Range("A1"), xlAscending, Header:=xlYes
aBuy = lo.DataBodyRange.Value2
.Sort .Range("A1"), xlAscending, , .Range("B1"), xlAscending, Header:=xlYes
End With
'...
For i = 1 To UBound(aResult)
'...
r = Application.Match(aResult(i, 2), rngMatch, 0) '<<<
'...
'...

Populate one dimensional array without looping

The following line of code will allocate an array as one dimensional sortedArr = WorksheetFunction.Transpose(lstIssues1.List). This line of code allocates an array as two dimensional arrIssues = Table.ListColumns(Table.ListColumns(strNumber).Range.column).DataBodyRange
For each of the above lines I call a bubble sort function. But it errors on one or the other depending on whether I put
If Arr(i) > Arr(j) Then ...
Or
If Arr(i, 1) > Arr(j, 1) Then ...
I can loop to fill arrIssues. But I am wondering if it's possible to fill it as a one dimensional array without looping.
UPDATE
Here is the code I am having trouble with
Private Sub cmdRemove_Click()
Dim SortedArr() As Variant
With lstPrevious
If .ListIndex = -1 Then Exit Sub
For i = .ListCount - 1 To 0 Step -1
If .Selected(i) = True Then
lstAdditional.AddItem .List(i)
.RemoveItem (i)
End If
Next i
End With
ReDim SortedArr(lstAdditional.ListCount - 1)
SortedArr = Application.Transpose(lstAdditional.List) 'ERROR Type Mismatch
Call BubbleSort(SortedArr)
Me.lstAdditional.List = SortedArr
txtFocus.SetFocus
End Sub
Public Sub BubbleSort(Arr)
Dim strTemp As String
Dim lngMin As Long
Dim lngMax As Long
lngMin = LBound(Arr)
lngMax = UBound(Arr)
For i = lngMin To lngMax
For j = i + 1 To lngMax
If Arr(i) > Arr(j) Then
strTemp = Arr(i)
Arr(i) = Arr(j)
Arr(j) = strTemp
End If
Next j
Next i
End Sub
Strangely, I use the same logic in another userform and it works. Sorry for the lack of clarity.
For each of the above lines I call a bubble sort function. But it errors on one or the other depending on whether I put If Arr(i) > Arr(j) Then ... Or If Arr(i, 1) > Arr(j, 1) Then ...
Your values of i, j are incorrect and hence it errors out. I am guessing that you are getting Subscript out of range error.
Here is a simple way to reproduce the error that you are getting.
Sub Sample()
Dim i As Long, j As Long, k As Integer
Dim MyAr As Variant
MyAr = Range("A1:A5").Value
For i = LBound(MyAr) To UBound(MyAr)
For j = LBound(MyAr) To UBound(MyAr)
If MyAr(j, 1) > MyAr(j + 1, 1) Then
k = MyAr(j, 1)
MyAr(j, 1) = MyAr(j + 1, 1)
MyAr(j + 1, 1) = k
End If
Next
Next
End Sub
The correct way is to loop till UBound(MyAr) - 1
Sub Sample()
Dim i As Long, j As Long, k As Integer
Dim MyAr As Variant
MyAr = Range("A1:A5").Value
For i = LBound(MyAr) To (UBound(MyAr) - 1)
For j = LBound(MyAr) To (UBound(MyAr) - 1)
If MyAr(j, 1) > MyAr(j + 1, 1) Then
k = MyAr(j, 1)
MyAr(j, 1) = MyAr(j + 1, 1)
MyAr(j + 1, 1) = k
End If
Next
Next
End Sub

VBA code won't write array to range, only it's first element

I need to do the following:
lift the range C2:AU264 into an 2D array
create another 1D array, (1 To 11880)
fill second array with values from the first one ("transpose")
write array 2 back to the sheet
Here is the code I am using:
Private Ws As Worksheet
Private budgets() As Variant
Private arrayToWrite() As Variant
Private lastrow As Long
Private lastcol As Long
Private Sub procedure()
Application.ScreenUpdating = False
Set Ws = Sheet19
Ws.Activate
lastrow = Ws.Cells.Find("*", searchorder:=xlByRows, searchdirection:=xlPrevious).row
lastcol = Ws.Cells.Find("*", searchorder:=xlByColumns, searchdirection:=xlPrevious).Column
ReDim budgets(1 To lastrow - 1, 1 To lastcol - 2)
budgets= Ws.Range("C2:AU265")
ReDim arrayToWrite(1 To (lastCol - 2) * (lastRow - 1))
k = 0
For j = 1 To UBound(budgets, 2)
For i = 1 To UBound(budgets, 1)
arrayToWrite(i + k) = budgets(i, j)
Next i
k = k + lastrow - 1
Next j
Set Ws = Sheet6
Ws.Activate
Ws.Range("E2").Resize(UBound(arrayToWrite)).Value = arrayToWrite
'For i = 1 To UBound(arrayToWrite)
'Ws.Range(Cells(i + 1, 5).Address).Value = arrayToWrite(i)
'Next i
Application.ScreenUpdating = True
End Sub
This just writes the first value from the range C2:AU264 (the first element of the first array) through the whole range E2:E11881. If however, I un-comment the For loop just before the end of my script and do it that way, it does work, but is slow. How can I write the array correctly using the first statement?
If you want to write an array to a range, the array must have two dimensions. Even if you only wish to write a single column.
Change
ReDim arrayToWrite(1 To (lastCol - 2) * (lastRow - 1))
to
ReDim arrayToWrite(1 To (lastCol - 2) * (lastRow - 1), 1 To 1)
and
arrayToWrite(i + k) = budgets(i, j)
to
arrayToWrite(i + k, 1) = budgets(i, j)
simply use transpose... change
Ws.Range("E2").Resize(UBound(arrayToWrite)).Value = arrayToWrite
to
Ws.Range("E2").Resize(UBound(arrayToWrite)).Value = Application.Transpose(arrayToWrite)
Hint: there is no need for ReDim budgets(1 To lastrow - 1, 1 To lastcol - 2).
If budgets is a variant then budgets = Ws.Range("C2:AU265") will automatically set the ranges (upper left cell (in this case C2) will be (1, 1)).
EDIT
Assuming you only want to write down all columns (one after another) below each other, you can shorten the macro a bit like that:
Private Sub procedure()
Dim inArr As Variant, outArr() As Variant
Dim i As Long, j As Long, k As Long
With Sheet19
.Activate
inArr = .Range(, .Cells(2, 3), .Cells(.Cells.Find("*", , , , 1, 2).Row, .Cells.Find("*", , , , 2, 2).Column)).Value
End With
ReDim outArr(1 To UBound(inArr) * UBound(inArr, 2))
k = 1
For j = 1 To UBound(inArr, 2)
For i = 1 To UBound(inArr)
k = k + 1
arrayToWrite(k) = budgets(i, j)
Next i
Next j
Sheet6.Range("E2:E" & UBound(arrayToWrite)).Value = Application.Transpose(arrayToWrite)
End Sub
And if you want each row transposed and below each other than simply switch the two For...-lines. (Still the code does basically the same like before)

is it possbile to create an collection of arrays in vba?

first of all, i'd like to say, i've sarched thorugh the net, but i haven't run into such a thing. i've seen collection of collections, or array of arrays, but not a collection of array.
what i want to do is, to collect ID's in collections for each District. Finally, i will join the values in the collections with Join function and ";" as delimiter, and then print them in a range of 4 column as a lookup list, for each class. For example;
Class2(0) will include 54020 and 30734, class2(1) will include 58618, class1(4) will include none, class3(7) will include 35516,34781 and 56874, and so on.
i want to loop through column C and put a select case statment to check the class and then assign the values to collections
Sub dict_coll()
Dim class1() As New Collection
Dim class2() As New Collection
Dim class3() As New Collection
Dim class4() As New Collection
Dim dict As New Scripting.Dictionary
Set dRange = range(range("a2"), range("a2").End(xlDown))
i = 0
For Each d In dRange
If Not dict.Exists(d.Value) Then
dict.Add key:=d.Value, item:=i
i = i + 1
End If
Next d
Set cRange = range(range("c2"), range("c2").End(xlDown))
For Each c In cRange
Select Case c.Value
Case "class1"
class1(dict(c.offset(0,-2).value)).Add c.Offset(0, 1).Value 'fails here
Case "class2"
class2(dict(c.offset(0,-2).value)).Add c.Offset(0, 1).Value 'fails here
Case "class3"
class3(dict(c.offset(0,-2).value)).Add c.Offset(0, 1).Value 'fails here
Case Else
class4(dict(c.offset(0,-2).value)).Add c.Offset(0, 1).Value 'fails here
End Select
Next c
End Sub
and what i want to see is as foloowing:
is there any easier and proper way of what i wanna do? any help wil be appreciated.
thanks
I didnt see that sb variable defined in your code.
Anyway, for me I see a case of straightforward arrays: There is fixed dimension of classes so it good enough for me. Furthermore, you can print back to worksheet so easily.
Public Sub test()
Const strPrefix = "class"
Dim districtRange As Range, outputRange As Range, r As Range
Dim arr() As String
Dim i As Long, j As Long, x As Long, y As Long
Dim district As String, str As String, idVal As String
Dim arr2 As Variant
Application.ScreenUpdating = False
ReDim arr(1 To 5, 1 To 1)
arr(1, 1) = "District"
arr(2, 1) = "Class 1"
arr(3, 1) = "Class 2"
arr(4, 1) = "Class 3"
arr(5, 1) = "Class 4"
Set districtRange = Range(Range("A2"), Range("C2").End(xlDown))
arr2 = districtRange.Value
For x = LBound(arr2, 1) To UBound(arr2, 1)
district = arr2(x, 1)
i = Val(Mid(arr2(x, 3), Len(strPrefix) + 1))
idVal = arr2(x, 2)
j = inArray(arr, district, 1) 'returns -1 if not found
If j >= 0 Then
arr(i + 1, j) = IIf(arr(i + 1, j) = "", idVal, arr(i + 1, j) & ";" & idVal)
Else
ReDim Preserve arr(1 To 5, 1 To UBound(arr, 2) + 1)
arr(1, UBound(arr, 2)) = district
arr(i + 1, UBound(arr, 2)) = idVal
End If
Next x
Set outputRange = Range("E1")
outputRange.Resize(UBound(arr, 2), UBound(arr, 1)).Value = Application.Transpose(arr)
outputRange.Sort Key1:=Range("E1"), Header:=xlYes, Order1:=xlAscending
Application.ScreenUpdating = True
End Sub
Public Function inArray(arr As Variant, k As String, Optional rowNum As Long, Optional colNum As Long) As Long
Dim i As Long, j As Long
inArray = -1
If rowNum Then
For i = LBound(arr, 2) To UBound(arr, 2)
If arr(rowNum, i) = k Then
inArray = i
Exit Function
End If
Next i
Else
For i = LBound(arr, 1) To UBound(arr, 1)
If arr(i, colNum) = k Then
inArray = i
Exit Function
End If
Next i
End If
End Function
by the way, i've found another solution, usinf both dictionary and 3-dimension array.
Sub test()
Dim Blg As New Scripting.Dictionary
Dim Sgm As New Scripting.Dictionary
Dim Siciller() As String
ReDim Siciller(0 To 23, 0 To 3, 0 To 5)
Set alanBolge = range(range("a2"), range("a2").End(xlDown))
Set alanSegment = range(range("c2"), range("c2").End(xlDown))
i = 0
For Each d In alanBolge
If Not Blg.Exists(d.Value) Then
Blg.Add Key:=d.Value, item:=i
i = i + 1
End If
Next d
k = 0
For Each d In alanSegment
If Not Sgm.Exists(d.Value) Then
Sgm.Add Key:=d.Value, item:=k
k = k + 1
End If
Next d
'data reading
For Each d In alanBolge
Siciller(Blg(d.Value), Sgm(d.Offset(0, 2).Value), dolusay(Siciller, Blg(d.Value), Sgm(d.Offset(0, 2).Value)) + 1) = d.Offset(0, 1).Value
Next d
'output
For x = 1 To 4
For y = 1 To 24
Set h = Cells(1 + y, 5 + x)
h.Select
h.Value = sonucgetir(Siciller, Blg(h.Offset(0, -x).Value), Sgm(h.Offset(-y, 0).Value))
Next y
Next x
End Sub
Public Function dolusay(ByVal data As Variant, ByVal i1 As Integer, ByVal i2 As Integer) As Integer
Dim count As Integer
count = 0
For j = 0 To UBound(data, 3) - 1
If Len(data(i1, i2, j)) > 0 Then
count = count + 1
End If
Next
dolusay = count
End Function
Public Function sonucgetir(ByVal data As Variant, ByVal i1 As Integer, ByVal i2 As Integer) As String
sonucgetir = ""
For i = 0 To UBound(data, 3)
If Len(data(i1, i2, i)) > 0 Then
x = data(i1, i2, i) & ";" & x
sonucgetir = Left(x, Len(x) - 1)
End If
Next i
End Function

WorksheetFunction.Small ERROR?

I have the following code:
Option Explicit
Dim ArrTest() As Variant
Dim ArrSmall() As Variant
Dim ArrTemp() As Variant
Dim k As Integer
Dim i As Integer
Sub test()
ReDim ArrTest(10, 2)
ReDim ArrSmall(10, 2)
ReDim ArrTemp(10, 1)
ArrTest = Range("A1:B10")
For k = 1 To 2
For i = 1 To 10
ArrTemp(i, 1) = ArrTest(i, k)
ArrSmall(i, k) = WorksheetFunction.Small(ArrTemp, i)
Cells(i, k + 10) = ArrSmall(i, k)
Next i
Next k
End Sub
Range("A1:B10") is an arbitrary range of numbers which should be ordered from small to big according to the WorksheetFunction.Small. With a single column this works perfectly fine. However, when applied as such (with a loop) the function copies values from the original range and the output is wrong.
Please try for yourself in an Excel sheet and tell me what I'm doing wrong or the function is wrong?
Thanks.
EDIT: I have it working with Application.Index which works in this particular example. See:
Sub test()
ReDim ArrTest(10, 2)
ReDim ArrSmall(10, 2)
ReDim ArrTemp(10, 1)
ArrTest = Range("A1:B10")
For k = 1 To 2
For i = 1 To 10
ArrTemp = Application.Index(ArrTest, 0, k)
'ArrTemp(i, 1) = ArrTest(i, 1)
ArrSmall(i, 1) = WorksheetFunction.Small(ArrTemp, i)
Cells(i, k + 10) = ArrSmall(i, 1)
Next i
Next k
End Sub
Which works fine. But when I apply the exact same logic to my original code it gives me the 1004 error: unable to get Small property. I have no clue.
to leave your code as much as it was (first example) works perfect:
Option Explicit
Dim ArrTest() As Variant
Dim ArrSmall() As Variant
Dim ArrTemp() As Variant
Dim k As Integer
Dim i As Integer
Sub test()
ReDim ArrTest(10, 2) '*
ReDim ArrSmall(10, 2)
ReDim ArrTemp(10, 1)
ArrTest = Range("A1:B10")
For k = 1 To 2
For i = 1 To 10
ArrTemp(i, 1) = ArrTest(i, k)
Next i
For i = 1 To 10
ArrSmall(i, k) = WorksheetFunction.Small(ArrTemp, i)
Cells(i, k + 10) = ArrSmall(i, k)
Next i
Next k
End Sub
No errors or whatever at all...
the '* just is not used at all ArrTest = Range("A1:B10") will set automatically all ranges... still the ranges would be to big for the other ranges...
I get exactly the same output like in your second example...
EDIT:
If you want to sort the full range (not every column itself) then you need something like:
Option Explicit
Dim ArrTest() As Variant
Dim ArrSmall() As Variant
Dim ArrTemp() As Variant
Dim k As Integer
Dim i As Integer
Sub test()
ReDim ArrTest(10, 2)
ReDim ArrSmall(10, 2)
ReDim ArrTemp(10, 1)
ArrTest = Range("A1:B10")
For k = 1 To UBound(ArrTest, 2)
For i = 1 To UBound(ArrTest)
ArrSmall(i, k) = WorksheetFunction.Small(ArrTest, i + ((k - 1) * UBound(ArrTest)))
Cells(i, k + 10) = ArrSmall(i, k)
Next i
Next k
End Sub
Still I beleave the first code should do what you desire ;)
The way you are using the ReDim statement is creating too many elements in the arrays. Try using the LBound function and UBound function after setting the worksheet range values into the first variant array.
Option Explicit
Sub test()
Dim ArrTest() As Variant, ArrSmall() As Variant, ArrTemp() As Variant
Dim k As Long, i As Long
ReDim ArrTest(10, 2) '<~~ unnecessary if writing values from the worksheet
Debug.Print LBound(ArrTest, 1) & ":" & UBound(ArrTest, 1)
Debug.Print LBound(ArrTest, 2) & ":" & UBound(ArrTest, 2)
'^^ this results in 0:10, 0:2. Not 1:10, 1:2
ArrTest = Range("A1:B10").Value2 '<~~ make sure you are putting values in
Debug.Print LBound(ArrTest, 1) & ":" & UBound(ArrTest, 1)
Debug.Print LBound(ArrTest, 2) & ":" & UBound(ArrTest, 2)
'^^ this results in 1:10, 1:2.
ReDim ArrSmall(LBound(ArrTest, 1) To UBound(ArrTest, 1), _
LBound(ArrTest, 2) To UBound(ArrTest, 2))
ReDim ArrTemp(LBound(ArrTest, 1) To UBound(ArrTest, 1), 1 To 1)
For k = LBound(ArrTest, 2) To UBound(ArrTest, 2)
For i = LBound(ArrTest, 1) To UBound(ArrTest, 1)
ArrTemp(i, 1) = ArrTest(i, k)
ArrSmall(i, k) = WorksheetFunction.Small(ArrTemp, i)
Cells(i, k + 10) = ArrSmall(i, k)
Next i
Next k
End Sub
    
It depends on how you write the code.
Sub test()
With Range("A1:B10")
For Each Column In .Columns
x = x + 1
For i = 1 To Column.Cells.Count
Cells(i, 10 + x).Value = Application.Small(Column, i)
Next i
Next Column
End With
End Sub
Edit:
Although this would work with Range my goal is to extract the columns
from an array without having to return to a Sheet. I think this is
limited somehow
Adding Array approach too..
Sub test()
Dim vArr As Variant, varrTemp As Variant
vArr = Application.Transpose(Range("A1:B10").Value)
For i = LBound(vArr, 1) To UBound(vArr, 1)
varrTemp = Application.Index(Application.Transpose(vArr), , i)
For x = LBound(varrTemp) To UBound(varrTemp)
Cells(x, 10 + i).Value = Application.Small(varrTemp, x)
Next x
Next i
End Sub

Resources