470 lines
14 KiB
QBasic
470 lines
14 KiB
QBasic
![]() |
Attribute VB_Name = "ex_VBA"
|
||
|
' ======== VBA Extensions for objects and messaging ========
|
||
|
' Shared module version: 20220623
|
||
|
' Tested in: TestCommons
|
||
|
' Depends on:
|
||
|
' Required reference: Scripting
|
||
|
Option Private Module
|
||
|
Option Explicit
|
||
|
|
||
|
Public Const VBA_INVALID_SIZE = -1
|
||
|
|
||
|
Public Enum SystemErrors
|
||
|
SYS_ERR_TYPE_MISMATCH = 13
|
||
|
SYS_ERR_INVALID_OPERATION = 17
|
||
|
End Enum
|
||
|
|
||
|
Public Function ArraySize(target As Variant, Optional nDimension% = 1) As Long
|
||
|
On Error GoTo INVALID_SIZE
|
||
|
|
||
|
ArraySize = UBound(target, nDimension) - LBound(target, nDimension) + 1
|
||
|
Exit Function
|
||
|
|
||
|
INVALID_SIZE:
|
||
|
ArraySize = VBA_INVALID_SIZE
|
||
|
End Function
|
||
|
|
||
|
' Returns dictionary of indicies of first occurence for every element in array @aSource
|
||
|
' Note: duplicate elements are ignored
|
||
|
Public Function ArrayToIndex(iSource As Variant) As Scripting.Dictionary
|
||
|
If Not IsArrayAllocated(iSource) Then _
|
||
|
Exit Function
|
||
|
|
||
|
Dim indicies As New Scripting.Dictionary
|
||
|
Dim vValue As Variant
|
||
|
Dim nIndex&: nIndex = LBound(iSource, 1)
|
||
|
For Each vValue In iSource
|
||
|
If Not indicies.Exists(vValue) Then _
|
||
|
Call indicies.Add(vValue, nIndex)
|
||
|
nIndex = nIndex + 1
|
||
|
Next vValue
|
||
|
Set ArrayToIndex = indicies
|
||
|
End Function
|
||
|
|
||
|
Public Function IsArrayAllocated(target As Variant) As Boolean
|
||
|
IsArrayAllocated = False
|
||
|
If Not IsArray(target) Then _
|
||
|
Exit Function
|
||
|
|
||
|
On Error Resume Next
|
||
|
Dim nSize&: nSize = UBound(target, 1)
|
||
|
If Err.Number <> 0 Then
|
||
|
Call Err.Clear
|
||
|
Exit Function
|
||
|
End If
|
||
|
On Error GoTo 0
|
||
|
|
||
|
IsArrayAllocated = LBound(target) <= UBound(target)
|
||
|
End Function
|
||
|
|
||
|
Public Function FixForwardedParams(params As Variant) As Variant
|
||
|
FixForwardedParams = params
|
||
|
If IsArrayAllocated(params) Then _
|
||
|
If VBA.IsArray(params(0)) Then _
|
||
|
If ArraySize(params, 1) = 1 Then _
|
||
|
FixForwardedParams = params(0)
|
||
|
End Function
|
||
|
|
||
|
Public Function CColl(ParamArray params() As Variant) As Collection
|
||
|
Set CColl = New Collection
|
||
|
Dim val As Variant
|
||
|
For Each val In params
|
||
|
Call CColl.Add(val)
|
||
|
Next val
|
||
|
End Function
|
||
|
|
||
|
Public Function CSet(ParamArray params() As Variant) As Scripting.Dictionary
|
||
|
Set CSet = New Scripting.Dictionary
|
||
|
Dim vValue As Variant
|
||
|
For Each vValue In params
|
||
|
CSet.Item(vValue) = 0
|
||
|
Next vValue
|
||
|
End Function
|
||
|
|
||
|
Public Function CDict(ParamArray params() As Variant) As Scripting.Dictionary
|
||
|
If UBound(params) > 0 Then _
|
||
|
If UBound(params) Mod 2 <> 1 Then _
|
||
|
Call Err.Raise(vbObjectError, Description:="Dictionary params should be key and value pairs")
|
||
|
|
||
|
Set CDict = New Scripting.Dictionary
|
||
|
If UBound(params) <= 0 Then _
|
||
|
Exit Function
|
||
|
Dim nItem&
|
||
|
For nItem = 0 To UBound(params) / 2 Step 1
|
||
|
Call CDict.Add(params(2 * nItem), params(2 * nItem + 1))
|
||
|
Next nItem
|
||
|
End Function
|
||
|
|
||
|
Public Function CollectionToDictionary(iSource As Collection) As Scripting.Dictionary
|
||
|
Dim iDict As New Scripting.Dictionary
|
||
|
Dim nIndex&: nIndex = 1
|
||
|
Dim vValue As Variant
|
||
|
For Each vValue In iSource
|
||
|
If Not iDict.Exists(vValue) Then _
|
||
|
Call iDict.Add(vValue, nIndex)
|
||
|
nIndex = nIndex + 1
|
||
|
Next vValue
|
||
|
Set CollectionToDictionary = iDict
|
||
|
End Function
|
||
|
|
||
|
' Add unique (key, value) pairs from Source to Destination without deep copy
|
||
|
Public Function DictionaryAbsorbShallow(iDestination As Scripting.Dictionary, iSource As Scripting.Dictionary)
|
||
|
Dim aKey As Variant
|
||
|
For Each aKey In iSource
|
||
|
If Not iDestination.Exists(aKey) Then _
|
||
|
Call iDestination.Add(aKey, iSource(aKey))
|
||
|
Next aKey
|
||
|
End Function
|
||
|
|
||
|
' Convert paramarray into unique list, where value equals 1-based index into input array
|
||
|
' Duplicates are skipped
|
||
|
Public Function UniqueList(ParamArray params() As Variant) As Scripting.Dictionary
|
||
|
Set UniqueList = New Scripting.Dictionary
|
||
|
Dim vValue As Variant
|
||
|
Dim nIndex&: nIndex = 1
|
||
|
For Each vValue In params
|
||
|
If Not UniqueList.Exists(vValue) Then _
|
||
|
Call UniqueList.Add(vValue, nIndex)
|
||
|
nIndex = nIndex + 1
|
||
|
Next vValue
|
||
|
End Function
|
||
|
|
||
|
' Compare two floats / integers and return difference or 0 if Precision is not met
|
||
|
' Note: positive precision indicates number of positions after decimal
|
||
|
' while negative precision indicates positions before decimal
|
||
|
Public Function CompareApproximate(val1 As Variant, val2 As Variant, nDecimalPrecision&) As Double
|
||
|
CompareApproximate = val1 - val2
|
||
|
If CompareApproximate = 0 Then _
|
||
|
Exit Function
|
||
|
If VBA.Log(VBA.Abs(CompareApproximate)) / VBA.Log(10) < -nDecimalPrecision Then _
|
||
|
CompareApproximate = 0
|
||
|
End Function
|
||
|
|
||
|
' Compate two Variants
|
||
|
' Integer values will resolve into their difference
|
||
|
' Other types will return 0 if objects are equal and unspecified number otherwise
|
||
|
' Object type will try to call Compare() function if it is available for the type
|
||
|
' otherwise objects will by compared using ObjPtr()
|
||
|
' If there is no way to compare values the Error SYS_ERR_INVALID_OPERATION will be raised
|
||
|
' Warning: multidimensional arrays are not supported!
|
||
|
Public Function CompareDeep(val1 As Variant, val2 As Variant) As Double
|
||
|
If TryMinus(val1, val2, CompareDeep) Then
|
||
|
Exit Function
|
||
|
ElseIf TryEquality(val1, val2) Then
|
||
|
CompareDeep = 0
|
||
|
Exit Function
|
||
|
ElseIf TryInequality(val1, val2) Then
|
||
|
CompareDeep = 1
|
||
|
Exit Function
|
||
|
End If
|
||
|
|
||
|
Dim vType As VBA.VbVarType: vType = VBA.VarType(val1)
|
||
|
If vType <> VBA.VarType(val2) Then
|
||
|
GoTo UNCOMPARABLE
|
||
|
ElseIf vType >= vbArray And vType <= vbArray + vbByte Then
|
||
|
CompareDeep = CompareDeepArray(val1, val2)
|
||
|
Exit Function
|
||
|
End If
|
||
|
|
||
|
Dim sType$: sType = VBA.TypeName(val1)
|
||
|
If sType <> VBA.TypeName(val2) Then _
|
||
|
GoTo UNCOMPARABLE
|
||
|
|
||
|
On Error GoTo UNCOMPARABLE
|
||
|
Select Case sType
|
||
|
Case "Collection": CompareDeep = CompareDeepCollection(val1, val2)
|
||
|
Case "Dictionary": CompareDeep = CompareDeepDictionary(val1, val2)
|
||
|
Case Else: CompareDeep = CompareDeepObjects(val1, val2)
|
||
|
End Select
|
||
|
Exit Function
|
||
|
|
||
|
UNCOMPARABLE:
|
||
|
Call Err.Raise(SYS_ERR_INVALID_OPERATION)
|
||
|
End Function
|
||
|
|
||
|
Public Function ExtractTimestamp(sLine$) As Double
|
||
|
Dim sStamp$: sStamp = ExtractDatePrefix(sLine)
|
||
|
If sStamp = vbNullString Then _
|
||
|
Exit Function
|
||
|
|
||
|
On Error GoTo INVALID_FORMAT
|
||
|
ExtractTimestamp = DateValue(sStamp) + TimeValue(sStamp)
|
||
|
Exit Function
|
||
|
|
||
|
INVALID_FORMAT:
|
||
|
Call Err.Clear
|
||
|
ExtractTimestamp = 0
|
||
|
End Function
|
||
|
|
||
|
Public Function TrimTimestamp(sLine$) As String
|
||
|
TrimTimestamp = sLine
|
||
|
Dim sStamp$: sStamp = ExtractDatePrefix(sLine)
|
||
|
If sStamp = vbNullString Then _
|
||
|
Exit Function
|
||
|
If ExtractTimestamp(sStamp) = 0 Then _
|
||
|
Exit Function
|
||
|
|
||
|
If VBA.Len(sLine) > VBA.Len(sStamp) + 2 Then
|
||
|
TrimTimestamp = VBA.Right(sLine, VBA.Len(sLine) - VBA.Len(sStamp) - 2)
|
||
|
Else
|
||
|
TrimTimestamp = vbNullString
|
||
|
End If
|
||
|
End Function
|
||
|
|
||
|
Public Function CommonPrefixLength(sText1$, sText2$, Optional nMode As VbCompareMethod = vbBinaryCompare) As Long
|
||
|
Dim nPrefixEnd&: nPrefixEnd = 0
|
||
|
Dim nLen&: nLen = VBA.Len(sText1)
|
||
|
nLen = IIf(nLen < VBA.Len(sText2), VBA.Len(sText2), nLen)
|
||
|
Do While nPrefixEnd < nLen
|
||
|
If VBA.StrComp(VBA.Mid(sText1, nPrefixEnd + 1, 1), _
|
||
|
VBA.Mid(sText2, nPrefixEnd + 1, 1), nMode) <> 0 Then _
|
||
|
Exit Do
|
||
|
nPrefixEnd = nPrefixEnd + 1
|
||
|
Loop
|
||
|
CommonPrefixLength = nPrefixEnd
|
||
|
End Function
|
||
|
|
||
|
' Formatting function
|
||
|
' Expected message format: "test {1} argument enumerating 1-based"
|
||
|
' Note: params array is 0-base enumerated
|
||
|
Public Function Fmt(sMsg$, ParamArray params() As Variant) As String
|
||
|
Dim vValue As Variant
|
||
|
Dim nIndex&: nIndex = 1
|
||
|
Fmt = sMsg
|
||
|
|
||
|
Dim unwrapped As Variant: unwrapped = params
|
||
|
unwrapped = FixForwardedParams(unwrapped)
|
||
|
For Each vValue In unwrapped
|
||
|
Fmt = VBA.Replace(Fmt, "{" & nIndex & "}", PrettyPrint(vValue))
|
||
|
nIndex = nIndex + 1
|
||
|
Next vValue
|
||
|
End Function
|
||
|
|
||
|
' Warning: multidimensional arrays are not supported!
|
||
|
Public Function PrettyPrint(vValue As Variant, Optional nIndent& = 0) As String
|
||
|
Select Case VBA.VarType(vValue)
|
||
|
Case VBA.vbEmpty: PrettyPrint = "[Empty]"
|
||
|
Case VBA.vbNull: PrettyPrint = "[Null]"
|
||
|
Case VBA.vbString: PrettyPrint = vValue
|
||
|
Case VBA.vbArray To VBA.vbArray + VBA.vbByte: PrettyPrint = PrintArray(vValue, nIndent)
|
||
|
Case VBA.vbObject
|
||
|
If vValue Is Nothing Then
|
||
|
PrettyPrint = "[Nothing]"
|
||
|
ElseIf VBA.TypeName(vValue) = "Collection" Then
|
||
|
PrettyPrint = PrintCollection(vValue, nIndent)
|
||
|
ElseIf VBA.TypeName(vValue) = "Dictionary" Then
|
||
|
PrettyPrint = PrintDictionary(vValue, nIndent)
|
||
|
Else
|
||
|
PrettyPrint = PrintCustomObject(vValue, nIndent)
|
||
|
End If
|
||
|
Case Else: PrettyPrint = Var2Str(vValue)
|
||
|
End Select
|
||
|
End Function
|
||
|
|
||
|
Public Function Indent(Optional nIndent&)
|
||
|
Indent = VBA.String$(nIndent, " ")
|
||
|
End Function
|
||
|
|
||
|
' ==========
|
||
|
Private Function ExtractDatePrefix(sLine$) As String
|
||
|
If sLine = vbNullString Then _
|
||
|
Exit Function
|
||
|
Dim nEnd&: nEnd = VBA.InStr(1, sLine, " ")
|
||
|
If nEnd <> 0 Then _
|
||
|
nEnd = VBA.InStr(nEnd + 1, sLine, " ")
|
||
|
If nEnd = 0 Then
|
||
|
nEnd = VBA.Len(sLine)
|
||
|
Else
|
||
|
nEnd = nEnd - 1
|
||
|
End If
|
||
|
|
||
|
If VBA.Mid(sLine, nEnd, 1) = ":" Then _
|
||
|
nEnd = nEnd - 1
|
||
|
ExtractDatePrefix = VBA.Left(sLine, nEnd)
|
||
|
End Function
|
||
|
|
||
|
Private Function Var2Str(target As Variant) As String
|
||
|
On Error GoTo CANNOT_CONVERT
|
||
|
Var2Str = CStr(target)
|
||
|
Exit Function
|
||
|
|
||
|
CANNOT_CONVERT:
|
||
|
Var2Str = "[T" & VarType(target) & "] "
|
||
|
On Error GoTo NON_STRING
|
||
|
If target Is Nothing Then
|
||
|
Var2Str = Var2Str & "Nothing"
|
||
|
Exit Function
|
||
|
End If
|
||
|
|
||
|
NON_STRING:
|
||
|
Var2Str = Var2Str & "non-stringable"
|
||
|
End Function
|
||
|
|
||
|
Private Function TryEquality(val1 As Variant, val2 As Variant) As Boolean
|
||
|
On Error Resume Next
|
||
|
TryEquality = val1 = val2
|
||
|
End Function
|
||
|
|
||
|
Private Function TryInequality(val1 As Variant, val2 As Variant) As Boolean
|
||
|
On Error Resume Next
|
||
|
TryInequality = val1 <> val2
|
||
|
End Function
|
||
|
|
||
|
Private Function TryMinus(val1 As Variant, val2 As Variant, ByRef dDiff As Double) As Boolean
|
||
|
On Error GoTo CANNOT_DIFF
|
||
|
|
||
|
dDiff = val1 - val2
|
||
|
TryMinus = True
|
||
|
Exit Function
|
||
|
|
||
|
CANNOT_DIFF:
|
||
|
TryMinus = False
|
||
|
End Function
|
||
|
|
||
|
Private Function CompareDeepArray(val1 As Variant, val2 As Variant) As Double
|
||
|
If Not IsArrayAllocated(val1) Then
|
||
|
CompareDeepArray = IIf(IsArrayAllocated(val2), 1, 0)
|
||
|
Exit Function
|
||
|
End If
|
||
|
|
||
|
CompareDeepArray = UBound(val1) - UBound(val2)
|
||
|
If CompareDeepArray <> 0 Then _
|
||
|
Exit Function
|
||
|
CompareDeepArray = LBound(val1) - LBound(val2)
|
||
|
If CompareDeepArray <> 0 Then _
|
||
|
Exit Function
|
||
|
|
||
|
On Error GoTo UNCOMPARABLE_ELEMENTS
|
||
|
Dim nItem&
|
||
|
For nItem = LBound(val1) To UBound(val2) Step 1
|
||
|
If CompareDeep(val1(nItem), val2(nItem)) <> 0 Then
|
||
|
CompareDeepArray = 1
|
||
|
Exit Function
|
||
|
End If
|
||
|
Next nItem
|
||
|
|
||
|
CompareDeepArray = 0
|
||
|
Exit Function
|
||
|
|
||
|
UNCOMPARABLE_ELEMENTS:
|
||
|
CompareDeepArray = 1
|
||
|
End Function
|
||
|
|
||
|
Private Function CompareDeepCollection(val1 As Variant, val2 As Variant) As Double
|
||
|
Dim nCount&: nCount = val1.Count
|
||
|
CompareDeepCollection = nCount - val2.Count
|
||
|
If CompareDeepCollection <> 0 Then _
|
||
|
Exit Function
|
||
|
|
||
|
On Error GoTo UNCOMPARABLE_ELEMENTS
|
||
|
Dim nItem&
|
||
|
For nItem = 1 To nCount Step 1
|
||
|
If CompareDeep(val1(nItem), val2(nItem)) <> 0 Then
|
||
|
CompareDeepCollection = 1
|
||
|
Exit Function
|
||
|
End If
|
||
|
Next nItem
|
||
|
|
||
|
CompareDeepCollection = 0
|
||
|
Exit Function
|
||
|
|
||
|
UNCOMPARABLE_ELEMENTS:
|
||
|
CompareDeepCollection = 1
|
||
|
End Function
|
||
|
|
||
|
Private Function CompareDeepDictionary(val1 As Variant, val2 As Variant) As Double
|
||
|
Dim nCount&: nCount = val1.Count
|
||
|
CompareDeepDictionary = nCount - val2.Count
|
||
|
If CompareDeepDictionary <> 0 Then _
|
||
|
Exit Function
|
||
|
|
||
|
On Error GoTo UNCOMPARABLE_ELEMENTS
|
||
|
Dim vKey As Variant
|
||
|
For Each vKey In val1
|
||
|
If Not val2.Exists(vKey) Then _
|
||
|
GoTo UNCOMPARABLE_ELEMENTS
|
||
|
If CompareDeep(val1(vKey), val2(vKey)) <> 0 Then
|
||
|
CompareDeepDictionary = 1
|
||
|
Exit Function
|
||
|
End If
|
||
|
Next vKey
|
||
|
|
||
|
CompareDeepDictionary = 0
|
||
|
Exit Function
|
||
|
|
||
|
UNCOMPARABLE_ELEMENTS:
|
||
|
CompareDeepDictionary = 1
|
||
|
End Function
|
||
|
|
||
|
Public Function CompareDeepObjects(val1 As Variant, val2 As Variant) As Double
|
||
|
On Error Resume Next
|
||
|
CompareDeepObjects = val1.Compare(val2)
|
||
|
If Err.Number = 0 Then _
|
||
|
Exit Function
|
||
|
Call Err.Clear
|
||
|
CompareDeepObjects = IIf(ObjPtr(val1) = ObjPtr(val2), 0, 1)
|
||
|
End Function
|
||
|
|
||
|
Private Function PrintCollection(target As Variant, nIndent&) As String
|
||
|
If target.Count = 0 Then
|
||
|
PrintCollection = "$Collection []"
|
||
|
Exit Function
|
||
|
End If
|
||
|
|
||
|
PrintCollection = "$Collection [" & vbNewLine
|
||
|
Dim nItem&: nItem = 0
|
||
|
Dim anItem As Variant
|
||
|
For Each anItem In target
|
||
|
nItem = nItem + 1
|
||
|
PrintCollection = PrintCollection & _
|
||
|
Indent(nIndent + 1) & PrettyPrint(anItem, nIndent + 1) & _
|
||
|
IIf(nItem <> target.Count, ",", "") & vbNewLine
|
||
|
Next anItem
|
||
|
PrintCollection = PrintCollection & Indent(nIndent) & "]"
|
||
|
End Function
|
||
|
|
||
|
Private Function PrintDictionary(target As Variant, nIndent&) As String
|
||
|
If target.Count = 0 Then
|
||
|
PrintDictionary = "$Dictionary []"
|
||
|
Exit Function
|
||
|
End If
|
||
|
|
||
|
PrintDictionary = "$Dictionary [" & vbNewLine
|
||
|
Dim nItem&: nItem = 0
|
||
|
Dim aKey As Variant
|
||
|
For Each aKey In target
|
||
|
nItem = nItem + 1
|
||
|
PrintDictionary = PrintDictionary & _
|
||
|
Indent(nIndent + 1) & aKey & ": " & _
|
||
|
PrettyPrint(target(aKey), nIndent + 1) & _
|
||
|
IIf(nItem <> target.Count, ",", "") & vbNewLine
|
||
|
Next aKey
|
||
|
PrintDictionary = PrintDictionary & Indent(nIndent) & "]"
|
||
|
End Function
|
||
|
|
||
|
Private Function PrintCustomObject(vValue As Variant, nIndent&) As String
|
||
|
On Error GoTo PRINT_TYPE
|
||
|
PrintCustomObject = vValue.ToString(nIndent)
|
||
|
Exit Function
|
||
|
PRINT_TYPE:
|
||
|
PrintCustomObject = "[" & VBA.TypeName(vValue) & "]"
|
||
|
End Function
|
||
|
|
||
|
Private Function PrintArray(target As Variant, nIndent&) As String
|
||
|
If Not IsArrayAllocated(target) Then
|
||
|
PrintArray = "$Array []"
|
||
|
Exit Function
|
||
|
End If
|
||
|
|
||
|
PrintArray = "$Array [" & vbNewLine
|
||
|
Dim nItem&
|
||
|
Dim nLast&: nLast = UBound(target, 1)
|
||
|
For nItem = LBound(target, 1) To nLast
|
||
|
PrintArray = PrintArray & _
|
||
|
Indent(nIndent + 1) & PrettyPrint(target(nItem), nIndent + 1) & _
|
||
|
IIf(nItem <> nLast, ",", "") & vbNewLine
|
||
|
Next nItem
|
||
|
PrintArray = PrintArray & Indent(nIndent) & "]"
|
||
|
End Function
|
||
|
|