Why is my User defined Function not showing results VBA - excel

I have created a UDF called YRatio(Str). When I use the insert formula for a cell the result appears on the dialog box but inside the cell it shows "=YRatio(C2)"
The result I get is right. But it doesn't appear in the cells.
Function YRatio(CellRef As String) As String
Dim Arr() As String
Dim tot As Integer
Dim yyes As Integer
Dim Str As String
Str = CellRef '.Value
Arr() = Split(Str, Chr(10))
For Each Line In Arr
tot = tot + 1
If Left(Line, 1) = "Y" Then
yyes = yyes + 1
End If
Next
YRatio = CStr(yyes) & "/" & CStr(tot)
End Function

inside the cell it shows "=YRatio(C2)"
You need to change the formatting of the cell to General. Currently it is formatted as Text
Also Arr() = Split(Str, Chr(10)) in your code should be Arr() = Split(Range(Str).Value, Chr(10)).
Imp Tip: Avoid using LINE and STR as variables. Use more meaningful names.
Your code can be written as
Option Explicit
Function YRatio(CellRef As Range) As String
Dim Arr() As String
Dim tot As Long
Dim yyes As Long
Dim itm As Variant
Arr() = Split(CellRef.Value2, Chr(10))
For Each itm In Arr
tot = tot + 1
If Left(itm, 1) = "Y" Then yyes = yyes + 1
Next
YRatio = yyes & "/" & tot
End Function
Here I am passing the cell as Range.
Now you can type in the cell =YRatio(C2) which is formatted as General and it will work.
You can use use this version of the code which uses UBound(Arr) to get the total elements of the array rather then using tot = tot + 1
Option Explicit
Function YRatio(CellRef As Range) As String
Dim Arr() As String
Dim yyes As Long
Dim itm As Variant
Arr() = Split(CellRef.Value2, Chr(10))
For Each itm In Arr
If Left(itm, 1) = "Y" Then yyes = yyes + 1
Next
YRatio = yyes & "/" & UBound(Arr)
End Function

Related

Paste Mulitple cell values into a single cell

I'm trying to copy the values of a range of cells(A1:A50) into a single cell (B1). I can do it manually by copying the cells to the clipboard and then pasting the clipboard into the formuala bar of B1 but I can't find a way of doing this in a macro other than getting the cells copied to the clipboard.
Hopefully someone can help me out here.
Sheet1.Range("A1:A50").SpecialCells(xlCellTypeConstants).Select
Selection.Copy
I would like the contents of cell B1 to look something like this:
Value of cell A1
Value of cell A2
Value of cell A3
...and so on
Just
Sub myConcat(rSource As Range, rTarget As Range, Optional sDelimiter = vbCrLf)
Dim oCell As Range
Dim sRes As String
sRes = vbNullString
For Each oCell In rSource
sRes = sRes & sDelimiter & oCell.Text
Next oCell
rTarget.Value = Right(sRes, Len(sRes) - Len(sDelimiter))
End Sub
Call it from your code like as
Sub tst_myConcat()
Call myConcat([A1:A50], [B1])
End Sub
Of course, this procedure can be easily converted to a function:
Function myConcat(rSource As Range, Optional sDelimiter = vbCrLf)
Dim oCell As Range
Dim sRes As String
sRes = vbNullString
For Each oCell In rSource
sRes = sRes & sDelimiter & oCell.Text
Next oCell
myConcat = Right(sRes, Len(sRes) - Len(sDelimiter))
End Function
In this case, just write in the target cell (B1) =myConcat(A1:A50)
Do not forget to include in the cell format Wrap text!
First Column To String
The FirstColumnToString function (UDF) has a fixed delimiter (Delimiter) which can manually be changed. But it can e.g. do the following:
=FirstColumnToString(A1:A2,A4,A6:C8,Sheet2!A1:A3)
where it will discard error values and zero-length strings ("") and choose only values from the first column of each range e.g. in range A6:C8 it will choose the values from A6:A8.
The Code
Option Explicit
Function FirstColumnToString(ParamArray SourceRanges() As Variant) _
As String
Const Delimiter As String = vbLf & vbLf
Dim RangesCount As Long
RangesCount = UBound(SourceRanges) - LBound(SourceRanges) + 1
Dim data As Variant
ReDim data(1 To RangesCount)
Dim Help As Variant
ReDim Help(1 To 1, 1 To 1)
Dim Element As Variant
Dim RowsCount As Long
Dim j As Long
For Each Element In SourceRanges
j = j + 1
If Element.Rows.Count > 1 Then
data(j) = Element.Columns(1).Value
Else
data(j) = Help
data(j)(1, 1) = Element.Columns(1).Value
End If
RowsCount = RowsCount + UBound(data(j))
Next Element
Dim Result As Variant
ReDim Result(1 To RowsCount)
Dim Current As Variant
Dim i As Long
Dim k As Long
For j = 1 To RangesCount
For i = 1 To UBound(data(j))
Current = data(j)(i, 1)
If Not IsError(Current) Then
If Current <> vbNullString Then
k = k + 1
Result(k) = Current
End If
End If
Next i
Next j
ReDim Preserve Result(1 To k)
FirstColumnToString = Join(Result, Delimiter)
End Function
A much simpler way of doing the job is to use the TREXTJOIN function in Excel:
With Sheet2.Range("A1:A50")
.AutoFilter Field:=1, Criteria1:="<>"
Sheet2.Range("B1").Value2 = WorksheetFunction.TextJoin(vbCrLf, True, _
.SpecialCells(xlCellTypeVisible))
.AutoFilter
End With

