Origins Destinations Excel VBA API (Google Maps) - excel

I am trying to find the distance and travel time between multiple origins and destinations. For some reason, my code does not work at all. There are no errors, i just have nothing as output. See the attached Image for Excel worksheet.
Sub Origins_Destinations()
Dim a, b, i, Str As String
Dim lineS As Variant
On Error Resume Next
'Application.ScreenUpdating = False
With CreateObject("WinHttp.WinHttpRequest")
Dim iRow As Long: iRow = ThisWorkbook.Worksheets(1).Range("g65000").End(xlUp).Row
For j = 4 To iRow
b = ThisWorkbook.Worksheets(1).Range("b4" & j)
a = ThisWorkbook.Worksheets(1).Range("a4" & j)
.Open "GET", "https://maps.googleapis.com/maps/api/distancematrix/json?units=imperial&origins=" & a & " &destinations= " & b & " &key=MY_KEY", False
.Send
lineS = Split(.ResponseText, vbLf)
For k = 25 To UBound(lineS)
If Trim(lineS(k)) = """distance"" : {" Then
Exit For
End If
Next k
ThisWorkbook.Worksheets(1).Range("c" & j) = lineS(k + 1)
ThisWorkbook.Worksheets(1).Range("d" & j) = lineS(k + 5)
Application.Wait (Now + TimeValue("0:00:01"))
Next j
End With
Application.ScreenUpdating = True
End Sub
enter image description here
Any suggestions ?????

As far as I understand, the code below is what you're looking for to get you started. Enter your Google Maps API Key in the Constant at the top, and then run sub TestRun.
It will replace disallowed characters in the address you provided, then loads the JSON results from Google Matrix into a string, and then, since we're only looking for 1 or 2 values, it will use a messy cheater-method to location the values, that I can't guarantee will always work:
It finds the first occurrence of the word "distance", and then the first occurrence of the word "value" after that, move 3 more characters to the right, and then take whatever is between there and the next " " blank space, and converts it to a value, hopefully the distance in meters.
Then it repeats (from beginning of file) to find "duration" in seconds, the same method. Note that the distance and duration are being returned to variables "byref".
As I said, it's very convoluted, but you get what you pay for. (Normally I wouldn't share code this "yucky", but you're in my neighborhood, so, Go Canada!)
Option Explicit
'3333 University Way,Kelowna,BC,V1V 1V7
'1555 Banks Rd, Kelowna, BC, V1X 7Y8
'1938 Pandosy Street, Kelowna, BC, V1Y 1R7
'2280 Baron Rd, Kelowna, BC, V1X 7W3
Const key = "YOUR-API-KEY-HERE"
Sub testRun()
Dim orig As String, dest As String, distance_Meters As Long, duration_Sec As Long
orig = EncodeEscapeString("3333 University Way,Kelowna,BC,V1V 1V7")
dest = EncodeEscapeString("1555 Banks Rd, Kelowna, BC, V1X 7Y8")
Call getGoogleDistanceMatrix(orig, dest, distance_Meters, duration_Sec)
Debug.Print distance_Meters & "m"
Debug.Print duration_Sec & "sec"
End Sub
Sub getGoogleDistanceMatrix(ByVal orig As String, ByVal dest As String, ByRef distance_Meters As Long, ByRef duration_Sec As Long)
Const distanceTag1 = """distance"""
Const distanceTag2 = """value"""
Const durationTag1 = """duration"""
Const durationTag2 = """value"""
Dim jSON As String, pStart As Long, pEnd As Long
jSON = Get_URL_text("https://maps.googleapis.com/maps/api/distancematrix/json?units=metric&origins=" & orig & "&destinations=" & dest & "&key=" & key)
pStart = InStr(jSON, distanceTag1) + Len(distanceTag1)
pStart = InStr(pStart, jSON, distanceTag2) + Len(distanceTag2) + 3
pEnd = InStr(pStart, jSON, " ")
distance_Meters = Val(Trim(Mid(jSON, pStart, pEnd - pStart)))
pStart = InStr(jSON, durationTag1) + Len(durationTag1)
pStart = InStr(pStart, jSON, durationTag2) + Len(durationTag2) + 3
pEnd = InStr(pStart, jSON, " ")
duration_Sec = Val(Trim(Mid(jSON, pStart, pEnd - pStart)))
End Sub
Function Get_URL_text(url As String) As String
Dim XMLHTTP As Object
Set XMLHTTP = CreateObject("MSXML2.serverXMLHTTP")
XMLHTTP.Open "GET", url, False
XMLHTTP.send
Get_URL_text = XMLHTTP.responseText
Set XMLHTTP = Nothing
End Function
Function EncodeEscapeString(str_In As String) As String
Dim s As String
s = str_In
s = Replace(s, "%", "%25")
s = Replace(s, " ", "%20")
s = Replace(s, Chr(34), "%22")
s = Replace(s, "<", "%3C")
s = Replace(s, ">", "%3E")
s = Replace(s, "#", "%23")
s = Replace(s, "|", "%7C")
EncodeEscapeString = s
End Function
This same "cheater method" can be used to to scrape bits of data from any URL (JSON, HTML, XML, CSV, etc) that has a consistent text output.
You may need to add a Tools -> Reference to support XMLHTTP.
Good luck with that! (and don't forget to "accept" this answer if it's at all useful, I already put more time into this than I intended!)

Related

Unable to update hyperlink address

I'm not very familiar with VBA and am stuck on the last step of finishing my code! What this code does is scan every sheet in the workbook for cells with hyperlinks, and then it goes into the hyperlink and prepends a URL onto it. Everything is working - the last Debug.Print here is properly printing out the correct URL. But then it just hangs and gets stuck on "link.Address = currentAddress". No warning, no error message, it just hangs and does nothing until I press Enter, at which point it highlights that line yellow.
I'm at a loss for how currentAddress can print just fine, but can't be set to the link's address? And it only happens for certain links. Here is one that WORKS:
http://localhost:8000/link?owner=lencompass&name=Create%20a%20JIRA%20ticket%20here!&worksheet=test%20sheet&test=true&destination=https://jira01.corp.censored.com:8443/secure/CreateIssue.jspa%3Fpid=14071%26issuetype=1
Here is one that does NOT WORK:
http://localhost:8000/link?owner=lencompass&name=Core%20Dash%20-%20Performance%20by%20Recipient%20Company%20%26%20Function(Last%2012%20months)&worksheet=test%20sheet&test=true&destination=https://censored.corp.censored.com/accounts/1337/insights/880%3FmultiPeers=309694%2C1586%2C10667%2C1441%2C1009%2C1337%2C1035%2C1028%2C3185%2C1815218%2C96622
These links work when I use them in the browser so I know they are valid links.
Here is my entire VBA script:
Sub trackify_links()
Dim I As Integer
' Loop through each sheet in this workbook
For I = 1 To ActiveWorkbook.Worksheets.Count
' loop through each cell in this sheet
Dim rwIndex As Long
Dim colIndex As Long
Dim maxRow As Long
maxRow = Worksheets(I).Cells(Worksheets(I).Rows.Count, 4).End(xlUp).Row
Worksheets("For IAs").Range("E16") = "Looping over " & maxRow & " rows in sheet: " & Worksheets(I).Name
For rwIndex = 1 To maxRow
' only loop up to the max filled-in column on this row
Dim maxColumn As Long
maxColumn = Worksheets(I).Cells(rwIndex, Worksheets(I).Columns.Count).End(xlToLeft).Column
For colIndex = 1 To maxColumn
Dim linkIndex As Long
Dim link As Hyperlink
For linkIndex = 1 To Worksheets(I).Cells(rwIndex, colIndex).Hyperlinks.Count
Set link = Worksheets(I).Cells(rwIndex, colIndex).Hyperlinks(linkIndex)
' only trackify a link if it isn't already
If Left(link.Address, 30) <> "http://localhost:8000/link?" Then
' this is a QA check - i noticed people putting their local machine paths as links here which won't work for anyone else. Output a list of weird links as a warning
If Left(link.Address, 3) = "../" Or Left(link.Address, 2) = "./" Then
Worksheets("For IAs").Range("E19") = "The link in cell (" & Col_Letter(colIndex) & rwIndex & ") in worksheet " & Worksheets(I).Name & " looks like it's a local path. These links will not work and have not been trackified - consider changing them."
Else
Dim currentAddress As String
' in order for the tracking link to properly redirect, there needs to be an "http://" or "https://" protocol at the beginning
If LCase(Left(link.Address, 7)) <> "http://" And LCase(Left(link.Address, 8)) <> "https://" Then
currentAddress = "https://" & link.Address
Else
currentAddress = link.Address
End If
' replace special characters with hex code so the link is not incorrectly parsed
currentAddress = ConvertToHex(currentAddress)
Dim extraParameters As String
extraParameters = "owner=" & ConvertToHex("lencompass") ' indicate this link belongs to lencompass
extraParameters = extraParameters & "&name=" & ConvertToHex(link.TextToDisplay) ' set the name of this link to the excel link's text"
extraParameters = extraParameters & "&worksheet=" & ConvertToHex(Worksheets(I).Name) ' indicate where in the workbook this link was clicked from (if tab format stays the same it basically will tell what kind of person is clicking)
If Worksheets("For IAs").Range("E3") <> "No" Then _
extraParameters = extraParameters & "&test=true" ' indicate this is a testing link if appropriate
' here we wrap the cell's current link into the tracking link, and customize it with some info about where in the workbook this link was clicked
Debug.Print ("currentaddress: " & currentAddress)
currentAddress = "http://localhost:8000/link?" & extraParameters & "&destination=" & currentAddress
Debug.Print (currentAddress)
link.Address = currentAddress
End If
End If
Next linkIndex
Next colIndex
Next rwIndex
Next I
End Sub
Function Col_Letter(lngCol As Long) As String
Dim vArr
vArr = Split(Cells(1, lngCol).Address(True, False), "$")
Col_Letter = vArr(0)
End Function
Function ConvertToHex(str As String) As String
ConvertToHex = Replace(Replace(Replace(Replace(str, "?", "%3F"), "&", "%26"), " ", "%20"), """", "%22")
End Function

