Use VBA to select and deselect multiple slicer items (OLAP data) - vba

I am working on a script which selects only the needed slicer items. I tried using .SlicerItems.Selected = True / False for selecting and deselecting but I am using an OLAP data source in which case .Selected is read-only. The slicer items are in the format of YYYYWW so 7th week of 2018 would be 201807.
I recorded a macro selecting some slicer items and this is what it gave me:
Sub Macro2()
ActiveWorkbook.SlicerCaches("Slicer_YYYYWW").VisibleSlicerItemsList = Array( _
"[Results].[YYYYWW].&[201726]", "[Results].[YYYYWW].&[201727]", _
"[Results].[YYYYWW].&[201728]", "[Results].[YYYYWW].&[201729]", _
"[Results].[YYYYWW].&[201730]", "[Results].[YYYYWW].&[201731]", _
"[Results].[YYYYWW].&[201732]", "[Results].[YYYYWW].&[201733]", _
"[Results].[YYYYWW].&[201734]", "[Results].[YYYYWW].&[201735]", _
"[Results].[YYYYWW].&[201736]", "[Results].[YYYYWW].&[201737]", _
"[Results].[YYYYWW].&[201738]", "[Results].[YYYYWW].&[201739]", _
"[Results].[YYYYWW].&[201740]", "[Results].[YYYYWW].&[201741]", _
"[Results].[YYYYWW].&[201742]", "[Results].[YYYYWW].&[201743]", _
"[Results].[YYYYWW].&[201744]", "[Results].[YYYYWW].&[201745]", _
"[Results].[YYYYWW].&[201746]", "[Results].[YYYYWW].&[201747]", _
"[Results].[YYYYWW].&[201748]", "[Results].[YYYYWW].&[201749]", _
"[Results].[YYYYWW].&[201750]", "[Results].[YYYYWW].&[201751]", _
"[Results].[YYYYWW].&[201801]", "[Results].[YYYYWW].&[201802]", _
"[Results].[YYYYWW].&[201803]")
End Sub
So I tried following this template and create an array like that. This is how far I have gotten:
Sub arrayTest()
Dim startDate As Long
Dim endDate As Long
Dim n As Long
Dim i As Long
Dim strN As String
Dim sl As SlicerItem
Dim strArr As Variant
Dim dur As Long
Dim result As String
endDate = Range("C17").Value ' endDate is the last SlicerItem to be selected
startDate = Range("G17").Value ' startDate is the first SlicerItem to be selected
dur = Range("C19").Value ' duration is the the number of SlicerItems to be selected
i = 0
ReDim strArr(dur) As Variant
With ActiveWorkbook.SlicerCaches("Slicer_YYYYWW")
' .ClearManualFilter
For n = startDate To endDate
strN = CStr(n) ' convert n to string
If n = 201753 Then ' this is needed for when the year changes
strN = CStr(201801)
n = 201801
End If
strArr(i) = """[Results].[YYYYWW].&[" & strN & "]""" ' write string into array
i = i + 1
' For Each sl In .SlicerCacheLevels(1).SlicerItems
' If sl.Name = strN Then
' sl.Selected = True
' Else
' sl.Selected = False ' this is read-only for OLAP data so it's not working
' End If
' Next
Next
MsgBox Join(strArr, ", ") ' the MsgBox returns the correct string to be applied to select the right slicer items
.VisibleSlicerItemsList = Join(strArr, ", ") ' Error 13: Type mismatch
End With
End Sub
Currently, the code gives Error 13: Type mismatch on .VisibleSlicerItemsList = Join(strArr, ", "), which is also commented. So I'm guessing that either dimensioning strArr as Variant is wrong, the data is not inserted correctly into strArr or it's just impossible to do it this way. In the case of the latest one, how should I do it?
The part commented out on lines 29-35 does not work as it gives the usual error of Application-defined or object-defined error (1004) on sl.Selected = False.