VBA counting substrings in a string across a range

I have been working on a worksheet in Excel and im trying to get accurate number of sub strings within a string across a range. Im using columns C and D that have text them im adding the following code to get a number of the occurrences of particular words and total them in column H by using a public function. I cannot get the code to span a range and bring back the answer. Maybe there is a better way.?
Option Compare Text
Function CountString(FullString As String, PartialString As String) As Integer
Dim cnt As Integer
cnt = 0
For i = 1 To Len(FullString)
If Mid(FullString, i, Len(PartialString)) = PartialString Then
cnt = cnt + 1
End If
Next i
CountString = cnt
End Function
A = "CatDogCatDogCatDogCatDogCatDogCatDogCatDogCatDogCatDogCatDog"
Msgbox UBound(Split(A, "Dog"))
This counts how many times dog appears in the string by using dog as a delimiter then counting how many elements in the array.
edit
Application of technique for OP:
Function CountString(FullString As String, PartialString As String) As Integer
CountString = UBound(Split(FullString, PartialString))
End Function
Edit 2
Set regEx = New RegExp
A="DogCatDogCatDogCatDogCatDogCatDogCatDogCatDogCatDogCatDogCat"
regEx.Pattern = "dog"
regEx.IgnoreCase = True
regEx.Global = True
Set MyMatches = regEx.Execute(A)
Msgbox MyMatches.Count
This counts the delimiter.
You have .test which returns true/false, .Execute which returns a collections of matches, and .replace which has many uses including replace and extracting parts of files.
Instead of your loop, I would use one single statement like
Function CountString(FullString As String, PartialString As String) As Integer
CountString = (Len(FullString) - Len(Replace(FullString, PartialString, ""))) / Len(PartialString)
End Function
(from here)
All the credit for the next code must go to #Mark. But, if on some installations the code returns wrong, please use the next variant:
Function CountString(FullString As String, PartialString As String) As Long
CountString = UBound(Split(FullString, PartialString))
If UBound(Split("x", "x")) = 2 Then CountString = CountString - 1
End Function
The way or function using to count total occurrences in a range, would be the next:
Sub countStringsInRange()
Dim sh As Worksheet, rng As Range, TotCount As Long, cel As Range, strSearch As String
strSearch = "Dog"
Set sh = ActiveSheet ' use here your sheet
Set rng = sh.Range("C8:D8") 'use here whatever range you need
For Each cel In rng
TotCount = TotCount + CountString(cel.Value, strSearch)
Next
Debug.Print TotCount
End Sub
After editing:
The next function is able to also process arrays (it works for strings, too):
Function CountStringArr(FullString As String, PartialString As Variant) As Long
Dim El As Variant, iCount As Long
If IsArray(PartialString) Then
For Each El In PartialString
iCount = iCount + UBound(Split(FullString, El))
Next
CountStringArr = iCount
Else
CountStringArr = UBound(Split(FullString, PartialString))
End If
End Function
It can be called as in the next example:
Dim x As String
x = "CatDogCatDogCatDogCatDogCatDogCatDogCatDogCatDogCatDogCatDog"
Debug.Print CountStringArr(x, Array("Dog", "Cat")) 'it returns 20
Debug.Print CountStringArr(x, "Dog") 'it returns 10
And it can be called from a range in the next way:
Sub countStringsInRangeBis()
Dim sh As Worksheet, rng As Range, TotCount As Long, cel As Range, strSearch As Variant
strSearch = Split("Dog,Cat", ",") 'or Array("Dog", "Cat")
Set sh = ActiveSheet ' use here your sheet
Set rng = sh.Range("C8:D8") 'use here whatever range you need
For Each cel In rng
TotCount = TotCount + CountStringArr(cel.Value, strSearch)
Next
Debug.Print TotCount
End Sub
Count Substrings (In a Range)
Function countString(SourceString As Range, _
ByVal SubString As String, _
Optional ByVal ignoreCase As Boolean = False) _
As Long
Dim Data As Variant, Curr As Variant
Dim i As Long, j As Long, Result As Long, iCase As Long
If ignoreCase Then iCase = 1
Data = SourceString.Value
If IsArray(Data) Then
GoSub CaseArray
Else
GoSub CaseValue
End If
countString = Result
Exit Function
CaseArray:
For i = 1 To UBound(Data)
For j = 1 To UBound(Data, 2)
Curr = Data(i, j): GoSub countValue
Next j
Next i
Return
CaseValue:
Curr = Data: GoSub countValue
Return
countValue:
If Not IsError(Curr) Then
Result = Result + UBound(Split(Curr, SubString, , iCase))
' Result = Result + (Len(Curr) _
- Len(Replace(Curr, SubString, "", , , iCase))) / Len(SubString)
End If
Return
End Function

