Finding the n-th cell in a column with a given property - excel

I don't have much experience but I'm trying to write a function that will search column A and the 1st time it finds a string beginning with "AT" it will copy that whole string to Cell N1, the 2nd string beginning with "AT" will be copied to N2, so on and so forth until column A is exhausted. This is my feeble attempt so far but I'm not having much luck.
Function Find_AT(ByVal I As Integer)
Dim c As Range
Dim COUNTER As Integer
Dim CAPTURE As Long
COUNTER = 0
For Each c In Range("A1", Range("A65636").End(xlUp))
If Left(c, 2) = AT Then
COUNTER = COUNTER + 1
If COUNTER = I Then
CAPTURE = c
Exit For
End If
End If
Next c
Find_AT = CAPTURE
End Function

Consider:
Function Find_AT(ByVal I As Long) As String
Dim c As Range
Dim COUNTER As Long
Dim CAPTURE As String
Dim v As String
COUNTER = 0
CAPTURE = "xx"
For Each c In Range("A1", Range("A65636").End(xlUp))
v = c.Text & " "
If Left(v, 2) = "AT" Then
COUNTER = COUNTER + 1
If COUNTER = I Then
CAPTURE = c.Address
Exit For
End If
End If
Next c
Find_AT = CAPTURE
End Function

The error with your code is that the text (the string) AT needs to be enclosed in double-quotes "AT". Add Option Explicit to the top of the Module and it would take you to this error when you try to compile or execute the function.
However, given your description, I suspect that you might want to write a sub-procedure (SUB) not a Function. A function is intended to return a value. If you want to use a function you might define it like this:
Function Find_AT(rng As Range, ByVal i As Integer)
That is, you would supply it a Range to search and the number 1 to find the first value in the range that begins with "AT". However, if you put this function in a cell and copy it down, it will still return only the first occurrence. You would need to manually change 1 to 2, 3, etc. (or use a variation of ROW() to automatically generate this sequence).
Anyway, I suspect you really want a SUB-procedure that you might run by clicking a button on the worksheet.
If you wish to continue with your current function, then you could declare the return type as a string:
Function Find_AT(ByVal i As Integer) As String
'...
Dim CAPTURE As String
'...
CAPTURE = c.Text
Otherwise, setting CAPTURE = c and attempting to return this value causes a problem because c is a Range object.

Filtering is much more efficient. Two approaches below:
Filter
Sub GetAT1()
X = Filter(Application.Transpose(Range([a1], Cells(Rows.Count, "A").End(xlUp))), "AT", True)
If UBound(X) > 0 Then [n1].Resize(UBound(X) + 1) = Application.Transpose(X)
End Sub
AutoFilter
Sub GetAT()
Dim rng1 As Range
Set rng1 = Range([a1], Cells(Rows.Count, "A").End(xlUp))
Application.ScreenUpdating = False
ActiveSheet.AutoFilterMode = False
rng1.AutoFilter 1, "=AT*"
rng1.Copy [n1]
If LCase$(Left$([n1], 2)) <> "at" Then [n1].Delete xlUp
ActiveSheet.AutoFilterMode = False
Application.ScreenUpdating = True
End Sub

Related

Excel taking really long to calculate a UDF VBA

