Build Excel function: Unmerge, calculate, re-merge. Problem: Function starts to run recursive before finishing - excel

My main goal is to be able to autofilter merged cells in one column.In the picture below I want row 7-9 to disappear when I remove "6" from the autofilter menu. But as I have figured, I need the value "6" to be held in all the cells "L7:L9" in order for Excel to do so.
The number 6 is calculated by adding "Num1" and "Num2" (2 * 3) by the following function I have placed in "L7":
Function Exposure(arg1 As Range, arg2 As Range) As Variant
Application.EnableEvents = False
Application.Calculation = xlManual
If Application.ThisCell.Offset(, -1).Value <> "-" And Application.ThisCell.Offset(, -2).Value <> "-" Then
Exposure = Left(Application.ThisCell.Offset(, -1).Value, 1) * Left(Application.ThisCell.Offset(, -2).Value, 1)
End If
If Exposure = 0 Then Exposure = "-"
Application.Calculation = xlAutomatic
Application.EnableEvents = True
End Function
I put the following formula inside the merged cell "L7":=Exposure(K7;J7). Then formula is dragged down."Num1" and "Num2" are controlled by valdiation fields, drop-down menu.
My plan was to unmerge after calculating the Exposure Variant, fill the same value in the remaining rows, then re-merge the same area. So I wrote this stand alone Sub:
Sub WorkingSub(rng As Range)
'Set rng = ActiveCell.MergeArea
rng.UnMerge
For i = 2 To rng.Cells.Count
rng.Cells(i).Value = rng.Cells(1).Value 'This line triggers recursion
Next i
rng.Offset(rng.Cells.Count).Copy 'Copies format from below
rng.PasteSpecial Paste:=xlPasteFormats 'Paste that keeps the values even after merging
End Sub
Which works on its own, but not when called inside the function above. After setting the first value, the function triggers "something", debug show the the function starting over, skipping the rng.PasteSpecial Paste:=xlPasteFormats code.
So my question to you guys is how do i write my function(s) to stop "recursing" and let me unmerge during the function call?
Or am I attacking this the wrong way? What would you do?
I am stuck with merged cells for lots of reasons, this is just one part of many inside this spreadsheet.

An interesting problem. You can capture the filter event through trapping a change in a calculation and then processing the rows of the table for visibility. I've made some assumptions for the initial table range assignment which may need some alteration.
The If Not VisRange Is Nothing Then is actually redundant as the prior line will throw a fit if an empty range is assigned, but I just kept it in. In order to get around having a null range, keep the header range in the initial MergedTableRange so there will always be a row visible
Within a cell either somewhere in the same worksheet or a 'dummy' worksheet
=SUBTOTAL(103,Sheet1!A3:H10) 'Or other table range
In the worksheet module code
Private Sub Worksheet_Calculate()
Dim ws As Worksheet: Set ws = Worksheets("Sheet1")
Dim MergedTableRange As Range: Set MergedTableRange = ws.Range("A2").CurrentRegion
Dim Cell As Range
Dim VisRange As Range: Set VisRange = MergedTableRange.SpecialCells(xlCellTypeVisible)
If Not VisRange Is Nothing Then
For Each Cell In VisRange
If Not Application.Intersect(Cell.MergeArea, VisRange).Address = Cell.MergeArea.Address Then
Cell.Rows.Hidden = True
End If
Next Cell
End If
End Sub

I came up with a different approach. Maybe there's a downside I'm missing. But my few test runs have succeeded.
I allready have a hidden sheet named "Template" where the formats for each new "#" is stored. So whenever the user wants to insert a new row, the template have the merged and the non-merged cells ready and insert is done through copy paste.
In that same sheet I made 2 merged rows in column 2, 3 merged cells in column 3 and so on:
This way I'm able to copy the correct number of merged rows to paste after filling the unmerged rows with their correct values.
I came to the conclusion that I could catch a Worksheet_change on the "Num1" and "Num2" columns instead of catching and canceling an autofilter call.
So I added:
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Target.Worksheet.Range("J:J")) Is Nothing Then
Call UnMergeMerge(Cells(Target.Row, "L").MergeArea)
End If
If Not Intersect(Target, Target.Worksheet.Range("K:K")) Is Nothing Then
Call UnMergeMerge(Cells(Target.Row, "L").MergeArea)
End If
End Sub
And the UnMergeMerge sub ended up being:
Sub UnMergeMerge(rng As Range)
Application.EnableEvents = False
Application.ScreenUpdating = False
rng.UnMerge
For i = 2 To rng.Cells.Count
rng.Cells(i).Value = rng.Cells(1).Value
Next i
With Sheets("Template")
.Range(.Cells(8, rng.Cells.Count), .Cells(8 + rng.Cells.Count, rng.Cells.Count)).Copy
End With
rng.PasteSpecial Paste:=xlPasteFormats
Application.EnableEvents = True
Application.ScreenUpdating = True
End Sub
Still not sure it's the fastest and best approach...Do you guys still believe catching, undoing and running a different autofilter would be more effective?

