The actual situation
I have a master workbook (Wb) with a user form - file explorer (Screenshot 1) and a Sheet2 for the results (Screenshot 2)
I have a source workbook, which has a huge amount of data(14k rows) and is quite frankly a mess.. For my calculation only 2 columns of the source workbook are relevant: column 8 (True Dates) and colum 30 (numerical values).
Important note regarding column 8 : only the rows with the first of every month is relevant for me. Meaning: 01.01., 01.02.,01.03.,01.04. etc. Every other row should be ignored.
I use the Wb to search for the source workbook(wb.Source) to apply the following calculations:
Task 1: calculating the avarage value if certain conditions are met
Task 2: counting rows if the following conditions are met:
If a row in ws.Source has date in Column 8 = "01.01" and numerical value in Column 30 is < 50 then count and enter the result in cell B8 in Sheet 2 of Wb
If a row in ws.Source has date in Column 8= "01.01" and Column 30 is > 50 and < 100 then count and enter the result in cell B9 in Sheet 2 of Wb
If a row in ws.Source has date in Column 8= "01.01" and Column 30 is > 100 then count and enter the result in cell B10 in Sheet 2 of Wb
This should be applied for every month.
The results are then added to my Wb Sheet2.
My problems/questions
1. Regarding Task 2: My CountIf function results in "0" for every month and I don't know why? See attached screenshots from my result sheet of Wb(Screenshot 2) and an example of rows of my wb.Source.(Screenshot 3)
2. Regarding Task 1: the calculation works so far, but how can I make sure that only the rows with the first of every month are considered and everything else ignored?
3. Is it possible to restrict the scope of the calculation in the sense that only the past 12 months are calculated, starting from a certain date (counting back). Meaning: To set a Date (Month/Year) in my user form and consider only the rows of the past 12 months for the calculation? If no date is set then "today" should be the starting point. I think the Const = YEAR has to be removed from the code, as there is definitely the possibility that the wb.Source contains rows from different years.
I would appreciate any help!
Private Sub CommandButton1_Click() ' select file
Dim fname As Variant
With Me
fname = Application.GetOpenFilename("Excel File(s) (*.xls*),*.xls*", , "Select FIle", , False)
If fname <> "False" Then .TextBox1.Text = fname
End With
End Sub
'calculate avarage
Private Sub CommandButton2_Click() ' update averages
Const YEAR = 2019
' open source workbook
Dim fname As String, wbSource As Workbook, wsSource As Worksheet
fname = Me.TextBox1.Text
If Len(fname) = 0 Then
MsgBox "No file selected", vbCritical, "Error"
Exit Sub
End If
Set wbSource = Workbooks.Open(fname, False, True) ' no link update, read only
Set wsSource = wbSource.Sheets("Sheet1") ' change to suit
Dim wb As Workbook, ws As Worksheet
Set wb = ThisWorkbook
Set ws = wb.Sheets("Sheet2") '
' scan down source workbook calc average
Dim iRow As Long, lastRow As Long
Dim sMth As String, iMth As Long
Dim count(12) As Long, sum(12) As Long
lastRow = wsSource.Cells(Rows.count, 1).End(xlUp).Row
For iRow = 1 To lastRow
If IsDate(wsSource.Cells(iRow, 8)) _
And IsNumeric(wsSource.Cells(iRow, 30)) Then
iMth = Month(wsSource.Cells(iRow, 8)) ' col H
sum(iMth) = sum(iMth) + wsSource.Cells(iRow, 30) ' Col AD
count(iMth) = count(iMth) + 1
End If
Next
'calculate number of rows (CountIf)
Dim x As Long
Dim m As Variant
For x = 1 To 12
m = "01." & Format(x, "00")
ws.Cells(8, 1 + x) = _
Application.WorksheetFunction.CountIfs(wsSource.Columns(8), m & "*", _
wsSource.Columns(30), "<=" & 50)
ws.Cells(9, 1 + x) = _
Application.WorksheetFunction.CountIfs(wsSource.Columns(8), m & "*", _
wsSource.Columns(30), ">" & 50, wsSource.Columns(30), "<=" & 100)
ws.Cells(10, 1 + x) = _
Application.WorksheetFunction.CountIfs(wsSource.Columns(8), m & "*", _
wsSource.Columns(30), ">" & 100)
Next x
' close source worbook no save
wbSource.Close False
' update Sheet 2 with averages
With ws.Range("A3")
For iMth = 1 To 12
.Offset(0, iMth - 1) = MonthName(iMth) & " " & YEAR
If count(iMth) > 0 Then
.Offset(1, iMth - 1) = sum(iMth) / count(iMth)
.Offset(1, iMth - 1).NumberFormat = "0.0"
End If
Next
End With
Dim msg As String
msg = iRow - 1 & " rows scanned in " & TextBox1.Text
MsgBox msg, vbInformation, "Sheet 2 updated"
End Sub
Related
I'm quite new to VBA, and I'm trying to calculate the values before/equal to the end date requested to the user, but can't seem to figure it out.
14/03/2022 (Q1)
21/03/2022 (R1)
08 (Q2)
10 (R2)
40 (Q3)
20 (R3)
my goal is to process each existing row and return the sum on that specific row in column N:N.
so if the user says end date is 21/03 the total value for 2nd row should be 18 and 3rd row 60.
The code should summarize the values corresponding to each A:A column existing cells row. I mean, for the third row, the data will be summarized from the third row bellow the one keeping the reference dates.
Here is my code so far:
Sub test()
Dim WS As Worksheet
Set WS = Worksheets("Sheet1")
Const EARLIEST = #1/1/2000#
Dim till As Date
'On Error GoTo Errorhandler
till = CDate(Application.InputBox(Prompt:="Please enter the date to split the hrs (dd/mm/yyyy):", Type:=2))
If till = 0 Then Exit Sub
If till < EARLIEST Then
MsgBox "You need to insert a valid date dd/mm/yyyy"
Exit Sub
End If
'need to do more spotchecks as check if date exists in the file
ActiveSheet.Range("N1").Select
ActiveCell.FormulaR1C1 = "Total until " & till
Dim lr, lc, lc1 As Long
Dim sum As Long
lr = Range("Q" & Rows.Count).End(xlUp).Row
lc = Cells(2, Columns.Count).End(xlToLeft).Column
lc1 = Cells(1, Columns.Count).End(xlToLeft).Column
Set dates = WS.Range("Q1" & lc1)
For i = 2 To lr
If dates.Value <= till Then
sum = sum + Range("Q2" & lc)
Range("N" & i).Value = sum
End If
Next i
Columns("N:N").Select
Selection.Style = "Comma"
Selection.NumberFormat = _
"_ * #,##0.0_)_ ;_ * (#,##0.0)_ ;_ * ""-""??_)_ ;_ #_ "
Selection.NumberFormat = "_ * #,##0_)_ ;_ * (#,##0)_ ;_ * ""-""??_)_ ;_ #_ "
Columns("N:N").EntireColumn.autofit
'Errorhandler:
'MsgBox "You need to insert a valid date dd/mm/yyyy"
'Resume
End Sub
Please, try the next version:
Sub testProcessDate_All()
Dim WS As Worksheet: Set WS = Worksheets("Sheet1")
Const EARLIEST = #1/1/2000#
Dim till As Date
till = CDate(Application.InputBox(Prompt:="Please enter the date to split the hrs (dd/mm/yyyy)" & vbCrLf & _
"or select a cell containing a date in the first row:", Type:=2))
If till = 0 Then Exit Sub
If till < EARLIEST Then
MsgBox "You need to insert a valid date dd/mm/yyyy"
Exit Sub
End If
Dim lc1 As Long: lc1 = WS.Cells(1, WS.Columns.Count).End(xlToLeft).Column
Dim LatestDate As Date
LatestDate = WS.Cells(1, lc1).Value
If till > LatestDate Then MsgBox "The chosen date is larger than last available date...": Exit Sub
'ActiveSheet.Range("N:N").ClearContents 'to be cleared for the next time
WS.Range("N1").Value = "Total until " & till
Dim lr As Long, sum As Long, i As Long, j As Long
If WS.Range("A2").Value = "" Then MsgBox "No values in A:A to run the code...": Exit Sub
lr = WS.Range("A1").End(xlDown).Row 'the last row in A:A (for a table...)
For j = 2 To lr
For i = WS.Range("Q1").Column To lc1
If CDate(WS.Cells(1, i).Value) <= till Then
sum = sum + WS.Cells(j, i).Value
Else
Exit For
End If
Next i
WS.Range("N" & j).Value = sum: sum = 0
Next j
End Sub
I need help for one part of my complete code. The code below should count the rows that meet the defined conditions for every month. The problem is that I only get "0" as result. I cant figure out why? Is it maybe because of my date format? My source wb contains dates in a format like "01.01.2019".
The code should also only count rows from the first of any month. Meaning: 01.01., 01.02.,01.03. etc.
My master workbook(wb) has an user form with file explorer function, where I can choose my source workbook(wbSource) and apply the macro on it. As seen in the complete code below.
Any help would be appreciated.
Dim x As Long
For x = 1 To 12
ws.Cells(8, 1 + x) = _
Application.WorksheetFunction.CountIfs(wsSource.Columns(8), Format(x, "00") & "." & MonthName(x), _
wsSource.Columns(30), "<=" & 50)
ws.Cells(9, 1 + x) = _
Application.WorksheetFunction.CountIfs(wsSource.Columns(8), Format(x, "00") & "." & MonthName(x), _
wsSource.Columns(30), ">" & 50, wsSource.Columns(30), "<=" & 100)
ws.Cells(10, 1 + x) = _
Application.WorksheetFunction.CountIfs(wsSource.Columns(8), Format(x, "00") & "." & MonthName(x), _
wsSource.Columns(30), ">" & 100)
Next x
Complete Code
Private Sub CommandButton2_Click() ' update averages
Const YEAR = 2019
' open source workbook
Dim fname As String, wbSource As Workbook, wsSource As Worksheet
fname = Me.TextBox1.Text
If Len(fname) = 0 Then
MsgBox "No file selected", vbCritical, "Error"
Exit Sub
End If
Set wbSource = Workbooks.Open(fname, False, True) ' no link update, read only
Set wsSource = wbSource.Sheets("Sheet1") ' change to suit
Dim wb As Workbook, ws As Worksheet
Set wb = ThisWorkbook
Set ws = wb.Sheets("Table 2") '
' scan down source workbook calc average
Dim iRow As Long, lastRow As Long
Dim sMth As String, iMth As Long
Dim count(12) As Long, sum(12) As Long
lastRow = wsSource.Cells(Rows.count, 1).End(xlUp).Row
For iRow = 1 To lastRow
If IsDate(wsSource.Cells(iRow, 8)) _
And IsNumeric(wsSource.Cells(iRow, 30)) Then
iMth = Month(wsSource.Cells(iRow, 8)) ' col H
sum(iMth) = sum(iMth) + wsSource.Cells(iRow, 30) ' Col AD
count(iMth) = count(iMth) + 1 '
End If
Next
' counting the rows
Dim x As Long
For x = 1 To 12
ws.Cells(8, 1 + x) = _
Application.WorksheetFunction.CountIfs(wsSource.Columns(8), Format(x, "00") & "." & MonthName(x), _
wsSource.Columns(30), "<=" & 50)
ws.Cells(9, 1 + x) = _
Application.WorksheetFunction.CountIfs(wsSource.Columns(8), Format(x, "00") & "." & MonthName(x), _
wsSource.Columns(30), ">" & 50, wsSource.Columns(30), "<=" & 100)
ws.Cells(10, 1 + x) = _
Application.WorksheetFunction.CountIfs(wsSource.Columns(8), Format(x, "00") & "." & MonthName(x), _
wsSource.Columns(30), ">" & 100)
Next x
' close source worbook no save
wbSource.Close False
' update Table 2 with averages
With ws.Range("A3")
For iMth = 1 To 12
.Offset(0, iMth - 1) = MonthName(iMth) & " " & YEAR
If count(iMth) > 0 Then
.Offset(1, iMth - 1) = sum(iMth) / count(iMth)
.Offset(1, iMth - 1).NumberFormat = "0.0"
End If
Next
End With
Dim msg As String
msg = iRow - 1 & " rows scanned in " & TextBox1.Text
MsgBox msg, vbInformation, "Table 2 updated"
End Sub
Result-Sheet in Wb - as you see there are only zeroes displayed in the number of rows table
Example of wb.Source - Columns H(8) and AD(30) are the only relevant one for the ounting of the rows. In the example belows the last row should not be counted as the date does not match the condition ( its not the first of the month).
Yes, the problem is from the way you handle the dates. Your data has true dates. They aren't random numbers but a count of days since Jan 1, 1900. They uniquely identify every day for the past 120 years, and the system will continue to identify every day for the next 120 years, too. I believe the code below will do, more or less, what you intended to do.
Sub WriteCountIf()
' 025
' The YearCell must contain a year number like 2019.
Const YearCell As String = "A1" 'change to suit
Dim DateRng As Range ' the range to search for dates in
Dim DaysRng As Range ' the range to search for days in
Dim StartDate As Date ' first day to include in count
Dim EndDate As Date ' last day to include in count
Dim C As Long ' the column to write to (= month)
With ActiveSheet ' better to define the sheet by name
' make sure your result range doesn't overlap the search ranges
Set DateRng = .Range(.Cells(11, 8), .Cells(.Rows.Count, 8).End(xlUp))
Set DaysRng = .Range(.Cells(11, 30), .Cells(.Rows.Count, 30).End(xlUp))
For C = 1 To 12
StartDate = DateSerial(.Range(YearCell).Value, C, 1)
EndDate = DateSerial(Year(StartDate), C + 1, 0)
.Cells(8, 1 + C).Value = Application.WorksheetFunction. _
CountIfs(DateRng, ">=" & StartDate, _
DateRng, "<=" & EndDate, _
DaysRng, "<=" & 50)
.Cells(9, 1 + C).Value = Application.WorksheetFunction. _
CountIfs(DateRng, ">=" & StartDate, _
DateRng, "<=" & EndDate, _
DaysRng, ">" & 50, _
DaysRng, "<=" & 100)
.Cells(10, 1 + C).Value = Application.WorksheetFunction. _
CountIfs(DateRng, ">=" & StartDate, _
DateRng, "<=" & EndDate, _
DaysRng, ">" & 100)
Next C
End With
End Sub
It seems that your data only show day and month. That's a matter of display (cell formatting). The underlying dates include the year. Therefore you need a year when you search them. I have added a year in A1. You can move that cell to anywhere or hard-program a year into the code.
Your definition of search ranges, just by columns, is insufficient, and it overlaps with the cells into which you want to write the counts. I have assumed a start row and my code finds the end of each column. Please try it out.
In the code below the counting procedure is converted into a function that returns its result as an array. This is called by the main procedure and written to the worksheet. I couldn't test the code en total but the part described above works as advertised.
Private Sub CommandButton2_Click() ' update averages
' "Year" is the name of a VBA function.
' To use the same word here as a variable name is asking for trouble.
Const Year As Integer = 2019
Dim wbSource As Workbook, wsSource As Worksheet
Dim Wb As Workbook, Ws As Worksheet
Dim fname As String
fname = Me.TextBox1.Text
If Len(fname) = 0 Then
MsgBox "No file selected", vbCritical, "Error"
Exit Sub
End If
Set Wb = ThisWorkbook
Set Ws = Wb.Sheets("Table 2") '
' open source workbook
Set wbSource = Workbooks.Open(fname, False, True) ' no link update, read only
Set wsSource = wbSource.Sheets("Sheet1") ' change to suit
Dim iRow As Long, lastRow As Long
Dim sMth As String, iMth As Long
Dim count(12) As Long, sum(12) As Long ' Confusing: Count is a VBA method
' scan down source workbook calc average
' observe how .Rows.count is written with lower case c
' because of your declaration of "count" in that way
lastRow = wsSource.Cells(wsSource.Rows.count, 1).End(xlUp).Row
For iRow = 1 To lastRow
If IsDate(wsSource.Cells(iRow, 8)) And _
IsNumeric(wsSource.Cells(iRow, 30)) Then
iMth = Month(wsSource.Cells(iRow, 8)) ' col H
sum(iMth) = sum(iMth) + wsSource.Cells(iRow, 30) ' Col AD
count(iMth) = count(iMth) + 1
End If
Next
' counting the rows
Ws.Cells(8, 2).Resize(3, 12).Value = RowsCount(Year, wsSource)
' close source worbook no save
wbSource.Close False
' update Table 2 with averages
With Ws.Range("A3")
For iMth = 1 To 12
.Offset(0, iMth - 1) = MonthName(iMth) & " " & Year
If count(iMth) > 0 Then
.Offset(1, iMth - 1) = sum(iMth) / count(iMth)
.Offset(1, iMth - 1).NumberFormat = "0.0"
End If
Next
End With
Dim msg As String
msg = iRow - 1 & " rows scanned in " & TextBox1.Text
MsgBox msg, vbInformation, "Table 2 updated"
End Sub
Private Function RowsCount(ByVal RefYear As Integer, _
Ws As Worksheet) As Variant
' 025
Dim Fun As Variant ' Function return value
Dim DateRng As Range ' the range to search for dates in
Dim DaysRng As Range ' the range to search for days in
Dim StartDate As Date ' first day to include in count
Dim EndDate As Date ' last day to include in count
Dim C As Long ' the column to write to (= month)
With Ws
' make sure your result range doesn't overlap the search ranges
Set DateRng = .Range(.Cells(11, 8), .Cells(.Rows.count, 8).End(xlUp))
Set DaysRng = .Range(.Cells(11, 30), .Cells(.Rows.count, 30).End(xlUp))
End With
ReDim Fun(1 To 3, 1 To 12)
For C = 1 To 12
StartDate = DateSerial(RefYear, C, 1)
EndDate = DateSerial(Year(StartDate), C + 1, 0)
Fun(1, C) = Application.WorksheetFunction. _
CountIfs(DateRng, ">=" & StartDate, _
DateRng, "<=" & EndDate, _
DaysRng, "<=" & 50)
Fun(2, C) = Application.WorksheetFunction. _
CountIfs(DateRng, ">=" & StartDate, _
DateRng, "<=" & EndDate, _
DaysRng, ">" & 50, _
DaysRng, "<=" & 100)
Fun(3, C) = Application.WorksheetFunction. _
CountIfs(DateRng, ">=" & StartDate, _
DateRng, "<=" & EndDate, _
DaysRng, ">" & 100)
Next C
RowsCount = Fun
End Function
Capitalization is very important in VBA. As the example of your use of the Count word demonstrates VBA will try to adapt to your style. Therefore, if you have no system you will destroy VBA's. The theory I follow is to use caps and smalls on all variable names. Then I type using only lower case. VBA will correct my typing and this serves as a spell checker, helping to avoid typos. There are a few exceptions to this but that's the general rule I follow.
It's the same with the choice of variable names. Generally I avoid using words VBA uses. That prevents confusion for me (never mind VBA). But if I do use VBA's words I follow the existing capitalization.
I Have Table like this, where i have to use macro because my table always change Every day (SSAS)
so i have use macro to filter automatically,
I am able to sum Amount based on same Vendorname, PONuber and Date on Column E (Subtotal).
and then filter to show Subtotal AMount >500
I want to show only row >500 (Column E), and pop up message to count PONumber (Column B) how many Unique PO Number (Only Visible Row to count)
i've been stuck how to count only Visible Unique PO Number and show it on Pop Up message
this is my Macro
Sub FilterCOunt_Click()
Dim Condition As Variant
Dim AVal As Variant
Dim LastRow As Long
Dim Hide, popup As Long
Dim message As String
Dim sht As Worksheet
'----------------------------
Dim dictionary As Object
Set dictionary = CreateObject("scripting.dictionary")
'---------------------------
Application.ScreenUpdating = False
Application.EnableEvents = False
Application.AskToUpdateLinks = False
Application.DisplayAlerts = False
Application.Calculation = xlAutomatic
Application.ScreenUpdating = False
Application.StatusBar = False
'------------------
Columns.EntireColumn.Hidden = False
Rows.EntireRow.Hidden = False
Columns("E:Z").EntireColumn.Delete
Range("E:Z").EntireColumn.Insert
Range("E1").Value = "Sub Total >500 "
Set sht = ActiveSheet
LastRow = sht.Range("B" & Rows.Count).End(xlUp).Row
'-------------------
For i = 2 To LastRow ' with last row count =SUMIFS(I:I,A:A,A8,B:B,B8,C:C,C8)
AVal = "A" & i
BVal = "B" & i
CVal = "C" & i
Worksheets("Sheet3").Range("E" & i).Formula = "=SUMIFS(D:D,A:A," & AVal & ",B:B," & BVal & ",C:C," & CVal & ")"
Next i
With sht.Range("E1:E" & LastRow)
.AutoFilter
.AutoFilter field:=1, Criteria1:=">=500"
End With
'----------Count Pop UP
Dim CountPO As Long
Range("G1").FormulaArray = "=SUM(IF(FREQUENCY(IF(SUBTOTAL(3,OFFSET(B2,ROW(B2:B22)-ROW(B2),1)),IF(B2:B22<>"",MATCH(""&B2:B22,B2:B22&"",0))),ROW(B2:B22)- ROW(B2)+1),1))"
MsgBox "We Found " & CountPO & " PO Open(s)", _
vbInformation, "PO Found"
End Sub
and this is the formula to count it
{=SUM(IF(FREQUENCY(IF(SUBTOTAL(3,OFFSET(B2,ROW(B2:B22)-ROW(B2),1)),IF(B2:B22<>"",MATCH("~"&B2:B22,B2:B22&"",0))),ROW(B2:B22)-ROW(B2)+1),1))}
If you are pulling from a Database via SSAS you can use Power Query to link to your SSAS DataModel to Excel and you can insert a Calculated Measure in Dax from there with DistinctCount.
Count:=Calculate(DistinctCount(TableName[PONumber]),TableName[Amount]>500)
Alternatively if you want total insights on your specified issue you can add a measured column and then you can use Power Pivot to filter for your criteria live on refresh to the data model, completely negating the need for VBA entirely.
Incidentally it is pertinent to remember VBA is the sledge hammer of solutions please use the DataModel Tools before you ever think of a macro solution remember, VBA is an Application Programming Language and many IT Security Systems will disable it because it opens the system up for malware, you can literally change any file or program in VBA including calling delete system files
Meanwhile having a set DataModel in a locked file that requires user access behind LAN security is easily more secure than allowing your computer to have open programatic access.
This is an alternative formula (which doesn't require any filtering)
=SUM(--(FREQUENCY(IF(B2:B20>500,MATCH(A2:A20,A2:A20,0)),ROW(A2:A20)-ROW(A2)+1)>0))
It's an array formula so using VBA
Range("E1").FormulaArray = "=SUM(--(FREQUENCY(IF(B2:B20>500,MATCH(A2:A20,A2:A20,0)),ROW(A2:A20)-ROW(A2)+1)>0))"
A formula for your cell E2, which is not an array formula, is
=SUMPRODUCT((B2=B$2:B$23)*(A2=A$2:A$23)*(D$2:D$23))
Copy it down, as usual.
See here for why not using an array formula (if you have an alternative).
I am not certain this solves your question, as I did not fully understand it.
You can use the following code. I have implemented Collection to get the unique count.
This will count the unique rows in B column where value in E column > 500.
Private Sub GetUniqueCount() AS Variant
Dim Test As New Collection
Dim rng As Range
For i = 2 To 6 'Replace 6 with last row(without filtration)
Value = Cells(i, "B").Value
check = Contains(Test, Value)
If Cells(i, "E").Value > 500 And Not check And Len(Value) > 0 Then
Test.Add Value, CStr(Value)
End If
Next i
GetUniqueCount = Test.count
End Sub
'Function to check if the value exists in Collection or not
Public Function Contains(col As Collection, key As Variant) As Boolean
Dim obj As Variant
On Error GoTo err
Contains = True
obj = col(key)
Exit Function
err:
Contains = False
End Function
Step 1: Post my code to a new module.
Step 2: Bind you button to the macro named "filterAndCount"
Step 3: Click the buton and rejoice :-)
Code description:
1) The code loops all the rows in the table.
2) First it checks if the Sub Total is above the limit (500).
3) If equal or below it hides the row and moves on to the next row.
4) If above it checks if the value already exists in the array values above.
5) If it does not exists then the value is added to the array.
6) When all rows have been looped only rows with a Sub Total above the limit is visible.
7) Only the unique and visible PO Numbers have been added to the array.
8) The number of values in the array is dispayed in a message box.
Dim wb As Workbook
Dim ws As Worksheet
Dim i As Double
Dim n As Double
Dim subTotalLimit As Double
Dim arr() As String
Sub filterAndCount()
Set wb = ThisWorkbook
Set ws = wb.ActiveSheet
i = 2
subTotalLimit = 500
n = 0
ReDim arr(0 To 0) As String
arr(0) = 0
ws.Columns("E:Z").EntireColumn.Delete
ws.Range("E:Z").EntireColumn.Insert
ws.Range("E1").Value = "Sub Total >500 "
Do While ws.Range("B" & i) <> ""
ws.Range("E" & i).Formula = "=SUMIFS(D:D,A:A,A" & i & ",B:B,B" & i & ",C:C,C" & i & ")"
If ws.Range("E" & i) < subTotalLimit Then
ws.Range("B" & i).EntireRow.Hidden = True
Else
If Not IsNumeric(Application.Match(Range("B" & i).Text, arr(), 0)) Then
arr(n) = Range("B" & i).Value
n = UBound(arr) + 1
ReDim Preserve arr(0 To n) As String
arr(n) = 0
End If
End If
i = i + 1
Loop
MsgBox UBound(arr)
End Sub
Use 2 Dictionary Objects, one for totals and one for unique PO's
Sub filterCOunt()
Const LIMIT = 500
Dim wb As Workbook, ws As Worksheet
Dim iRow As Long, iLastRow As Long, amount As Single
Dim sVendor As String, sPO As String, msg As String, sKey As String
Dim dictPO As Object, dictTotal As Object
Set dictPO = CreateObject("Scripting.Dictionary")
Set dictTotal = CreateObject("Scripting.Dictionary")
Set wb = ThisWorkbook
Set ws = ActiveSheet
iLastRow = ws.Range("B" & Rows.Count).End(xlUp).Row
' first pass to total by po and vendor
For iRow = 2 To iLastRow
sVendor = Trim(ws.Cells(iRow, 1))
sPO = Trim(ws.Cells(iRow, 2))
amount = CSng(ws.Cells(iRow, 4))
sKey = sVendor & "_" & sPO
' sub total
If dictTotal.exists(sKey) Then
dictTotal(sKey) = dictTotal(sKey) + amount
Else
dictTotal.Add sKey, amount
End If
Next
' second pass for PO numbers
For iRow = 2 To iLastRow
sVendor = Trim(ws.Cells(iRow, 1))
sPO = Trim(ws.Cells(iRow, 2))
sKey = sVendor & "_" & sPO
' sub total
ws.Cells(iRow, 5) = dictTotal(sKey)
If dictTotal(sKey) > LIMIT Then
If Not dictPO.exists(sPO) Then
dictPO.Add sPO, iRow
End If
End If
Next
' filter
With ws.Range("E1:E" & iLastRow)
.AutoFilter
.AutoFilter field:=1, Criteria1:=">=" & LIMIT
End With
msg = "No of open PO's = " & dictPO.Count
MsgBox msg, vbInformation
End Sub
First, for your code Count Pop UP to work, let's change all from "" to """"
Second, to be able to notify a Unique PO Number and show it on Pop Up message, you must call the value received from cell G1, or, safer, use evaluate to get the result of this expression.
Your code will probably work now
'Dim CountPO As Long
Range("G1").FormulaArray = "=SUM(IF(FREQUENCY(IF(SUBTOTAL(3,OFFSET(B2,ROW(B2:B22)-ROW(B2),1)),IF(B2:B22<>"""",MATCH(""""&B2:B22,B2:B22&"""",0))),ROW(B2:B22)- ROW(B2)+1),1))"
MsgBox "We Found " & [g1].Value2 & " PO Open(s)", vbInformation, "PO Found"
however, your formula only counts all unique values including less than 500, in addition it is quite long. You can replace it using the shorter formula like the following code:
Dim formula_string As String
formula_string = "=SUMPRODUCT((B2:B22>3)*(C2:C22<>"""")/COUNTIF(B2:B22,B2:B22&""""))"
MsgBox "We Found " & Application.Evaluate(formula_string) & " PO Open(s)", vbInformation, "PO Found"
Hope it helps!
I have an invoice from a service provider that I need to format so I can use the data in Excel. But, the formatting is not consistent.
There are three (3) columns:
ID
Description
Amount
Many ID#s on the invoice have a one line (row) description.
But just as many have 2-11 lines (rows) of description.
The ID# is only listed once with each set of description lines.
Up to this point, I have used Excel Formulas. But, all my formulas is making things go very slow.
VBA would be way faster.
What I have done is created an index system looking for new ID#s.
Then I have created a cascading concatenate formula based on the given index system.
The amount has been easy to pull out using a LEFT formula, since the amount lists USD.
I then have a second sheet that does a VLOOKUP off of the first sheet to pull the ID's, final concatenated descriptions, and Amounts.
Our last invoice had 17,427 lines of data with only 1,717 ID#s.
Here is an example of what I am working with:
I want it to look like this:
one of the possible solutions below:
'assume that Id in column `A`, Description in column `B`, Amount in `C` and header in row 1
Sub somecode()
Dim wb As Workbook: Set wb = ActiveWorkbook
Dim sh As Worksheet: Set sh = wb.ActiveSheet
Dim lastRow&: lastRow = sh.Cells(Rows.Count, "B").End(xlUp).Row
Dim idColumn As Range: Set idColumn = sh.Range("A1:A" & lastRow)
Dim dic As Object: Set dic = CreateObject("Scripting.Dictionary")
Dim cl As Range, keyID, valueDescription$, valueAmount$
For Each cl In idColumn
If cl.Value <> "" And Not dic.exists(cl.Value) Then
dic.Add cl.Value, sh.Cells(cl.Row, "B").Value & "|" & sh.Cells(cl.Row, "C").Value
keyID = cl.Value
valueDescription = sh.Cells(cl.Row, "B").Value
valueAmount = sh.Cells(cl.Row, "C").Value
ElseIf cl.Value = "" Then
valueDescription = valueDescription & " " & sh.Cells(cl.Row, "B").Value
dic(keyID) = valueDescription & "|" & valueAmount
End If
Next cl
Set sh = wb.Sheets.Add: sh.Name = "Result " & Date & " " & Replace(Time(), ":", "-")
Dim dkey, xRow&: xRow = 1
For Each dkey In dic
sh.Cells(xRow, "A").Value = dkey
sh.Cells(xRow, "B").Value = Split(dic(dkey), "|")(0)
sh.Cells(xRow, "C").Value = Split(dic(dkey), "|")(1)
xRow = xRow + 1
Next dkey
sh.Columns("A:C").AutoFit
End Sub
test:
I wrote code for you to do this job. Please install it in a standard code module. That is one that you have to insert. None of the existing is suitable.
Option Explicit
Enum Nws ' Worksheet setup (set values as required)
NwsFirstDataRow = 2
NwsNumColumns = 8 ' total number of columns in the sheet
NwsID = 1 ' Columns: 1 = column A
NwsDesc ' undefined = previous + 1
NwsAmt = 5 ' 5 = column E
End Enum
Sub MergeRows()
' Variatus #STO 24 Jan 2020
Dim Wb As Workbook
Dim Ws As Worksheet
Dim Rng As Range
Dim RowArr As Variant
Dim Desc As String, Amt As Double
Dim Tmp As Variant
Dim R As Long
' define workbook and worksheet as required
Set Wb = ActiveWorkbook ' this need not be ThisWorkbook
Set Ws = Wb.Worksheets("Invoice") ' change as appropriate
Application.ScreenUpdating = False
With Ws
R = .Cells(.Rows.Count, NwsDesc).End(xlUp).Row
For R = R To NwsFirstDataRow Step -1
If (R Mod 25) = 3 Then 'NwsFirstDataRow Then
Application.StatusBar = "Another " & R & " rows to process."
End If
Tmp = Trim(.Cells(R, NwsID).Value)
If Len(Tmp) Then
Set Rng = Range(.Cells(R, 1), .Cells(R, NwsNumColumns))
RowArr = Rng.Value
RowArr(1, NwsAmt) = TextToAmount(RowArr(1, NwsAmt))
If Len(Desc) Then
' if you want a comma instead of a line break
' replace Chr(10) with "," in the next line:-
RowArr(1, NwsDesc) = RowArr(1, NwsDesc) & Chr(10) & Desc
RowArr(1, NwsAmt) = RowArr(1, NwsAmt) + Amt
Desc = ""
Amt = 0
End If
With Rng
.Value = RowArr
.Cells.VerticalAlignment = xlTop
.Cells(NwsAmt).NumberFormat = "$#,##0.00"
End With
.Rows(R).AutoFit
Else
Tmp = Trim(.Cells(R, NwsDesc).Value)
If Len(Desc) Then Desc = Chr(10) & Desc
Desc = Tmp & Desc
Tmp = TextToAmount(.Cells(R, NwsAmt).Value)
If Tmp Then Amt = Amt + Tmp
.Rows(R).EntireRow.Delete
End If
Next R
End With
With Application
.ScreenUpdating = True
.StatusBar = "Done"
End With
End Sub
Private Function TextToAmount(ByVal Amt As Variant) As Double
Dim Tmp As Variant
Tmp = Trim(Amt)
If Len(Tmp) Then Tmp = Mid(Tmp, InStr(Tmp, "$") + 1)
TextToAmount = Val(Tmp)
End Function
Before you can run it you need to set the enumerations at the top to tell the code where your data and columns are. Toward the same end, please set the variables for workbook (Wb) and worksheet (Ws) in the procedure itself.
Note that the code adds the price, if any, in the rows that are deleted to the amount set against the remaining item.
Finally, you will see that I programmed the different rows to become lines in a single cell. That isn't what you asked for. If you want the items separate by commas look for the remark in the code where you can change this.
The group column in my table contains a value as either 1 or 2 . I want to copy the row with value as 1 to Sheet2 and rows with values as 2 to sheet3 using a button. Also it should show error message if cells are left blank or if value is neither 1 nor 2.
Roll no meter width group
112 150 130 1
Since i am new to coding i have following this approach
check if the cell is empty and generate an error message
check if the cell contains value other than 1 or 2 and generate error message
finally copy the row with values as 1 to Sheet2 and rest all in sheet3
I need help in doing this is an effective way. As i have to keep the size of file down
enter code here
Private Sub CommandButton2_Click()
Dim i As Integer
p = Sheet1.Range("l1").Value 'no. of filled cells in the range
Application.DisplayAlerts = False
Sheet1.Activate
''checking if the range is empty
For i = 29 To p + 29
If Sheet1.Range("l" & i).Value = "" Then
MsgBox ("PLEASE ENTER THE SHRINKAGE GROUP FOR CELL NO. l" & i)
Range("L" & i).Activate
End
End If
Next i
'' checking if the range contains values other than 1 or 2
For i = 29 To p + 29
If Sheet1.Range("l" & i).Value <> 1 And Sheet1.Range("l" & i).Value <> 2 Then
MsgBox ("SHADE GROUP DOES NOT EXIST FOR CELL NO. l" & i)
Range("L" & i).Activate
End
End If
Next i
' sort based on the group
Range("a29:L300").Sort _
Key1:=Range("l29"), Header:=xlYes
'count the number of rolls in group 1
Dim x, y As Long
Dim a, b As Integer
x = Range("L" & Rows.Count).End(xlUp).Row
If x < 29 Then x = 29
a = Application.WorksheetFunction.CountIf(Range("L12:L" & x), 1) + 28
Range("M1").Value = a
' count the number of rolls in group 2
y = Range("L" & Rows.Count).End(xlUp).Row
If y < 29 Then y = 29
b = Application.WorksheetFunction.CountIf(Range("L12:L" & x), 2)
Range("n1").Value = b
'' copying groupwise to different sheet
Sheet1.Range("a29", "l" & a).Copy
Sheet2.Range("a5").PasteSpecial xlPasteAll
Sheet2.Range("a5").PasteSpecial xlPasteValuesAndNumberFormats
'' copying group 2
Sheet1.Range("a" & a + 1, "l" & a + b).Copy
Sheet5.Range("a5").PasteSpecial xlPasteAll
Sheet5.Range("a5").PasteSpecial xlPasteValuesAndNumberFormats
End Sub
Create named ranges for your source data and for the rows after which you want it to be copied. In this example I've used "source", "range1" and "range2". Then the following code copies the source data into the appropriate place:
Sub copyData()
Dim source As Range, range1 As Range, range2 As Range
Dim r As Range
Set source = Range("source")
Set range1 = Range("range1")
Set range2 = Range("range2")
For Each r In source.Rows
If r.Cells(1, 4).Value = 1 Then
copyRow r, range1
ElseIf r.Cells(1, 4).Value = 2 Then
copyRow r, range2
Else
' handle error here
End If
Next r
End Sub
Sub copyRow(data As Range, targetRange As Range)
Set targetRange = targetRange.Resize(targetRange.Rows.Count + 1, targetRange.Columns.Count)
For i = 1 To 3
targetRange.Cells(targetRange.Rows.Count, i).Value = data.Cells(1, i).Value
Next i
End Sub
There's probably a much more elegant way of doing this involving array formulae, but this should do the trick.
For validating that each cell contains only "1" or "2", you can include additional code where I've put a comment, but you'd be better off handling this as a data validation.