example2 example1 The file name I'm trying to match is on Row A and I'm looking through Row I to see if there is a match I found this code I can't recall where but I am trying to match row of part numbers to a row of its image file names. This code works, however, there is a problem when I run it it takes really long to calculate even just 1 column and when I do hundreds at a time my excel just stops responding, and I have thousands of products I need to match. I am really new with VBA so I can't even figure out the problem.
Please help, thank you.
'Name function and arguments
Function SearchChars(lookup_value As String, tbl_array As Range) As String
'Declare variables and types
Dim i As Integer, str As String, Value As String
Dim a As Integer, b As Integer, cell As Variant
'Iterste through each cell
For Each cell In tbl_array
'Save cell value to variable
str = cell
'Iterate through characters
For i = 1 To Len(lookup_value)
'Same character?
If InStr(cell, Mid(lookup_value, i, 1)) > 0 Then
'Add 1 to number in array
a = a + 1
'Remove evaluated character from cell and contine with remaning characters
cell = Mid(cell, 1, InStr(cell, Mid(lookup_value, i, 1)) - 1) & Mid(cell, InStr(cell, Mid(lookup_value, i, 1)) + 1, 9999)
End If
'Next character
Next i
a = a - Len(cell)
'Save value if there are more matching characters than before
If a > b Then
b = a
Value = str
End If
a = 0
Next cell
'Return value with the most matching characters
SearchChars = Value
End Function
EDIT (post seeing the data): The following should be notably faster (as well as notably simpler)
'Name function and arguments
Function SearchChars(lookup_value As String, tbl_array As Range) As String
'Declare variables and types
Dim inLenMatched%, vnVal, varLookupValues()
'Puts lookup cell values into a array (to speed things up)
varLookupValues = tbl_array.Value
'Iterate through each lookup value
For Each vnVal In varLookupValues
'Ignore empty cells
If vnVal <> "" Then
'Does part number appear in filename?
If InStr(lookup_value, vnVal) > 0 Then
'Is this match the most complete match so far?
If Len(vnVal) > inLenMatched Then
inLenMatched = Len(vnVal)
SearchChars = vnVal
End If
End If
End If
Next vnVal
'Return match value (or 'No Match' if not matched)
If SearchChars = "" Then SearchChars = "No Match"
End Function
The above is just one off-the-cuff approach.
There are other (and quite possible faster) ways to approach this.
The most obvious step (regardless of method) to improving performance would be to limit tbl_array to only the rows with data (not the entire column).
Separately: Without knowing all possible cases, it's impossible to say for sure. But, in all probability, this can be done with Native excel functions, and (if so) that will deliver the best performance.
As said, minimizing the interactions with the sheet by assigning the range to an array will structurally make your macros faster.
Not tested but these minor changes in your code should help you on the right track:
Option Explicit
'Name function and arguments
Function SearchChars2(lookup_value As String, tbl_array As Range) As String
'Declare variables and types
Dim i As Integer, str As String, Value As String
Dim a As Integer, b As Integer, cell As Variant
'Iterste through each cell => replace with array
'adapt to correct sheet
Dim arr
arr = tbl_array
For Each cell In arr 'tbl_array
'Save cell value to variable
str = cell
'Iterate through characters
For i = 1 To Len(lookup_value)
'Same character?
If InStr(cell, Mid(lookup_value, i, 1)) > 0 Then
'Add 1 to number in array
a = a + 1
'Remove evaluated character from cell and contine with remaning characters
cell = Mid(cell, 1, InStr(cell, Mid(lookup_value, i, 1)) - 1) & Mid(cell, InStr(cell, Mid(lookup_value, i, 1)) + 1, 9999)
End If
'Next character
Next i
a = a - Len(cell)
'Save value if there are more matching characters than before
If a > b Then
b = a
Value = str
End If
a = 0
Next cell
'Return value with the most matching characters
SearchChars2 = Value
End Function
I was trying to modify your existing code, but I found it easier to just rewrite it using what I consider to be a better structure. And After running the code over 26 columns & 432 rows, It only took 0.2 seconds to find the Closest Matching String.
I moved every value into an array.
I converted the lookup_value and the "cell values" into an array of bytes.
I compared the byte arrays to count matching "characters".
And then I return the string that had the highest number of matching "characters".
Sub Example()
Dim StartTime As Double
StartTime = Timer * 1000
Debug.Print SearchChars3("Toddleson", Range("A1:Z432"))
Debug.Print "Time Elapsed: " & Timer * 1000 - StartTime & " ms"
'Time Elapsed: 171.875 ms
End Sub
Function SearchChars3(lookup_value As String, tbl_array As Range) As String
Dim ClosestMatch As String, HighestMatchCount As Integer
Dim tbl_values() As Variant
tbl_values = tbl_array.Value
Dim LkUpVal_Bytes() As Byte
LkUpVal_Bytes = ToBytes(lookup_value)
Dim Val As Variant
For Each Val In tbl_values
If Val = "" Then GoTo nextVal
Dim Val_Bytes() As Byte
Val_Bytes = ToBytes(CStr(Val))
Dim MatchCount As Integer
MatchCount = CountMatchingElements(LkUpVal_Bytes, Val_Bytes)
If MatchCount > HighestMatchCount Then
HighestMatchCount = MatchCount
ClosestMatch = Val
End If
nextVal:
Next
SearchChars3 = ClosestMatch
End Function
Function ToBytes(InputStr As String) As Byte()
Dim ByteArr() As Byte
ReDim ByteArr(Len(InputStr) - 1)
Dim i As Long
For i = 0 To Len(InputStr) - 1
ByteArr(i) = AscW(Mid(InputStr, i + 1, 1))
Next
ToBytes = ByteArr
End Function
Function CountMatchingElements(Arr1 As Variant, Arr2 As Variant) As Integer
'As elements from Arr1 are found in Arr2, those elements are removed from Arr2, to prevent re-matching with the same elements
'To enable this feature, Arr2 is turned into a Collection
Dim Col2 As New Collection
Dim v As Variant
For Each v In Arr2
Col2.Add v
Next
Dim MatchCount As Integer, i As Long
For Each v In Arr1
For i = 1 To Col2.Count
If Col2.Item(i) = v Then
MatchCount = MatchCount + 1
Col2.Remove (i)
Exit For
End If
Next
Next
CountMatchingElements = MatchCount
End Function
A further optimization could be to have a second version of the ToBytes function that directly outputs the values into a Collection. Then, you can change CountMatchingElements to accept a collection and it wont need to convert the second array into a collection.
I will leave that as an idea for you to experiment with.

