I have this code for finding the column letter of a given header:
Public Function GetColumnLetter(ByRef in_cells As Range, ByVal column_header As String, Optional look_at As Excel.XlLookAt = xlPart) As String
GetColumnLetter = Split(in_cells.Find(what:=column_header, LookAt:=look_at, SearchOrder:=xlByRows).Address(ColumnAbsolute:=False), "$")(0)
End Function
But it seems to be a little slow. It seems to take a few seconds on some worksheets when it seems like it shouldn't take more than a second. I have to do this for a number of different columns in a number of different worksheets.
Is there a faster way of doing this?
EDIT: I just made a helper function that calls this function, but w/just the first row (in_cells.Range("1:1)), and also changed xlByRows to xlByColumns, and this has sped things enough.
For what it's worth, here is an extremely fast function that does what you want without invoking Find. According to my (very old) notes, it came from here. The parameter c represents the index of the column in question, as in the property Selection.Column.
Public Function GetColumnLetter(ByVal c As Long) As String
Dim p As Long
While c
p = 1 + (c - 1) Mod 26
c = (c - p) \ 26
GetColumnLetter = Chr$(64 + p) & GetColumnLetter
Wend
End Function
EDIT: Given clarifications in comments, here is a setup for testing .Find vs .Match, which appears to be faster. Using the variant values of the array might be faster still, but I'll leave it here.
Set up a first row of data:
Public Sub MakeUglyFirstRow()
Dim rng As Excel.Range
Dim i As Long, p As Long
Dim strChar As String
Dim initialLength As Integer
Set rng = ActiveSheet.Rows(1)
initialLength = 5
For i = 1 To rng.Cells.Count
p = 1 + (i - 1) Mod 26
strChar = String(initialLength, Chr$(64 + p))
rng.Cells(i).Value = strChar
If i Mod 26 = 0 Then initialLength = initialLength + 1
Next i
End Sub
The original function (plus error handling for a value not found), and a match version calling the function above:
Public Function GetColumnLetter_ByFind(ByRef in_cells As Range, ByVal column_header As String, Optional look_at As Excel.XlLookAt = xlPart) As String
Dim rngFound As Excel.Range
Set rngFound = in_cells.Find(what:=column_header, LookAt:=look_at, SearchOrder:=xlByRows)
If Not (rngFound Is Nothing) Then
GetColumnLetter_ByFind = Split(rngFound.Address(ColumnAbsolute:=False), "$")(0)
End If
End Function
Public Function GetColumnLetter_ByMatch(in_cells As Range, text_to_find As String, Optional look_at As Excel.XlLookAt = XlLookAt.xlPart) As String
On Error Resume Next
Dim rngFirstRow As Excel.Range
Dim result As Variant
Dim col As Long
Dim r As Long
Set rngFirstRow = in_cells.Rows(1)
col = 0
With Application.WorksheetFunction
If look_at = xlPart Then
result = .Match("*" + text_to_find + "*", rngFirstRow, 0)
Else
result = .Match(text_to_find, rngFirstRow, 0)
End If
If .IsError(result) = False Then
col = CLng(result) 'will need an offset if the range's first column is not 1
End If
End With
If col > 0 Then
GetColumnLetter_ByMatch = GetColumnLetter(col)
End If
End Function
The (very crude) test method (some parameter explanation below):
Public Sub Test_ColumnFinding(Optional testString As String = "yyy", _
Optional numberOfTests As Long = 1000, _
Optional printResults As Boolean = True, _
Optional printEvery As Integer = 10)
Dim rng As Excel.Range
Dim timStart1 As Single, timEnd1 As Single, timTotal1 As Single
Dim timStart2 As Single, timEnd2 As Single, timTotal2 As Single
Dim strTest1 As String, strTest2 As String
Dim i As Long
Set rng = ActiveSheet.Rows(1)
For i = 1 To numberOfTests
timStart1 = Timer
strTest1 = GetColumnLetter_ByFind(rng, testString, IIf(i Mod 3 = 0, XlLookAt.xlPart, XlLookAt.xlWhole))
timEnd1 = Timer
timTotal1 = timTotal1 + (timEnd1 - timStart1)
timStart2 = Timer
strTest2 = GetColumnLetter_ByMatch(rng, testString, IIf(i Mod 3 = 0, XlLookAt.xlPart, XlLookAt.xlWhole))
timEnd2 = Timer
timTotal2 = timTotal2 + (timEnd2 - timStart2)
If printResults Then
If i Mod printEvery = 0 Then
Debug.Print i, "GetColumnLetter_ByFind", strTest1, timEnd1 - timStart1
Debug.Print i, "GetColumnLetter_ByMatch", strTest2, timEnd2 - timStart2
End If
End If
Next i
Debug.Print "GetColumnLetter_ByFind took " & timTotal1 / numberOfTests & " seconds on avg to execute"
Debug.Print "GetColumnLetter_ByMatch took " & timTotal2 / numberOfTests & " seconds on avg to execute"
End Sub
Where testString controls distance to match, numberOfTests the repetition, printResults whether or not to see debug output, and printEvery how often to check in on that output.
My results, for 1000 tests but no result debug output:
GetColumnLetter_ByFind took 0.003546875 seconds on avg to execute
GetColumnLetter_ByMatch took 0.00134375 seconds on avg to execute
Related
So the problem is more in depth than a simple comparison. Essentially im trying to model this dice roll known as the roll and keep system. Example would be 5k3. Where I would roll 5 dice and keep the 3 highest then add them together.
I've gotten my little macro program to roll the dice. Then I put them in an array in my example that would be an array with 5 indices. Now I need to take those 5 dice, and only keep the largest 3 of them.
The code is here A2 gives me the number of sides on the dice, B2 gives me how many I roll, and C2 gives me how many I keep. This rolls 10 dice, but then I transfer 5 of them into my actual dicepool. I know I could probably skip that, but I can deal with that later.
Private Sub CommandButton1_Click()
Dim i As Integer
Dim j As Integer
Dim k As Integer
Dim RandNum As Integer
Dim RollArray() As Integer
Dim KeptArray() As Integer
Dim RollArrayDummy() As Integer
Dim NumRoll As Integer
Dim Kept As Integer
Dim Largest As Integer
NumRoll = Range("B2").Value
ReDim RollArray(NumRoll)
Kept = Range("C2").Value
ReDim KeptArray(Kept)
For i = 5 To 15
Randomize
RandNum = 1 + Rnd() * (Range("A2").Value - 1)
Cells(i, 1).Value = RandNum
Next i
For j = 1 To NumRoll
RollArray(j) = Cells(4 + j, 1).Value
Cells(4 + j, 2).Value = RollArray(j)
Next j
k = 1
i = 1
m = 1
Largest = 1
For k = 1 To Kept
m = 1
KeptArray(k) = Largest
If m <= NumRoll Then
If Largest >= RollArray(m) And Largest >= KeptArray(k) Then
Largest = KeptArray(k)
Else
KeptArray(k) = Largest
Largest = RollArray(m)
End If
m = m + 1
End If
Cells(4 + k, 3).Value = KeptArray(k)
Next k
End Sub
I've tried so many things, like creating a dummy array, and comparing the variable Largest with it. And a ton of other things. My big problem is that I can't reuse any of the numbers.
If I roll 5 and keep 3. Say I roll [4,2,3,3,6] . I keep the [6,4,3]. Im sure this is incredibly simple and im overlooking it but its driving me absolutely insane.
Today I was watching some MonteCarlo simulations, so I have decided to do the whole question from the beginning. Thus, imagine that this is the input:
After the first roll, this is what you get:
The values in yellow are the top 3, which are kept. This is the result from the second roll:
And here is the whole code:
Public Sub RollMe()
Dim numberOfSides As Long: numberOfSides = Range("A2")
Dim timesToRoll As Long: timesToRoll = Range("B2")
Dim howManyToKeep As Long: howManyToKeep = Range("C2")
Dim cnt As Long
Dim rngCurrent As Range
Cells.Interior.Color = vbWhite
Set rngCurrent = Range(Cells(1, 6), Cells(1, 6 + timesToRoll - 1))
For cnt = 1 To timesToRoll
rngCurrent.Cells(1, cnt) = makeRandom(1, numberOfSides)
Next cnt
Dim myArr As Variant
With Application
myArr = .Transpose(.Transpose(rngCurrent))
End With
WriteTopN howManyToKeep, myArr, Cells(2, lastCol(rowToCheck:=2))
End Sub
Public Sub WriteTopN(N As Long, myArr As Variant, lastCell As Range)
Dim cnt As Long
For cnt = 1 To N
Set lastCell = lastCell.Offset(0, 1)
lastCell = WorksheetFunction.Large(myArr, cnt)
lastCell.Interior.Color = vbYellow
Next cnt
End Sub
The makeRandom and lastCol functions are some functions that I use for other projects as well:
Public Function makeRandom(down As Long, up As Long) As Long
makeRandom = CLng((up - down + 1) * Rnd + down)
If makeRandom > up Then makeRandom = up
If makeRandom < down Then makeRandom = down
End Function
Function lastCol(Optional strSheet As String, Optional rowToCheck As Long = 1) As Long
Dim shSheet As Worksheet
If strSheet = vbNullString Then
Set shSheet = ActiveSheet
Else
Set shSheet = Worksheets(strSheet)
End If
lastCol = shSheet.Cells(rowToCheck, shSheet.Columns.Count).End(xlToLeft).Column
End Function
Instead of looping through the array "manually", the WorksheetFunction.Large() nicely returns the Nth-largest value.
And if you are willing to color the "dice", which were used to take the top score, you may add this piece:
Public Sub ColorTopCells(howManyToKeep As Long, rngCurrent As Range, myArr As Variant)
Dim colorCell As Range
Dim myCell As Range
Dim cnt As Long
Dim lookForValue As Long
Dim cellFound As Boolean
For cnt = 1 To howManyToKeep
lookForValue = WorksheetFunction.Large(myArr, cnt)
cellFound = False
For Each myCell In rngCurrent
If Not cellFound And myCell = lookForValue Then
cellFound = True
myCell.Interior.Color = vbMagenta
End If
Next myCell
Next cnt
End Sub
It produces this, coloring the top cells in Magenta:
Edit: I have even wrote an article using the code above in my blog here:
vitoshacademy.com/vba-simulation-of-rolling-dices
Try this, changed a few things:
Edited the random bit too
Private Sub CommandButton1_Click()
Dim i As Long, j As Long, k As Long
Dim RandNum As Long
Dim RollArray() As Long
Dim KeptArray() As Long
Dim NumRoll As Long
Dim Kept As Long
NumRoll = Range("B2").Value
ReDim RollArray(1 To NumRoll)
Kept = Range("C2").Value
ReDim KeptArray(1 To Kept)
For i = 5 To 15
Randomize
'RandNum = 1 + Rnd() * (Range("A2").Value - 1)
RandNum = 1 + Int(Rnd() * Range("A2").Value)
Cells(i, 1).Value = RandNum
Next i
For j = 1 To NumRoll
RollArray(j) = Cells(4 + j, 1).Value
Cells(4 + j, 2).Value = RollArray(j)
Next j
For k = 1 To Kept
KeptArray(k) = Application.WorksheetFunction.Large(RollArray, k)
Cells(4 + k, 3).Value = KeptArray(k)
Next k
End Sub
Makes use of the Excel large function
Here is my attempt to fix this problem. I left the reading cell values and writing results to the OP as I am focused on the logic of the process.
There are three main functions. DiceRollSim(), RollDie() and GetNLargestIndex() as well as a function to test the code, named Test().
DiceRollSim() runs the particular simulation given the number of sides, and number of die and the number to keep. It prints the results in the output window. DollDie() fills in an array of random values simulating the rolling of the die. Caution is needed to make sure the interval probabilities are maintained as VBA does round values when converting the result of Rnd() into integers. Finally, GetNLargestIndex() is the meat of the answer, as it takes the die roll results, creates an array of index values (the 1st, 2nd, 3rd .. ) and then sorts the array based on the values of the die rolls.
Option Explicit
Public Sub Test()
DiceRollSim 6, 15, 3
' Example, 15k3:
' Rolling 15 die.
' x(1) = 5 *
' x(2) = 4
' x(3) = 4
' x(4) = 2
' x(5) = 4
' x(6) = 5 **
' x(7) = 6 ***
' x(8) = 1
' x(9) = 4
' x(10) = 3
' x(11) = 1
' x(12) = 3
' x(13) = 5
' x(14) = 3
' x(15) = 3
' Sorting die values.
' x(7) = 6
' x(6) = 5
' x(1) = 5
' Sum of 3 largest=16
End Sub
Public Sub DiceRollSim(ByVal n_sides As Long, ByVal n_dice As Long, ByVal n_keep As Long)
Dim die() As Long, i As Long
ReDim die(1 To n_dice)
Debug.Print "Rolling " & n_dice & " die."
Call RollDie(n_sides, n_dice, die)
For i = 1 To n_dice
Debug.Print "x(" & i & ")=" & die(i)
Next i
Dim largest() As Long
Debug.Print "Sorting die values."
Call GetNLargestIndex(die, n_keep, largest)
Dim x_sum As Long
x_sum = 0
For i = 1 To n_keep
x_sum = x_sum + die(largest(i))
Debug.Print "x(" & largest(i) & ")=" & die(largest(i))
Next i
Debug.Print "Sum of " & n_keep & " largest=" & x_sum
End Sub
Public Sub RollDie(ByVal n_sides As Long, ByVal n_dice As Long, ByRef result() As Long)
ReDim result(1 To n_dice)
Dim i As Long
For i = 1 To n_dice
' Rnd() resurns a number [0..1)
' So `Rnd()*n_sides` returns a floating point number zero or greater, but less then n_sides.
' The integer conversion `CLng(x)` rounds the number `x`, and thus will not keep equal
' probabilities for each side of the die.
' Use `CLng(Floor(x))` to return an integer between 0 and n_sides-1
result(i) = 1 + CLng(WorksheetFunction.Floor_Math(Rnd() * n_sides))
Next i
End Sub
Public Sub GetNLargestIndex(ByRef die() As Long, ByVal n_keep As Long, ByRef index() As Long)
Dim n_dice As Long, i As Long, j As Long, t As Long
n_dice = UBound(die, 1)
' Instead of sorting the die roll results `die`, we sort
' an array of index values, starting from 1..n
ReDim index(1 To n_dice)
For i = 1 To n_dice
index(i) = i
Next i
' Bubble sort the results and keep the top 'n' values
For i = 1 To n_dice - 1
For j = i + 1 To n_dice
' If a later value is larger than the current then
' swap positions to place the largest values early in the list
If die(index(j)) > die(index(i)) Then
'Swap index(i) and index(j)
t = index(i)
index(i) = index(j)
index(j) = t
End If
Next j
Next i
'Trim sorted index list to n_keep
ReDim Preserve index(1 To n_keep)
End Sub
I'm not a VBA developer but i need a function multile values return of a VLOOKUP.
I found this one:
Public Function CercaTableMultiplo(lookupval, lookuprange As Range, indexcol As Long)
'updateby Extendoffice 20151118
Dim x As Range
Dim result As String
result = ""
For Each x In lookuprange
If x = lookupval Then
result = result & " " & x.Offset(0, indexcol - 1)
End If
Next x
CercaTableMultiplo = result
End Function
i edit it as fallow
Public Function CercaTableMultiplo(lookupval, lookuprange As Range, indexcol As Long, delimitator)
Dim x As Range
Dim result As String
result = ""
Dim first As Boolean
first = True
For Each x In lookuprange
If x = lookupval Then
If first = True Then
result = result & x.Offset(0, indexcol - 1)
first = False
Else
result = result & delimitator & x.Offset(0, indexcol - 1)
End If
End If
Next x
CercaTableMultiplo = result
End Function
The parameters are (F217;Foglio2!E:G;3;", ")
It works when:
the first column is little the second is empty and the third is populate.
Not works when:
the first column is little the second is populate (like a description text) and the third is populate.
I suppose it is a overflow issue but my short VBA developer experience stops here,
there are some checking that can i do?
I am trying to exit the function if either the value is equal to the value I'm looking for, or if the row is equal to the row I'm looking for.
But every time I use Exit Function, it doesn't work. And if I replace it with End Function it tells me that I don't have an End to my If statement. And I'm getting lost.
Function recursion(whereItEnds As Integer, lookingFor As Variant, currentMarker As Range, I As Integer, wsEverything As Worksheet) As Integer
Dim col As Integer
Dim newMarker As String
newMarker = currentMarker.Value
Dim currentMarker1 As Range
recursion = 2
col = 2
If (StrComp(lookingFor, newMarker, vbTextCompare) = 0) Then
Exit Function
End If
While (IsEmpty(wsEverything.Cells(col, "B").Value) = False)
If (StrComp(wsEverything.Cells(col, "B").Value, newMarker, vbTextCompare) = 0) Then
wsEverything.Cells.Range("A" & col, "F" & col).Copy
Worksheets("Review").Cells.Range("A" & I).PasteSpecial
Worksheets("Review").Cells.Range("G" & I).Value = col
I = I + 1
Set currentMarker1 = wsEverything.Cells(col, "E")
If (col = whereItEnds) Then
Exit Function
End If
recursion = recursion(whereItEnds, lookingFor, currentMarker1, I, wsEverything)
End If
col = col + 1
Wend
End Function
I'm almost completely out of ideas as to why neither works.
EDIT: It hits the if statements, it goes into those codes. but when debugging, it touches "exit function" but then it just keeps going. i just want it to end the statement. This is pulling data off another long sheet and putting it on a second sheet. it is checking for child parent circular errors. where a parent in the future is dependent on the child that originally was dependent on it.
Is this what you want?
Before (Sheet1):
After (Review Sheet):
Option Explicit
Public Sub TestRecursion()
Dim result As Variant, ws As Worksheet
Set ws = Sheet1
result = Recursion(ws.Cells(2, 8), ws.Cells(2, 2), ws.Cells(2, 5), 2, ws)
End Sub
Public Function Recursion(ByVal whereItEnds As Long, lookingFor As Variant, _
ByRef currentMarker As Range, ByVal i As Long, _
ByRef wsEverything As Worksheet) As Long
Dim col As Long, newMarker As String, currentMarker1 As Range
newMarker = currentMarker.Value
Recursion = 2
col = 2
If StrComp(lookingFor, newMarker, vbTextCompare) = 0 Then Exit Function
While Len(wsEverything.Cells(col, "B").Value2) > 0
If StrComp(wsEverything.Cells(col, "B").Value2, newMarker, vbTextCompare) = 0 Then
wsEverything.Cells.Range("A" & col, "F" & col).Copy
Worksheets("Review").Cells.Range("A" & i).PasteSpecial
Worksheets("Review").Cells.Range("G" & i).Value = col
i = i + 1
Set currentMarker1 = wsEverything.Cells(col, "E")
If col = whereItEnds Then Exit Function
Recursion = Recursion(whereItEnds, lookingFor, currentMarker1, i, wsEverything)
End If
col = col + 1
Wend
End Function
If so, you can provide more context explaining the logic for the expected result
Probably a less convoluted solution can be found for this
I'm having some problem with the following code.
rng is a range that represents quite a large table. E.g. A1:G600. refArr is a list of row numbers. I need the following code to pass back a range so that just the rows from refArr can be selected out of rng.
However, when my refArr is too long (i.e. when it has over 54 items exactly) it gives me an error.... Any ideas why this would be the case?
Function RangeSelector(rng As Range, refArr As Variant) As Range
Set RangeSelector = Intersect(rng, rng.Range("A" & Replace(Join(refArr, ","), ",", ",A")).EntireRow.Offset(1))
End Function
The problem is that the maximum string length for Range is 255. We can get around that by using Union.
Notice that Range.address will only return 255 characters even if the actual address is much longer
In the test I use RangeSelector to select every other row from 1 to 600
Test
Sub TestRangeSelector()
Const MAXROWS As Long = 300
Dim refArr(1 To MAXROWS), x As Long
Dim Target As Range
For x = 1 To MAXROWS
refArr(x) = x * 2
Next
Set Target = RangeSelector(Range("A1:G600"), refArr)
Target.Select
Debug.Print "Absolute Address: "; Len(Target.Address), Target.Address
Debug.Print "Relative Address: "; Len(Target.Address(False, False)), Target.Address(False, False)
End Sub
RangeSelector
Function RangeSelector(rng As Range, refArr) As Range
Dim s As String, Target As Range, v As Variant, x As Long
For x = LBound(refArr) To UBound(refArr)
s = s & refArr(x) & ":" & refArr(x) & ","
If x = UBound(refArr) Or Len(s) >= 251 Then
s = Left(s, Len(s) - 1)
If Target Is Nothing Then
Set Target = rng.Range(s)
Else
Set Target = Union(Target, rng.Range(s))
End If
s = ""
End If
Next
Set RangeSelector = Intersect(rng, Target)
End Function
This works Lastrow = 8, but not 9 (Type mismatch)
If i remove If Not (myarray = Empty) Then it does not work for 8
What is the easiest way to solve this?
Public Function GetRowToWriteOn(ByVal SheetName As String, ByVal idnr As Integer) As Integer
LastRow = (Sheets(SheetName).UsedRange.Rows.Count) + 1
MsgBox (LastRow)
myarray = Sheets(SheetName).Range("d8:d" & LastRow).Value
If Not (myarray = Empty) Then
For row = 1 To UBound(myarray, 1)
If (myarray(row, 1) = idnr) Then
GetRowToWriteOn = row
Exit Function
End If
Next
End If
GetRowToWriteOn = LastRow
Exit Function
End Function
MyArray is taking 2 different types, depending on the range given.
If you are looking at 1 cell, then it is a single variant (which can be tested if it is Empty)
If you are looking at 2 or more cells, then it becomes an array of variant, so you would have to test each cell.
myarray = Sheets(SheetName).Range("d8:d8").Value - myarray gets the value in d8
myarray = Sheets(SheetName).Range("d8:d9").Value - myarray(1,1) gets the value in d8, and myarray(2,1) gets the value in d9
to test, use:
if vartype(myarray)=vbArray then
' run through the array
else
' do single value stuff
endif
I feel like your code should look more like this
Option Explicit
Public Function GetRowToWriteOn(ByVal SheetName As String, ByVal idnr As Integer) As Integer
Dim lastrow As Long, row As Long
lastrow = (Sheets(SheetName).UsedRange.Rows.Count) + 1
MsgBox (lastrow)
Dim myarray() As Variant
myarray = Sheets(SheetName).Range("d8:d" & lastrow).Value
If Not (IsEmpty(myarray)) Then
For row = 1 To UBound(myarray, 1)
If (myarray(row, 1) = idnr) Then
GetRowToWriteOn = row
Exit Function
End If
Next
End If
GetRowToWriteOn = lastrow
Exit Function
End Function
BUT I also think there is another way to do what you want. A little simpler and used built in functions. I think I captured your intention here:
Dim RowToWriteOn As Long, SheetName As String, lastRow As Long
Dim rng As Range
SheetName = "Sheet1"
lastRow = (Sheets(SheetName).UsedRange.Rows.Count) + 1
Set rng = Sheets(SheetName).Range("d" & lastRow)
RowToWriteOn = rng.End(xlUp).row
Public Function GetRowToWriteOn(ByVal SheetName As String, _
ByVal idnr As Integer) As Long
Dim lastRow As Long, f As Range
lastRow = Sheets(SheetName).Cells(Rows.Count, 4).End(xlUp).Row
Set f = Sheets(SheetName).Range("D8:D" & lastRow).Find(what:=idnr, _
lookat:=xlWhole)
If Not f Is Nothing Then
GetRowToWriteOn = f.Row
Else
GetRowToWriteOn = lastRow + 1
End If
End Function
myarray = Sheets(SheetName).Range("d8:d" & LastRow)
(without value)...
And you can use: if ubound(myArray) > 1 then ;..
I think it could be as easy as this, no...?