I am super new to VBA. I am working on a problem where I am looping and creating a sentence, but I am having a problem with an overflowing row. Can you explain where i went wrong?
Sub clue()
Dim name, room, weapon As String
Dim can, dag, lead, rev, rop, wre, total, least As Double
Dim row As Integer
Cells(1, 1).Activate
cam = 0
dag = 0
lead = 0
rev = 0
rop = 0
wre = 0
row = 1
Do Until IsEmpty(ActiveCell)
name = ActiveCell.Value
room = ActiveCell.Offset(1, 0).Value
weapon = ActiveCell.Offset(2, 0).Value
Cells(row, 3).Value = name & " in the " & room & " with the " & weapon & "."
If weapon = "Candlestick" Then
can = can + 1
End If
If weapon = "Dagger" Then
dag = dag + 1
End If
If weapon = "Lead Pipe" Then
lead = lead + 1
End If
If weapon = "Revolver" Then
rev = rev + 1
End If
If weapon = "Rope" Then
rop = rop + 1
End If
If weapon = "Wrench" Then
wre = wre + 1
End If
ActiveCell.End(xlDown).End(xlDown).Activate
row = row + 1
Loop
total = can + dag + lead + rev + rop + wre
Cells(2, 6) = can
Cells(3, 6) = dag
Cells(4, 6) = lead
Cells(5, 6) = rev
Cells(6, 6) = rop
Cells(7, 6) = wre
Cells(2, 7) = can / total
Cells(3, 7) = dag / total
Cells(4, 7) = lead / total
Cells(5, 7) = rev / total
Cells(6, 7) = rop / total
Cells(7, 7) = wre / total
least = 1000000000
If can < least Then least = can
If dag < can Then least = dag
If lead < dag Then least = lead
If rev < lead Then least = rev
If rop < rev Then least = rop
If wre < rop Then least = wre
Cells(10, 5) = least
End Sub
I am trying to print out a sentence on a row using certain inputs, but as the inputs change I want to print the next sentence on the next row (hence the row=row+1) but it keeps saying that there is an "overflow" problem and i need to change something but I don't know why. Does anyone know?
Thanks!
You probably have an overflow once you have read 32K rows
and it happens because you declared your row variable as integer.
Change it to long and you will be able to work on 2 billions rows
Dim row As Long
Try to remember this :
Byte between 0 and 255.
Integer between -32,768 and 32,767.
Long between – 2,147,483,648 and 2,147,483,647.
Currency between -922,337,203,685,477.5808 and 922,337,203’685,477.5807.
Single between -3.402823E38 and 3.402823E38.
Double between -1.79769313486232D308 and 1.79769313486232D308.
Row returns a Long, not an Integer
Note that
Dim name, room, weapon As String
only defines weapon as string, the rest is variant.
The correct syntax is
Dim name as string, room as string, weapon As String
I liked the "game" purposed question and, since you declare yourself "a super new to VBA", I think it can help you the following refactoring of your initial code
Option Explicit
Sub clue()
Dim can As Long, dag As Long, lead As Long, rev As Long, rop As Long, wre As Long, row As Long
Dim weapon As String
Dim roomsRng As Range, areaRng As Range, roomsReportRng As Range, finalStatsRng As Range, leastStatsRng As Range ' these are useful range variable. you'll set them and use to avoid loosing control over what you're actually handling
' here follows a section dedicated to setting relevant "ranges". this helps a lot in avoiding loosing control over what you're actually handling
With ActiveSheet 'always explicitly qualify which worksheet do you want to work with. "ActiveSheet" is the currently active one, but you may want to qualify 'Worksheets("MySheetName")'
Set roomsRng = .Range("A1:A" & .cells(.Rows.Count, 1).End(xlUp).row) 'set roomsRng range as the one collecting activesheet cells in column "A" down to the last non empty one
Set roomsRng = roomsRng.SpecialCells(xlCellTypeConstants, xlTextValues) 'select only non blank cells of "roomsRng" range (skip blanks)
Set roomsReportRng = .cells(1, 3) ' set the range you start writing rooms report from
Set finalStatsRng = .Range("F2") ' set the range you'll start writing final stats from
Set leastStatsRng = .Range("E10") ' set the range you'll write the least found weapon number in
End With
For Each areaRng In roomsRng.Areas 'loop through all "Areas" of "roomsRng" range cells: an "Area" is a group of contiguous cells
Call WriteRoomsReport(areaRng.cells, roomsReportRng, row, weapon) 'write room report
Call UpdateWeaponsStats(weapon, can, dag, lead, rev, rop, wre) ' update weapons statistics
Next areaRng
Call WriteFinalStats(can, dag, lead, rev, rop, wre, finalStatsRng, leastStatsRng) ' write final statistics
End Sub
Sub WriteRoomsReport(roomCells As Range, reportCell As Range, row As Long, weapon As String)
Dim arr As Variant 'it'll be used as an array, see below
arr = Application.Transpose(roomCells) 'initialize the Variant as an array, filling it up with "roomCells" range content
reportCell.Offset(row).Value = arr(1) & " in the " & arr(2) & " with the " & arr(3) & "." 'write the report line
weapon = arr(3) ' store the weapon value to pass back to calling sub
row = row + 1 'update the row for subsequent use
End Sub
Sub UpdateWeaponsStats(weapon As String, can As Long, dag As Long, lead As Long, rev As Long, rop As Long, wre As Long)
' use "Select Case" pattern to avoid multiple and unuesful If-then repetition
' once a "case" is hit, its correspondant statements will be processed and then control passes to the statement following the "End Select" one
Select Case weapon
Case "Candlestick"
can = can + 1
Case Is = "Dagger"
dag = dag + 1
Case "Lead Pipe"
lead = lead + 1
Case Is = "Revolver"
rev = rev + 1
Case "Rope"
rop = rop + 1
Case Is = "Wrench"
wre = wre + 1
End Select
End Sub
Sub WriteFinalStats(can As Long, dag As Long, lead As Long, rev As Long, rop As Long, wre As Long, finalStatsRng As Range, leastStatsRng As Range)
Dim total As Long, least As Long
Dim weaponArr As Variant
total = can + dag + lead + rev + rop + wre
weaponArr = Array(can, dag, lead, rev, rop, wre)
With finalStatsRng.Resize(6) ' select a range of 6 cells in one clolumn, starting from the passed "finalStatsRng" range and resizing it up to enclose the subsequent 5 cells below it
.Value = Application.Transpose(weaponArr) ' fill the selected range (using ".Value" property of the "Range" object) with the "array" technique
With .Offset(, 1) ' shift one column to the right of selected range
.FormulaR1C1 = "=RC[-1]/" & total ' write in all cells a formula that takes the value form the adjacent cell and divide it by the "total" variable value
.Value = .Value ' have formulas replaced with values. you can comment this and cells will remain with formulas (they show the resulting values, but if you select one of them you'll see a formula in the formula ribbon of Excel UI
End With
End With
leastStatsRng.Value = Application.WorksheetFunction.Min(weaponArr) 'get the minimum value of all passed values calling the "MIN" function (which belongs to "WorksheetFuncion" object of the "Application" object -Excel) over the array filled with weapon countings
End Sub
the above code pattern has the following aims:
break code into specific functions or subs
to let you both better control the flow of your code by means of the "main" sub (that should be a sequence of statements like "Call DoThis()", "Call DoThat()" , ...) and concentrate on specific subs/functions to handle specific job
thus leading to a much more easily maintainable and "debuggable" code
use only some (out of the many) relevant VBA and Excel VBA techniques, like using Range object (see Resize(), Offset(), End(), SpecialCells() methods), Arrays (via Variant type variable), WorksheetFunction object.
of course you'll need to study all of those techniques (and many others!) exploiting such resources as SO itself, MSDN site (https://msdn.microsoft.com/en-us/library/office/ee861528.aspx) and many others you easily get in the web just googling a significant issue
as a final (and sad) note, I must warn you: building a game is something that would eventually lead towards "true" OOP, like VBA is not.
should "building games" be your true aim, then you'd better switch immediately to some true OOP language, like C#, and correspondent IDE, like Visual Studio (whose Community Edition version is currently free)
Related
I'm trying to check two sets of information in two different tabs, and then get all the records into a third tab, highlighting discrepancies in the information and also records that are present in a set but not the other. As an added difficulty, the information that I need to check is not written exactly in the same way in both tabs. Eg: in one of the tabs products are called "Product 1, Product 2", etc, whereas the other uses just the numbers.
I'm pretty new to VBA, and my best idea so far is selecting a column with IDs in one of the sets and using Find to check the other set for matches. After that I'd like to use Offset on the value Find returns to check the other cells in the row.
However, I'm encountering and 'Object variable or With block variable not set' error and I don't know what I'm doing wrong.
Below is the code, I'd really appreciate any help with using Offset in this scenario (or ideas on a more efficient way to get the results).
Sub Test()
Dim Candi_ID As String
Dim Full_Name As String
Dim i_Row As Object
Dim i_Cell As Range
Dim MD_Range As Integer
Dim i_Cell As Range
Sheets("M Report").Select
MD_Range = Application.WorksheetFunction.CountA(Range("C:C")) 'column with the IDs
For R = 2 To MD_Range
Candi_ID = Sheets("M Report").Cells(R, 3)
Full_Name = Sheets("M Report").Cells(R, 1)
If Candi_ID <> "" Then
With Sheets("i Report").Range("B:B")
Set i_Cell = .Find(What:="*" & Candi_ID, LookIn:=xlValues)
If i_Cell Is Nothing Then
Sheets("Tracker").Range("A" & Last_Row + 1) = Candi_ID
Sheets("Tracker").Range("A" & Last_Row + 1).Interior.Color = RGB(255, 0, 0)
Else
Last_Row = Sheets("Tracker").Cells(.Rows.Count, "A").End(xlUp).Row
Sheets("Tracker").Range("A" & Last_Row + 1) = Candi_ID
End If
If Full_Name <> "" Then
If Full_Name = i_Cell.Offset(0, -1) Then 'full name is one cell to the left of the ID cell
Sheets("Tracker").Range("C" & Last_Row + 1) = Full_Name
Else
Sheets("Tracker").Range("C" & Last_Row + 1) = Full_Name
Sheets("Tracker").Range("C" & Last_Row + 1).Interior.Color = RGB(255, 0, 0)
End If
End If
End With
End If
Last_Row = Last_Row + 1
Next R
End Sub
You need another test in case i_Cell was not set on this line:
Set i_Cell = .Find(What:="*" & Candi_ID, LookIn:=xlValues)
Something like:
If Full_Name <> vbNullString And Not i_Cell Is Nothing Then
If it is Nothing, and you don't test for this further down, you will get the error you mention.
Also, you have a duplicate declaration, some missing declarations, and use Long rather than Integer. Put Option Explicit at the top of all your modules. Avoid .Select, which slows your code, and use With statements where possible.
I replaced the empty literal string "" with vbNullString.
I am trying to copy data from one sheet as long as the meet the twp below criteria. However, not all the data is being transferred. Any thing stand out to anyone as wrong in my code?
Private Sub FIlist()
Dim LastRow As Long, fgLastRow As Long
Dim c As Integer
LastRow = ActiveWorkbook.Sheets("DaysReport").Range("A1000000").End(xlUp).Row
LastRow = LastRow + 1
Call StartCode
With ActiveWorkbook
For c = 1 To LastRow
If .Sheets("DaysReport").Range("B1").Offset(c - 1, 0) = "ACCEPT" And .Sheets("DaysReport").Range("C1").Offset(c - 1, 0) = "ST" Then
fgLastRow = ActiveWorkbook.Sheets("FG LIST").Range("A1000000").End(xlUp).Row
fgLastRow = fgLastRow + 1
.Sheets("FG LIST").Range("A" & fgLastRow) = .Sheets("DaysReport").Range("A2").Offset(c - 1, 0)
End If
c = c + 1
Next c
End With
Call EndCode
End Sub
The first thing that jumps out is that c should be Long as well.
The use of ActiveWorkbook may be a deliberate design choice - but if it always runs from this workbook, then use ThisWorkbook. Your user could change the workbook or active window at any time, thus causing chaos and mayhem (or at least unknown or undefined results).
Don't use Call - this is now deprecated. Not a show stopper, but still a bad habit.
Watch your index offsets, they can be confusing. Instead of c-1 all the time, just set your start parameters earlier. This means that we remove a +1 in a couple of spots as well!
Now that I tidied the code up - I saw the biggie. And the cause of your problems. I have left it commented in the code below. You are in a loop, and you also increment c (c = c + 1). This means that you skip every second row. If you really want to skip every second row then use For c = 0 To LastRow Step 2 because it is clearer code and your intention is obvious.
Private Sub FIlist()
Dim LastRow As Long, fgLastRow As Long
Dim c As Integer
StartCode
With ThisWorkbook.Sheets("DaysReport")
LastRow = .Range("A1000000").End(xlUp).Row
For c = 0 To LastRow
If .Range("B1").Offset(c, 0) = "ACCEPT" And .Range("C1").Offset(c, 0) = "ST" Then
fgLastRow = ThisWorkbook.Sheets("FG LIST").Range("A1000000").End(xlUp).Row + 1
ThisWorkbook.Sheets("FG LIST").Range("A" & fgLastRow) = .Range("A2").Offset(c, 0)
End If
'c = c + 1
Next c
End With
EndCode
End Sub
You must get rid of that
c = c + 1
Which is making your loop variable update by steps of two !
Furthermore you may want to adopt the following refactoring of your code:
Private Sub FIlist()
Dim cell As Range
Dim fgSht As Worksheet
Set fgSht = ActiveWorkbook.Sheets("FG LIST")
StartCode
With ActiveWorkbook.Sheets("DaysReport")
For Each cell In .Range("A1", .Cells(.Rows.Count, 1).End(xlUp))
If cell.Offset(,1).Value = "ACCEPT" And cell.Offset(,2).Value = "ST" Then fgSht.Cells(fgSht.Rows.Count, 1).End(xlUp).Offset(1).Value = cell.Offset(1).Value
Next
End With
EndCode
End Sub
Please note that I wrote:
If cell.Offset(,1).Value = "ACCEPT" And cell.Offset(,2).Value = "ST" Then fgSht.Cells(fgSht.Rows.Count, 1).End(xlUp).Offset(1).Value = cell.Offset(1).Value
To cope with your code that copied the value in column A one row below the current loop row
Should you actually need to copy the value in column A current row, then just remove that last .Offset(1)
What the case is:
So I got a "results sample" in excel format that needs filtering and reshaping to look nice. It is a result that will be not identical all the time but it follows similar rules. I have to filter it further and make it a little more tidy. I have figured out the filtering part, but I am not sure how to sort the remaining data, in a tidy way.
What the situation is:
There are six columns involved.
Notice: Real deal is not THAT simple, but what I need can be demonstrated using such a simple example and then I can manage more complex stuff myself I suppose.
For our example we use columns from B to G
The data are set as pairs of a "title" and a value.
For instance, if you look the first example picture I provide, The first detais the pair B3 and C3.
As you can see, looking at the same picture, D3 and E3 is an empty pair.
Same goes for D4 - E4 and F4 - G4 and so on until a last one at B11 - C11.
Starting data example:
[
What I want to achieve:
I would like, using Visual Basic for Applications, to sort the data, starting from let's say for our example B3 (see second picture) and fill three SETS of two columns, (BC, DE, FG) if there are no data inside those cells.
Notice: If a cell like D3 is null then SURELY E3 will be null too so there can be just only one check. I mean we can check either value columns or title columns.
Notice2: The B,D,F or C,E,G columns DON'T have to be sorted. I just want all the not-null values of B,D,F and their respective values from C,E,G gathered together neat so printing will not need 30 pages but just a few (too many spaces between is causing it and I try to automate the cleanup)
Here's something to start with. The first double loop populates a VBA Collection with Range variables that refer to the Cells that contain the titles.
The associated values are obtained by using an offset. The middle double loop performs a bubble sort on the latter (highly inefficient - you might want to replace it with something else). The next if statement creates a 2nd sheet if it doesn't exist on which to write out the results (last loop).
Option Explicit
Sub GatherData()
Dim lastRow As Integer, lastCol As Integer
Dim r As Integer, c As Integer
Dim vals As Collection
Set vals = New Collection
With Sheets(1)
lastCol = .UsedRange.Columns(.UsedRange.Columns.Count).Column
lastRow = .UsedRange.Rows(.UsedRange.Rows.Count).row
For c = 1 To lastCol Step 2
For r = 1 To lastRow
If (Trim(Cells(r, c).Value) <> "") Then
vals.Add .Cells(r, c)
End If
Next
Next
End With
' Bubble Sort
Dim i As Integer, j As Integer
Dim vTemp As Range
For i = 1 To vals.Count - 1
For j = i + 1 To vals.Count
If vals(i).Value > vals(j).Value Then
Set vTemp = vals(j)
vals.Remove j
vals.Add vTemp, vTemp, i
End If
Next j
Next i
Dim sht2 As Worksheet
If ThisWorkbook.Worksheets.Count = 1 Then
Set sht2 = ThisWorkbook.Sheets.Add(After:=ThisWorkbook.Sheets(1))
Else
Set sht2 = Worksheets(2)
End If
With sht2
r = 3
c = 2
For i = 1 To vals.Count
.Cells(r, c).Value = vals(i).Value
.Cells(r, c + 1).Value = vals(i).Offset(, 1).Value
c = c + 2
If c = 8 Then
r = r + 1
c = 2
End If
Next
End With
End Sub
Here is a method using the Dictionary object. I use early binding which requires setting a reference to Microsoft Scripting Runtime. If you are going to be distributing this, you might want to convert this to late-binding.
We assume that your data is properly formed as you show it above. In other words, all the titles are in even numbered columns; and the results are in the adjacent cell.
We create the dictionary using the Title as the Key, and the adjacent cell value for the Dictionary item.
We collect the information
Transfer the Keys to a VBA array and sort alphabetically
create a "Results Array" and populate it in order
write the results to a worksheet.
I will leave formatting and header generation to you.
By the way, there is a constant in the code for the number of Title/Value pair columns. I have set it to 3, but you can vary that.
Enjoy
Option Explicit
Option Compare Text 'If you want the sorting to be case INsensitive
'set reference to Microsoft Scripting Runtime
Sub TidyData()
'Assume Titles are in even numbered columns
'Assume want ColPairs pairs of columns for output
'Use dictionary with Title as key, and Value as the item
Dim dctTidy As Dictionary
Dim arrKeys As Variant
Dim wsSrc As Worksheet, wsRes As Worksheet, rRes As Range
Dim vSrc As Variant, vRes As Variant
Dim LastRow As Long, LastCol As Long
Dim I As Long, J As Long, K As Long, L As Long
Dim V As Variant
'in Results
Const ColPairs As Long = 3
'Set Source and results worksheet and range
Set wsSrc = Worksheets("sheet1")
Set wsRes = Worksheets("sheet2")
Set rRes = wsRes.Cells(1, 2)
'Read source data into variant array
With wsSrc.Cells
LastRow = .Find(what:="*", after:=.Item(1, 1), _
LookIn:=xlValues, searchorder:=xlByRows, searchdirection:=xlPrevious).Row
LastCol = .Find(what:="*", after:=.Item(1, 1), _
LookIn:=xlValues, searchorder:=xlByColumns, searchdirection:=xlPrevious).Column
vSrc = .Range(.Cells(1, 1), .Cells(LastRow, LastCol))
End With
'Collect the data into a dictionary
Set dctTidy = New Dictionary
For I = 1 To UBound(vSrc, 1)
For J = 2 To UBound(vSrc, 2) Step 2
If vSrc(I, J) <> "" Then _
dctTidy.Add Key:=vSrc(I, J), Item:=vSrc(I, J + 1)
Next J
Next I
'For this purpose, we can do a simple sort on the dictionary keys,
' and then create our results array in the sorted order.
arrKeys = dctTidy.Keys
Quick_Sort arrKeys, LBound(arrKeys), UBound(arrKeys)
'Create results array
ReDim vRes(1 To WorksheetFunction.RoundUp(dctTidy.Count / ColPairs, 0), 1 To ColPairs * 2)
I = 0
J = 0
For Each V In arrKeys
K = Int(I / ColPairs) + 1
L = (J Mod ColPairs) * 2 + 1
vRes(K, L) = V
vRes(K, L + 1) = dctTidy(V)
I = I + 1
J = J + 1
Next V
'write the results
Set rRes = rRes.Resize(UBound(vRes, 1), UBound(vRes, 2))
With rRes
.Worksheet.Cells.Clear
.Value = vRes
.HorizontalAlignment = xlCenter
End With
End Sub
Sub Quick_Sort(ByRef SortArray As Variant, ByVal first As Long, ByVal last As Long)
Dim Low As Long, High As Long
Dim Temp As Variant, List_Separator As Variant
Low = first
High = last
List_Separator = SortArray((first + last) / 2)
Do
Do While (SortArray(Low) < List_Separator)
Low = Low + 1
Loop
Do While (SortArray(High) > List_Separator)
High = High - 1
Loop
If (Low <= High) Then
Temp = SortArray(Low)
SortArray(Low) = SortArray(High)
SortArray(High) = Temp
Low = Low + 1
High = High - 1
End If
Loop While (Low <= High)
If (first < High) Then Quick_Sort SortArray, first, High
If (Low < last) Then Quick_Sort SortArray, Low, last
End Sub
Assuming we got all variables set and initialized properly, in this example:
Sheets("sheetname").Select ' because stupid things can happen...
For i = 3 To 13
Let newrangeT = "B" & i '
Let newrangeV = "C" & i '
If Sheets("sheetname").Range(newrangeV) <> "" Then
values(Position) = Sheets("sheetname").Range(newrangeV)
titles(Position) = Sheets("sheetname").Range(newrangeT)
Position = Position + 1
Else
' Don't do anything if the fields are null
End If
Next i
Sheets("sheetname").Range("B1:G13").Clear
' We then get each data from the arrays with a For loop.
' We set a columnset variable to 1.
' We set a currentrow variable to 3.
' If columnset is 1 data will enter in B and C and columnset = columnset +1
' Then if columnset is 2 we set data to DE and columnset = columnset +1
' But if columnset is 2we set data to FG and columnset = 1 and currentrow = currentrow +1
' Iterating the arrays will result in a neat setting of the data, but it will add zeros for all the nulls. Thus we need an If statement that will exclude that values checking the TITLE array (that should contain a title instead). if the value is not 0 then... we run what I describe, otherwise we do nothing.
Putting the data in the array is half of the trick.
Then we clear the area.
We set two string variables to declare ranges (actually cell reference) for every cell iterated in the loop. Here I demonstrated only for column set B,C
but we have to do the same for the rest of the columns.
The If statement here checks for null. You might have different needs, so changing the if statement changes the filtering. Here I check if the cells are not null. If the cells of column C contain data, put those data in values array and the respective B data on titles array but where? Position starts as 1 and we then iterate it +1 each time it adds something.
You can set data from an array using this command:
' current_row is set to the first row of the spreadsheet we wanna fill.
Sheets("sheetname").Select ' because stupid things can happen...
newrangeV = "C" & current_row
Sheets("sheetname").Range(newrangeV) = values(j)
The rest is just putting things together.
In any case, I wanna thank both of the people involved in this question, because I might didn't got the solution, but I got an idea of how to do other stuff, like accidentally learning something new. Cheers.
I am a brand new VBA user attempting to copy and paste data based on a range of dates. In column one I have dates and in column two I have the data I would like to copy and paste. CurYear refers to the end date in the range I am looking for and StatDate refers to the beginning date in the Range I am looking for. When I run this code it crashes Excel. Please help I am very lost
Worksheets("Weekly").Select
Dim nRows As Integer
Dim CurYear As Date
Dim StartDate As Date
nRows=Range("A1").CurrentRegions.Count.Rows
CurYear=Range("I265").Value
StartDate=Range("M5").Value
Do While Cells(nRows,1)<>""
if Cells(nRows,1).Value< CurYear & Cells(nRows,1)> StartDate Then
Cells(nRows,1).Offset(0,1).Copy
Worksheets("Weekly").Range("H41").Paste
Loop
End If
Put "option explicit" at the top of your code (before the sub) and it will tell you things to fix. Doing that will fix the part of your error where your end if was outside the loop instead of inside it but it won't catch that you weren't changing your loop counter. Try this code instead. It is actually pretty much the same as what you had with a couple minor changes.
Option Explicit
Sub test()
Dim sht As Worksheet, i As Long, l As Long, j
Dim nRows As Integer
Dim CurYear As Date
Dim StartDate As Date
Set sht = Worksheets("Test1") ' set the sheet as object isntead of selecting it for faster code and avoiding other issues
nRows = Cells(sht.Rows.Count, "B").End(xlUp).Row 'Last used row in column B - current region lastrow gets twitchy in some circumstances and should be avoided unless there is a reason to use it
l = 41
CurYear = range("I265").Value
StartDate = range("M5").Value
For i = 1 To nRows
If Cells(i, 1).Value < CurYear And Cells(i, 1).Value > StartDate Then 'for If statements you use "and" not "&"
Cells(l, 15) = Cells(i, 2) 'you will want something like this line and the next if you don't want to overwrite H41 if there is more than one match
l = l + 1
End If
Next i
End Sub
Also, to help with debugging, Open your locals window (View in the VBE). Step through your code with F8, watching your variables in the locals window to ensure that they are what you expect them to be at that step in your script.
If you do this with your code, you will see that you were missing a counter change with your variable for your loop. So it was looking for nRow to eventually be "" but it stays at whatever it was set to. Infinite loop. I changed it to a for next format but 6 of 1 and half dozen of another for your code.
Welcome to VBA. Don't poke yer eye out. :-)
Instead of using copy/ paste that uses a lot of memory and makes the program run slow, you maybe want to consider the following code that serves the same purpose as your code or Rodger's yet faster than using Select and copy/ paste syntax.
Sub Test()
Dim nRows As Long, LastRow As Long 'Declare as Long instead of Integer to avoid overflow
Dim CurYear As Date, StartDate As Date
LastRow = Cells(Rows.Count, 1).End(xlUp).Row 'Count the last used row in column 1 where you put the first data (dates)
nRows = 2 'Set the starting point of row where you put the first data (dates). In this example I use 2
CurYear = Range("I265").Value
StartDate = Range("M5").Value
Do
If Cells(nRows, 1).Value < CurYear And Cells(nRows, 1) > StartDate Then 'Use And not &
Cells(nRows, 5).Value = Cells(nRows, 2).Value 'This is essentially a "copy/ paste" syntax. Change the value (5) to the column you want to paste the value in column 2
End If
nRows = nRows + 1 'Set an increment value so each looping the nRows will increase by 1
Loop Until nRows = LastRow + 1 'Added by 1 so that the data in LastRow will keep being processed
End Sub
As the title says. Is there any function or VBA code which does the same function as a countif and is a lot faster. Currently in the middle of massive countif and it is just eating up my CPU.
It is just a basic countif inside the worksheet. Not in VBA.
=countif(X:X,Y) However the lists are massive. So both lists are around 100,000~ rows
If you can do without a count of the occurances and simply wish to check if the value x exists in the column of y's, then returning a boolean TRUE or FALSE with the ISNUMBER function evaluating a MATCH function lookup will greatly speed up the process.
=ISNUMBER(MATCH(S1, Y:Y, 0))
Fill down as necessary to catch all returns. Sort and/or filter the returned values to tabulate results.
Addendum:
Apparently there is. The huge improvement in the MATCH function calculation times over the COUNTIF function made me wonder if MATCH couldn't be put into a loop, advancing the first cell in its lookup_array parameter to the previously returned row number plus one until there were no more matches. Additionally, subsequent MATCh calls to lookup the same number (increasing the count) could be made to increasingly smaller lookup_array cell ranges by resizing (shrinking) the height of the column by the returned row number as well. If the processed values and their counts were stored as keys and items in a scripting dictionary, duplicate values could be instantly resolved without processing a count.
Sub formula_countif_test()
Dim tmr As Double
appOFF
tmr = Timer
With Sheet2.Cells(1, 1).CurrentRegion
With .Offset(1, 0).Resize(.Rows.Count - 1, .Columns.Count) 'skip header
.Cells(1, 3).Resize(.Rows.Count, 1).FormulaR1C1 = _
"=countif(c1, rc2)" 'no need for calculate when blocking in formulas like this
End With
End With
Debug.Print "COUNTIF formula: " & Timer - tmr
appON
End Sub
Sub formula_match_test()
Dim rw As Long, mrw As Long, tmr As Double, vKEY As Variant
'the following requires Tools, References, Microsoft Scripting Dictionary
Dim dVALs As New Scripting.dictionary
dVALs.CompareMode = vbBinaryCompare 'vbtextcompare for non-case sensitive
appOFF
tmr = Timer
With Sheet2.Cells(1, 1).CurrentRegion
With .Offset(1, 0).Resize(.Rows.Count - 1, .Columns.Count) 'skip header
For rw = 1 To .Rows.Count
vKEY = .Cells(rw, 2).Value2
If Not dVALs.Exists(vKEY) Then
dVALs.Add Key:=vKEY, _
Item:=Abs(IsNumeric(Application.Match(vKEY, .Columns(1), 0)))
If CBool(dVALs.Item(vKEY)) Then
mrw = 0: dVALs.Item(vKEY) = 0
Do While IsNumeric(Application.Match(vKEY, .Columns(1).Offset(mrw, 0).Resize(.Rows.Count - mrw + 1, 1), 0))
mrw = mrw + Application.Match(vKEY, .Columns(1).Offset(mrw, 0).Resize(.Rows.Count - mrw + 1, 1), 0)
dVALs.Item(vKEY) = CLng(dVALs.Item(vKEY)) + 1
Loop
End If
.Cells(rw, 3) = CLng(dVALs.Item(vKEY))
Else
.Cells(rw, 3) = CLng(dVALs.Item(vKEY))
End If
Next rw
End With
End With
Debug.Print "MATCH formula: " & Timer - tmr
dVALs.RemoveAll: Set dVALs = Nothing
appON
End Sub
Sub appON(Optional ws As Worksheet)
Application.ScreenUpdating = True
Application.EnableEvents = True
Application.Calculation = xlCalculationAutomatic
End Sub
Sub appOFF(Optional ws As Worksheet)
Application.ScreenUpdating = False
Application.EnableEvents = False
Application.Calculation = xlCalculationManual
End Sub
I used 10K rows with columns A and B filled by RANDBETWEEN(1, 999) then copied and pasted as values.
Elapsed times:
Test 1¹ - 10K rows × 2 columns filled with RANDBETWEEN(1, 999)
COUNTIF formula: 15.488 seconds
MATCH formula: 1.592 seconds
Test 2² - 10K rows × 2 columns filled with RANDBETWEEN(1, 99999)
COUNTIF formula: 14.722 seconds
MATCH formula: 3.484 seconds
I also copied the values from the COUNTIF formula into another column and compared them to the ones returned by the coded MATCH function. They were identical across the 10K rows.
¹ More multiples; less zero counts
² More zero counts, less multiples
While the nature of the data clearly makes a significant difference, the coded MATCH function outperformed the native COUNTIF worksheet function every time.
Don't forget the VBE's Tools ► References ► Microsoft Scripting Dictionary.
I use the following technique in place of COUNTIF. I have 115k rows of data and the calculation step was basically instantaneous, but you spend a bit more time setting it up.
Make a copy of the data you want to count and put in column A of a new sheet.
Sort the data you want to count (such that all identical items are next to each other).
Put the following formula in column B =IF(A2=A1,B2+1,1). Populate the column with the formula then paste value.
Put a sequential number in column C (just 1,2,3,4 ... up to the number of rows you have)
Sort everything by column C descending. The result is that in column B, the biggest count comes first.
Select column A and B, then use "Remove Duplicate" function. Now you're left with one entry per distinct row of Data and the biggest count for each.
Back in your real data sheet, use =VLOOKUP(A2,Sheet2!A:B,2,false) to get the count.
If you want to make a macro out of this, simply use Record Macro while performing the above actions.
Try sumproduct(countif(x:x,y:y))
It’s slightly faster but by how much I am not sure.
Also let us know if you have found a better option out there.
There is an easy workaround for COUNTIF, after sorting the data. You may add this to your VB Script, and run. For data with around 1 lakh line items, normal COUNTIF takes almost 10-15 mins. This script will get the counts in <10 secs.
Sub alternateFunctionForCountIF()
Dim DS As Worksheet
Set DS = ThisWorkbook.ActiveSheet
Dim lcol As Integer
lcol = DS.Cells(1, Columns.Count).End(xlToLeft).Column
Dim fieldHeader As String
Dim lrow As Long, i As Long, j As Long
Dim countifCol As Integer, fieldCol As Integer
fieldHeader = InputBox("Enter the column header to apply COUNTIF")
If Len(fieldHeader) = 0 Then
MsgBox ("Invalid input. " & Chr(13) & "Please enter the column header text and try again")
Exit Sub
End If
For i = 1 To lcol
If fieldHeader = DS.Cells(1, i).Value Then
fieldCol = i
Exit For
End If
Next i
If fieldCol = 0 Then
MsgBox (fieldHeader & " could not be found among the headers. Please enter a valid column header")
Exit Sub
End If
countifCol = fieldCol + 1
lrow = DS.Cells(Rows.Count, "A").End(xlUp).Row
DS.Range(DS.Cells(1, countifCol).EntireColumn, DS.Cells(1, countifCol).EntireColumn).Insert
DS.Cells(1, countifCol) = fieldHeader & "_count"
DS.Sort.SortFields.Clear
DS.Sort.SortFields.Add Key:=Range(DS.Cells(2, fieldCol), DS.Cells(lrow, fieldCol)), SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
With DS.Sort
.SetRange Range(DS.Cells(1, 1), DS.Cells(lrow, lcol))
.header = xlYes
.MatchCase = False
.Orientation = xlTopToBottom
.SortMethod = xlPinYin
.Apply
End With
Dim startPos As Long, endPos As Long
Dim checkText As String
For i = 2 To lrow
checkText = LCase(CStr(DS.Cells(i, fieldCol).Value))
If (checkText <> LCase(CStr(DS.Cells(i - 1, fieldCol).Value))) Then
startPos = i
End If
If (checkText <> LCase(CStr(DS.Cells(i + 1, fieldCol).Value))) Then
endPos = i
For j = startPos To endPos
DS.Cells(j, countifCol) = endPos - startPos + 1
Next j
End If
Next i
MsgBox ("Done")
End Sub