Grouping Values by Unique Index in a String Array - arrays

How can I retrieve only unique array of this example.
"58|270,58|271,58|272,59|270,59|271,59|272"
I want this array to be stored like :
"58,270,271,272|59,270,271,272"
Can someone help me in ASP classic or VB script

This isn't a straight forward problem I found myself thinking about it for a few minutes before I finally thought of a way of doing it.
To produce the output from the input specified requires some sort of custom de-serialise / serialise approach. The code below creates a 2D array that will contain the unique indexes (58, 59 etc.) and populate them with a comma delimited list of the associated values (done it like this to make the serialise easy).
Structure wise it will look something like this when de-serialised
----- Array Debug ------
data(0, 0) = 58
data(1, 0) = 270,271,272
data(0, 1) = 59
data(1, 1) = 270,271,272
We then use that as the basis to build the serialised string in the format required.
'Function takes string input in the form <index>|<value>, ... extracts
'them into a 2D array groups duplicate indexes together.
Function DeserialiseToCustomArray(str)
Dim a1, a2, x, y, idx
If Len(str & "") > 0 Then
a1 = Split(str, ",")
ReDim data(1, 0)
For x = 0 To UBound(a1)
a2 = Split(a1(x), "|")
If IsArray(data) Then
idx = -1
'Check for duplicates
For y = 0 To UBound(data, 2)
If data(0, y) = a2(0) Or IsEmpty(data(0, y)) Then
idx = y
Exit For
End If
Next
'No duplicate found need to add a new element to the array.
If idx = -1 Then
idx = UBound(data, 2) + 1
ReDim Preserve data(1, idx)
End If
data(0, idx) = a2(0)
If IsEmpty(data(1, idx)) Then
data(1, idx) = a2(1)
Else
data(1, idx) = Join(Array(data(1, idx), a2(1)), ",")
End If
End If
Next
End If
DeserialiseToCustomArray = data
End Function
'Function takes a 2D array built from DeserialiseToCustomArray() and
'serialises it into a custom string in the form <index>,<value>, ... | ...
Function SerialiseArray(data)
Dim x, y
Dim str: str = Empty
If IsArray(data) Then
For y = 0 To UBound(data, 2)
If y > 0 And y <= UBound(data, 2) Then str = str & "|"
str = str & data(0, y) & "," & data(1, y)
Next
End If
SerialiseArray = str
End Function
Couple examples of usage:
Dim str: str = "58|270,58|271,58|272,59|270,59|271,59|272"
Dim data, result
data = DeserialiseToCustomArray(str)
result = SerialiseArray(data)
WScript.Echo "input: " & str
WScript.Echo "output: " & result
Output:
Result: 58,270,271,272|59,270,271,272
Dim str: str = "58|270,58|271,58|272,59|270,59|271,59|272,60|345,61|345,58|270,60|200"
Dim data, result
data = DeserialiseToCustomArray(str)
result = SerialiseArray(data)
WScript.Echo "input: " & str
WScript.Echo "output: " & result
Output:
Result: 58,270,271,272,270|59,270,271,272|60,345,200|61,345
Note: If using these examples in Classic ASP remove the WScript.Echo and replace with Response.Write.

A common way to get unique items from an array is to put them as keys into a Dictionary:
a = Array(58, 270, 271, 272, 270, 271, 272)
Set d = CreateObject("Scripting.Dictionary")
For Each i In a
d(i) = True 'value can be anything, relevant is using i as key
Next
WScript.Echo Join(d.Keys, ",") 'Output: 58,270,271,272

Related

Array.Exists for boolean comparison in integer array (converted from string)

