How to delete blank rows or rows with spaces in a table that has one or more cells vertically merged? - vba

I need a vba script or help on what I'm writing in order not to exit the iteration when the table contains vertically and horizontally merged cells.
Example of the table:
---------
| | | <-- I don't want these rows deleted, they can be skipped
|---| |
| | | <-- I don't want these rows deleted, they can be skipped
|---|---|
| | | <-- this must be checked for emptiness in order to decide to delete or not
|---|---|
| | | <-- this must be checked for emptiness in order to decide to delete or not
|---|---|
My script in VBA so far:
Public Sub DeleteEmptyRows()
Dim c As String
c = ""
Dim oTable As Table, oRow As Integer
' Specify which table you want to work on.
For Each oTable In ActiveDocument.Tables
For oRow = oTable.Rows.Count To 1 Step -1
'On Error GoTo NextIteration
MsgBox oTable.Rows(oRow).Range.Text
'If Len(oTable.Rows(oRow).Range.Text) = oTable.Rows(oRow).Cells.Count * 2 + 2 Then
If Len(Replace(oTable.Rows(oRow).Range.Text, Chr(13) & Chr(7), vbNullString)) = 0 Then
oTable.Rows(oRow).Delete
End If
Next oRow
Next oTable
MsgBox c
End Sub
How to reproduce the error:
Create a 5x5 table. Select cell(0,0) and cell(1, 0) and merge them. Select cell(0, 1) and cell(0, 2) and merge. Run the script and get the 5991 error..
The problem is that I get a run-time error 5991: Can't access to individual lines in this collection because there are vertically merged cells.
I really don't know what to do because if this error happens no row will be looked after. Usually my tables have a header that has vertically merged cells and the body rows are not, so I cannot do anything...
for Word.

