Recalculating Excel VBA Function on one sheet disrupts other sheets - excel

I made a function that counts the number of items in a given month.
Column A is the month, and Column B is the number of items in that month.
Cell B1 has:
=countItems(A1)
Excel data:
Code:
Function countItems(month)
Application.Volatile
Dim count As Integer
If Not (month = 0) Then
count = 0
Do While Not (Cells(month.row + count + 1, 3) = 0)
count = count + 1
Loop
countItems = count
Else
countItems = ""
End If
End Function
I dragged the formula down from B1 to B500 and it works properly for every month. The formula returns nothing if there is no month in the corresponding A cell.
I have multiple sheets using the same formula on similarly-structured data sets.
Whenever the values in column B update for this Sheet 1, the other sheets will change too. However, Sheet 2 will update using Column C from Sheet 1.
If I have Sheet 2 recalculate, Sheet 1 will update using Column C from Sheet 2.
The function counts the number of items in a given month by checking how far down it can read in Column C before it finds the blank cell, indicating that the month is over.
Sheet 2 has 1 item in the first month, but it will still return 3 due to Sheet 1 having 3 items (counts Row 1 through 3 and stops at Row 4).
The second month of Sheet 2 begins on Row 3. But since the function is reading Column C from Sheet 1, it will run into the blank cell after counting 1 more item (counts Row 3 and stops at Row 4). Therefore no matter how many items are in Sheet 2 Month 2, it will return 1.
The function always uses the correct Column A, and only displays a number in Column B where there is date in Column A.
The consequence is that only 1 sheet can have the correct values, and doing that disrupts the other sheets.
I cannot solve this at the moment because I am new to VBA.
I have thought of making all of the function's cell references include a self-reference to the current cell's sheet, but I don't know how to do that and I don't know if it would work.
Edit: I couldn't make it work this way, but Application.Caller.Offset() with relative cell position worked as a solution. I am still wondering if there is a way to use absolute cell position though.
The sheets are not grouped together.

it's because there's a "time-space shift" between the range passed to the function and the range "felt" as the "caller" one by the function
you can see this behavior by modifying the function code as follows
Function countItems(month)
Application.Volatile
Dim count As Integer
Dim r As Range
Dim p As Variant, a As Variant
Set r = Application.Caller '<~~ retrieve the actual "calling cell" of the current function "instance"
p = r.Parent.Name
a = r.Address
MsgBox p & " vs " & month.Parent.Name & vbCrLf & a & " vs " & month.Address '<~~ compare the function "calling cell" vs the "passed cell"
If Not (month = 0) Then
count = 0
Do While Not (Cells(month.Row + count + 1, 3) = 0)
count = count + 1
Loop
countItems = count
Else
countItems = ""
End If
End Function
and you'll see msgboxs prompts showing you differences between the function "calling cell" and "passed cell" addresses and/or sheets
so to avoid this behavior you could rely on the "calling range only", like follows:
Option Explicit
Function countItems(month)
Application.Volatile
Dim r As Range
Set r = Application.Caller '<~~ retrieve the actual "calling cell" of the current function "instance"
'the "calling cell" is the one with the called function in its formula, i.e. in column "B" as per your data structure. then ...
If Not IsEmpty(r.Offset(, -1)) Then '<~~ ... the column with dates are one column to the left, and ...
Do While Not IsEmpty(r.Offset(countItems + 1, 1)) '<~~ ... the values to be checked for are one column to the right
countItems = countItems + 1
Loop
End If
End Function

Related

Count and CountIf Confusion VBA

