How do I have my code recognize cells with a specific string STRUCTURE, while ignoring others? - vba

So, here's what I'm facing.
I have a bit of vba code, that will find every cell in a single column containing a specific string. The words it looks for are in an array called totArray. This works well.
Problem is, when a word in the cell contains "SUBTOTAL" or something similar, it will still find it and copy it. Also, there will be words where TOTAL or TOTAAL aren't followed by numbers of unknown length.
Please see my code attached. How do I get it so that it will find every case where the cell contains TOTAL or TOTAAL but followed always by a non-common structure of numbers only.
Sub CopyValues()
Dim totArray As Variant
Dim wsSource As Worksheet
Dim wsDest As Worksheet
Dim NoRows As Long
Dim DestNoRows As Long
Dim I As Long
Dim J As Integer
Dim rngCells As Range
Dim rngFind As Range
Dim Found As Boolean
totArray = Array("TOTAAL ", "TOTAL")
Set wsSource = Worksheets("Input")
Set wsDest = Worksheets("Output")
NoRows = wsSource.Range("A65536").End(xlUp).Row
DestNoRows = 2
For I = 1 To NoRows
Set rngCells = wsSource.Range("A" & I)
Found = False
For J = 0 To UBound(totArray)
Found = Found Or Not (rngCells.Find(totArray(J)) Is Nothing)
Next J
If Found Then
rngCells.Copy wsDest.Range("B" & DestNoRows)
DestNoRows = DestNoRows + 1
End If
Next I
End Sub

To find a string with characters before/after the specified string:
You could put wildcards into the string, see this documentation.
"*TOTAL" would find Subtotal, Grandtotal etc as the asterisk wildcards any number of characters.
"*TOTAL????? would find any word with total at the end (like the example above) and with up to 5 characters after the word (as there are 5 question marks). For example: Subtotal123 or Subtotal54321
*TOTAL ????? would find Subtotal 123 or Subtotal 54321 (notice the space can be used in the string between characters/wildcards).
Using this info you should be able to adjust your Array strings to work in your situation.
To find an exact match to the specified string:
You should specify the LookAt parameter in your .find method.
e.g. rngCells.Find(totArray(J), , , LookAt:= xlWhole).
Using the LookAt function is pretty straightforward.
xlWhole means the search value must match the entire cell contents.
xlPart means the search value only has to match part of the cell
Note: In that example the After and LookIn parameters are omitted.
The LookAt parameter is the 4th parameter which is why there are blanks between the commas.
If you don't specify your parameters, Excel uses the parameters from the last search you ran either from using the .Find method via VBA or the GUI find window.
You can read more about each parameter, including the LookAt parameter in this article about the vba find method.

Related

How to count specific char in vba

So I need to count how many ž and č are there in all of these fields.
Example.
http://prntscr.com/jwz1em
I tryed with this code but it gives me 0
Function slova(iVal)
Dim output As Integer
output = Application.WorksheetFunction.CountIf(Range("A2:A11"), "ž")
End Function
I see multiple problems with your code:
There is no assignment of return value to function, in my example slova = charCnt, so it wouldn't return anything besides default 0 no matter what.
It lacks Application.Volatile, so the formula used in Excel cell would require navigating to cell and pressing ENTER to force an update when data in range changes.
Function has an argument iVal which isn't used anywhere.
Application.WorksheetFunction.CountIf returns count of cells, so it is limited to 1 character per cell. On top of it, correctly specified argument would be "*ž*"
Here is my solution to count all occurrences of hardcoded character in hardcoded range (must have exactly 1 column).
Function slova() As Long
Application.Volatile
Dim vData As Variant
Dim rowCounter As Long, charCnt As Long
Const myLetter As String = "ž"
vData = Range("A2:A11")
For rowCounter = LBound(vData) To UBound(vData)
If vData(rowCounter, 1) <> vbNullString Then
charCnt = charCnt + UBound(Split(vData(rowCounter, 1), myLetter))
End If
Next rowCounter
slova = charCnt
End Function
As you use function, you can also take advantage of it and use source range as an argument, the same goes for character.

Vectorial formula for cell validation in Excel using VBA

