I tried to write a simple bubble sort function in VBA but it does not work. The code is following
Public Function BubbSort(arr As Variant) As Variant
Dim arrReturn As Variant
arrReturn = Array()
ReDim arrReturn(UBound(arr))
arrReturn = arr.Value
For i = 1 To UBound(arr) - 1
For j = 1 To UBound(arr) - 1
If arrReturn(j) > arrReturn(j + 1) Then
temp = arrReturn(j)
arrReturn(j) = arrReturn(j + 1)
arrReturn(j + 1) = temp
End If
Next j
Next i
arr = arrReturn
End Function
In Excel I tried to select 4x1 range of cells and insert formula $=BubbSort(A1:A4)$ and press Ctrl+Shift+Enter so it works as array function, but it said "Function has an error". Any help please?
If arr is a range then UBound(arr) will throw an error. The way around this is to use the line
arrReturn = arr.Value
instead of just
arrReturn = arr
and then to use Ubound(arrReturn)
Also -- arrReturn would be a 2-d array rather than a 1-d array. All references to arrReturn should look like e.g. arrReturn(j,1)
Finally -- you aren't returning anything (which is assigning to the function name in VBA).
The following code seems to work (if Bubble sort can ever be described as "working"):
Public Function BubbSort(arr As Variant) As Variant
Dim arrReturn As Variant
Dim i As Long, j As Long, temp As Variant
arrReturn = arr.Value
For i = 1 To UBound(arrReturn) - 1
For j = 1 To UBound(arrReturn) - 1
If arrReturn(j, 1) > arrReturn(j + 1, 1) Then
temp = arrReturn(j, 1)
arrReturn(j, 1) = arrReturn(j + 1, 1)
arrReturn(j + 1, 1) = temp
End If
Next j
Next i
BubbSort = arrReturn
End Function
The above will only work when passed a range contained in a column. It is possible to make it more flexible to be able to handle either a column range or a row range or a VBA array.
While John Coleman's code will technically work it is fundamentally flawed. You will notice that the outer loop over i is invariant - the loop value is never used.
Bubblesort is indeed inefficient (at least for long sequences) but not as much as the code enforces.
At the end of the inner loop the biggest element will have propagated to the very end of the sequence. So, there is no need to compare it with any previous elements during the next repetitions of the inner loop. If you shorten the inner loop every time by 1 (in total, by the number of outer loop completions so far, i) you reduce the number of loops by 50%:
Public Function Bubble1(arr As Variant) As Variant
Dim arrReturn As Variant
Dim i As Long, j As Long, temp As Variant
arrReturn = arr.Value
For i = UBound(arrReturn, 1) To 2 Step -1
For j = 1 To i - 1
If arrReturn(j, 1) > arrReturn(j + 1, 1) Then
temp = arrReturn(j, 1)
arrReturn(j, 1) = arrReturn(j + 1, 1)
arrReturn(j + 1, 1) = temp
End If
Next j
Next i
Bubble1 = arrReturn
End Function
While we're at it we can further reduce the effort by observing that if all elements are sorted (that is, no exchanges have happened in the inner loop) we are finished - further loops are redundant. We can implement this with a flag:
Public Function Bubble2(arr As Variant) As Variant
Dim arrReturn As Variant
Dim i As Long, j As Long, temp As Variant
Dim sorted As Boolean
arrReturn = arr.Value
For i = UBound(arrReturn, 1) To 2 Step -1
sorted = True
For j = 1 To i - 1
If arrReturn(j, 1) > arrReturn(j + 1, 1) Then
temp = arrReturn(j, 1)
arrReturn(j, 1) = arrReturn(j + 1, 1)
arrReturn(j + 1, 1) = temp
sorted = False
End If
Next j
If sorted Then Exit For
Next i
Bubble2 = arrReturn
End Function
Related
I'm currently working on this vba programming question.
Here's my code:
Option Explicit
Option Base 1
Function ShiftVector(rng As Range, n As Integer) As Variant
Dim A() As Variant, B() As Variant
Dim nr As Integer, i As Integer
nr = rng.Rows.Count
ReDim A(nr, 1) As Variant
ReDim B(nr, 1) As Variant
A = rng
For i = 1 To nr - n
If i <= (nr - n) Then
B(i, 1) = A(i + n, 1)
End If
Next i
For i = (nr - n + 1) To nr
If i <= nr Then
B(i, 1) = A(i - nr + n, 1)
End If
Next i
ShiftVector = B
End Function
When n=3, the top 3 items cannot be wrapped down to the bottom. Instead, 0s are showing in the bottom three cells.
Here is the flowchart to the problem.
Any assistance will be much appreciated, thanks!
Running your code, it works nicely for values between 0 to the number of rows of rng (including n=3. However, it fails for values of n > nr and also n < 0 (negative), which is important if the user of your function needs to shift the vector down by n positions.
I also have to admit that I struggled to understand the logic of your algorithm and why you need 2 loops to perform the task (entirely a failure on my part).
Edit:
I completely undertand the logic of your algorithm now with the aid of your nice flow-chart. I don't see why it wouldn't work for you, it works well on my computer for 0 <= n <= nr.
End of Edit
In situations like this using Mod seems like the most natural option to me. It accounts for all values of n nicely (positive, negative or zero)
Here is a working code. If you want to build your own algorithm, then look away now :)
Please note the use of Long wherever one might be tempted to use Integer, but obviously you should change it if it is a requirement of the question.
Function ShiftVector(rng As Range, n As Long) As Variant
Application.Volatile
Dim aResult() As Variant
Dim vRng As Variant
Dim lRows As Long
Dim i As Long
Dim i_new As Long
vRng = rng.Value ' Process vector more efficiently
lRows = rng.Rows.Count ' Don't use Ubound(vRng) here
ReDim aResult(1 To lRows, 1 To 1)
If lRows = 1 Then
' vRng is simply the value of the cell (not array of values)
' You'll get an error in you try something like vRng(i, 1)
aResult(1, 1) = vRng
Else
For i = 1 To lRows
' This is the transformation rule.
' Using Mod is the most natural way in this case
' and works for all values of n (+, - and 0).
' iRows * Abs(n) ensures (i - n + iRows * Abs(n)) > 0
i_new = (i - n + lRows * Abs(n)) Mod lRows
' If n is a multiple of iRows then i_new=0
If i_new = 0 Then i_new = lRows
' Perform transformation
aResult(i_new, 1) = vRng(i, 1)
Next i
End If
ShiftVector = aResult
End Function
Something like this:
Function ShiftVector(rng As Range, n As Integer) As Variant
Dim A As Variant, B As Variant
Dim nr As Long, i As Long, pos As Long
A = rng.Value
nr = UBound(A, 1)
ReDim B(1 To nr, 1 To 1)
For i = 1 To nr
pos = IIf(i > n, i - n, (nr - n) + i)
B(pos, 1) = A(i, 1)
Next i
ShiftVector = B
End Function
FYI I would avoid Option Base 1 in VBA - the default base is zero and it's best to leave it at that. And Option Base 1 has no impact on the base of an array obtained using someRange.Value (always 1)
I was wondering if someone can help me with the following,
In VBA in Excel, I have the following table :
Column 1|Column2|Column3|Column4|Column5|Column6
---------|---------|---------|---------|---------|---------
1.2.3.4|Apple%Car|Canada%USA|Tomatoes|Hotel|Montreal%Paris%New-York
1.3.4.6|Cat%Uniform%Dog|France|Ananas|Motel|Amsterdam%San-Diego
And I would like to convert this in Excel using VBA into the following table :
Column 1|Column 2|Column 3|Column 4|Column 5|Column 6
:---------:|:---------:|:---------:|:---------:|:---------:|:---------:
1.2.3.4|Apple|Canada|Tomatoes|Hotel|Montreal
1.2.3.4|Apple|Canada|Tomatoes|Hotel|Paris
1.2.3.4|Apple|Canada|Tomatoes|Hotel|New-York
1.2.3.4|Apple|USA|Tomatoes|Hotel|Montreal
1.2.3.4|Apple|USA|Tomatoes|Hotel|Paris
1.2.3.4|Apple|USA|Tomatoes|Hotel|New-York
1.2.3.4|Car|Canada|Tomatoes|Hotel|Montreal
1.2.3.4|Car|Canada|Tomatoes|Hotel|Paris
1.2.3.4|Car|Canada|Tomatoes|Hotel|New-York
1.2.3.4|Car|USA|Tomatoes|Hotel|Montreal
1.2.3.4|Car|USA|Tomatoes|Hotel|Paris
1.2.3.4|Car|USA|Tomatoes|Hotel|New-York
1.3.4.6|Cat|France|Ananas|Motel|Amsterdam
1.3.4.6|Cat|France|Ananas|Motel|San-Diego
1.3.4.6|Uniform|France|Ananas|Motel|Amsterdam
1.3.4.6|Uniform|France|Ananas|Motel|San-Diego
1.3.4.6|Dog|France|Ananas|Motel|Amsterdam
1.3.4.6|Dog|France|Ananas|Motel|San-Diego
Does anyone have an idea how to do this ?
Thank you !
To get my brain going I bit. This does more or less what you want (However there is room for improvement as it currently can produce duplicate rows which it then removes at the end. I've missed something but as you haven't tried anything I haven't put any more effort in figuring out where this is happening exactly).
You'll also have to change the Ranges for where your inputs and outputs come from in the ConvertToTable sub. This uses a recursive function (i.e. one that calls itself) to populate your output
Option Explicit
Public Sub ConvertToTable()
Dim data As Variant, tmp() As Variant
Dim arr() As Variant
Dim i As Long
Dim c As Range
With Sheet2
data = Range(.Cells(1, 1), .Cells(2, 6)).Value2
End With
For i = LBound(data, 1) To UBound(data, 1)
tmp = Application.Index(data, i, 0)
arr = PopulateResults(tmp, "%", arr)
Next i
With Sheet4
With .Range(.Cells(1, 1), .Cells(UBound(arr, 2), UBound(arr, 1)))
.Value2 = Application.Transpose(arr)
.RemoveDuplicates Columns:=Array(1, 2, 3, 4, 5, 6), Header:=xlNo
End With
End With
End Sub
Public Function PopulateResults(tmp As Variant, delimiter As String, Results() As Variant) As Variant()
Dim i As Long, j As Long
Dim DelCount As Long, MaxDel As Long
Dim tmp2 As Variant
On Error Resume Next
i = UBound(Results, 2) + 1
If i = 0 Then i = 1
On Error GoTo 0
ReDim Preserve Results(1 To UBound(tmp), 1 To i)
For j = 1 To UBound(tmp)
Results(j, i) = tmp(j)
If InStr(1, tmp(j), delimiter, vbTextCompare) Then
DelCount = 0
Results(j, i) = Split(tmp(j), delimiter)(DelCount)
Do
DelCount = DelCount + 1
tmp2 = tmp
tmp2(j) = Split(tmp(j), delimiter)(DelCount)
Results = PopulateResults(tmp2, delimiter, Results)
Loop Until DelCount = Len(tmp(j)) - Len(Replace(tmp(j), delimiter, vbNullString))
End If
Next j
PopulateResults = Results
End Function
Thank you very much, It is much appreciated. Sorry for the delay, I didn't get any e-mail notification for the response.
I played with the source code and I have the following, it works for all the column that contain short value.. :
'Transform the data
Dim data As Variant, tmp() As Variant
Dim arr() As String
Dim i As Long
Dim c As Range
With Aggregation_Source
data = Range(Cells(1, 1), Cells(2, 8)).Value2
End With
For i = LBound(data, 1) To UBound(data, 1)
tmp = Application.Index(data, i, 0)
arr = PopulateResults(tmp, "%", arr)
Next i
With Aggregation_Source
With Range(Cells(1, 1), Cells(UBound(arr, 2), UBound(arr, 1)))
.Value2 = Application.Transpose(arr)
.RemoveDuplicates Columns:=Array(1, 2, 3, 4, 5, 6, 7), Header:=xlNo
End With
End With
End Sub
Public Function PopulateResults(tmp As Variant, delimiter As String, Results() As String) As String()
Dim i As Long, j As Long
Dim DelCount As Long, MaxDel As Long
Dim tmp2 As Variant
On Error Resume Next
i = UBound(Results, 2) + 1
If i = 0 Then i = 1
On Error GoTo 0
ReDim Preserve Results(1 To UBound(tmp), 1 To i)
For j = 1 To UBound(tmp)
Results(j, i) = tmp(j)
If InStr(1, tmp(j), delimiter, vbTextCompare) Then
DelCount = 0
Results(j, i) = Split(tmp(j), delimiter)(DelCount)
Do
DelCount = DelCount + 1
tmp2 = tmp
tmp2(j) = Split(tmp(j), delimiter)(DelCount)
Results = PopulateResults(tmp2, delimiter, Results)
Loop Until DelCount = Len(tmp(j)) - Len(Replace(tmp(j), delimiter, vbNullString))
End If
Next j
PopulateResults = Results
End Function
Now, I think that the code crash because I have one column that contains two long text separated by % more than a 1000 characters, I will try to change the type for arr() to see if it works but I think I am missing something in the code .
Really new and self taught so would appreciate any help
Have allocated a range from a spreadsheet to an array but it starts the data in row 1 of the array.
Dim arr As Variant
arr = Range("DATA")
Is there any way of starting in row 0?
Thanks in advance
Range operations are slow, the best is to use a temporary array , and then make a copy to another array while changing the start position.
Indeed array -> array = very fast
Dim arr, arr2 As Variant
arr2 = Range("DATA").value2
redim arr(0 to ubound(arr2)-1, 0 to ubound(arr2,2)-1)
for i= 0 to ubound(arr2)-1
for j= 0 to ubound(arr2,2)-1
arr(i,j)= arr2(i+1,j+1)
next
next
Well, if you are going to clean-up range-derived VBA arrays, why not make them 1-dimensional instead of falsely 2-dimensional in the case that you are dealing with a column range or a row range? The following does that as a default and also uses 0-based as a default, but provides ways to override both defaults:
Function RangeToArray(R As Range, Optional b As Long = 0, Optional collapse As Boolean = True) As Variant
'returns a b-based array of the values in R
'if the Range is 1-dimensional and collapse is true, the array returned is 1-dimensional
Dim i As Long, j As Long, m As Long, n As Long, k As Long
Dim vals As Variant, arr As Variant
m = R.Rows.Count
n = R.Columns.Count
If m = 1 And n = 1 Then 'to catch an edge-case
ReDim vals(1 To 1, 1 To 1)
vals(1, 1) = R.Value
Else
vals = R.Value
End If
If collapse And (m = 1 Or n = 1) Then
k = Application.WorksheetFunction.Max(m, n)
ReDim arr(b To b + k - 1)
For i = 1 To k
If m = 1 Then
arr(b + i - 1) = vals(1, i)
Else
arr(b + i - 1) = vals(i, 1)
End If
Next i
Else
ReDim arr(b To b + m - 1, b To b + n - 1)
For i = 1 To m
For j = 1 To n
arr(b + i - 1, b + j - 1) = vals(i, j)
Next j
Next i
End If
RangeToArray = arr
End Function
It would be used like A = RangeToArray(Range("A1:B3"))
I want to combine arrays in my program. Take those as an example:
a = {1,2,3,4,5}
b = {6,7,8,9,10}
This should produce an array containing first element of first array with all elements of second array except first element, which is {1,7,8,9,10}. This should continue for all possible combinations, producing these output arrays:
{1,7,8,9,10} {6,2,8,9,10} {6,7,3,9,10} {6,7,8,4,10} {6,7,8,9,5}...
For 2,3,4 elements of first array with second array
{1,2,8,9,10} {6,2,3,9,10} {6,7,3,4,10} {6,7,8,4,5} {1,7,8,9,5}...
{1,7,3,9,10} {6,2,8,4,10} {6,7,3,9,5} {1,7,8,4,10}.....
and vice versa for second array.
For the first combination I've tried:
For I = 0 To 4
'first array loop
For J = 0 To 4
'second array loop
If I <> J Then
arr(J) = arr2(J)
Else
arr(J)=arr1(J)
End If
Next
Next
This will give you every combination possible:
Function fifth(ParamArray arr() As Variant) As Variant()
Dim temp() As Variant
Dim i As Long
Dim j As Long
Dim t As Long
For i = LBound(arr) + 1 To UBound(arr)
If UBound(arr(LBound(arr))) <> UBound(arr(i)) Then
MsgBox "Arrays not same size"
Exit Function
End If
Next i
ReDim temp(1 To (UBound(arr) + 1) ^ (UBound(arr(LBound(arr))) + 1), LBound(arr(LBound(arr))) To UBound(arr(LBound(arr)))) As Variant
For i = 1 To (UBound(arr) + 1) ^ (UBound(arr(LBound(arr))) + 1)
For j = 1 To (UBound(arr(LBound(arr))) + 1)
t = Int((i Mod ((UBound(arr) + 1) ^ j)) / (((UBound(arr) + 1) ^ j) / (UBound(arr) + 1)))
temp(i, j - 1) = arr(t)(j - 1)
Next j
Next i
fifth = temp
End Function
You would call thus:
Sub ArrCombine()
Dim arr1() As Variant
Dim arr2() As Variant
Dim rsltarr() As Variant
arr1 = Array(1, 2, 3, 4, 5)
arr2 = Array(6, 7, 8, 9, 10)
rsltarr = fifth(arr1, arr2)
ActiveSheet.Range("A1").Resize(UBound(rsltarr, 1), UBound(rsltarr, 2) + 1).Value = rsltarr
End Sub
It will output on the active sheet, this:
This also has the advantage of being dynamic. You can use more than two arrays. The only rule is that they need to have the same number of values.
The only other limit is the number of rows on a worksheet. So (number of arrays) ^ (number of values) cannot exceed 2^20.
I'm using VBA to calculate pairwise slopes, store them in an array, then using Chip Pearson's technique of transposing the array on a workheet to sort them. My code fails when the number of slopes exceeds 65K, which would make sense in Excel 2003, due to number of rows. I thought it would work in Excel 2010, but I seem to have the same issue. Does anyone know if there's limitations to the Resize property or Transpose method?
Thanks
Sub pairwise()
Dim endrow As Long, i As Long, j As Long, s As Long
Dim num As Double, denom As Double, sij As Double
Dim r As Range
Dim slopes()
endrow = Range("A1").End(xlDown).Row
n = endrow - 1
nrd = endrow * n / 2
ReDim slopes(nrd)
Debug.Print LBound(slopes); UBound(slopes)
For i = 1 To n
For j = (i + 1) To endrow
num = Cells(i, 2).Value - Cells(j, 2).Value
denom = Cells(i, 1).Value - Cells(j, 1).Value
If denom <> 0 Then
sij = num / denom
slopes(s) = sij
s = s + 1
End If
Next j
Next i
Set r = Range("C1").Resize(UBound(slopes) - LBound(slopes) + 1, 1)
r = Application.Transpose(slopes)
' sort the range
r.Sort key1:=r, order1:=xlAscending, MatchCase:=False
End Sub
I found the same limitation on the INDEX function. http://dailydoseofexcel.com/archives/2013/10/11/worksheetfunction-index-limitations/
Here's how you can make the output array a two dimensional array and read in all the values at once rather than inside a loop.
Sub pairwise()
Dim lEndRow As Long
Dim vaValues As Variant
Dim aSlopes() As Variant
Dim lCnt As Long
Dim rOutput As Range
Dim i As Long, j As Long
'A 2d array here can easily be written to a sheet
lEndRow = Sheet3.Range("a1").End(xlDown).Row
ReDim aSlopes(1 To lEndRow * (lEndRow - 1), 1 To 1)
'Create a two-d array of all the values
vaValues = Sheet3.Range("A1").Resize(lEndRow, 2).Value
'Loop through the array rather than the cells
For i = LBound(vaValues, 1) To UBound(vaValues, 1) - 1
For j = 1 + 1 To UBound(vaValues, 1)
If vaValues(i, 1) <> vaValues(j, 1) Then
lCnt = lCnt + 1
aSlopes(lCnt, 1) = (vaValues(i, 2) - vaValues(j, 2)) / (vaValues(i, 1) - vaValues(j, 1))
End If
Next j
Next i
'Output the array to a range, and sort
Set rOutput = Sheet3.Range("C1").Resize(UBound(aSlopes, 1), UBound(aSlopes, 2))
rOutput.Value = aSlopes
rOutput.Sort rOutput.Cells(1), xlAscending, , , , , , , , False
End Sub
It a limitation of the Transpose method.
My suggestion would be to declare your array as 2D from the start
Redim Slopes(1 To nrd, 1 To 1)
Also, your should use the Variant Array approach instead of looping over cells in your For loop