VBA - Tree visualisation. Possibly the hardest code to date - vba

I've been stuck here for hours trying to come up with a VBA excel logic for this problem. Think of this as a tree problem, in which the number of branches/nodes is determined by the user and inputted at the start.
For every node, there is 3 possible outcomes, staying flat, increasing by 1% or decreasing by 1%. i.e. for 5 nodes, you will get 5^3 nodes and so on...
How do i code this in vba so that it auto-populates the spreadsheet?
I am trying to model a population birth rate in a city where after every year, T+0 to T+n, where n is the number of nodes. So every +1 year, there could either be 101% of the population, 100% or 99% of the population remaining in the city.
To add to the complexity, the number of people in the city has to be dynamic at any time. So for example we know the path in the 5th year where we get +1% of population every year. so at 5th year we should have (1.01)^5 people. However, since this city is dynamic, there could be people leaving or entering the city, so manual adjustments to the city population has be to catered for.
E.G. At 5th year, 5000 left the city to another place. So the spreadsheet has to be dynamic enough to adjust the 5th level node from (1.01)^5 to (1.01)^5 - 5000. And the 6th node carries on from there.... branching out again.
Not sure if i explained this clearly enough. But this seems to be really hard to code with my amateur vba skills. Is this possible?
Also, the spreadsheet seems to crash when i simulate 10 levels and above
Sub test()
Dim startvalue As Double, levels As Integer, j As Long, i As Long, k As Long
Application.ScreenUpdating = False
startvalue = Sheets("Sheet1").Range("A2")
levels = Sheets("Sheet1").Range("B2")
Sheets("Sheet2").Activate
Cells.ClearContents
Range("A1") = startvalue
For j = 2 To levels
For k = Cells(Rows.Count, j - 1).End(xlUp).Row To 1 Step -1
If Cells(k, j - 1) <> "" Then
Rows(k + 1).Insert shift:=xlDown
Cells(k + 1, j) = Cells(k, j - 1).Value * 0.99
Cells(k, j) = Cells(k, j - 1).Value
Rows(k).Insert shift:=xlDown
Cells(k, j) = Cells(k + 1, j - 1).Value * 1.1
End If
Next k
Next j
End Sub

First and foremost, I'd suggest you store your results on internal memory, then display them on your spreadsheet once it's done. Inserting rows within a loop is a major performance killer.
As for the data structure, you could use a two-dimensional array storing the results of each year
e.g.
year 1 : array(1,1) to array(1,3)
year 2 : array(2,1) to array(2,9)
year 3 : array(3,1) to array(3,27)
...
Once this structure is valued, use a 2nd loop to display it on the spreadsheet "plainly", without the pain of inserting those lines everywhere
It goes like that
Dim values() As Long
Sub main()
Dim startvalue As Double, levels As Integer
Dim i, j, k As Long
startvalue = 2000
levels = 3
ReDim values(levels, 3 ^ levels)
' == Calculate pop evolution for every year ==
values(1, 1) = startvalue
For i = 2 To levels
k = 1
For j = 1 To 3 ^ (levels - 2)
values(i, k) = values(i - 1, j)
values(i, k + 1) = values(i - 1, j) * 0.99
values(i, k + 2) = values(i - 1, j) * 1.01
k = k + 3
Next j
Next i
' == Display in on spreadsheet ==
Sheets("Sheet2").Activate
For i = 1 To levels
Cells(4, i) = i
k = 1
For j = 1 To 3 ^ (i - 1)
Cells(4 + k, i) = values(i, k)
k = k + 1
Next j
Next i
End Sub

Related

Speeding calculations

With some 20K observations, the following code takes some 7.5 sec to run
'Remember time when macro starts
StartTime = Timer
For i = 2 To UBound(avTransposed, 2)
For J = 1 To UBound(avTransposed, 1)
k = IIf(J = 1, k + 1, k)
' If J = 1 Then k = k + 1
ReDim Preserve TrueUsedRangeArray(1 To Dim2, 1 To k)
TrueUsedRangeArray(J, k) = avTransposed(J, i)
Next
Next
'Determine how many seconds code took to run
SecondsElapsed = Round(Timer - StartTime, 2)
Without the
k = IIf(J = 1, k + 1, k) line (or If J = 1 Then k = k + 1), it takes less than one sec!!
Any idea?
The ReDim Preserve is probably killing performance. Every time it is used, it creates a new array and copies the existing array in.
You can work out up-front the size of TrueUsedRangeArray, something like the following
ReDim TrueUsedRangeArray(1 To Ubound(avTransposed, 2), 1 To Ubound(avTransposed, 1))
Too many things in your inner loop which do not need to be there:
For i = 2 To UBound(avTransposed, 2)
k = k + 1
ReDim Preserve TrueUsedRangeArray(1 To Dim2, 1 To k)
For J = 1 To UBound(avTransposed, 1)
TrueUsedRangeArray(J, k) = avTransposed(J, i)
Next
Next
As Patrick notes though, you do not need the redim preserve in the loop, since you already know the final size of TrueUsedRangeArray from the dimensions of avTransposed

