Search and replace with wildcards in Excel VBA - excel

I use comma as decimal separator, but sometimes I receive files where values are below a set limit, and then the file uses point as "<2.5". Sometimes there is one digit before the decimal separator, and sometimes there are two digits.
I need to be able to replace the point with a comma in cells with begin with the "less than" symbol, but retain the actual numbers, so that "<2.5" is replaced with "<2,5" and "<10.0" is replaced with "<10,0". This needs to be done in Excel VBA.
I can't do a general search for "." and replace with ",", since there are places where I need to keep the point as it is.
Anyone have an idea of how to achieve this?

Approach via Replace function
You could read in data to a datafield array, replace the mentioned "<" data via Replace function and write them back in one statement by the following code. - Of course it's possible to use RegEx, too as mentioned in above comment.
Notes
a) I assume you are using data in column A:A via Set rng = ws.Range("A1:A" & n); this can easily changed to any other range.
b) Assigning values to a variant datafield array automatically creates a one based 2-dim array, which you address in case of one column only e.g. via v(1,1), v(2,1), v(3,1) etc. to v(n,1).
Example Code
Option Explicit
Sub replaceLowerThan()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("MySheet") ' << Change to your sheet name
Dim n As Long, i As Long
Dim rng As Range
Dim v
' get last row number and define data range
n = ws.Range("A" & ws.Rows.Count).End(xlUp).Row
Set rng = ws.Range("A1:A" & n)
' write data to 1-based 2-dim datafield array
v = rng.Value2
' replace "<..." values
For i = 1 To n
If Not IsError(v(i, 1)) Then ' omit cells with errors like #DIV/0!
If v(i, 1) Like "<*" Then v(i, 1) = Replace(v(i, 1), ".", ",")
End If
Next i
' write values back
rng.Value2 = v
End Sub

This worked:
Dim strPattern As String: strPattern = "(<[0-9]+)[\.]"
Dim strReplace As String: strReplace = "$1,"
Dim myreplace As Long
Dim strInput As String
Dim Myrange As Range
Set RegEx = CreateObject("VBScript.RegExp")
Set Myrange = ActiveSheet.UsedRange
For Each cell In Myrange
If strPattern <> "" Then
strInput = cell.Value
With RegEx
.Global = True
.MultiLine = True
.IgnoreCase = False
.Pattern = strPattern
End With
If RegEx.Test(strInput) Then
cell.Value = (RegEx.Replace(strInput, strReplace))
End If
End If
Next

Related

Drill down into relevant SUMIFS when multiple SUMIFS are nested in an IF statement