I am writing a VBA formula to check that all characters in a cell "TestChars" are allowed, where allowed means that each character appears in a list defined by another cell "AllowedChars". To make things even harder, I would like this formula to work on ranges of cells rather than on a single cell.
The current code seems to work:
Option Explicit
Public Function AllCharsValid(InputCells As Range, AllowedChars As String) As Boolean
' Check that all characters in InputCells are among
' the characters in AllowedChars
Dim Char As String
Dim Index As Integer
Dim RangeTestChars As Range
Dim TestChars As String
For Each RangeTestChars In InputCells
TestChars = RangeTestChars.Value
For Index = 1 To Len(TestChars)
Char = Mid(TestChars, Index, 1)
If InStr(AllowedChars, Char) = 0 Then
AllCharsValid = False
Exit Function
End If
Next Index
Next RangeTestChars
AllCharsValid = True
End Function
I have the following questions:
The formula takes a range and returns a single boolean. I would prefer a vectorized function, where, given an input range, you get a corresponding range of booleans. It seems like built-in formulas like 'EXACT' can do this (those formulas where you have to press ctrl-shift-enter to execute them and where you get curly-brackets). Is there a way to do that with user-defined functions?
I am not new to programming, however I am completely new to VBA (I started literally today). Is there any obvious problem, weirdness with the above code?
Are there special characters, extremely long texts or particular input values that would cause the formula to fail?
Is there an easier way to achieve the same effect? Is the code slow?
when you start typing built-in formulas in excel you get suggestions and auto-completion. This doesn't seem to work with my formula, am I asking for too much or is it possible to achieve this?
I realize that this question contains several weakly related sub-questions, so I would be very happy also with sub-answers.
The following code will return a range of boolean values offset one column from the initial input range. Simply create a new tab in Excel and run testAllCharsValid and show the Immediate window in the IDE to see how it works.
Sub testAllCharsValid()
Dim i As Integer
Dim cll As Range, rng As Range
Dim allowedChars As String
' insert test values in sheet: for testing purposes only
With ActiveSheet ' change to Thisworkbook.Sheets("NameOfYourSheet")
Set rng = .Range("A1:A10")
For i = 1 To 10
.Cells(i, 1) = Chr(i + 92)
Next i
End With
' fill allowedChars with letters a to z: for testing purposes only
For i = 97 To 122
allowedChars = allowedChars & Chr(i)
Next i
' get boolean range
Set rng = AllCharsValid(rng, allowedChars)
' check if the returned range contains the expected boolean values
i = 0
For Each cll In rng
i = i + 1
Debug.Print i & " boolean value: " & cll.Value
Next cll
End Sub
' Check that all characters in InputCells are among
' the characters in AllowedChars
Public Function AllCharsValid(InputCells As Range, allowedChars As String) As Range
Dim BoolTest As Boolean
Dim Char As String
Dim Index As Integer
Dim RangeTestChars As Range, RangeBooleans As Range, RangeTemp As Range
Dim TestChars As String
For Each RangeTestChars In InputCells
BoolTest = True
TestChars = RangeTestChars.Value
For Index = 1 To Len(TestChars)
Char = Mid(TestChars, Index, 1)
If InStr(allowedChars, Char) = 0 Then BoolTest = False
Next Index
Set RangeTemp = RangeTestChars.Offset(0, 1) ' change offset to what suits your purpose
RangeTemp.Value = BoolTest
If RangeBooleans Is Nothing Then
Set RangeBooleans = RangeTestChars
Else
Set RangeBooleans = Union(RangeBooleans, RangeTemp)
End If
Next RangeTestChars
Set AllCharsValid = RangeBooleans
End Function
cf 2) If the length of the test string is zero, the function will return True for the cell in question, which may not be desirable.
cf 3) There is a limit to how many characters an Excel cell can contain, read more here. I suppose, if you concatenated some very long strings and sent them to the function, you could reach the integer limit of +32767, which would cause a run-time error due to the integer Index variable. However, since the character limit of Excel cells is exactly +32767, the function should work as is without any problems.
cf 4) None that I know of.
cf 5) This is not the easiest thing to achieve, but there is help to be found here.

Selecting one column from each row in a table

