Avoid Loop over Arrays in VBA? - vba

my goal is to write a function that converts returns to prices.
I have a vector of returns stored inside a range in excel like this:
r1
r2
...
rn
Now suppose that these returns are stored in Column B.
In VBA wrote the following code
Dim r As Range
Set r = ThisWorkbook.Sheets("Foglio1").Range("B2:B" & _
ThisWorkbook.Sheets("Foglio1").Range("B" & Rows.Count).End(xlUp).Row)
Dim temp() As Variant
temp = r
So I succesfully assigned the value r1, r2, ..., rn to an array that I called temp.
Now if I were in R or MATLAB I would have done the following, in order to convert return to prices:
temp = cumprod(1 + temp)
with one line of command I would have converted returns to prices
(1 + temp) should sum 1 to each element of array and cumprod should return me a vector with the cumulative product.
Is it possible that to achieve the same result I am forced to use for loop in VBA?
thank you very much for your time
have a great week end

Yes the only way to do this directly in VBA is with loops.
It is also possible to do it indirectly in VBA by using Excel Worksheet functions, but its actually usually faster to copy the range into a VBA array as you are doing and then process it with loops.
You can also write (or find and download) libraries that have callable functions and subroutines to hide the Loops from you, but they're still doing the loops.
As one comment said "Learn to love the loops". That's just how it works in VBA.
Ironically, I think the actual fastest way to do this would be to add a new column, let's say starting at Z2 that had Z2=B2+1 and every other row/cell was Z*=(B*+1)*Z[*-1].

You could do with SQL maybe?
This worked for my testing
Public Function PRODUCT_FUNCTION(strRange As String)
Dim c As ADODB.Connection
Dim r As ADODB.Recordset
strInputFile = ThisWorkbook.FullName
Set c = New ADODB.Connection
strConnectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & strInputFile & ";" & _
"Extended Properties=""Excel 12.0 Xml;HDR=No"";"
c.ConnectionString = strConnectionString
c.Open
strsql = "Select Exp(Sum(Log([F1]))) from [Sheet1$" & strRange & "]"
Set r = New ADODB.Recordset
r.Open strsql, c, 1
PRODUCT_FUNCTION = r.Fields(0).Value
r.Close
c.Close
Set r = Nothing
Set c = Nothing
End Function

there's actually a way exploiting PasteSpecial() method of Range object and WorksheetFunction.Product() method:
Function CumulativeDiscount(discountsRng As Range) As Double
With discountsRng
.Copy
With .Offset(, .Parent.UsedRange.Columns.Count)
.Value = 1
.PasteSpecial , Operation:=xlPasteSpecialOperationAdd
Application.CutCopyMode = False
CumulativeDiscount = WorksheetFunction.Product(Application.Transpose(.Cells))
.ClearContents
End With
End With
End Function
that you could use in your "main" code as follows:
Sub main()
With ThisWorkbook.Sheets("Foglio1")
MsgBox CumulativeDiscount(.Range("B2", .Cells(.Rows.Count, "B").End(xlUp)))
End With
End Sub
the only limitation being WorksheetFunction.Product() accepts up to 30 arguments, i.e. the maximum number of discounts to be multiplied is 30

Related

How to remove column headings on returned data when making T-SQL calls from within VBA?

I am using VBA to output information into an Excel worksheet that has been gathered from a SQL Server database called "PHB". I can connect to the database and pull information by calling a view.
When I dump the data into my Excel worksheet the column headings of the database data are included and I don't want that. I have to use an offset to get the data to look right. I can manipulate the results worksheet and remove the columns with VBA. If there is some switch I can use on either (VBA or T-SQL) end it seems like it would be a much cleaner and simpler approach.
Here are the relevant parts of my logic:
Public Sub Show_ProductCode()
Dim PHB_cnn As New ADODB.Connection
Dim ProductCode_qry As String
Dim ProductCode_rst As New ADODB.Recordset
Dim ProductCode_qtbl As QueryTable
Dim ProductCode As String
Dim OffsetAmt As String
Dim OffsetAmt_int As Integer
PHB_cnn.Provider = "sqloledb"
PHB_cnn.CursorLocation = adUseClient
PHB_cnn.Open p_PHB_Connect_s 'In Module
.
.
.
For Each c In DataRange_rng
ProductCode = c.Value
ProductCode_qry = "SELECT * FROM vw_ShowPurchaseHistory WHERE ProductCode = '" & ProductCode & "'"
ProductCode_rst.Open ProductCode_qry, PHB_cnn, adOpenStatic, adLockOptimistic
With ProductCode_rst
OffsetAmt = .RecordCount
If ProductCode_rst.EOF Then
Debug.Print "No Records"
Else
OffsetAmt_int = OffsetAmt_int + (CInt(OffsetAmt) * 2)
With Worksheets("Results")
Set ProductCodes_qtbl = .QueryTables.Add(ProductCode_rst, .Range("A" & OffsetAmt_int))
ProductCodes_qtbl.Refresh
End With
End If
End With
If ProductCode_rst.State = adStateOpen Then ProductCode_rst.Close
Set ProductCode_rst = Nothing
Set ProductCode_qtbl = Nothing
Next c
exit_Show_ProductCode:
If ProductCode_rst.State = adStateOpen Then ProductCode_rst.Close
Set ProductCode_rst = Nothing
Set ProductCode_qtbl = Nothing
Exit Sub
err_Show_ProductCode:
MsgBox Err.Description, vbOKOnly
Resume exit_Show_ProductCode
End Sub
My input data:
My output:
your code is going to be very inefficient as it is executing a SQL statement for each ProductCode. It would be better to loop through these values and build up a SQL IN statement and then, after the loop, execute it once e.g.
...
ProductCode_in = "('productcode1', 'productcode2','productcode3',...., 'productcode_n')"
ProductCode_qry = "SELECT * FROM vw_ShowPurchaseHistory WHERE ProductCode IN '" & ProductCode_in
...
You'll then end up with all your data in Excel with a single header row - which is simple to delete with a VBA statement (sheet.row(1).delete).