I had a similar issue to overcome. Which I resolved using the following code:
Sub show_SlicerItems()
Dim sc As SlicerCache
Dim sL As SlicerCacheLevel
Dim si As SlicerItem
Dim slicerItems_Array()
Dim i As Long
Application.ScreenUpdating = False
Set sc = ActiveWorkbook.SlicerCaches("Slicer_Name")
Set sL = sc.SlicerCacheLevels(1)
ActiveWorkbook.SlicerCaches("Slicer_Name").ClearManualFilter
i = 0
For Each si In sL.SlicerItems
ReDim Preserve slicerItems_Array(i)
If si.Value <> 0 Then
slicerItems_Array(i) = si.Name
i = i + 1
End If
Next
sc.VisibleSlicerItemsList = Array(slicerItems_Array)
Application.ScreenUpdating = True
End Sub

You need to feed .VisibleSlicerItemsList an array, not a string. Ditch the Join.
And your strArr assignment should be like this: strArr(i) = "[Results].[YYYYWW].&[" & strN & "]" i.e. you don't need to pad it out with extra "
Edit: Out of interest, I happen to be building a commercial add-in that is effectively a Pop-up Slicer, that allows you to filter an OLAP PivotTable to show all items between a range like you are attempting to do. It also lets you filter on wildcards, crazy combinations of AND and OR, and filter on lists stored in external ranges.
Here's a screenshot of it in action. Note there is a search bar up the top that lets you use < or > together to set lower and upper limits, which is what I've done in the current Search. And you can see the result: it has correctly identified the 14 items from the PivotField that fit the bill.
All I need to do to filter the PivotTable on these is click the "Filter on selected items" option, and it does just that:
But working out how to do this - particularly given the limitations of the PivotTable object model (especially where OLAP PivotTables are concerned) was a VERY long term project, with many, many hurdles to overcome to make it work seamlessly. I can't share the code I'm afraid, as this is a commercial offering that I aim to release shortly. But I just wanted to highlight that while this is certainly possible, you are going to be biting off quite a bit if you want it to not throw errors when items don't exist.

Forget my other answer...you can use the Labels Filter to do this easily, provided the field of interest is in the PivotTable as either a Rows or Columns field. Fire up the Macro Recorder, and do the following:
...and you'll see that the PivotTable gets filtered:
...and the resulting code is pretty simple:
ActiveSheet.PivotTables("PivotTable1").PivotFields("[Table1].[YYYYWW].[YYYYWW]" _
).PivotFilters.Add2 Type:=xlCaptionIsBetween, Value1:="201726", Value2:= _
"201803"

Use this:
Sub seleciona_lojas()
Dim strArr()
Dim x As Long
Dim i As Long
For x = 2 To 262
ReDim Preserve strArr(i)
strArr(i) = "[Lojas].[Location_Cd].&[" & Planilha5.Range("B" & x).Value & "]"
i = i + 1
Next x
ActiveWorkbook.SlicerCaches("SegmentaçãodeDados_Location_Cd1").VisibleSlicerItemsList = strArr
End Sub

Related

Simple way to find max level 1 WBS and put all Level 2 WBS into array?