I have a table structured (Table Name: Table2) like below:
Using VBA, I want to select ONLY a single column value of the current row by iterating over each row.
Here is the code and I wrote:
Function findColumnValue(strColCombIdent As String, strColumnName As String) As String
On Error Resume Next
Dim strRetResult As String
Dim wsMapMasterRefSheet As Worksheet
'Referes to the table Table2.
Dim loMapMaster As ListObject
Set wsMapMasterRefSheet = ThisWorkbook.Worksheets("Sheet3")
Set loMapMaster = wsMapMasterRefSheet.ListObjects("Table2")
'All rows of the table Table2
Dim rAllRows As Range
Set rAllRows = loMapMaster.DataBodyRange
'Holds one row from the databody range for processing.
Dim rCurrRow As Range
'Process data
Dim strTemp As String
For Each rCurrRow In rAllRows
strTemp = rCurrRow.Columns(2)
Debug.Print strTemp
Next rCurrRow
findColumnValue = strRetResult
End Function
I was hoping to get results like below (ONLY the value of the column 2):
1.5
1.5
1.8
4
3
3
1
2
10
12
5
7
Instead I end up with something like this (All values from column#2 onwards, for each processing row.)
1.5
0.045150462962963
1.5
4.52083333333333E-02
1.8
4.72685185185185E-02
4
0.168090277777778
3
3.1
3
8.47800925925926E-02
1
4.16666666666667E-02
2
8.33449074074074E-02
10
10.1.1.1
12
1.3.4.5
5
0.212511574074074
7
8.54166666666667E-02
Using
strTemp = rCurrRow.Columns(1, 2)
instead of
strTemp = rCurrRow.Columns(2)
Causes runtime error 1004
Since each iteration points to a range object in the For loop; I was thinking using
rCurrRow.Columns(2)
will point to current Row's column#2 and hence print out only the column's value.
Is my logic misplaced?
One additional question:
Why does the MSDN Excel Reference guide describes Columns as a Property; where as clearly the "Columns" usage clearly takes parameters
Here is the link I referred:
http://msdn.microsoft.com/en-us/library/office/ff197454(v=office.15).aspx
Either specify you want to iterate rows:
For Each rCurrRow In rAllRows.Rows
or only look at the ListRows in the first place:
Function findColumnValue(strColCombIdent As String, strColumnName As String) As String
On Error Resume Next
Dim strRetResult As String
Dim wsMapMasterRefSheet As Worksheet
'Referes to the table Table2.
Dim loMapMaster As ListObject
Set wsMapMasterRefSheet = ThisWorkbook.Worksheets("Sheet3")
Set loMapMaster = wsMapMasterRefSheet.ListObjects("Table2")
'All rows of the table Table2
Dim rAllRows As ListRows
Set rAllRows = loMapMaster.ListRows
'Holds one row from the databody range for processing.
Dim rCurrRow As ListRow
'Process data
Dim strTemp As String
For Each rCurrRow In rAllRows
strTemp = rCurrRow.Range(, 2)
Debug.Print strTemp
Next rCurrRow
findColumnValue = strRetResult
End Function
You can call your variable rCurrRow all you want; VBA still won't know that you mean for it to contain an entire row of range rAllRows. It just assumes that rCurrRow represents one cell, such that For Each rCurrRow In rAllRows means "for each individual cell in this range".
What you need to do is limit the range being looped through. This should work; not tested.
For Each rCurrRow In rAllRows.Columns(2)
strTemp = rCurrRow
Debug.Print strTemp
Next rCurrRow
In fact I wouldn't call that variable rCurrRow at all; if you're going to use it in this way, call it e.g. cell instead.
EDIT: now that you have clarified your question in a comment below, you could do this:
For i = 1 To rAllRows.Rows.Count
Set rCurrRow = rAllRows.Rows(i)
strTemp = rCurrRow.Cells(1,2)
Debug.Print strTemp
Next i
But even better and faster would be to load the entire range to a two-dimensional Variant array at once, and loop over that array — much faster than looping over many cells.
Dim v As Variant
v = rAllRows ' load entire range to a 2D array
For i = 1 To UBound(v,1)
strTemp = v(i,2)
Debug.Print strTemp
Next i
Why does the MSDN Excel Reference guide describes Columns as a Property; where as clearly the "Columns" usage clearly takes parameters
Both methods and properties can take parameters. The distinction is more or less as follows:
Properties are things that you can get (like a range's Address, which takes no parameter, or subrange such as Column or Row or Cells, which do) and/or set (like a range's .Interior.Color, or .Hidden status). They are usually nouns.
Methods are things that do something to/with the range, and as such are usually verbs. Like .Select (takes no parameters) or .Copy (takes one parameter) or even .Speak.

In Excel 2010, how could I remove duplicates and concatenate values within a cell range that includes multiple values cells?

I made a document in Excel 2010 however, the functionality I'm hoping to get from it doesn't seem to be possible (at least not with the default Excel functions) and I don't know enough about VB programming to make my own UDF. (I'm actually using one I found online which does part of what I want, but doesn't meet all of my needs.)
Let me break it down:
I have multiple sheets with groups of fields where users can add numbers (some will be blank, some will contain a single number, some will contain multiple comma-separated numbers)
I have an "Overview" sheet where I want to Concatenate those numbers (and remove any duplicates) within a few different sections (only looking at specific field groups).
I found a ConcatIf UDF that works fairly well for this, however it can't handle non-consecutive cells to concatenate (For example, I want to concatenate and remove duplicates from cells D30, G30, J30 and M30 together) (Here's the UDF:)
Function ConcatIf(ByVal compareRange As Range, ByVal xCriteria As Variant, Optional ByVal stringsRange As Range, _
Optional Delimiter As String, Optional NoDuplicates As Boolean) As String
Dim i As Long, j As Long
With compareRange.Parent
Set compareRange = Application.Intersect(compareRange, Range(.UsedRange, .Range("a1")))
End With
If compareRange Is Nothing Then Exit Function
If stringsRange Is Nothing Then Set stringsRange = compareRange
Set stringsRange = compareRange.Offset(stringsRange.Row - compareRange.Row, _
stringsRange.Column - compareRange.Column)
For i = 1 To compareRange.Rows.Count
For j = 1 To compareRange.Columns.Count
If (Application.CountIf(compareRange.Cells(i, j), xCriteria) = 1) Then
If InStr(ConcatIf, Delimiter & CStr(stringsRange.Cells(i, j))) <> 0 Imp Not (NoDuplicates) Then
ConcatIf = ConcatIf & Delimiter & CStr(stringsRange.Cells(i, j))
End If
End If
Next j
Next i
ConcatIf = mid(ConcatIf, Len(Delimiter) + 1)
End Function
It also can't handle the "multiple numbers in one cell" as separate numbers.
Is there a way to make a Concatenate UDF that "parses" the cells it's looking at to look for duplicates between the multiple numbers cells and the single numbers cells, and then output the result? Preferably allowing it to take a series of non-consecutive cells to work on (across different sheets).
Sorry if the explanation is a bit convoluted, it's my first time asking for this kind of help. :x
Here's an example:
If I have cells with:
2,4,6
2,6
2
4
6
6,8
I'd want to be able to simply get:
2,4,6,8
Right now, instead, I'd get:
2,4,6,2,6,6,8
Try the below. You can adapt it appropriately if you need to change the delimiter etc. I have documented what it is doing and why.
Example formula: =blah(A1:A7,A8,C9) (it can also be called from code)
Example output: 2,4,6,8
Public Function Blah(ParamArray args()) As String
'Declarations
Dim uniqueParts As Collection
Dim area As Range
Dim arg, arr, ele, part
Dim i As Long
'Initialisations
Set uniqueParts = New Collection
'Enumerate through the arguments passed to this function
For Each arg In args
If TypeOf arg Is Range Then 'range so we need to enumerate its .Areas
For Each area In arg.Areas
arr = area.Value 'for large ranges it is greatly quicker to load the data at once rather than enumerating each cell in turn
For Each ele In arr 'enumerate the array
addParts CStr(ele), uniqueParts 'Call our sub to parse the data
Next ele
Next area
ElseIf VarType(arg) > vbArray Then 'an array has been passed in
For Each ele In arg 'enumerate the array
addParts CStr(ele), uniqueParts 'Call our sub to parse the data
Next ele
Else 'assume can be validly converted to a string. If it cannot then it will fail fast (as intended)
addParts CStr(arg), uniqueParts 'Call our sub to parse the data
End If
Next arg
'process our results
If uniqueParts.Count > 0 Then
ReDim arr(0 To uniqueParts.Count - 1)
For i = 1 To uniqueParts.Count
arr(i - 1) = uniqueParts(i)
Next i
'we now have an array of the unique parts, which we glue together using the Join function, and then return it
Blah = Join(arr, ",")
End If
End Function
'Sub to parse the data. In this case the sub splits the string and adds the split elements to a collection, ignoring duplicates
Private Sub addParts(partsString As String, ByRef outputC As Collection)
'ByRef is unecessary but I use it to document that outputC must be instantiated
Dim part
For Each part In Split(partsString, ",")
On Error Resume Next 'existing same key will raise an error, so we skip it and just carry on
outputC.Add part, part
On Error GoTo 0
Next part
End Sub