Related

Excel VBA - Copy a formula result to a cell in another sheet + add succesive formula results in consecutive rows each time the formula is recalculated

I am running a partially randomize set of data and trying to find the best solutions depending on certain parameter changes. I need to "record" certain solutions and then compare different results for different parameters each time the randomized variables are recalculated.
I would like to do the to following:
On Sheet1, cell S255, is the result of a formula =SUM(M252:S252)
I need to automatically add that result (Sheet1 S255), to Sheet5, column A, starting at A1.
Then, each time the formula is recalculated and the result changes, I need the new result to be added to the consecutive row to the previous result (so the second result would go to A2, third one to A3, and so on).
Looking for similar cases I have come to be able to do 1. and 2. using this event:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim ws As Worksheet: Set ws = Sheets("Sheet5") 'declare and set the worksheet the data is to be copied into, amend the sheet name as required
If Target.Address = "$S$255" Then 'if anything changes in C6 or C9 in this sheet
ws.Range("A1").Value = Target.Parent.Range("S255") 'copy the value from cell C10 in this sheet to Sheet2 in cell E5
End If
End Sub
Doing 3. is proving more challenging. What event would be suitable to do so?
Thanks in advance for your time and understanding!
Thats how you are able to solve it with the Worksheet_Calculate event like BigBen suggested. Under "Random numbers" are just a few numbers with the RANDBETWEEN-Function for showing puposes. Just change the code for your case. Everytime you press "Delete" for exaple the new sum will be set under the Results column.
Private Sub Worksheet_Calculate()
Dim lastRow As Long
'EnableEvents must be switched off so that the macro does not
'call itself in an endless loop by cell change
Application.EnableEvents = False
lastRow = WorksheetFunction.CountA(Range("C:C")) + 1
Range("C" & lastRow).Value = Range("A9").Value
Application.EnableEvents = True
End Sub
'example saving sum ONLY IF SUM CHANGES
Private Sub Worksheet_Calculate()
Dim offsetLastSum As Long, curSum As Double
offsetLastSum = Range("NEXT_SUM_OFFSET").Value2
curSum = Range("THE_SUM").Value2
With Range("SUM_HISTORY_HEAD")
If .Offset(offsetLastSum - 1).Value2 <> curSum Then
Application.EnableEvents = False
.Offset(offsetLastSum).Value2 = curSum
Application.EnableEvents = True
End If
End With
End Sub

Insert blank row wherever a cell is empty vba

I am trying to create add some code to my macro to add a blank row whenever the value in column "B" is blank. I have the following code, but it is not doing what I want it to. It is entering too many blank rows.
Columns("B:B").Select
Selection.SpecialCells(xlCellTypeBlanks).Select
Selection.EntireRow.Insert
Sheets("Attendance Audit Hastus").Protect
Any ideas of how I can accomplish this?
If there are four adjacent/consecutive blank cells like B4:B7, the code in the question will insert four rows above them. Try this. It will insert only one row below the blank cells. So the new row will be B8 if the blank cells are B4:B7
Sub InsertOneRowBelowBlankCells()
Dim BColBlnk As Range, ar As Range
Set BColBlnk = Range("B:B").SpecialCells(xlCellTypeBlanks)
For Each ar In BColBlnk.Areas
ar.Cells(ar.Rows.Count, 1).Offset(1).EntireRow.Insert
Next
End Sub
EDIT
And if you want one row above the blank cells, replace ar.Cells(ar.Rows.Count, 1).Offset(1).EntireRow.Insert with ar.Cells(1, 1).EntireRow.Insert
For inserting two rows above the blank cells as per comment below
Sub InsertOneRowBelowBlankCells()
Dim BColBlnk As Range, ar As Range
Set BColBlnk = Range("B:B").SpecialCells(xlCellTypeBlanks)
For Each ar In BColBlnk.Areas
ar.Cells(1, 1).Resize(2, 1).EntireRow.Insert
Next
End Sub
In order to get all cells in column "B" until the last one, you can do this:
Last_Cell_In_B = Columns("B:B").SpecialCells(xlCellTypeLastCell).Row
Range("B1", "B" & Last_Cell_In_B).Select
Like this, you only add empty rows inside your array, not outside of it.
Your code works perfectly in a standard module, so I think you are trying to use its in a event case, in sheet "Attendance Audit Hastus" right? So you need to double click in your sheet icon in project tree and put this code:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim cell As Range
Dim MRange As Range
Set MRange = Range("B:B")
If Not Intersect(Target, MRange) Is Nothing Then
For Each cell In Target
MRange.SpecialCells(xlCellTypeBlanks).Select
Next cell
End If
Application.EnableEvents = False
Selection.EntireRow.Insert
Application.EnableEvents = True
End Sub
Note the Application.EnableEvents = False is used here to prevent prevent an infinite loop of cascading events. After the action you need to set Application.EnableEvents = True to return your normal process.