I can't seem to find anything about how I might do this from the documentation. My question basically has it all. I need the max WBS level 1 value as an integer, and then to loop through all its level2 subtasks/summaries and put a couple of their values into an array.
It would also be handy if I could get number of subtasks that belong to that summary before iterating so I could dim my array with the correct rows/columns and not have to transpose it after-the-fact.
Any help or guidance would be appreciated, MS Project documentation is awful and the internet doesn't have much else on a lot of this.
I Don't want to have to do this:
Dim TopVal As Integer
For Each t in ActiveProject.Tasks
Dim tVal As Integer
tVal = t.WBS.Split("."c)(0)
If tVal > TopVal Then TopVal = tVal
Next t
Unfortunately, you will have to loop to figure things out. MS Project doesn't allow you to pull in a set of fields (like all the WBSs) into an array without looping through everything. For this problem, you'll need to determine two different bits of information: what level WBS you're working with and how many levels of sub-tasks are underneath that given WBS.
At the main program level, you'll need to run through ALL the tasks and determine the WBS level of each task. Once you get the level you want, then you can determine the number of sub-tasks.
Private Sub test()
With ThisProject
Dim i As Long
For i = 1 To .Tasks.count
Dim subWBSCount As Long
If .Tasks.Item(i).OutlineLevel = 2 Then
subWBSCount = GetSubWBSCount(.Tasks.Item(i).wbs, i)
Debug.Print "At level 2 (" & .Tasks.Item(i).wbs & _
") there are " & subWBSCount & " sub tasks"
'-----------------------------------------------
' you can properly dimension your array here,
' then fill it with the sub-task information
' as needed
'-----------------------------------------------
End If
Next i
End With
End Sub
When you need to count the sub-tasks under the level 2 WBS, it's easiest to break into a separate function to keep the logic straight. What it does it to start with the given task and work down, comparing each subsequent task's WBS "prefix" -- meaning if you're looking for sub-tasks under WBS 1.1, then when you see WBS 1.1.1 and 1.1.2, you need to really compare the "1.1" parts of each of them. Count until you run out of sub-tasks.
Private Function GetSubWBSCount(ByVal topWBS As String, ByVal wbsIndex As Long) As Long
'--- loop to find the given WBS, then determine how many
' sub tasks lie under that WBS
With ThisProject
Dim j As Long
Dim count As Long
For j = (wbsIndex + 1) To .Tasks.count
Dim lastDotPos As Long
lastDotPos = InStrRev(.Tasks.Item(j).wbs, _
".", , vbTextCompare)
Dim wbsPrefix As String
wbsPrefix = Left$(.Tasks.Item(j).wbs, _
lastDotPos - 1)
If wbsPrefix = topWBS Then
count = count + 1
'--- check for the edge case where this is
' the very last task, and so our count is
' finished
If j = .Tasks.count Then
GetSubWBSCount = count
Exit Function
End If
Else
'--- once we run out of sub-wbs tasks that
' match, we're done
GetSubWBSCount = count
Exit Function
End If
Next j
End With
End Function
Here's the whole test module:
Option Explicit
Private Sub test()
With ThisProject
Dim i As Long
For i = 1 To .Tasks.count
Dim subWBSCount As Long
If .Tasks.Item(i).OutlineLevel = 2 Then
subWBSCount = GetSubWBSCount(.Tasks.Item(i).wbs, i)
Debug.Print "At level 2 (" & .Tasks.Item(i).wbs & _
") there are " & subWBSCount & " sub tasks"
'-----------------------------------------------
' you can properly dimension your array here,
' then fill it with the sub-task information
' as needed
'-----------------------------------------------
End If
Next i
End With
End Sub
Private Function GetSubWBSCount(ByVal topWBS As String, ByVal wbsIndex As Long) As Long
'--- loop to find the given WBS, then determine how many
' sub tasks lie under that WBS
With ThisProject
Dim j As Long
Dim count As Long
For j = (wbsIndex + 1) To .Tasks.count
Dim lastDotPos As Long
lastDotPos = InStrRev(.Tasks.Item(j).wbs, _
".", , vbTextCompare)
Dim wbsPrefix As String
wbsPrefix = Left$(.Tasks.Item(j).wbs, _
lastDotPos - 1)
If wbsPrefix = topWBS Then
count = count + 1
'--- check for the edge case where this is
' the very last task, and so our count is
' finished
If j = .Tasks.count Then
GetSubWBSCount = count
Exit Function
End If
Else
'--- once we run out of sub-wbs tasks that
' match, we're done
GetSubWBSCount = count
Exit Function
End If
Next j
End With
End Function
I'm not sure what you mean by "I need the max WBS level 1". Wouldn't this just be the first task in your project?.. i.e. ActiveProject.Tasks.Item(1)
As for level 2 tasks in an array: Take a look at the .outlineLevel property of the task. This property tells you if the task is WBS level 1, 2, 3, etc.
See https://msdn.microsoft.com/en-us/vba/project-vba/articles/task-outlinelevel-property-project for further details
As for "dim my array with the correct rows/columns": while you could use an array and either first figure out its size, or keep resizing it as you find more elements; another approach I'd suggest is use a data structure that you can add elements to. My top choice for this is the Collection data type. It is built-in and easy to use, but there are others available too that may be more appropriate for your situation.
I think this snippet should do what your asking for:
Function getLevel2Tasks() As Collection
Dim t As Task
Dim level2Tasks As Collection
Set level2Tasks = New Collection
For Each t In ActiveProject.Tasks
If t.outlineLevel = 2 Then
level2Tasks.Add Item:=t
End If
Next
Set getLevel2Tasks = level2Tasks
End Function
Consider use t.OutlineLevel to sort them
This code finds the task with the highest WBS (e.g. maximum of first part of WBS code), and counts its subtasks based on the outline structure of the schedule.
Sub GetMaxWBSTaskInfo()
Dim MaxWBS As Integer
Dim tsk As Task
Dim MaxWbsTask As Task
Dim NumSubtasks As Integer
' expand all subprojects so loop goes through all subproject tasks
Application.SelectAll
Application.OutlineShowAllTasks
Application.SelectBeginning
For Each tsk In ActiveProject.Tasks
If Split(tsk.WBS, ".")(0) > MaxWBS Then
MaxWBS = Split(tsk.WBS, ".")(0)
Set MaxWbsTask = tsk
End If
Next
NumSubtasks = ChildCount(MaxWbsTask)
Debug.Print "Max WBS level=" & MaxWBS, "Task: " & MaxWbsTask.Name, "# subtasks=" & NumSubtasks
End Sub
Function ChildCount(tsk As Task) As Integer
Dim s As Task
Dim NumTasks As Integer
For Each s In tsk.OutlineChildren
NumTasks = NumTasks + 1 + ChildCount(s)
Next s
ChildCount = NumTasks
End Function