pulling out data from a colums in Excel

I have the following Data in Excel.
CHM0123456 SRM0123:01
CHM0123456 SRM0123:02
CHM0123456 SRM0256:12
CHM0123456 SRM0123:03
CHM0123457 SRM0789:01
CHM0123457 SRM0789:02
CHM0123457 SRM0789:03
CHM0123457 SRM0789:04
What I need to do is pull out all the relevent SRM numbers that relate to a single CHM ref. now I have a formular that will do some thing like this
=INDEX($C$2:$C$6, SMALL(IF($B$8=$B$2:$B$6, ROW($B$2:$B$6)-MIN(ROW($B$2:$B$6))+1, ""), ROW(A1)))
however this is a bit untidy and I really want to produce this same using short vb script, do i jsut have to right a loop that will run though and check each row in turn.
For x = 1 to 6555
if Ax = Chm123456
string = string + Bx
else
next
which should give me a final string of
SRM0123:01,SRM123:02,SRM0256:12,SRM0123:03
to use with how i want.
Or is ther a neater way to do this ?
Cheers
Aaron
my current code
For x = 2 To 6555
If Cells(x, 1).Value = "CHM0123456" Then
outstring = outstring + vbCr + Cells(x, 2).Value
End If
Next
MsgBox (outstring)
End Function
I'm not sure what your definition of 'neat' is, but here is a VBA function that I consider very neat and also flexible and it's lightning fast (10k+ entires with no lag). You pass it the CHM you want to look for, then the range to look in. You can pass a third optional paramater to set how each entry is seperated. So in your case you could write (assuming your list is :
=ListUnique(B2, B2:B6555)
You can also use Char(10) as the third parameter to seperat by line breaks, etc.
Function ListUnique(ByVal search_text As String, _
ByVal cell_range As range, _
Optional seperator As String = ", ") As String
Application.ScreenUpdating = False
Dim result As String
Dim i as Long
Dim cell As range
Dim keys As Variant
Dim dict As Object
Set dict = CreateObject("scripting.dictionary")
On Error Resume Next
For Each cell In cell_range
If cell.Value = search_text Then
dict.Add cell.Offset(, 1).Value, 1
End If
Next
keys = dict.keys
For i = 0 To UBound(keys)
result = result & (seperator & keys(i))
Next
If Len(result) <> 0 Then
result = Right$(result, (Len(result) - Len(seperator)))
End If
ListUnique = result
Application.ScreenUpdating = True
End Function
How it works: It simple loops through your range looking for the search_string you give it. If it finds it, it adds it to a dictionary object (which will eliminate all dupes). You dump the results in an array then create a string out of them. Technically you can just pass it "B:B" as the search array if you aren't sure where the end of the column is and this function will still work just fine (1/5th of a second for scanning every cell in column B with 1000 unique hits returned).
Another solution would be to do an advancedfilter for Chm123456 and then you could copy those to another range. If you get them in a string array you can use the built-in excel function Join(saString, ",") (only works with string arrays).
Not actual code for you but it points you in a possible direction that can be helpful.
OK, this might be pretty fast for a ton of data. Grabbing the data for each cell takes a ton of time, it is better to grab it all at once. The the unique to paste and then grab the data using
vData=rUnique
where vData is a variant and rUnique is the is the copied cells. This might actually be faster than grabbing each data point point by point (excel internally can copy and paste extremely fast). Another option would be to grab the unique data without having the copy and past happen, here's how:
dim i as long
dim runique as range, reach as range
dim sData as string
dim vdata as variant
set runique=advancedfilter(...) 'Filter in place
set runique=runique.specialcells(xlCellTypeVisible)
for each reach in runique.areas
vdata=reach
for i=lbound(vdata) to ubound(vdata)
sdata=sdata & vdata(i,1)
next l
next reach
Personally, I would prefer the internal copy paste then you could go through each sheet and then grab the data at the very end (this would be pretty fast, faster than looping through each cell). So going through each sheet.
dim wks as worksheet
for each wks in Activeworkbook.Worksheets
if wks.name <> "CopiedToWorksheet" then
advancedfilter(...) 'Copy to bottom of list, so you'll need code for that
end if
next wks
vdata=activeworkbook.sheets("CopiedToWorksheet").usedrange
sData=vdata(1,1)
for i=lbound(vdata) + 1 to ubound(vdata)
sData=sData & ","
next i
The above code should be blazing fast. I don't think you can use Join on a variant, but you could always attempt it, that would make it even faster. You could also try application.worksheetfunctions.contat (or whatever the contatenate function is) to combine the results and then just grab the final result.
On Error Resume Next
wks.ShowAllData
On Error GoTo 0
wks.UsedRange.Rows.Hidden = False
wks.UsedRange.Columns.Hidden = False
rFilterLocation.ClearContents