Parsing a function for its arguments - vba

I have a UDF in written in VBA, which I call from my sheet. The function takes 3 arguments: Function CONCATIF(arg1 As Range, arg2 As Boolean, Optional arg3 As Range) As Variant
The UDF needs to know the formula of arg2, i.e. to intercept arg2 before it has been evaluated to TRUE or FALSE. To do this I use Application.Caller.Formula, which gives me (in its simplest form) "=CONCATIF(arg1, arg2, arg3)" (or in place of ,arg3) either ) or,))
I can then get arg 2 by simply using Split(Application.Caller.Formula, ",")
There are several problems I'd like to deal with though
Splitting at commas means that none of my arguments can contain commas, which they may have to
The formula may be nested, e.g =SUM(1,IF(CONCATIF(arg1, arg2, arg3)="a",1,0)), so I don't know which item of my split array is arg2. (I think this should be fairly easy to fix: find CONCATIF in the string and chop off the start, count the open/close brackets which follow it until open = close, and then chop off the end.
The arguments may be formulae themselves; arg1 could be a reference to a range, not an actual range.
Tricky: CONCATIF may come up multiple times in 1 formula, but with standard string searches I'll always pick up the first one (I may just have to return an error if there are multiple in 1 formula, as I can't think how to get around this at all)
So what I want: A generic way of obtaining the correct CONTCATIF() formula from the caller cell, and then parsing out the three arguments as 3 strings in an array. For reference, here's my code (sorry, naming is a little different from question)
Public Function CONCATIF(checkRange As Range, testFunction As Boolean, Optional concatRange As Range) As Variant
Dim concatArray() As Variant
Dim formulaText As String, formulaParts() As String, formulaTest As String
Dim topLeft As Range, subCell As Range
Dim newTest As String
Dim results() As Boolean, result As Boolean
Dim loopval As Long
'''
'input checking
'''
If concatRange Is Nothing Then
concatArray = checkRange
ElseIf Not (checkRange.Cells.Count = concatRange.Cells.Count And checkRange.Rows.Count = concatRange.Rows.Count And checkRange.Rows.Count = 1) Then
CONCATIF = CVErr(xlErrValue)
Exit Function
Else
concatArray = concatRange.Value2
End If
'''
'Extract test function
'''
formulaText = Application.Caller.Formula
formulaParts = Split(formulaText, ",") 'Assumes 1)no commas 2) formula isn't nested 3) formula doesn't contain nested functions
formulaTest = formulaParts(1) 'get the test function as a string to be evaluated
Set topLeft = checkRange.Cells(1, 1) 'This is the 'reference' cell - substitute each of the values in the check range for this to test each one
ReDim results(0)
On Error GoTo Err
'''
'Run test on each of the cells in checkRange
'''
For Each subCell In checkRange
newTest = Replace(formulaTest, topLeft.Address(0, 0), subCell.Address)
If Count(newTest, "(") < Count(newTest, ")") Then 'when optional parameter is missed out, sometimes you get a ,) and sometimes a ) after formulaTest, so we must check
newTest = Left(newTest, Len(newTest) - 1)
End If
result = (Evaluate(newTest))
skip:
results(UBound(results)) = result
ReDim Preserve results(UBound(results) + 1)
Next subCell
'''
'Then use array of Booleans for UDF function
'''
CONCATIF = "test"
Exit Function
Err:
result = False 'if the evaluate results in an error, it means the input was invalid, so probably won't meet the criteria, therefore can be ignored
loopval = loopval + 1
If loopval > checkRange.Cells.Count Then CONCATIF = CVErr(xlErrNA): Exit Function 'exit error loop gracefully if I've missed some edge case
Resume skip
End Function
And then referenced in my UDF is this:
Function Count(str As String, chr As String) As Long 'counts the number of instances of a character in a string
Count = Len(str) - Len(Replace(str, chr, ""))
End Function

If you had a proper formula parser you could solve all of these problems except the one of handling multiple calls to CONCATIF in a single formula: I don't know of a way of finding out 100% which instance of CONCATIF is currently being called.
There are various Formula parsers around you could maybe adapt: start here
http://ewbi.blogs.com/develops/2004/12/excel_formula_p.html

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.

Excell cell value is not read as Number?