Import SQLight database on Excel

I'm trying to import data from a SQLight database to EXCEL with vba and here is my code :
Sub Importer_Contrat()
Dim conn As Object, rst As Object
Dim strSQL As String, table_name As String
Set conn = CreateObject("ADODB.Connection")
Set rst = CreateObject("ADODB.Recordset")
' OPEN CONNECTION
conn.Open "DRIVER=SQLite3 ODBC Driver;Database=" & Chemin_BDD & BDD2 & ";"
strSQL = "SELECT * FROM " & Contract_Table
' OPEN RECORDSET]
rst.Open strSQL, conn
' OUTPUT TO WORKSHEET
sh_test_sql.Range("test_paste").CopyFromRecordset rst
rst.Close
End Sub
My data data base has only 3 columns (chrono is Integer, Nom is Text and Date is Integer)
The vba works wells when I request an Integer but each time it is asked to import Data from the column Name which is Text and not Integer it doesn't work.
With the code above I just receive the first colum Chrono in Integer.
What is also very strange is that if i use this code :
strSQL = "SELECT * FROM " & Contract_Table
' OPEN RECORDSET]
rst.Open strSQL, conn
Do While Not rst.EOF
MsgBox rst(1)
rst.MoveNext
Loop
I can see the Text I want to import but it doesn't work with the recorset. Do you know where the problem is coming from ? I need to paste a big table on my Excel sheet and I've been looking the answer for days now.
Thank you in advance !
It seems that, while the data is present in the recordset, the CopyFromRecordset method fails to work. As I have used this method quite a lot against other data sources, I would assume that there is an issue with the ODBC driver.
There are many ways to read the data from a recordset. You can loop over it manually, or you can use the GetRows method to build a 2-dimensional array. This 2-dimensional array has the column as first index and the row as second, both dimensions are 0-based.
The following code uses this method and writes the data into a sheet - only that rows and columns are exchanged. I did a test with Worksheetfunction.Transpose to change this but got a runtime error Type mismatch.
Dim myData
myData = rst.GetRows
Dim r As Range
With ThisWorkbook.Sheets(1)
Set r = .Range(.Cells(1, 1), .Cells(UBound(x, 1) + 1, UBound(x, 2) + 1))
r.Value = x
End With
Thank you for your answer. I also think that there is an issue with the ODBC driver and the function CopyFromRecordset. I have solved my issue with a loop on each value of the sql database and an Array that I paste in my Excel :
ReDim Contract_Array(nb_row - 1, Nb_col_DB_Contracts - 1)
For row_runner = 0 To nb_row - 1
For col_runner = 0 To Nb_col_DB_Contracts - 1
Contract_Array(row_runner, col_runner) = rst(col_runner)
Next col_runner
rst.MoveNext
Next row_runner
sh_test_sql.Range("A1:G2").Value = Contract_Array
Thank you for your help !

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

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

Circular reference is user defined function VBA excel

My aim is to add the values of certain columns using a user defined function in the actual row. The columns are given in another table. I am reading the name of rows, calculating the actual value and sum them. This function called once from excel but executed 4 times. At the end it indicates a circular reference error. There is no such error in the excel file, I checked if the udf return just 42 then there is no error. First I suspected Application.Caller, but ruled out.
Function SumColumnsWithSuffix(suffix As String, rowNumber) As Integer
'can be used only in Összesíto table
Dim myTable As Excel.ListObject
Dim mySheet As Excel.Worksheet
Dim myRow As Excel.ListRow
Set mySheet = ThisWorkbook.Worksheets("összesíto")
Set myTable = mySheet.ListObjects("Számlák")
Dim columnName As String
result = 0
For Each myRow In myTable.ListRows
columnName = Intersect(myRow.Range, myTable.ListColumns("Oszlop név").Range)
columnName = "Összesíto[" & columnName & " " & suffix & "]"
'actualRow = Application.Caller.row
'rowName = actualRow & ":" & actualRow
rowName = rowNumber & ":" & rowNumber
myRowRange = Range(rowName)
actualValue = Intersect(Range(columnName), Range(rowName))
result = result + actualValue
Next myRow
SumColumnsWithSuffix = result
End Function
myRowRange is not explicitly declared (or used, actually) so it is implicitly a Variant. That means your assignment here...
myRowRange = Range(rowName)
...is also making an implicit call to .Value. That call will evaluate the results of every single cell in Range(rowName) to populate the array of Variant that it returns. If any of those cells contains a call to SumColumnsWithSuffix, you'll get a circular reference.

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.