I'm trying to find if certain values in a given string of numbers are less than a numeric value.
I'm trying to create a new integer array and iterate through the string array to fill it. Then I'm trying to use Array.Exists(arr, predicate), but I don't think I'm using it correctly.
I wasn't able to simply do Array.Exists(larr, element => element <=16) because my compiler required I have parentheses like Array().Exists(larr, element...) or Array(larr).Exists(lar, element...).
What would you recommend changing in my code so that I can transform a string into an array of numbers or some other data structure that will allow me to check if any value within it is <=some value?
Dim orgnameB As Range
Dim orgList As Range
Set orgList = Worksheets("Org List").Range("B3:B10")
sites = Worksheets("Org List").Cells(orgnameB.row, "E")
max_num = Evaluate("Max(" & sites & ")")
sarr = Split(sites, ", ")
Dim ArrayLen As Integer
ArrayLen = UBound(sarr) - LBound(sarr) + 1 'this was causing issues, unsure how to do this
Dim larr(ArrayLen) As Integer
Set result = Worksheets("Org List").Cells(orgnameB.row, "O")
'Loop through str array to convert to int array
For i = LBound(sarr) To UBound(sarr)
larr(i) = CInt(sarr(i))
Next i
For Each orgnameB In orgList
If IsEmpty(orgnameB) = True Then
Exit For
End If
If max_num >= 1 And max_num <= 15 Then
result.Value = "YES"
ElseIf max_num >= 16 And max_num <= 50 Then
If Array(larr).Exists(larr, element => element <= 16) = True Then
result.Value = "YES"
End If
ElseIf max_num >= 51 And max_num <= 250 Then
If Array(larr).Exists(larr, element => element <= 51) = True Then
result.Value = "YES"
End If
ElseIf max_num >= 251 And max_num <= 1000 Then
If Array(larr).Exists(larr, element => element <= 250) = True Then
result.Value = "YES"
End If
ElseIf max_num >= 1001 Then
If Array(larr).Exists(larr, element => element <= 1000) = True Then
result.Value = "YES"
End If
End If
Next
End Sub
FilterXML() as VBA simulation of Array.Exists():
Of course Excel VBA cannot apply specific .Net or C syntax.
Therefore the following example demonstrates how to rebuild a similar functionality (returning True for existant array findings) via the FilterXML() function which allows to specify multiple conditions.
It offers the extra benefit, however that it doesn't return only a boolean value, but that it returns all found (numeric) elements as 0-based 1-dim output array
which can be analyzed and/or reused easily eventually.
Example call
Dim InArray() As Variant, limits() As Variant, OutArray() As Variant
InArray = Array(10, 11, 12, 13, 14, 15)
limits = Array(9) ' find elements <= 9
Debug.Print "Array exists: " & ArrExists(InArray, limits, OutArray), UBound(OutArray) + 1 & " Element(s) ~> " & Join(OutArray, ",")
' Array exists: False 0 Element(s) ~>
limits = Array(13) ' find elements <= 13
Debug.Print "Array exists: " & ArrExists(InArray, limits, OutArray), UBound(OutArray) + 1 & " Element(s) ~> " & Join(OutArray, ",")
' Array exists: True 4 Element(s) ~> 10,11,12,13
limits = Array(11, 13) ' find elements >=11 and <= 13
Debug.Print "Array exists: " & ArrExists(InArray, limits, OutArray), UBound(OutArray) + 1 & " Element(s) ~> " & Join(OutArray, ",")
' Array exists: True 3 Element(s) ~> 11,12,13
End Sub
Help Function `ArrExists()
The following functions profits from filtering a well formed XML content by
a so called XPath expression, which is available since vers. 2013+.
Note that findings are automatically assigned to a 2-dim array, only non-findings result in an error value.
In order to return results again to a "flat" 1-dim output array, they need to be transposed.
Function ArrExists(InArray, limits, Optional OutArray) As Boolean
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'1. define FILTERXML arguments
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
' a) build Xml content
Dim XmlContent As String
XmlContent = "<items><i>" & Join(InArray, "</i><i>") & "</i></items>"
' b) build XPath expression
Dim XPathExpression As String
If UBound(limits) Then
If limits(1) > limits(0) Then
XPathExpression = "//i[.>=" & limits(0) & "][.<=" & limits(1) & "]"
Else
XPathExpression = "//i[.>=" & limits(1) & "][.<=" & limits(0) & "]"
End If
Else
XPathExpression = "//i[.<=" & limits(0) & "]"
End If
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'2. apply FILTERXML() function
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'a) get results as 2-dim array
Dim results: results = Application.FilterXML(XmlContent, XPathExpression)
'b) check for no result or one result only
If Not IsArray(results) Then
If IsError(results) Then
ReDim results(0 To 0, 1 To 1)
Else
Dim tmp As Variant: tmp = results
ReDim results(1 To 1, 1 To 1)
results(1, 1) = tmp
End If
End If
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'3.Function results
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'a)return existence as boolean value
If LBound(results) Then ArrExists = True
'b)optional: overwrite referenced array
If Not IsMissing(OutArray) Then
If ArrExists Then
OutArray = Application.Transpose(results) ' results now 1-based
ReDim Preserve OutArray(0 To UBound(OutArray) - 1) ' redim as zero-based
Else
ReDim OutArray(-1 To -1)
End If
End If
End Function

MS Access VBA loop stops without error or apparent cause

I'm trying to compare two arrays of data in MS Access - one is generated from an API GET, and the other is generated from two columns of a table. I'm using a double loop to do the comparison, I suspect this isn't the best way but I'm still learning my way around loops and arrays. The code I'm using is as follows:
Sub ParseList(ResCount As Long)
Dim db As DAO.Database
Dim rstConts As DAO.Recordset
Dim midstr As String, emailstr As String, Fname As String, Lname As String, SubStatus As String, echeck As String, Mecheck As String, ArrEcheck As String, ArrMecheck As String, MSub As String
Dim ArrResp() As String
Dim ArrConts() As Variant
Dim SubStart As Long, SubCount As Long, Fstart As Long, Fcount As Long, Lstart As Long, LCount As Long, Diffcount As Long, c As Long, i As Long, t As Long, y As Long, u As Long, v As Long
Dim IsSub As Boolean
Set db = CurrentDb
Udate = SQLDate(Now)
ReDim ArrResp(1 To ResCount, 1 To 4) As String
'This section parses a JSON response into an array
For i = 1 To ResCount
midstr = ""
emailstr = ""
x = InStr(t + 2, GetListStr, "}}") + 21
y = InStr(x + 1, GetListStr, "}}")
If y = 0 Then
Exit Sub
End If
midstr = Mid(GetListStr, x, y - x)
emailstr = Left(midstr, InStr(midstr, ",") - 2)
SubStart = InStr(midstr, "Status") + 9
SubCount = InStr(InStr(midstr, "Status") + 8, midstr, ",") - SubStart - 1
SubStatus = Replace(Mid(midstr, SubStart, SubCount), "'", "''")
Fstart = InStr(midstr, ":{") + 11
Fcount = InStr(InStr(midstr, ":{") + 11, midstr, ",") - (Fstart + 1)
Fname = Replace(Mid(midstr, Fstart, Fcount), "'", "''")
Lstart = InStr(midstr, "LNAME") + 8
LCount = InStr(InStr(midstr, "LNAME") + 8, midstr, ",") - (Lstart + 1)
Lname = Replace(Mid(midstr, Lstart, LCount), "'", "''")
If SubStatus = "subscribed" Then
MSub = "True"
Else
MSub = "False"
End If
ArrResp(i, 1) = emailstr
ArrResp(i, 2) = MSub
ArrResp(i, 3) = Fname
ArrResp(i, 4) = Lname
t = y
Next i
'This section grabs two columns from a database table and adds them to a second array
Set rstConts = CurrentDb.OpenRecordset("SELECT Primary_Email, EMailings FROM TBLContacts")
rstConts.MoveLast
rstConts.MoveFirst
c = rstConts.RecordCount
ReDim ArrConts(1 To c) As Variant
ArrConts = rstConts.GetRows(c)
'This loops through the JSON response array, and when it finds a matching value in the Table array it checks if a second value in the table array matches or not
For u = 1 To ResCount
Debug.Print u
echeck = ArrResp(u, 1)
Mecheck = ArrResp(u, 2)
For v = 0 To c
If ArrConts(0, v) = "" Then
Else
ArrEcheck = ArrConts(0, v)
ArrMecheck = ArrConts(1, v)
If ArrEcheck = echeck Then
If ArrMecheck = Mecheck Then
Debug.Print echeck & "Match"
Else
Debug.Print echeck & "No Match"
End If
End If
End If
Next v
Next u
MsgBox "Done"
End Sub
The code above simply doesn't complete and the msgbox is never shown. The debug.print line near the end only goes to 1, and I can't figure out why. If I remove the conditions from the second loop section:
If ArrConts(0, v) = "" Then
Else
ArrEcheck = ArrConts(0, v)
ArrMecheck = ArrConts(1, v)
If ArrEcheck = echeck Then
If ArrMecheck = Mecheck Then
Debug.Print echeck & "Match"
Else
Debug.Print echeck & "No Match"
End If
End If
End If
Then I can successfully complete the Main loop, and receive the 'Done' message. But I've been unable to narrow down why the second loop isn't completing properly, and I'm stuck.
Because arrays are zero-indexed, you need to subtract 1 from the upper limit of nested For loop which should have thrown an error on the subsequent If line when loop exceeded the record limit.
For u = 1 To ResCount
Debug.Print u
echeck = ArrResp(u, 1)
Mecheck = ArrResp(u, 2)
For v = 0 To c - 1 ' REDUCE UPPER LIMIT BY 1
If ArrConts(0, v) = "" Then ' LINE NO LONGER SHOULD ERR OUT
...
Next v
Next u
With that said, consider parsing JSON to an MS Access table using the VBA-JSON library. Then use SQL to check values with JOIN and WHERE in set-based processing between table to table. This is much more efficient that looping between arrays.

Parse Multiple Arrays and write all possible combinations

I am trying to use VBA in Excel to write all of the possible combinations of the contents of three arrays to a column to create a wordlist.
Currently, I know how to loop through the arrays and get some output that I want but I can't figure out how to build the loop to give me all possible combinations of the baseWord(n) & numberCharSet(n) & specialCharSet(n).
How do I properly loop through the baseWord array to get all combinations of each baseWord with the contents of the specialCharSet and numberCharSet arrays?
Example:
Cloud1!
Cloud1#
Cloud1#
Cloud1$
Cloud2!
etc...
Private Sub createWordlist()
Dim baseWord(1 To 2) As String
baseWord(1) = "Cloud"
baseWord(2) = "cloud"
Dim numberCharSet(1 To 4) As String
numberCharSet(1) = "1"
numberCharSet(2) = "2"
numberCharSet(3) = "3"
numberCharSet(4) = "4"
Dim specialCharSet(1 To 4) As String
specialCharSet(1) = "!"
specialCharSet(2) = "#"
specialCharSet(3) = "#"
specialCharSet(4) = "$"
x = 1
y = 1
z = 4
w = 1
For Each Item In baseWord
Range("A" & x).Value = baseWord(w) & numberCharSet(y) & specialCharSet(z)
x = x + 1
y = y + 1
z = z - 1
Next
End Sub
As #ScottCraner mentioned in the comments, all you need to do is nest 3 loops:
For Each word In baseWord
For Each num In numberCharSet
For Each special In specialCharSet
Debug.Print word & num & special
Next
Next
Next

Assigning multiple records generated via string parsing to a VBA array

A line in my text file is in the following format:
length of field : value
Example:
"006:170000,004:0002,009:000358827,009:003628325,005:71001,002:71,004:0000,000:,005:95.16,010:1000000.00" ... (length can be much longer)
*The value can also have a comma
I am currently exporting this data in such a way that inside a loop I retrieve the first value and after that I assign this value to a predefined temporary array. This process (string parsing) is repeated until I reach the end of the string. But I find this is slow for large files. What would be a faster way to assign the values I get via parsing of the string to my array?
You have an empty value in your example string 000:,.
You can split the values by the colon and then trim the last 4 characters off of each element in the array. I emptied the first element in the array because it contains a key not a value.
Sub ArrayUsingSplit()
Const StringValues = "006:170000,004:0002,009:000358827,009:003628325,005:71001,002:71,004:0000,000:,005:95.16,010:1000000.00"
Dim arrValues
Dim x As Long
arrValues = Split(StringValues, ":")
arrValues(0) = ""
For x = 1 To UBound(arrValues)
arrValues(x) = Left(arrValues(x), Len(arrValues(x)) - 4)
Next
Debug.Print Join(arrValues, "~")
End Sub
Is this what you are doing?
Sub ArrayUsingMid()
Const StringValues = "006:170000,004:0002,009:000358827,009:003628325,005:71001,002:71,004:0000,000:,005:95.16,010:1000000.00"
Dim i As Long, iStart As Integer, iEnd As Integer, iLength
Dim arrValues()
iEnd = 1
Do
ReDim Preserve arrValues(i)
iStart = InStr(iEnd, StringValues, ":") + 1
iEnd = InStr(iStart, StringValues, ":")
iLength = iEnd - iStart - 4
If iEnd > 0 Then
arrValues(i) = Mid(StringValues, iStart, iLength)
Else
arrValues(i) = Mid(StringValues, iStart)
End If
i = i + 1
Loop Until iEnd = 0
Debug.Print Join(arrValues, "~")
End Sub
It can be a bit shorter with Evaluate. For example a = [{001,0;005,01.23}] will generate 2 dimensional array with the values ( 1, 0 ) , ( 5 , 1.23 ). The code can be something like
s = "006:170000,004:0002,009:000358827,009:003628325,005:71001,002:71,004:0000,000:"
s = Replace(s, ",", ";")
s = Replace(s, ":", ",0") ' ",0 because Evaluate doesn't allow blank values like "{1,}"
a = Evaluate("{" & s & "}") ' ( 6, 170000 ), ( 4, 2 ), ( 9, 358827 ), ...
If you want to keep the leading zeros, you can keep them as strings:
s = "006:170000,004:0002,009:000358827,009:003628325,005:71001,002:71,004:0000,000:"
s = Replace(s, ",", """;""")
s = Replace(s, ":", """,""")
s = "{""" & s & """}"
a = Evaluate(s) ' ( "006", "170000" ), ( "004", "0002" ), ...

Add item to array in VBScript

How do you add an item to an existing array in VBScript?
Is there a VBScript equivalent to the push function in Javascript?
i.e.
myArray has three items, "Apples", "Oranges", and "Bananas" and I want to add "Watermelons" to the end of the array.
Arrays are not very dynamic in VBScript. You'll have to use the ReDim Preserve statement to grow the existing array so it can accommodate an extra item:
ReDim Preserve yourArray(UBound(yourArray) + 1)
yourArray(UBound(yourArray)) = "Watermelons"
For your copy and paste ease
' add item to array
Function AddItem(arr, val)
ReDim Preserve arr(UBound(arr) + 1)
arr(UBound(arr)) = val
AddItem = arr
End Function
Used like so
a = Array()
a = AddItem(a, 5)
a = AddItem(a, "foo")
There are a few ways, not including a custom COM or ActiveX object
ReDim Preserve
Dictionary object, which can have string keys and search for them
ArrayList .Net Framework Class, which has many methods including:
sort (forward, reverse, custom), insert, remove,
binarysearch, equals, toArray, and toString
With the code below, I found Redim Preserve is fastest below 54000, Dictionary is fastest from 54000 to 690000, and Array List is fastest above 690000. I tend to use ArrayList for pushing because of the sorting and array conversion.
user326639 provided FastArray, which is pretty much the fastest.
Dictionaries are useful for searching for the value and returning the index (i.e. field names), or for grouping and aggregation (histograms, group and add, group and concatenate strings, group and push sub-arrays). When grouping on keys, set CompareMode for case in/sensitivity, and check the "exists" property before "add"-ing.
Redim wouldn't save much time for one array, but it's useful for a dictionary of arrays.
'pushtest.vbs
imax = 10000
value = "Testvalue"
s = imax & " of """ & value & """"
t0 = timer 'ArrayList Method
Set o = CreateObject("System.Collections.ArrayList")
For i = 0 To imax
o.Add value
Next
s = s & "[AList " & FormatNumber(timer - t0, 3, -1) & "]"
Set o = Nothing
t0 = timer 'ReDim Preserve Method
a = array()
For i = 0 To imax
ReDim Preserve a(UBound(a) + 1)
a(UBound(a)) = value
Next
s = s & "[ReDim " & FormatNumber(timer - t0, 3, -1) & "]"
Set a = Nothing
t0 = timer 'Dictionary Method
Set o = CreateObject("Scripting.Dictionary")
For i = 0 To imax
o.Add i, value
Next
s = s & "[Dictionary " & FormatNumber(timer - t0, 3, -1) & "]"
Set o = Nothing
t0 = timer 'Standard array
Redim a(imax)
For i = 0 To imax
a(i) = value
Next
s = s & "[Array " & FormatNumber(timer - t0, 3, -1) & "]" & vbCRLF
Set a = Nothing
t0 = timer 'Fast array
a = array()
For i = 0 To imax
ub = UBound(a)
If i>ub Then ReDim Preserve a(Int((ub+10)*1.1))
a(i) = value
Next
ReDim Preserve a(i-1)
s = s & "[FastArr " & FormatNumber(timer - t0, 3, -1) & "]"
Set a = Nothing
MsgBox s
' 10000 of "Testvalue" [ArrayList 0.156][Redim 0.016][Dictionary 0.031][Array 0.016][FastArr 0.016]
' 54000 of "Testvalue" [ArrayList 0.734][Redim 0.672][Dictionary 0.203][Array 0.063][FastArr 0.109]
' 240000 of "Testvalue" [ArrayList 3.172][Redim 5.891][Dictionary 1.453][Array 0.203][FastArr 0.484]
' 690000 of "Testvalue" [ArrayList 9.078][Redim 44.785][Dictionary 8.750][Array 0.609][FastArr 1.406]
'1000000 of "Testvalue" [ArrayList 13.191][Redim 92.863][Dictionary 18.047][Array 0.859][FastArr 2.031]
Slight change to the FastArray from above:
'pushtest.vbs
imax = 10000000
value = "Testvalue"
s = imax & " of """ & value & """"
t0 = timer 'Fast array
a = array()
ub = UBound(a)
For i = 0 To imax
If i>ub Then
ReDim Preserve a(Int((ub+10)*1.1))
ub = UBound(a)
End If
a(i) = value
Next
ReDim Preserve a(i-1)
s = s & "[FastArr " & FormatNumber(timer - t0, 3, -1) & "]"
MsgBox s
There is no point in checking UBound(a) in every cycle of the for if we know exactly when it changes.
I've changed it so that it checks does UBound(a) just before the for starts and then only every time the ReDim is called
On my computer the old method took 7.52 seconds for an imax of 10 millions.
The new method took 5.29 seconds for an imax of also 10 millions, which signifies a performance increase of over 20% (for 10 millions tries, obviously this percentage has a direct relationship to the number of tries)
Based on Charles Clayton's answer, but slightly simplified...
' add item to array
Sub ArrayAdd(arr, val)
ReDim Preserve arr(UBound(arr) + 1)
arr(UBound(arr)) = val
End Sub
Used like so
a = Array()
AddItem(a, 5)
AddItem(a, "foo")
this some kind of late but anyway and it is also somewhat tricky
dim arrr
arr= array ("Apples", "Oranges", "Bananas")
dim temp_var
temp_var = join (arr , "||") ' some character which will not occur is regular strings
if len(temp_var) > 0 then
temp_var = temp_var&"||Watermelons"
end if
arr = split(temp_var , "||") ' here you got new elemet in array '
for each x in arr
response.write(x & "<br />")
next'
review and tell me if this can work
or initially you save all data in string and later split for array
Not an answer Or Why 'tricky' is bad:
>> a = Array(1)
>> a = Split(Join(a, "||") & "||2", "||")
>> WScript.Echo a(0) + a(1)
>>
12

Resources