Add cell address to dynamic array in VBA

I have a script wherein I loop through a 9x9 array and if a cell contains a 0, it will change the number such that the number is unique across the row, column and a 3x3 square within. Every time one such cell is found and changed, I want to add that cell location to an array so that if it comes to be that the number that replaced the 0 is not optimal, I can easily go back to that cell that was changed and try a new number. How do I do this?
Below is the code I have written so far and I have denoted my "pseudo-code" with three apostrophes (''')
that further explains what I want it to do.
The Check Function determines whether a number from 1 to 9 can be placed in the current cell based on the conditions I mentioned (Sudoku Rules).
It deals with recursion so let me know if I need to explain in a more clear manner.
Sub Solve()
Dim x As Integer, y As Integer, row As Integer, col As Integer, rw As Integer, cl As Integer, a As Worksheet, puzzle As Range, n As Integer, num As Integer
Dim startcol As Integer, startrow As Integer, check1 As Boolean, check2 As Boolean, check3 As Boolean, r As Integer, c As Integer, x1 As Double, y1 As Double, z As Boolean
Dim fillednums(1 To 9, 1 To 9) As String
Set a = ThisWorkbook.Worksheets("Puzzle")
Set puzzle = a.Range(Cells(4, 4), Cells(12, 12))
startcol = 4
startrow = 4
For row = startrow To startrow + 8
For col = startcol To startcol + 8
If a.Cells(row, col).Value = 0 Then
For num = 1 To 9
If Check(col, row, num) = True Then
a.Cells(row, col).Value = num
'''Add cell address to array
Call Solve
ElseIf num = 9 And a.Cells(row, col).Value = 0 Then
'''Go back one index of the array (fillednums) and use check() function for numbers greater than the one in the cell and up to 9
'''If that still doesnt work, go back to cell before this one that was changed and check again (recursively)
'''Call Solve() again to try new number
'a.Cells(row, col).Value = 0
End If
Next num
End If
Next col
Next row
End Sub
For the recursion, you can start with the first empty cell in the puzzle. For each possible value, pass the next free cell to the child to check for a solution. The process continues until a solution is found (assuming valid puzzle).
The main Solve function must return True or False so the parent knows if a solution has been found.
Function GetNextCell(cc) ' get next free cell in puzzle
GetNextCell = Cells(cc.Row, cc.Column+1) ' move next column
If (GetNextCell.Column = 13) Then ' go to next row
GetNextCell = Cells(cc.Row+1, 4)
End If
If GetNextCell.Row = 13 Then ' off the grid
GetNextCell = Nothing ' no more cells
End If
If GetNextCell <> Nothing And GetNextCell.Value <> "" Then
GetNextCell GetNextCell(GetNextCell) ' skip filled cells
End If
Function Solve(cc) as Boolean
' we only care about our single cell
For num = 1 to 9 ' all possible values for this cell
cc.Value = num
If Check(cc.column, cc.row, num) Then ' so far so good
NextCell = GetNextCell(cc) ' get next cell for child to process
if NextCell = Nothing Then ' no more cells and current values work
Solve = True ' puzzle solved
Exit Function
Else ' call child with next cell
If Solve(NextCell) Then ' did child solve puzzle ?
Solve = True ' puzzle solved
Exit Function
End If
' Child could not find solution based on current values
End If
End If
Next
cc.Value = "" ' No solution found at this point, must revert back to parent to try next value
Solve = False ' no solution found
End Function
Solve(GetNextCell(Cells(4,3))) ' first empty cell in block, must return true

How can I create a loop with variable criteria/condition?

I have a function that loops through X sheets to count how many meet a certain criteria, but I want this criteria to be variable.
This is my code for the function:
Function f_1(Condition As Variant, Condition_TrueFalse As Boolean, i_Start As Long, i_End As Long) As Long
f_1 = 0
For i = i_Start To i_End
Select Case Condition
Case Condition_TrueFalse
f_1 = Application.Sum(f_1, 1)
End Select
Next i
End Function
I have a workbook with 20 worksheets, with the names "WorksheetNumber1", "WorksheetNumber2", "WorksheetNumber3", "WorksheetNumber4", ..., "WorksheetNumber20".
So, for example, I might have this subroutine:
Sub(Workbook1 As Workbook)
Dim count As Long
'example A
count = f_1(IsEmpty(Workbook1.Worksheets("WorksheetNumber" & i).Cell(1,1)),False,1,10)
MsgBox(count)
'example B
count = f_1(Application.CountA(Workbook1.Worksheets("WorksheetNumber" & i).Range("$C$3:$E$5"))>0,True,1,5)
MsgBox(count)
End Sub
I have multiple of these criteria, so I really don't want to repeat the code; since the structure of the code is similar, I thought I might be able to make the criteria variable.
What I want is:
For Example A, if cell(1,1) is not empty for WorksheetNumberi (for i = 1 To 10), then f_1 goes up by one; the final value for f_1 is returned and assigned to the variable 'count'; and then 'count' is displayed.
For Example B, if the count of non-empty cells in range $C$3:$E$5 is greater than 0 for WorksheetNumberi (for i = 1 To 5), then the f_1 goes up by one; the final value for f_1 is returned and assigned to the variable 'count'; and then 'count' is displayed.
Currently, I'm running into a "Subscript Out of Range" error, in the "count = ..." lines of code, which I'm guessing is because of the 'i' in Worksheets("SheetNumber" & i). How might I go about coding to achieve the results I want?
Thank you so much!
Provided all of your tests can be expressed as worksheet formulas then you can do something like this:
Sub tester()
Debug.Print CountIt(ThisWorkbook, "A1<>""""", 1, 5)
Debug.Print CountIt(ThisWorkbook, "COUNTA(C3:E5)>0", 1, 5)
Debug.Print CountIt(ThisWorkbook, "CountRedFont(B3:B5)", 1, 5)
End Sub
Function CountIt(wb As Workbook, theTest As String, _
fromSheet As Long, toSheet As Long) As Long
Dim n As Long, i As Long
For i = fromSheet To toSheet
n = n + IIf(wb.Sheets(i).Evaluate(theTest), 1, 0)
Next i
CountIt = n
End Function
'test UDF
Function CountRedFont(rng As Range)
Dim c As Range, n As Long
For Each c In rng.Cells
If c.Font.Color = vbRed Then n = n + 1
Next c
CountRedFont = n
End Function

Excel Vba Code to Check Quantity in a Column

I have a macro to format a spreadsheet. I need some excel vba code to add to the start to check that the Quantity in a column is always '1'
The code needs to check the column from cell H2 to the bottom of the data in H2 (until it finds a blank cell).
If all the values are '1' do nothing and continue running the macro. If it finds any other number (either negative or positive) display a MsgBox "Warning: Quantities Other Than '1' Found. Fix Errors and Re-Run!" then when 'OK' is selected exit the macro.
Something like this:
Sub YourExistingCode()
If QuantityErrorFound Then
MsgBox "Warning: Quantities Other Than '1' Found. Fix Errors and Re-Run!"
Exit Sub
Else
'~~> Run your code
End If
End Sub
Function QuantityErrorFound() As Boolean
Dim cl As Range, result As Boolean
result = False
For Each cl In Range("H2:H" & Range("H2").End(xlDown).Row)
If cl.Value <> 1 Then
result = True
End If
Next cl
QuantityErrorFound = result
End Function
I've used a function (QuantityErrorFound) to make it easier to integrate into your existing code
In your existing code simply add the if statement to check whether an error is found
Just a slight change to Alex P's code really. As you're dealing with 1s a simple sum will be quicker than a loop
Function QuantityErrorFound() As Boolean
Dim result As Boolean
Dim lastR as long
Dim sumCells as long
Dim cntCells as Long
result = False
'lastR = Range("H2").End(xlDown).Row
lastR= Cells(rows.count, Range("H2").Column).End(Excel.xlUp).Row '<< assuming below the last cell is empty then this is a better approach to above line.
sumCells = Excel.Application.Sum(Range("H2:H" & lastR))
cntCells = Range("H2:H" & lastR).cells.count
if (sumCells = cntCells) then
result = True
end if
QuantityErrorFound = result
End Function
Personally in my work spreadsheets I would use a formula in a hidden cell (named range called "ErrorCheck") like this:
=if(countif(H2:H10000,"<>1")>0,"error","ok")
Then in my vba all I need is the following:
if ((range("ErrorCheck") = "error") then
MsgBox "Warning: Quantities Other Than '1' Found. Fix Errors and Re-Run!"
else
...
...
Edit
Please see flaw in my check as pointed out by Ian Cook. I will leave the code as is - but you should force values in column H to be either 1 or 0 if using the above. This could be done with a simple formula:
=if(<current formula>=1,1,0)
or
=1*(<current formula>=1)
Or, defend Ian's possible problem, by changing the Sum in my vba to a countIf:
Function QuantityErrorFound() As Boolean
Dim result As Boolean
Dim lastR as long
Dim sumCells as long
Dim cntCells as Long
result = False
'lastR = Range("H2").End(xlDown).Row
lastR= Cells(rows.count, Range("H2").Column).End(Excel.xlUp).Row '<< assuming below the last cell is empty then this is a better approach to above line.
sumCells = Excel.Application.WorksheetFunction.CountIf(Range("H2:H" & lastR),"=1") '<<not tested and may need to read ...,1)
cntCells = Range("H2:H" & lastR).cells.count
if (sumCells = cntCells) then
result = True
end if
QuantityErrorFound = result
End Function
Then again if using the above it could be simplified to the following:
Function QuantityErrorFound() As Boolean
Dim result As Boolean
Dim lastR as long
Dim sumCells as long
result = False
lastR= Cells(rows.count, Range("H2").Column).End(Excel.xlUp).Row
sumCells = Excel.Application.WorksheetFunction.CountIf(Range("H2:H" & lastR),"<>1")
if (sumCells = 0) then
result = True
end if
QuantityErrorFound = result
End Function

resetting variables in excel vba

I am trying to build a model with VBA doing some work for me. Assume I have 4 variables - unit, lease start date, lease p.a, and alternative lease p.a. There are more, but that does not matter for my problem. VBA loops through each line and gets the value for unit i in respective column.
So, logically, I could declare unit as string, lease start date as date, lease p.a as single, and alternative lease p.a. as single. The problem I have is that I need to distinguish between empty entry and 0. The default numerical value is going to be 0. the distinction between 0 and empty is crucial. The only way I found to get around this is to declare everything as Variant and then check if the corresponding range is empty. If it is empty, then lease the Variant default value (Empty), otherwise assign the value.
I have a feeling that this is going to seriously affect my code performance. Ultimately, there will be lots of variables and I want to refer to those variables in the code. Like, if isempty(AltLease) = true then do one thing, otherwise something else.
I also find that I can not empty single or date variables(date is actually not a problem, since it drops to 1900). Can anyone suggest something?
Here is the code:
Dim tUnitName As Variant
Dim tNumberOfUnits As Variant
Dim tLeaseCurLeaseLengthDef as Variant
Dim tLeaseCurLeaseLengthAlt as Variant
Sub tenancyScheduleNew()
Dim lRow As Long
Dim i As Long
lRow = Sheet2.Cells(Rows.Count, 2).End(xlUp).Row
For i = 3 To lRow
reAssignVariables i
Next i
End Sub
Sub reAssignVariables(i As Long)
tAssetName = checkIfEmpty(i, getColumn("Sheet4", "tAssetName", 3))
tNumberOfUnits = checkIfEmpty(i, getColumn("Sheet4", "tNumberOfUnits", 3))
tLeaseCurLeaseLengthDef = checkIfEmpty(i, getColumn("Sheet4", "tLeaseCurLeaseLengthDef", 3))
tLeaseCurLeaseLengthDef = checkIfEmpty(i, getColumn("Sheet4", "tLeaseCurLeaseLengthAlt", 3))
End Sub
Function getColumn(sh As String, wh As String, colNo As Long) As Long
Dim refSheet As Worksheet
Dim rFound As Range
Set refSheet = Sheets(sh)
With refSheet
Set rFound = .Columns(1).Find(What:=wh, After:=.Cells(1, 1), LookIn:=xlValues, LookAt:=xlWhole)
On Error GoTo 0
If Not rFound Is Nothing Then
getColumn = rFound.Offset(0, colNo - 1).Value
Else
End If
End With
End Function
This is the way I am doing it now, which I think will slow the performance down. This is only small part of variables that I have made - there is going to be much more. I just need to understand how to construct it correctly in the first place. More specifically, if there is a value in tLeaseCurLeaseLengthAlt, then code should use that, alternatively, use default value.
You can't empty a variable of type integer, since empty is not an integer. If you have a variant variable which is currently of subtype integer you can reset it to empty:
Sub test()
Dim v As Variant
Debug.Print TypeName(v)
v = 1
Debug.Print TypeName(v)
v = Empty
Debug.Print TypeName(v)
End Sub
output:
Empty
Integer
Empty
Also, the performance hit of using variants might not be as great as you fear. An informal test:
Sub InformalTest(n As Long)
Dim i As Long, sum1 As Double
Dim j As Variant, sum2 As Variant
Dim start As Double, elapsed1 As Double, elapsed2 As Double
start = Timer
For i = 1 To n
sum1 = sum1 + 1#
Next i
elapsed1 = Timer - start
start = Timer
For j = 1 To n
sum2 = sum2 + 1#
Next j
elapsed2 = Timer - start
Debug.Print "Nonvariant time: " & elapsed1 & ", Nonvariant sum: " & sum1
Debug.Print "Variant time: " & elapsed2 & ", Variant sum: " & sum2
End Sub
Sample output:
InformalTest 1000000
Nonvariant time: 0.060546875, Nonvariant sum: 1000000
Variant time: 0.099609375, Variant sum: 1000000
InformalTest 10000000
Nonvariant time: 0.521484375, Nonvariant sum: 10000000
Variant time: 0.599609375, Variant sum: 10000000
Maybe you could create your own classes? Example for Single.
Class Single2
Private m_value As Single
Private m_hasValue As Boolean
Public Property Let Initialize(ByVal source As Range)
m_hasValue = False
m_value = 0
If source Is Nothing Then _
Exit Property
' add any checks you need to recognize the source cell as non-empty
' ... to distinguish between empty entry and 0
If Trim(source.Value) = "" Then _
Exit Property
If Not IsNumeric(source.Value) Then _
Exit Property
m_value = CSng(source.Value)
m_hasValue = True
End Property
Public Property Get Value() As Single
Value = m_value
End Property
Public Property Get HasValue() As Boolean
HasValue = m_hasValue
End Property
And use the class like this:
Module:
Dim lease As Single2
Set lease = New Single2
lease.Initialize = Range("a1")
If lease.HasValue Then
Debug.Print "lease has value ... " & lease.Value
Else
Debug.Print "lease hasn't value ... "
End If

Resources