Application.Match not exact value

Have a piece of code that looks for matches between 2 sheets (sheet1 is customer list and rData is copied pdf with invoices). It usually is exact match but in some cases I'm looking for 6 first characters that matches rData
Dim rData As Variant
Dim r As Variant
Dim r20 As Variant
Dim result As Variant
Dim i As Long
rData = ActiveWorkbook.Sheets(2).Range("A1:A60000")
r20 = ActiveWorkbook.Sheets(1).Range("C2:C33")
For Each r In r20
result = Application.Match(r, rData, 0)
If Not IsError(result) Then
For i = 1 To 5
If (result - i) > 0 Then
If (Left(Trim(rData(result - i, 1)), 3) = "418") Then
MsgBox "customer: " & r & ". invoice: " & rData(result - i, 1)
End If
End If
Next
For i = 1 To 15
If (result + i) > 0 Then
If (Left(Trim(rData(result + i, 1)), 3) = "418") Then
MsgBox "customer: " & r & ". invoice: " & rData(result + i, 1)
End If
End If
Next
End If
Next r
End Sub
Only part of this that is giving me a headache is this part result = Application.Match(r, rData, 0). How do it get match for not exact match?
Sample of Sheet1
This is what more or less looks like. Matching after CustomerNumber# is easy because they are the same every invoice. BUT sometimes invoice does not have it so I'm searching after CustomerName and sometimes they have uppercase letters, sometimes there is extra stuff behind it and therefore it cannot find exact match.
Hope it makes sense.
To match the customer name from your customer list to the customer name in the invoice even if it has extra characters appended, you can use the wildcard * in Match().
You also have a typo in the Match() function. r20 should be rData.
This is your code with the fixes applied:
Sub Test()
'v4
Dim rData As Variant
Dim r As Variant
Dim r20 As Variant
Dim result As Variant
Dim i As Long
rData = ActiveWorkbook.Sheets(2).Range("A1:A60000")
r20 = ActiveWorkbook.Sheets(1).Range("C2:C33")
For Each r In r20
result = Application.Match(r & "*", rData, 0) ' <~ Fixed here
If Not IsError(result) Then
For i = 1 To 5
If (result - i) > 0 Then
If (Left(Trim(rData(result - i, 1)), 3) = "418") Then
MsgBox "customer: " & r & ". invoice: " & rData(result - i, 1)
End If
End If
Next
For i = 1 To 15
If (result + i) > 0 Then
If (Left(Trim(rData(result + i, 1)), 3) = "418") Then
MsgBox "customer: " & r & ". invoice: " & rData(result + i, 1)
End If
End If
Next
End If
Next r
End Sub
Notes:
Match() is case insensitive, so it works with different capitalisations.
The data in Sheets(2) must all be text for Match() to work correctly with wildcards.
EDIT1: New better version
EDIT2: Refactored constants and made data ranges dynamic
EDIT3: Allows for any prefix to an invoice number of a fixed length
The following is a better, rewritten version of your code:
Sub MuchBetter()
'v3
Const s_InvoiceDataWorksheet As String = "Sheet2"
Const s_InvoiceDataColumn As String = "A:A"
Const s_CustomerWorksheet As String = "Sheet1"
Const s_CustomerStartCell As String = "C2"
Const s_InvoiceNumPrefix As String = "418"
Const n_InvoiceNumLength As Long = 8
Const n_InvScanStartOffset As Long = -5
Const n_InvScanEndOffset As Long = 15
Dim ƒ As Excel.WorksheetFunction: Set ƒ = Excel.WorksheetFunction ' Shortcut
With Worksheets(s_InvoiceDataWorksheet).Range(s_InvoiceDataColumn)
With .Parent.Range(.Cells(1), .Cells(Cells.Rows.Count).End(xlUp))
Dim varInvoiceDataArray As Variant
varInvoiceDataArray = ƒ.Transpose(.Cells.Value2)
End With
End With
With Worksheets(s_CustomerWorksheet).Range(s_CustomerStartCell)
With .Parent.Range(.Cells(1), .EntireColumn.Cells(Cells.Rows.Count).End(xlUp))
Dim varCustomerArray As Variant
varCustomerArray = ƒ.Transpose(.Cells.Value2)
End With
End With
Dim varCustomer As Variant
For Each varCustomer In varCustomerArray
Dim dblCustomerIndex As Double
dblCustomerIndex = Application.Match(varCustomer & "*", varInvoiceDataArray, 0)
If Not IsError(dblCustomerIndex) _
And varCustomer <> vbNullString _
Then
Dim i As Long
For i = ƒ.Max(dblCustomerIndex + n_InvScanStartOffset, 1) _
To ƒ.Min(dblCustomerIndex + n_InvScanEndOffset, UBound(varInvoiceDataArray))
Dim strInvoiceNum As String
strInvoiceNum = Right$(Trim$(varInvoiceDataArray(i)), n_InvoiceNumLength)
If (Left$(strInvoiceNum, Len(s_InvoiceNumPrefix)) = s_InvoiceNumPrefix) Then
MsgBox "customer: " & varCustomer & ". invoice: " & strInvoiceNum
End If
Next
End If
Next varCustomer
End Sub
Notes:
It is a good idea to use constants so all literal values are typed once only and kept grouped together.
Using the RVBA naming convention greatly increases the readability of the code, and reduces the likelihood of bugs.
Using long, appropriately named variables makes the code essentially self-documenting.
Using .Value2 whenever reading cell values is highly recommended (it avoids implicit casting, making it slightly faster as well as eliminating certain issues caused by the casting ).
Surprisingly, in VBA there are good reasons to put a variable declaration as close as possible to the first use of the variable. Two such reasons are 1) it improves readability, and 2) it simplifies future refactoring. Just remember that the variable is not reinitialised every time the Dim is encountered. Initialisation only occurs the first time.
The twin loops have been rolled into one according to the DRY principle.
Whilst the check for an empty customer name/number is not strictly necessary if you can guarantee it will never be so, it is good defensive programming as an empty value will cause erroneous results.
The negative index check inside the loop has been removed and replaced with the one-time use of the Max() worksheet function in the For statement.
The Min() worksheet function is also used in the For statement to avoid trying to read past the end of the array.
Always use worksheet functions on the WorksheetFunction object unless you are explicitly checking for errors, in which case use the Application object.