VBA Scrape Date Widget from Search Results

when searching for a particular event. e.g. "oscars 2018 date", Google shows a widget with the date of the event, before any search results. I need to get this date in Excel but it seems difficult in terms of actual coding. I have been tinkering with these functions but not getting any results. The div I am interested in is:
<div class="Z0LcW">5 March 2018, 1:00 am GMT</div>
Here is the full code I am trying to use:
Option Explicit
Public Sub Example()
Call GoogleSearchDescription("oscars 2018 date")
End Sub
Public Function GoogleSearchDescription(ByVal SearchTerm As String) As String
Dim Query As String: Query = "https://www.google.com/search?q=" & URLEncode(SearchTerm)
Dim HTML As String: HTML = GetHTML(Query)
Dim Description() As String: Description = RegExer(HTML, "(<div class=""Z0LcW"">[\w\s.<>/]+<\/div>)")
Description(0) = FilterHTML(Description(0))
Debug.Print Description(0)
Debug.Print "ok"
End Function
Public Function GetHTML(ByVal URL As String) As String
On Error Resume Next
Dim HTML As Object
With CreateObject("InternetExplorer.Application")
.navigate URL
Do Until .ReadyState = 4: DoEvents: Loop
Do While .Busy: DoEvents: Loop
Set HTML = .Document.Body
GetHTML = HTML.innerHTML
.Quit
End With
Set HTML = Nothing
End Function
Private Function URLEncode(ByVal UnformattedString As String) As String
'CAUTION: This function URLEncodes strings to match Google Maps API URL specifications, see note below for details
'Note: We convert spaces to + signs, and skip converting plus signs to anything because they replace spaces
'We also skip ampersands [&] as they should not be parsed out of a valid query
Dim Index As Long, ReservedChars As String: ReservedChars = "!#$'()*/:;=?#[]""-.<>\^_`{|}~"
'Convert all % symbols to encoding, as the unformatted string should not already contain URL Encoded characters
UnformattedString = Replace(UnformattedString, "%", "%" & Asc("%"))
'Convert spaces to plus signs to match Google URI query specifications
UnformattedString = Replace(UnformattedString, " ", "+")
'Iterate through the reserved characters for encoding
For Index = 1 To (Len(ReservedChars) - 1)
UnformattedString = Replace(UnformattedString, Mid(ReservedChars, Index, 1), "%" & Asc(Mid(ReservedChars, Index, 1)))
Next Index
'Return URL encoded string
URLEncode = UnformattedString
End Function
Private Function FilterHTML(ByVal RawHTML As String) As String
If Len(RawHTML) = 0 Then Exit Function
Dim HTMLEntities As Variant, HTMLReplacements As Variant, Counter As Long
Const REG_HTMLTAGS = "(<[\w\s""':.=-]*>|<\/[\w\s""':.=-]*>)" 'Used to remove HTML formating from each step in the queried directions
HTMLEntities = Array(" ", "<", ">", "&", """, "&apos;")
HTMLReplacements = Array(" ", "<", ">", "&", """", "'")
'Parse HTML Entities into plaintext
For Counter = 0 To UBound(HTMLEntities)
RawHTML = Replace(RawHTML, HTMLEntities(Counter), HTMLReplacements(Counter))
Next Counter
'Remove any stray HTML tags
Dim TargetTags() As String: TargetTags = RegExer(RawHTML, REG_HTMLTAGS)
'Preemptively remove new line characters with actual new lines to separate any conjoined lines.
RawHTML = Replace(RawHTML, "<b>", " ")
For Counter = 0 To UBound(TargetTags)
RawHTML = Replace(RawHTML, TargetTags(Counter), "")
Next Counter
FilterHTML = RawHTML
End Function
Public Function RegExer(ByVal RawData As String, ByVal RegExPattern As String) As String()
'Outputs an array of strings for each matching expression
Dim RegEx As Object: Set RegEx = CreateObject("VBScript.RegExp")
Dim Matches As Object
Dim Match As Variant
Dim Output() As String
Dim OutputUBound As Integer
Dim Counter As Long
With RegEx
.Global = True
.MultiLine = True
.IgnoreCase = True
.Pattern = RegExPattern
End With
If RegEx.test(RawData) Then
Set Matches = RegEx.Execute(RawData)
For Each Match In Matches
OutputUBound = OutputUBound + 1
Next Match
ReDim Output(OutputUBound - 1) As String
For Each Match In Matches
Output(Counter) = Matches(Counter)
Counter = Counter + 1
Next Match
RegExer = Output
Else
ReDim Output(0) As String
RegExer = Output
End If
End Function
You can use data from web, with this query
https://www.google.com/search?q=oscars+2018+date&oq=oscars+2018
then check the whole page and import. it for me it was in row 27.

Split string into possible combinations AA-200A/B/C or ranges AA-100 to 105

I have strings (they are actually part numbers) in text files that have not been entered correctly (in full). I need to split and then concatenate them to represent the full part number.
For example:
String ZVN-798-100A/B/C should have been entered as:
ZVN-798-100A
ZVN-798-100B
ZVN-798-100C
String XPD-279-100 to 103 should have been entered as:
XPD-279-100
XPD-279-101
XPD-279-102
XPD-279-103
My code splits these correctly:
AA-10-100A/B/C
BB-20-100A to C
DD-40-100 / 110 / 120
EE-50-100A~H
But not these:
CC-30-100 thru 105
FF-60-110 to 15
For simplicity of posting to SO I have created a single sub of my code:
Private Sub btnRun_Click(sender As System.Object, e As System.EventArgs) Handles btnRun.Click
Dim arrSplitEach(2) As String
arrSplitEach(0) = "\"
arrSplitEach(1) = "/"
arrSplitEach(2) = ","
Dim arrSplitAll(2) As String
arrSplitAll(0) = " to "
arrSplitAll(1) = " thru "
arrSplitAll(2) = "~"
Dim strFromFile(5) As String
strFromFile(0) = "AA-10-100A/B/C"
strFromFile(1) = "BB-20-100A to C"
strFromFile(2) = "CC-30-100 thru 15"
strFromFile(3) = "DD-40-100 / 110 / 120"
strFromFile(4) = "EE-50-100A~H"
strFromFile(5) = "FF-60-100 to 115"
Dim arrOutput As New ArrayList
Dim iSplitEach As Integer
Dim iSplitAll As Integer
Dim strSplitter As String
rtbOutput.Clear()
rtbOutput.Update()
For iString As Integer = LBound(strFromFile) To UBound(strFromFile)
Dim s As String = strFromFile(iString).ToString.Trim
If s <> "" Then
For iSplitEach = LBound(arrSplitEach) To UBound(arrSplitEach)
strSplitter = arrSplitEach(iSplitEach).ToString
If s.Contains(strSplitter) Then
Dim parts As Array = Replace(s, " ", "").Split(strSplitter)
Dim derived As New List(Of String)
derived.Add(parts(0))
Dim intLoopParts As Integer
For intLoopParts = 1 To parts.Length - 1
If Not Len(parts(intLoopParts)) = 0 And Not parts(0).Length < Len(parts(intLoopParts)) Then
derived.Add(parts(0).Remove(parts(0).Length - Len(parts(intLoopParts))) & parts(intLoopParts))
End If
Next
For Each strPart As String In derived
'If strNotVerifiedSplit.Contains(strPart.ToLower.Trim) = False Then
If Not arrOutput.Contains(strPart.Trim) Then
arrOutput.Add(Replace(strPart.Trim, " ", ""))
strFromFile(iString).Equals(strFromFile(iString) & " | Split")
End If
Next
derived.Clear()
End If
Next iSplitEach
For iSplitAll = LBound(arrSplitAll) To UBound(arrSplitAll)
strSplitter = arrSplitAll(iSplitAll).ToString
If s.Contains(strSplitter) Then
Dim strMain As String = Replace(Strings.Left(s, InStr(s, strSplitter) - 1), " ", "")
Dim strStart As String = Mid(s, InStr(s, strSplitter) - 1, 1)
Dim strEnd As String = Strings.Right(s, 1)
Dim strToPlace As String
For Each c As Char In "ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray()
strToPlace = Strings.Left(strMain, Len(strMain) - 1) & c
If Not strToPlace = "" Then
If Not arrOutput.Contains(strToPlace.Trim) Then
arrOutput.Add(Replace(strToPlace, " ", ""))
strFromFile(iString).Equals(strFromFile(iString) & " | Split")
End If
End If
If c = strEnd Then
Exit For
End If
Next c
End If
Next iSplitAll
End If
s = ""
Next iString
For iOutput As Integer = 0 To arrOutput.Count - 1
rtbOutput.SelectionStart = rtbOutput.TextLength
rtbOutput.SelectionLength = 0
If Not arrOutput(iOutput) = "" Then
rtbOutput.AppendText(arrOutput(iOutput).Trim & vbCrLf)
End If
Next
End Sub
I have found many articles about splitting strings, but do not see a duplicate to this specific case.
It seems like overkill to have to add another chunk of code just to deal with the number ranges and I hope someone can offer some wise advice to improve my existing code.
I would do it like this and avoid the VB6 code style:
Private fList() As String = {"\", "/", ","}
Private fRange() As String = {" to ", " thru ", "~"}
Private Const Letters As String = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
Private Function SplitParts(Part As String) As IEnumerable(Of String)
Dim S, Vals() As String
For Each S In fList
Vals = Split(Part, S)
If Vals.Length > 1 Then Return FixList(Vals)
Next
For Each S In fRange
Vals = Split(Part, S)
If Vals.Length > 1 Then Return FixRange(Vals)
Next
Return {Part}
End Function
Private Function FixList(Vals() As String) As List(Of String)
Dim Ret As New List(Of String), First, Suffix As String
First = Vals.First.Trim
Ret.Add(First)
For i As Integer = 1 To Vals.Length - 1
Suffix = Vals(i).Trim
Ret.Add(First.Substring(0, First.Length - Suffix.Length) & Suffix)
Next
Return Ret
End Function
Private Function FixRange(Vals() As String) As IEnumerable(Of String)
Dim Range As New List(Of String), First, Last, Format As String, i, iMin, iMax As Integer
First = Vals.First.Trim : Last = Vals.Last.Trim
If Integer.TryParse(Last, iMax) AndAlso Integer.TryParse(First.Substring(First.Length - Last.Length), iMin) Then
Format = New String("0"c, Last.Length)
For i = iMin To iMax
Range.Add(i.ToString(Format))
Next
ElseIf Last.Length = 1 Then
iMin = Letters.IndexOf(First.Last) : iMax = Letters.IndexOf(Last)
If iMin >= 0 AndAlso iMax >= 0 Then
For i = iMin To iMax
Range.Add(Letters(i))
Next
End If
End If
First = First.Substring(0, First.Length - Vals.Last.Trim.Length) 'Prefix
Return Range.Select(Function(X) First & X)
End Function

Click all checkboxes on website

I have (in Excel, VBA) a sub that will navigate to this webpage:
http://www.nordea.dk/wemapp/currency/dk/valutaKurser
And copy the quoted rates. However, I would like to remove all the rates, and only add the ones my sub needs.
To do this, I need to check all of the boxes (to the left in the table), but the number of boxes is not constant - so I can't hardcode the boxnames.
I suppose one way to this is to extract the entire html, determine the number of rows, and then loop. But it seems very unhandy. Surely there is some smatter way, which requires less code and less storage?
You can Split() the table by vbNewLine and search for the currency in each row of the set (mind the headers).
Since the inputs are named like table:body:rows:2:cells:1:cell:check you can match the currency with the checkbox.
In practice it looks like (up to getting the element names):
Function FirstRow(myArray() As String, Optional curr As String) As Long
If curr = "" Then
curr = "*[A-Z][A-Z][A-Z]/[A-Z][A-Z][A-Z]*"
Else
curr = "*" & curr & "*"
End If
For i = LBound(myArray) To UBound(myArray)
If myArray(i) Like curr Then 'FX denoted as "XXX/XXX"
FirstRow = i
Exit Function
End If
Next i
End Function
Sub ert()
Dim myArray() As String, URLStr As String
URLStr = "http://www.nordea.dk/wemapp/currency/dk/valutaKurser"
Set ie = CreateObject("InternetExplorer.Application")
ie.Visible = False
ie.Navigate URLStr
Do Until (ie.readyState = 4 And Not ie.Busy)
DoEvents
Loop
On Error GoTo Err:
Data = ie.Document.body.innerHTML 'innerText
If False Then
Err:
Data = ie.Document.body.innerHTML
End If
myArray = Split(Data, "<tr>") 'vbNewLine) 'vbCrLf)
'For i = LBound(myArray) To UBound(myArray)
' Cells(i + 1, 1).Value2 = myArray(i)
'Next
Dim curr() As String
ReDim curr(2)
curr(0) = "DKK/NOK"
curr(1) = "EUR/SEK"
curr(2) = "USD/CHF"
For i = LBound(curr) To UBound(curr)
x = FirstRow(myArray, curr(i)) - FirstRow(myArray) + 1
MsgBox "table:body:rows:" & x & ":cells:1:cell:check"
Next i
ie.Quit
Set ie = Nothing
End Sub
I'm sorry, the vbNewLine won't cut it this time, my bad. Also, checking a named element shouldn't be that hard, had you provided your snipplet for checking all the boxes I would have even given it a shot.

Accessing SurveyMonkey API from VBA

I am tying to set-up a Excel VBA project to readout individual survey responses into a form in Excel for some calculations and then PDF reporting.
However I have great difficulty to deploy the .NET library (SurveyMonkeyApi) to be available for reference in VBA.
I have set up a VisualStudio project to test that way , and I can install it for that specific VS project (through NuGet PM). But the library is not made available for Excel on that machine.
I have downloaded (on another machine) the libraries through standalone NuGet and they download OK but then I am at loss on how to register for Excel VBA access. On top of it there is a dependency on NewtonsoftJson library too (which downloaded automatically on both occasions).
Good advice appreciated!
I just saw this now - is there a feature for StackOverflow to alert me when a comment is added or a question answered, so I know to look back?
Here is starting code:
Option Explicit
Public Const gACCESS_TOKEN As String = "xxxxxxxxxxxxxxxxxxxxxx"
Declare Function GetTickCount Lib "kernel32" () As Long
Declare Sub Sleep Lib "kernel32" (ByVal lngMilliSeconds As Long)
' for a JSON parser see https://code.google.com/p/vba-json/
Public Sub test()
Dim vRequestBody As Variant, sResponse As String, sSurveyID As String
sSurveyID = "1234567890"
vRequestBody = "{""survey_id"":" & """" & sSurveyID & """" _
& ", ""fields"":[""collector_id"", ""url"", ""open"", ""type"", ""name"", ""date_created"", ""date_modified""]" _
& "}"
sResponse = SMAPIRequest("get_collector_list", vRequestBody)
End Sub
Function SMAPIRequest(sRequest As String, vRequestBody As Variant) As String
Const SM_API_URI As String = "https://api.surveymonkey.net/v2/surveys/"
Const SM_API_KEY As String = "yyyyyyyyyyyyyyyyyyyyyyyy"
Dim bDone As Boolean, sMsg As String, sUrl As String, oHttp As Object ' object MSXML2.XMLHTTP
Static lsTickCount As Long
If Len(gACCESS_TOKEN) = 0 Then
Err.Raise 9999, "No Access token"
End If
On Error GoTo OnError
sUrl = SM_API_URI & URLEncode(sRequest) & "?api_key=" & SM_API_KEY
'Debug.Print Now() & " " & sUrl
Application.StatusBar = Now() & " " & sRequest & " " & Left$(vRequestBody, 127)
Set oHttp = CreateObject("MSXML2.XMLHTTP") ' or "MSXML2.ServerXMLHTTP"
Do While Not bDone ' 4.33 offer retry
If GetTickCount() - lsTickCount < 1000 Then ' if less than 1 sec since last call, throttle to avoid sResponse = "<h1>Developer Over Qps</h1>"
Sleep 1000 ' wait 1 second so we don't exceed limit of 2 qps (queries per second)
End If
lsTickCount = GetTickCount()
'Status Retrieves the HTTP status code of the request.
'statusText Retrieves the friendly HTTP status of the request.
'Note The timeout property has a default value of 0.
'If the time-out period expires, the responseText property will be null.
'You should set a time-out value that is slightly longer than the expected response time of the request.
'The timeout property may be set only in the time interval between a call to the open method and the first call to the send method.
RetryPost: ' need to do all these to retry, can't just retry .Send apparently
oHttp.Open "POST", sUrl, False ' False=not async
oHttp.setRequestHeader "Authorization", "bearer " & gACCESS_TOKEN
oHttp.setRequestHeader "Content-Type", "application/json"
oHttp.send CVar(vRequestBody) ' request body needs brackets EVEN around Variant type
'-2146697211 The system cannot locate the resource specified. => no Internet connection
'-2147024809 The parameter is incorrect.
'String would return {"status": 3, "errmsg": "No oJson object could be decoded: line 1 column 0 (char 0)"} ??
'A Workaround would be to use parentheses oHttp.send (str)
'"GET" err -2147024891 Access is denied.
'"POST" Unspecified error = needs URLEncode body? it works with it but
SMAPIRequest = oHttp.ResponseText
'Debug.Print Now() & " " & Len(SMAPIRequest) & " bytes returned"
sMsg = Len(SMAPIRequest) & " bytes returned in " & (GetTickCount() - lsTickCount) / 1000 & " seconds: " & sRequest & " " & Left$(vRequestBody, 127)
If Len(SMAPIRequest) = 0 Then
bDone = MsgBox("No data returned - do you wish to retry?" _
& vbLf & sMsg, vbYesNo, "Retry?") = vbNo
Else
bDone = True ' got reply.
End If
Loop ' Until bdone
Set oHttp = Nothing
GoTo ExitProc
OnError: ' Pass True to ask the user what to do, False to raise to caller
Select Case MsgBox(Err.Description, vbYesNoCancel, "SMAPIRequest")
Case vbYes
Resume RetryPost
Case vbRetry
Resume RetryPost
Case vbNo, vbIgnore
Resume Next
Case vbAbort
End
Case Else
Resume ExitProc ' vbCancel
End Select
ExitProc:
End Function
Public Function URLEncode(StringVal As String, Optional SpaceAsPlus As Boolean = False) As String
Dim StringLen As Long
StringLen = Len(StringVal)
If StringLen > 0 Then
ReDim result(StringLen) As String
Dim i As Long, CharCode As Integer
Dim Char As String, Space As String
If SpaceAsPlus Then Space = "+" Else Space = "%20"
For i = 1 To StringLen
Char = Mid$(StringVal, i, 1)
CharCode = Asc(Char)
Select Case CharCode
Case 97 To 122, 65 To 90, 48 To 57, 45, 46, 95, 126
result(i) = Char
Case 32
result(i) = Space
Case 0 To 15
result(i) = "%0" & Hex(CharCode)
Case Else
result(i) = "%" & Hex(CharCode)
End Select
Next i
URLEncode = Join(result, "")
End If
End Function
EDIT 23-APRIL add more code.
the Me. comes from code in a Userform.
Set jLib = New JSONLib
vRequestBody = "{"
If Me.txtDaysCreated > "" Then
vRequestBody = vRequestBody & JKeyValue("start_date", Format$(Now() - CDbl(Me.txtDaysCreated), "yyyy-mm-dd")) & ","
End If
If Me.txtTitleContains > "" Then
' title contains "text", case insensitive
vRequestBody = vRequestBody & JKeyValue("title", Me.txtTitleContains) & ","
End If
vRequestBody = vRequestBody _
& JKeyValue("fields", Array("title", "date_created", "date_modified", "num_responses", _
"language_id", "question_count", "preview_url", "analysis_url")) & "}"
'returns in this order: 0=date_modified 1=title 2=num_responses 3=date_created 4=survey_id
' and in date_created descending
sResponse = GetSMAPIResponse("get_survey_list", vRequestBody)
------------------------------------------
Function JKeyValue(sKey As String, vValues As Variant) As String
Dim jLib As New JSONLib
JKeyValue = jLib.toString(sKey) & ":" & jLib.toString(vValues)
Set jLib = Nothing
End Function
Edit 25-April overview of VBA code to get the data
This is covered in the SM documentation, but I'll sketch how that looks in VBA.
the response to get_survey_details gives you all the survey setup data. Use
Set oJson = jLib.parse(Replace(sResponse, "\r\n", " "))
to get a json object.
Set dictSurvey = oJson("data")
gives you the dictionary so you can get data like dictSurvey("num_responses"). I take it you know how to index into dictionary objects to get field values.
Set collPages = dictSurvey("pages")
gives you a collection of Pages. The undocumented field "position" gives you the order of pages in the survey UI.
For lPage = 1 To collPages.Count
Set dictPage = collPages(lPage)
Set collPageQuestions = dictPage("questions") ' gets you the Qs on this page
For lPageQuestion = 1 To collPageQuestions.Count
Set dictQuestion = collPageQuestions(lPageQuestion) ' gets you one Q
Set collAnswers = dictQuestion("answers") ' gets the QuestionOptions for this Q
For lAnswer = 1 To collAnswers.Count
Set dictAnswer = collAnswers(lAnswer) ' gets you one Question Option
etc etc
Then given the number of responses from above, loop through the respondents 100 at a time - again see the SM doc for details of how to specify start and end dates to do incremental downloads over time.
create a json object from the response to "get_respondent_list"
Collect the fields for each respondent and accumulate a list of at most 100 respondent IDs.
Then "get_responses" for that list.
Set collResponsesData = oJson("data")
For lResponse = 1 To collResponsesData.Count
If not IsNull(collResponsesData(lResponse)) then
... get fields...
Set collQuestionsAnswered = collResponsesData(lResponse)("questions")
For lQuestion = 1 To collQuestionsAnswered.Count
Set dictQuestion = collQuestionsAnswered(lQuestion)
nQuestion_ID = CDbl(dictQuestion("question_id"))
Set collAnswers = dictQuestion("answers") ' this is a collection of dictionaries
For lAnswer = 1 To collAnswers.Count
On Error Resume Next ' only some of these may be present
nRow = 0: nRow = CDbl(collAnswers(lAnswer)("row"))
nCol = 0: nCol = CDbl(collAnswers(lAnswer)("col"))
nCol_choice = 0: nCol_choice = CDbl(collAnswers(lAnswer)("col_choice"))
sText = "": sText = collAnswers(lAnswer)("text")
nValue = 0: nValue = Val(sText)
On Error GoTo 0
and save all those values in a recordset or sheet or whatever
Hope that helps.
I access the SM API in straight VBA.
Just CreateObject("MSXML2.XMLHTTP") then issue calls and use the SimpleJsON JSONLib to parse it.
If I wanted to access VB.Net code, I'd package it with ExcelDNA to create a XLL and that gives a straight Excel addin.
I would think you would need to add it into the References for your Excel project.
From the Ribbon, select, Tools, then References, then scroll through the list looking for something about SurveyMonkey API.
So encouraged by #sysmod I have tried to do something in VBA directly. I have left out the JSON for now as I am already in trouble. The below is giving me "Developer Inactive" as a result, though I have another project in VB.NET where the same key and token works fine.
Public Sub GetSMList()
Dim apiKey As String
Dim Token As String
Dim sm As Object
apiKey = "myKey"
Token = "myToken"
Set sm = CreateObject("MSXML2.XMLHTTP.6.0")
With sm
.Open "POST", "https://api.surveymonkey.net/v2/surveys/get_survey_list", False
.setRequestHeader "Authorization", "Bearer " & Token
.setRequestHeader "Content-Type", "application/json"
.send "api_key=" & apiKey
result = .responseText
End With
End Sub

Resources