Excel / VBA - get first normal form (1NF) [duplicate] - vba

I have values in column B separated by commas. I need to split them into new rows and keep the other data the same.
I have a variable number of rows.
I don't know how many values will be in the cells in Column B, so I need to loop over the array dynamically.
Example:
ColA ColB ColC ColD
Monday A,B,C Red Email
Output:
ColA ColB ColC ColD
Monday A Red Email
Monday B Red Email
Monday C Red Email
Have tried something like:
colArray = Split(ws.Cells(i, 2).Value, ", ")
For i = LBound(colArray) To UBound(colArray)
Rows.Insert(i)
Next i

Try this, you can easily adjust it to your actual sheet name and column to split.
Sub splitByColB()
Dim r As Range, i As Long, ar
Set r = Worksheets("Sheet1").Range("B999999").End(xlUp)
Do While r.row > 1
ar = Split(r.value, ",")
If UBound(ar) >= 0 Then r.value = ar(0)
For i = UBound(ar) To 1 Step -1
r.EntireRow.Copy
r.Offset(1).EntireRow.Insert
r.Offset(1).value = ar(i)
Next
Set r = r.Offset(-1)
Loop
End Sub

You can also just do it in place by using a Do loop instead of a For loop. The only real trick is to just manually update your row counter every time you insert a new row. The "static" columns that get copied are just a simple matter of caching the values and then writing them to the inserted rows:
Dim workingRow As Long
workingRow = 2
With ActiveSheet
Do While Not IsEmpty(.Cells(workingRow, 2).Value)
Dim values() As String
values = Split(.Cells(workingRow, 2).Value, ",")
If UBound(values) > 0 Then
Dim colA As Variant, colC As Variant, colD As Variant
colA = .Cells(workingRow, 1).Value
colC = .Cells(workingRow, 3).Value
colD = .Cells(workingRow, 4).Value
For i = LBound(values) To UBound(values)
If i > 0 Then
.Rows(workingRow).Insert xlDown
End If
.Cells(workingRow, 1).Value = colA
.Cells(workingRow, 2).Value = values(i)
.Cells(workingRow, 3).Value = colC
.Cells(workingRow, 4).Value = colD
workingRow = workingRow + 1
Next
Else
workingRow = workingRow + 1
End If
Loop
End With

This will do what you want.
Option Explicit
Const ANALYSIS_ROW As String = "B"
Const DATA_START_ROW As Long = 1
Sub ReplicateData()
Dim iRow As Long
Dim lastrow As Long
Dim ws As Worksheet
Dim iSplit() As String
Dim iIndex As Long
Dim iSize As Long
'Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
With ThisWorkbook
.Worksheets("Sheet4").Copy After:=.Worksheets("Sheet4")
Set ws = ActiveSheet
End With
With ws
lastrow = .Cells(.Rows.Count, ANALYSIS_ROW).End(xlUp).Row
End With
For iRow = lastrow To DATA_START_ROW Step -1
iSplit = Split(ws.Cells(iRow, ANALYSIS_ROW).Value2, ",")
iSize = UBound(iSplit) - LBound(iSplit) + 1
If iSize = 1 Then GoTo Continue
ws.Rows(iRow).Copy
ws.Rows(iRow).Resize(iSize - 1).Insert
For iIndex = LBound(iSplit) To UBound(iSplit)
ws.Cells(iRow, ANALYSIS_ROW).Offset(iIndex).Value2 = iSplit(iIndex)
Next iIndex
Continue:
Next iRow
Application.CutCopyMode = False
Application.Calculation = xlCalculationAutomatic
'Application.ScreenUpdating = True
End Sub

A formula solution is close to your requirement.
Cell G1 is the delimiter. In this case a comma.
Helper E1:=SUM(E1,LEN(B1)-LEN(SUBSTITUTE(B1,$H$1,"")))+1
You must fill the above formula one row more.
A8:=a1
Fill this formula to the right.
A9:=LOOKUP(ROW(1:1),$E:$E,A:A)&""
Fill this formula to the right and then down.
B9:=MID($H$1&LOOKUP(ROW(A1),E:E,B:B)&$H$1,FIND("艹",SUBSTITUTE($H$1&LOOKUP(ROW(A1),E:E,B:B)&$H$1,$H$1,"艹",ROW(A2)-LOOKUP(ROW(A1),E:E)))+1,FIND("艹",SUBSTITUTE($H$1&LOOKUP(ROW(A1),E:E,B:B)&$H$1,$H$1,"艹",ROW(A2)-LOOKUP(ROW(A1),E:E)+1))-FIND("艹",SUBSTITUTE($H$1&LOOKUP(ROW(A1),E:E,B:B)&$H$1,$H$1,"艹",ROW(A2)-LOOKUP(ROW(A1),E:E)))-1)&""
Fill down.
Bug:
Numbers will be converted to Text. Of course you can remove the &"" at the end of the formula, but blank cells will be filled with 0.

