How to split cells containing "hard returns" - vba

How to split cells containing "hard returns" (paragraph marks)
as in the picture below?
the desired result:
Here is my code
Sub SplitCells()
'
Dim selT As String
Dim arr
Dim i As Integer
selT = selection.Range.Text
arr = Split(selT, ChrW(13))
selection.Range.Cut
selection.Cells.Split NumRows:=UBound(arr) + 1, NumColumns:=1, MergeBeforeSplit:=False
selection.MoveDown wdLine, 1
For i = UBound(arr) To 0 Step -1
selection.MoveUp wdLine, 1
selection.TypeText arr(i)
Next
End Sub
It works, but I feel this code is clumsy and hope someone can tell me a elegant way.

Try the following; it will split all affected rows in the selected table.
Sub Demo()
Application.ScreenUpdating = False
Dim Tbl As Table, RngA As Range, RngB As Range
Dim i As Long, l As Long, r As Long, c As Long, p As Long
With Selection
If .Information(wdWithInTable) = False Then
MsgBox "Please select a table/cell and try again."
Exit Sub
End If
Set Tbl = .Tables(1)
With Tbl
l = .Columns.Count
For i = .Range.Cells.Count To 1 Step -1
With .Range.Cells(i).Range
Do While .Characters.Last.Previous = vbCr
.Characters.Last.Previous = vbNullString
Loop
End With
Next
For r = .Rows.Count To 1 Step -1
With .Rows(r)
If .Range.Paragraphs.Count > l + 1 Then
For c = 1 To .Cells.Count
If .Cells(c).Range.Paragraphs.Count > p Then p = .Cells(c).Range.Paragraphs.Count
Next
If p > 1 Then .Cells.Split Numrows:=p, Numcolumns:=1, MergeBeforeSplit:=False
For c = 1 To .Cells.Count
Set RngA = .Cells(c).Range
If RngA.Paragraphs.Count > 1 Then
For p = RngA.Paragraphs.Count To 2 Step -1
Set RngB = RngA.Paragraphs(p).Range
RngB.End = RngB.End - 1
If Len(RngB.Text) > 0 Then
With Tbl.Cell(r + p - 1, c).Range
.FormattedText = RngB.FormattedText
RngB.Delete
End With
End If
RngA.Paragraphs(p - 1).Range.Characters.Last = vbNullString
Next
End If
Next
End If
End With
Next
End With
End With
Application.ScreenUpdating = True
End Sub
Compared to your approach, the above code also has the advantage of preserving any text formatting.

There's nothing wrong with it, really. In order to move up/down in a table with split/merged cells you need Selection...
Here's code that uses the object model instead of Selection as much as possible. But I'm not sure I'd term it "more elegant" or "less clumsy". Possibly, it's more self-documenting since it uses Word objects where ever possible.
One change I did make is to test whether the selection is in a table before doing anything. If the user would forget to select a cell without such a test a cryptic error messsage would display, which is always annoying...
Sub SplitCells()
'
Dim cel As Word.Cell
Dim selT As String
Dim arr
Dim i As Integer
Dim nrCells As Long
If Selection.Information(wdWithInTable) Then
Set cel = Selection.Cells(1)
selT = cel.Range.Text
arr = Split(selT, ChrW(13))
nrCells = UBound(arr)
cel.Range.Delete
cel.Split NumRows:=nrCells, NumColumns:=1 ', _
'MergeBeforeSplit:=False
cel.Select
Selection.MoveDown wdLine, nrCells - 1
For i = nrCells - 1 To 0 Step -1
Set cel = Selection.Cells(1)
cel.Range.Text = arr(i)
cel.Select
Selection.MoveUp wdLine, 1
Next
Else
MsgBox "Please select a table cell and try again."
End If
End Sub

Related

Add row when cell in table is not empty