I am trying to add the data in the two cells of the excel sheet but even if the excel cell is of the type number it does not add up the cells. It seems that there is space infornt of the number that it does not add....image is below.
Is there a vba code to remove this space from each of the cell if its presesnt.
I have exported the excel from a pdf.
Excel will attempt to convert any value to a number if you apply an operator to it, and this conversion will handle spaces. So you can use =A1*1 or A1+0 to convert a value in A1 to a number, or something like this within a function =SUM(IFERROR(A1*1,0)).
That kind of implicit conversion automatically performs a trim(). You can also do this conversion explicitly by using the funciton N(), or NumberValue() for newer versions of Excel. However, as others have pointed out, many characters won't be automatically handled and you may need to use Substitute() to remove them. For instance, Substitute(A1,160,"") for a non-breaking space, a prime suspect because of its prevalence in html. The Clean() function can give you a shortcut by doing this for a bunch of characters that are known to be problematic, but it's not comprehensive and you still need to add your own handling for a non-breaking space. You can find the ASCII code for any specific characters that are grieving you by using the Code() function... for instance Code(Mid(A1,1,1))
Character Handling UDF
The UDF below gives flexibility to the character handling approach by allowing multiple characters to be removed from every cell in a range, and produces a result that can be used as an argument. For example, Sum(RemoveChar(A1:A5,160)) would remove all non-breaking spaces from the range being summed. Multiple characters can removed by being specified in either a range or array, for example Sum(RemoveChar(A1:A5,B1:B3)) or Sum(RemoveChar(A1:A5,{160,150})).
Function RemoveChar(R As Range, ParamArray ChVal() As Variant)
Dim x As Variant
Dim ResVals() As Variant
ReDim ResVals(1 To R.Count)
'Loop through range
For j = 1 To R.Count
x = R(j).Value2
If x <> Empty Then
'Try treating character argument as array
'If that fails, then try treating as Range
On Error Resume Next
For i = 1 To UBound(ChVal(0))
x = Replace(x, Chr(ChVal(0)(i)), "")
Next
If Err = 92 Then
Err.Clear
For Each Rng In ChVal(0)
x = Replace(x, Chr(Rng.Value2), "")
Next
End If
Err.Raise (Err)
On Error GoTo 0
'If numeric then convert to number
'so that numbers will be treated as such
'when array is passed as an argument
If IsNumeric(x) Then
ResVals(j) = Val(x)
Else
ResVals(j) = x
End If
End If
Next
'Return array of type variant
RemoveChar = ResVals
End Function
Numeric Verifying UDF
The drawback with replacing characters is that it's not comprehensive. If you want something that's more of a catch-all, then perhaps something like this.
Function GetNumValues(R As Range)
Dim c, temp As String
Dim NumVals() As Double
ReDim NumVals(1 To R.Count)
'Loop through range
For j = 1 To R.Count
'Loop through characters
'Allow for initial short-circuit if already numeric
For i = 1 To Len(R(j).Value2)
c = Mid(R(j).Value2, i, 1)
'If character is valid for number then include in temp string
If IsNumeric(c) Or c = Application.DecimalSeparator Or c = Application.ThousandsSeparator Then
temp = temp + c
End If
Next
'Assign temp string to array of type double
'Use Val() function to convert string to number
NumVals(j) = Val(temp)
'Reset temp string
temp = Empty
Next
'Return array of type double
GetNumValues = NumVals
End Function

Excel formula calculating once then deleting

I have an excel formula:
=SplitKey(GetSysCd(INDEX([ReportValue],MATCH("mtr_make_model",[FieldName],0)),INDEX([ListName],MATCH("mtr_make_model",[FieldName],0))), 0)
which is running a few subroutines in VBA, but mainly matching values and inserting those values into a cell. When it finds a value for "mtr_make_model" it runs and matches the values inside a sys codes table. The issue I am having is that it is calculating once and then it removes the formula and now has solely the value... In the event that I go to the mtr_make_model field and change the value, the formula does not recalculate. Has anyone heard of this happening? Is this due to something in the VBA code? How do I make that formula stay put and if certain values change, the formula recalculates?
Thanks in advance.
Here are the two functions:
Public Function GetSysCd(ByVal name As String, sysCdTableName As String) As String
Dim r As Integer
Dim sysCdTable As Range
Dim nameList As Variant
Dim sysCd As String
On Error GoTo GetSysCd_Error
Set sysCdTable = Worksheets("sys_cd").Range(sysCdTableName)
nameList = WorksheetFunction.Index(sysCdTable, 0, 2)
r = WorksheetFunction.Match(name, nameList, 0)
sysCd = WorksheetFunction.Index(sysCdTable, r, 1)
GetOutOfHere:
On Error GoTo 0
GetSysCd = sysCd
Exit Function
GetSysCd_Error:
sysCd = ""
GoTo GetOutOfHere
End Function
Public Function SplitKey(s As String, v As Integer)
Dim aString As Variant
Dim r As Integer
If Len(s) > 2 Then
aString = Split(s, "_")
If v = 0 Or v = 1 Then
SplitKey = aString(v)
Else
SplitKey = aString(0)
End If
Else
SplitKey = ""
End If
End Function
I don't think the functions are relevant at this point, but rather just a matter of the function not recalculating when a variable in the formula changes...
The problem could be that Excel only recalculates functions when one of their arguments changes, and your GetSysCd function is referring to a range that is not in its argument list
Set sysCdTable = Worksheets("sys_cd").Range(sysCdTableName)
where sysCdTableName is just a string rather than a reference.
You can make the functions recalculate in real time by adding Application.Volatile True to the top of each function.

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