I have a VBA script to drill down into a cell containing a SUMIFS, and then filter the raw data sheet to isolate the rows pertaining. The script works on a cell that has just one SUMIFS.
Some of my cells contain an IF statement with two SUMIFS (depending on what variable the IF is).
I am trying to find a way to first find the relevant SUMIFS in the IF statement, and then use the correct one to then filter.
My code:
Double click script loaded on worksheet to trigger the macro (this is working)
Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
FilterBySUMIFs Target.Cells(1)
End Sub
Script loaded to Module for the SUMIFS
Option Explicit
Sub FilterBySUMIFs(r As Range)
Dim v, ctr As Integer
Dim intField As Integer, intPos As Integer
Dim strCrit As String
Dim rngCritRange1 As Range, rngSUM As Range
Dim wksDataSheet As Worksheet
If Not r.Formula Like "*SUMIFS(*" Then Exit Sub
'split formula by comma, strip the right paren
v = Split(Left(r.Formula, Len(r.Formula) - 1), ",")
'the first criteria range is the 2nd element of the array
Set rngCritRange1 = Range(v(LBound(v) + 1))
'use first criteria range to get a reference to the data sheet
With rngCritRange1
Set wksDataSheet = Workbooks(.Parent.Parent.Name).Worksheets(.Parent.Name)
End With
'clear any existing filter, turn filtering on if needed
With wksDataSheet
If .AutoFilterMode And .FilterMode Then
'clear existing autofilter
.ShowAllData
ElseIf Not .AutoFilterMode Then
'display autofilter arrows
rngCritRange1.CurrentRegion.AutoFilter
End If
End With
'set the filters
For ctr = LBound(v) + 1 To UBound(v)
If ctr Mod 2 <> 0 Then
With wksDataSheet
'determine field in case table does not start in column A
intField = .Range(v(ctr)).Column - .AutoFilter.Range.Columns(1).Column + 1
'use evaluate instead of range(v(ctr + 1))
'so both cell-reference and hard-coded criteria are handled.
strCrit = Evaluate(v(ctr + 1))
.Range(v(ctr)).AutoFilter Field:=intField, Criteria1:=strCrit
End With
End If
Next
'strip left paren and everything to left of it,
' get the sum range from first element of array
intPos = InStr(1, v(LBound(v)), "(")
Set rngSUM = Range(Replace(v(LBound(v)), Left(v(LBound(v)), intPos), ""))
'select the SUM range so total displays in status bar
Application.Goto rngSUM
ActiveWindow.ScrollRow = 1
End Sub
Sub KV_FilterBySumIf()
End Sub
My SUMIFS looks as follows:
=IF($C$6="ALL",SUMIFS(IS!Actual_Total,IS!Curr_Bud,H$9,IS!Master_Sub_Account,$C14),SUMIFS(IS!Actual_Total,IS!Curr_Bud,H$9,IS!Master_Sub_Account,$C14,IS!Project_Desc,$C$6))
I came up with a function that can split the IF formula into it's TRUE and FALSE parts and return relevant part based on the expression. So if the expression is true, the function returns the True part of the IF Formula.
The function I have made is not a robust function, and it only works if the given formula is in the structure of "=IF(< expression >, SUMIFS(...), SUMIFS(...))". And it evaluates the expression with the ActiveSheet.
Sub Example()
Dim SumIfsFormula As String
SumIfsFormula = "=IF($C$6=""ALL"",SUMIFS(IS!Actual_Total,IS!Curr_Bud,H$9,IS!Master_Sub_Account,$C14),SUMIFS(IS!Actual_Total,IS!Curr_Bud,H$9,IS!Master_Sub_Account,$C14,IS!Project_Desc,$C$6))"
Debug.Print RelevantSumIfs(SumIfsFormula)
'Output when TRUE : SUMIFS(IS!Actual_Total,IS!Curr_Bud,H$9,IS!Master_Sub_Account,$C14)
'Output when FALSE : SUMIFS(IS!Actual_Total,IS!Curr_Bud,H$9,IS!Master_Sub_Account,$C14,IS!Project_Desc,$C$6)
End Sub
Function RelevantSumIfs(SumIfsFormula As String) As String
Dim IfResult As Boolean
IfResult = Application.Evaluate("=" & Split(Split(SumIfsFormula, "(")(1), ",")(0))
Dim regex As Object
Set regex = CreateObject("VBScript.RegExp")
With regex
.Global = True
.IgnoreCase = True
.Pattern = "SUMIFS\([^)]+\)"
End With
Dim Matches As Object
Set Matches = regex.Execute(SumIfsFormula)
Dim TargetSumIfs
If IfResult Then
TargetSumIfs = Matches(0)
Else
TargetSumIfs = Matches(1)
End If
RelevantSumIfs = TargetSumIfs
End Function
This function could be improved by changing the Regex pattern from explicitly searching for SUMIFS to searching for any function. Something like .Pattern = "[A-Za-z0-9]+\([^)]+\)". But this may also match many other parts of the formula, including the surrounding IF(...). Which is why I left it as .Pattern = "SUMIFS\([^)]+\)"

Replace cell content in Excel with array content

I have an Excel worksheet which includes a text column which stores both text and numbers. I am trying to extract serial numbers, which are 13 digits long, and replace the column content with these serial numbers. I was able to extract the serial numbers but I am stalling on how to replace the cell.value with the serial numbers. Below is my initial approach:
Sub extract_digits()
Dim cell As Range
Dim arr As Variant, arrElem As Variant
Dim final_arr As Variant
With Worksheets("Test_1")
For Each cell In .Range("H5", .Cells(.Rows.Count, "H").End(xlUp))
arr = Split(Replace(cell.Value, " ", " "), " ")
For Each arrElem In arr
If Len(arrElem) = 13 Then MsgBox arrElem
Next arrElem
Next cell
End With
End Sub
The second option (Remove Letters From Strings/Numbers/Cells With User Defined Function) from this site should do the trick:
https://www.extendoffice.com/documents/excel/3244-excel-remove-letters-from-strings-cells-numbers.html
Function below is from above website, it's not mine
Function StripChar(Txt As String) As String
With CreateObject("VBScript.RegExp")
.Global = True
.Pattern = "\D"
StripChar = .Replace(Txt, "")
End With
End Function
How is this working
Is using Regular Expressions (RegEx). The .Pattern means it's looking for any non numeric (ie 0 to 9), it's then using the .Replace function to replace any non numeric with nothing.
How do you use this
Option 1 You can either use it directly in a column next to your column, eg =StripChar(H5) and then just copy that formula down.
Option 2 Alternatively using your code
Sub extract_digits()
Dim cell As Range
Dim arr As Variant, arrElem As Variant
Dim final_arr As Variant
With Worksheets("Test_1")
For Each cell In .Range("H5", .Cells(.Rows.Count, "H").End(xlUp))
cell = StripChar(cell.Value)
Next cell
End With
End Sub

