I am trying to read all values in column G until it finds a blank cell. If values are "Permits Received" or "Cancelled" then I write "Ready to Build" in column H. If I encounter anything other than received or cancelled then I write "Missing Permits". So, I need to read ALL populated cells in column G and write ready... or missing... in column H. The problem with my code is 1) its probably not the best approach, and 2) it only reads the first cell in column G then writes the output.
This is for an automated workbook that works like a champ except for this loop. I have been goofing with For Next, Do While and For Each with varying success but the code below is the closest I've been.
Dim i As Integer, j As Integer, rng As Range
Set rng = Range("$G$2:$G$" & ActiveSheet.UsedRange.Rows.Count) ' Set range to all used rows in column G
For i = 2 To rng.Rows.Count
Do While Cells(i, 7).Value = ""
If Cells(i + 1, 7).Value = "Permits Received" Or Cells(i + 1, 7).Value = "Cancelled" Then
Cells(i, 8).Value = "Ready to Build"
Else: Cells(i, 8).Value = "Missing Permits"
End If
i = i + 1
If i = rng.Rows.Count Then Exit For ' Without this code it will read all rows, not just used rows
Loop
Next i
I expect the loop to read all column G values then decide if it is "Ready to Build" or "Missing Permits". The code runs to the 35766 then errs with Overflow if the Exit For is not included.
(new) I only need one output line (col H) per each line or group of lines (col G). The attached image shows how the output should look. Thank you so, so much for looking at this!!! I've been staring at it for a week!
Example of input and correct output, need code for column H
Example of output from latest solution
Solution based on the image published:
Assuming that the default value for any FIB:BUR group is "Missing Permits", unless all of its FIB:PERMITs have the values "Permits Received" or "Cancelled" then it should be marked as "Ready to Build".
This proposed solution uses AutoFilter object (Excel) combined with the Range.SpecialCells method (Excel). To create a range in which the groups are separated by Range.Areas property (Excel).
Then it uses For…Next and the WorksheetFunction.CountIf to validate the presence of "Permits Received" or "Cancelled", and Range.Offset Property (Excel) to set the resulting value for the group.
Sub Solution()
Dim rSrc As Range, rTrg As Range
Dim rArea As Range
Dim bCnt As Byte 'Change data type to long if the number of FIB:PERMITs by FIB:BUR exceeds 255
With ThisWorkbook.Worksheets("DATA") 'change as required
If Not (.AutoFilter Is Nothing) Then .Cells(1).AutoFilter
Set rSrc = .Cells(2, 7).Resize(-1 + .UsedRange.Rows.Count, 2)
End With
With rSrc
.Columns(2).ClearContents
.Offset(-1, 0).Resize(1 + .Rows.Count).AutoFilter
.AutoFilter Field:=1, Criteria1:="<>"
Set rTrg = .Columns(1).SpecialCells(xlCellTypeVisible)
.AutoFilter
End With
For Each rArea In rTrg.Areas
bCnt = 0
With WorksheetFunction
bCnt = .CountIf(rArea, "Cancelled")
bCnt = bCnt + .CountIf(rArea, "Permits Received")
rArea.Cells(1).Offset(-1, 1).Value2 = _
IIf(bCnt = rArea.Rows.Count, "Ready to Build", "Missing Permits")
End With: Next
End Sub
Answer to original question
Instead of using a Do…Loop within the For…Next, you could have used IF…ELSEIF or Select Case statement. This proposed solution uses Select Case
Sub Solution_1()
Dim rTrg As Range, lRow As Long
With ThisWorkbook.Worksheets("DATA") 'change as required
Set rTrg = .Cells(2, 7).Resize(-1 + .UsedRange.Rows.Count, 2)
End With
With rTrg
For lRow = 1 To .Rows.Count
Select Case .Cells(lRow, 1).Value2
Case vbNullString 'NO ACTION!
Case "Permits Received", "Cancelled"
.Cells(lRow, 2).Value2 = "Ready to Build"
Case Else
.Cells(lRow, 2).Value2 = "Missing Permits"
End Select: Next: End With
End Sub
However, I try to avoid For…Next whenever is possible, so this alternate solution uses
AutoFilter object (Excel) combined with the Range.SpecialCells method (Excel).
Sub Solution_2()
Dim rTrg As Range
With ThisWorkbook.Worksheets("DATA") 'change as required
If Not (.AutoFilter Is Nothing) Then .Cells(1).AutoFilter
Set rTrg = .Cells(2, 7).Resize(-1 + .UsedRange.Rows.Count, 2) ' Set range to all used rows in column G
End With
With rTrg
.Offset(-1, 0).Resize(1 + .Rows.Count).AutoFilter
.Columns(2).Value2 = "!"
.AutoFilter Field:=2, Criteria1:="!"
.AutoFilter Field:=1, Criteria1:="=Cancelled", _
Operator:=xlOr, Criteria2:="=Permits Received"
.Columns(2).SpecialCells(xlCellTypeVisible).Value2 = "Ready to Build"
.AutoFilter Field:=1, Criteria1:="<>"
.Columns(2).SpecialCells(xlCellTypeVisible).Value2 = "Missing Permits"
.AutoFilter Field:=1
.Columns(2).SpecialCells(xlCellTypeVisible).ClearContents
.Cells(1).AutoFilter
End With
End Sub
I've encountered a similar issue when using a for loop to iterate rows like that when I use an integer as the data type for my loop index variable. The Excel integer data is 2-bytes in length and has a range of -32,768 to 32,767. Perhaps your mention of "35766" was a typo for 32766 or 32767. If you change your variable 'i' from an int to a long, I would expect your issue to go away.
If this macro is something for your personal use rather than something others will be using, I've often used the following approach to iterate down one column and modify another cell that's on the same row:
Do While IsEmpty(ActiveCell.value) = False
If ActiveCell.value = "X" Then
ActiveCell.Offset(0, 1).value = "M" ' ActiveCell.Offset(row_offset, column_offset)
ElseIf ActiveCell.value = "Y" Then
ActiveCell.Offset(0, 1).value = "N"
End If
ActiveCell.Offset(1, 0).Activate ' From currently active cell, activate the next cell one row down
Loop
Before running a macro using this technique, you'd have to activate the first cell in the column being evaluated - which becomes the cell first evaluated as the ActiveCell.
Related
Need help creating a macro which removes initials (example : "T.") from column H if column J equals to "Company One".
I've tried the code below, but it has no effect. How can I go about this?
Option Explicit
Public Sub removeInitials()
With ThisWorkbook.ActiveSheet.UsedRange
If ActiveSheet.AutoFilter Is Nothing Then .AutoFilter
.AutoFilter Field:=10, Criteria1:="Company One"
.Columns(8).Offset(2).Replace What:=" *[A-Z].", Replacement:=""
.AutoFilter
End With
End Sub
If you don't have to large a database this should do the trick w/o messing with filters. Note: I changed the column for Employer for my example. I also changed the data a little to show it works thus the inclusion of the starting/ending points.
Public Sub removeInitials()
Dim wks As Worksheet
Dim lRow As Long
Set wks = ActiveSheet
lRow = 2
With wks
Do While (.Cells(lRow, 1) <> "")
If (.Cells(lRow, 3) = "Company Two" And Right(.Cells(lRow, 1), 1) = ".") Then
Debug.Print "Current Row " & lRow
.Cells(lRow, 1) = Left((.Cells(lRow, 1)), Len(.Cells(lRow, 1)) - 3)
End If
Debug.Print lRow
lRow = lRow + 1
Loop
End With
End Sub 'removeInitials
Starting Point
Ending Point
If you want to avoid VBA, you could just do a formula...put the below in cell K2 (or wherever...) and drag down.
=TRIM(IF(J2="Company One",SUBSTITUTE(H2,IFERROR(MID(H2,FIND(".",H2)-1,2),""),""),H2))
You could also have spill range as shown in column L in screen shot.
=FILTER(TRIM(IF(J2:J9999="Company One",SUBSTITUTE(H2:H9999,IFERROR(MID(H2:H9999,FIND(".",H2:H9999)-1,2),""),""),H2:H9999)),H2:H9999<>"")
If you really want a macro, here's a dynamic one that is pretty straight forward. A perfect answer would do an array, but looping through helps see what's happening.
Sub doReplace()
Dim changeRange As Range, aCell As Range, aPosition As Long
Const companySkip = "Company One"
Set changeRange = Intersect(Range("H:H"), ActiveSheet.UsedRange).Offset(1, 0)
For Each aCell In changeRange.Cells
If aCell.Offset(0, 2).Value = companySkip Then
aPosition = InStr(1, aCell.Value, ".", vbBinaryCompare)
If aPosition > 0 Then
aCell.Value = Trim(Replace(aCell.Value, Mid(aCell.Value, aPosition - 1, 2), ""))
End If
End If
Next aCell
End Sub
I made a few changes, and this worked:
Public Sub removeInitials()
Dim rng As Range
Set rng = ActiveSheet.UsedRange
With rng
.AutoFilter Field:=3, Criteria1:="Company One"
.Columns.Item(1).Replace What:=" ?.", Replacement:=""
.AutoFilter
End With
End Sub
And here is a before execution / after execution snapshot for a comparable example:
What I had to change to get it working:
The first parameter you are passing in the replace method wasn't working for me: " *[A-Z]."
Here is a very useful thread regarding pattern syntax, specifically for the replace method if you're interested: How to use Regular Expressions (Regex) in Microsoft Excel both in-cell and loops
I wasn't sure why you used ".Columns(8).Offset(2)" (granted, I can't see the context of your Excel worksheet - but it seemed like you needed to return a range object for the single column with the names only - which you could then run the replace method on. That's what I did using ".Columns.Item(1)"
I'm currently looking for a code to improve my Dashboard. Actually, I need to know how to use a loop in a column X who will affect a column Y (cell on the same line).
To give you an example:
Column A: I have all Production Order (no empty cell)
Column B: Cost of goods Sold (Sometimes blank but doesn't matter)
I actually pull information from SAP so my Column B is not in "Currency".
The action should be:
If A+i is not empty, then value of B+i becomes "Currency".
It's also for me to get a "generic" code that I could use with other things.
This is my current code...
Sub LoopTest()
' Select cell A2, *first line of data*.
Range("A2").Select
' Set Do loop to stop when an empty cell is reached.
Do Until IsEmpty(ActiveCell)
ActiveCell.Offset(0, 1).Style = "Currency"
ActiveCell.Offset(1, 0).Select
Loop
End Sub
Another example, getting Last Row, in case your data contains any blank rows.
Sub UpdateColumns()
Dim wks As Worksheet
Dim lastRow As Long
Dim r As Long
Set wks = ActiveSheet
lastRow = ActiveSheet.Cells.SpecialCells(xlLastCell).Row
For r = 2 To lastRow
If wks.Cells(r, 1) <> "" Then
wks.Cells(r, 2).NumberFormat = "$#,##0.00"
End If
Next r
End Sub
I can see I was a little slower than the others, but if you want some more inspiration, heer is a super simple solution (as in easy to understand as well)
Sub FormatAsCurrency()
'Dim and set row counter
Dim r As Long
r = 1
'Loop all rows, until "A" is blank
Do While (Cells(r, "A").Value <> "")
'Format as currency, if not blank'
If (Cells(r, "B").Value <> "") Then
Cells(r, "B").Style = "Currency"
End If
'Increment row
r = r + 1
Loop
End Sub
Try the following:
Sub calcColumnB()
Dim strLength As Integer
Dim i As Long
For i = 1 To Rows.Count
columnAContents = Cells(i, 1).Value
strLength = Len(columnAContents)
If strLength > 0 Then
Cells(i, 2).NumberFormat = "$#,##0.00"
End If
Next i
End Sub
Explanation--
What the above code does is for each cell in Column B, so long as content in column A is not empty, it sets the format to a currency with 2 decimal places
EDIT:
Did not need to loop
Here's a really simply one, that I tried to comment - but the formatting got messed up. It simply reads column 1 (A) for content. If column 1 (A) is not empty it updates column 2 (B) as a currency. Changing active cells makes VBA more complicated than it needs to be (in my opinion)
Sub LoopTest()
Dim row As Integer
row = 1
While Not IsEmpty(Cells(row, 1))
Cells(row, 2).Style = "Currency"
row = row + 1
Wend
End Sub
I have a scenario where I have to look up two columns in worksheet1 and search for match in worksheet2, if any matching column found then replace the value.
Currently I have fixed out to find a match in one column. Here is my code
Sub FindMatch()
Dim x As String
Dim found As Boolean
' Select first line of data.
Range("A2").Select
' Set search variable value.
x = "A"
' Set Boolean variable "found" to false.
found = False
' Set Do loop to stop at empty cell.
Do Until IsEmpty(ActiveCell)
' Check active cell for search value.
If ActiveCell.value = x Then
found = True
Exit Do
End If
' Step down 1 row from present location.
ActiveCell.Offset(1, 0).Select
Loop
' Check for found.
If found = True Then
MsgBox "Value found in cell " & ActiveCell.Address
Else
MsgBox "Value not found"
End If
End Sub
I want to search column "A" and "D" in worksheet 1 against column "A" & "B" in worksheet 2
I searched in SO but didn't see any post similar to my requirement. Can anyone help me out.
Thanks !
This should do it.
Sub FindMatch()
Dim WS_one As Worksheet
Dim WS_two As Worksheet
Set WS_one = Worksheets("Sheet1")
Set WS_two = Worksheets("Sheet2")
' Set worksheets to be used.
Set lastCell = WS_one.Range("A:A").Find _
("*", after:=Cells(1, 1), SearchDirection:=xlPrevious)
'set lastCell as last cell with any value
For x = 1 To lastCell.Row
'Set For loop to run until last cell with value.
With WS_one
'with sheet one only
.Select
Col_ValueA = .Cells(x, 1).Value
Col_ValueD = .Cells(x, 4).Value
'define value to search for
End With
With WS_two
'with sheet two only
.Select
Set findvalue = .Range("A:A").Find _
(Col_ValueA, after:=Cells(1, 1))
'find 1st value equal to column A value
Do
'Do while no match found, and still values to check
If Not findvalue Is Nothing Then
'if value exist then check for match
If .Cells(findvalue.Row, 2).Value = Col_ValueD Then
'check double match
ReplaceValue = .Cells(findvalue.Row, 3).Value
Exit Do
Else
'if second value doesn't match find new Column A value
temp = findvalue.Row
'security check
Set findvalue = .Range("A:A").Find _
(Col_ValueA, after:=Cells(temp, 1))
'find new row
If temp = findvalue.Row Then
'if row doesnt change exit do
Exit Do
End If
End If
Else
'if no column A match found exit do
Exit Do
End If
Loop
End With
If Not ReplaceValue = "" Then
' replacement exists paste in column E and reset replacement value
WS_one.Cells(x, 5).Value = ReplaceValue
ReplaceValue = ""
End If
Next
WS_one.Select
End Sub
Essentially the code matches column A of sheet one with sheet two and if that value matches we check the second value. By the way, you might notice that the code I posted doesn't have select range and Active Cell anymore, try not to use those to often as the are not very secure and tend to make the code slower.
I have two spreadsheets, one contains programs and one projects. I use D loops to look at projects, within programs, within countries. I am trying to figure out if a set of cells for each project is blank. I have tried a few things. In the case below sustTrue should stay at 0 if rangeVar is blank for all the projects, but it does not. Please help!
Sub NO_Sheet()
Sheets("Program_FINAL").Select
Range("C2").Select ' C column is country
Dim IndicatorLineIterator
Do Until IsEmpty(ActiveCell) ' loop until country is blank
IndicatorLineIterator = 61
Dim PRGNum
PRGNum = ActiveCell.Offset(0, -2).Value ' Identify the program number
Sheets("Project_FINAL").Select
Range("A2").Select ' A column is the project number
Dim rangeVar, sustTrue
sustTrue = 0
Do Until IsEmpty(ActiveCell) ; loop until Project number is blank
If PRJNum = ActiveCell.Value Then
'rangeVar = ("O" & ActiveCell.Row & ":S" & ActiveCell.Row)
rangeVar = Range(ActiveCell.Offset(0, 14) & ":" & ActiveCell.Offset(0, 17))
If Not IsEmpty(rangeVar) Then
sustTrue = sustTrue + 1
MsgBox (sustTrue)
End If
End If
ActiveCell.Offset(1, 0).Select
Loop
End If
'Sheets(SheetADPName).Range("M16").Value = sustTrue
Sheets("Program_FINAL").Select
ActiveCell.Offset(1, 0).Select
Loop
End Sub
I kept working on it and the following worked for me. There is probably a more streamlined way to do this, but this works!
If Not ActiveCell.Offset(0, 14) = "" Or Not ActiveCell.Offset(0, 15) = "" Or Not ActiveCell.Offset(0, 16) = "" Or Not ActiveCell.Offset(0, 17) = "" Then
sustTrue = sustTrue + 1
MsgBox (sustTrue)
End If
Your rangeVar is not a range object.
Strongly type your variables:
Dim rangeVar as Range
Dim sustTrue as Long 'Or As Integer
Dim cl as Range
Then use the Set keyword to assign object variables and instead of concatenating, we can just list the two cells separated by a comma, and this will create the range:
Set rangeVar = Range(ActiveCell.Offset(0, 14), ActiveCell.Offset(0, 17))
Further, you can't reliably use the IsEmpty on a range array, only a single cell or other simple data type. You can get around this by iterating the range:
For each cl in rangeVar.Cells
If Not IsEmpty(cl.Value) Then
sustTrue = sustTrue + 1
End If
Next
MsgBox (sustTrue)
The IsEmpty function may not be a foolproof way to do this check. But the rest of the logic above should help. Let me know if you have trouble with the IsEmpty part.
Another alternative would be to simply subtract the number of blanks from the total number of cells in that range (again, may not be 100% reliable if the cells aren't "truly" blank...)...
sustTrue = rangeVar.Cells.Count - rangeVar.SpecialCells(xlCellTypeBlanks).Cells.Count
RangeVar is a range object. You would need to do Isempty(RangeVar.Value)
I think this has been answered already here: Using VBA to check if below cell is empty
I'm looking for a way to copy a range of cells, but to only copy the cells that contain a value.
In my excel sheet I have data running from A1-A18, B is empty and C1-C2. Now I would like to copy all the cells that contain a value.
With Range("A1")
Range(.Cells(1, 1), .End(xlDown).Cells(50, 3)).Copy
End With
This will copy everything from A1-C50, but I only want A1-A18 and C1-C2 to be copied seen as though these contain data. But it needs to be formed in a way that once I have data in B or my range extends, that these get copied too.
'So the range could be 5000 and it only selects the data with a value.
With Range("A1")
Range(.Cells(1, 1), .End(xlDown).Cells(5000, 3)).Copy
End With
Thanks!
Thanks to Jean, Current code:
Sub test()
Dim i As Integer
Sheets("Sheet1").Select
i = 1
With Range("A1")
If .Cells(1, 1).Value = "" Then
Else
Range(.Cells(1, 1), .End(xlDown)).Copy Destination:=Sheets("Sheet2").Range("A" & i)
x = x + 1
End If
End With
Sheets("Sheet1").Select
x = 1
With Range("B1")
' Column B may be empty. If so, xlDown will return cell C65536
' and whole empty column will be copied... prevent this.
If .Cells(1, 1).Value = "" Then
'Nothing in this column.
'Do nothing.
Else
Range(.Cells(1, 1), .End(xlDown)).Copy Destination:=Sheets("Sheet2").Range("B" & i)
x = x + 1
End If
End With
Sheets("Sheet1").Select
x = 1
With Range("C1")
If .Cells(1, 1).Value = "" Then
Else
Range(.Cells(1, 1), .End(xlDown)).Copy Destination:=Sheets("Sheet2").Range("C" & i)
x = x + 1
End If
End With
End Sub
A1 - A5 contains data, A6 is blanc, A7 contains data. It stops at A6 and heads over to column B, and continues in the same way.
Since your three columns have different sizes, the safest thing to do is to copy them one by one. Any shortcuts à la PasteSpecial will probably end up causing you headaches.
With Range("A1")
Range(.Cells(1, 1), .End(xlDown)).Copy myDestinationRangeA
End With
With Range("B1")
' Column B may be empty. If so, xlDown will return cell C65536
' and whole empty column will be copied... prevent this.
If .Cells(1, 1).Value = "" Then
'Nothing in this column.
'Do nothing.
Else
Range(.Cells(1, 1), .End(xlDown)).Copy myDestinationRangeB
EndIf
End With
With Range("C1")
Range(.Cells(1, 1), .End(xlDown)).Copy myDestinationRangeC
End With
Now this is ugly, and a cleaner option would be to loop through the columns, especially if you have many columns and you're pasting them to adjacent columns in the same order.
Sub CopyStuff()
Dim iCol As Long
' Loop through columns
For iCol = 1 To 3 ' or however many columns you have
With Worksheets("Sheet1").Columns(iCol)
' Check that column is not empty.
If .Cells(1, 1).Value = "" Then
'Nothing in this column.
'Do nothing.
Else
' Copy the column to the destination
Range(.Cells(1, 1), .End(xlDown)).Copy _
Destination:=Worksheets("Sheet2").Columns(iCol).Cells(1, 1)
End If
End With
Next iCol
End Sub
EDIT
So you've changed your question... Try looping through the individual cells, checking if the current cell is empty, and if not copy it. Haven't tested this, but you get the idea:
iMaxRow = 5000 ' or whatever the max is.
'Don't make too large because this will slow down your code.
' Loop through columns and rows
For iCol = 1 To 3 ' or however many columns you have
For iRow = 1 To iMaxRow
With Worksheets("Sheet1").Cells(iRow,iCol)
' Check that cell is not empty.
If .Value = "" Then
'Nothing in this cell.
'Do nothing.
Else
' Copy the cell to the destination
.Copy Destination:=Worksheets("Sheet2").cells(iRow,iCol)
End If
End With
Next iRow
Next iCol
This code will be really slow if iMaxRow is large. My hunch is that you're trying to solve a problem in a sort of inefficient way... It's a bit hard to settle on an optimal strategy when the question keeps changing.
Take a look at the paste Special function. There's a 'skip blank' property that may help you.
To improve upon Jean-Francois Corbett's answer, use .UsedRange.Rows.Count to get the last used row. This will give you a fairly accurate range and it will not stop at the first blank cell.
Here is a link to an excellent example with commented notes for beginners...
Excel macro - paste only non empty cells from one sheet to another - Stack Overflow