Given #A.S.H.'s excellent and brief answer, the VBA function below might be a bit of an overkill, but it will hopefully be of some help to someone looking for a more "generic" solution. This method makes sure not to modify the cells to the left, to the right, or above the table of data, in case the table does not start in A1 or in case there is other data on the sheet besides the table. It also avoids copying and inserting entire rows, and it allows you to specify a separator other than a comma.
This function happens to have similarities to #ryguy72's procedure, but it does not rely on the clipboard.
Function SplitRows(ByRef dataRng As Range, ByVal splitCol As Long, ByVal splitSep As String, _
Optional ByVal idCol As Long = 0) As Boolean
SplitRows = True
Dim oldUpd As Variant: oldUpd = Application.ScreenUpdating
Dim oldCal As Variant: oldCal = Application.Calculation
On Error GoTo err_sub
'Modify application settings for the sake of speed
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual
'Get the current number of data rows
Dim rowCount As Long: rowCount = dataRng.Rows.Count
'If an ID column is specified, use it to determine where the table ends by finding the first row
' with no data in that column
If idCol > 0 Then
With dataRng
rowCount = .Offset(, idCol - 1).Resize(, 1).End(xlDown).Row - .Row + 1
End With
End If
Dim splitArr() As String
Dim splitLb As Long, splitUb As Long, splitI As Long
Dim editedRowRng As Range
'Loop through the data rows to split them as needed
Dim r As Long: r = 0
Do While r < rowCount
r = r + 1
'Split the string in the specified column
splitArr = Split(dataRng.Cells(r, splitCol).Value & "", splitSep)
splitLb = LBound(splitArr)
splitUb = UBound(splitArr)
'If the string was not split into more than 1 item, skip this row
If splitUb <= splitLb Then GoTo splitRows_Continue
'Replace the unsplit string with the first item from the split
Set editedRowRng = dataRng.Resize(1).Offset(r - 1)
editedRowRng.Cells(1, splitCol).Value = splitArr(splitLb)
'Create the new rows
For splitI = splitLb + 1 To splitUb
editedRowRng.Offset(1).Insert 'Add a new blank row
Set editedRowRng = editedRowRng.Offset(1) 'Move down to the next row
editedRowRng.Offset(-1).Copy Destination:=editedRowRng 'Copy the preceding row to the new row
editedRowRng.Cells(1, splitCol).Value = splitArr(splitI) 'Place the next item from the split string
'Account for the new row in the counters
r = r + 1
rowCount = rowCount + 1
Next
splitRows_Continue:
Loop
exit_sub:
On Error Resume Next
'Resize the original data range to reflect the new, full data range
If rowCount <> dataRng.Rows.Count Then Set dataRng = dataRng.Resize(rowCount)
'Restore the application settings
If Application.ScreenUpdating <> oldUpd Then Application.ScreenUpdating = oldUpd
If Application.Calculation <> oldCal Then Application.Calculation = oldCal
Exit Function
err_sub:
SplitRows = False
Resume exit_sub
End Function
Function input and output
To use the above function, you would specify
the range containing the rows of data (excluding the header)
the (relative) number of the column within the range with the string to split
the separator in the string to split
the optional (relative) number of the "ID" column within the range (if a number >=1 is provided, the first row with no data in this column will be taken as the last row of data)
The range object passed in the first argument will be modified by the function to reflect the range of all the new data rows (including all inserted rows). The function returns True if no errors were encountered, and False otherwise.
Examples
For the range illustrated in the original question, the call would look like this:
SplitRows Range("A2:C2"), 2, ","
If the same table started in F5 instead of A1, and if the data in column G (i.e. the data that would fall in column B if the table started in A1) was separated by Alt-Enters instead of commas, the call would look like this:
SplitRows Range("F6:H6"), 2, vbLf
If the table contained the row header plus 10 rows of data (instead of 1), and if it started in F5 again, the call would look like this:
SplitRows Range("F6:H15"), 2, vbLf
If there was no certainty about the number of rows, but we knew that all the valid rows are contiguous and always have a value in column H (i.e. the 3rd column in the range), the call could look something like this:
SplitRows Range("F6:H1048576"), 2, vbLf, 3
In Excel 95 or lower, you would have to change "1048576" to "16384", and in Excel 97-2003, to "65536".

Related

Making an Associative Table of Unique Identifiers