Move content to separate cells from one column

Recently I got a big sheet to reformat. I'm not very familiar with vba but I know some stuff and tried my best I could.
It has a Column which has a phone number, some e-mail addresses and a website.
I provided you a small example of how it was, how it should be and how far I have gone.
As you can see I inserted two columns after Id and renamed the header to Phone number, E-Mails and Website. Moving the number was not really hard but I struggle at moving the E-Mail addresses and the website.
In the original sheet Id, Phone number, ... are at the top left (Id A1, Phone number B1, ...)
There are no empty rows in the file. Finding a difference between the E-Mail addresses and the website is done by looking if the cell contains an #. It would be great if someone could help me
Sub RearangeWorkSheet2()
Const IDColumn = 1
Dim arrData()
Dim i As Long, j As Long, RecordID As Long, lastRow As Long, x As Long, y As Long
lastRow = Range("B" & Rows.Count).End(xlUp).row
ReDim arrData(3, 0)
For x = 2 To lastRow
If Cells(x, 1) <> "" Then
RecordID = i
ReDim Preserve arrData(3, i)
arrData(0, RecordID) = Cells(x, 1)
i = i + 1
End If
If IsNumeric(Left(Cells(x, 2), 3)) Then
y = 1
ElseIf InStr(Cells(x, 2), "#") Then
y = 2
Else
y = 3
End If
For j = RecordID To UBound(arrData, 2)
If IsEmpty(arrData(y, j)) Or j = UBound(arrData, 2) Then Exit For
Next
If Not IsEmpty(arrData(y, j)) Then
ReDim Preserve arrData(3, i)
i = i + 1
j = j + 1
End If
arrData(y, j) = Cells(x, 2)
Next
Worksheets("Sheet1").Range("D2").Resize(UBound(arrData, 2) + 1, 4).Value = WorksheetFunction.Transpose(arrData)
End Sub

First VBA Code: Run-time Error "1004"