Extract numbers from comment and add them

As the title suggests I am looking for a way to retrieve all the numbers from a cell comment and add them up. The only way I can think to do this would be to retrieve the comment as a string, assign each set of numbers to a variable, then add up the variables?
I am having a hard time with the logic, I don't know a way to retrieve the numbers out of a comment.
So far I have:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim varComment As String
For i = 19 To 30
If Not Intersect(Target, Range("N19:N30")) Is Nothing Then
On Error Resume Next
varComment = Cells(Ni).Comment.Text
Next i
End If
End Sub
The use is that I have a comment in cells N19:N30 that contains dollar values, "Food - $20, Gas - $40, etc..." I want the cell value to be updated anytime a new listing is made to reflect the total cost. Make sense?
Without making any assumptions on the numbers I would extract the numbers with a regex expression and then sum them up. I used a function found here and modified it slightly.
Function CleanString(strIn As String) As String
Dim objRegex
Set objRegex = CreateObject("vbscript.regexp")
With objRegex
.Global = True
'.Pattern = "[^\d]+"
.Pattern = "[^0-9" & Application.DecimalSeparator & "]"
CleanString = .Replace(strIn, vbCrLf)
End With
End Function
With this function you can then add up the numbers in a comment
Function commentSum(cmt As Comment) As Double
Dim vDat As Variant
Dim i As Long
Dim res As Double
vDat = Split(CleanString(cmt.Text), vbCrLf)
For i = LBound(vDat) To UBound(vDat)
If Len(vDat(i)) > 0 Then
res = res + CDbl(vDat(i))
End If
Next i
commentSum = res
End Function
For testing purposes
Sub TestCmtAdd()
Dim rg As Range
Dim sngCell As Range
Set rg = Range("A1:A10")
For Each sngCell In rg
If Not (sngCell.Comment Is Nothing) Then
MsgBox "Sum of numbers in comment of cell: " & sngCell.Address & " is " & commentSum(sngCell.Comment)
End If
Next
End Sub
My below code is working under the following assumption:-
-
Each Number MUST be start with "$" (spaces between $ and the Number will be trimed)
Each Number MUST end with "," (spaces between "," and the Number will be trimed)
Your "varComment" is already populated
Note: Split the comment with "vbCrLf" did not work with me
Dim SplitedComment() As String
Dim tmpStr As Variant
Dim DolarSignLoc, yourSum As Integer
' For Each Comment, Do the following
SplitedComment() = Split(varComment, ",") ' Split the Comment by ",", we'll need ONLY the output that Contain "$" ( some of the output may NOT contain that char)
yourSum = 0 ' initialize your Sum Variable
For Each tmpStr In SplitedComment ' for each Text in the SplittedComment
DolarSignLoc = InStr(tmpStr, "$") ' Get the Location of the "$" ( ZERO if not exist)
If DolarSignLoc > 0 Then ' ONLY Process the Text if contains "$"
tmpStr = Right(tmpStr, Len(tmpStr) - DolarSignLoc) ' Excetract your Number
yourSum = yourSum + CInt(Trim(tmpStr)) ' Add to your Summation
End If
Next

How to extract only cells which contain a letter AND a number in Excel?

