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