Let and Get array of fixed size - arrays

I have a class that has a fixed-size array of Double, for example
Private m_values(8) as Double
What is the correct syntax for the Let and Get methods for an array?
Public Property Let Values (RHS(8) as Double)
m_values = RHS
End Property
Public Property Get Values() as Double
Values = m_values
End Property
The specific parts of the syntax I am unclear about:
a. In the Let method, is RHS(8) as Double the correct way to pass an array of 8 Double?
b. Can I copy one array to another simply using assignment? (e.g. m_values = values)
c. For the Get method, is it correct for the function to be declared as Double or should it be something like as Double(8)?

The only way to declare a property that can hold arrays is as Variant property.
Private m_values As Variant
Public Property Let Values(RHS As Variant)
m_values = RHS
End Property
Public Property Get Values() As Variant
Values = m_values
End Property
Public Sub Test()
Dim x(8) As Double
x(1) = 123.55
x(2) = 456.45
x(5) = 789.66
x(8) = 123.777
' assign value to property
Values = x
' get value from property
Dim y() As Double
y = Values
Dim i As Integer
For i = 0 To UBound(y)
Debug.Print y(i)
Next
End Sub

Try to keep the following rules:
'starting point- array with 8 elements
Dim arrStart(8) As Double
arrStart(8) = 1 'here- for testing with Local window
'passing array to another...Variant type variable
'no array declaration required
Dim arrVariant As Variant
arrVariant = arrStart
'passing array to another Double variable
'dynamic array declaration required
Dim arrEmpty() As Double
arrEmpty = arrStart
These rules work also when passing variable (as an parameter) to another property, function or subroutine. This also means that you can't declare Get Property as an array and you should declare it as a Variant type.

Related

Setting string a variable to get values from a structured array