How do I turn a validation list from a cell into a list with VBA

I have a sheet that I need to paste data to according to the validation lists in those sheets. In the sheet, there are many columns each with their own data validation list - some are written directly as "yes;no" others are references "='$$VALUES$$'!$IJ$1:$IJ$12".
What I need is to find a way to add each item in each list to an array. Using the code below I could find the references above.
Debug.Print Cells(2, 6).Validation.Formula1
Is there any elegant way to store the output as a list containing each valid input. My only idea so far is to first check which type of output I get, and then if it is the list form of "yes;no" then look for the number of ; and then split it item by item. And in case its the sheet range reference split it by sheet and range and convert that range to an array.
Something like this, will do it. I'd set a range rather than using activecell, and also check validation is present to reduce your errors.
Sub get_val_lists()
Dim arrOutput() As Variant
If Left(ActiveCell.Validation.Formula1, 1) <> "=" Then
arrOutput = Split(ActiveCell.Validation.Formula1, ",")
Else
arrOutput = Application.Transpose( _
Range(Mid(ActiveCell.Validation.Formula1, 2)).value)
End If
End Sub
I was a bit pressed for time so I ended up doing an inelegant solution myself. Posting it here in case somebody else runs into the same problem.
Sub ValidList()
Dim strFormula As String
Dim intLastSemi As Integer
Dim intCurSemi As Integer
Dim intSemi As Integer
Dim aryList() As Variant
Dim intLen As Integer
Dim blnCont As Boolean
Dim strSheet As String
Dim strRange As String
Dim intSplit As Integer
Dim ws As Worksheet
Dim rng As Range
Dim e As Variant
Dim Row As Integer
Dim Col As Integer
'This is just an example, turning it into a fucntion based on row and col later
'so now my test validation list is just in A1
Row = 1
Col = 1
strFormula = Cells(Row, Col).Validation.Formula1
intLen = Len(strFormula)
If InStr(1, strFormula, "=") Then 'Sheet reference
intSplit = InStr(1, strFormula, "!")
strSheet = Right(Left(strFormula, intSplit - 1), intLen - intSplit - 3)
strRange = Right(strFormula, intLen - intSplit)
Set ws = Worksheets(strSheet)
Set rng = ws.Range(strRange)
aryList() = rng
ElseIf Not InStr(1, strFormula, ";") Then 'Hardcoded list
intSemi = 0
intLastSemi = 0
blnCont = True
While blnCont
intCurSemi = InStr(intLastSemi + 1, strFormula, ";")
If intCurSemi <> 0 Then
intSemi = intSemi + 1
ReDim Preserve aryList(intSemi)
aryList(intSemi) = Right(Left(strFormula, intCurSemi - 1), intCurSemi - intLastSemi - 1)
intLastSemi = intCurSemi
ElseIf intCurSemi = 0 Then
intSemi = intSemi + 1
ReDim Preserve aryList(intSemi)
aryList(intSemi) = Right((strFormula), intLen - intLastSemi)
blnCont = False
End If
Wend
End If
'For my attempt at passing the array to a function
'For Each e In aryList
' MsgBox e
'Next
'ReDim ValidList(UBound(aryList))
'ValidList = aryList
End Sub

