I'm using a modified match function since years which worked fine. But suddenly I get runtime error 13 for no reason. This function is called two times in the actual process before the error pops up. The first time everything works fine, the second time I get the error. Here's the code:
Public Function xMatch(ByRef Direction_Range As Range, ByVal Find_Value_Or_String, Occ_Number As Integer, Row_True_Or_Column_False As Boolean, RelativePosition_True_Or_AbsolutePosition_False As Boolean) As Integer
Dim xMTcell
toolVar1 = 0
xMatch = 0
occurrencesCount = 0
If RelativePosition_True_Or_AbsolutePosition_False = True Then
If Row_True_Or_Column_False = True Then
toolVar1 = Range(Split(Direction_Range.Address, ":")(0)).Row - 1
Else
toolVar1 = Range(Split(Direction_Range.Address, ":")(0)).Column - 1
End If
End If
For Each xMTcell In Direction_Range
If xMTcell.Value = Find_Value_Or_String Then
occurrencesCount = occurrencesCount + 1
If occurrencesCount = Occ_Number Then
If Row_True_Or_Column_False = True Then
xMatch = xMTcell.Row - toolVar1
Else
xMatch = xMTcell.Column - toolVar1
End If
Exit For
End If
End If
Next xMTcell
End Function
toolVar1 and occurrencesCount are declared in the module. The function can search in any ranges (Direction_Range) and find the Value (Find_Value_Or_String). In contrary to a regular match function you can decide (Occ_Number) which find will be the one you need if there are multiple ones in that range. Also, you can decide if you want the row or the column of that find and if you want that row/column absolute (position compared to worksheet) or relative (position compared to Direction_Range).
The error occurs in this line:
If xMTcell.Value = Find_Value_Or_String Then
Since xMTcell is part of Direction_Range I checked the Range and it is clearly the right one plus it IS a range and nothing else. I also checked the Value and it is a String he is looking for which can be found manually in that range. I can't understand why it works fine in other stages of the process with the exact same type of Direction_Range and Find_Value_Or_String and suddenly it doesn't. I already tried declaring xMTcell as Range but it made no difference.
Anyone an idea?
Regards
Carl
According to the comments i did following checks:
Debug.Print VarType(Direction_Range)
Debug.Print Direction_Range.Address
Debug.Print VarType(xMTcell.Value)
Debug.Print VarType(Find_Value_Or_String)
Debug.Print xMTcell.Address
and for the non-bugging process i get
8204
$A$4:$N$4
8
8
$B$4
and for the bugging process
8204
$A$5:$A$16
8204
8
$A$5:$A$16
so its a problem with the range, why does it behave differently?
TL;DR:
Change For Each xMTcell In Direction_Range to For Each xMTcell In Direction_Range.Cells.
Some debugging and general tips (summarizing the comments and your feedback in them):
Dim xMTcell - make that a Range.
Use Debug.Print (Control+G to bring up the Immediate Window and inspect the output).
Debug.Print VarType(xMTcell.Value) returns 8204: Per the VarType docs, that means that xMTcell.Value is a vbArray of vbVariants (8192 + 12 = 8204).
Debug.Print VarType(Find_Value_or_String) returns 8: Again per the VarType docs, this means Find_Value_or_String is a String.
The type mismatch is because you can't compare a String to an array.
The fact that xMTCell.Value is an array points to xMTCell being a multi-cell range, not a single cell.
... which is verified by the output of Debug.Print xMTcell.Address being a multi-cell range.
Most likely the issue is that you passed a Row or Column as your Direction_Range, i.e. you used Rows or Columns to return a range. When looping over a Row or Column, you need to specify that you're looping over the individual cells.
Related
This code used to work, but recently I am getting an error (out of stack space).
I think the code is failing because I am calling a function too many times without exiting/ending.
If that is the case, how many times can you call a function and is there something I can do to fix this?
I am not the original author of this code.
I included the sub where the error occurs.
Sub CalculatePct(e As Variant)
Dim G As Integer
Dim pct As Double
Dim Owned100Pct As Boolean
If entities(e) < 0 Then
pct = 0
Owned100Pct = True ' Keeps track if the entity exists in the table other than as a parent
For G = 1 To UBound(MainArray, 1)
If MainArray(G, colEntity) = e Then
Owned100Pct = False
If entities(MainArray(G, colParent)) = -1 Then
'If we don't know the parent's ownership percentage, go and calculate it
CalculatePct MainArray(G, colParent)
End If
pct = pct + CDbl(MainArray(G, colPct)) / 100 * entities(MainArray(G, colParent))
End If
Next
If Owned100Pct Then
'Assume 100% owned if we don't know the parentage
'("Outside" entities won't go through here as they are already set to 0%)
entities(e) = 1
Else
'Store the entity's percentage
entities(e) = pct
End If
End If
End Sub
A #TimWilliams noted in the comments - you have an endless recursion loop.
Highlighting the problem area:
Sub CalculatePct(e As Variant)
[...]
If entities(MainArray(G, colParent)) = -1 Then
CalculatePct MainArray(G, colParent)
End If
[...]
End Sub
e is the parameter, and entities(e) is checked. In the code, MainParent(G, colParent) is used in place of e, so the next call to the routine gives e = MainParent(G, colParent).
Up to the point in the code, you do not change the value of G, colParent. entities or MainArray. So if entities(MainArray(G, colParent)) = -1 it will be forever calling itself.
Without knowing anything else about the code (including if recursion is necessary) I cannot suggest any definitive solutions. However, some things to consider:
Rewriting to be a loop instead of a recursive call
Making the recursive call to a subset of MainArray
Doing any amendments to G or colParent prior to the recursive
call
You've offered no indication of what line the error occurs on nor what MainArray represents but I'm guessing that MainArray has grown to a size greater than what can be accessed with a signed short integer.
Change the declaration of your iteration variable to a Signed Long Integer. This raises the functional limit of the variable from 32,767 iterations to 2,147,483,647.
Dim G As Long
I have an issue where I am trying to compare a values that can be alphanumeric, only numeric, or only alphabetic.
The code originally worked fine for comparing anything within the same 100s group (IE 1-99 with alphabetic components). However when I included 100+ into it, it malfunctioned.
The current part of the code reads:
For j = 1 To thislength
If lennew < j Then
enteredval = Left("100A", lennew)
ElseIf lennew >= j Then
enteredval = Left("100A", j)
End If
If lenold < j Then
cellval = Left("67", lenold)
ElseIf lenold >= j Then
cellval = Left("67", j)
End If
'issue occurs here
If enteredval >= cellval Then
newrow = newrow+1
End If
Next j
The issue occurs in the last if statement.
When cycling through the 100 is greater than the 67 but still skips over. I tried to declare them both as strings (above this part of code) to see if that would help but it didn't.
What I am trying to accomplish is to sort through a bunch of rows and find where it should go. IE the 100A should go between 100 and 100B.
Sorry lennew=len("100A") and lennold=len("67"). And thislength=4or whatever is larger of the two lengths.
The problem is that you're trying to solve the comparison problem by attacking specific values, and that's going to be a problem to maintain. I'd make the problem more generic by creating a function that supplies takes two values returns -1 if the first operand is "before" the second, 0 if they are the same, and 1 if the first operand is "after" the second per your rules.
You could then restructure your code to eliminate the specific hardcoded prefix testing and then just call the comparison function directly, eg (and this is COMPLETELY untested, off-the-cuff, and my VBA is VERRRRRY stale :) but the idea is there: (it also assumes the existence of a simple string function called StripPrefix that just takes a string and strips off any leading digits, which I suspect you can spin up fairly readily yourself)
Function CompareCell(Cell1 as String, Cell2 as String) as Integer
Dim result as integer
Dim suffix1 as string
Dim suffix2 as string
if val(cell1)< val(cell2) Then
result = -1
else if val(cell1)>val(cell2) then
result = 1
else if val(cell1)=val(cell2) then
if len(cell1)=len(cell2) then
result =0
else
' write code to strip leading numeric prefixes
' You must supply StripPrefix, but it's pretty simple
' I just omitted it here for clarity
suffix1=StripPrefix(cell1) ' eg returns "ABC" for "1000ABC"
suffix2=StripPrefix(cell2)
if suffix1 < suffix2 then
result = -1
else if suffix1 > suffix2 then
result = 1
else
result = 0
end if
end if
return result
end function
A function like this then allows you to take any two cell references and compare them directly to make whatever decision you need:
if CompareCell(enteredval,newval)>=0 then
newrow=newrow+1
end if
I have a function that takes optional arguments in pairs: firstRange_1, secondRange_2; firstRange_2, secondRange_2; etc.
For each optional argument I need to execute a series of statements if the argument is passed to the function.
For example
dim firstRange_1 as range
dim secondRange_1 as range
dim firstRange_2 as range
dim secondRange_2 as range
etc.
dim firstCell_1 as string
dim lastCell_1 as string
dim firstCell_2 as string
dim lastCell_2 as string
etc.
If IsMissing(firstRange_1) = False Then
firstCell_1 = secondRange_1.Cells(1,1).Address
lastCell_1 = secondRange_1.Cells(secondRange_1.Rows.Count, secondRange_1.Rows.Count)
End if
if IsMissing(firstRange_2) = False Then
firstCell_2 = secondRange_2.Cells(1,1).Address
lastCell_2 = secondRange_2.Cells(secondRange_2.Rows.Count, secondRange_2.Rows.Count)
End If
Is it possible to "build" (sorry if the terminology isn't correct, I'm not yet experienced in programming or vba) the variables on the fly?
for example a loop like
For n=1 to 100
If IsMissing(firstRange_ & "n") = False Then
firstCell_ & "n" = secondRange_ & "n".Cells(1,1).Address
lastCell_ & "n" = secondRange_ & "n".Cells(secondRange_ & "n".Rows.Count, secondRange_ & "n".Rows.Count)
End If
Next
Edit:
See my comments to Branislav Kollár for updates.
I think what you need to rewrite the function to use ParamArrays (see the "Using an Indefinite Number of Arguments" section). Something like this:
myFunction(ParamArray userRanges()) As Range'or whatever Data Types you need
This way, you could use the LBound and UBound functions to see how many arguments were passed into function, leaving the necessity to check if they are missing.
For example you can create a 2 new arrays inside the function (not the argument array) for determining the first and last cells of each argument range. This is not the only way, you can use 2D arrays or put everything into one array. This is just one way.
Function myFunction(ParamArray userRanges()) As Range
Dim firstCell() As Range
Dim lastCell() As Range
ReDim firstCell(UBound(userRanges))
ReDim lastCell(UBound(userRanges))
For x = 0 To UBound(userRanges)
Set firstCell(x) = userRanges(x).Range("A1")
Set lastCell(x) = firstCell_1(x).Offset(userRanges(x).Rows.Count - 1, userRanges(x).Columns.Count - 1)
Next x
'other code to actually do something with the cells
'...
End Function
Try this, if you have any trouble, please let us know.
One more link to learn about this Understanding the ParamArray
Edit 1
Based on comment from OP, I rewritten the code, so that now each input range userRanges will have firstCell and lastCell stored in appropriate arrays. I didn't realize the limitation of my previous post before.
The only think to keed in mind now, is that the index 0 is first range; 1 is second range; 2 is third range; etc.
Or you can use Option Base 1 to make it more naturally indexed, but that is not recommended for some reason.
You can't dynamically name variables, but you can use Arrays. They are stupid powerful, so it's worth learning about them.
Essentially you will make 2 arrays. One for your input (variable1_n) and one of your outputs (output_1_n).
Dim inputArray(1 to 100) as String 'or whatever type these are supposed to be
Dim outputArray(1 to 100) as Range 'perhaps these are ranges?
For i = 1 to 100
Set outputArray(i) = function(inputArray(i))
Next i
Now you have an array full of ranges!
Below is the code i have put together from various examples to try achieve my goal. Concept is to be dynamic and retrieve from survey sheet within my workbook, to be able to obtain the corresponding TVD for the MD
--Use while loop only to run code if there is a depth in Column B Present. Nested loop uses the difference between depths to calculate a gradient.
---The issue i'm having is getting past my first debug error "Invalid Qualifier".
----Lastly, any suggestions for how i would then return the TVD to Column A, relevant to the looked up MD, within the nested loop to maintain the row in which the MD was grabbed. Sorry for making this so wordy, been working on this for over 10hrs while at work.
http://www.wellog.com/tvd.htm
Sub MdtoTVD()
Dim MD1 As String, MD2 As Integer
Dim TVD1 As String, TVD2 As Integer
Dim Srng As Range 'Survey MD column
Dim MDrng As Range 'MdtoTVD MD column as range
Dim MDdiff As Integer ' Var to calculate difference of MD end from MD start
Dim TVDdiff As Integer ' Var to calculate difference of TVD end from TVD start
Dim TVDincr As Double ' var to use for stepping TVD
Dim MDrow As Integer
Dim i As Long
MDrng = Range("Surveys!B27:B215") 'range from the survey sheet within my report book
Srng = Range("Surveys!G27:G215") 'range from the survey sheet within my report book
Dim X As Integer
X = 2
While Not (IsEmpty(Sheets("MDtoTVD").Cells(X, 2).Value)) 'runs loop as long as there a MD depth to be looked up
Cells(X, 2) = MDrow 'assigns current row value to for MD input
MD1.Value = Application.WorksheetFunction.Index(Srng, Application.WorksheetFunction.Match(MDrow, MDrng, 1)) ' retrieves Start point for MD
MD2.Value = Application.WorksheetFunction.Index(Srng, Application.WorksheetFunction.Match(MDrow, MDrng, 1) + 1) 'retrieves end point for MD
TVD1.Value = Application.WorksheetFunction.Index(MDrng, Application.WorksheetFunction.Match(MDrow, Srng, 1)) 'retrieves start point for TVD
TVD2.Value = Application.WorksheetFunction.Index(MDrng, Application.WorksheetFunction.Match(MDrow, Srng, 1) + 1) 'retrieves end point for TVD
MDdiff.Value = (MD2 - MD1) 'assigns and calculates difference of MD end from MD start
TVDdiff.Value = (TVD2 - TD1) 'assigns and calculates difference of TVD end from TVD start
TVDincr.Value = MDdiff / TVDdiff 'Divides MD by TVD to get increment per foot
For i = 1 To MDdiff Step TVDincr 'set max loop run to amount of feet between survey points
Cells(X, 1).Value = TVD1 + i 'uses the loop to increment the TVD from start point
Next i
Wend
End Sub
I can see a number of problems with your code:
MD1, MD2, TVD1, TVD2 are all of type String. Also, MDdiff, TVDdiff and TVDIncr are all of type Integer. The property Value is not defined for a string or integer variable. Just remove the .Value from all of them and you won't get the "Invalid Qualifier" error.
After you do the above, the following lines will give another error about type mismatch:
MDdiff = (MD2 - MD1)
TVDdiff = (TVD2 - TD1)
because you're trying to subtract a string from another string and assign the result to an integer. Not sure what to advise there, you have to consider what you're trying to achieve and act accordingly. Maybe they shouldn't be strings in the first place? I don't know, up to you to determine that.
At the very least, you can cast strings to integers if you're really sure they're string representations of integers by doing CInt(string_var) or use CLng to convert to long. If the strings are not string representations of integers and you try to cast them to integers, you'll get a type mismatch error.
When you assign a value to a Range object, you need to use Set. So do:
Set MDrng = Range("Surveys!B27:B215")
Set Srng = Range("Surveys!G27:G215")
to correctly set the ranges.
Another problem is that you haven't assign a value to X but you use it as a cell index. By default, uninitialised numeric variables in VBA get assigned the value of 0, so doing .Cells(X, 2) will fail because row 0 is not a valid row index.
In this line:
TVDincr = MDdiff / TVDdiff
you're dividing two integers and you assign the result to another integer. Note that if the result of the division happens to be a decimal (like 3 / 2 = 1.5), your TVDincr integer will actually contain just 1, i.e. you lose some precision. I don't understand your code to know if it's ok or not, you have to judge for yourself, I'm pointing it out just in case you're not aware of that.
Also, if TVDdiff happens to be 0, then you'll get a "division by zero" error.
This line in your For loop:
Cells(X, 1).Value = TVD1 + i
will also generate an error, because you're trying to numerically add TVD1 (a string) and i (a long). Perhaps you're trying to concatenate the two, in which case you should replace + with &.
There's also a problem when calling the WorksheetFunctions, but I haven't been able to determine the cause. Probably if you fix the other errors then it'll be easier to understand what's going on, not sure though. You just have to investigate things a little bit too.
I'm analyzing a code from the website and I tried it on my side as well but seems it doesn't work. Could you please tell me why? would greatly appreciate your help.
Thanks
Private Sub CommandButton1_Click()
Dim N, D As Single
Dim tag As String
N = Cells(2, 2)
Select Case N
Case Is < 2
MsgBox "It is not a prime number"
Case Is = 2
MsgBox "It is a prime number"
Case Is > 2
D = 2
Do
If N / D = Int(N / D) Then
MsgBox "It is not a prime number"
tag = "Not Prime"
Exit Do
End If
D = D + 1
Loop While D <= N - 1
If tag <> "Not Prime" Then
MsgBox "It is a prime number"
End If
End Select
End Sub
The single biggest problem I see is using Single instead of Integer or Long. Primes are positive integers and are not thought of in the context of decimal values (as far as I know). Thus by using a singles and comparing them to divided ints, you're opening yourself up to nasty edge-case rouding errors.
The line If N / D = Int(N / D) Then is using a poor method to see whether or not the numbers are prime. It's assuming that every time you divide a floating point number (in this case, the single) by the divisor, if it has a decimal remainder, then the integer conversion of that remainder will not be equal. However, I've run into rounding errors sometimes with floating point numbers when trying to compare answers, and in general, I've learned to avoid using floating point to int conversions as a way of comparing numbers.
Here's some code you might try instead. Some things to note:
I've changed the types of N and D so that they are Longs and not Singles. This means they are not floating point and subject to possible rounding errors.
I've also explicitly converted the cell value to a long. This way you know in your code that you are not working with a floating point type.
For the comparison, I've used Mod, which returns the remainder of the N divided by D. If the remainder is 0, it returns true and we know we don't have a prime. (Note: Remainder is often used with \, which only returns the integer value of the result of the division. Mod and \ are commonly used in precise division of integer types, which in this case is very appropriate.
Lastly, I changed your message box to show the actual number being compared. Since the number in the cell is converted, if the user enters a floating point value, it will be good for them to see what it was converted to.
You'll probably also note that this code runs a lot faster than your code when you get to high numbers in the hundreds of millions.
'
Sub GetPrime()
Dim N As Long
Dim D As Long
Dim tag As String
N = CLng(Cells(2, 2))
Select Case N
Case Is < 2
MsgBox N & " is not a prime number"
Case Is = 2
MsgBox N & " is a prime number"
Case Is > 2
D = 2
Do
If N Mod D = 0 Then
MsgBox N & " is not a prime number"
tag = "Not Prime"
Exit Do
End If
D = D + 1
Loop While D <= N - 1
If tag <> "Not Prime" Then
MsgBox N & " is a prime number"
End If
End Select
End Sub
NOTE: I changed the name of the procedure to be GetPrime. In your code, you had:
Private Sub CommandButton1_Click()
In the line above, you are defining a procedure (also called a method or sometimes just referred to as a sub). The word Sub indicates you are defining a procedure in code that returns no value. (Sometimes you might see the word Function instead of Sub. This means the procedure returns a value, such as Private Function ReturnANumber() As Long.) A procedure (Sub) is a body of code that will execute when called. Also worth noting, an excel macro is stored in VBA as a Sub procedure.
In your line of code, CommandButton1_Click() is the name of the procedure. Most likely, this was created automatically by adding a button to an Excel spreadsheet. If the button is tied to the Excel spreadsheet, CommandButton1_Click() will execute each time the button is pressed.
In your code, Private indicates the scope of the procedure. Private generally means that the procedure cannot be called outside of the module or class in which it resides. In my code, I left out Private because you may want to call GetPrime from a different module of code.
You mentioned in your comments that you had to change the name of my procedure from GetPrime() to CommandButton1_Click(). That certainly works. However, you could also have simply called GetPrime from within CommandButton1_Click(), like below:
Private Sub CommandButton1_Click()
'The following line of code will execute GetPrime() '
'Since GetPrime does not have parameters and does not return a value, '
'all you need to do is put the name of the procedure without the () '
GetPrime
End Sub
'Below is the entire code for the Sub GetPrime() '
Sub GetPrime()
'The body of the code goes below: '
' ... '
End Sub
Hopefully this helped to explain a little bit about VBA to further your understanding!
I'm not sure where you copied this code from, but it's terribly inefficient. If I may:
Dim N, D As Long will cause D to be a Long, and N to be a variant. As you may know, variants are one of the slowest data types available. This line should be: Dim N As Long, D As Long
You only need to test every other number as an even number will always be divisible by two. (Therefore can not possibly be prime).
You don't need to test all the way up to N. You only need to test up to the Square Root of N. This is because after the square root the factors just switch sides, so you are just retesting values.
For Loops only evaluate the For-Line once for the life of the loop, but Do and While loops evaluate their conditional on every loop, so N-1 is being evaluated many, many times. Store this value in a variable if you want to use a Do Loop.
Ok, so now that we have dispensed with the blah, blah, blah, here is the code. I structured it so you can use it as a UDF from Excel as well (Ex: =ISPRIME(A2)):
Option Explicit
Sub GetPrime()
Dim varValue As Variant
varValue = Excel.ActiveSheet.Cells(2&, 2&).Value
If IsNumeric(varValue) Then
If CLng(varValue) = varValue Then
If IsPrime(varValue) Then
MsgBox varValue & " is prime", vbInformation, "Prime Test"
Else
MsgBox varValue & " is not prime", vbExclamation, "Prime Test"
End If
Exit Sub
End If
End If
MsgBox "This operation may only be performed on an integer value.", vbCritical, "Tip"
End Sub
Public Function IsPrime(ByVal num As Long) As Boolean
Dim lngNumDiv As Long
Dim lngNumSqr As Long
Dim blnRtnVal As Boolean
''//If structure is to optimize logical evaluation as AND/OR operators do not
''//use short-circuit evaluation in VB.'
If num = 2& Then
blnRtnVal = True
ElseIf num < 2& Then 'Do nothing, false by default.
ElseIf num Mod 2& = 0& Then 'Do nothing, false by default.
Else
lngNumSqr = Sqr(num)
For lngNumDiv = 3& To lngNumSqr Step 2&
If num Mod lngNumDiv = 0& Then Exit For
Next
blnRtnVal = lngNumDiv > lngNumSqr
End If
IsPrime = blnRtnVal
End Function
You can optimise it further (and make it more readable, in my opinion) by making the following changes. First performance:
Use longs, not floats. This will result in a huge speed increase.
You don't need to check up to n-1, only the square root of n. That's because if a factor d greater than sqrt(n) exists, its counterpart n/d would have already been found under sqrt(n). We use a special variable for this so that we don't get overflow by calculating divisor2. It also speeds it up by calculating that once rather than calculating the square every time through the loop (even though getting the square root is undoubtedly slower than squaring, it only happens once).
Do a special check first for multiples of two then you need only check that your number is a multiple of an odd number, effectively doubling the speed (not checking if you're a factor of a multiple of two).
Use the modulo operator rather than division/multiplication.
Now readability:
Use descriptive variable names.
Use a boolean for boolean values (not a string like tag).
Move the message box logic down to the bottom, based on the isPrime boolean, rather than scattering the messages amongst your code.
With all those changes, the following code can detect a 9-digit prime number (795,028,841) in well under a second. In fact, we can detect the largest 31-bit prime (2,147,483,647) in the same time.
Based on benchmarks (putting a 10,000-iteration for loop around the select), it takes 35 seconds on my box to detect that 31-bit prime. That's about 285 times per second - hopefully that'll be fast enough for you :-)
Option Explicit
Public Sub Go()
Dim number As Long
Dim divisor As Long
Dim maxdivisor As Long
Dim isPrime As Boolean
number = CLng(Cells(2, 2))
Select Case number
Case Is < 2
isPrime = False
Case Is = 2
isPrime = True
Case Is > 2
isPrime = True
If number mod 2 = 0 Then
isPrime = False
Else
maxdivisor = CLng(Sqr(number)) + 1
divisor = 3
Do
If number mod divisor = 0 Then
isPrime = False
Exit Do
End If
divisor = divisor + 2
Loop While divisor <= maxdivisor
End If
End Select
If isPrime Then
MsgBox "Number (" & number & ") is prime"
Else
MsgBox "Number (" & number & ") is not prime"
End If
End Sub