I'm trying to create an associative table on a sheet that is pulling in data from a different sheet. By associative I mean, if the data is changed in the source data sheet, it would be reflected on the new sheet. I also want to only have the new sheet's table to be contingent on having a certain unique value. In my case, I want to pull up information related to a part number. The original source data will have many rows that contain the same part number, but I only care to display one of them.
This is what I have so far:
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
IsInArray = (UBound(Filter(arr, stringToBeFound)) > -1)
End Function
Dim ref() As Variant
Dim row As Integer
row = 92
Worksheets("Part Tracking Scorecard").Activate
While Cells(row, 6).Value:
If IsInArray(Cells(row, 6).Value, ref) Then
row = row + 1
ElseIf Not IsInArray(Cells(row, 6).Value, ref) Then
ReDim Preserve ref(1 To UBound(ref) + 1) As Variant
ref(UBound(ref)) = Cells(row, 6).Value
Worksheets("Unique Parts").Activate
?????
row = row + 1
To satisfy my condition to only showcase the unique part numbers, I initialized an empty array called "ref". Then, as I iterate through the source sheet, I would check if the part number was in ref with the function "IsInArray". If it was in it, it would move onto the next row, if it wasn't add the part number into the empty array and move to the next row.
The portion with the "????" is where I'm having most of my issue trying to figure out. That part is supposed to be where I make the new table with the date from the unique part number. The very simple and tedious thing I could do is make some loop to run through the columns of the rows and put in a vlookup function. I was wondering if there may be a more robust or more elegant way in doing this.
You've had the right reflex tyring to define an array to stock your values. Here are a few tips of how I would get around to doing it (not perfect, but it should help you out):
Function IsInArray(stringToBeFound As String, arr As Variant) As Boolean
IsInArray = (UBound(Filter(arr, stringToBeFound)) > -1)
End Function
Dim Source as Worksheets
Set Source = Worksheets("Part Tracking Scoreboard")
Dim ref1(), ref2() As Variant
Dim row, index, index2 As Integer
row = 92
ref1 = Source.Range(Worksheets(Source.Cells(row,1), Source.Cells(lastrow, last column))
'Start by placing your ENTIRE source sheet in ref1, if your source sheet is big, this will help you win A LOT of time during the looping phase. Notice how I start from row 92 seeing as this is where you started your loop
'lastrow and lastcolumn represent the position of the last cell in your source file
For index = row to lastrow
If Not IsInArray(ref1(row, 6).Value, ref2) Then
ref2(index) = ref1(index) 'copy the entire row from source to ref2
Next index
Dim NewFile as Worksheet
Set Newfile = Sheets("NewSheetName")
Dim ref2dimension_x, ref2dimension_y as Integer 'find dimensions of ref2 array
ref2dimension_x= UBound(ref2, 1) - LBound(ref2, 1) + 1
ref2dimension_y = UBound(ref2, 2) - LBound(ref2, 2) + 1
For index = 2 to ref2dimension_x 'go through entire new sheet and set values
For index2 = 1 to ref2dimension_y
NewFile.Cells(index, index2).Value = ref2(index - 1, index2)
Next index2
Next index
ref1() = nothing
ref2() = nothing 'free up the space occupied by these arrays
I was not sure about what you were trying to do exactly during the else loop. If you intention is to copy the entire row, this should work. If you want to copy only specific data from the source sheet, you will need to find the indexes of the corresponding columns (hardcode them if they are not going to budge, or use a loop to find them through string comparison otherwise).
This solution combines some macros that I use frequently (so even if you don't use them now, they might be helpful in the future). It won't work if the data in the unique table needs to be "live", but if it'd be sufficient for it to be updated whenever the workbook is opened/closed (or on demand), this is a lot less complicated than the array version.
Basically you just:
Copy the main/unduplicated table to a new sheet
Remove duplicates by part number
Remove unnecessary columns from unduplicated table (if applicable)
I'm assuming that your source data is in a formal Excel Table (ListObject). Just swap out "PartTable" for whatever your actual table is called.
Sub makeUniqueTable()
Application.ScreenUpdating = False
Dim MainWS As Worksheet
Set MainWS = ThisWorkbook.Sheets("Part Tracking Scorecard")
Dim UniqueWS As Worksheet
Set UniqueWS = ThisWorkbook.Sheets("Unique Parts")
UniqueWS.Cells.Clear
Call cloneTable(MainWS.ListObjects("PartTable"), "UniquePartTable", UniqueWS)
Dim UniquePartTable As ListObject
Set UniquePartTable = UniqueWS.ListObjects("UniquePartTable")
Call removeDuplicates(UniquePartTable, "Part Number")
'Optional: remove unnecessary columns by listing columns to be deleted...
'Call deleteColumns(UniquePartTable, Array("Unnecessary Column 1", "Unnecessary Column 2"))
'...or kept:
'Call deleteColumns(UniquePartTable, Array("Part Number", "Manufacturer", "Product Description"), True)
Application.ScreenUpdating = True
End Sub
Sub cloneTable(tbl As ListObject, newName As String, Optional newWS As Worksheet = Nothing)
'Copies a table (tbl) to a new worksheet (newWS) and gives it a name (newName)
'If there is any data in newWS, the new table will be added to the right of the used range
'If newWS is omitted, new table will be added to same worksheet as original table
Dim ws As Worksheet
Dim lastColumn As Long
Dim newRng As Range
Dim newTbl As ListObject
If newWS Is Nothing Then
Set ws = tbl.Parent
lastColumn = ws.Cells(1, ws.Columns.Count).End(xlToLeft).Column
Set newRng = ws.Range(ws.Cells(1, lastColumn + 2), ws.Cells(1 + tbl.ListRows.Count, lastColumn + tbl.ListColumns.Count + 1))
Else
Set ws = newWS
If ws.ListObjects.Count > 0 Then
lastColumn = ws.Cells(1, ws.Columns.Count).End(xlToLeft).Column
Set newRng = ws.Range(ws.Cells(1, lastColumn + 2), ws.Cells(1 + tbl.ListRows.Count, lastColumn + tbl.ListColumns.Count + 1))
Else
Set newRng = ws.Range(ws.Cells(1, 1), ws.Cells(1 + tbl.ListRows.Count, tbl.ListColumns.Count))
End If
End If
tbl.Range.Copy
newRng.PasteSpecial Paste:=xlPasteValuesAndNumberFormats
Application.CutCopyMode = False
Set newTbl = ws.ListObjects.Add(xlSrcRange, newRng, , xlYes)
newTbl.Name = newName
End Sub
Sub removeDuplicates(tbl As ListObject, Optional colName As Variant = "")
'Removes duplicates from a table (tbl) based on column header names (colName()) provided by user
'If no column names are provided, duplicates will be removed based on all columns in table
Dim i As Long
Dim j As Long
If Not IsArray(colName) Then
If colName = "" Then
ReDim colNumArr(0 To tbl.ListColumns.Count - 1) As Variant
For i = 0 To tbl.ListColumns.Count - 1
colNumArr(i) = tbl.ListColumns(i + 1).Range.Column
Next
Else
ReDim colNumArr(0 To 0) As Variant
colNumArr(0) = tbl.ListColumns(colName).Range.Column
End If
Else
ReDim colNumArr(0 To UBound(colName) - LBound(colName)) As Variant
j = 0
For i = LBound(colName) To UBound(colName)
colNumArr(j) = tbl.ListColumns(colName(i)).Range.Column
j = j + 1
Next
End If
tbl.Range.removeDuplicates Columns:=(colNumArr), Header:=xlYes
End Sub
Sub deleteColumns(tbl As ListObject, ByVal colName As Variant, Optional invert As Boolean = False, Optional sheetCol As Boolean = True)
'Deletes column(s) from sheet based on header names (colName) from a table (tbl)
'Will result in error if provided column contains multiple tables
'colName can be a String or an array of Strings
'Inverted mode deletes all columns *except* those in colName
Dim i As Long
Dim j As Long
Dim x As Boolean
If Not IsArray(colName) Then
tempStr = colName
ReDim colName(1 To 1) As String
colName(1) = tempStr
End If
If invert = False Then
For i = LBound(colName) To UBound(colName)
If sheetCol = True Then
tbl.Parent.Columns(tbl.ListColumns(colName(i)).Range.Column).Delete
Else
tbl.ListColumns(colName(i)).Delete
End If
Next
Else
For i = tbl.ListColumns.Count To 1 Step -1
x = False
For j = LBound(colName) To UBound(colName)
If tbl.HeaderRowRange(i).Value = colName(j) Then
x = True
Exit For
End If
Next
If x = False Then
If sheetCol = True Then
tbl.Parent.Columns(tbl.ListColumns(i).Range.Column).Delete
Else
tbl.ListColumns(i).Delete
End If
End If
Next
End If
End Sub

How to concatenate adjacent cells in Excel VBA

I have data in a single column.
Some of the cells starts with "index" like: (1), (2), etc.
I want to concatenate such cells in order, and place the result to the next column, and clear the original cells.
Could you please tell me how to do this in VBA? Thank you!
Please see the picture: col_A has the data, col_C and col_D are the desired result
You could do something like this. I can't vouch for it as I've only tested in on your sample case. So it won't work for non-contiguous numbered sub-entries - nor will it work for cases where the sub-entries are not in order. Both of the later could, of course, be incorporated into a more robust version that you've have to refactor on your own. In fact the regular expression already picks up the sub-entry # if you want to implement the latter.
Sub process()
Dim maxRow As Integer: maxRow = 100
Dim items As Collection
Dim regEx As Object
Dim matches As Object
Set items = New Collection
Set re = CreateObject("vbscript.regexp")
re.Global = True
re.IgnoreCase = True
re.Pattern = "\((\d+)\).*"
Dim val As String
Dim row As Integer, rowPtr As Integer: row = 1
Dim matchTest As Boolean, preMatchTest As Boolean: preMatchTest = False
Do While row < maxRow:
val = Cells(row, "A").Value
matchTest = re.Test(val)
If Not preMatchTest And matchTest Then
rowPtr = row
Do While row < maxRow + 1:
val = Cells(row, "A").Value
matchTest = re.Test(val)
If matchTest Then
Set matches = re.Execute(val)
itemNum = matches(0).submatches(0)
items.Add val
Cells(row, "A") = ""
Else
For Each colVal In items:
Cells(rowPtr - 1, "B") = Cells(rowPtr - 1, "B") & colVal
Next
Set items = New Collection
Exit Do
End If
row = row + 1
preMatchTest = matchTest
Loop
End If
preMatchTest = False
row = row + 1
Loop
End Sub
The prematch/match if statement looks for the start of sub-entries and once found goes into the inner loop that adds them to the 'items' collection. After the last one is found the collection is concatenated and stored at the saved location ('rowPtr') of the main entry. Also note that column 'A' and the max # of rows looked at (maxRow) are hardcoded into the macro.
you could use AutoFilter() method and Areas property of Range object
Option Explicit
Sub main()
Dim area As Range
With Range("A1", Cells(Rows.Count, 1).End(xlUp)).Offset(, 2)
.Offset(, -2).Copy .Cells
.AutoFilter Field:=1, Criteria1:="(*"
If Application.WorksheetFunction.Subtotal(103, .Cells) > 1 Then
For Each area In .Resize(.Rows.Count - 1).Offset(1).SpecialCells(xlCellTypeVisible).Areas
area(1).Offset(-1, 1).Value = Join(Application.Transpose(area.Value), "")
area.ClearContents
Next
End If
.Parent.AutoFilterMode = False
End With
End Sub

Trim a cell with VBA in a loop

I'm trying to use the trim function without success. After searching for the solution on this forum and other sources online I have seen many different approaches.
Is there no simple way of trimming a cell in VBA?
What I want is something like this:
Sub trimloop()
Dim row As Integer
row = 1
Do While Cells(row, 1) <> ""
Cells(row, 2) = trim(Cells(row, 2))
row = row + 1
Loop
So that when there is a value in column A (1) the value in column B (2) should be trimmed of any extra spaces. I just cant get this to work for me.
Appreciate any help/tips!
Regards
Jim
So i made the code a bit accurate and mistakeproof and it worked.
So i can recommend you to double check, if you have correct row and column values, because you probably targeting wrong cells. (cause your code is working)
Sub trimloop()
Dim row As Integer
Dim currentSheet As Worksheet
Set currentSheet = sheets("Sheet1")
row = 2
Do While currentSheet.Cells(row, 1) <> ""
currentSheet.Cells(row, 2).Value = Trim(currentSheet.Cells(row, 2).Value)
row = row + 1
Loop
End Sub
Use Application.WorksheetFunction.Trim(string)
Sub trimloop()
Dim row As Integer
row = 1
With ThisWorkbook.ActiveSheet
Do While .Cells(row, 1) <> ""
.Cells(row, 2) = Application.WorksheetFunction.Trim(.Cells(row, 2))
row = row + 1
Loop
End With
End Sub
this is the optimized version of your code, in case of big data sheets:
Option Explicit
Sub trimloop()
Dim row As Long, max As Long
Dim Data() As Variant
With ThisWorkbook.Sheets(1)
max = .Cells(1, 1).End(xlDown).row 'this does the same as your code, on first empty cell it stops
'the following finds the last un-empty cell of column(1):
'max= .cells(.rows.count,1).end(xlup).row
'copies values from sheet to memory (is faster for working with later)
Data = .Range(.Cells(1, 1), .Cells(max, 2)).Value2
'loop :
For row = 2 To max + 1
'work with memory instead of sheet
Data(row, 2) = Trim(Data(row, 2))
'for complete delete of all spaces use : = replace( StringName," ", "")
Next row
'write back to sheet
.Range(.Cells(1, 1), .Cells(max, 2)).Value2 = Data
End With
erase Data 'free memory
End Sub
Don't know if this overly simplified... but thought I would simply throw it out there this worked for me. The only predecessor step is you assign a "named range" to your workbook/worksheet/dataset ... name a data set and then iterate over the data set with this code
Sub forEachLoop()
For Each cell In Range("yourNamedRange")
cell.Value = Trim(cell.Value)
Next cell
End Sub

Why do my VBA code sometimes work and most of the times it doesn't?

Sub UpdateCSAH()
Dim S As String
Dim R As Long
Dim RR As Long
Dim CC As Long
Dim i As Long
Dim j As Long
Dim csah() As String 'an array that stores the CSAH sites
ReDim csah(1 To 100, 1 To 7)
Dim Ran As Range
Dim Ran1 As Range
Set Ran = Worksheets("Current Sites").Range("A1").CurrentRegion 'Ran is the region that has values
RR = 1 'row number in csah
CC = 1 'column number in csah
'check each value in Ran to see if its Route section has "CSAH"
For Each cell In Ran
R = cell.row
S = CStr(Cells(R, 4).value)
If InStr(S, "CSAH") > 0 Then 'check if "CSAH" is in the Route section
If CC > 7 Then 'reset the column number and go to the next row when reach the end of the column
CC = 1
RR = RR + 1
End If
csah(RR, CC) = cell.value
CC = CC + 1
End If
Next cell
Worksheets("CSAH Sites").Select
Range("A2:G100").Select
Selection.ClearContents
'assign each array values to cells in sheet"CSAH Sites"
i = 1
j = 1
For i = 1 To UBound(csah, 1)
For j = 1 To UBound(csah, 2)
Cells(i + 1, j) = csah(i, j)
Next j
Next i
'format the CSAH Sites values
Set Ran1 = Worksheets("CSAH Sites").Range("A1").CurrentRegion
For Each cell In Ran1
If cell.row = 1 Then
With cell.Font
.Color = -11489280
End With
ElseIf cell.row Mod 2 = 0 Then
With cell.Interior
.Color = 10092441
End With
End If
Next cell
End Sub
I have an Excel worksheet named "Current Sites" that has some data. If the 4th column has the word "CSAH", I want to store the values of that row into an array and assign those values to cells in the worksheet named "CSAH Sites". My code sometimes works (the 1st time you click), and most of times it doesn't work or doesn't work properly.
Please help me out! Thanks A Bunch!!
It looks like you want to check every row of data in the "Current Sites" sheet and if column 4 includes the "CSAH" text, then write the first 7 columns of data for that entry to the "CSAH Sites" sheet and add some colour to the even-numbered rows.
To check every row of data, you can read down just one column and use either the Offset or the Cells method to see the values of neighbouring cells. In your code you were "touching" every cell and each time you were then looking at the value in column 4 and also checking to see if the code had gone past column 7. That slows things down a lot and makes the code hard to understand.
You can also assign the values from a range of cells directly to another range of cells without using variables or an array.
See if this does what you want:
Sub UpdateCSAH()
Dim currentSitesRange As Range
Dim thisSiteRange As Range
Dim outputCell As Range
Dim numRowsOfData As Long
Const NUM_COLUMNS_OF_DATA As Integer = 7
Set currentSitesRange = Worksheets("Current Sites").Range("A1")
numRowsOfData = currentSitesRange.CurrentRegion.Rows.Count
Set currentSitesRange = currentSitesRange.Resize(RowSize:=numRowsOfData) 'currentSitesRange is the region that has values
Worksheets("CSAH Sites").Range("A2:G100").ClearContents
Set outputCell = Worksheets("CSAH Sites").Range("A2")
For Each thisSiteRange In currentSitesRange.Cells
' Look for "CSAH" in the Route section (column D)
If InStr(1, thisSiteRange.Offset(ColumnOffset:=3).Value, "CSAH", vbTextCompare) > 0 Then
' Found "CSAH" so write NUM_COLUMNS_OF_DATA columns of data to CSAH Sites sheet
outputCell.Resize(ColumnSize:=NUM_COLUMNS_OF_DATA).Value = thisSiteRange.Resize(ColumnSize:=NUM_COLUMNS_OF_DATA).Value
' Format the even-numbered rows
If outputCell.Row Mod 2 = 0 Then
With outputCell.Resize(ColumnSize:=NUM_COLUMNS_OF_DATA).Interior
.Color = 10092441
End With
End If
Set outputCell = outputCell.Offset(RowOffset:=1)
End If
Next thisSiteRange
End Sub

trouble in obtaining sum of values after checking rows and columns

I have some trouble trying to get the values of the first column with EG and 0 adding up the values found at column H, EG and 0. The sum will be placed at Column O row 3.
I have tried to write out the code, to find the Name, EG and also the temp, 0. Unfortunately, the code looks for the last 0 seen at column O. If you have any idea, do share yours, will appreciate your help! THanks!
Sub Macro1()
Dim LastRow1 As Long, RowG As Range, RowCheck As Long, Rowtosave As Long, LastCol1 As Long
Dim EGCheck As Long, ColEG As Range, firstEG As Long, IGCheck As Long, ColIG As Range
Dim findtemp As Range, tempRow As Long, tempRow1 As Long, lastEG As Long
Dim totalvalue As Long, valuestoadd As Long, tempCheck As Long
Dim emptycol As Long, empty1 As Range, Colempty As Long, tempcol As Long
LastRow1 = 50
LastCol1 = 50
For RowCheck = 1 To LastRow1
With Worksheets("Sheet1").Cells(RowCheck, 1)
If .Value = "Name" Then
Rowtosave = RowCheck
For EGCheck = 1 To LastCol1
With Worksheets("Sheet1").Cells(Rowtosave, EGCheck)
If .Value = "EG" Then
firstEG = EGCheck
End If
End With
Next EGCheck
For IGCheck = 1 To LastCol1
With Worksheets("Sheet1").Cells(Rowtosave, IGCheck)
If .Value = "IG" Then
lastEG = IGCheck - 1
End If
End With
Next IGCheck
End If
End With
Next RowCheck
'Look for temp
totalvalue = 0
For RowCheck = 1 To LastRow1
With Worksheets("Sheet1").Cells(RowCheck, 1)
If .Value = "temp" Then
tempRow1 = RowCheck
For tempCheck = 1 To lastEG
With Worksheets("Sheet1").Cells(tempRow1, tempCheck)
If .Value = "0" Then
tempcol = tempCheck
valuestoadd = Worksheets("Sheet1").Cells(tempRow1, tempcol).Select
totalvalue = totalvalue + valuestoadd
End If
End With
Next tempCheck
End If
End With
Next RowCheck
'Look for empty column
emptycol = 1
Do
Set empty1 = Worksheets("Sheet1").Cells(tempRow1, emptycol)
If empty1 = "" Then
emptycol = emptycol + 1
Colempty = emptycol
empty1 = totalvalue
End If
Exit Do
emptycol = emptycol + 1
Loop
End Sub
Follow up:
What my code needs to do is as follows:
Check Row for "Name".
After finding "Name" will look for "EG" and set it to first "EG"
Then it will try to look for first "IG" so that we can have the last of the first"EG" (note: "EG" groups Column B-D, IG group Column E to G, 2nd EG group H-J and so on.
After that, look for temp.
Then find 0 or 100 or overall which will be within the column from for example "EG"'s category, "First EG" TO "Last of the first EG".
Then add 1 to Row so that we can tabulate the sum of test1.
Get the sum of 0 from "First EG(Column B)" and "Second EG(Column H)" and place it at an empty column say Column O. For this example, the resultant will be 2+ 0 = 2
I realized the confusion caused when i use lastEG. What i meant for lastEG was last of first EG
I hope this clear up the doubts.
Discussion with Tony and follow up on improving the question
There is a row with "Name" in column A. Call this the Name row. It may not necessary be on the first row.
Within the Name row, there are cells containing "EG". A few cells after a cell containg "EG" is a cell containing "IG". Call an range starting at a cell containing "EG" and continuing to (but not including) the next cell containing "IG", an EG range. In the example, the EG ranges are B:D and H:J.
There is a row with "temp" in column A. Call this the Temp row. In the example, the Temp row is immediately under the Name row but apparently this is not a requirement. The Name and Temp rows can appear anywhere within the Sheet and Name will always be above temp but that does not means they are paired together, some times we can have name, month, temp.
There is a row with "test1" in column A. Call this the Test1 row.
In the example, the Test1 Row is immediately under the Temp row. The code does not access the Test1 row. The explanation says that the row under the Temp row is the Test1 row but this is not checked.
Within each EG range within the Temp row, there is a cell containing '0', '100' and 'Overall'. The cell below the zero will be empty or contain a number. The value of those numbers for all EG ranges is to be totalled. This total is to be saved in the cell after the first empty cell below the temp row, for example, O3 - Q3 where values will be placed at.
I hope this gets better and Tony, I have used some of your points and edited it.
I have corrected the most obvious errors in your code but it makes little sense to me so I cannot tell if it does what you want.
' Option Explicit ensures that misspelt names are
' not taken as an implicit declaration
Option Explicit
Sub Macro1()
' This is legal syntax but I find it confusing. I only have two or more
' variables in a Dim if they are closely related.
Dim LastRow1 As Long, RowG As Range, RowCheck As Long, Rowtosave As Long, LastCol1 As Long
Dim EGCheck As Long, ColEG As Range, firstEG As Long, IGCheck As Long, ColIG As Range
Dim findtemp As Range, tempRow As Long, tempRow1 As Long, lastEG As Long
Dim totalvalue As Long, valuestoadd As Long, tempCheck As Long
Dim emptycol As Long, empty1 As Range, Colempty As Long, tempcol As Long
' You can nest Withs.
' You only use one worksheet so let us declare that at the top.
With Worksheets("Sheet1")
' These statements will have to be replaced when the number of rows and
' columns exceed 50.
'LastRow1 = 50
'LastCol1 = 50
LastRow1 = .Cells.SpecialCells(xlCellTypeLastCell).Row
LastCol1 = .Cells.SpecialCells(xlCellTypeLastCell).Column
Rowtosave = 0 ' Initialise in case not found
For RowCheck = 1 To LastRow1
' I use: With .Cells(R, C)
' .Value = 5
' .NumberFormat = "0.00"
' .Font.Bold = True
' End With
' because I want to refer to .Cells(R, C) in several ways.
' I do not see the benefit of:
' With .Cells(R, C)
' .Value = 5
' End With
If .Cells(RowCheck, 1).Value = "Name" Then
Rowtosave = RowCheck
firstEG = 0 ' Initialise so you can test for
lastEG = 0 ' EG and IG not being found.
For EGCheck = 1 To LastCol1
If .Cells(Rowtosave, EGCheck).Value = "EG" Then
firstEG = EGCheck
' Without the Exit For, the search will continue and
' firstEG will identify the last EG not the first.
Exit For
End If
Next EGCheck
For IGCheck = 1 To LastCol1
If .Cells(Rowtosave, IGCheck).Value = "IG" Then
' I find the name lastEG confusing
lastEG = IGCheck - 1
End If
Next IGCheck
' If you really want the last IG on the row
' better to search backwards.
For IGCheck = LastCol1 To 1 Step -1
If .Cells(Rowtosave, IGCheck).Value = "IG" Then
lastEG = IGCheck - 1
Exit For
End If
Next IGCheck
End If
' I assume there is only one row with Name in column A
' so there is no point in searching further.
Exit For
Next RowCheck
If Rowtosave = 0 Then
Call MsgBox("Name row not found", vbOKOnly)
Exit Sub
End If
If lastEG = 0 Then
Call MsgBox("EG not found on Name row", vbOKOnly)
Exit Sub
End If
'Look for temp
totalvalue = 0
tempRow1 = 0 ' Initialise in case not found
tempcol = 0
For RowCheck = 1 To LastRow1
If .Cells(RowCheck, 1).Value = "temp" Then
For tempCheck = 1 To lastEG
tempRow1 = RowCheck
' I assume .Cells(tempRow1, tempCheck).Value is numeric.
' If so "0" has to be converted to numeric for every test.
' Better: If .Cells(tempRow1, tempCheck).Value = 0 Then
' I might use "With .Cells(tempRow1, tempCheck)" because you
' test its value then use the value. However, this code
' makes no sense to me.
If .Cells(tempRow1, tempCheck).Value = "0" Then
' Why are you saving tempcol?
tempcol = tempCheck
' Why are you selecting valuestoadd?
' You have not activated the sheet. You cannot select a
' a cell except within the active worksheet.
'valuestoadd = .Cells(tempRow1, tempcol).Select
' Why are you totalling all the cells with a value of zero.
totalvalue = totalvalue + .Cells(tempRow1, tempcol).Value
End If
Next tempCheck
' I assume there is only one temp row
Exit For
End If
Next RowCheck
' This loop appears to be searching for a empty cell
' in which to place totalvalue.
'Look for empty column
emptycol = 1
Do
' Why are you selecting the cell?
Set empty1 = .Cells(tempRow1, emptycol)
If empty1 = "" Then
' Why are you place the value in the cell after the empty one?
emptycol = emptycol + 1
' How will you use Colempty?
Colempty = emptycol
' You cannot set a range to a numeric.
'empty1 = totalvalue
' "empty1.Value = totalvalue" would be OK
.Cells(tempRow1, emptycol).Value = totalvalue
' I have moved the Exit Do to inside the If.
' Before "emptycol = emptycol + 1" could not be reached
Exit Do
End If
emptycol = emptycol + 1
Loop
End With
End Sub
New section in response to user1204868's questions and new explanation
I started answering your questions but I am unable to reconcile all the information you have given. The code, the explanation and the questions are not consistent. The following is my attempt to create a consistent description.
There is a row with "Name" in column A. Call this the Name row.
Within the Name row, there are cells containing "EG". A few cells after a cell containg "EG" is a cell containing "IG". Call an range starting at a cell containing "EG" and continuing to (but not including) the next cell containing "IG", an EG range. In the example, the EG ranges are B:D and H:J.
There is a row with "temp" in column A. Call this the Temp row. In the example, the Temp row is immediately under the Name row but apparently this is not a requirement. The Name and Temp rows can appear anywhere within the Sheet and in either sequence.
There is a row with "test1" in column A. Call this the Test1 row.
In the example, the Test1 Row is immediately under the Temp row. The code does not access the Test1 row. The explanation says that the row under the Temp row is the Test1 row but this is not checked.
Within each EG range within the Temp row, there is a cell containing zero. The cell below the zero will be empty or contain a number. The value of those numbers for all EG ranges is to be totalled. This total is to be saved in the cell after the first empty cell within the Temp row overwritten any existing value. In the example, the first empty cell in the Temp row is in column N so the total will be saved in column O overwriting the existing zero.
I doubt this explanation is completely correct. You should copy this explanation to your question and correct it so that Tim or I or someone else can understand what you really want.
Your question about the Name row not being row 1 suggests that you are concerned that as you develop the worksheet, the Name row may move down. You need to decide what about the worksheet is fixed and what may change. For example, must the Test1 row be immediately under the Temp row?
What are the rows with "test2" and "test3" in column A. Are they related to the Test1 row?
Comments on code from testing1.xlsm
Issue 1
You have removed my Option Explicit so you have undeclared variables.
Add:
Dim totalvalue1 As Long
Dim totalvalue2 As Long
Issue 2
.Cells(LastRow1, 1).Select
You can only select a cell within the active worksheet. We are accessing worksheet Sheet1 using With so it does not have to be active.
There is no need to select this cell so delete this statement.
Issue 3
End If
' I assume there is only one row with Name in column A
' so there is no point in searching further.
Exit For
Next RowCheck
As explained in my comment, this block must be replaced with:
Exit For
End If
Next RowCheck
Issue 4
For IGCheck = 1 To LastCol1
If .Cells(Rowtosave, IGCheck).Value = "IG" Then
lastEG = IGCheck - 1
Exit For
ElseIf .Cells(Rowtosave, IGCheck).Value = "EG" Then
lastEG = IGCheck - 1
'Exit For
ElseIf .Cells(Rowtosave, IGCheck).Value = "CG" Then
lastEG = IGCheck - 1
'Exit For
End If
Next IGCheck
I guess from this code and you explanations that you are searching the Name row for a range of columns which start with a cell containing "EG" and end one column before:
the next column with a cell containing "CG" or
the next column with a cell containing "EG" or
the next column with a cell containing "IG"
I also guess that you have commented out the Exit For statements because this code does not work.
Your problem is For IGCheck = 1 To LastCol1. You are starting the search at column 1 so you find the first "EG" again.
Replace this block of code with:
If firstEG = 0 Then
Call MsgBox("EG not found on Name row", vbOKOnly)
Exit Sub
End If
For IGCheck = firstEG + 1 To LastCol1
If .Cells(Rowtosave, IGCheck).Value = "IG" Or _
.Cells(Rowtosave, IGCheck).Value = "EG" Or _
.Cells(Rowtosave, IGCheck).Value = "CG" Then
lastEG = IGCheck - 1
Exit For
End If
Next IGCheck
Issue 5
If lastEG = 0 Then
Call MsgBox("EG not found on Name row", vbOKOnly)
Exit Sub
End If
The code discussed under the previous issue reveals that an EG range can be ended by the next EG. This suggests that the final EG range could be ended by the end of the used range.
Replace this block of code with:
If lastEG = 0 Then
lastEG = LastCol1 - 1
End If
Issue 6
For n = 1 To LastRow1
:
Next
The whole of the second block of code is surrounded by this For Loop. With your example data, this is the equivalent of For n = 1 to 5. Within this block of code, you look for the row (RowCheck=3) with column A containing the value "temp". You then access data from row RowCheck+n. That is, you acess rows 4 to 8. Is this really what you want to do?
Issue 7
For tempCheck = 1 To lastEG
I think this should be:
For tempCheck = firstEG To lastEG
Issue 8
The second block of code is a muddle. I can see what the code does but I do not undertand why so it is difficult for me to comment. But I will try.
The loop looking for the Temp row needs to be on the outside so something like:
For RowCheck = 1 To LastRow1
If .Cells(RowCheck, 1).Value = "temp" Then
' Set up new headers on Name row
' Process all the test rows
Exit For
End If
Next
The code places an "EG" on the row above the Temp row. This assumes the row above the Temp row is the Name row. Either:
The Temp row is always going to be the row below the Name row in which case you do not need to search for the Temp row or
you should place the "EG" on row Rowtosave.
Whichever, you only need to do it once.
You should not attempt to process the rows with "testN" in column A with For n = 1 To LastRow1. Choices include:
You have made LastRow1 two less than the true last row. Remove the - 2 from the statement initialising LastRow1. Replace For n = 1 To LastRow1 with For n = 1 To LastRow1 - tempRow1
Replace For n = 1 To LastRow1 ... Next with a Do Loop that continues until column A is empty or does not contain "testN" or whatever terminates this table.