I have a simple problem in VB.net.
I want to retrieve datas from an array declared as structure using a string to visualize the variable.
Below my code:
Module DataAnalisys
Dim InputData(100000) As InputDataStructure
Dim VariablesParameter(6) As VariablesParameterStructure
Dim VariablesGlobal(6) As VariablesDataStrucutre
Structure InputDataStructure
Dim ID As Integer
Dim FileId As Integer
Dim ProductionDate As Date
Dim ProductionTime As Date
Dim Shift As Integer
Dim IPP As Integer
Dim BPS As Integer
Dim SerialNumber As String
Dim Top As Single
Dim Bottom As Single
Dim Right As Single
Dim Left As Single
Dim OffCutH As Single
Dim OffCutV As Single
Dim Row As Byte
Dim Col As String
Dim Position As String
Dim Pack As Integer
Dim Sheet As Integer
Dim SheetInPack As Integer
End Structure
Structure VariablesParameterStructure
Dim NameParameter As String
Dim Target As Single
Dim Tolerance As Single
Dim LowTolerance As Single
Dim UppTolerance As Single
End Structure
Structure VariablesDataStrucutre
Dim NameData As String
Dim Position As String
Dim N As Long
Dim Mean As Single
Dim Difference As Single
Dim Scrap As Single
Dim ScrapGreat As Single
Dim ScrapLess As Single
Dim SeMean As Single
Dim StDev As Single
Dim Min As Single
Dim Q1 As Single
Dim Median As Single
Dim Q3 As Single
Dim Max As Single
End Structure
Sub RoutineAnalisysGlobal()
For B As Byte = 0 To VariablesGlobal.Length - 1
Dim ID As String = VariablesParameter(B).NameParameter
Dim Target As Single = VariablesParameter(B).Target
Dim LowTol As Single = VariablesParameter(B).LowTolerance
Dim UppTol As Single = VariablesParameter(B).UppTolerance
With VariablesGlobal(B)
.NameData = ID
.Position = "All"
.N = InputData.Length
.Mean = InputData.Average(Function(f) f.ID)
.Difference = .Mean - Target
.ScrapLess = InputData.Count(Function(f) f.ID < LowTol)
.ScrapGreat = InputData.Count(Function(f) f.ID > UppTol)
.Q1 = InputData.FirstQuartile(Function(F) F.ID)
.Min = InputData.Min(Function(f) f.ID)
.Median = InputdataMedian(Function(f) f.ID)
.Q3 = InputData.ThirdQuartile(Function(f) f.ID)
.Max = InputData.Max(Function(f) f.ID)
End With
Next
End Sub
End Module
The VariablesParameter().Name are Top, Bottom, Right, Left, OffCutH and OffCutV.
The code does not work when I'm using the part (Function(f) f.ID) as I want to use the function on different items of the structure of InputData according the for cycle. I want to display f.Top f.Bottom f.Right f.Left f.OffCutH and f.OffCutV.
Does anyone want to help me? I was looking on how convert the string ID into the variable of InputData.?????? structure.
I think what you're looking for is something like this:
Dim functions As Func(Of InputDataStructure, Single)() = {Function(ids) ids.Top,
Function(ids) ids.Bottom,
Function(ids) ids.Right,
Function(ids) ids.Left,
Function(ids) ids.OffCutH,
Function(ids) ids.OffCutV}
For i = 0 To VariablesGlobal.GetUpperBound(0)
With VariablesGlobal(i)
'...
.Mean = InputData.Average(functions(i))
'...
End With
Next
By way of explanation, the Function(f) f.ID part of this line:
.Mean = InputData.Average(Function(f) f.ID)
is a lambda expression, i.e. a delegate for an anonymous method. The Average method that you're calling requires a delegate for a method that has a single argument of type T, where T matches the generic type of the IEnumerable(Of T) that you're calling it on, and returns a value of a numeric type, e.g. Integer or Single. In your case, you are calling Average on InputData, which implements IEnumerable(Of InputDataStructure). Your lambda expression has a parameter of type InputDataStructure and returns an Integer, so it works with Average. The Average method invokes that delegate for every item in the list, gets all the numeric results and then divides that by the number of items in the list to get the average.
To make it clearer what is happening there, you could used a named method instead of a lambda expression. You could write this method:
Private Function GetID(f As InputDataStructure) As Integer
Return f.ID
End Function
and then change your code to this:
.Mean = InputData.Average(AddressOf GetID)
Hopefully you can identify the parts of that named method that correspond to the lambda express and the parts that are inferred.
Now, you have a number of lines of code there that use the same lambda expression, e.g.
.Min = InputData.Min(Function(f) f.ID)
.Median = InputdataMedian(Function(f) f.ID)
.Q3 = InputData.ThirdQuartile(Function(f) f.ID)
for something so simple, it is customary to do it the way you have. You do have the option, though, of writing the lambda expression once, assigning it to a variable and then using that variable multiple times. You could do this:
Dim f As Func(Of InputDataStructure, Integer) = Function(ids) ids.ID
or this:
Dim f = Function(ids As InputDataStructure) ids.ID
to end up with the variable f referring to a delegate that takes an argument of type InputDataStructure and returns a value of type Integer. You can then use that variable where you previously used multiple lambda expressions:
.Min = InputData.Min(f)
.Median = InputdataMedian(f)
.Q3 = InputData.ThirdQuartile(f)
So, now that we determined that all you need is a delegate to a method that takes an argument of the correct type and returns the correct type, take another look at the code I posted. It first creates an array of six delegates, each of which takes an argument of type InputDataStructure and returns a value of type Single. You can then use an element of that array wherever such a delegate is expect.
That means that you can loop over that array and pass each delegate to all the extensions methods that expect such a delegate, including Average. If you get the delegate at index 0 then you get the delegate that returns the Top property value, so Average will get the average Top value. If you use index 5 then you get the delegate that returns the OffCutV property value, so Average will get the average OffCutV value. Etc.
For the record, I recommend using parameter names in lambdas that indicate the type. You used f but that's meaningless. The parameter type is InputDataStructure so ids is indicative of that.

What is the best strategy in VBA to transfer a full array from one userform lists to another?

I am passing the content of a two dimensional array (first column header and than all column numbers) to a userform named Listbox1. Here the user selects items that are passed to Listbox2 and at last the user activates a process that sends data to a webservice.
So far I have managed to populate Listbox1 with data headers only but have the problem to retain all values in listbox2, not only the header. I thought of creating a private variable at the userform level to store the full array but it seems to generate a type mismatch issue with the let/get properties.
What is the best strategy to handle a full set of data with two lists?
Private pArr As Variant
Public Property Get arr() As Variant
Set arr = pArr
End Property
Public Property Let arr(Value As Variant)
Set pArr = Value
End Property
Private Sub LoadModelData()
Dim i As Integer
Dim myArray As Variant
Dim v As Variant
Dim mystring As String
myArray = ReadModelData(this is the function returning the array data from a range)
Set pArr = myArray
For i = LBound(myArray) To UBound(pArr)
If pArr(i)(1, 1) <> vbNullString Then
frmListModelItms.List1.AddItem (pArr(i)(1, 1))
End If
Next i
End Sub
my understading is arr property of your class is an array, not an object
hence don't use Set keyword:
Private pArr As Variant
Public Property Get arr() As Variant
arr = pArr
End Property
Public Property Let arr(Value As Variant)
pArr = Value
End Property

