Excel: parsing cell and creating a function - excel

I have some cells with, for an example, this value:
5*A-2*B-4*C
In other cells I would put the values for A, B and C. I would like to make additional cell that would count the value. So, for an example, if in some cells it is written that the value of A is 2, value of B is 3 and value of C is 1, I would like an additional cell that would calculate and put the value 0 (that's the result of 5*2-2*3-4*1). Possible variables are A, B and C, but they don't have to be contained in every cell (e.g., some cell may be just 5*A-3*C).
Is that possible? Does anyone know how to write that function?
P.S. I can't split manualy values in different cells because there are hundreds of them.
Thanks.

The comment from Saladin Akara points the way to your simplest solution. Define some named formulas (see the Excel help topic "Working with names") for A, B, and C. Then any other cells can contain formulas that use those named values.
If that's not enough, for example if you really want to see and edit the formula in a cell, and then calculate the value of the formula in a different cell, you can use Excel's built-in evaluator without having to parse the formula yourself. The easiest way to do so is via the Evaluate method of the Application object. (Again, see the help.) Charles Williams has example code on his website that evaluates an Excel expression here: http://www.decisionmodels.com/calcsecretsh.htm
Going beyond that, you can use Application.Evaluate to evaluate expressions with (scalar) parameters without defining any names, and still without actually parsing the formula, by doing some rudimentary string replacement. There are several examples on the web, but a very good one from Doug Jenkins is here: http://newtonexcelbach.wordpress.com/2008/04/22/evaluate-function/

This can be done with the VBA function EVALUTE
simple example:
Function ev(f As Variant, A As Range, B As Range, C As Range) As Variant
Dim s As String
s = f
s = Replace(s, "A", "~A~")
s = Replace(s, "B", "~B~")
s = Replace(s, "C", "~C~")
s = Replace(s, "~A~", A.Address)
s = Replace(s, "~B~", B.Address)
s = Replace(s, "~C~", B.Address)
ev = Evaluate(s)
End Function
eg if your expression is in A2, and the values of A, B, C are in C2:E2
=ev(A2,C2,D2,E2)
returns the calculated value
You would want to use a ParamArray instead of A, B, C the variable values to allow for an arbitrary number of variables.
A slightly more complete version:
Function ev(expr As Variant, VarNames As Range, varValues As Range) As Variant
Dim s As String
Dim i As Long
s = expr
For i = 1 To VarNames.Columns.Count
s = Replace(s, VarNames.Cells(1, i), "~" & VarNames.Cells(1, i) & "~")
Next
For i = 1 To VarNames.Columns.Count
s = Replace(s, "~" & VarNames.Cells(1, i) & "~", varValues.Cells(1, i).Address)
Next
ev = Evaluate(s)
End Function
Usage:
Same data as above plus variable names in C1:E1
=ev(A2,C1:E1,C2:E2)

Related

Using string functions to indicate member of an enumeration in VBA

I have to copy some data from one table to another. There are several groups of columns that are the same except for a name at the start of the column header. I want to put those names into an array, and then loop through the groups doing the same operations on each group. I'd like to do this using an enumeration of the headers, but I can't find a way to reference the members of the enumeration by concatenating the name and the remainder of the header. Boiling it down to the essentials, the code would look like this:
Enum Headers
a1 = 1
b1
c1
End Enum
Sub test()
Dim Pick() As Variant
Pick = Array("a", "b", "c")
For i = 1 To 3
n = Headers.Concat(Pick(i), "1")
MyTable.DataBodyRange.Cells(Row, n) = OtherTable.DataBodyRange.Cells(Row, n)
Next i
End Sub
I know how to work around this, using a slightly different approach, and that's what I think I'll end up doing.
MyTable.ListColumns(Pick(i) & "1").DataBodyRange.Cells(Row,0)
That works because the column reference is to a string, so I can piece the string together in that fashion.
But I wanted to know if anyone has a way to use something akin to what I outlined above using an enumeration.
n is the value of the enumeration member, so it could be 1, 2, or 3.
This would be the column where I want to copy or paste data. The row would be "Row", so the reference is to Cells(Row, n).
Instead of specifying the member as a1, b1 or c1, I want to use an expression Concat(Pick(i), "1"), where Pick(i) will be "a", "b", or "c".
Both tables would have headers a1, b1, c1.
The problem is that
n = Headers.Concat(Pick(i), "1")
but
n = Headers.a1
would be valid. I just can't substitute Concat(Pick(i) for a1, since a "valid VB identifier" is required following "Headers.", not a string.
is not a valid statement.
My question is whether there is some way to accomplish what the invalid code is trying to do. Sort of like an indirect reference in Excel, where you can turn a string into a range or file reference.

Excel VBA extract values from nested function

Is it possible to extract the resulting value of a nested sub-function in VBA?
For example, from this INDEX-MATCH look-up function, I would like to find the value from the results from the two MATCH() functions:
=+INDEX(Sheet3!$B$5:$I$15;MATCH($A6;Sheet3!$A$5:$A$15;0);1)
I am looking for a macro that can return the value of the nested MATCH() function as an input in a VBA code. That is, I realize I could extract the MATCH() subfunction and have VBA paste it as a value. What I want is identify the value of the MATCH() function and calculate with it within my VBA script.
As I'm not quite following what you're saying, I'll take a guess... you want to use the match() scenario separately?
dim x as long, y as long, z as string
x = application.match($A" & ActiveCell & ",Sheet3!$A$5:$A$15,0)"
y = x*3 'perform some work using the row number x, knowing match of 1 = row 5 on sheet3
z = application.index(Sheet3!$B$5:$I$15," & y & ")"
Took that a step further where you can utilize said arithmetic'd match value.
Inherently, the match only provides a row number from your array, so i don't know what the "extract" you are referring to would be. If you are looking to extract a value that you're matching to, then you'll just end up with what is (from your example) in A6.
Edit: Updating code to be usable (previously just gave a concept)
dim x as long, y as long, z as string
with sheets("Sheets3")
x = application.match(ActiveCell.value,.range(.cells(5,"A"),.cells(15,"A")),0)
y = x*1 'perform some work using the row number x, knowing match of 1 = row 5 on sheet3
z = application.index(.range(.cells(5,"B"),.cells(15,"B")),y) 'This index was changed to be a SINGLE COLUMN (was B5:I15)
end with

Splitting text to columns in Excel

I have a column in Excel that consists of data in the following format: "NAME OF BAND Album Title". I'd like to split this cell into two--one for the all-caps band name and another for Album Title. Below are a few examples from the data:
Column A
ABSORBED Demo '98
ABSTRACT CELL THEORY Act
ABSTRACT SATAN Elite 7512
ABSTRACT SATAN Aryan Blitzkrieg Union
ABSTRACT SATAN Satanic Blood Circle
ABSTRACT SHADOWS Symphony of Hakel
Splitting by space doesn't work since bands have varying numbers of words in their name. Any help would be appreciated.
Here is a formula solution. No VBA required.
Assuming your list starts in cell A1, enter the following formula in cell B1:
=LEFT(A1,MATCH(,--(CODE(MID(A1,ROW(OFFSET($A$1,,,LEN(A1))),1))<96),)-3)
This is an array formula and must be confirmed with Ctrl+Shift+Enter.
And then in cell C1, enter this:
=MID(A1,MATCH(,--(CODE(MID(A1,ROW(OFFSET($A$1,,,LEN(A1))),1))<96),)-1,99)
This is an array formula and must be confirmed with Ctrl+Shift+Enter.
Now select the range B1:C1 and copy downward as far as needed.
.
Here is how they work. We'll discuss the first formula.
The MID function splits the value of cell A1 into individual characters. The CODE function returns the ASCII code number for each char. We test each code number to see if it is less than 96, which is "a" the first lower case char.
This gives us an array of Boolean values (TRUE or FALSE), one Booelan value for each char in cell A1.
We convert the Booleans to ONES and ZEROES by the double unary (--).
We search the array for the location of the first ZERO by using the MATCH function.
The end of the contiguous upper case letters is three char locations prior to the location returned by MATCH.
That's it.
What makes all of that possible is the array at the heart of the formula produced by the ROW/OFFSET combination. In conjunction with the LEN function, this combo produces a vector array that looks something like {1;2;3;4;5;6;7;8;9;10;11}. That array's last and largest number is equal to the length of the value in cell A1.
.
UPDATE
Here is a sample workbook showing these formulas work on the question's example data: http://www.excelhero.com/samples/torentino_excelhero.xlsx
Something like this might split off the band names from the albums.
Sub splt()
Dim rw As Long, p As Long, r As Long, sp As Long, v As Long
Dim bnd As String, ttl As String, tmp As Variant
With Worksheets("Sheet1")
For rw = 2 To .Cells(Rows.Count, 1).End(xlUp).Row
p = 1
bnd = vbNullString
ttl = Trim(.Cells(rw, 1).Value2) & Chr(32)
tmp = Split(ttl)
For v = LBound(tmp) To UBound(tmp)
If UCase(tmp(v)) = tmp(v) Then
bnd = Join(Array(bnd, tmp(v)), Chr(32))
Else
Exit For
End If
Next v
.Cells(rw, 2) = bnd
.Cells(rw, 3) = Trim(Right(ttl, Len(ttl) - Len(bnd)))
Next rw
End With
End Sub

Using MIN/MAX in excel array formula

I have a simple array formula in excel that doesn't work in the way I wish. In columns A and B there is data (A1 is paired with B1 and so on) while in column F there is the calculation based on the parameter in column E.
In cell F1 the formula is:
{=SUM(MAX(A$1:A$9, E1)*B$1:B$9)}
What this formula does is:
=MAX(A$1:A$9, E1)*B$1 + MAX(A$1:A$9, E1)*B$2 + ...
Instead, I need a formula that does this:
=MAX(A$1, E1)*B$1 + MAX(A$2, E1)*B$2 + ...
In words, the formula I wrote (the first one) always finds the max between the values from A1 to A9 and E1, multiplies it by the i-th B value and sums the results. What I need is a formula that finds the max between the i-th A value and E1, and not between all the A values.
What I'm looking for is easily done by adding in column C the formula =MAX(A1;E$1)*B1 and then in F1 just =SUM(A1:A9), but I can't use this solution because in column F the same formula is repeated, with the E parameter changing every time.
I can use a IF instruction: in F1 I can write
{=SUM(IF(A$1:A$9>E1, A$1:A$9, E1)*B$1:B$9)}
While this formula does what I need in this case, I think it's a bad solution because I find it difficult to read and to expand. For example, if there is another parameter in column D and the factor is MIN(MAX(A$1:A$9;E1);D1), using IF will result in a very long and very unreadable and complicated formula.
Are there better solutions to my problem? Thank you all!
NOTE: syntax may vary a little because I am using the italian version of excel.
The problem is that MAX takes an array as an argument. Functions that normally take an array never return an array - they were designed to turn an array into one number. No matter how many arrays you throw at MAX, it's always just going to return one number.
I couldn't come up with a good solution, so here's a bad one
=SUMPRODUCT(((A1:A9*(A1:A9>E1))+(E1*(A1:A9<=E1)))*B1:B9)
I don't think that really increases the maintainability of the IF-based formula that you're trying to avoid. I think you're stuck with IF or a helper column.
Another possibility is a VBA function.
Public Function SumMaxMin(rRng1 As Range, rRng2 As Range, ParamArray vaMinMax() As Variant) As Double
Dim rCell As Range
Dim dReturn As Double
Dim aMult() As Double
Dim lCnt As Long
Dim i As Long
ReDim aMult(1 To rRng1.Cells.Count)
For Each rCell In rRng1.Cells
lCnt = lCnt + 1
aMult(lCnt) = rCell.Value
For i = LBound(vaMinMax) To UBound(vaMinMax) Step 2
If Not Evaluate(aMult(lCnt) & vaMinMax(i + 1) & vaMinMax(i)) Then
aMult(lCnt) = vaMinMax(i)
End If
Next i
Next rCell
For i = LBound(aMult) To UBound(aMult)
dReturn = dReturn + aMult(i) * rRng2.Cells(i).Value
Next i
SumMaxMin = dReturn
End Function
For your example
=SumMaxMin(A1:A9,B1:B9,E1,">")
Adding another condition
=SumMaxMin(A1:A9,B1:B9,E1,">",D1,"<")
It will error if your ranges aren't the same number of cells or you pass arguments that don't work with Evaluate.
Another possibility for avoiding repetitions of cell references is:
=SUM(B1:B9*ABS(A1:A9-E1*{1,-1}))/2
assuming values are non-negative. More generally to return an array of pairwise max values:
=MMULT((A1:A9-E1*{1,-1})^{2,1}^{0.5,1},{1;1}/2)
which returns:
MAX(A1,E1)
MAX(A2,E1)
...
MAX(A9,E1)
I don't remember ever cracking this problem, but for maintainability I'd probably do something like this:
{=SUM((A1:A9<E1)*E1*B$1:B$9) + SUM((A1:A9>=E1)*A1:A9*B$1:B$9)}
If I understand the problem correctly, using IF instead of MAX should do:
=SUM(IF($A$1:$A$9>E1;$A$1:$A$9;E1)*$B$1:$B$9)

Calculate SUM without Adding Values to Rows/Columns?

When calculating series in Excel, most tutorials begin by setting sequence values to certain range of cells, say
A1=1, A2=2, A3=3,..., A10=10
and to get the value of 1+2+...+10, execute
A11=SUM(A1:A10)
But I don't want the "generate the sequence in worksheet cells first" part because initially I don't know the 'n' (10 in the above) and I want to define a custom function that takes n as a function argument.
So, is there a way to do something like this?
B1 = SUM([1:10]) // adding array of 'constants', not cell reference
EDIT: If I could 'summon' some (array of) big number(s) without any cell/ROW/COL operation as in calling rand(), that would be great.
Try using Array Formula as below
=SUM(ROW(A1:A10)) and then press CTRL+SHIFT+ENTER
Row(A1:A10) will become {1,2,3,4,5,6,7,8,9,10}.
Usage:
If you want to sum cells A20 to A50
sumjeff("A", 20,50)
Code
Function sumJeff(letter As String, nFrom As Integer, nTo As Integer) As Double
Dim strAddress As String
strAddress = letter & nFrom & ":" & letter & nTo
sumJeff = Application.WorksheetFunction.Sum(Range(strAddress))
End Function

Resources