VBA -- faster way to deal with numbers stored as text - excel

I have this function which manages to remove all numbers stored as text:
Public Function find_numbers_formated_as_text(ByVal sh As Worksheet)
Dim c As Range
On Error GoTo A
''''For Each r In sh.UsedRange.Cells.SpecialCells(xlCellTypeConstants, xlTextValues)
For Each c In sh.ListObjects(1).DataBodyRange
''''For Each c In sh.UsedRange.Cells.SpecialCells(xlCellTypeConstants, xlNumbers)
''''For Each c In sh.ListObjects(1).DataBodyRange.Cells.SpecialCells(xlCellTypeConstants, xlNumbers)
If IsNumeric(c.Value) = True Then
c.Value = c.Value
End If
Next c
Exit Function
A:
On Error GoTo 0
End Function
But it is really slow... Does anyone have any suggestion on how to make it faster?
I did try some other things which is why there are some of the comments in the source code. But comments didn't work, because range was also empty in my case (even if table was full of data).

Please, replace this part of your code:
For Each C In sh.ListObjects(1).DataBodyRange
''''For Each c In sh.UsedRange.Cells.SpecialCells(xlCellTypeConstants, xlNumbers)
''''For Each c In sh.ListObjects(1).DataBodyRange.Cells.SpecialCells(xlCellTypeConstants, xlNumbers)
If IsNumeric(C.Value) = True Then
C.Value = C.Value
End If
Next C
with this one:
Dim lRng As Range, arr, i As Long, j As Long
Set lRng = sh.ListObjects(1).DataBodyRange
arr = lRng.Value2
For i = 1 To UBound(arr)
For j = 1 To UBound(arr, 2)
If IsNumeric(arr(i, j)) Then arr(i, j) = CDbl(arr(i, j))
Next j
Next i
With lRng
.NumberFormat = "General"
.Value2 = arr
End With
It places the range in an array and all process take place only in memory, the modified array content being dropped at the end of the code, at once.
The most time consuming is the iteration between each cell and writing cell bay cell...
If "General" formatting may bother your list object format, please state in which columns the conversion should be done, and I will adapt the code to format only the respective table columns.
Now I could see one of your comments saying that in the range to be processed exist (and must remain) formulas. The above code does not deal with such a situation. You should state it in your question, I think...
Edited:
If the formulas used to return a numeric value (not a string) and, by mistake the respective range has been formatted as Text, you can try the next way (to maintain the formulas):
With sh.ListObjects(1).DataBodyRange
.NumberFormat = "General"
.Formula = .Formula
End With

There are lots of answers to this question. This is a simple thing to try. Add this before:
Application.Calculation = xlManual
and this after:
Application.Calculation = xlAutomatic

It's faster to store the range as array than changing values in the sheet.
sh.ListObjects(1).DataBodyRange.formula = sh.ListObjects(1).DataBodyRange.formula
The numbers will default to numbers if they were text so you don't need to test if it's number.
(You will not lose formulas using this method.)

Related

How to find, copy a different column and then paste somewhere else with multiple values

I am looking to search the text in first column for specific words and when they're found copy and paste the adjacent column to somewhere else.
I've got this code which works fine if the text is exactly those words but if anything else is there it fails (i.e super consolidator).
I'm still very new to VBA and have just adapted some other code to get to this point. I figure the find function would be a good way to go about it but I can't wrap my head around how to avoid the infinite loops. Any help here would be appreciated
Sub Test()
Dim lr As Long
Dim r As Long
' Find last row in column A with data
lr = Cells(Rows.Count, "A").End(xlUp).Row
' Loop through all rows in column A
For r = 1 To lr
' Check value on entry
If (Cells(r, "A") = "Super") Or (Cells(r, "A") = "Pension") Or (Cells(r, "A") = "SMSF") Then
' Copy column B and paste in C where found
Cells(r, "B").Select
Selection.Copy
ActiveCell.Offset(0, 1).PasteSpecial
End If
Next r
End Sub
What you're looking for is called Wildcard string comparision. And you can use VBA's Like operator to achieve your output
If (Cells(r, "A") Like "Super*") Or (Cells(r, "A") Like "Pension*") Or (Cells(r, "A") Like "SMSF*") Then
Here the * in Super* means that the text should start with "Super" and it can have anything after that.
If you'd like to search if the cell contains "Super" anywhere, you can use *Super* - * at both ends of Super
To have a more robust code I moved the "signal" words you are checking for into an array at the beginning of the sub.
Same with the column indexes of the column you want to copy and the target index.
By that it is much easier to make adjustments if the requirements change, e.g. look for a forth word etc.
Furthermore you should avoid implicit referencing cells. That's why I added the ws-variable - you have to adjust your sheet name.
Plus I added a generic function isInArray that takes the cell-value plus the array with the lookup values and returns true or false. Here the like-operator is implemented.
You don't need to select-copy/paste the values - you can simply write them to the target cell: .Cells(r, targetColumnIndex).value = .Cells(r, sourceColumnIndex).value.
But be aware: if you have a lot of data it would make more sense to load everything into an array and work on that ... but that's the next lesson to learn ;-)
Option Explicit
Public Sub copyValues()
Dim arrLookupValues(2) As Variant
arrLookupValues(0) = "Super"
arrLookupValues(1) = "Pension"
arrLookupValues(2) = "SMSF"
Const sourceColumnIndex As Long = 2 'take value from column B
Const targetColumnIndex As Long = 3 'write value to colum C
application.screenupdating = false
Dim lr As Long
Dim r As Long
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Sheet1") 'adjust this to your needs
With ws
lr = .Cells(.Rows.Count, "A").End(xlUp).Row
For r = 1 To lr
' Check value on entry
If isInArray(.Cells(r, 1).value, arrLookupValues) Then
' write value of column B (2) to C (3)
.Cells(r, targetColumnIndex).value = .Cells(r, sourceColumnIndex).value
End If
Next r
End With
application.screenupdating = true
End Sub
Private Function isInArray(value As Variant, arrLookFor As Variant) As Boolean
Dim i As Long
For i = LBound(arrLookFor) To UBound(arrLookFor)
If value like arrLookFor(i) & "*" Then
isInArray = True
Exit For
End If
Next
End Function

Empty cells showing as non-empty when trying to fill in

I have an Excel file with 3 columns, the first 2 were concatenated into the 3rd one.
Then I ran this VBA in order to make the empty cells equal to the above populated cell.
Sub fillempty()
With Selection.SpecialCells(xlCellTypeBlanks)
.FormulaR1C1 = "=R[-1]C"
.Value = .Value
End With
End Sub
Saying that there are no empty cells, despite being visible empty. So if I take the so called non-empty cells but visible empty and clear content and run the VBA again, it does populate.
The problem arises because I have hundreds of thousands of values.
If I run the VBA independently for 1st or 2nd column works, but not for the Concatenated one.
It appears that some cells are empty with the naked eye, but unless their content is cleared the script won't work...
C2 = IF(A2 <> "", CONCATENATE(A2, B2), "") then dragged it down for hundreds of thousands of values
Any help would be more than appreciated.
Thanks
Dim c As Range
For Each c in Selection.Cells
if Len(Trim(c.value))=0 Then c.value=c.offset(-1,0).value
Next c
or faster as long as you don't need to preserve any formulas:
Dim r as long, c as long, data, cols as long
data = selection.value
cols = ubound(data, 2)
for r=2 to ubound(data, 1)
for c = 1 to cols
if Len(Trim(data(r, c))) = 0 Then data(r, c) = data(r-1, c)
next c
next r
selection.Value = data
The isEmpty() function is handy, too. It returns FALSE if there's a formula in it (but the cell is clear). Looks like this:
Sub IsA1ReallyEmpty()
If IsEmpty(Range("A1").Value) = True Then
MsgBox "Yep, A1 is empty."
Else
MsgBox "Nope, it has stuff in it."
End If
End Sub

VBA - Returning values from a table which fit a certain criteria and outputting them in a new sheet

Background:
As part of a project I am working on which involves fuzzy string matching, I have implemented the Levenshtein Distance algorithm in VBA to calculate the "similarity" between two strings (see this question for part of the code/more insight into my project).
So, I created a table in Sheet1 in Excel with row and column headers which are strings (located in cells A2:A2146 and B1:TU1, respectively), and I am comparing these strings with the LevenshteinDistance function. The function populates the empty cells in the table (in my case, B2:TU2146) with what I call the matchScore. The idea is this: the more similar two strings are, the lower their matchScore. It follows that if two strings match exactly, we would have matchScore = 0.
(1) More specifically, suppose the value of S1 (one of my column headers) is "recursion" and the value of cell A532(one of my row headers) is "recursion". After executing my "similarity" function, the value returned in cell S532 of the table is 0.
What I want to achieve:
For the purposes of my problem and the heuristic I've defined to measure string similarity, I am particularly interested in string pairs for which matchScore <= 1 is true (this includes the example (1) above).
The data table is huge, and it is difficult for me to get visibility into the "good data" (matchScore <= 1). Thus, I want Excel to find each value in the table which is <=1 and output them in Sheet2 along with the strings that were paired off as "good matches." Therefore, there should be three columns of data in Sheet2. To reference again the example above (1), when my code finishes running, I should see 0, "recursion", and "recursion" in cells A1 through C1 (assuming this was the only "good match" I found in the table).
What I have tried to implement as a solution:
Sub FindMatches()
Dim r As Long, c As Range
r = 1
For Each c In Range("B2:BY2146").Cells
If c.Value <= 1 Then Sheets("Sheet2").Cells(r, 1).Resize(1, 3).Value = Array(c.Value, Cells(c.Row, 1), Cells(1, c.Column))
r = r + 1
Next c
End Sub
Effectively, this sub does nothing. Where am I wrong in the way I am attempting to implement my solution, and what can I do to solve this issue?
Here's an updated sub:
Sub FindMatches()
On Error GoTo errHandler
Dim r As Long, c As Range
Application.ScreenUpdating = False
With Sheets("Sheet2")
r = 1
For Each c In Range("B2:BY2146").Cells
If c.Value <= 1 Then
.Cells(r, 1).Resize(1, 3).Value = Array(c.Value, Cells(c.Row, 1).Value, Cells(1, c.Column).Value)
r = r + 1
End If
Next c
End With
Recover:
On Error Resume Next
Application.ScreenUpdating = True
Exit Sub
errHandler:
MsgBox Err.Description, vbExclamation + vbOKOnly, "Error"
Resume Recover
End Sub
Note that Array returns a one-dimensional array, whereas .Value, when assigned an array, expects a 2D one. So I've split the assignments across 3 lines of code.
EDIT
To my surprise, assigning a 1D, zero-based array to the .Value property of a single-row range works beautifully, whereas I thought a 2D, 1-based was a requirement. So my initial paragraph above is baloney and #Profex really found the issue.
A With block provides a bit more performance, and the Application.ScreenUpdating management provides lots more. It is important to reset Application.ScreenUpdating to True in case of error.
Beware of unqualified references, i.e. Range and Cells not preceded by their parent object (e.g. ... In Range("B2:BY2146").Cells and Cells(c.Row, 1); those are looking at whichever worksheet is the active one when they're invoked. In your case, if the source values are on Sheet1, you could use e.g. ... In Sheets("Sheet1").Range("B2:BY2146").Cells and Sheets("Sheet1").Cells(c.Row, 1).
Qualify sheets("sheet1") in the assignment statement & loop.
Put r=r+1 inside the if statement.
Sub FindMatches()
Dim r As Long, c As Range
r = 1
For Each c In Sheets("Sheet1").Range("B2:BY2146").Cells
If c.Value <= 1 Then
Sheets("Sheet2").Range(Cells(r, 1), Cells(r, 3)).Value = Array(c.Value, Sheets("sheet1").Cells(c.Row, 1), Sheets("sheet1").Cells(1, c.Column).Value)
r = r + 1
End If
Next c
End Sub

Most efficient way of offseting cell that contains "x"

I've been using Columns(2).Copy Destination:=Columns(1) provisionally to offset all cells that contain "x" one column to the left, however it seems to be very memory consuming and not very reliable as I can't choose to offset only "x" values.
Is there any way I can make only the cells on which this contition was met to be offset, and on a more efficient way?
Similar to the below answer, but with a For-Each loop and for the whole range:
Sub MoveOver()
Dim rng As Range
Dim c As Range
Set rng = ThisWorkbook.Worksheets("Sheet1").Range("B:B")
For Each c In rng
If c.Value = "x" Then c.Offset(0, -1) = c.Value
Next c
End Sub
you would probably want to try something like this
For i = 1 To 100
If ActiveSheet.Cells(i, 2).Value = "x" Then
ActiveSheet.Cells(i, 1).Value = ActiveSheet.Cells(i, 2).Value
End If
Next
just set the 100 for however many rows you have. I'm not even sure this would run any faster than what you have. Unless I am missing your goal here.
and if you want to go through all cells in the sheet
For Each rcell In ActiveSheet.Cells
If recll.Column >= 1 And rcell.Value = "x" Then
ActiveSheet.Cells(rcell.Row, rcell.Column - 1).Value = ActiveSheet.Cells(rcell.Row, rcell.Column).Value
End If
Next
but this one would presumably take a really long time. Ideally you should use nested loops if you know the last row and column you are using. This way you only iterate of cells that you believe contain data.

Changing the sign of numeric values in a column

I have data in column D.
There is a header in column D1 and numeric values in D2 downward. I would like to select all numeric values in column D (the number of values is unknown) and multiply them by -1 (replacing the current values in column D). How would I do this through VBA code?
If I could use formulas in Excel I would simply drag the formula D2*-1 downward; however, I need the VBA equivalent.
The following works almost instantaneously when tested with 100,000 random values:
Sub MultColDbyOne()
Dim i As Long, n As Long, A As Variant
n = Cells(Rows.Count, "D").End(xlUp).Row
A = Range(Cells(2, "D"), Cells(n, "D")).Value
For i = LBound(A) To UBound(A)
A(i, 1) = -A(i, 1)
Next i
Range(Cells(2, "D"), Cells(n, "D")).Value = A
End Sub
The sub works by first determining the last row with data in column D, then transferring it to a VBA array (which is, somewhat annoyingly, a 2-dimensional array with only 1 column), looping through that array replacing each number in it by its negative, then transferring it back. This Range to array then back to Range strategy is fairly common (and fairly efficient) in VBA.
Just for curiosity I wanted to employ selecting special cells (numbers) feature of Excel. I created another function and tested the speed against the function created by #John Coleman.
If column D contains 10,000 values, #John Coleman's function is faster.
If column D contains 1,000,000 values, this function is faster.
Sub ChangeSignColD()
Dim v, x As String
Application.ScreenUpdating = 0
x = Selection.Address
With Cells(1, 5)
v = .Value
.Value = -1
.Copy
Columns("D:D").SpecialCells(2, 1).PasteSpecial -4163, 4
.Value = v
End With
Range(x).Select
Application.CutCopyMode = 0
End Sub
In addition, I noticed that this function would not error if there was e.g. some text value in the column.
I like how #Zygd solve it, but i propose to use a cell for the -1 not interfering with existing working range.
Sub InvertNumericSign()
Dim LastCell As Range
Dim SignRng As Range
Set LastCell = Cells.SpecialCells(xlCellTypeLastCell)
Set SignRng = Selection
If Not LastCell = "" Then Set LastCell = LastCell(2, 2)
LastCell = -1
LastCell.Copy
SignRng.PasteSpecial Paste:=xlPasteValues, Operation:=xlMultiply
LastCell.ClearContents
End Sub

Resources