I have a for loop that iterates through each sheet in my workbook. Each sheet is identical for most purposes. I am making an overview sheet in my workbook to express the results from the data in the other sheets(the ones that are identical).
There are 11 vehicles, each with their own sheet that has data from a test from each day. On any given day there can be no tests, or all the way to 30,000 tests. The header of each column in row 47 states the date in a "06/01/2021 ... 06/30/2021" format. The data from each iteration of the test will be pasted in the column of the correct date starting at row 49.
So what my overview page needs to display is the data from the previous day. In one cell on my overview there is a formula for obtaining just the number of the day of the month like 20 or 1 etc. Using this number, the number of the day is the same as the column index that the previous day's data will be in conveniently. So in my for loop I want to have a table that has the name of each sheet in column B, in column C I want to display the total number of tests done in that day(not the sum of all the data), in column D I need the number of tests with a result of 0, in column E I need the number of tests that have a result above the upper tolerance, and in column F I need the number of tests that have a result below the lower tolerance minus the result in column D.
I've been playing with the Application.WorksheetFunction.Count and CountIf functions but I keep getting 0 for every single cell. I made two arrays for the upper and lower tolerance values which are type Longs called UTol and LTol respectively. TabList is a public array that has each of the sheet names for the vehicles stored as strings. finddate is an integer that reads in Today's day number which is yesterday's column index. I've included a picture of the table in question and my for loop code:
finddate = Worksheets("Overview").Range("A18").Value
For TabNRow = 1 To 11
startcol = 3
Worksheets(TabList(TabNRow)).Activate
Debug.Print (TabList(TabNRow))
'Total number of tests done that day
Worksheets("Overview").Cells(startrow, startcol).Value = Application.WorksheetFunction.Count(Range(Cells(49, finddate), Cells(30000, finddate)))
startcol = 4
'Total number of 0 results, this used to get the number of 0's from each sheet but that row has since been deleted
Worksheets("Overview").Cells(startrow, startcol).Value = Worksheets(TabList(TabNRow)).Cells(48, finddate).Value
startcol = 5
'Total number of results above tolerance
Worksheets("Overview").Cells(startrow, startcol).Value = Application.WorksheetFunction.CountIf(Range(Cells(49, finddate), Cells(30000, finddate)), ">" & UTol(TabNRow))
startcol = 6
'Total number of results below tolerance
Worksheets("Overview").Cells(startrow, startcol).Value = Application.WorksheetFunction.CountIf(Range(Cells(49, finddate), Cells(30000, finddate)), "<" & LTol(TabNRow))
startrow = startrow + 1
Next TabNRow
```
No need to select/activate sheets when referencing them. See: How to avoid using Select in Excel VBA
Something like this should work:
Dim wsOv As Worksheet, wsData As Worksheet, rngData As Range
Set wsOv = ThisWorkbook.Worksheets("Overview")
finddate = wsOv.Range("A18").Value
For TabNRow = 1 To 11
Set wsData = ThisWorkbook.Worksheets(TabList(TabNRow))
Set rngData = wsData.Range(wsData.Cells(49, finddate), _
wsData.Cells(Rows.Count, finddate).End(xlUp))
Debug.Print wsData.Name, rngData.Address 'edited
With wsOv.Rows(2 + TabNRow)
.Columns("C").Value = Application.Count(rngData)
.Columns("D").Value = Application.CountIf(rngData, 0)
.Columns("E").Value = Application.CountIf(rngData, ">" & UTol(TabNRow))
.Columns("F").Value = Application.CountIf(rngData, "<" & LTol(TabNRow)) _
- .Columns("D").Value
End With
Next TabNRow

Linking two different Excel tables and auto-populate one when populate another

I have two diferent excel formatted tables one near another. First table (green headers) is a table where I have to add some chemical formulas in Column A and Column B will be SUM of all compounds that I will add from second table (yellow headers), which represents Periodic System of Elements!
The formula that I am using in Table 2 (yellow headers) for calculating chemical compounds is this:
=C$2*MAX(IFERROR(IF(FIND(C$1&ROW($1:$99);MolM.[#[Mol. Formula]]);ROW($1:$99);0);0);IFERROR(IF(FIND(C$1&CHAR(ROW($65:$90));MolM.[#[Mol. Formula]]&"Z");1;0);0)) (CSE formula)
What and how I am usually doing this update of new compounds is that I am adding new chemical formulas in Column A manually (that is okay) and then dragging main formula in Table 2 (yellow header) to calculate all elements, and then SUM in column B for the main result!
My question is, is there a possibility to be more automated, just when I type new compound in Column A it will expand as normal table do, but also to auto-expand and calculate rest of compounds, without that I drag the formula manually..?
Hopefully this was clear enough.
Is there any possibility to make this happen? Is the only solution Power Query or?
I'm making a wild guess here.
Say that you write NH3 in A3, and then have it print I2 (value of "N") in I3, and C2 * 3 (value of "H" times 3, "H3") in C3. To then have B3 calculate the total value with =SUM() or similar.
You could have a VBA sub that looks for the value and prints this.
Here is a prototype of that:
Sub molFunc(chem As String, formRow As Long)
Dim i As Long, c As String, atoms As Range, a as Range
Set atoms = Range("C1", Cells(1, Columns.count).End(xlToLeft))
For i = 1 To Len(chem)
If Not Mid(chem, i + 1, 1) = UCase(Mid(chem, i + 1, 1)) Then
c = Mid(chem, i, 2)
i = i + 1
Else
c = Mid(chem, i, 1)
End If
For Each a In atoms
If a.Value = c Then
If IsNumeric(Mid(chem, i + 1, 1)) Then
a.Offset(formRow - 1).Value = a.Offset(1) * Mid(chem, i + 1, 1)
Else
a.Offset(formRow - 1).Value = a.Offset(1)
End If
End If
Next a
Next i
End Sub
Then you can call it from a Worksheet_Change event in the worksheet of your choice.
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Range("A:A")) Is Nothing And Target.count = 1 Then
Application.EnableEvents = False
Call molFunc(Target.Value, Target.Row)
Application.EnableEvents = True
End If
End Sub

Excel VBA: how to code a Sum formula with variable rows to put in a worksheet

I want to put a Sum formula in a row of a table, in columns C to H, but the code I’ve come up with somehow doesn’t work. The situation is as follows:
the number of the 1st row of the table varies (the 1st column is
always B)
the number of the 1st row in the formula varies, but is always the 3rd row of the table
the number of the row that should contain the formula varies, but in the macro I calculate that number relative to the 1st row of the table
the number of the last row in the formula varies, but is always 1 less than the number of the row that should contain the formula
To be more specific and hopefully more clear, let’s say that:
the number of the first row of the table = startnum
then the number of the 1st row in the formula = startnum+3
the number of the row that should contain the formula = startnum+x
then the number of the last row in the formula = startnum+x-1
Trying to find out what my code could be, I recorded a macro. Based on that I have tested the following code:
With Worksheets("A&N")
.Range("C16:H16").Formula = "=SUM(C7:C15)"
End With
This works fine, but as I’ve described, the numbers 16, 7 and 15 are actually variable.
I’ve tried to translate this code to my situation, and made this code:
Set rngOpmaak = Range(rngTabel.Cells(startnum + x, 2), rngTabel.Cells(startnum + x, 7))
rngOpmaak.Formula = "=SUM(“C” & startnum + 3 & “:C” & startnum + x -1)"
When I run the macro I get the message that the second line can’t be compiled. I’ve seen solutions on this site that to me look exactly like my code, so I don’t understand what’s wrong with mine.
I’ve also tried:
rngOpmaak.FormulaR1C1 = "=SUM(R" & startnum + 3 & "C:R" & startnum + x -1 & "C)"
But with startnum=2 (1st row of the table) the formula becomes =SUM(C$3:C$5) to =SUM(H$3:H$5) (without the quotations) instead of =SUM(C4:C6) to =SUM(H4:H6).
Can anyone help me with what the line of code should be? All suggestions are much appreciated.
You'll need to adjust this to your situation, as I didn't use the same variables you did (preferring more expressive ones):
Sub test()
Dim output As Range, dataStart As Integer, dataEnd As Integer, rowsOfData As Integer, nCols As Integer
' Row 1: Table headers
' Row 2: 1st row of data
' Row 3: more data
' Row 4: more data
' Row 5: last row of data
' Row 6: Summary row (location of formula)
dataStart = 2
dataEnd = 5
rowsOfData = 1 + dataEnd - dataStart
nCols = 4
Set output = Range(Cells(dataEnd + 1, 1), Cells(dataEnd + 1, nCols))
output.FormulaR1C1 = "=SUM(R[-" & rowsOfData & "]C:R[-1]C)"
End Sub
The generated formula uses relative references, as it was given offsets ([]) from the current cell, rather than absolute R/C values, e.g.
R2C:R5C -> A$2:A$5 no matter where in column A the formula is entered
R[-4]C:R[-1] -> A2:A5 if the formula is entered in A6.

How do I get excel to merge cells only when other cells are filled?

I need to merge cells using a formula so that the cells only merge when cells on another tab are filled.
I have 2 tabs with the same amount of columns in each. I want cells a1-d1 to merge in tab 1 when cells a1-d1 in tab 2 are filled and for the value of d1 in tab 2 to be inputted into the newly merged cells in tab 1.
this is what I have:
Excel VBA Methods and Function (Excel Macros) overview
Since you want to change cells i do not believe that you can use a formula (even not a user defined one). Therefore i wrote an excel vba macro for your problem.
FirstRows(): Is the starting point. It loops over 10 rows and calls the other methods
CheckEmptyCellValues(curRow): This method checks for empty cells in tab2 (sheet 2 in excel)
MergeCells(curRow) takes the current row as a number (any integer from 1 to max amount of rows) and merges the cells from column 1 to 4 on Sheet 1 (the first sheet in excel)
Fully working demo tested with 4 columns and 10 rows
Sub FirstRows()
Sheets(1).Select
For curRow = 2 To 11
Merge = CheckEmptyCellValues(curRow)
If Merge = 4 Then
MergeCells (curRow)
cellValue = Sheets(2).Cells(curRow, 4).Value
Sheets(1).Cells(curRow, 1).Value = cellValue
End If
Next
End Sub
Sub MergeCells(curRow)
Sheets(1).Select
Range(Cells(curRow, 1), Cells(curRow, 4)).MergeCells = True
End Sub
Function CheckEmptyCellValues(curRow)
Merge = 0
Sheets(2).Select
For i = 1 To 4
If Len(Cells(curRow, i).Value) > 0 Then
Merge = Merge + 1
End If
Next
CheckEmptyCellValues = Merge
End Function
Below you can see the result. The values from sheet 2 haven been copied to sheet 1 (second image). In Sheet 1 the Cells in a row are merged (in row 2 from Cell A2 up to Cell D2 (A2-D2 is now just one cell) if in the first image (sheet 2) every cell (from column a to column d) in a row had a value.
Bugs in the modified code
There are a few things in the modifiend code that are not possible or could lead to a wrong understanding
Function CheckEmptyCellValues(curColumn)
Merge = 0
Sheets(2).Select
For i = A To d
If Len(Cells(curColumn, 11).Value) > 0 Then
Merge = Merge + 1
End If
Next
CheckEmptyCellValues = Merge
End Function
The line For i = A To d is not possible. If you want to use a loop you have to use numbers: For i = 1 To 4 this would repeat the code between For and Next4 times starting with 1
This line Cells(curColumn, 11).Value is technical correct but misleading. Excel uses the first value after (for the row-index and the second value for the column-index. Both values have to be a number: Cells(4,2).Value returns the Cell value from the 4th. row and the second Column (in the Excel Gui the Cell B4)
Try changing this line For i = A To d to this For i = 1 To 4 and see if that returns the wished result.
Bugs part 2
In your other modification you have some of the same bugs:
The loop For curColumn = A to d needs numbers instead of letters (unless A and d were a variable filled with a number but according to your code sample this is not the case
The line cellValue = Sheets(2).Cells(curColumn, d).Value has the same bug, if d is just the letter d and not something like d = 4 than you can not use it in a loop.
This is the code from your comment:
Sub FirstRows()
Sheets(1).Select
For curColumn = A To d
Merge = CheckEmptyCellValues(curColumn)
If Merge = d Then
MergeCells(curColumn)
cellValue = Sheets(2).Cells(curColumn, d).Value
Sheets(1).Cells(curColumn, d).Value = cellValue
End If
Next
End
Sub Sub MergeCells(curColumn)
Sheets(1).Select
Range(Cells(curColumn, 1), Cells(curColumn, d)).MergeCells = True
End Sub
Be carefull it is not running.

Excel VBA - Loop through range and set formula in each cell

I've got a workbook where I have one worksheet which contains a lot of data.
My goal is to create a macro that inserts a formula in a separate sheet to copy the data from the first sheet. Lets call the first sheet "Numbers1" and the second sheet "TidyNumbers1".
In the sheet "TidyNumbers1" I want to loop through each cell from column A to M and rows 1 to 60. So I've got a macro that so far looks like this:
Sub updateFormulasForNamedRange()
Dim row, col, fieldCount As Integer
colCount = 13
RowCount = 60
For col = 1 To colCount
For row = 1 To RowCount
Dim strColCharacter
If col > 26 Then
strColCharacter = Chr(Int((row - 1) / 26) + 64) & Chr(((row - 1) Mod 26) + 65)
Else
strColCharacter = Chr(row + 64)
End If
Worksheets("TidyNumbers1").Cells(row, col).Formula = "=IF(Numbers1!E" & col & "<>0;Numbers1!" & strColCharacter & row & ";"")"
Next row
Next col
End Sub
But the formula is supposed to looks like this for Column A, row 2:
IF(Numbers1!E2<>0;Numbers1!A2;"")"
And the formula in Column A, row 3 should look like this:
IF(Numbers1!E3<>0;Numbers1!A3;"")"
Formula in Column B, row 2 should look like this:
IF(Numbers1!E2<>0;Numbers1!B2;"")"
In other words, the formula looks to see if the value in Column E, row % is anything but 0 and copies it if conditions are met.
But, I see that I need to translate my integer variable Row with letters, because the formula probably needs "A" instead of 1. Also, I get a 1004 error (Application-defined or object-defined error) if I just try to use:
Worksheets("Numbers1").Cells(row, col).Formula = "=IF(Numbers1!E" & row & "<>0;Numbers1!" & col & row & ";"")"
I clearly see that the integer row should be translated to letters, if that's possible. Or if anyone has any other suggestions that might work. Also, the 1004 error is unclear to me why happens. I can define a string variable and set the exact same value to it, and there's no error. So it's probably the formula bar that whines about it I guess?
Here is a former post of mine containing functions for conversion of column numbers to letters and vice versa:
VBA Finding the next column based on an input value
EDIT: to your 1004 error: Try something like this:
=IF(Numbers1!E" & row & "<>0,Numbers1!A" & row & ","""")"
(use ; instead of ,, and "" for one quotation mark in a basic string, """" for two quotation marks).
Would not it be easier to get the cell address with the Cells.Address function?
For example:
MsgBox Cells(1, 5).Address
Shows "$E$1"
Best Regards

Resources