How to Split text and loop to next row

I'm using the split function to split text using spaces. I have gotten my macro to split the text but I am having difficulties getting the loop to move to the next row below to split.
Sub Split_Text_Test1()
Dim Txt As String
Dim i As Integer
Dim FullName As Variant
Range("A1").Select
Txt = ActiveCell.Value
FullName = Split(Txt, " ")
For i = 0 To UBound(FullName)
Cells(1, i + 1).Value = FullName(i)
Next i
End Sub
You probably need to change the bit inside your loop thus as you are starting at A1. This assumes you want the entries in A2 and down. Not generally advisable to use Select/Activate, not very robust.
Edited to move across columns rather than down rows.
For i = 0 To UBound(FullName)
Range("A1").Offset(,i + 1).Value = FullName(i)
Next i
That said, you can avoid a loop altogether and use
Range("B1").Resize(, UBound(FullName) + 1).Value = FullName
In this case I would use a loop (and your solution was not that far from this):
Dim Txt As String
Dim i As Integer
Dim FullName As Variant
Dim R As Integer, C As Integer, MaxR as Integer
C = 1 ' can be another loop as well
For R = 1 to 1000
Txt = Trim(Cells(r,1).Value) ' will remove space from start and end
FullName = Split(Txt, " ")
For i = 0 To UBound(FullName)
Cells(R , C + 1 + i ).Value = FullName(i)
Next i
Next R
Sub Split_Text_Test1()
Dim Txt As String
Dim i As Integer
Dim FullName As Variant
Dim R As Integer, C As Integer
Range("A1").Select ' assumes that the cells below that are empty
Txt = ActiveCell.Value
FullName = Split(Txt, " ")
R = ActiveCell.Row
C = ActiveCell.Column
For i = 0 To UBound(FullName)
Cells(R + 1 + i, C).Value = FullName(i)
Next i
End Sub
I added few thing to your code, see if that serves your purpose. However, as SJR said Text to columns option in Data menu would do the same with less effort.
Sub Split_Text_Test1()
Dim Txt As String
Dim i As Integer
Dim FullName As Variant
Dim lastRow As Long
Dim myRange As Range
With ActiveSheet.UsedRange
lastRow = .Rows(.Rows.Count).Row
End With
Debug.Print lastRow
'Range("A1").Select
Set myRange = ActiveSheet.Range("A1", "A" & lastRow)
For Each cell In myRange
Txt = cell.Value
FullName = Split(Txt, " ")
For i = 0 To UBound(FullName)
Cells(cell.Row, i + 1).Value = FullName(i)
Next i
Next cell
End Sub