This is what I came up with to delete all rows in a table which do not contain any merged cells and do not contain any text.
The problem with tables containing merged cells is not so much deleting the rows but identifying which cells are actually merged and then removing whats left.
The way I approached this was to loop through all the cells in table and for each row workout how many columns are counted (horizontally merged cells and cells vertically merged from above are ignored) and thanks to this page (http://word.mvps.org/FAQs/MacrosVBA/GetRowColSpan.htm)
if any of the cells in the row are the top of a vertically merged cell we can tell.
Finally we also check if there is any text in the row.
This is the code I came up with, hopefully with the comments it should be straightforward. Unfortunately due to the way Word deals with this stuff the cells have to Selected rather than just using ranges - this isn't ideal because it significantly slows things down. It has worked in all my tests.
Option Explicit
Public Sub DeleteEmptyRows()
Dim oTable As Table, oCol As Integer, oRows As Integer
Dim iMergeCount() As Integer, dCellData() As Double
Dim MyCell As Cell
Dim iCurrentRow As Integer, iRowCounter As Integer
'Watching this happen will slow things down considerably
Application.ScreenUpdating = False
' Specify which table you want to work on.
For Each oTable In ActiveDocument.Tables
'We need to store the number of columns to determine if there are any merges
oCol = oTable.Columns.Count
ReDim dCellData(1 To oTable.Rows.Count, 1 To 3)
'The first column will count the number of columns in the row if this doesn't match the table columns then we have merged cells
'The second column will count the vertical spans which tells us if a vertically merged cell begins in this row
'The third column will count the characters of all the text entries in the row. If it equals zero it's empty.
iCurrentRow = 0: iRowCounter = 0
For Each MyCell In oTable.Range.Cells
'The Information property only works if you select the cell. Bummer.
MyCell.Select
'Increment the counter if necessary and set the current row
If MyCell.RowIndex <> iCurrentRow Then
iRowCounter = iRowCounter + 1
iCurrentRow = MyCell.RowIndex
End If
'Check column index count
If MyCell.ColumnIndex > VBA.Val(dCellData(iRowCounter, 1)) Then dCellData(iRowCounter, 1) = MyCell.ColumnIndex
'Check the start of vertically merged cells here
dCellData(iRowCounter, 2) = dCellData(iRowCounter, 2) + (Selection.Information(wdEndOfRangeRowNumber) - Selection.Information(wdStartOfRangeRowNumber)) + 1
'Add up the length of any text in the cell
dCellData(iRowCounter, 3) = dCellData(iRowCounter, 3) + VBA.Len(Selection.Text) - 2 '(subtract one for the table and one for cursor(?))
'Just put this in so you can see in the immediate window how Word handles all these variables
Debug.Print "Row: " & MyCell.RowIndex & ", Column: " & MyCell.ColumnIndex & ", Rowspan = " & _
(Selection.Information(wdEndOfRangeRowNumber) - _
Selection.Information(wdStartOfRangeRowNumber)) + 1
Next MyCell
'Now we have all the information we need about the table and can start deleting some rows
For oRows = oTable.Rows.Count To 1 Step -1
'Check if there is no text, no merges at all and no start of a vertical merge
If dCellData(oRows, 3) = 0 And dCellData(oRows, 1) = oCol And dCellData(oRows, 2) = oCol Then
'Delete the row (we know it's totally unmerged so we can select the first column without issue
oTable.Cell(oRows, 1).Select
Selection.Rows.Delete
End If
Next oRows
Next oTable
Application.ScreenUpdating = True
End Sub

You should check in your conditions Range.MergeCells property, which will return TRUE in case cells in the range are merged.

Related

How to check if cell table in word contains bookmark?

I want to check if a table cell contains a bookmark, if so, get the name of that bookmark.
I used the method
Selection.Cells(1).Range.Bookmarks.Count
But the result is not correct, all cells in the same row return the same value, even though only 1 cell contains the bookmark.
However, when using Selection.Cells(1).Range.BookmarkID to check, only cells containing bookmarks will have a value of non-zero, cells without bookmarks will have a value of 0.
So I don't know how to determine the cell containing the bookmark.
I create a table with 3 rows and 5 columns and insert 3 bookmarks in column 2 and try the following test
Sub GetNameBookmark()
Dim i As Integer
Dim j As Integer
For i = 1 To 3
For j = 1 To 5
Debug.Print ActiveDocument.Tables(2).Rows(i).Cells(j).Range.BookmarkID & _
":" & ActiveDocument.Tables(2).Rows(i).Cells(j).Range.Bookmarks(1).Name
Next
Next
End Sub
Print results:
0:BM01
1:BM01
0:BM01
0:BM01
0:BM01
0:BM02
2:BM02
0:BM02
0:BM02
0:BM02
0:BM03
3:BM03
0:BM03
0:BM03
0:BM03
Seems pretty straightforward:
Sub GetNameBookmark()
With Selection.Cells(1).Range
If .BookmarkID > 0 Then Debug.Print .Bookmarks(1).Name
End With
End Sub

How to create a loop to read a range of cells and determine which have values and what is to the right of each

I'm trying to have a program that can read a range of cells which consist of 12 cells (let's say: P79, R79, T79, V79, X79, Z79, AB79, AD79, AF79, AH79, AJ79, AL79) and under those cells there are 6 cells (let's say: V81, X81, Z81, AB81, AD81, AF81), the program is looking for whether or not there are values typed in the cells within the described range.
The program should be able to read the cells from left to right on the top row and loop down to the bottom row and read that from right to left.
If all the cells in the top row have values in them, then the program breaks and doesn't read the values in the bottom row.
As the program reads the values from each cell it should create a table consisting of three columns (let's say: M88, N88, O88), the leftmost column should have the cell number (in order of cell as read by the program (whichever cell has a value first in the loop is given the number 1 and then the next cell to have a value is given number 2 etc.). The middle column should have whatever value is written in it's corresponding cell read from the range. The right column should have the value of whatever is to the right of each cell containing a value.
The first value to be read with a value should give the value "Left End" and the last value to read (whether or not it is the 12th cell to have a value in the top row or the leftmost cell to have a value in the bottom row) should give the value "Right end".
An example of what a row from the table could look like:
Cell # Cell Value Position/Left/Right
1 First Left End
This is the code I have so far:
Sub Code()
Dim wb As Workbook: Set wb = ThisWorkbook
Dim ws As Worksheet: Set ws = wb.Sheets("Sheet1")
Dim i As Integer, j As Integer, k As Integer
' First loop to compare a car to the rest after it
For i = 1 To 12
For j = i + 1 To 12
If Not IsEmpty(ws.Range("Cell_" & i)) And Not IsEmpty(ws.Range("Cell_" & j)) Then
ws.Range("B82").Offset(i).Value = j
Exit For
End If
Next j
Next i
' Loop backwards to find "Right End"
For k = 12 To 2 Step -1 '24 To 2
If Not IsEmpty(ws.Range("Cell_12")) Then
ws.Range("B82").Offset(12).Value = "Right End"
Exit For
' Has the "Right End" Follow when cars are left blank for lower row
ElseIf IsEmpty(ws.Range("Cell_" & k)) And Not IsEmpty(ws.Range("Cell_" & k - 1)) Then
ws.Range("B82").Offset(k - 1).Value = "Right End"
Exit For
End If
Next k
What I have here merely inserts a count into a cell range, what I'm trying to do is have my code actually read the cells in the range in the order I described and one at a time look at which cells have values written in them and look at which cells (with values in them) are to the right of any cell with a value and produce the table described above.
After reading your explanation, which was quite challenging I tried to recreate what you are asking.
I used cells A1:L1 with numbers 1 to 12. in the row below that A2:L2, some numbers have been added. with an if value <> "" you can see which cells contain a value.
In the second worksheet the table is made:
Sub test()
Dim a As Integer
Dim i As Integer
Dim name As String
ActiveWorkbook.Sheets(1).Activate
a = 1
For i = 1 To endcel
If Sheets(1).Range("a1").Offset(a, i - 1).Value <> "" Then
name = Sheets(1).Range("A1").Offset(a, i - 1).Value
Sheets(2).Activate
Sheets(2).Range("b2").Offset(i).Value = name
End If
Next i
End Sub
Does this help? You can adapt it a bit to your problem.
Good luck!

Prevent Vertically Merged Cells from Breaking Across Page - Automatically

I have to create documents that have large tables of data copied into them from Excel. The tables can be hundreds of rows long and generally ~20 columns wide. Many of the columns have been merged vertically to enhance readability and group sets of data.
I have been able to write a macro that will fully format the entire table, except I have not been able to figure out how to automatically prevent the Vertically Merged cells from breaking/splitting across multiple pages. To do it manually, you select all of the rows in the merger except for the last one and then you turn on "Keep With Next" in the paragraph settings. I thought this would be easy to do, but you can not access individual rows in VBA if there are any vertically merged cells in the table.
Does anyone have an idea how to automatically go through the rows and set the "Keep With Next" property for groups of rows that have been merged together?
Here is an example of how Word normally handles vertically merged cells across tables:
This is how I would like it to look, with doing all the work manually:
Yes, working with merged cells in Word (and Excel for that matter) is quite annoying.
This can be done, though, by accessing individual cells in table. I have written the following Sub Routine below that should work for you. I assumed that you had at least one column with no vertically merged cells in it and that you only had one column that controlled the length of the merged block. Although adding more controlling columns should be easy.
Sub MergedWithNext() 'FTable As Table)
Dim Tester As String
Dim FTable As Table
Dim i As Integer
Dim imax As Integer
Dim RowStart As Integer
Dim RowEnd As Integer
Dim CNMerged As Integer
Dim CNNotMerged As Integer
Dim CNMax As Integer
CNMerged = 2 'A column number that is vertically merged that you don't want to split pages
CNNotMerged = 1 'A column number that has no vertical mergers
Set FTable = Selection.Tables(1)
With FTable
imax = .Rows.Count
CNMax = .Columns.Count
'Start with no rows kept with next
ActiveDocument.Range(Start:=.Cell(1, 1).Range.Start, _
End:=.Cell(imax, CNMax).Range.End).ParagraphFormat.KeepWithNext = False
On Error Resume Next
For i = 2 To imax 'Assume table has header
Tester = .Cell(i, CNMerged).Range.Text 'Test to see if cell exists
If Err.Number = 0 Then 'Only the first row in the merged cell will exist, others will not
'If you are back in this If statement, then you have left the previous block of rows
'even if that was a block of one. The next If statement checks to see if the previous
'row block had more than one row. If so it applies the "KeepWithNext" property
If (RowEnd = (i - 1)) Then
'.Cell(RowStart, 1).Range.ParagraphFormat.KeepWithNext = True
ActiveDocument.Range(Start:=.Cell(RowStart, CNNotMerged).Range.Start, _
End:=.Cell(RowEnd - 1, CNNotMerged).Range.End).ParagraphFormat.KeepWithNext = True
'Use RowEnd - 1 because you don't care if the whole merged block stays with the next
'row that is not part of the merger block
End If
RowStart = i 'Beginning of a possible merger block
RowEnd = 0 'Reset to 0, not really needed, used for clarity
Else
RowEnd = i 'This variable will be used to determine the last merged row
Err.Clear
End If
If i = imax Then 'Last Row
If (RowStart <> imax) Then
ActiveDocument.Range(Start:=.Cell(RowStart, CNNotMerged).Range.Start, _
End:=.Cell(imax - 1, CNNotMerged).Range.End).ParagraphFormat.KeepWithNext = True
'Use imax - 1 because you don't care if the whole merged block stays with the next
'row that is not part of the merger block
End If
End If
Next i
On Error GoTo 0
End With
End Sub
This code will loop through each row in the table, excluding the header, looking for vertically merged cells. Once it finds a block, it will assign the "Keep With Next" property to each row in the block, except for the last row.

VBA Excel word search and copying formulas

I'm searching for a VBA macro for Excel, which can detect the word "mean", in column A. After this it would copy the yellow row with the formula in C to J.
The formula counts the average from one row after the last "mean" to the next =AVERAGE (C1323:C1437)
after every sixth mean there also needs to be Area and 150 copyied two rows after mean and I and J Need to be changed. Consequently I and J would refer to the cell A1441 in this case (=G1439/C1439*$A$1441) till the end of the file.
I'm not quite sure if it's easy or not but I'm totally overchallenged. I would be very thankful for help.
Sub Makro1()
'
' Makro1 Makro
'
' Tastenkombination: Strg+q
strSearchWord = "Mean"
i = Application.WorksheetFunction.CountIf(Range("A:A"), strSearchWord)
Y = 2
For x = i To 0
i = Application.WorksheetFunction.Match(strSuchWort, Range("A:A"), 0)
Range("C" & i).Select
Application.CutCopyMode = False
ActiveCell.FormulaR1C1 = "=AVERAGE(R[-147]C:R[-1]C)" ' that's still wrong, should be something like i-y?
Selection.AutoFill Destination:=Range("C" & i:"J" & i), Type:=xlFillDefault
Range("CY:JY").Select
i = Y
'for each fifth i
'Range("A" & i + 3).Select
' ActiveCell.FormulaR1C1 = "=RC[-2]/RC[-6]*R2159C1"
Next x
End Sub
it's still wrong, but my first draft.
#stucharo the Area correction is difficult to describe I've added a better Picture with formulas. I hpe that now it's understandable
If your line ActiveCell.FormulaR1C1 = "=AVERAGE(R[-147]C:R[-1]C)" needs to change the number of rows betwen means each time then you'll need to add a variable as you comment suggests. Also, just writing the string to the cells value (ActiveCell.Value) means that you will see it written as a formaula when you click the cell in the workbook (and it'll highlight the range etc.). You could try replacing it with:
ActiveCell.Value = "=AVERAGE(R[" & i - Y & "]C:R[-1]C)"
although since I can't see the first row of your sheet I'm not certain that'll give you the correct range of rows each time.
If your row number is likely to change and you are copying over the same number of columns each time then it might also be just as easy to write the formula directly to cells within a loop, rather than explicitly copying it.
Adding text after every 6th "mean" would require you to keep count of how many means had passed so far. This can be done by incrememnting a counter variable and using the Mod operator will tell you the remainder after a division. Therefor numberOfMeans Mod 6 will give you the remainder when divided by 6 and when this equals zero you know you have a multiple of 6. I've tried to capture all this into the code below.....
Sub Test()
Application.ScreenUpdating = False
Dim startRow As Integer
startRow = 2
Dim endrow As Integer
endrow = Range("A2").End(xlDown).row
Dim lastMeanRow As Integer
lastMeanRow = startRow - 1
Dim areaRow as Integer
areaRow = lastMeanRow + 3
Dim meanCounter As Integer
meanCounter = 0
Dim avgColHeight As Integer
Dim col As Integer
Dim row As Integer
'Check each row in the sheet
For row = startRow To endrow
'Cols i and j in every row need to be modified
For col = 9 To 10
Cells(row, col).Value = "=RC[-2]/RC[-6]*R" & areaRow & "C1"
Next col
'If column 1 of that row contains "mean" then
If Cells(row, 1).Value = "mean" Then
'Calculate the column height to average over....
avgColHeight = row - lastMeanRow - 1
'...and loop through each of the columns....
'(including i and j to add average)
For col = 3 To 10
'....inserting the averaging formula.
Cells(row, col).Value = "=AVERAGE(R[-" & avgColHeight & "]C:R[-1]C)"
Next col
'Then increment the counter to keep track of the number of means
meanCounter = meanCounter + 1
'If the number of means is a multiple of 6 then
If (meanCounter Mod 6 = 0) Then
'insert the "Area" and "150" strings
Cells(row + 2, 1).Value = "Area"
Cells(row + 3, 1).Value = "150"
areaRow = row + 3
End If
'Finally change the lastMeanRow to the mean row we have just processed.
lastMeanRow = row
End If
'Do it again until we reach the end of the data
Next row
Application.ScreenUpdating = True
End Sub
I also noticed your point on the value of area changing periodically. Writing this programatically, as above, will aloow you to add some logic over the value of "Area" and when it changes.
You clearly have a long list of data and want to automate the creation of the rows and formulas you describe.
It is possible write VBA to scan through the data and modify the formulas etc but first I would question if this is the best approach to give you what you need.
Excel has a feature called "pivot tables" which essentially allows you to summerise data in a list.
for instance if the list had one row for each city in the world and gave the population in the city, and a column gave which country it was in. A pivot table could be used to create the average population for a country of the countries cities. I suspect you are doing this sort of thing.
If you don't know about pivot tables you should find out about them. See here
In your case your mean row is summeriseing data in the rows above it. To use pivot tables you would have to have a column that defined which group each row is in. You pivot table would sue this column as a row summary and you would then create the average for all the other column.
#Nathalie. It's hard to help without knowing more. eg Is the data delivered with the mean text already inserted. It looks like column A has a number the represent the row number within the group (and this could be used by a formula to create the "Group Name" column you need for pivot tables.
You can get the pivot tables to do the area adjustment by:
Creating a new set of columns which contains formulas that cause the values in columns C to J to be copied except for when it is the 6th set of data in which case you adjust the values in C to J accordingly).
You probably need to introduce columns that:
A. give the "group name"
B. give a count of which group it is in so every 6th you can do the adjustment you need.
4 by using pivot tables and basic techniques you will find it easie rot update the refresh the data, should you need to.