Array as a Class Member

I'm designing a dynamic buffer for outgoing messages. The data structure takes the form of a queue of nodes that have a Byte Array buffer as a member. Unfortunately in VBA, Arrays cannot be public members of a class.
For example, this is a no-no and will not compile:
'clsTest
Public Buffer() As Byte
You will get the following error: "Constants, fixed-length strings, arrays, user-defined types and Declare statements not allowed as Public members of object modules"
Well, that's fine, I'll just make it a private member with public Property accessors...
'clsTest
Private m_Buffer() As Byte
Public Property Let Buffer(buf() As Byte)
m_Buffer = buf
End Property
Public Property Get Buffer() As Byte()
Buffer = m_Buffer
End Property
...and then a few tests in a module to make sure it works:
'mdlMain
Public Sub Main()
Dim buf() As Byte
ReDim buf(0 To 4)
buf(0) = 1
buf(1) = 2
buf(2) = 3
buf(3) = 4
Dim oBuffer As clsTest
Set oBuffer = New clsTest
'Test #1, the assignment
oBuffer.Buffer = buf 'Success!
'Test #2, get the value of an index in the array
' Debug.Print oBuffer.Buffer(2) 'Fail
Debug.Print oBuffer.Buffer()(2) 'Success! This is from GSerg's comment
'Test #3, change the value of an index in the array and verify that it is actually modified
oBuffer.Buffer()(2) = 27
Debug.Print oBuffer.Buffer()(2) 'Fail, diplays "3" in the immediate window
End Sub
Test #1 works fine, but Test #2 breaks, Buffer is highlighted, and the error message is "Wrong number of arguments or invalid property assignment"
Test #2 now works! GSerg points out that in order to call the Property Get Buffer() correctly and also refer to a specific index in the buffer, TWO sets of parenthesis are necessary: oBuffer.Buffer()(2)
Test #3 fails - the original value of 3 is printed to the Immediate window. GSerg pointed out in his comment that the Public Property Get Buffer() only returns a copy and not the actual class member array, so modifications are lost.
How can this third issue be resolved make the class member array work as expected?
(I should clarify that the general question is "VBA doesn't allow arrays to be public members of classes. How can I get around this to have an array member of a class that behaves as if it was for all practical purposes including: #1 assigning the array, #2 getting values from the array, #3 assigning values in the array and #4 using the array directly in a call to CopyMemory (#3 and #4 are nearly equivalent)?)"
So it turns out I needed a little help from OleAut32.dll, specifically the 'VariantCopy' function. This function faithfully makes an exact copy of one Variant to another, including when it is ByRef!
'clsTest
Private Declare Sub VariantCopy Lib "OleAut32" (pvarDest As Any, pvargSrc As Any)
Private m_Buffer() As Byte
Public Property Let Buffer(buf As Variant)
m_Buffer = buf
End Property
Public Property Get Buffer() As Variant
Buffer = GetByRefVariant(m_Buffer)
End Property
Private Function GetByRefVariant(ByRef var As Variant) As Variant
VariantCopy GetByRefVariant, var
End Function
With this new definition, all the tests pass!
'mdlMain
Public Sub Main()
Dim buf() As Byte
ReDim buf(0 To 4)
buf(0) = 1
buf(1) = 2
buf(2) = 3
buf(3) = 4
Dim oBuffer As clsTest
Set oBuffer = New clsTest
'Test #1, the assignment
oBuffer.Buffer = buf 'Success!
'Test #2, get the value of an index in the array
Debug.Print oBuffer.Buffer()(2) 'Success! This is from GSerg's comment on the question
'Test #3, change the value of an index in the array and verify that it is actually modified
oBuffer.Buffer()(2) = 27
Debug.Print oBuffer.Buffer()(2) 'Success! Diplays "27" in the immediate window
End Sub
#Blackhawk,
I know it is an old post, but thought I'd post it anyway.
Below is a code I used to add an array of points to a class, I used a subclass to define the individual points, it sounds your challenge is similar:
Mainclass tCurve
Private pMaxAmplitude As Double
Private pCurvePoints() As cCurvePoint
Public cDay As Date
Public MaxGrad As Double
Public GradChange As New intCollection
Public TideMax As New intCollection
Public TideMin As New intCollection
Public TideAmplitude As New intCollection
Public TideLow As New intCollection
Public TideHigh As New intCollection
Private Sub Class_Initialize()
ReDim pCurvePoints(1 To 1500)
ReDim curvePoints(1 To 1500) As cCurvePoint
Dim i As Integer
For i = 1 To 1500
Set Me.curvePoint(i) = New cCurvePoint
Next
End Sub
Public Property Get curvePoint(Index As Integer) As cCurvePoint
Set curvePoint = pCurvePoints(Index)
End Property
Public Property Set curvePoint(Index As Integer, Value As cCurvePoint)
Set pCurvePoints(Index) = Value
End Property
subclass cCurvePoint
Option Explicit
Private pSlope As Double
Private pCurvature As Double
Private pY As Variant
Private pdY As Double
Private pRadius As Double
Private pArcLen As Double
Private pChordLen As Double
Public Property Let Slope(Value As Double)
pSlope = Value
End Property
Public Property Get Slope() As Double
Slope = pSlope
End Property
Public Property Let Curvature(Value As Double)
pCurvature = Value
End Property
Public Property Get Curvature() As Double
Curvature = pCurvature
End Property
Public Property Let valY(Value As Double)
pY = Value
End Property
Public Property Get valY() As Double
valY = pY
End Property
Public Property Let Radius(Value As Double)
pRadius = Value
End Property
Public Property Get Radius() As Double
Radius = pRadius
End Property
Public Property Let ArcLen(Value As Double)
pArcLen = Value
End Property
Public Property Get ArcLen() As Double
ArcLen = pArcLen
End Property
Public Property Let ChordLen(Value As Double)
pChordLen = Value
End Property
Public Property Get ChordLen() As Double
ChordLen = pChordLen
End Property
Public Property Let dY(Value As Double)
pdY = Value
End Property
Public Property Get dY() As Double
dY = pdY
End Property
This will create a tCurve with 1500 tCurve.Curvepoints().dY (for example)
The trick is to get the index process correct in the main class !
Good luck !
Not the most elegant solution, but modeling from the code you provided...
In clsTest:
Option Explicit
Dim ArrayStore() As Byte
Public Sub AssignArray(vInput As Variant, Optional lItemNum As Long = -1)
If Not lItemNum = -1 Then
ArrayStore(lItemNum) = vInput
Else
ArrayStore() = vInput
End If
End Sub
Public Function GetArrayValue(lItemNum As Long) As Byte
GetArrayValue = ArrayStore(lItemNum)
End Function
Public Function GetWholeArray() As Byte()
ReDim GetWholeArray(LBound(ArrayStore) To UBound(ArrayStore))
GetWholeArray = ArrayStore
End Function
And in mdlMain:
Sub test()
Dim buf() As Byte
Dim bufnew() As Byte
Dim oBuffer As New clsTest
ReDim buf(0 To 4)
buf(0) = 1
buf(1) = 2
buf(2) = 3
buf(3) = 4
oBuffer.AssignArray vInput:=buf
Debug.Print oBuffer.GetArrayValue(lItemNum:=2)
oBuffer.AssignArray vInput:=27, lItemNum:=2
Debug.Print oBuffer.GetArrayValue(lItemNum:=2)
bufnew() = oBuffer.GetWholeArray
Debug.Print bufnew(0)
Debug.Print bufnew(1)
Debug.Print bufnew(2)
Debug.Print bufnew(3)
End Sub
I added code to pass the class array to another array to prove accessibility.
Even though VBA won't allow us to pass arrays as properties, we can still use Functions to pick up where properties fall short.

VBA - Returning array from Property Get

If arrays are returned by reference, why doesn't the following work:
'Class1 class module
Private v() As Double
Public Property Get Vec() As Double()
Vec = v()
End Property
Private Sub Class_Initialize()
ReDim v(0 To 3)
End Sub
' end class module
Sub Test1()
Dim c As Class1
Set c = New Class1
Debug.Print c.Vec()(1) ' prints 0 as expected
c.Vec()(1) = 5.6
Debug.Print c.Vec()(1) ' still prints 0
End Sub
You don't have a let property. Also, the get property is returning the entire array, rather than just the element in question. Change the return type of Property Get from Double() to just plain Double. Add Property Let. Note that it takes two inputs, but only one is passed to it. The last variable (MyValue, in this case) is assumed to get it's value from whatever is after the = sign. Put a break point somewhere early in Test1() and see how the values are affected in the Locals window. Compare the variables created by the original code versus my code:
'Class1 class module
Private v() As Double
Public Property Get Vec(index As Long) As Double
Vec = v(index)
End Property
Public Property Let Vec(index As Long, MyValue As Double)
v(index) = MyValue
End Property
Private Sub Class_Initialize()
ReDim v(0 To 3)
End Sub
' end class module
'Begin module
Sub Test1()
Dim c As Class1
Set c = New Class1
Debug.Print c.Vec(1) ' prints 0 as expected
c.Vec(1) = 5.6
Debug.Print c.Vec(1) ' prints 5.6
End Sub
'End module
In VBA, arrays are never returned by reference unless they are returned through a ByRef parameter. Furthermore, whenever you use = to assign an array to a variable, you've made a new copy of the array, even if you're assigning it to a ByRef argument inside of a procedure, so you're pretty much out of luck trying to make this work.
Some alternative are...
Use a VBA.Collection instead of an array.
Make your own class that encapsulates an array and exposes procedures for indirectly accessing and manipulating the internal array.
I want to suggest another nice way to do this using a Collection and a static Property without the need to use a class:
imagine you want to have the xlCVError enum as an array (or collection), e.g. to loop through it on errors and handle it based on the actual error.
The following is initialized once on access:
'from https://stackoverflow.com/a/56646199/1915920
Static Property Get XlCVErrorColl() As Collection
Dim c As Collection 'will be already initalized after 1st access
'because of "Static Property" above!
Set XlCVErrorColl = c
If Not c Is Nothing Then Exit Property
'initialize once:
Set c = New Collection
c.Add XlCVError.xlErrDiv0
c.Add XlCVError.xlErrNA
c.Add XlCVError.xlErrName
c.Add XlCVError.xlErrNull
c.Add XlCVError.xlErrNum
c.Add XlCVError.xlErrRef
c.Add XlCVError.xlErrValue
Set XlCVErrorColl = c
End Property
Turning this into an array or implementing it as an array is straight forward, but collections seem to be more useful to me, with the disadvantage that their elements are not implicitely typed/(compile-time-)type checked.
So this would e.g. turn it into an (read-only) array (with the in-mem-copy-disadvantage mentioned in other answers/comments):
'from https://stackoverflow.com/a/56646199/1915920
Static Property Get XlCVErrorArr() As XlCVError()
Dim a() As XlCVError
XlCVErrorArr = a
If UBound( a ) > 0 Then Exit Property
'initialize once:
Dim c As Collection: Set c = XlCVErrorColl
ReDim a(c.Count)
Dim i As Integer: For i = 1 To c.Count
a(i) = c(i)
Next i
XlCVErrorArr = a
End Function
So transforming the example from Clayton Ss answer into a static, modifiable module property using some array it would be:
'module (no class required)
'from https://stackoverflow.com/a/56646199/1915920
Private v() As Double
Static Property Get Vec(index As Long) As Double
If UBound(v) < 3 Then 'initialize once:
ReDim v(0 To 3) 'one could initialize it with anyting after here too
end if
Vec = v(index)
End Property
Public Property Let Vec(index As Long, MyValue As Double)
v(index) = MyValue
End Property

VBA Array of variant type as class property

I have a class that handles several numeric arrays (type double) and also needs to handle an array of descriptors, which will include a mix of strings and integers, which need to be utilized as strings and numbers accordingly. So I decide to make an array property of type variant (not a variant containing an array). But this one does not seem to work, while the type double arrays do.
Specifically, this type double array-property works fine, to receive or return an array all at once:
Private p_dbNumericArray() As Double
Public Property Let NumericArray(Value() As Double)
p_dbNumericArray() = Value()
End Property
Public Property Get NumericArray() As Double()
NumericArray() = p_dbNumericArray()
End Property
But when I try the same pattern with an array of type variant, the Get property returns an empty/unallocated variant array:
Private p_vaVariantArray() As Variant
Public Property Let VariantArray(Value() As Variant)
p_vaVariantArray() = Value()
End Property
Public Property Get VariantArray() As Variant()
VariantArray() = p_vaVariantArray()
End Property
Wrapping an array in a variant (instead of having an array of type variant), of course works fine:
Private p_vaVariantArray As Variant
Public Property Let VariantArray(Value As Variant)
p_vaVariantArray = Value
End Property
Public Property Get VariantArray() As Variant
VariantArray = p_vaVariantArray
End Property
But is it known and standard that the pattern that works for Dim D() As Double does not work for Dim V() As Variant, in properties?
Public Property Get VariantArray() As Variant()
VariantArray = p_vaVariantArray()
End Property
Note the missing parentheses.

Resources