Excel VBA: How to use a variable for VisibleItemsList?

I'm trying to control a Pivot table's filter by using VBA and I'm struggling to correctly using the "VisibleItemsList" with a variable that contains the filters settings, I'll post the code for a better understandig:
Public Sub updatePivotTable(param2Set() As Variant)
'Array("[Budget].[AM].&[Aeffe USA]","[Budget].[AM].&[Alberganti]","[Budget].[AM].&[Blokh]","[Budget].[AM].&[Bondi]")
Dim y As Integer
Dim parametersCount As Integer
parametersCount = UBound(param2Set)
Dim filters As String
'filters = "Array("
For y = 0 To parametersCount - 1
filters = filters + """[Budget].[AM].&[" & param2Set(y) & "]"", "
Next y
filters = filters + """[Budget].[AM].&[" & param2Set(y) & "]"""
With Sheets("Export BUDG")
.PivotTables("pvtToExport").PivotFields("[Budget].[AM].[AM]").VisibleItemsList = Array(filters)
End With
End Sub
The error I get is:
Query (1, 28) Sintax ',' incorrect. ("[Budget].[AM].&[Caprioli]",
"[Budget].[AM].&[Bondi]")."
Can anyone point me in the right direction? Thanks.

Read from a web page and using two determiner for new row and next cell in vba excel

I am looking for a way to read from a feed webpage which its structure is something like
A,B,C;E,F,G;....
I want to read this data and put A B and C in the first row and put E F and G in row 2, and etc.
I was looking for a function in VBA, but most of them are for only one determiner.
I also was thinking of using string functions of VBA, which that would be the last resort! Since I must read a long string and then use a cursor (which I don't know if it is like c or not!) that probably leads to unstable performance because first I don't know the volume of data, and second I want to use it in a loop.
Could you please help me with the best solution?
feed = "A,B,C;E,F,G;...."
CSV = Replace( feed, ";", vbNewLine )
TSV = Replace( CSV , ",", vbTab )
Set do = CreateObject("New:{1C3B4210-F441-11CE-B9EA-00AA006B1A69}") ' this is a late bound MSForms.DataObject
do.SetText TSV
do.PutInClipboard
ActiveSheet.Paste
Sub Test()
ParseString1 "A,B,C;D,E,F;G,H,I,J,K,L"
ParseString2 "A,B,C;D,E,F;G,H,I,J,K,L"
End Sub
Sub ParseString1(data As String)
Dim clip As MSForms.DataObject
Set clip = New MSForms.DataObject
data = Replace(data, ",", vbTab)
data = Replace(data, ";", vbCrLf)
clip.SetText data
clip.PutInClipboard
Range("A" & Rows.Count).End(xlUp).Offset(1).PasteSpecial
End Sub
Sub ParseString2(data As String)
Dim aColumns, aRows
Dim x As Long
aRows = Split(data, ";")
For x = 0 To UBound(aRows)
aColumns = Split(aRows(x), ",")
Range("A" & Rows.Count).End(xlUp).Offset(1).Resize(1, UBound(aColumns) + 1) = aColumns
Next
End Sub
You'll need to set a reference to the Microsoft Forms 2.0 Object Library if you use ParseString1.

Excel VBA - Perform Operations on visible cells only

I have a database that has in excess on 200,000 rows. When I was writing a VBA script I had a database of about 20,000 rows in mind so I didn't care whether the database was filtered or not because the VBA script ran quickly. So given the realization that the database is huge and testing the VBA script I was surprised to notice how slowly it ran. So without further to say this is how my code looks like :
Set wsDB = ThisWorkbook.Sheets("DB")
Dim nameIndex As Long: nameIndex = Application.Match(name, wsDB.Rows(1), 0)
Dim formula As String
formula = "=IFERROR(AVERAGEIFS(" + GRA(nameIndex) + "," + GRA(dateIndex) + ",R2C," + GRA(cellNameIndex) + ",RC1" + "),"""")"
where GRA is a function that returns the address of the range of a column.
Private Function GRA(ByRef rngIndex As Long)
GRA = "DB!" + CStr(Range(Cells(2, rngIndex), Cells(rowNos, rngIndex)).Address(1, 1, xlR1C1, 0, 0))
End Function
So given that I now filter the table beforehand how can I adjust my code so that it ignores all the hidden rows and takes into account only what is visible. Of course I am aware that a simple dirty solution would be to simply copy the filter database and paste it in a new sheet but that will affect the performance which is what I'm trying to improve.
You can use the following function to return a range of only visible cells.
Function VisibleCells(Rng As Range) As Variant
Dim R As Range
Dim Arr() As Integer
Dim RNdx As Long
Dim CNdx As Long
If Rng.Areas.Count > 1 Then
VisibleCells = CVErr(xlErrRef)
Exit Function
End If
ReDim Arr(1 To Rng.Rows.Count, 1 To Rng.Columns.Count)
For RNdx = 1 To Rng.Rows.Count
For CNdx = 1 To Rng.Columns.Count
Set R = Rng(RNdx, CNdx)
If (R.EntireRow.Hidden = True) Or _
(R.EntireColumn.Hidden = True) Then
Arr(RNdx, CNdx) = 0
Else
Arr(RNdx, CNdx) = 1
End If
Next CNdx
Next RNdx
VisibleCells = Arr
End Function
The above code came from http://www.cpearson.com/excel/VisibleCells.aspx.
Normally I would only post code that I write however this does exactly what I was thinking.