I have a series of addresses from which I need to extract postal codes.The data is very sloppily formatted (not separated, some with spacings some without etc..) meaning the only way I can think to extract the postcodes is to create a column to which is added only the values which contain Text and a Number as these are the only cells to contain the postal code.
The data is too messy to isolate exactly where the postcode lies but I would like something to return a result like above.
How could I return only cell O2 & P2 from the range K2:R2?
*Address here is made up
Though I believe that #DarrenBartrup-Cook has a better answer. This quick dirty little UDF will do it bassed on the mix of numbers and text like asked.
Function pcode(rng As Range)
Dim rngt As Range
Dim chr As String
Dim i As Integer
For Each rngt In rng
If Not IsNumeric(rngt) Then
For i = 1 To Len(rngt)
If IsNumeric(Mid(rngt, i, 1)) Then
pcode = Trim(pcode & " " & rngt.Value)
Exit For
End If
Next i
End If
Next rngt
End Function
Put this in a module attached to the workbook, NOT the worksheet code or ThisWorkbook code.
You would call it from the sheet with this formula:
=pcode(I5:P5)
For a VBA result you could use the code below.
In cell T2 enter =GetPostCode(K2:R2),
or in VBA you can use Debug.Print GetPostCode(Sheet1.Range("K2:N2"))
I can't remember where I got the pattern from, but can probably be improved.
Public Function GetPostCode(AddressRange As Range) As Variant
Dim rCell As Range
Dim sAddressString As String
For Each rCell In AddressRange
sAddressString = sAddressString & " " & rCell.Value
Next rCell
sAddressString = Trim(sAddressString)
GetPostCode = ValidatePostCode(sAddressString)
End Function
Public Function ValidatePostCode(strData As String) As Variant
Dim RE As Object, REMatches As Object
Dim UKPostCode As String
'Pattern could probably be improved.
UKPostCode = "(?:(?:A[BL]|B[ABDHLNRST]?|C[ABFHMORTVW]|D[ADEGHLNTY]|E[CHNX]?|F[KY]|G[LUY]?|" _
& "H[ADGPRSUX]|I[GMPV]|JE|K[ATWY]|L[ADELNSU]?|M[EKL]?|N[EGNPRW]?|O[LX]|P[AEHLOR]|R[GHM]|S[AEGKLMNOPRSTWY]?|" _
& "T[ADFNQRSW]|UB|W[ACDFNRSV]?|YO|ZE)\d(?:\d|[A-Z])? \d[A-Z]{2})"
Set RE = CreateObject("VBScript.RegExp")
With RE
.MultiLine = False
.Global = False
.IgnoreCase = True
.Pattern = UKPostCode
End With
Set REMatches = RE.Execute(strData)
If REMatches.Count = 0 Then
ValidatePostCode = CVErr(xlErrValue)
Else
ValidatePostCode = REMatches(0)
End If
End Function
Edit: I thought it wasn't working as it only return E17 3RU which is in Walthamstow, but HE17 3RU isn't a valid postcode (http://www.royalmail.com/find-a-postcode) so it found the valid one.

Detect non empty last cell location using Excel VBA

In my Excel sheet, I have VBA code to detect the last non-empty cell in Column A and add incremental serial number value in that cell (in below example cell A6 value should be SN104).
This processing is limited only to Column A, and in this image example first non-empty last cell is at A6, sometimes it can be after 100 cells or 1000 cells.
Is there any simple way to handle this scenario?
Public Function GetLastCell(ByVal startRng as Range) as Range
With startRng
Set GetLastCell = IIf(.Offset(1).Value = "", .Offset(0), .End(xlDown))
End With
End Function
For your example, you can define a Range variable rng, and call the above function in this way:
Dim rng as Range
Set rng = GetLastCell( Range("A1") )
Then rng is referring to the last cell of Column A
Something like
Dim lngLastUsedRow as Integer
lngLastUsedRow = Range("A65536").End(xlUp).Row
Dim lngFirstEmptyRow as Integer
lngFirstEmptyRow = Range("A65536").End(xlUp).Offset(1,0)
// do your increment
newValue = Cint(Mid(CurrentWorkSheet.Range("A" + lngLastUsedRow).Value,2)) + 1
CurrentWorkSheet.Range("A" & lngFirstEmptyRow).Value = "SN" + newValue
I don't have excel on me, I can't test it right now. But this should get you started.
Something like this which
Find the true last used cell in any Excel version, and handles a blank result
Parses the string in the last non-blank cell (handling any length of alpha then numeric)to update the next blank cell
Sub GetTrueLastCell()
Dim rng1 As Range
Dim objRegex As Object
Dim strFirst As String
Set rng1 = Columns("A").Find("*", [a1], xlFormulas)
If Not rng1 Is Nothing Then
Set objRegex = CreateObject("vbscript.regexp")
With objRegex
.Pattern = "^(.+?[^\d])(\d+)$"
If .test(rng1.Value) Then
strFirst = .Replace(rng1.Value, "$1")
rng1.Value = strFirst & (Val(Right$(rng1.Value, Len(rng1.Value) - Len(strFirst)) + 1))
End If
End With
Else
MsgBox "Data range is blank"
End If
End Sub
Assumptions:
Next cell in list is empty
Serial N's only have three digits after 'SN' string (i.e., if it reaches 1000, earlier ones don't need padding, like '0100'
-
Dim rAll As Range, rLast As Range, rNext As Range, iNextSN As Integer
Set rAll = Intersect(Sheet1.Cells(1).CurrentRegion, Sheet1.Columns(1)) ' Column 'A' can be contiguous with others
Set rLast = rAll.Cells(rAll.Cells.Count) ' Last cell in current list
Set rNext = rLast.Offset(1) ' Next cell below current list
iNextSN = CInt(Right(rLast.Value, 3)) ' Get value of last serial N
rNext.Value = "SN" & iNextSN + 1 ' Assemble next SN with increment
-

Resources