Match partial text string (90%) two column in two different sheet

I'm trying to match (90%) partial text string from a sheet column to another sheet column and bring end result to the master sheet column.
I found a VBA solution but I have some problems with that.
1) it's matching exact text
2) finding a problem to match two different sheet columns.
Please help me to sort this out.
Sub lookup()
Dim TotalRows As Long
Dim rng As Range
Dim i As Long
'Copy lookup values from sheet1 to sheet3
Sheets("BANK STATEMENT ENTRY").Select
TotalRows = ActiveSheet.UsedRange.Rows.Count
Range("F3:F" & TotalRows).Copy Destination:=Sheets("TEST").Range("A1")
'Go to the destination sheet
Sheets("TEST").Select
For i = 1 To TotalRows
'Search for the value on sheet2
Set rng = Sheets("INFO").UsedRange.Find(Cells(i, 1).Value)
'If it is found put its value on the destination sheet
If Not rng Is Nothing Then
Cells(i, 2).Value = rng.Value
End If
Next
End Sub
I have done a text mining project and I know you cannot use that approach, you have to break the strings into substrings and then analyze them. It will be a whole project, but you are lucky since I did it for you.
Let's simplify the problem and say that you have two ranges of strings and you want to find every similar strings between two groups. Also, you want to have a tolerance to minimize the matching pairs.
Assume ABCDE and 12BCD00. They have B, C, D, BC, CD and BCD in common. So the longest common substring is BCD which is 3 characters: 3/length of ABCDE(5) will be 60% similarity with the first string and 3/7=43% similarity. So if you can get a list of all those common substrings among all the strings in two ranges you can come up with a better list to filter and get what you want.
I wrote a bunch of functions. To use it easily, just copy and paste both groups of strings in one sheet and generate the final report on the same sheet too to understand how it works.
Function FuzzyFind, finds all of the common substrings and gives you 1st string from Group1/range1, 2nd string from group2/range2, common substring and percentages of similiarity for both strings. The good thing is you can tell the function how small you want your substrings e.g. in the previous example, if you say iMinCommonSubLength=3, it will only give you BCD, if you say iMinCommonSubLength=2 it will give you BC, CD and BCD and so on.
Use function Main. I also included a Test sub.
Functions:
Sub TestIt()
Call Main(ActiveSheet.Range("A1:A10"), ActiveSheet.Range("B1:B10"), 4, ActiveSheet.Range("D1"))
End Sub
Sub Main(rng1 As Range, rng2 As Range, iMinCommonSubLength As Integer, Optional rngReportUpperLeftCell As Range)
Dim arr() As Variant
Dim rngReport As Range
If rngReport Is Nothing Then Set rngReport = ActiveSheet.Range("A1")
arr = FuzzyFind(rng1, rng2, iMinCommonSubLength)
Set rngReport = rngReportUpperLeftCell.Resize(UBound(arr, 1), UBound(arr, 2))
rngReport.Value = arr
rngReport.Columns(1).NumberFormat = "#"
rngReport.Columns(2).NumberFormat = "#"
rngReport.Columns(3).NumberFormat = "#"
rngReport.Columns(4).NumberFormat = "0%"
rngReport.Columns(5).NumberFormat = "0%"
End Sub
Function GetCharacters(str As String) As Variant
Dim arr() As String
ReDim arr(Len(str) - 1)
For i = 1 To Len(str)
arr(i - 1) = Mid$(UCase(str), i, 1)
Next
GetCharacters = arr
End Function
Function GetIterations(iStringLength As Integer, iSubStringLength As Integer) As Integer
If iStringLength >= iSubStringLength Then
GetIterations = iStringLength - iSubStringLength + 1
Else
GetIterations = 0
End If
End Function
Function GetSubtrings(str As String, iSubLength As Integer) As Variant
Dim i As Integer
Dim count As Integer
Dim arr() As Variant
count = GetIterations(Len(str), iSubLength)
ReDim arr(1 To count)
For i = 1 To count
arr(i) = Mid(str, i, iSubLength)
Next i
GetSubtrings = arr()
End Function
Function GetLongestCommonSubStrings(str1 As String, str2 As String, iMinCommonSubLeng As Integer)
Dim i As Integer
Dim iLongestPossible As Integer
Dim iShortest As Integer
Dim arrSubs() As Variant
Dim arr1() As Variant
Dim arr2() As Variant
ReDim arrSubs(1 To 1)
'Longest possible common substring length is the smaller string's length
iLongestPossible = IIf(Len(str1) > Len(str2), Len(str2), Len(str1))
If iLongestPossible < iMinCommonSubLeng Then
'MsgBox "Minimum common substring length is larger than the shortest string." & _
' " You have to choose a smaller common length", , "Error"
Else
'We will try to find the first match of common substrings of two given strings, exit after the first match
For i = iLongestPossible To iMinCommonSubLeng Step -1
arr1 = GetSubtrings(str1, i)
arr2 = GetSubtrings(str2, i)
ReDim arrSubs(1 To 1)
arrSubs = GetCommonElement(arr1, arr2)
If arrSubs(1) <> "" Then Exit For 'if you want JUST THE LONGEST MATCH, comment out this line
Next i
End If
GetLongestCommonSubStrings = arrSubs
End Function
Function GetCommonElement(arr1() As Variant, arr2() As Variant) As Variant
Dim i As Integer
Dim j As Integer
Dim count As Integer
Dim arr() As Variant
count = 1
ReDim arr(1 To count)
For i = 1 To UBound(arr1)
For j = 1 To UBound(arr2)
If arr1(i) = arr2(j) Then
ReDim Preserve arr(1 To count)
arr(count) = arr1(i)
count = count + 1
End If
Next j
Next i
GetCommonElement = arr
End Function
Function FuzzyFind(rng1 As Range, rng2 As Range, iMinCommonSubLength As Integer) As Variant
Dim count As Integer
Dim i As Integer
Dim arrSubs As Variant
Dim str1 As String
Dim str2 As String
Dim cell1 As Range
Dim cell2 As Range
Dim rngReport As Range
Dim arr() As Variant 'array of all cells that are partially matching, str1, str2, common string, percentage
count = 1
ReDim arr(1 To 5, 1 To count)
For Each cell1 In rng1
str1 = UCase(CStr(cell1.Value))
If str1 <> "" Then
For Each cell2 In rng2
str2 = UCase(CStr(cell2.Value))
If str2 <> "" Then
ReDim arrSubs(1 To 1)
arrSubs = GetLongestCommonSubStrings(str1, str2, iMinCommonSubLength)
If arrSubs(1) <> "" Then
For i = 1 To UBound(arrSubs)
arr(1, count) = cell1.Value
arr(2, count) = cell2.Value
arr(3, count) = arrSubs(i)
arr(4, count) = Len(arrSubs(i)) / Len(str1)
arr(5, count) = Len(arrSubs(i)) / Len(str2)
count = count + 1
ReDim Preserve arr(1 To 5, 1 To count)
Next i
End If
End If
Next cell2
End If
Next cell1
FuzzyFind = TransposeArray(arr)
End Function
Function TransposeArray(arr As Variant) As Variant
Dim arrTemp() As Variant
ReDim arrTemp(LBound(arr, 2) To UBound(arr, 2), LBound(arr, 1) To UBound(arr, 1))
For a = LBound(arr, 2) To UBound(arr, 2)
For b = LBound(arr, 1) To UBound(arr, 1)
arrTemp(a, b) = arr(b, a)
Next b
Next a
TransposeArray = arrTemp
End Function
Don't forget to clear the sheet before generating new reports. Insert a table and use its autofilter to easily filter your stuff.
last but not least, don't forget to click on the check mark to announce this as the answer to your question.

Resources