This is the code that I have right now.
Do While IsEmpty(objTable.cell(intNoOfRow,2).Range.Text) = False
intNoOfRows = intNoOfRows + 1
Loop
Any help would be greatly appreciated.
IsEmpty tests to see if a Variant has a type of empty. It will always return False for a String. Test against vbNullString instead:
Do While objTable.cell(intNoOfRow,2).Range.Text = vbNullString
intNoOfRows = intNoOfRows + 1
Loop
We cannot use this code:
IsEmpty(objTable.cell(intNoOfRow,2).Range.Text) will always True, so the code will run into Infinite Loop. While Len(.Cell(intNoOfRows, 2).Range.Text) <>0 always return True too. We are not sure if this is by design or bug, but the new cell always contains character. Cell(intNoOfRow, 2).Range.Text = vbNullString doesn't work too.
Find a worked way on my side(InStr( [start], string, substring, [compare] ) ):
With objTable
Dim strCellText As String
strCellText = .Cell(intNoOfRow, 2).Range.Text
Do While InStr(1, strCellText, "", vbBinaryCompare) <> 1
MsgBox "OK"
Loop
End With
Some old try:
With objTable
For Each c In .Range.Rows
intNoOfRows = intNoOfRows + 1
Next
End With
Or
You can use the .Range.Rows.Count, but the following code still need to adjust
With objTable
Do While intNoOfRows < .Range.Rows.Count
intNoOfRows = intNoOfRows + 1
'MsgBox .Range.Rows.Count
'If intNoOfRows > .Range.Rows.Count Then
' Exit Do
'End If
Loop
End With
Try something based on:
Sub Demo()
Dim r As Long, i As Long
With ActiveDocument.Tables(1)
For r = 1 To .Rows.Count
With .Rows(r)
If Len(.Range.Text) = .Cells.Count * 2 + 2 Then i = i + 1
End With
Next
End With
MsgBox "There are " & i & " empty rows in the table"
End Sub
Similarly, to add a row before a non-empty row, you might use something like:
Sub Demo()
Dim r As Long, i As Long
With ActiveDocument.Tables(1)
For r = 1 To .Rows.Count
If Len(.Rows(r).Range.Text) > .Rows(r).Cells.Count * 2 + 2 Then
.Rows.Add .Rows(r)
End If
Next
End With
End Sub

Deleting blank pages in large documents

