I am trying to get sum by month value each time if the two strings on two sheets match
Now I don't see anywhere it is going in an infinite loop but still this program is not responding after a while and I have to eventually close excel via; task manager because even Break command wasn't working.
This is a fairly simply program but I don't know how can I make it shorter than this Please advise.
Option Explicit
Sub SumByMon()
Application.ScreenUpdating = False
Dim wk As Worksheet, wt As Worksheet
Dim Astr As String, Bstr As String
Dim i, j, FinalRow, FinalRowG As Long
Dim sm As Double, Jsum As Double, Fsum As Double, Msum As Double, Asum As Double, Masum As Double, Jusum As Double, Julsum As Double, Ausum As Double, Ssum As Double, Osum As Double, Nsum As Double, Dsum As Double
Dim Dt
Dim LMon As Integer
Set wk = Sheets("BR Mailing List_12-4-15 (3)")
Set wt = Sheets("Total By Month")
FinalRowG = wk.Range("N900000").End(xlUp).Row
FinalRow = wt.Range("A900000").End(xlUp).Row
For i = 2 To FinalRow
Jsum = 0
Fsum = 0
Msum = 0
Asum = 0
Masum = 0
Jusum = 0
Julsum = 0
Ausum = 0
Ssum = 0
Osum = 0
Nsum = 0
Dsum = 0
Astr = Trim(wt.Range("A" & i))
For j = 2 To FinalRowG
Bstr = Trim(wk.Range("N" & j))
If Astr = Bstr Then
Dt = wk.Range("T" & j).Value
LMon = Month(Dt)
Select Case LMon
Case 1
sm = wk.Range("S" & j).Value
Jsum = Jsum + sm
Case 2
sm = wk.Range("S" & j).Value
Fsum = Fsum + sm
Case 3
sm = wk.Range("S" & j).Value
Msum = Msum + sm
Case 4
sm = wk.Range("S" & j).Value
Asum = Asum + sm
Case 5
sm = wk.Range("S" & j).Value
Masum = Masum + sm
Case 6
sm = wk.Range("S" & j).Value
Jusum = Jusum + sm
Case 7
sm = wk.Range("S" & j).Value
Julsum = Julsum + sm
Case 8
sm = wk.Range("S" & j).Value
Ausum = Ausum + sm
Case 9
sm = wk.Range("S" & j).Value
Ssum = Ssum + sm
Case 10
sm = wk.Range("S" & j).Value
Osum = Osum + sm
Case 11
sm = wk.Range("S" & j).Value
Nsum = Nsum + sm
Case 12
sm = wk.Range("S" & j).Value
Dsum = Dsum + sm
Case Else
Debug.Print LMon
End Select
Else: End If
Next j
wt.Range("B" & i) = Jsum
wt.Range("C" & i) = Fsum
wt.Range("D" & i) = Msum
wt.Range("E" & i) = Asum
wt.Range("F" & i) = Masum
wt.Range("G" & i) = Jusum
wt.Range("H" & i) = Julsum
wt.Range("I" & i) = Ausum
wt.Range("J" & i) = Ssum
wt.Range("K" & i) = Osum
wt.Range("L" & i) = Nsum
wt.Range("M" & i) = Dsum
Next i
wt.Select
Range("A1").Select
Application.ScreenUpdating = True
End Sub
Thanks for all your effort but even by using array method it is getting in Non-Responding state if you want to have a look at the File Here it is.
There are a number of reasons why this code could have problems:
This line could fail if it's an old or compatibility mode version of Excel : wk.Range("N900000").End(xlUp).Row.
You are writing every cell individually which is very time-consuming. If Sheet3 has a lot of rows in it then your code could appear locked because it's taking so long to write
Your declarations have ceded control of types because all the 'untyped' declarations are Variants. This makes debugging very difficult. In your comment you ask "is it necessary?". Answer: not critical, but it will increase your debugging task by an order of magnitude and the code might work in ways you don't expect. In truth, a practical answer is "yes, it's very necessary".
There are no checks of the cell values and types. If cells are empty or not dates, your code will still run, And if all your variables are Variants, your code will aggregate incorrectly when you run Month(dt).
Using the .Text property can cause problems. If for example the date column is too narrow and you have #### in the cell, then that will be the .Text value (again, out of your control if your variable is an 'undeclared' Variant. Better would be Cstr(cell.Value) or Cstr(cell.Value2).
Your code is very inefficient because it loops through the same data in Sheet1 over and over again. Far better would be to load that just once into a collection whose key is the string value that you are testing. I haven't done that in the sample below as I'm a bit short of time but you should look into doing it. If Sheet1 has a lot of rows then your code really will be slow.
The other point is that it's far quicker to write an array to the Worksheet rather than one cell at a time. In your case, the month aggregations are ideally suited to an array. So you could optimise and shorten your code by using one. The code below deals with the points above and uses an array as an example for you.
You also seem a little unclear about the Debug.Print suggestion made by Noam Hacker. It's a good suggestion so I've given you a couple of examples of it in this code:
Public Sub SumByMonWithArray()
Dim startRowA As Long, startRowB As Long
Dim finalRowA As Long, finalRowB As Long
Dim strA As String, strB As String
Dim m() As Variant
Dim dt As Variant
Dim r As Long, c As Long
Dim i As Long, j As Long
'Define the start and end rows of each sheet
startRowA = 2
startRowB = 2
finalRowA = Sheet3.Cells(Sheet3.Rows.Count, "A").End(xlUp).Row
finalRowB = Sheet1.Cells(Sheet1.Rows.Count, "N").End(xlUp).Row
'Dimension your array
r = finalRowA - startRowA + 1
If r < 1 Then Exit Sub 'exit if there's no data
ReDim m(1 To r, 1 To 12)
For i = startRowA To finalRowA
Debug.Print "In loop i=" & CStr(i) 'shows progress (delete after testing)
strA = Trim(CStr(Sheet3.Cells(i, "A").Value2))
'If test value isn't blank run the comparison
If strA <> "" Then
r = i - startRowA + 1
For j = startRowB To finalRowB
Debug.Print "In subloop i=" & CStr(i) & ", j=" & CStr(j) 'shows progress (delete after testing)
strB = Trim(CStr(Sheet1.Cells(j, "N").Value2))
'If there's a match aggregate the month array
If strB <> "" And strA = strB Then
'Populate a Variant with cell value and check it's a date
dt = Sheet1.Cells(j, "T").Value
If IsDate(dt) Then
c = Month(dt) 'Gets the column index of the array
m(r, c) = m(r, c) + CDbl(Sheet1.Cells(j, "S").Value2)
End If
End If
Next
End If
Next
'Write the aggregate array to Sheet 3
With Sheet3
.Cells(startRowA, "B").Resize(UBound(m, 1), UBound(m, 2)).Value = m
.Activate
.Range("A1").Select
End With
Application.ScreenUpdating = True
End Sub
Consider this mock-up data in Sheet1:
First add a column to the right of column T (Date of Sales?) with formula =MONTH(T2) for cell U2.
Add/Change the Monthly label to Integer (B1:M1 in sample).
Then create dynamic named ranges:
SalesItemCol with formula =OFFSET(Sheet1!$N$1,1,0,COUNTA(Sheet1!$N:$N)-1,1)
SalesQtyCol with formula =OFFSET(Sheet1!$N$1,1,5,COUNTA(Sheet1!$N:$N)-1,1)
SalesMonthCol with formula =OFFSET(Sheet1!$N$1,1,7,COUNTA(Sheet1!$N:$N)-1,1)
Finally on B2, use formula =SUMIFS(SalesQtyCol,SalesItemCol,$A2,SalesMonthCol,B$1) then auto fill the rest.
Alternatively you can create macro to do the above...
Related
I am writing a script to print in a message box, the cell value and repetitive number counts from 1-5.
Currently, I have a for loop that counts the total number of rows I have in my spreadsheet. I am unsure of how to add another for loop (nested for loop) to call the program to add 1 to 5 to the first 5 rows, and restart at 1 to 5 again at the 6th row, and so on.
For example,
If values in cells A1 to A10 are "Apple" respectively, I want to concetenate numbers from 1 to 5 such that I get the results below:
A1 = "Apple1"
A2 = "Apple2"
A3 = "Apple3"
A4 = "Apple4"
A5 = "Apple5"
A6 = "Apple1" 'it starts from 1 again
A7 = "Apple2"
and so on
Below is my sample code:
Option Explicit
Sub appendCount()
Dim q, i, rowStart, rowEnd , rowNum, LR as Long
LR = Cells(Rows.Count, 1).End(xlUp).Row
rowNum = Range("A1:A" & LR).Count
For q = 1 To rowNum Step 1
If Not IsNull(Range("A" & q)) Then
For i = 1 to 5
MsgBox Range("A" & q).Value & i
Next i
End If
Next q
End Sub
Any help would be greatly appreciated!
I believe the following will do what you expect, it will look at the values on Column A and add the count to them on Column B:
Option Explicit
Sub appendCount()
Dim LR As Long, rownumber As Long, counter As Long
Dim ws As Worksheet: Set ws = Worksheets("Sheet1")
'declare and set the worksheet you are working with, amend as required
counter = 0
LR = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
For rownumber = 1 To LR Step 1
If Not IsEmpty(ws.Range("A" & rownumber)) Then
counter = counter + 1
If counter = 6 Then counter = 1
ws.Range("B" & rownumber).Value =ws.Range("A" & rownumber).value & counter
End If
Next rownumber
End Sub
IsNull() on a cell will always return False. Replace IsNull by IsEmpty,
or use someCell <> "".
See https://stackoverflow.com/a/2009754/78522
Working with arrays will be faster. Also, mod will fail with large numbers so the below is written to handle large numbers. The point to start renumbering is also put into a constant to allow easy access for changing. Code overall is thus more flexible and resilient.
Option Explicit
Public Sub AddNumbering()
Dim arr(), i As Long, lastRow As Long, index As Long
Const RENUMBER_AT = 6
With ThisWorkbook.Worksheets("Sheet1")
lastRow = .Cells(.Rows.Count, "A").End(xlUp).Row
Select Case lastRow
Case 1
ReDim arr(1, 1): arr(1, 1) = .Range("A1").Value
Case Else
arr = .Range("A1:A" & lastRow).Value
End Select
index = 1
For i = LBound(arr, 1) To UBound(arr, 1)
If arr(i, 1) <> vbNullString Then
If i - (CLng(i / RENUMBER_AT) * RENUMBER_AT) <> 0 And i <> 1 Then
index = index + 1
Else
index = 1
End If
arr(i, 1) = arr(i, 1) & CStr(index)
End If
Next
.Range("A1").Resize(UBound(arr, 1), UBound(arr, 2)) = arr
End With
End Sub
I understand your question is values in cells A1 to A10 are "Apple" respectively, you want to content Numbers from 1 to 5, then A6 to A10 content Numbers are also from 1 to 5.
This my test code, you can try it:
Option Explicit
Sub appendCount()
Dim q, i, cou, rowStart, rowEnd, rowNum, LR As Long
LR = Cells(Rows.count, 1).End(xlUp).Row
rowNum = Range("A1:A" & LR).count
cou = 1
For q = 1 To rowNum Step 1
If Not IsNull(Range("A" & q)) Then
For i = 1 To 5
MsgBox Range("A" & q).Value & cou
cou = cou + 1
If cou = 6 Then
cou = 1
End If
Next i
End If
Next q
End Sub
Your declaration is wrong, despite what you might expect these variables are NOT declared as Long but as Variant: q, i, rowStart, rowEnd , rowNum you must include the type for each variable separately.
This code should do the trick for you:
Sub appendCount()
Dim q As Long, LR As Long, rowNum As Long
LR = Cells(Rows.Count, 1).End(xlUp).Row
rowNum = Range("A1:A" & LR).Count
For q = 1 To rowNum Step 1
If Not Len(Range("A" & q).Value) = 0 Then
If q Mod 5 = 0 Then
MsgBox Range("A" & q).Value & 5
Else
MsgBox Range("A" & q).Value & (q Mod 5)
End If
End If
Next q
End Sub
Sub appendCount()
Dim q, c, i, rowStart, rowEnd, rowNum, LR As Long
LR = Cells(Rows.Count, 1).End(xlUp).Row
rowNum = Range("A1:A" & LR).Count
c = 1
For q = 1 To rowNum Step 1
If Not IsEmpty(Range("A" & q)) Then
If (c Mod 6) <> 0 Then
Range("B" & q).Value = Range("A" & q).Value & (c Mod 6)
Else
c = c + 1
Range("B" & q).Value = Range("A" & q).Value & (c Mod 6)
End If
End If
c = c + 1
Next q
End Sub
This would do it:
Sub Loops()
Dim i As Long, iMultiples As Long, iMultiple As Long
iMultiples = WorksheetFunction.Ceiling_Math(Cells(Rows.Count, 1).End(xlUp).Row, 5, 0) ' this rounds up to the nearest 5 (giving the number of multiples
For iMultiple = 1 To iMultiples
For i = 1 To 5
If Not IsNull(Range("A" & i).Value) Then Range("A" & i).Value = "Apple" & i 'This can be tweaked as needed
Next
Next
End Sub
I am writing a VBA code on excel using loops to go through 10000+ lines.
Here is an example of the table
And here is the code I wrote :
Sub Find_Matches()
Dim wb As Workbook
Dim xrow As Long
Set wb = ActiveWorkbook
wb.Worksheets("Data").Activate
tCnt = Sheets("Data").UsedRange.Rows.Count
Dim e, f, a, j, h As Range
xrow = 2
Application.ScreenUpdating = False
Application.Calculation = xlManual
For xrow = 2 To tCnt Step 1
Set e = Range("E" & xrow)
Set f = e.Offset(0, 1)
Set a = e.Offset(0, -4)
Set j = e.Offset(0, 5)
Set h = e.Offset(0, 3)
For Each Cell In Range("E2:E" & tCnt)
If Cell.Value = e.Value Then
If Cell.Offset(0, 1).Value = f.Value Then
If Cell.Offset(0, -4).Value = a.Value Then
If Cell.Offset(0, 5).Value = j.Value Then
If Cell.Offset(0, 3).Value = h.Value Then
If (e.Offset(0, 7).Value) + (Cell.Offset(0, 7).Value) = 0 Then
Cell.EntireRow.Interior.Color = vbYellow
e.EntireRow.Interior.Color = vbYellow
End If
End If
End If
End If
End If
End If
Next
Next
End Sub
As you can imagine, this is taking a lot of time to go through 10000+ lines and I would like to find a faster solution. There must be a method I don't think to avoid the over looping
Here are the condition :
For each line, if another line anywhere in the file has the exact same
:
Buyer ID (col. E)
`# purchased (col. F)
Product ID (col.A)
Payment (col. J)
Date purchased (col. H)
Then, if the SUM of the Amount (col. L) the those two matching line is
0, then color both rows in yellow.
Note that extra columns are present and not being compared (eg- col. B) but are still important for the document and cannot be deleted to ease the process.
Running the previous code, in my example, row 2 & 5 get highlighted :
This is using nested dictionaries and arrays to check all conditions
Timer with my test data: Rows: 100,001; Dupes: 70,000 - Time: 14.217 sec
Option Explicit
Public Sub FindMatches()
Const E = 5, F = 6, A = 1, J = 10, H = 8, L = 12
Dim ur As Range, x As Variant, ub As Long, d As Object, found As Object
Set ur = ThisWorkbook.Worksheets("Data").UsedRange
x = ur
Set d = CreateObject("Scripting.Dictionary")
Set found = CreateObject("Scripting.Dictionary")
Dim r As Long, rId As String, itm As Variant, dupeRows As Object
For r = ur.Row To ur.Rows.Count
rId = x(r, E) & x(r, F) & x(r, A) & x(r, J) & x(r, H)
If Not d.Exists(rId) Then
Set dupeRows = CreateObject("Scripting.Dictionary")
dupeRows(r) = 0
Set d(rId) = dupeRows
Else
For Each itm In d(rId)
If x(r, L) + x(itm, L) = 0 Then
found(r) = 0
found(itm) = 0
End If
Next
End If
Next
Application.ScreenUpdating = False
For Each itm In found
ur.Range("A" & itm).EntireRow.Interior.Color = vbYellow
Next
Application.ScreenUpdating = True
End Sub
Before
After
I suggest a different approach altogether: add a temporary column to your data that contains a concatenation of each cell in the row. This way, you have:
A|B|C|D|E
1|Mr. Smith|500|A|1Mr. Smith500A
Then use Excel's conditional formatting on the temporary column, highlighting duplicate values. There you have your duplicated rows. Now it's only a matter of using a filter to check which ones have amounts equal to zero.
You can use the CONCATENATE function; it requires you to specify each cell separately and you can't use a range, but in your case (comparing only some of the columns) it seems like a good fit.
Maciej's answer is easy to implement (if you can add columns to your data without interrupting anything), and I would recommend it if possible.
However, for the sake of answering your question, I will contribute a VBA solution as well. I tested it on dataset that is a bit smaller than yours, but I think it will work for you. Note that you might have to tweak it a little (which row you start on, table name, etc) to fit your workbook.
Most notably, the segment commented with "Helper column" is something you most likely will have to adjust - currently, it compares every cell between A and H for the current row, which is something you may or may not want.
I've tried to include a little commentary in the code, but it's not much. The primary change is that I'm using in-memory processing of an array rather than iterating over a worksheet range (which for larger datasets should be exponentially faster).
Option Base 1
Option Explicit
' Uses ref Microsoft Scripting Runtime
Sub Find_Matches()
Dim wb As Workbook, ws As Worksheet
Dim xrow As Long, tCnt As Long
Dim e As Range, f As Range, a As Range, j As Range, h As Range
Dim sheetArr() As Variant, arr() As Variant
Dim colorTheseYellow As New Dictionary, colorResults() As String, dictItem As Variant
Dim arrSize As Long, i As Long, k As Long
Dim c As Variant
Set wb = ThisWorkbook
Set ws = wb.Worksheets("Data")
ws.Activate
tCnt = ws.UsedRange.Rows.Count
xrow = 2
Application.ScreenUpdating = False
Application.Calculation = xlManual
' Read range into an array so we process in-memory
sheetArr = ws.Range("A2:H" & tCnt)
arrSize = UBound(sheetArr, 1)
' Build new arr with "helper column"
ReDim arr(1 To arrSize, 1 To 9)
For i = 1 To arrSize
For k = 1 To 8
arr(i, k) = sheetArr(i, k)
arr(i, 9) = CStr(arr(i, 9)) & CStr(arr(i, k)) ' "Helper column"
Next k
Next i
' Iterate over array & build collection to indicate yellow lines
For i = LBound(arr, 1) To UBound(arr, 1)
If Not colorTheseYellow.Exists(i) Then colorResults = Split(ReturnLines(arr(i, 9), arr), ";")
For Each c In colorResults
If Not colorTheseYellow.Exists(CLng(c)) Then colorTheseYellow.Add CLng(c), CLng(c)
Next c
Next i
' Enact row colors
For Each dictItem In colorTheseYellow
'Debug.Print "dict: "; dictItem
If dictItem <> 0 Then ws.ListObjects(1).ListRows(CLng(dictItem)).Range.Interior.Color = vbYellow
Next dictItem
End Sub
Function ReturnLines(ByVal s As String, ByRef arr() As Variant) As String
' Returns a "Index;Index" string indicating the index/indices where the second, third, etc. instance(s) of s was found
' Returns "0;0" if 1 or fewer matches
Dim i As Long
Dim j As Long
Dim tmp As String
ReturnLines = 0
j = 0
tmp = "0"
'Debug.Print "arg: " & s
For i = LBound(arr, 1) To UBound(arr, 1)
If arr(i, 9) = s Then
j = j + 1
'Debug.Print "arr: " & arr(i, 9)
'Debug.Print "ReturnLine: " & i
tmp = tmp & ";" & CStr(i)
End If
Next i
'If Left(tmp, 1) = ";" Then tmp = Mid(tmp, 2, Len(tmp) - 1)
'Debug.Print "tmp: " & tmp
If j >= 2 Then
ReturnLines = tmp
Else
ReturnLines = "0;0"
End If
End Function
On my simple dataset, it yields this result (marked excellently with freehand-drawn color indicators):
Thanks everybody for your answers,
Paul Bica's solution actually worked and I am using a version of this code now.
But, just to animate the debate, I think I also found another way around my first code, inspired by Maciej's idea of concatenating the cells and using CStr to compare the values and, of course Vegard's in-memory processing by using arrays instead of going through the workbook :
Sub Find_MatchesStr()
Dim AmountArr(300) As Variant
Dim rowArr(300) As Variant
Dim ws As Worksheet
Dim wb As Workbook
Set ws = ThisWorkbook.Sheets("Data")
ws.Activate
Range("A1").Select
rCnt = ws.Cells.SpecialCells(xlCellTypeLastCell).Row
For i = 2 To rCnt
If i = rCnt Then
Exit For
Else
intCnt = 0
strA = ws.Cells(i, 1).Value
strE = ws.Cells(i, 5).Value
strF = ws.Cells(i, 6).Value
strH = ws.Cells(i, 8).Value
strL = ws.Cells(i, 10).Value
For j = i To rCnt - 1
strSearchA = ws.Cells(j, 1).Value
strSearchE = ws.Cells(j, 5).Value
strSearchF = ws.Cells(j, 6).Value
strSearchH = ws.Cells(j, 8).Value
strSearchL = ws.Cells(j, 10).Value
If CStr(strE) = CStr(strSearchE) And CStr(strA) = CStr(strSearchA) And CStr(strF) = CStr(strSearchF) And CStr(strH) = CStr(strSearchH) And CStr(strL) = CStr(strSearchL) Then
AmountArr(k) = ws.Cells(j, 12).Value
rowArr(k) = j
intCnt = intCnt + 1
k = k + 1
Else
Exit For
End If
Next
strSum = 0
For s = 0 To UBound(AmountArr)
If AmountArr(s) <> "" Then
strSum = strSum + AmountArr(s)
Else
Exit For
End If
Next
strAppenRow = ""
For b = 0 To UBound(rowArr)
If rowArr(b) <> "" Then
strAppenRow = strAppenRow & "" & rowArr(b) & "," & AmountArr(b) & ","
Else
Exit For
End If
Next
If intCnt = 1 Then
Else
If strSum = 0 Then
For rn = 0 To UBound(rowArr)
If rowArr(rn) <> "" Then
Let rRange = rowArr(rn) & ":" & rowArr(rn)
Rows(rRange).Select
Selection.Interior.Color = vbYellow
Else
Exit For
End If
Next
Else
strvar = ""
strvar = Split(strAppenRow, ",")
For ik = 1 To UBound(strvar)
If strvar(ik) <> "" Then
strVal = CDbl(strvar(ik))
For ik1 = ik To UBound(strvar)
If strvar(ik1) <> "" Then
strVal1 = CDbl(strvar(ik1))
If strVal1 + strVal = 0 Then
Let sRange1 = strvar(ik - 1) & ":" & strvar(ik - 1)
Rows(sRange1).Select
Selection.Interior.Color = vbYellow
Let sRange = strvar(ik1 - 1) & ":" & strvar(ik1 - 1)
Rows(sRange).Select
Selection.Interior.Color = vbYellow
End If
Else
Exit For
End If
ik1 = ik1 + 1
Next
Else
Exit For
End If
ik = ik + 1
Next
End If
End If
i = i + (intCnt - 1)
k = 0
Erase AmountArr
Erase rowArr
End If
Next
Range("A1").Select
End Sub
I still have some mistakes (rows not higlighted when they should be), the above code is not perfect, but I thought it'd be OK to give you an idea of where I was going before Paul Bica's solution came in.
Thanks again !
If your data is only till column L, then use below code, I found it is taking less time to run....
Sub Duplicates()
Application.ScreenUpdating = False
Dim i As Long, lrow As Long
lrow = Cells(Rows.Count, 1).End(xlUp).Row
Range("O2") = "=A2&E2&F2&J2&L2"
Range("P2") = "=COUNTIF(O:O,O2)"
Range("O2:P" & lrow).FillDown
Range("O2:O" & lrow).Copy
Range("O2:O" & lrow).PasteSpecial xlPasteValues
Application.CutCopyMode = False
For i = 1 To lrow
If Cells(i, 16) = 2 Then
Cells(i, 16).EntireRow.Interior.Color = vbYellow
End If
Next
Application.ScreenUpdating = True
Range("O:P").Delete
Range("A1").Select
MsgBox "Done"
End Sub
I have an sheet "Result" and I am trying to count the number of "Green", "red" and "" values in the column "K" of my sheet. I am then printing this value In my sheet "status". in sheet status I have a table with column A as week number. So if the weeks in the column A of sheet "status" is the same as the weeknumber in sheet "result" of column O, then I start counting for the values in column K
I have the code working, But I am lost, due to somereason, the count value I receive is not the correct one. For eg "green" I have 73 rows with green in column K of result. but I could see it printed in my sheet "status" as 71.
Could anyone help to figure what is going wrong ?
Sub result()
Dim i As Integer
Dim j As Integer
Dim cnt As Integer
Dim cntu As Integer
Dim sht As Worksheet
Dim totalrows As Long
Set sht = Sheets("Status")
Sheets("Result").Select
totalrows = Range("E5").End(xlDown).Row
n = Worksheets("Result").Range("E5:E" & totalrows).Cells.SpecialCells(xlCellTypeConstants).Count
For i = 2 To WorksheetFunction.Count(sht.Columns(1))
cntT = 0
cntu = 0
cntS = 0
If sht.Range("A" & i) = Val(Format(Now, "WW")) Then Exit For
Next i
For j = 5 To WorksheetFunction.CountA(Columns(17))
If sht.Range("A" & i) = Range("Q" & j) And Range("K" & j) = "Green" Then cntT = cntT + 1
If sht.Range("A" & i) = Range("Q" & j) And Range("K" & j) = "Red" Then cntu = cntu + 1
If sht.Range("A" & i) = Range("Q" & j) And Range("F" & j) = "" Then cntS = cntS + 1
If cntT <> 0 Then sht.Range("C" & i) = cntT
If cntu <> 0 Then sht.Range("D" & i) = cntu
If cntS <> 0 Then sht.Range("B" & i) = cntS
If n <> 0 Then sht.Range("G" & i) = n
Next j
If cntR + cntu <> 0 Then
'sht.Range("D" & i) = cntR / cntu * 100
End If
End Sub
I worked my way through your code and found a irregularities in your loops. Your variables I and j seem to be counting both rows and valid rows. Therefore I renamed these variables to make clear that they are rows. Also, your code tests each row for Red, Green and "". I think it can only be one of these. Therefore, if one is a match the other two can't be. This can lead to double counting. Finally, I found that you seem to be writing the final result to the Status sheet, in the same cells, many, many times.
I'm sorry, the following code isn't tested because I have no data. But I have tried to address the above problems.
Option Explicit
Sub MyResult() ' "Result" is a word reserved for the use of VBA
Dim cntT As Integer, cntU As Integer, cntS As Integer
Dim WsStatus As Worksheet, WsResult As Worksheet
Dim TotalRows As Long
Dim Rs As Integer, Rr As Long ' RowCounters: Status & Result
Dim n As Integer
Set WsStatus = Sheets("Status")
Set WsResult = Sheets("Result")
TotalRows = Range("E5").End(xlDown).Row
n = WsResult.Range("E5:E" & TotalRows).Cells.SpecialCells(xlCellTypeConstants).Count
' Improper counting: Rs is not necessarily aligned with the row number:
' For Rs = 2 To WorksheetFunction.Count(WsStatus.Columns(1))
For Rs = 2 To TotalRows
If WsStatus.Cells(Rs, "A").Value = Val(Format(Now, "WW")) Then Exit For
' If WsStatus.Range("A" & Rs) = Val(Format(Now, "WW")) Then Exit For
Next Rs
' Improper counting: Rr is not necessarily aligned with the row number:
' For Rr = 5 To WorksheetFunction.CountA(Columns(17))
With WsStatus
For Rr = 5 To TotalRows
If (.Cells(Rs, "A").Value = .Cells(Rs, "Q").Value) Then
If (.Cells(Rs, "K").Value = "Green") Then
cntT = cntT + 1
ElseIf (.Cells(Rs, "K").Value = "Red") Then
cntU = cntU + 1
Else
If (.Cells(Rs, "A").Value = "") Then cntS = cntS + 1
End If
End If
Next Rr
End With
With WsResult.Rows(Rs)
' it would be better to write even 0 to these cells
' if you don't want to show 0, format the cell to hide zeroes
.Cells(2).Value = IIf(cntS, cntS, "") ' 2 = B
.Cells(3).Value = IIf(cntT, cntT, "") ' 3 = C
.Cells(4).Value = IIf(cntU, cntU, "") ' 4 = D
.Cells(7).Value = IIf(n, n, "") ' 7 = G
End With
' If cntR + cntU <> 0 Then ' cntR isn't defined
'WsStatus.Range("D" & Rs) = cntR / cntu * 100
End If
End Sub
I urge you to use Option Explicit at the top of your sheet and declare every variable you use.
I am working on an accounting VBA program that will post Journal entries to a Ledger, and then generate trial balances (i.e. print out the values on a new sheet following "Bal. " in the Ledger). To do this, I need a way to assign the numerical part of the balance cells to a variable or collection. Unfortunately, when I use Debug.Print I see the only value stored is 0 (I am testing just with Common Stock). My expression is: y = Application.Evaluate("=SUM(R[-" & x & "]C:R[-1]C)-SUM(R[-" & x & "]C[1]:R[-1]C[1])") where y represents the balance of Common Stock. How do I properly store the balance value in a variable?
' TODO BE ABLE TO RUN MULTIPLE TIMES
' CHECK FOR POSTED MARK & START WRITING WHEN
' r = "one of the keys", or just creates new Ledger Worksheet every time
Sub MacCompileData()
Application.ScreenUpdating = False
Dim lastRow As Long, x As Long
Dim data, Key
Dim r As Range
Dim cLedger As Collection, cList As Collection
Set cLedger = New Collection
With Worksheets("Journal")
lastRow = .Range("B" & .Rows.Count).End(xlUp).Row
For x = 2 To lastRow
Key = Trim(.Cells(x, 2))
On Error Resume Next
Set cList = cLedger(Key)
If Err.Number <> 0 Then
Set cList = New Collection
cLedger.Add cList, Key
End If
On Error GoTo 0
cLedger(Key).Add Array(.Cells(x, 1).Value, .Cells(x, 3).Value, .Cells(x, 4).Value)
Worksheets("Journal").Cells(x, 5).Value = ChrW(&H2713)
Next
End With
With Worksheets("Ledger")
Dim IsLiability As Boolean
Dim y As Integer
For Each r In .Range("A1", .Range("A" & .Rows.Count).End(xlUp))
If r <> "" Then
On Error Resume Next
Key = Trim(r.Text)
If Key = "LIABILITIES" Then
IsLiability = True
End If
data = getLedgerArray(cLedger(Key))
If Err.Number = 0 Then
Set list = cLedger(Key)
x = cLedger(Key).Count
With r.Offset(2).Resize(x, 3)
.Insert Shift:=xlDown, CopyOrigin:=r.Offset(1)
.Offset(-x).Value = data
If IsLiability Then
.Offset(0, 2).Resize(1, 1).FormulaR1C1 = "=""Bal. "" & TEXT(SUM(R[-" & x & "]C:R[-1]C)-SUM(R[-" & x & "]C[1]:R[-1]C[1]),""$#,###"")"
' LOOK HERE FOR Y
y = Application.Evaluate("=SUM(R[-" & x & "]C:R[-1]C)-SUM(R[-" & x & "]C[1]:R[-1]C[1])")
Debug.Print "Common Stock Balance Equals "; y
Else
.Offset(0, 1).Resize(1, 1).FormulaR1C1 = "=""Bal. "" & TEXT(SUM(R[-" & x & "]C:R[-1]C)-SUM(R[-" & x & "]C[1]:R[-1]C[1]),""$#,###"")"
End If
r.Offset(1).EntireRow.Delete
End With
End If
On Error GoTo 0
End If
Next
End With
Application.ScreenUpdating = True
End Sub
Function getLedgerArray(c As Collection)
Dim data
Dim x As Long
ReDim data(1 To c.Count, 1 To 3)
For x = 1 To c.Count
data(x, 1) = c(x)(0)
data(x, 2) = c(x)(1)
data(x, 3) = c(x)(2)
Next
getLedgerArray = data
End Function
Here is a solution that I was able to figure out, though I am not sure if it is the most efficient. In line before the formula is set, I set a Range named BalanceCell to the cell where the formula will be written. I then used the Mid Function to get the string number value from the cell (since the length of "Bal. " is always 5 characters) after the formula is put into BalanceCell.
If IsLiability Then
Set BalanceCell = .Offset(0, 2).Resize(1, 1)
BalanceCell.FormulaR1C1 = "=""Bal. "" & TEXT(SUM(R[-" & x & "]C:R[-1]C)-SUM(R[-" & x & "]C[1]:R[-1]C[1]),""$#,###"")"
y = Mid(BalanceCell.Value, 6, Len(BalanceCell.Value))
Debug.Print "Common Stock Balance is "; y
I'm trying to set up an Excel (2010) spreadsheet to calculate overtime for employees from a spreadsheet generated by the time clock. The report from the time clock gives total hours only. Overtime can be calculated by separating hours into regular hours and OT hours. Anything over 10 hours in a day counts as OT hours. Once you have hit 40 REGULAR hours (not including OT), all hours past that point are counted as OT. Then all OT is added up. If you never hit 40 regular hours, but still have daily OT, then daily OT is used.
I feel like this shouldn't be too terribly difficult. I've tried using some conditional formulas to calculate and break out the OT, but haven't been able to come up with anything that works in all cases and allows the process to be automated. I've included a link below to an example spreadsheet generated by the time clock. Is it possible to break out the OT the way I want without using VBA?
Example Spreadsheet
Please let me know if you need any additional information. At least some ideas of where to start would be very welcome, or if there are other posts that address similar matters I could use to get going (I haven't been able to find any that quite work in this situation). Thanks!
I needed a little brain challenge this morning so I decided to help you out. This is how I solved your problem.
Turn on developer
tab
Open the Visual Basic Editor ALT+F11 or
Insert a one standard Module
Copy and Paste the below code into that Module
Option Explicit
Sub OTHours()
Sheets(2).Activate
Range("G2:G" & Range("G" & Rows.Count).End(xlUp).Row).ClearContents
Dim c As Collection
Set c = New Collection
Dim e As Collection
Set e = New Collection
On Error GoTo RowHandler
Dim i As Long, r As Range
For i = 2 To Range("A" & Rows.Count).End(xlUp).Row
Set r = Range("C" & i)
c.Add r.Row, r.Offset(0, -2) & "£" & r
Next i
For i = 1 To c.Count
If i <> c.Count Then
Dim j As Long
j = c.Item(i)
Dim m As Merged
Set m = New Merged
m.Name = Range("A" & c.Item(i))
m.Dates = Range("C" & c.Item(i))
Do Until j = c.Item(i + 1)
m.Hours = m.Hours + Range("F" & j)
m.Row = j
j = j + 1
Loop
Else
Dim k As Long
k = c.Item(i)
Set m = New Merged
m.Name = Range("A" & c.Item(i))
m.Dates = Range("C" & c.Item(i))
Do Until IsEmpty(Range("A" & k))
m.Hours = m.Hours + Range("F" & k)
m.Row = k
k = k + 1
Loop
End If
e.Add m
Next i
For i = 1 To e.Count
'Debug.Print e.Item(i).Name, e.Item(i).Dates, e.Item(i).Hours, e.Item(i).Row
Range("G" & e.Item(i).Row) = IIf(e.Item(i).Hours - 10 > 0, e.Item(i).Hours - 10, vbNullString)
Next i
PrintOvertime e
Exit Sub
RowHandler:
Resume Next
End Sub
Private Sub PrintOvertime(e As Collection)
Application.DisplayAlerts = False
Dim ws As Worksheet
For Each ws In Sheets
If StrComp(ws.Name, "Overtime Only", vbTextCompare) = 0 Then ws.Delete
Next
Application.DisplayAlerts = True
Sheets.Add(After:=Sheets(Sheets.Count)).Name = "Overtime Only"
Set ws = Sheets("Overtime Only")
With ws
Dim i As Long
.Range("A1") = "Applicant Name"
.Range("B1") = "Date"
.Range("C1") = "OT hours"
.Range("D1") = "Week Number"
For i = 1 To e.Count
If (e.Item(i).Hours - 10 > 0) Then
.Range("A" & .Range("A" & Rows.Count).End(xlUp).Row + 1) = e.Item(i).Name
.Range("B" & .Range("B" & Rows.Count).End(xlUp).Row + 1) = e.Item(i).Dates
.Range("C" & .Range("C" & Rows.Count).End(xlUp).Row + 1) = e.Item(i).Hours - 10
End If
Next i
.Columns.AutoFit
End With
PrintWeekNum
End Sub
Private Sub PrintWeekNum()
Dim ws As Worksheet
Set ws = Sheets("Overtime Only")
With ws
Dim i As Long
For i = 2 To .Range("C" & Rows.Count).End(xlUp).Row
Dim r As String
r = .Range("B" & i).Text
.Range("D" & i) = WorksheetFunction.WeekNum(Right(r, 4) & "-" & Left(r, 2) & "-" & Right(Left(r, 5), 2))
Next i
End With
End Sub
Now insert a Class Module
Copy and Paste the below code to it
Option Explicit
Public Name As String
Public Dates As Date
Public Hours As Double
Public Row As Long
Rename your Class Module to Merged
Note: you need to turn on the Properties Window, either click View on the menu bar then select Properties Window or hit F4
Select the Class Module and rename it from Class1 to Merged
Go back to the spreadsheet view and select Time Detail Sheet
Hit ALT+F8
or
select Macros on the Developer tab and hit Run
The OVERTIME results will be filled in to your Time Details Sheet column G
Also
There will be a new sheet added named Overtime Only which will have a table of all people who did extra hours. (and only people who earned Overtime)
The results will look like
Time Detail
Overtime Only
I took the answer from #mehow and modified it a bit to take weekly overtime into account. I'm not sure if it's the cleanest or most efficient way to go about it, but it gets the job done.
I created an additional class module, "DlyHrs," which holds hrs for a single day for a single employee. Each person has a collection of these DlyHrs objects, so their total regular and OT hours for the week can be tracked.
Class Module "DlyHrs" -
Option Explicit
Public Day As Date
Public totHrs As Double
Public regHrs As Double
Public otHrs As Double
Public row As Long
I modified the Class Module "Merged" as so -
Option Explicit
Public Name As String
Public Hrs As Collection
Public regHrs As Double
Public otHrs As Double
Public totHrs As Double
So far, it seems to be working, and breaking out all daily and weekly overtime correctly. Here is the entire code for the macro -
Option Explicit
Sub OTHours()
ThisWorkbook.Sheets("Time Detail").Activate
Range("T2:T" & Range("T" & Rows.Count).End(xlUp).row).ClearContents
Range("T1") = "OT"
Dim c As Collection
Set c = New Collection
Dim e As Collection
Set e = New Collection
On Error GoTo RowHandler
Dim i As Long, r As Range
For i = 2 To Range("A" & Rows.Count).End(xlUp).row
Set r = Range("H" & i)
c.Add r.row, r.Offset(0, -7) & "£" & r
Next i
'store name of previous person to know when to add new person to collection
Dim prev As String
prev = vbNullString
For i = 1 To c.Count
Dim j As Long
j = c.Item(i)
Dim curr As String
curr = Range("A" & j)
'if not dealing with a new person, add hours to existing person
'rather than creating new person
If curr = prev Then GoTo CurrentPerson
Dim m As Merged
Set m = New Merged
m.Name = Range("A" & c.Item(i))
Set m.Hrs = New Collection
CurrentPerson:
Dim curHrs As DlyHrs
Set curHrs = New DlyHrs
curHrs.Day = Range("H" & c.Item(i))
If i <> c.Count Then
'Add up hours column
Do Until j = c.Item(i + 1)
curHrs.totHrs = curHrs.totHrs + Range("K" & j)
curHrs.row = j
j = j + 1
Loop
Else
Do Until IsEmpty(Range("A" & j))
curHrs.totHrs = curHrs.totHrs + Range("K" & j)
curHrs.row = j
j = j + 1
Loop
End If
'break out regular and OT hours and add to current person
If m.regHrs = 40 Then 'all hrs to OT
curHrs.otHrs = curHrs.totHrs
m.totHrs = m.totHrs + curHrs.totHrs
m.otHrs = m.otHrs + curHrs.totHrs
ElseIf m.regHrs + curHrs.totHrs > 40 Then 'approaching 40
curHrs.regHrs = 40 - m.regHrs
curHrs.otHrs = curHrs.totHrs - curHrs.regHrs
m.totHrs = m.totHrs + curHrs.totHrs
m.regHrs = m.regHrs + curHrs.regHrs
m.otHrs = m.otHrs + curHrs.otHrs
ElseIf curHrs.totHrs > 10 Then 'not approaching 40, but daily ot
curHrs.otHrs = curHrs.totHrs - 10
curHrs.regHrs = curHrs.totHrs - curHrs.otHrs
m.totHrs = m.totHrs + curHrs.totHrs
m.regHrs = m.regHrs + curHrs.regHrs
m.otHrs = m.otHrs + curHrs.otHrs
Else 'no daily or weekly ot
m.totHrs = m.totHrs + curHrs.totHrs
m.regHrs = m.regHrs + curHrs.totHrs
End If
If curHrs.otHrs <> 0 Then
Range("T" & curHrs.row) = curHrs.otHrs
End If
m.Hrs.Add curHrs
Dim nextPerson As String
nextPerson = Range("A" & j)
'check if next name is a new person. if so, add current person to collection
If curr <> nextPerson Then
e.Add m
End If
prev = curr
Next i
Exit Sub
RowHandler:
Resume Next
End Sub