I am receiving a run-time error, but that may be the least of my problems. The logic makes sense in my head but I may not be using the correct syntax or functions. My code is below with comments and "hopes":
Sub Random_Points()
Dim i As Integer
Dim j As Integer
Dim Max As Integer
Dim RandomNumber As Integer
Max = 100 '(Max is being multiplied by the Rnd function to provide a random number between 0-100)
For i = 2 To 100 Step 1
RandomNumber = Int(Rnd * Max)
ThisWorkbook.Sheets("VBA").Cells(i, 2).Value = RandomNumber
'(The for loop above with start assigned cells values starting with Cells(2,2) to Cells(100,2))
'(I DO NOT WANT DUPLICATE VALUES...therefore after the value is assigned above I want the code to compare the newly assigned cell to all the cells above it.)
For j = 1 To 98 Step 1
'(...and IF the cell values are the same...)
If ThisWorkbook.Sheets("VBA").Cells(i, 2).Value = ThisWorkbook.Sheets("VBA").Cells(i - j, 2).Value Then
'(...A new random number will be assigned...)
RandomNumber = Int(Rnd * Max)
ThisWorkbook.Sheets("VBA").Cells(i, 2).Value = RandomNumber
End If
'(...and then re-checked vs all the others)
Next j
'(Next cell is assigned...loop restarts)
Next i
End Sub
Your problem is in your nested loop. As j increments, it approaches and finally equals i. Subsequently, when you use the two values in .Cells(i - j, 2).Value, there is no Range.Cells property with a row number less than 1.
The solution is to change your nested For ... Next statement so that j never reaches i.
'was ...
For j = 1 To 98 Step 1
'should be ...
For j = 1 To (i - 1) Step 1
You only need to check the values up to i in any event.
fwiw, a WorksheetFunction object's use of MATCH function and VBA's IsError function would be faster.
Sub randomPoints_part_deux()
Dim i As Long, mx As Long, randNum As Long
mx = 100 '(mx is being multiplied by the Rnd function to provide a random number between 0-100)
With ThisWorkbook.Sheets("VBA")
'seed the column of numbers so you have something to check against
randNum = Int(Rnd * mx)
.Cells(2, 2) = randNum
For i = 3 To 100 Step 1
Do While Not IsError(Application.Match(randNum, .Range(.Cells(2, 2), .Cells(i - 1, 2)), 0))
randNum = Int(Rnd * mx)
Loop
.Cells(i, 2) = randNum
Next i
'optional formula to count unique in C2
.Cells(2, 3).Formula = "=SUMPRODUCT(1/COUNTIF(B2:B100, B2:B100))"
End With
End Sub
since you don't want duplicates you can either generate random numbers and then repeatedly check if they are already used or you can generate your list first and then pull from it randomly. The second option is easier.
Sub Random100()
Dim i As Integer
Dim j As Integer
Dim Max As Integer
Dim RandomNumber As Integer
Dim cNum As New Collection
Max = 100 '(Max is being multiplied by the Rnd function to provide a random number between 0-100)
For i = 0 To Max 'fill collection with 0-100 in order
cNum.Add i
Next i
k = cNum.Count - 1
For j = 0 To k
RandomNumber = Int(Rnd * (k - j)) + 1
ThisWorkbook.Sheets("VBA").Cells(j + 2, 2).Value = cNum(RandomNumber)
cNum.Remove (RandomNumber)
Next j
End Sub
If your purpose is to get a range of unique values, then a better approach would be to shuffle a serie:
Const MIN = 1
Const MAX = 98
Dim values(MIN To MAX, 0 To 0) As Double, i&, irand&
' generate all the values
For i = MIN To MAX
values(i, 0) = i
Next
' shuffle the values
For i = MIN To MAX
irand = MIN + Math.Round(Rnd * (MAX - MIN))
value = values(i, 0)
values(i, 0) = values(irand, 0)
values(irand, 0) = value
Next
' copy the values to the sheet
ThisWorkbook.Sheets("VBA").Range("A2").Resize(MAX - MIN + 1, 1) = values

VBA Macro Speed Up

I would appreciate some help on the following VBA Macro problem,
screenshot here:
I have to compare the data in 2 columns - Index & Sec_Index. In case of a match it should check which Values is assigned to the Sec_Index and fill a "1" to the matching Value column corresponding to Index and "0" for the other Value columns (I hope the screenshot explains it better)
I wrote a short macro which works good. However I have huge amounts of data - both Index columns contain at least 400000-500000 lines. This makes my code useless since it will take extreme long durations to execute.
Is there a way to make this work? I read about Variant arrays, but I'm not that familiar with them.
You can put this formula (if Excel 2007 or above):
=COUNTIFS($H$2:$H$5,$B2,$I$2:$I$5,"A")
into C2 and copy it down and across; just change "A" to "B" and "C".
Added In view of the number of rows, I would import the data into MS Access, create a Crosstab Query, then copy this data back to Excel.
Try this, not overly robust but does work. Not sure how quickly this will compare to what you may have had?
It did about 60,000 rows with 25 keys in about 5 seconds.
Edit: Added timer to function.
Public Declare Function GetTickCount Lib "kernel32.dll" () As Long
public Sub main()
Dim t As Long
t = GetTickCount
Application.ScreenUpdating = False
Dim Arr1(), Arr() As Double
Dim x, y, i, j As Double
Dim v As String
x = Cells(Rows.Count, 2).End(xlUp).Row - 2
y = Cells(Rows.Count, 8).End(xlUp).Row - 2
Range("c2", "e" & x + 2) = 0
ReDim Arr1(x)
ReDim Arr2(y)
i = 0
Do Until Cells(i + 2, 2) = ""
Arr1(i) = Cells(i + 2, 2)
i = i + 1
Loop
i = 0
Do Until Cells(i + 2, 8) = ""
Arr2(i) = Cells(i + 2, 2)
i = i + 1
Loop
i = 0
Do Until i > UBound(Arr1)
j = 0
Do Until j > UBound(Arr2)
If Arr1(i) = Arr2(j) Then
v = Cells(Arr2(j) + 1, 9)
Select Case v
Case "a"
Cells(i + 2, 3) = 1
Case "b"
Cells(i + 2, 4) = 1
Case "c"
Cells(i + 2, 5) = 1
End Select
Exit Do
End If
j = j + 1
Loop
i = i + 1
Loop
MsgBox GetTickCount - t, , "Milliseconds"
End Sub

