VBA to make Excel formulae more readable - vba

I've had a glance around on the site, but advice on making Excel formulae (formulas?) more readable tends to be directed towards 'use named ranges' or 'use helper columns'. I'm not after techniques to make formulae easier to read, I'd like to create a macro to display the formulae in a more readable manner. You can skip to the end for a shortened problem description. I say this because although usually I like to explain my approach, I feel this time that my approach may be a distraction from the essence of the problem. Also, I think that the problem can be distilled down quite nicely without all the preceding background
Problem background
I have a lot of nasty nest-within-nest complicated formulae, here's one I wrote today (see Excel sample document)
or as code:
=IFERROR(IF(Latest[Weekday Num]=5,0,M13+MAX(Table3[Lunchtime],Latest[Lunchtime])-INDEX(Table4[Out],Latest[Weekday Num]))+Table3[Time remaining]-SUMIFS(Table4[Work time],Table4[Weekday Num],">"&Latest[Weekday Num],Table4[Weekday Num],"<5")+Latest[Clocked In]-S13,"Refresh csv")
As ugly as it may be, IMO it's actually quite well constructed; IFS and SUMIFS where possible to avoid nesting of formulae, all named ranges stored in tables, helper columns used where applicable. Yet under certain circumstances it returns a buggy result and the formula is not pleasant to read through. (It's not the longest either, I have compiled formulae with several a4 pages, of code, although that was when I couldn't use vba to construct helper columns)
I would like a macro which takes this formula and splits it into a sort of tree of branching functions, where each nested function is another twig - perhaps split the components of the formula across cells in the workbook
Current approach
I have so far attempted the following approach: Columns A&B are used to split the formula into smaller parts. I define these parts as being the functions and arguments of Excel formulae, so the formula IF(A1=1,A2,B1) is split into to IF(, A1=1,, A2, and B1). I do this with the following formula in column B:
=LEFT(A2,IFERROR(MAX(1,MIN(IFERROR(FIND("(",A2),LEN(A2)+1),IFERROR(FIND(")",A2),LEN(A2)+1),FIND(",",A2))),LEN(A2)))
Meanwhile, column A looks at the last component found by column B, and chops it off the long formula (using =SUBSTITUTE(A2,B3,"",1)).
So, for an original formula in A2 (as text), B2 is the first component of it (such asIF( in my example) and A3 is the formula in A2 minus the first component in B2. I drag down to iterate.
The macro
That gives me a list of the components of the formula in each of the cells of column B. My macro then decides what level each component is, and indents the component by that many cells. The level is defined as the number of open brackets preceding a component of the formula * or 'segment' in my code that haven't been closed. The macro comments explain this.
Sub DispFormula()
'takes a split-up formula and indents lines appropriately
Dim CurrLev As Integer, OBrac As Integer, CBrac As Integer
Dim Segment As Range 'each part of the split up formula
LastRow = Sheets("sheet1").Cells(Rows.Count, 2).End(xlUp).Row
Set orange = Range("B2:B" & LastRow) 'all the segments make an orange
CurrLev = 0 'the "level" of the "segment" - it's level is a measure of how many layers deep the formula is nested
'if(a=1,b,c) is split into 4 components: `if(`, `a=1,`, `b,` & `c)` where `if(` is level 0 and all the other segments are level 1
OBrac = 0 'how many open brackets have happened/ precede a segment of the formula
CBrac = 0 'how many closed brackets have happened
On Error Resume Next
For Each Segment In orange
If InStr(Segment, "(") <> 0 Then
OBrac = OBrac + 1
ElseIf InStr(Segment, ")") <> 0 Then
CBrac = CBrac + 1
End If
Cells(Segment.Row, CurrLev + 3) = Segment 'copies the segment value into a column indented by a number of cells equal to the order of the segment
CurrLev = OBrac - CBrac 'current level is how many brackets have been opened - how many have been closed,
'ie. the number of brackets preceding a segment which are currently open
Next Segment
End Sub
How it could be improved
That's how far I've got so far. What I really want though is for the tree of indentations to be replaced with a tree of dropdown lists. For a formula =IF(MAX(arg1,arg2)=1,arg3,MIN(arg1,arg2)) I would like to split into segments: IF(, MAX(, ARG1,, ARG2), =1,, ARG3,, MIN(, ARG1, & ARG2)) htn display them. Not like this: (as I currently am)
IF(
MAX(
ARG1,
ARG2)
=1,
ARG3,
MIN(
ARG1,
ARG2))
But like this: (or similar)
IF(◀
Which when you click on ◀ becomes this:
IF(▼
MAX(◀
=1,
ARG3,
MIN(◀
And then expanding Min to
IF(▼
MAX(◀
=1,
ARG3,
MIN(▼
ARG1,
ARG2))
Shortened problem description
To summarise:
I have a formula in Excel; formulae in Excel have the general form FUNCTION(argument1, arg2, arg3...)
Each argument may be simple (a constant, a text string, a cell reference) or complex (another formula with its own function and arguments)
I would like a macro which takes an input formula, and creates some form of user interface (be it specially located cells as in my example, or some other approach) to display the function in a tree like fashion
That means in the first layer of the UI, I display the function, layer 2 has the arguments of the function, if the arguments are complex they will have further sub layers
To access the sub layers, each function can be expanded with a dropdown arrow or some other means to reveal its arguments (the 'layer' below it). Like a Reddit post where you can click on the [+] to get 1 layer deeper.

I use this site to work with long formulas and it has helped me alot.
It doesn't have the UI components you mention, but it does a great job of "beautifying" the output.
Excel Formula Beautifier

Related

Scanning through lists in VBA and adding each hit in the same cell

I have been leveraging for the past several months a couple of lines of code in VBA which with the help of the stackoverflow community I was able to adjust as needed (link Looping and scanning a list of e.g., 200 strings against a column with string data (Excel, VBA, Macros)). Essentially, the code scans row by row through a list of pre-defined keywords against a range of data highlighting possible hits in an adjacent empty column/cell. For example, if my range/column contained "DOG ABC LLC" and my keyword list/array contained "ABC" the macro helped easily highlight the hit in another column by displaying it.
I have noticed one issue with this method that often more than one keyword hit could occur. For example, I can have an array containing both "ABC" and "DOG" as separate keywords. The current loop in place sadly only factors in the first hit apparently and then moves on. I was wondering whether there is an easy way of adjusting the code so that one could add all possible hits into a cell after a comma or space. Therefore instead of seeing just "DOG" or "ABC", one could clearly see that there were 2 hits "ABC , DOG". Here is the code I have been using thus far:
Dim wordsArray() As Variant
wordsArray = Worksheets("Keywords").Range("B2:B439").Value
Dim word As Variant
Dim cell As Range
For Each cell In Worksheets("Normalized").Range("J2:J49010")
For Each word In wordsArray
If InStr(cell.Value, word) > 0 Then
cell.Offset(0, -1).Value = word
End If
Next word
Next cell
Thank you in advance for advice!

Vba email generator, subject from excel spreadsheet

I have a vba code that generates an email. I would like the subject to be the data from the first and last cells in my list. The thing is, my list isnt of a set length, sometimes it contains 5 pieces of data sometimes 8 etc. How do i tell vba to pick the first and last cell no matter the length of the list?
thanks
For me, best practice is to just have cells on your sheet that calculate the first and last row (different ways you can do that), then give those cells a range name such as FirstRow and LastRow. In your vba then you refer to these cells to make your code dynamic.
e.g:
firstRow = Range("FirstRow)
lastRow = Range("lastRow")
test = range(cells(firstRow,lastRow))
-- Note I have not written VBA in many many years so am writing the above from memory so it may be not be exact.
Of course you can do it all entirely in VBA using the xlDown method mentioned previously but I prefer the transparency of it being on the main page so that easily spot if something breaks.
Range("A1").End(xlDown).Value
Where the cell is where you want to start and the End part moves all the way to the end

Excel // Dynamic Range in Sum If Function

I’m trying to have a quite complicated Sum if function more reliable. The situation is that I’m using that formula (see below) to sum values between two dates from a separate sheet in the workbook.
=SUMIFS(wochen!$F11:$BM11;wochen!$F$8:$BM$8;">="&DATE(YEAR(T$8);1;1);wochen!$F$8:$BM$8;"<="&DATE(YEAR(T$8);MONTH(T$8);DAY(T$8)))
The handicap is that I was hoping to implement some Kind of Lookup function or match function to always get the values from the correct row.
So far the sum_range is static and I can’t make sure its picking the correct line.
I already played around with this kind Address / Match Function
=ADDRESS(MATCH($B13;'F_P&L'!$B$1:$B$267;0);MATCH(T$6;'F_P&L'!$F$6:$CP$6;0);;;"wochen!")
into that Sum_Range of the Sum If Formular but that doesn’t work either as it comes out as Text
I’m happy for any Idea which van be implemented via excel Formulas or VBA
I hope I explained everything clear, if not I'm happy to provide more information's
Many Thanks in Advance
Dennis
Here is a simple example which you can extend to your case. It uses the =INDIRECT() formula.
If you know through some way what the correct rows are, say you know your range of interest is in line 11, ...
Put the following in a cell
A1: = 11 ' You can use VLOOKUP etc. to generate the row number
B1: = "F" & A1 & ":BM"&A1 ' That makes B1 read: F11:BM11
C1: = SUM(INDIRECT(B1)) ' This will make it the equivalent of writing =SUM(F11:BM11)
Writing =function(INDIRECT(*anything*)) where *anything* is a string describing the address of a range is the equivalent of writing =function(** selecting the range **)
Note on Performance: Do note, however, that INDIRECT is a volatile functions so any changes in the workbook with calculations set to automatic will cause it to recalculate. So there might be performance implications in certain cases.

Copy cells if specific text is found

I seem to have a problem and currently have not found a solution to it, which is why I address this question to you:
Each day I have a list of invoices and orders coming from different suppliers, and the orders are based on part numbers and types.
This list is imported as text and then goes through a macro I made, to arrange everything in cells.
I also need to go through some steps to format this list based on the type of order (ex: windshield, carpets, wheels, etc ). what I usually do is to filter everything and select the order type that I am interested, and then copy on the same row cells with text and formulas from another worksheet, named "template", which is a list of conditions .
Since it varies from day to day, it may not necessarily contain all part types, which is I couldn't use a macro, and I have to continue by hand, and sometimes the list exceeds 200-300 lines.
To give you an example, if E2 has "windshield" I copy in M2 to Q2 a selection of cells from "Template" (M2 to Q2), if "carpets" I copy M3 to Q3, and so on. the list of conditions is around 15 - 20 rows, and sometimes 2 conditions may apply (if order exceeds $5000 I mark it red, if overdue I bold everything, etc) but mainly I copy based on text in coll E.
If this could be copied into a macro, I would really appreciate it, as I need to take some steps every time, like auto-fit, copy header, format the amounts as number (from text), change text color based on order type, etc, and this too takes time.
I hope this information is enough to make an idea about this, and if not, I could post an example of the list I have to work with.
Many thanks in advance for your support
Use Application.Worksheetfunction.Match to find in which row in Template the to-be-copied cells can be found, then copy range M-Q for this row and paste in your file
You are asking too much in one question to get help here. We are best at single issue questions. The text and code below is intended you give you some ideas. If your code does not work, post the relevant part here and explain the difference between what it does and what you want it to do.
The problems you mention do not sound difficult. I would expect basic VBA to be enough to get you started. Are you looking for bits of relevant code without learning VBA. If you are, this is a big mistake. Search the web for "Excel VBA tutorial" or visit a large library and review their Excel VBA Primers. There are many tutorials and books to choose from so select one that is right for you. The time spent learning the basics will quickly repay itself.
Dim RowCrnt As Long
Dim RowLast As Long
With Worksheets("xxxx")
RowLast = .Cells(Rows.Count,"E").End(xlUp).Row
For RowCrnt = 2 to RowLast
' Code to process each row goes here
Next
End With
The above is probably the structure of your code. The For loop will examine each row in turn which will allow you to take relevant actions.
I have used "E" as a column letter because your question suggests column "E" is the most important. However, code that references columns in this way can be very confusing. Worse, if the column positions change, you will have to work carefully through your code changing the column letters. Better to have some statements at the top like this:
Const ColDate As String = "A"
Const ColAmtInv As string = "B"
Const ColAmtPaid As string = "C"
Const ColProdType As String = "E"
With these constants every reference to a column uses a name not a letter. The code is easier to read and, if a column moves, one change to the constant statement will fix the problem.
The Select Case statement is useful:
Select Case .Cells(RowCrnt, ColProdType).Value
Case "carpets"
' code for carpets
Case "windshield"
' code for carpets
Case Else
' Unknown product type
.Cells(RowCrnt, ColProdType).Font.Color = RGB(255, 0, 0)
End Select
You can have a string of If statements so multiple actions can occur on a single row:
If DateAdd("m", 1, .Cells(RowCrnt,ColDate).Value) < Now() And _
.Cells(RowCrnt,ColAmtInv).Value) > .Cells(RowCrnt,ColAmtPaid).Value Then
' Invoice overdue
.Rows(RowCrnt).Font.Bold = True
End If
If .Cells(RowCrnt,ColAmtInv).Value) > 5000 Then
' Large invoice
.Rows(RowCrnt).Font.Color = RGB(255, 0, 0)
End If
You do not have to write the entire macro in one go. If most action is because of the product type then try that first.
Hope this helps get you started.

Major formatting issue in Excel - VLOOKUP

I need help with a formatting issue in Excel, which is interfering with the VLOOKUP function in my Excel sheet.
I have two sheets with more than 20,000 column values as lookup, and the same number of values for reference. All the values in both cells are weirdly formatted, some with green triangles at the upper left corner of cells, some are text, etc.
Is there a way in Excel using a macro/VBA to remove or make all formatting similar in both sheets? The reason for VBA is because the person who is going to work with this file needs everything automated and is not familiar with Excel at all. I already have the VLOOKUP function in the cells, I just need to work with the formatting.
Well, I fight with partial lookups this way:
In the items array, I create new empty FIRST column and then place formula
="+"&B2
This will take the content of Cell B2 and add + in the front of it.
When I do vlookup, I add "+" to searcheable value
=VLOOKUP("+"&A6,A:O,2,FALSE)
Therefore, instead of comparing for example Strings and numbers, I compare Strings, by adding "+" in the front.
Another technique, is to kill all formatting:
Select whole column, click DATA-TEXT TO COLUMNS-DELIMITED and then DESELECT ALL DELIMITERS. Click Finish. This will clear your formatting.
===========================================================================
This is the VBA solution you asked for:
Call it from Excel
=GetLookup(G2,A:C)
Here is VBA:
Function GetLookup(LOOKFOR As String, RANGEARRAY As Range) As String
GetLookup = Application.WorksheetFunction.VLookup("+" & LOOKFOR, RANGEARRAY, 3, False)
End Function
Good luck!
I'm assuming the data type in all of the cells is the same, or you want it to all be the same. The following steps will make the cells a uniform type:
Save your workbook, in case this does not do as you require
Select all cells you wish to be of the same cell type
Press Ctrl+1, on the "Number" tab, select the type you wish these cells to take. Press OK.
Open the VBA editor using Alt+F11
Open the immediate window with Ctrl+G
Type the following: for each cell in selection : cell.formula = cell.value : next cell
Press enter (you may have to wait a few seconds).
If you take this action with the same data type (e.g. choose "Text" for both ranges in step 3) on both your ranges you should be "comparing apples with apples" and your VLOOKUP should function as required.
Hope this helps.
Edit: formatting, clarification