excel - vectores - Cómo devolver el número de dimensiones de una variable(Variante) que se le pasó en VBA
ubound vba (9)
¿Qué tal si usamos ubound (var) + 1? Eso debería darte el último elemento de la mayoría de las variables (a menos que sea un rango personalizado, pero en ese caso ya deberías conocer esa información). El rango de una variable convencional (por ejemplo, cuando se usa la función dividida) comienza con 0; Ubound te proporciona el último elemento de la variable. Entonces, si tienes una variable con 8 elementos, por ejemplo, irá de 0 (lbound) a 7 (ubound), y puedes saber la cantidad de elementos que acaban de agregar ubound (var) + 1. Por ejemplo:
Public Sub PrintQntElements()
Dim str As String
Dim var As Variant
Dim i As Integer
str = "Element1!Element2!Element3!Element4!Element5!Element6!Element7!Element8"
var = Split(str, "!")
i = UBound(var) + 1
Debug.Print "First element: " & LBound(var)
Debug.Print "Last element: " & UBound(var)
Debug.Print "Quantity of elements: " & i
End Sub
Imprimirá esta salida en la ventana Inmediato:
Primer elemento: 0
Último elemento: 7
Cantidad de elementos: 8
Además, si no está seguro de que el primer elemento (lbound) sea 0, puede usar:
i = UBound (var) - LBound (var) + 1
¿Alguien sabe cómo devolver el número de dimensiones de una variable (Variante) que se le pasó en VBA?
@cularis y @Issun tienen respuestas perfectamente adecuadas para la pregunta exacta. Sin embargo, voy a cuestionar tu pregunta. ¿Realmente tienes un grupo de matrices de recuento de dimensiones desconocidas flotando? Si está trabajando en Excel, la única situación donde esto debería ocurrir es un UDF en el que se le puede pasar una matriz 1-D o una matriz 2-D (o una matriz que no sea una matriz), pero nada más.
Casi nunca deberías tener una rutina que espere algo arbitrario. Y por lo tanto, probablemente tampoco deba tener una rutina general de "encontrar un número de dimensiones de matriz".
Entonces, con eso en mente, aquí están las rutinas que uso:
Global Const ERR_VBA_NONE& = 0
Global Const ERR_VBA_SUBSCRIPT_OUT_OF_RANGE& = 9
''Tests an array to see if it extends to a given dimension
Public Function arrHasDim(arr, dimNum As Long) As Boolean
Debug.Assert IsArray(arr)
Debug.Assert dimNum > 0
''Note that it is possible for a VBA array to have no dimensions (i.e.
''''LBound'' raises an error even on the first dimension). This happens
''with "unallocated" (borrowing Chip Pearson''s terminology; see
''http://www.cpearson.com/excel/VBAArrays.htm) dynamic arrays -
''essentially arrays that have been declared with ''Dim arr()'' but never
''sized with ''ReDim'', or arrays that have been deallocated with ''Erase''.
On Error Resume Next
Dim lb As Long
lb = LBound(arr, dimNum)
''No error (0) - array has given dimension
''Subscript out of range (9) - array doesn''t have given dimension
arrHasDim = (Err.Number = ERR_VBA_NONE)
Debug.Assert (Err.Number = ERR_VBA_NONE Or Err.Number = ERR_VBA_SUBSCRIPT_OUT_OF_RANGE)
On Error GoTo 0
End Function
''"vect" = array of one and only one dimension
Public Function isVect(arg) As Boolean
If IsObject(arg) Then
Exit Function
End If
If Not IsArray(arg) Then
Exit Function
End If
If arrHasDim(arg, 1) Then
isVect = Not arrHasDim(arg, 2)
End If
End Function
''"mat" = array of two and only two dimensions
Public Function isMat(arg) As Boolean
If IsObject(arg) Then
Exit Function
End If
If Not IsArray(arg) Then
Exit Function
End If
If arrHasDim(arg, 2) Then
isMat = Not arrHasDim(arg, 3)
End If
End Function
Tenga en cuenta el enlace al excelente sitio web de Chip Pearson: http://www.cpearson.com/excel/VBAArrays.htm
Ver también: ¿Cómo puedo determinar si una matriz se inicializa en VB6? . Personalmente, no me gusta el comportamiento indocumentado del que depende, y el rendimiento rara vez es tan importante en el código de Excel VBA que estoy escribiendo, pero de todos modos es interesante.
Encontré una manera bastante simple de verificar, probablemente cargada con un montón de falsa pas de codificación, jerga incorrecta y técnicas desacertadas, pero nunca menos:
Dim i as Long
Dim VarCount as Long
Dim Var as Variant
''generate your variant here
i = 0
VarCount = 0
recheck1:
If IsEmpty(Var(i)) = True Then GoTo VarCalc
i = i + 1
GoTo recheck1
VarCalc:
VarCount= i - 1
Nota: VarCount obviamente devolverá un número negativo si Var (0) no existe. VarCount es el número de referencia máximo para usar con Var (i), i es el número de variantes que tiene.
Microsoft ha documentado la estructura de VARIANT y SAFEARRAY, y al usarlas puede analizar los datos binarios para obtener las dimensiones.
Crea un módulo de código normal. Yo llamo al mío "mdlDims". Lo usarías llamando a la función simple ''GetDims'' y pasando una matriz.
Option Compare Database
Option Explicit
Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByVal Destination As Long, ByVal Source As Long, ByVal Length As Integer)
Private Declare Function VarPtrArray Lib "msvbvm60.dll" Alias "VarPtr" (var() As Any) As Long
''http://msdn.microsoft.com/en-us/library/windows/desktop/ms221482(v=vs.85).aspx
Private Type SAFEARRAY
cDims As Integer
fFeatures As Integer
cbElements As Long
cLocks As Long
pvData As Long
End Type
''Variants are all 16 bytes, but they are split up differently based on the contained type
''VBA doesn''t have the ability to Union, so a Type is limited to representing one layout
''http://msdn.microsoft.com/en-us/library/windows/desktop/ms221627(v=vs.85).aspx
Private Type ARRAY_VARIANT
vt As Integer
wReserved1 As Integer
wReserved2 As Integer
wReserved3 As Integer
lpSAFEARRAY As Long
data(4) As Byte
End Type
''http://msdn.microsoft.com/en-us/library/windows/desktop/ms221170(v=vs.85).aspx
Private Enum VARENUM
VT_EMPTY = &H0
VT_NULL
VT_I2
VT_I4
VT_R4
VT_R8
VT_CY
VT_DATE
VT_BSTR
VT_DISPATCH
VT_ERROR
VT_BOOL
VT_VARIANT
VT_UNKNOWN
VT_DECIMAL
VT_I1 = &H10
VT_UI1
VT_UI2
VT_I8
VT_UI8
VT_INT
VT_VOID
VT_HRESULT
VT_PTR
VT_SAFEARRAY
VT_CARRAY
VT_USERDEFINED
VT_LPSTR
VT_LPWSTR
VT_RECORD = &H24
VT_INT_PTR
VT_UINT_PTR
VT_ARRAY = &H2000
VT_BYREF = &H4000
End Enum
Public Function GetDims(VarSafeArray As Variant) As Integer
Dim varArray As ARRAY_VARIANT
Dim lpSAFEARRAY As Long
Dim sArr As SAFEARRAY
''Inspect the Variant
CopyMemory VarPtr(varArray.vt), VarPtr(VarSafeArray), 16&
''If the Variant is pointing to an array...
If varArray.vt And (VARENUM.VT_ARRAY Or VARENUM.VT_BYREF) Then
''Get the pointer to the SAFEARRAY from the Variant
CopyMemory VarPtr(lpSAFEARRAY), varArray.lpSAFEARRAY, 4&
''If the pointer is not Null
If Not lpSAFEARRAY = 0 Then
''Read the array dimensions from the SAFEARRAY
CopyMemory VarPtr(sArr), lpSAFEARRAY, LenB(sArr)
''and return them
GetDims = sArr.cDims
Else
''The array is uninitialized
GetDims = 0
End If
Else
''Not an array, you could choose to raise an error here
GetDims = 0
End If
End Function
Para devolver el número de dimensiones sin errores de deglución:
Private Declare PtrSafe Sub memcpy Lib "kernel32" Alias "RtlMoveMemory" ( _
ByRef dest As Any, ByVal src As LongPtr, ByVal size As LongPtr)
Public Function GetDimensions(source As Variant) As Integer
Dim vt As Long, ptr As LongPtr
memcpy vt, VarPtr(source), 2 '' read the variant type (2 bytes) ''
If (vt And &H2000) = 0 Then Exit Function '' return 0 if not an array ''
memcpy ptr, VarPtr(source) + 8, Len(ptr) '' read the variant data at offset 8 ''
If (vt And &H4000) Then memcpy ptr, ptr, Len(ptr) '' read by reference if the data is a reference ''
If ptr Then memcpy GetDimensions, ptr, 2 '' read the number of dimensions at offset 0 (2 bytes) ''
End Function
Uso:
Sub Examples()
Dim list1
Debug.Print GetDimensions(list1) '' >> 0 ''
list1 = Array(1, 2, 3, 4)
Debug.Print GetDimensions(list1) '' >> 1 ''
Dim list2()
Debug.Print GetDimensions(list2) '' >> 0 ''
ReDim list2(2)
Debug.Print GetDimensions(list2) '' >> 1 ''
ReDim list2(2, 2)
Debug.Print GetDimensions(list2) '' >> 2 ''
End Sub
Para las matrices, MS tiene un buen método que involucra el bucle hasta que ocurre un error.
"Esta rutina prueba la matriz denominada Xarray probando el LBound de cada dimensión. Usando un ciclo For ... Next, la rutina recorre el número de posibles dimensiones de la matriz, hasta 60000, hasta que se genera un error. toma el contrapasado en el que falló el ciclo, resta uno (porque el anterior fue el último sin error) y muestra el resultado en un cuadro de mensaje .... "
http://support.microsoft.com/kb/152288
Versión limpiada del código (decidió escribir como una función, no como sub):
Function NumberOfDimensions(ByVal vArray As Variant) As Long
Dim dimnum As Long
On Error GoTo FinalDimension
For dimnum = 1 To 60000
ErrorCheck = LBound(vArray, dimnum)
Next
FinalDimension:
NumberOfDimensions = dimnum - 1
End Function
Supongo que quiere decir sin usar On Error Resume Next que a la mayoría de los programadores no le gusta y que también significa que durante la depuración no puede usar ''Break On All Errors'' para que el código se detenga (Tools-> Options-> General-> Error Trapping-> Break on All Errors).
Para mí, una solución es enterrar cualquier Curriculum en caso de error siguiente en una DLL compilada, en los viejos tiempos esto habría sido VB6. Hoy podrías usar VB.NET pero elijo usar C #.
Si Visual Studio está disponible para usted, aquí hay alguna fuente. Devolverá un diccionario, el Dicitionary.Count devolverá el número de dimensiones. Los elementos también contendrán LBound y UBound como una cadena concatenada. Siempre estoy consultando una matriz no solo por sus dimensiones, sino también por LBound y UBound de esas dimensiones, así que las reúno y devuelvo todo un paquete de información en un diccionario de scripts.
Aquí está la fuente de C #, inicie una Biblioteca de clases llamándola BuryVBAErrorsCS, establezca ComVisible (verdadero) y añada una referencia a la biblioteca COM ''Microsoft Scripting Runtime'', Register for Interop.
using Microsoft.VisualBasic;
using System;
using System.Runtime.InteropServices;
namespace BuryVBAErrorsCS
{
// Requires adding a reference to COM library Microsoft Scripting Runtime
// In AssemblyInfo.cs set ComVisible(true);
// In Build tab check ''Register for Interop''
public interface IDimensionsAndBounds
{
Scripting.Dictionary DimsAndBounds(Object v);
}
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IDimensionsAndBounds))]
public class CDimensionsAndBounds : IDimensionsAndBounds
{
public Scripting.Dictionary DimsAndBounds(Object v)
{
Scripting.Dictionary dicDimsAndBounds;
dicDimsAndBounds = new Scripting.Dictionary();
try
{
for (Int32 lDimensionLoop = 1; lDimensionLoop < 30; lDimensionLoop++)
{
long vLBound = Information.LBound((Array)v, lDimensionLoop);
long vUBound = Information.UBound((Array)v, lDimensionLoop);
string concat = (string)vLBound.ToString() + " " + (string)vUBound.ToString();
dicDimsAndBounds.Add(lDimensionLoop, concat);
}
}
catch (Exception)
{
}
return dicDimsAndBounds;
}
}
}
Para el código de VBA del cliente de Excel aquí hay alguna fuente
Sub TestCDimensionsAndBounds()
''* requires Tools->References->BuryVBAErrorsCS.tlb
Dim rng As Excel.Range
Set rng = ThisWorkbook.Worksheets.Item(1).Range("B4:c7")
Dim v As Variant
v = rng.Value2
Dim o As BuryVBAErrorsCS.CDimensionsAndBounds
Set o = New BuryVBAErrorsCS.CDimensionsAndBounds
Dim dic As Scripting.Dictionary
Set dic = o.DimsAndBounds(v)
Debug.Assert dic.Items()(0) = "1 4"
Debug.Assert dic.Items()(1) = "1 2"
Dim s(1 To 2, 2 To 3, 3 To 4, 4 To 5, 5 To 6)
Set dic = o.DimsAndBounds(s)
Debug.Assert dic.Items()(0) = "1 2"
Debug.Assert dic.Items()(1) = "2 3"
Debug.Assert dic.Items()(2) = "3 4"
Debug.Assert dic.Items()(3) = "4 5"
Debug.Assert dic.Items()(4) = "5 6"
Stop
End Sub
NOTA BIEN : Esta respuesta maneja las variantes de la cuadrícula extraídas de una hoja de trabajo con Range.Value así como las matrices creadas en código usando Dim s (1), etc. Algunas de las otras respuestas no hacen esto.
Function ArrayDimension(ByRef ArrayX As Variant) As Byte
Dim i As Integer, a As String, arDim As Byte
On Error Resume Next
i = 0
Do
a = CStr(ArrayX(0, i))
If Err.Number > 0 Then
arDim = i
On Error GoTo 0
Exit Do
Else
i = i + 1
End If
Loop
If arDim = 0 Then arDim = 1
ArrayDimension = arDim
End Function
Function getDimension(var As Variant) As Long
On Error GoTo Err
Dim i As Long
Dim tmp As Long
i = 0
Do While True
i = i + 1
tmp = UBound(var, i)
Loop
Err:
getDimension = i - 1
End Function
Esa es la única forma en que puedo pensar. No es bonito….
En cuanto a MSDN, básicamente hicieron lo mismo.