Passing a global variable in vba to a function

So i have this number of different two dimensional arrays that contain different physical attributes of a flow (mach number, temperature, etc). i need to plot these values in excel and calculate different averages. these arrays are declared globally. To do this i used to write one subroutine for each array but its not a good way, because you should keep it as general as possible right?
so what i end up with is: the subroutine is passed the arrays and the corresponding worksheet...
or use pointers? would also be interesting to know how you would solve this in other more advanced programming languages (C++,..) since vba is not quite my weapon of choice but in this case its necessary.
It errors with a "Type mismatch: Array or user-defined type expected!"
When i call i use:
Call WriteFlowVariable(ws_meridian_velocity, MeridSpeed(), average_MeridSpeed(), area_average_MeridSpeed(), mass_average_MeridSpeed())
This is the Subroutine:
Sub WriteFlowVariable(ws As Worksheet, FlowVariable() As Double, average_FlowVariable() As Double, area_average_FlowVariable(), mass_average_FlowVariable())
Dim i As Integer
Dim j As Integer
Dim sum_v As Double
Dim FlowVariabledeltaA(1000, 300) As Double
Dim FlowVariabledeltaA_added(1000) As Double
Dim FlowVariabledeltaM(1000, 300) As Double
Dim FlowVariabledeltaM_added(1000) As Double
'write titles of the charts
ws.Cells.Clear
For j = 0 To SecNumber - 1
ws.Cells(1, j + 2).value = (j + 1)
Next j
ws.Cells(1, SecNumber + 2) = "Arithmetic Average"
ws.Cells(1, SecNumber + 3) = "Area Average"
For i = 0 To number_of_axial_positions - 1
sum_v = 0
'Write section number
ws.Cells(i + 2, 1).value = i + 1
'Write value and calculate arithmetic, mass and area average
For j = 0 To SecNumber - 1
ws.Cells(i + 2, j + 2).value = FlowVariable(i, j)
sum_v = sum_v + FlowVariable(i, j)
If j < SecNumber - 1 Then
FlowVariable(i, j) = FlowVariable(i, j) * deltaA(i, j)
FlowVariabledeltaA_added(i) = FlowVariabledeltaA_added(i) + FlowVariabledeltaA(i, j)
FlowVariabledeltaM(i, j) = FlowVariable(i, j) * deltaM(i, j)
FlowVariabledeltaM_added(i) = FlowVariabledeltaM_added(i) + FlowVariabledeltaM(i, j)
End If
Next j
average_FlowVariable(i) = sum_v / SecNumber
'Write arithmetic average to the third last column
ws.Cells(i + 2, SecNumber + 2) = average_FlowVariable(i)
area_average_FlowVariable(i) = FlowVariabledeltaA_added(i) / crosssectionareaInput(i)
'Write area average to the second last column
ws.Cells(i + 2, SecNumber + 3) = area_average_FlowVariable(i)
mass_average_FlowVariable(i) = FlowVariabledeltaM_added(i) / crosssectionareaInput(i)
'Write mass average to the last column
ws.Cells(i + 2, SecNumber + 4) = mass_average_FlowVariable(i)
Next i
End Sub
I didn't examine your Sub too closely at all, but assuming
FlowVariable(,) As Double
average_FlowVariable(,) As Double
area_average_FlowVariable(,)
mass_average_FlowVariable(,)
are all declared correctly to be considered global variables, your call should look as follows:
Call WriteFlowVariable(ws_meridian_velocity, MeridSpeed, average_MeridSpeed, area_average_MeridSpeed, mass_average_MeridSpeed)
... In other words, remove the ().
Also, since these are 2-D arrays, your Sub should look as follows:
Sub WriteFlowVariable(ws As Worksheet, FlowVariable(,) As Double, average_FlowVariable(,) As Double, area_average_FlowVariable(,), mass_average_FlowVariable(,))Dim i As Integer
Hope this does the trick, otherwise, lemme know and I'll look closer.