I have code like this:
Dim MyACL As Variant
Dim Person As List
Redim MyACL(0)
Person("Detail1") = "Something1"
.
.
.
Person(Detailx") = "Somethingx"
ForAll name in names
ReDim Preserve MyAcl(Ubound(MyACL)+1)
Person("Name") = name
MyACL = ArrayAppend(MyACL,Person)
End ForAll
It throws error "Type Mismatch". Do you know, how to create an array of lists? Thank you.
This is a typical example of when you want to use a class instead, and create an array of that class. That class, in turn can contain a list (as well as other things). Can be very powerful!
Updated:
The benefit of using a class is that you can add business logic in the class, and it is very easy to extend it with more functionality later. Below is an example, based on the question above, but with additional functionality.
Class PersonObject
Public PersonAttribute List As String
Public NABdoc As NotesDocument
Public PersonName As String
Public Sub New(personname As String)
Dim nab as New NotesDatabase("Server/Domain","names.nsf")
Dim view as NotesView
'*** Get person document from Domino directory
Set view = nab.GetView("PeopleByFirstName")
Set me.NABdoc = view.GetDocumentByKey(personname)
'*** Set person name in object
me.PersonName = personname
'*** Set some values from person doc
me.PersonAttribute("Email") = GetValue("InternetAddress")
me.PersonAttribute("Phone") = GetValue("OfficePhone")
End Sub
Public Function GetValue(fieldname as String) as String
GetValue = me.NABdoc.GetItemValue(fieldname)(0)
End Function
Public Sub AddAttribute(attributename as String, value as string)
me.PersonAttribute(attributename) = value
End Sub
End Class
You can now very easily build you a list, using this class (and assuming that names is a list of unique names):
Dim person List As PersonObject
Dim personname As String
ForAll n in names
'*** Create person object based on name
person(n) = New PersonObject(n)
'*** Store additional info about this person
person.AddAttribute("Age","35")
End ForAll
Hopefully this gives you an idea of what you can do with classes.
You can also take a look at the following two blog entries about the basics of object oriented Lotusscript:
http://blog.texasswede.com/object-oriented-lotusscript-for-beginners-part-1/
http://blog.texasswede.com/object-oriented-lotusscript-for-beginners-part-2/
If you explicitely declare a variable as Array (as you do in your Redim Statement), then it can not be "reassigned" using arrayappend.
And it is NOT necessary to do it that way. just replace the line MyACL = ArrayAppend(MyACL,Person) with MyACL(Ubound(MyACL)) = Person
Take care: With that example code you will never fill MyACL(0) as the first Element filled is MyACL(1)
To begin filling the array with element 0 the code needs to be changed like this:
Dim max As Integer
max = 0
ForAll thisName In names
ReDim Preserve MyAcl(max)
Person("Name") = thisName
MyACL(max) = Person
max = max + 1
End ForAll
BUT: I don't know, if this is a good idea, as you can not access the "Detail1- Property" of Person directly.
Something like
detail = MyACL(1)("Detail1")
is not possible. You always have to have a temporary variable like this:
person = MyACL(1)
detail = person("Detail1")
Related
I've been wrestling with this problem for a while. My problem is that I have a bunch of JSON data and I want to represent it as objects.
Arrays are problematic.
I create a class module such as FancyCat with a public Name as String for its name.
Then I can set this with
Dim MyFancyCat as FancyCat
Set MyFancyCat = new FancyCat
FancyCat.Name = JSONData("Name")
I've typed that from memory but I think it's correct. Anyhoo, it works fine.
The problem is that a fancy cat has several pairs of socks. The number of socks is variable.
In vba you cannot for some reason have a public array. So this code is illegal:
public Socks() as FancySock 'Illegal
Looking on SO I found two solutions, one, to make it private and use a property to access it, and the other, to declare it as Variant and then stick an array into it later.
My approach to populating this array, is to examine the JSON array to get the Count, and then to ReDim the array to match and then populate it.
The problem is my ReDim statement refuses to work.
It seems I cannot redim a property, I get an error. And I also get an error trying to redim the public variant field. My ReDim works OK if I declare a local array and redim it, so potentially I could do that and then assign it to the property... but it just seems bizarre that I can't redim it directly.
Any idea why it's not working?
With the Variant approach above my code is:
ReDim MyFancyCat.Socks(socksLength) As FancySocks
And in the FancyCat class module:
public Socks As Variant
I get Method or Data Member Not Found.
The error for the other approach was different but I rejigged all my code to try the second approach so I am not sure what it was.
Edit: I'm gonna explain what I am trying to do a bit more clearly. I have some JSON data coming in, and I want to store it as an object hierarchy.
In C# I would do this (pseudo code without linq shortcuts):
var myData = ReadJsonData(); // Produces a kind of dictionary
var myFancyCat = new FancyCat();
myFancyCat.Name = myData["Name"];
myFancyCat.Age = myData["Age"];
myFancyCat.Socks = new List<FancySock>();
foreach (var sock in myData["Socks"])
{
myFancyCat.Socks.Add(sock);
}
In excel I want to do the same thing.
So I make a class module for FancyCat and FancySock and give FancyCat public members for Name, Age etc but then I also want an array of socks that my cat owns. I wanted to do this with strongly typed references, e.g. my c# code above I can do:
myFancyCat.Socks[0].Colour // Intellisense works, shows colour as a property
However it seems in excel you can't have publicly declared arrays. So you can get around this according to the comments by declaring it as variant and then sticking an array in anyway, but you would lose the intellisense. Or you can use a get/let property which kinda works but is more fiddly as it seems you can't actually expose an array using a get/let you have to have it take an index and expose elements individually.
So at this point I am thinking forget the strongly typed it's not happening, perhaps use a collection?
The FancySock class may have further nested arrays within it. I've read that there's no ByRef for arrays (at least, not completely - I think you can get an array ByRef but not set one?). I am not sure if that would create problems with trying to set it.
But ultimately, I just want to end up with my JSON data represented easily in an OO way, so that in my excel ultimately I can just do
myFancyCat.Name or myFancyCat.Socks.Count or myFancyCat.Socks(1).Colour etc
It seems much harder than it looks to simply deserialise JSON into 'objects' in vba.
Please, try the next way:
Insert a class module, name it FancyCat and copy the next code:
Option Explicit
Private arrL As Object
Public myName As String, myAge As Long
Public Sub Class_Initialize()
Set arrL = CreateObject("System.Collections.ArrayList")
End Sub
Public Property Let Name(strName As String)
myName = strName
End Property
Public Property Let Age(lngAge As String)
myAge = lngAge
End Property
Public Property Let SocksAdd(sMember)
arrL.Add sMember
End Property
Public Property Get Socks() As Variant
Socks = arrL.toarray()
End Property
Use it in the next testing Sub:
Sub testClassDictListArray()
Dim myFancyCat As New FancyCat, myData As Object
Dim arrSocks, sock
Set myData = CreateObject("Scripting.Dictionary") 'this should be the dictionary returned by ParseJSON
myData.Add "Name", "John Doe": myData.Add "Age", 35
myData.Add "Socks", Array("Blue", "White", "Red", "Green", "Yellow")
myFancyCat.Name = myData("Name")
myFancyCat.Age = myData("Age")
For Each sock In myData("Socks")
myFancyCat.SocksAdd = sock
Next sock
arrSocks = myFancyCat.Socks
Debug.Print Join(arrSocks, "|")
End Sub
I am not sure I perfectly understand the scenario you try putting in discussion...
If you want to benefit of instellisense suggestions, I will tell you what references to be added. Even, I will send two pieces of code to automatically add the necessary references (I mean, Scripting.Dictionary and ArrayList`).
Please, test it and send some feedback.
In your class:
Private m_Name As String
Private m_Socks() As String
Public Property Let Name(Name As String)
m_Name = Name
End Property
Public Property Get Name() As String
Name = m_Name
End Property
Public Sub SetSize(Quantity As Long)
ReDim m_Socks(1 To Quantity)
End Sub
Public Property Let Socks(Index As Long, Sock As String)
m_Socks(Index) = Sock
End Property
Public Property Get Socks(Index As Long) As String
Socks = m_Socks(Index)
End Property
In a regular module:
Sub UseFancyCat()
Dim MyFancyCat As FancyCat
Set MyFancyCat = New FancyCat
MyFancyCat.Name = "Fancy Name"
MyFancyCat.SetSize 2
MyFancyCat.Socks(1) = "Sock1"
MyFancyCat.Socks(2) = "Sock2"
Debug.Print MyFancyCat.Name
Debug.Print MyFancyCat.Socks(1)
Debug.Print MyFancyCat.Socks(2)
End Sub
I have a question, in vb.net, how do I go through two lists with a for each or another method?
for example i have defined 3 list
Public NSPS As New List(Of String)
Public CONTENEDOR As New List(Of String)
Public IDCONTENEDOR As New List(Of String)
I have 2 excel files with the same variable CONTENEDOR, and in the other its called IDCONTENEDOR
So i need to create a third excel file that finds the rows with the same IDcontenedor and it adds the variable NSPS as appropriate, to that row in the new excel .
What is the correct way to go through 2 lists with 2 cycles in vb.net
to just fill information in a new excel i use this code
Dim aux As Integer = 1
While (aux <= CONTENEDOR.Count)
hoja1.Cells("B" & aux + 1).Value = NPLANILLA.Item(aux - 1)
aux += 1
End While
aux = 1
Thanks in advance!
I am not sure if I understand your problem. The following answer is based on the best guess I could make.
To find a value corresponding to a key or id, use a dictionary. Dictionaries are much faster than loops for lookups.
Dim dict As new Dictionary(Of String, String)
Assuming that the 2 variables CONTENEDOR and IDCONTENEDOR have the same length and that the first list contains the values corresponding to the ids at the same index, you can fill the dictionary with
For i As Integer = 0 To CONTENEDOR.Count - 1
dict.Add(IDCONTENEDOR(i), CONTENEDOR(i))
Next
Now given an id of type String, you can get the value with
Dim value As String
If dict.TryGetValue(id, value) Then
NSPS.Add(value)
Else
' We did not find a value with this id.
End If
I am not sure where you get the id from, maybe from NPLANILLA.Item(aux - 1)? If it is typed as Object then convert it to String:
Dim id As String
id = CType(NPLANILLA.Item(aux - 1), String)
I'm having a class module with some data:
Private sharedFolders() As String
Public Property Let SetSharedFolders(val As String)
Dim i As Integer
sharedFolders = Array("folder one", "folder two")
i = UBound(sharedFolders)
i = UBound(sharedFolders)
ReDim Preserve sharedFolders(i)
sharedFolders(i) = CStr(val)
End Property
Property Get GetSharedFolders()
GetSharedFolders = sharedFolders()
End Property
And I want to add something to this property from other module like this:
Sub PrepareData()
Dim e
Dim s
Dim a(2) As String
Set e = New Entry
a(0) = "add one"
a(1) = "add two"
For Each s In a
e.SetSharedFolders (s) 'Here comes exception
Next
For Each s In e.GetSharedFolders
Debug.Print s
Next
End Sub
But I receive an "wrong number of arguments or invalid property assignment vba" exception... Can anyone assist?
Addendum
Thanks to #AJD and #Freeflow to pointing out a mistake and idea to make it easier. Decided to make as like below.
Class Module:
Private sharedFolders As New Collection
Public Property Let SetSharedFolders(val As String)
If sharedFolders.Count = 0 Then ' if empty fill with some preset data and add new item
sharedFolders.Add "folder 1"
sharedFolders.Add "folder 2"
sharedFolders.Add CStr(val)
Else
sharedFolders.Add CStr(val)
End If
End Property
Property Get GetSharedFolders() As Collection
Set GetSharedFolders = sharedFolders
End Property
and regular module:
Sub AddData()
Dim e As New Entry ' creating an instance of a class
Dim s As Variant ' variable to loop through collection
Dim a(1) As String 'some array with data to insert
a(0) = "add one"
a(1) = "add two"
For Each s In a
e.SetSharedFolders = s
Next
For Each s In e.GetSharedFolders
Debug.Print s
Next
End Sub
Initially I thought the problem lies in this code:
i = UBound(sharedFolders)
i = UBound(sharedFolders)
ReDim Preserve sharedFolders(i)
sharedFolders(i) = CStr(val)
i is set twice to the same value, and then the sharedFolders is reDimmed to the same value it was before! Also, there is some trickery happening with the use of ix within a 0-based array.
But the problem is most likely how you have declared your variables.
For Each s In a
e.SetSharedFolders (s) 'Here comes exception
Next
s is a Variant, and a is a Variant. At this point VBA is trying to guess how to handle a For Each loop with two Variants. And then the improper call is made. The correct syntax is:
e.SetSharedFolders s '<-- no parenthesis
There are plenty of posts on StackOverflow explaining how to call routines and what the impact of the evaluating parenthesis are!
However, at this point we are only assuming it is passing in a single element of the array - it could be passing the full array itself (albeit unlikely).
And the third factor -
Public Property Let SetSharedFolders(val As String)
The parameter val is being passed ByRef and should be passed ByVal. This also has unintended side effects as I found out (Type mismatch trying to set data in an object in a collection).
Public Property Let SetSharedFolders(ByVal val As String)
All in all you have the perfect storm of ambiguity driving to an unknown result.
The answer here is to strongly type your variables. This removes about two layers of ambiguity and areas where errors can happen. In addition, this will slightly improve code execution.
Another aspect is to understand when you should pass something ByVal and when to use the default (preferably explicitly) ByRef.
And a final gratuitous hint: Use a Collection instead of an Array. Your code you have implies a Collection will be more efficient and easier to manage.
Addendum
(thanks to #FreeFlow):
If the OP changes the definition of sharedfolders to Variant rather than String() then the array statement will work as expected.
The line e.SetSharedFolders (s) will work fine if it is changed to e.SetSharedFolders = s because the method SetSharedFolders is a Let Property not a Sub. There are other errors but these two changes will make the code run.
I've been trying to learn how to create customized collections in Excel VBA and I found this piece of code on MSDN. While I understand most of it, can anyone tell me what the last code Set Add = empNew is doing? I don't understand it's comment. Thank you!
' Methods of the Employees collection class.
Public Function Add(ByVal Name As String, _
ByVal Salary As Double) As Employee
Dim empNew As New Employee
Static intEmpNum As Integer
' Using With makes your code faster and more
' concise (.ID vs. empNew.ID).
With empNew
' Generate a unique ID for the new employee.
intEmpNum = intEmpNum + 1
.ID = "E" & Format$(intEmpNum, "00000")
.Name = Name
.Salary = Salary
' Add the Employee object reference to the
' collection, using the ID property as the key.
mcolEmployees.Add empNew, .ID
End With
' Return a reference to the new Employee.
Set Add = empNew
End Function
You will notice that Add is the name of the Function. By issuing Set Add = newEmp your code is declaring that the return value (or object, in this case) of the function, is the newly created employee object newEmp. This means that the function will pass the variable newEmp back to its caller.
Say that you had some procedure calling your function, you would be able to do this:
Sub listEmployees
Dim e As Employee
' Create a new employee, and assign the variable e to point to this object
Set e = Add("John", 1000) ' Notice that the only reason we use "add" here is because it is the name of the function you provided
' e is now an Employee object, after being created in the line above, meaning we can access whatever properties is defined for it. The function Add lists some properties, so we can use those as examples.
Debug.Print e.Name
Debug.Print e.Salary
Debug.Print e.ID
End Sub
First, you need to define the new Type you have created, so put the following code on top of your module:
Public Type Employee
id As String
Name As String
Salary As Long
End Type
Then, inside your Public Function Add , change to Dim empNew As Employee.
Not sure why you need the following line : mcolEmployees.Add empNew, .id ??
and the last line modify to Add = empNew.
Then, when I test this Function from the following Sub:
Sub testEmp()
Dim s As Employee
s = Add("Shai", 50000)
End Sub
I get for s in the immediate window the following values:
s.id = E00001
s.Name = "Shai"
s.Salary = 50000
I hope this is what you intended in your post.
Let's say that I have an object...
Class test
Public a
Public b
End class
And in my code I would like to instantiate it without knowing a predetermined variable name to store the new instance.
Is this possible? How would I then dim the random variable?
I want to be able to do this because I don't want my user input to be stored or saved in the same variable with other data conflict. Like say for example I am storing stats for an athlete and I ask the athlete's name. If every time the user enters a different name, I don't want to have this information in the same object instance. Could I create an object for the athlete and reference this object inside another object?
I have a snippet of code:
Function addStats
dim pAtt, pComp, pInt, pTds, pYds, endNum, pName
Wscript.StdOut.WriteLine "What is your quarterback's name"
pName = Wscript.StdIn.ReadLine
Wscript.StdOut.WriteLine "How many attempts: "
pAtt = Wscript.StdIn.ReadLine
'chkNum(pAtt)
Wscript.StdOut.WriteLine "How many completions: "
pComp = Wscript.StdIn.ReadLine
'chkNum(pComp)
Wscript.StdOut.WriteLine "How many yards: "
pYds = Wscript.StdIn.ReadLine
'chkNum = pYds
Wscript.StdOut.WriteLine "How many touchdowns: "
pTds = Wscript.StdIn.ReadLine
'chkNum = pTds
Wscript.StdOut.WriteLine "How many interceptions: "
pInt = Wscript.StdIn.ReadLine
'chkNum = pInt
endNum = UBound(newStats) + 1
redim preserve newStats(endNum)
'---- vvvv ----
set newStats(endNum) = new QB
'---- ^^^^ ----
newStats(endNum).att = pAtt
newStats(endNum).comp = pComp
newStats(endNum).yds = pYds
newStats(endNum).tds = pTds
newStats(endNum).ints = pInt
newStats(endNum).qbname = pName
Wscript.StdOut.WriteLine "Stats Added"
writeBuffer()
end Function
The object is:
class QB
dim att, comp, yds, tds, ints, qbname
public property let qbAtt(n)
att = n
end property
public property let qbComp(n)
comp = n
end property
public property let qbYds(n)
yds = n
end property
public property let qbTds(n)
tds = n
end property
public property let qbInt(n)
ints = n
end property
public property let qName(n)
qbname = n
end property
public property get qbAtt
qbAtt = att
end property
public property get qbComp
qbComp = comp
end property
public property get qbYds
qbYds = yds
end property
public property get qbTds
qbTds = tds
end property
public property get qbInt
qbInt = ints
end property
public property get qName
qName = qbname
end property
end class
The highlighted statement is me instantiating the object with a global variable - newStat(). My belief is that I would have to either create a class for just the quarterback and somehow reference this into a variable array that is determined on the quarterback's name or create a function that takes the user prompt variable that asks the quarterback's name and set it as an array that instantiates the QB class.
I think you're confusing classes, objects, and variables. Classes are basically templates that describe which properties and behavior entities of a particular category have. Objects are instances of a class. Variables are identifiers that refer to objects (or data of primitive types).
Take lockers for example. Lockers usually have a color, they can be opened and closed, and they may contain a number of items. Thus a general description of lockers (a class "Locker") might look like this:
Class Locker
Public color
Public closed
Public content
Sub Class_Initialize
closed = True
content = Array()
End Sub
Sub Open
closed = False
End Sub
Sub Close
closed = True
End Sub
End Class
The constructor (Class_Initialize) is a special method that is only called when an object (an instance of a class) is created. It sets the initial state of the object. Kind of like when a locker is built in the factory.
A particular locker (object) might be green and contain a hat and a magazine, while another locker (object) might be blue and contain a book and a jacket.
Set locker_A = New Locker
locker_A.color = "green"
locker_A.content = Array("hat", "magazine")
Set locker_B = New Locker
locker_B.color = "blue"
locker_B.content = Array("book", "jacket")
To be able to actually work with objects (or other data) in a program you need variables. These identifiers (locker_A and locker_B in the example above) allow you to refer to particular objects in your program code in order to access their properties and methods.
When you run a statement
Set newStats(2) = New QB
it creates a new instance of the class QB (a new object) and places a reference to that object in the third slot of the array newStats. Afterwards you can use newStats(2) to refer to that object in your program or script. For instance:
newStats(2).yds = 42 'yards
If you have a number of QB object and store each of them in a different array slot there shouldn't be any conflict between them, as long as you don't replace a reference in one slot with another one.
Set newStats(2) = newStats(5) '<-- don't do this
If you wanted to access objects from the list by a particular property (for instance the name) instead of by index you'd use a Dictionary instead of an array:
Set newStats = CreateObject("Scripting.Dictionary")
newStats.CompareMode = vbTextCompare 'make lookups case-insensitive
...
Set player = New QB
player.att = pAtt
...
player.qbname = pName
newStats.Add pName, player
Then you could access a particular player like this:
name = "Charley Johnson"
WScript.Echo newStats(name).yds