EXCEL VBA- Average all rows containing numerical values for each column in a Merged Area

I have multiple spreadsheets that each roughly look like this:
I'm trying to find a way to go through each of the SPEAKER HEADERS in Row 1, and summarize the scores that are associated with the corresponding survey question ("Was the CONTENT good? Was the SPEAKER relevant? What the DELIVERY good?) grouped by color.
I can't think of a clever way of doing this automatically.
I can get the RANGE SPANS of the Merged Cells like this:
For Each Cell In src_sheet.UsedRange.Cells
If Cell.Row = 1 And IsEmpty(Cell) = False Then
MsgBox Cell.MergeArea.Address
End If
Next
I then need to iterate over the range provided by the address, getting the numerical values in all the rows BELOW that range.
For example, running the current macro produces this:
I need to take $C$1:$E$1 and run a for loop that say FROM C1 to E1 average all the numbers in the rows below it. I have no idea how to do this.
I was thinking about augmenting the selection in include everything used
Is there a better way to do this?
This is the tragically bad way I'm doing it now (which I'm quite proud of on account of being new to excel):
For Each Cell In src_sheet.UsedRange.Cells
If Cell.Row = 1 And IsEmpty(Cell) = False Then
Set rng = Range(Cell.MergeArea.Address) 'Equal to the Address of the Merged Area
startLetter = Mid(rng.Address, 2, 1) 'Gets letter from MergeArea Address
endLetter = Mid(rng.Address, 7, 1) 'Gets letter from MergeArea Address
On Error GoTo ErrHandler:
Set superRange = Range(startLetter & ":" & endLetter)
ErrHandler:
endLetter = startLetter
Set superRange = Range(startLetter & ":" & endLetter)
Resume Next
superRange.Select
MsgBox Application.Average(Selection)
In order to get rid of the error you are having, you need to change:
Set rng = Cell.MergeArea.Address
to
Set rng = Range(Cell.MergeArea.Address)
Ideally, this data would be better stored in a database so that it could be queried easily. If that's not an option, then the way you are going at it in Excel is as valid as most any other approach.
EDIT
Once you obtain the address of the left-most column for each of your speakers, you can loop through each column to obtain averages.
'Number of columns in the current speaker's range.
numColumns = rng.Columns.Count
'First row containing data.
currentRow = 4
'First column containing data.
firstColumn = rng.Column
'Loop through each column.
For col = firstColumn to firstColumn + (numColumns -1)
totalValue = 0
'Loop through each row.
Do While Cells(currentRow,col).value <> ""
totalValue = totalValue + Cells(currentRow,col).Value
currentRow = currentRow + 1
Loop
averageValue = totalValue / (currentRow - 3)
'Reset the currentRow value to the top of the data area.
currentRow = 4
'Do something with this average value before moving on to the next column.
Next
If you don't know what row is the start of your data, you can keep checking every row below rng.Row until you hit a numeric value.
The method above assumes that you have no blank entries in your data area. If you have blank entries, then you should either sort the data prior to running this code, or you would need to know how many rows you must check for data values.