We use SAS to output to rtf files then compile a load together, creating very large documents in most cases. But when compiling these outputs into one single document blank pages are sometimes created and we need to go through searching for and deleting these pages.
The code I have so far seems to work for the most part, but I am coming across a memory error on some occasions and wondering if anyone has suggestions to improve the code?
The following is the start of the code which is where I am getting the memory error on some occasions:
Sub BlankPage()
Application.ScreenUpdating = False
Application.Options.Pagination = False
Dim i As Long, Rng As Range, Blank As Integer, Page() As Long
With ActiveDocument
Blank = 0
For i = 1 To .ComputeStatistics(wdStatisticPages)
Set Rng = .GoTo(What:=wdGoToPage, Name:=i)
Set Rng = Rng.GoTo(What:=wdGoToBookmark, Name:="\page")
If Len(Rng) < 3 Then
Blank = Blank + 1
ReDim Preserve Page(Blank)
If Blank = 1 Then
Page(Blank) = Rng.Information(wdActiveEndAdjustedPageNumber)
Else
Page(Blank) = ((Rng.Information(wdActiveEndAdjustedPageNumber) - Blank) + 1)
End If
End If
Next i
Application.Options.Pagination = True
Application.ScreenUpdating = True
If Blank = 1 Then
MsgBox "Found " & Blank & " page to check"
Else
MsgBox "Found " & Blank & " pages to check"
End If
Dim j As Long, StrPages As String, intResponse As Integer, No As Long
No = 0
If Blank > 0 Then
For j = 1 To Blank
Set Rng = .GoTo(What:=wdGoToPage, Name:=((Page(j) + No))
Set Rng = Rng.GoTo(What:=wdGoToBookmark, Name:="\page")
Selection.GoTo(What:=wdGoToPage, Which:=wdGoToNext, Name:=(Page(j) + No))
intResponse = MsgBox("Are you sure you want to delete this page?", vbYesNo)
If intResponse = vbYes Then
StrPages = StrPages & " " & Rng.Information(wdActiveEndAdjustedPageNumber)
Rng.Delete
Else
No = No + 1
End If
Next j
End If
End With
This is somewhat messy and more of a hack because I don't have your data (and therefore I am not sure what Len(rng) < 3 is about, but you can add another loop to declare your array size one time, instead of doing it for each loop.
This can either result in a performance loss or performance gain (I would think gain since you are not constantly using ReDim Preserve).
Sub BlankPage()
Application.ScreenUpdating = False
Application.Options.Pagination = False
Dim i As Long, Rng As Range, Blank As Integer, Page() As Long
With ActiveDocument
Blank = 0
'Get the array size ONE time only
For i = 1 To .ComputeStatistics(wdStatisticPages)
If Len(Rng) < 3 Then Blank = Blank + 1
Next
ReDim Page(Blank) '< -- You ReDim only once -- <
Blank = 0
For i = 1 To .ComputeStatistics(wdStatisticPages)
Set Rng = .GoTo(What:=wdGoToPage, Name:=i)
Set Rng = Rng.GoTo(What:=wdGoToBookmark, Name:="\page")
If Len(Rng) < 3 Then
blank = blank + 1
If Blank = 1 Then '< -- remove the ReDim preserve -- <
Page(Blank) = Rng.Information(wdActiveEndAdjustedPageNumber)
Else
Page(Blank) = ((Rng.Information(wdActiveEndAdjustedPageNumber) - Blank) + 1)
End If
End If
Next i
Application.Options.Pagination = True
Application.ScreenUpdating = True

Deleting "empty" rows when they just "appear empty"

I can not manage to cleanse my data of the "empty" rows. There is no problem in deleting the "0" but those cells which are empty are not empty but have something like "null strings" in it.
Sub Reinigung()
Application.ScreenUpdating = False 
Application.EnableEvents = False 
ListeEnde3 = ThisWorkbook.Sheets("input").Cells(Rows.Count, 1).End(xlUp).Row
For Zeile1 = 2 To ListeEnde3
If ThisWorkbook.Sheets("input").Cells(Zeile1, 14) = "0" Or ThisWorkbook.Sheets("2018").Cells(Zeile1, 14) = "" Then
ThisWorkbook.Sheets("input").Rows(Zeile1).Delete
Zeile1 = Zeile1 - 1
Else
End If
Next
' ThisWorkbook.Sheets("import").Columns(14).SpecialCells(xlCellTypeBlanks).EntireRow.Delete
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub
That code just freezes my excel, if i leave out the
thisWorkbook.Sheets("2018").Cells(Zeile1, 14) = ""
part, it works and deletes all rows, where colum 14 contains a "0".
If I check the cells which appear blank with =isblank it returns "false". There is no "space" in the cell and no " ' ".
What to do?
edit
After the first tips my code looks like this now:
Sub Reinigung()
Dim ListeEnde3 As Long
Dim Zeile1 As Long
Application.ScreenUpdating = False 
Application.EnableEvents = False 
ListeEnde3 = ThisWorkbook.Sheets("import").Cells(Rows.Count, 1).End(xlUp).Row
For Zeile1 = ListeEnde3 To 2 Step -1
Set rngX = ThisWorkbook.Sheets("import").Cells(Zeile1, 14)
If (rngX = "0" Or rngX = "") Then 'or rngY = vbNullString
ThisWorkbook.Sheets("import").Rows(Zeile1).Delete
End If
Next Zeile1
' ThisWorkbook.Sheets("import").Columns(14).SpecialCells(xlCellTypeBlanks).EntireRow.Delete
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub
Excel still crashes / freezes (I waited for 5 minutes) but since the code runs "smoothly" with F8 I wanted to give it a shot with less data: It works!
If I am not reducing the data there are ~ 70000 rows to check. I let it run on 720 rows and it worked.
Any way to tweak the code in a way that it can handle the 70000+ rows? I didn't think that it would be too much.
Thanks!
You can use AutoFilter and delete the visible rows (not tested) :
Dim ws As Worksheet: Set ws = ThisWorkbook.Worksheets("import")
ws.UsedRange.AutoFilter 14, Array("=0", "="), xlFilterValues
ws.UsedRange.Offset(1).EntireRow.Delete
ws.AutoFilterMode = False
Another way is to simply use internal arrays and write out the new data set which has valid rows.
It is very fast.
If your dataset has formulas then you'll have to use extra code, but if it's constants only, then the below should do:
Sub Reinigung()
'Here I test with column E to Z, set Ranges appropriately
Application.ScreenUpdating = False
Application.EnableEvents = False
Dim ListeEnde3 As Long, x As Long, y As Long
'last row of data - set to column of non-blank data
ListeEnde3 = ThisWorkbook.Sheets("import").Cells(Rows.Count, 5).End(xlUp).Row
Dim ws As Worksheet
Set ws = ThisWorkbook.Sheets("import")
Dim startCell As Range
'set to whatever cell is the upper left corner of data
Set startCell = ThisWorkbook.Sheets("import").Range("E1")
Dim arr As Variant, arrToPrint() As Variant
'Get rightmost column of data instead of hardcoding to "Z"
'write dataset into an array
arr = ws.Range(startCell, ws.Range("Z" & ListeEnde3)).Value
x = UBound(arr) - LBound(arr) + 1 'num of rows of data
y = UBound(arr, 2) - LBound(arr, 2) + 1 'num of columns of data
ReDim arrToPrint(1 To x, 1 To y) 'array to hold valid/undeleted data
Dim i As Long, j As Long, printCounter As Long, arrayColumnToCheck as Long
arrayColumnToCheck = 14 - startCell.Column + 1 '14 is column N
For i = 1 To x
If arr(i, arrayColumnToCheck ) <> 0 And arr(i, arrayColumnToCheck ) <> vbNullString Then
printCounter = printCounter + 1
For j = 1 To y
'put rows to keep in arrToPrint
arrToPrint(printCounter, j) = arr(i, j)
Next j
End If
Next i
'Print valid rows to keep - only values will print - no formulas
startCell.Resize(printCounter, y).Value = arrToPrint
'Delete the rows with zero & empty cells off the sheet
startCell.Offset(printCounter).Resize(ListeEnde3 - printCounter, y).Delete xlShiftUp
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub
You can add IsEmpty to your code to check the cells filling
Sub Reinigung()
Application.ScreenUpdating = False
Application.EnableEvents = False
ListeEnde3 = ThisWorkbook.Sheets("input").Cells(Rows.Count, 1).End(xlUp).Row
For Zeile1 = 2 To ListeEnde3
Set rngX = ThisWorkbook.Sheets("input").Cells(Zeile1, 14)
Set rngY = ThisWorkbook.Sheets("2018").Cells(Zeile1, 14)
If (rngX = "0" And (Not IsEmpty(rngX))) Or (rngY = "") Then
ThisWorkbook.Sheets("input").Rows(Zeile1).Delete
Zeile1 = Zeile1 - 1
End If
Next
' ThisWorkbook.Sheets("import").Columns(14).SpecialCells(xlCellTypeBlanks).EntireRow.Delete
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub
NEVER a good idea to alter a loop counter: Zeile1 = Zeile1 - 1
Instead start at the end and use Step -1 in your loop to work backward.
You are in a infinite loop because the loop doesnt move forward. If Zeile=3 and there is a "" in row3 in the '2018' sheet, then it will always be stuck on the Zeile1 = 3 line. You will always be coming back to that "" on row 3 in '2018'sheet.
For Zeile1 = ListeEnde3 To 2 Step -1
Set rngX = ThisWorkbook.Sheets("input").Cells(Zeile1, 14)
Set rngY = ThisWorkbook.Sheets("2018").Cells(Zeile1, 14)
If (rngX = "0" Or rngY = "") Then 'or rngY = vbNullString
ThisWorkbook.Sheets("input").Rows(Zeile1).Delete
End If
Next Zeile1

Can I make this code execution time shorter?

This code takes more than 10 seconds to finish. Is there a faster way to do this?
If a particular cell in a row consist of "H" character then hide the entire row and also explain the contents of the cell with a given background color here, it's index code is 19.
Option Explicit
Sub TailoredInputs()
Dim ws As Worksheet
Dim i, j, l As Integer
Set ws = Sheets("Inputs")
Application.ScreenUpdating = False
Range("A7:A200").EntireRow.Hidden = False
With ws
.Select
j = 10
Do While j <= 149
If .Cells(j, "J").Value = "H" Then
For l = 4 To 9
If .Cells(j, l).Interior.ColorIndex = 19 Then
.Cells(j, l).ClearContents
Else: End If
Next l
.Cells(j, "J").EntireRow.Hidden = True
Else: End If
If .Cells(j, "K").Value = "H" Then
For l = 4 To 9
If .Cells(j, l).Interior.ColorIndex = 19 Then
.Cells(j, l).ClearContents
Else: End If
Next l
.Cells(j, "J").EntireRow.Hidden = True
Else: End If
j = j + 1
Loop
Range("Spendinginput").Select
End With
Application.ScreenUpdating = True
End Sub
Untested:
Sub TailoredInputs()
Dim ws As Worksheet
Dim i, j, l As Integer, rngHide As Range
Set ws = Sheets("Inputs")
Application.ScreenUpdating = False
ws.Range("A7:A200").EntireRow.Hidden = False
For j = 10 To 149
If ws.Cells(j, "J").Value = "H" Or ws.Cells(j, "K").Value = "H" Then
For l = 4 To 9
If ws.Cells(j, l).Interior.ColorIndex = 19 Then
ws.Cells(j, l).ClearContents
End If
Next l
'build the range which will be hidden
If rngHide Is Nothing Then
Set rngHide = ws.Cells(j, 1)
Else
Set rngHide = Application.Union(rngHide, ws.Cells(j, 1))
End If
End If
Next j
'anything to hide? Hide it.
If Not rngHide Is Nothing Then rngHide.EntireRow.Hidden = True
ws.Range("Spendinginput").Select
Application.ScreenUpdating = True
End Sub
The first thing I'd be looking at would be getting rid of the explicit loop for rows 10 through 149.
You could instead use the Range.Find method to locate the first cell containing H in the range you're interested in. As with all potential optimisations, you should check it but I would imagine Excel searching for a value under the covers might be faster than checking every single cell manually.
For example, consider this code:
Option Explicit
Public Declare PtrSafe Function GetTickCount Lib "kernel32.dll" () As Long
Sub Macro1()
Dim ws As Worksheet
Dim j As Integer
Dim t As Long
Dim x As Range
If False Then ' or use true for explicit loop '
t = GetTickCount
j = 1
Do While j <= 9999
If Worksheets(1).Cells(j, 1).Value = "H" Then
MsgBox ("found it " & j & " " & (GetTickCount - t))
j = 10000
End If
j = j + 1
Loop
Else
t = GetTickCount
Set x = Range("A1:A9999").Find("H")
MsgBox ("found it " & x.Row & " " & (GetTickCount - t))
End If
End Sub
With true in the if statement (explicit loop) and a worksheet with nothing but a H in cell A9999, it takes about 46 milliseconds to find the value. Using the Range.Find() method drops that to zero.

Searching over multiple columns in excel vba

I am able to search a text in column A of my spreadsheet by using this
With WB.Sheets("MySheet")
Set FindRow = .Range("A:A").Find(What:="ProjTemp1", LookIn:=xlValues)
End With
After which I can get the row number by doing FindRow.Row
How do I then get back the row number where Column A == "ProjTemp1" && Column B == "ProjTemp2" && Column C == "ProjTemp3"
Try to use Autofilter:
Dim rng As Range
'disable autofilter in case it's already enabled'
WB.Sheets("MySheet").AutoFilterMode = False
With WB.Sheets("MySheet").Range("A1:C1")
'set autofilter'
.AutoFilter Field:=1, Criteria1:="=ProjTemp1"
.AutoFilter Field:=2, Criteria1:="=ProjTemp2"
.AutoFilter Field:=3, Criteria1:="=ProjTemp3"
End With
With WB.Sheets("MySheet")
On Error Resume Next
Set rng = .Range("A2:A" & .Rows.Count).Rows.SpecialCells(xlCellTypeVisible)
On Error GoTo 0
End With
If Not rng Is Nothing Then
MsgBox rng.Row ' returns first visible row number
End If
WB.Sheets("MySheet").AutoFilterMode = False 'disable autofilter'
An alternative suggestion is to just loop through the table and use nested if-statements like this:
Sub ReturnRowNumber()
Dim i As Long, GetRow As Long
For i = 2 To Sheets("MySheet").Cells(Rows.Count, 1).End(xlUp).Row
'Criteria search
If Sheets("MySheet").Cells(i, 1).Value = "ProjTemp1" Then
If Sheets("MySheet").Cells(i, 2).Value = "ProjTemp2" Then
If Sheets("MySheet").Cells(i, 3).Value = "ProjTemp3" Then
'Returns row
GetRow = i
End If
End If
End If
Next i
End Sub
Just posted similar reply at MSDN and wanted to share here if anyone is still using VBA. The function for multiple match that works pretty fast.
It might help a lot if you are interested in effective code since using Application.Match() is much much faster that Find() or INDEX() method or simple looping.
The syntax is the same as COUNTIFS() but it returns the match index instead of counting.
Public Function MultiMatch(ParamArray X0() As Variant) As Variant
MultiMatch = CVErr(xlErrNA)
If UBound(X0) = -1 Then Exit Function
On Error GoTo ErrorHandler
Set Xws = X0(1).Parent
X_rFrow = X0(1)(1, 1).Row
X_rLrow = X_rFrow + X0(1).Rows.Count - 1
jLAST = UBound(X0)
l = X_rFrow
j = 0
Do While IsError(MultiMatch) And j + 1 <= jLAST And Not IsError(X1)
jCOL = X0(j + 1).Column
Set TRNG = Xws.Range(Xws.Cells(l, jCOL), Xws.Cells(X_rLrow, jCOL))
X1 = Application.Match(X0(j), TRNG, 0)
If Not IsError(X1) Then
l = TRNG(X1).Row
If X1 = 1 Then
If j + 1 = jLAST Then
MultiMatch = l - X_rFrow + 1
Else
j = j + 2
End If
Else
j = 0
End If
End If
Loop
Exit Function
ErrorHandler:
MultiMatch = CVErr(xlErrName)
End Function
This can work in such a way that X amount of values ​​to search are Y columns to search for X values ​​in a row, having 0 as a result of nothing and Row>= 1 the row that has the X amount of values ​​per column in the same row.
Public Function find(sheetName As String, initCol As Integer, initRow As Integer, ParamArray values()) As Variant
Dim i As Long, GetRow As Long
On Error GoTo nextRow
For i = initRow To Sheets(sheetName).cells(Rows.Count, 1).End(xlUp).row
For ii = 0 To UBound(values)
If Sheets(sheetName).cells(i, initCol + ii).Value2 = values(ii) Then
GetRow = ii
If ii = UBound(values) Then
find = i
Exit Function
End If
GoTo nextCol
End If
If ii = 0 Then GoTo nextRow
nextCol:
Next ii
nextRow:
Next i
endFind:
find = GetRow
End Function
Use :
vRow = find("sheet", 1, 1, "test", "test1","test2")
"sheet" = sheetName, 1 = Col index start, 1 = row number start, ["test","test1","test2"] is ParamArray
"find" Function will search "test" in colunm A, "test1" in B &
"test2" in C and it will return the row number that has these values
​​followed in the same row