How to Autofilter then Copy and Paste Visible Cells only

I am trying to basically fill in any blank cells in column "AM" with values from column "AN" in a worksheet called "Operator" by assigning a shape to a macro with the following code. Please NOTE that the cells in An have an equation in them ,so I only want to copy the values.
Sub PendingChanges()
Range("AM1:AM10").CurrentRegion.AutoFilter Field:=1, Criteria1:="="
Worksheets("Operator").Range("AM1:AM10").SpecialCells(xlCellTypeVisible).Value = Worksheets("Operator").Range("AN1:AN10").Value
Selection.AutoFilter Field:=1
End Sub
I know that there is a "SpecialCells" method that displays visible cells only (so after autofiltering, it would display the blanks for me) but I'm not sure how to include it into my code!
The following screenshot is how the sheet will initially look: (in this example the cell values of AN3 and AN5 will paste into AM3 and AM5 respectively:
My code autofilters column "AN" for any blank cells, then tries to copy cells in AN and pastes the visible cells values into cells in AM
The result should be the following:
No need to filter here; you can just use SpecialCells(xlCellTypeBlanks), and then Offset on the result to refer to the same rows, but in column "AN".
Sub PendingChanges()
On Error Resume Next
Dim blankCells as Range
Set blankCells = Worksheets("Operator").Range("AM1:AM10").SpecialCells(xlCellTypeBlanks)
On Error GoTo 0
If Not blankCells Is Nothing Then
Dim rng as Range
For Each rng in blankCells.Areas
rng.Value = rng.Offset(,1).Value
Next
End If
End Sub
Some notes:
The On Error Resume Next and On Error GoTo 0 are needed since a SpecialCells(xlCellTypeBlanks) call will fail if there are no blanks. They temporarily disable and then re-enable error handling.
Areas are each distinct area of a non-contiguous range. For example, if blankCells refers to AM2 and AM4:AM5, then AM2 is the first area and AM4:AM5 is the second.
You need to loop through the areas because trying to value transfer .Value = .Value doesn't work correctly when there is more than one area.
You don't need to make the filters and then fill the blanks from next column
You may try the below code, it may directly solve your problem.
[VBA]
Sub test()
Dim rBlanks As Range
Set rBlanks = Nothing
With ThisWorkbook.Sheets("Operator")
On Error Resume Next
Set rBlanks = Intersect(.Range("AM:AM"), .UsedRange).SpecialCells(xlCellTypeBlanks)
On Error GoTo 0
If Not rBlanks Is Nothing Then
rBlanks.FormulaR1C1 = "=RC[1]"
Intersect(.Range("AM:AM"), .UsedRange).Copy
.Range("AM1").PasteSpecial (xlPasteValues)
Application.CutCopyMode = False
End If
End With
End Sub
[/VBA]

Log 'RTD' Value Changes in Another Worksheet

I'm having some issues finding an answer for this.
In Sheet1, I have a range of cells ("A4:Q4") that all have certain RTD functions in them, where they are gathering real time stock data from an outside program. These cells update every few seconds, depending on the changes from the parent program.
What I want to do, is have it so that every time any value in that range changes (ie. everytime the RTD values update), copy that range's values and paste them to the next available empty row in Sheet2. This should effectively create a long list of values, but I'm having an issue with the RTD stuff. My current code will do what I want, but only if the values in the range are changed manually, NOT when the RTD values get updated. Even when the RTD values are updating/changing, it's not copying those new values over to Sheet2, if that makes sense. It would seem it has something to do with the macro not realizing that the values are changing automatically. When I make my own changes to the values in that range, it works, but that renders the RTD functions in the cells useless.
Here is what I have:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim KeyCells As Range
' The variable KeyCells contains the cells that will
' cause an alert when they are changed.
Set KeyCells = Worksheets("Sheet1").Range("A4:Q4")
' Wait for change to happen...
If Not Application.Intersect(KeyCells, Range(Target.Address)) _
Is Nothing Then
' once change happens, copy the range (yes keep R4 value too)
ThisWorkbook.Worksheets("Sheet1").Range("A4:R4").Copy
' Paste it into the next empty row of Sheet2
With ThisWorkbook.Worksheets("Sheet2")
Dim NextRow As Range
Set NextRow = ThisWorkbook.Worksheets("Sheet2").Range("A" & .UsedRange.Rows.Count + 1)
NextRow.PasteSpecial Paste:=xlValues, Transpose:=False
Application.CutCopyMode = False
End With
End If
End Sub
I'm thinking a potential solution would be to make a loop where by it stores each value in that range, and then every half a second or 1 second it would compare the stored values to the "current" values and see if there's any change. If there is, copy that range's values to Sheet2. But this seems clunky.
Any ideas? Thanks!
As noted in the comments, the Worksheet.Change event doesn't fire when a cell changes value due to formula recalculation. So you can use the Worksheet.Calculate event.
Unlike the Worksheet.Change event, there is no Target in the Worksheet.Calculate event. You can test that a cell within your specific range has recalculated using the following:
In the ThisWorkbook code module:
Private Sub Workbook_Open()
PopulateKeyValueArray
End Sub
In the Sheet1 code module:
Private Sub Worksheet_Calculate()
On Error GoTo SafeExit
Application.EnableEvents = False
Dim keyCells As Range
Set keyCells = Me.Range("A4:Q4")
Dim i As Long
For i = 1 To UBound(KeyValues, 2)
If keyCells(, i).Value <> keyValues(1, i) Then
Dim lastRow As Long
With Sheet2
lastRow = .Cells(.Rows.Count, 1).End(xlUp).Row + 1
.Range("A" & lastRow & ":R" & lastRow).Value = Me.Range("A4:R4").Value
End With
Exit For
End If
Next i
SafeExit:
PopulateKeyValueArray
Application.EnableEvents = True
End Sub
In a normal code module:
Public keyValues()
Public Sub PopulateKeyValueArray()
keyValues = Sheet1.Range("A4:Q4").Value
End Sub
(1): keyValues is a Public array that is populated with the values in keyCells when the workbook first opens.
(2): When any cell changes due to formula recalculation in Sheet1, the values in keyCells are compared one-by-one to their corresponding element in keyValues. If there is a difference, i.e. a cell in keyCells has been updated, then the latest values in A4:R4 are written to the next available row in Sheet2. The Exit For ensures that this value transfer only happens once, even if multiple cells have changed. Finally, keyValues is updated with the latest values in keyCells.
(3): PopulateKeyValueArray reads the values from Sheet1:Range("A4:Q4") into the keyValues array.
Note that keyValues will be empty when you first add the code to your workbook, so either save and re-open, or run PopulateKeyValueArray to populate the array.

Find row indices of empty cells in a given range

Is it possible to write a vba macro that determines if there are any empty cells in a given range and returns the row number of that cell?
I'm new to vba and all that I managed to write after searching the internet was something that takes a range and colors every emty cell in it red:
Sub EmptyRed()
If TypeName(Selection) <> "Range" Then Exit Sub
For Each cell In Selection
If IsEmpty(cell.Value) Then cell.Interior.Color = RGB(255, 0, 0)
Next cell
End Sub
The macro does basically what I want, but instead of coloring the empty cell red I would like to know the row index of the empty cell.
A little background info: I have a very large file (about 80 000 rows) that contains many merged cells. I want to import it into R with readxl. Readxl splits merged cells, puts the value in the first split cell and NA into all others. But a completely empty cell would also be assigned NA, so I thought the best thing would be to find out which cells are empty with Excel, so that I know which NA indicate a merged cell or an empty cell. Any suggestions on how to solve this problem are very welcome, thanks!
Edit: To clarify: Ideally, I want to unmerge all cells in my document and fill each split cell with the content of the previously merged cell. But I found macros on the web that are supposed to do exactly that, but they didn't work on my file, so I thought I could just determine blank cells and then work on them in R. I usually don't work with Excel so I know very little about it, so sorry if my thought process is far too complicated.
To do exactly what you state in your title:
If IsEmpty(cell.Value) Then Debug.Print cell.Row
But there are also Excel methods to determine merged cells and act on them. So And I'm not sure exactly what you want to do with the information.
EDIT
Adding on what you say you want to do with the results, perhaps this VBA code might help:
Option Explicit
Sub EmptyRed()
Dim myMergedRange As Range, myCell As Range, myMergedCell As Range
Dim rngProcess As Range
With Application
.ScreenUpdating = False
.Calculation = xlCalculationManual
.EnableEvents = False
End With
Set rngProcess = Range("A1:B10")
For Each myCell In rngProcess
If myCell.MergeCells = True Then
Set myMergedRange = myCell.MergeArea
With myMergedRange
.MergeCells = False
.Value = myCell(1, 1)
End With
End If
Next myCell
With Application
.ScreenUpdating = True
.Calculation = xlCalculationAutomatic
.EnableEvents = True
End With
End sub
Note that I explicitly declare all variables, and I hard coded the range to check. There are various ways of declaring the range to be checked; using 'Selection' is usually rarely preferred.
Before anything else: From the opposite end of the spectrum, you can use Range.MergeCells or Range.MergeArea to determine if a Cell is part of a Merged Area. But, I digress...
You can use Cell.Row to get the row number. How you return or display that is up to you - could be a Message Box, a delimited string, or an array, or even a multi-area range.
A Sub cannot return anything once called, so you may want a Function instead, e.g. Public Function EmptyRed() As String
(Also, I would recommend you get in the habit of explicitly declaring all of your variables, and perhaps using Option Explicit too, before you run into a typo-based error. Just add Dim cell As Range at the top of the sub for now)
Sub FF()
Dim r, wksOutput As Worksheet
Dim cell As Range, rng As Range, rngArea As Range
With Selection
.UnMerge
'// Get only blank cells
Set rng = .SpecialCells(xlCellTypeBlanks)
'// Make blank cells red
rng.Interior.Color = vbRed
End With
'// Create output worksheet
Set wksOutput = Sheets.Add()
With wksOutput
For Each rngArea In rng.Areas
For Each cell In rngArea
r = r + 1
'// Write down the row of blank cell
.Cells(r, 1) = cell.Row
Next
Next
'// Remove duplicates
.Range("A:A").RemoveDuplicates Array(1), xlNo
End With
End Sub
There are a couple ways:
Sub EmptyRed()
Dim rgn,targetrgn as range
Dim ads as string ‘ return rgn address
Set targetrgn= ‘ your selection
For Each rgn In Targetrgn
If IsEmpty(rgn.Value) Then
‘1. Use address function, and from there you can stripe out the column and row
Ads=application.worksheetfunction.addres(cell,1)’ the second input control the address format, w/o $
‘2. Range.row & range.column
Ads=“row:” & rgn.row & “, col: “ & rgn.column
End if
Next rgn
End Sub
Ps: I edited the code on my phone and will debug further when I have a computer. And I am just more used to use “range” rather than “cell”.
To clarify: Ideally, I want to unmerge all cells in my document and fill each split cell with the content of the previously merged cell.
Cycle through all cells in the worksheet's UsedRange
If merged, unmerge and fill the unmerged area with the value from the formerly merged area.
If not merged but blank, collect for address output.
Sub fillMerged()
Dim r As Range, br As Range, mr As Range
For Each r In ActiveSheet.UsedRange
If r.Address <> r.MergeArea.Address Then
'merged cells - unmerge and set value to all
Set mr = r.MergeArea
r.UnMerge
mr.Value = mr.Cells(1).Value
ElseIf IsEmpty(r) Then
'unmerged blank cell
If br Is Nothing Then
Set br = r
Else
Set br = Union(br, r)
End If
End If
Next r
Debug.Print "blank cells: " & br.Address(0, 0)
End Sub

Resources