Extend vlookup to calculate cost of goods - excel

I have sales report from e-shop and need to calculate cost of goods for each order line. Order line can look like one of these:
2x Lavazza Crema e Aroma 1kg - 1x Lavazza Dolce Caffe Crema 1kg
1x Lavazza Vending Aroma Top 1kg - 1x Arcaffe Roma 1Kg - 1x Kimbo - 100% Arabica Top Flavour
So, what I need Excel to do is to take each product, find its cost with vlookup function from another sheet and then multiply it with amount ordered. The issue is that nr of products ordered can vary from 1 to 10+.
I tried to calculate it with VBA, but the code is not working (I didn´t use multiplying at the moment, I know)
Maybe it is possible to solve this problem with excel formulas?
Function GoodsCost(str, Optional strDelim As String = " ")
larray = Split(str, strDelim)
Set lookup_range = Worksheets("Products").Range("B:E")
For i = LBound(larray) To UBound(larray)
skuarray = Split(larray(i), "x ")
skucost = Application.WorksheetFunction.VLookup(UBound(skuarray), lookup_range, 4, False)
cost = cost + skucost
Next i
GoodsCost = cost
End Function

Well, it seems like now the problem is solved. Of course, it works only if make an assumption that dashes(-) are not present in product descriptions. But it can be set up in product list. The other opportunity is to use another delimeter (for example "/"). We can use Ctrl+F to find all combinations like "x -" and replace them with "x /")
Function GoodsCost(str)
Dim answer As Double
Set Products = Worksheets("Products").Range("B:E")
larray = Split(str, " - ")
For i = LBound(larray) To UBound(larray)
sku = Split(larray(i), "x ")
Price = Application.WorksheetFunction.VLookup(sku(1), Products, 4, False) * sku(0)
answer = answer + Price
Next i
GoodsCost = answer
End Function

Below you find a UDF (User Defined Function) which you can use in your worksheet. After installing it in a standard code module (VBE names these like "Module1") you can call it from the worksheet like =CostOfGoods($A2) where A2 is the cell containing and order line as you have described.
Option Explicit
Function CostOfGoods(Cell As Range) As Single
' 15 Jan 2018
Const Delim As String = " - "
Dim Fun As Single ' function return value
Dim Sale As Variant
Dim Sp() As String
Dim i As Long
Dim PriceList As Range
Dim Qty As Single, Price As Single
Dim n As Integer
Sale = Trim(Cell.Value)
If Len(Sale) Then
Sp = Split(Sale, Delim)
Do While i <= UBound(Sp)
If InStr(Sp(i), "x ") = 0 Then
If Not ConcatSale(Sp, i, Delim) Then Exit Do
End If
i = i + 1
Loop
With Worksheets("Products")
i = .Cells(.Rows.Count, "B").End(xlUp).Row
' price list starts in row 2 (change as required)
Set PriceList = Range(.Cells(2, "B"), .Cells(i, "E"))
End With
For i = 0 To UBound(Sp)
Qty = Val(Sp(i))
n = InStr(Sp(i), " ")
Sp(i) = Trim(Mid(Sp(i), n))
On Error Resume Next
Price = Application.VLookup(Sp(i), PriceList, 4, False)
If Err Then
MsgBox "I couldn't find the price for" & vbCr & _
Sp(i) & "." & vbCr & _
"The total cost calculated excludes this item.", _
vbInformation, "Price not found"
Price = 0
End If
Fun = Fun + (Qty * Price)
Next i
End If
CostOfGoods = Fun
End Function
Private Function ConcatSale(Sale() As String, _
i As Long, _
Delim As String) As Boolean
' 15 Jan 2018
Dim Fun As Boolean ' function return value
Dim x As Long, f As Long
x = UBound(Sale)
If (i > 0) And (i <= x) Then
i = i - 1
Sale(i) = Sale(i) & Delim & Sale(i + 1)
For f = i + 1 To x - 1
Sale(f) = Sale(f + 1)
Next f
Fun = True
End If
If Fun Then ReDim Preserve Sale(x - 1)
ConcatSale = Fun
End Function

I have tested this and it works with dashes in product description:
Function GoodsCost(str, Optional strDelim As String = " ")
larray = Split(str, " ")
'split the cell contents by space
Set lookup_range = Worksheets("Products").Range("B:E")
'set lookup range
For i = LBound(larray) To UBound(larray) 'loop through array
nextproduct:
LPosition = InStr(larray(i), "x") 'find multiplier "x" in string
If LPosition = Len(larray(i)) Then 'if the last character is x
If Product <> "" Then GoTo lookitup 'lookup product
Quantity = larray(i) 'get quantity
Else
Product = Product & " " & larray(i) 'concatenate array until we get a full product description to lookup with
End If
Next i
lookitup:
If Right(Product, 2) = " -" Then Product = Left(Product, Len(Product) - 2)
If Left(Product, 1) = " " Then Product = Right(Product, Len(Product) - 1)
'above trim the Product description to remove unwanted spaces or dashes
cost = Application.WorksheetFunction.VLookup(Product, lookup_range, 4, False)
Quantity = Replace(Quantity, "x", "")
GoodsCost = cost * Quantity
MsgBox Product & " # Cost: " & GoodsCost
Product = ""
If i < UBound(larray) Then GoTo nextproduct
End Function

I'd use Regular Expressions to solve this. First it finds in the string were the 'delimiters' are by replacing the - with ; detecting only - that are next to a number followed by an x (i.e. a multiplier so ignoring - in product names). It then splits each of these results into a quantity and the product (again using RegEx). It then finds the product in your data and returns the cost of goods. If there is an error, or the product isn't in your data it returns a #Value error to show that there is an issue.
Public Function GoodsCost(str As String) As Double
Dim lookup_range As Range, ProductMatch As Range
Dim v, Match
Dim qty As Long
Dim prod As String
Dim tmp() As String
On Error GoTo err
Set lookup_range = Worksheets("Products").Range("B:E")
With CreateObject("vbscript.regexp")
.Global = True
.ignorecase = True
.pattern = "(\s\-\s)(?=[0-9]+x)"
If .test(str) Then
tmp = Split(.Replace(str, ";"), ";")
Else
ReDim tmp(0)
tmp(0) = str
End If
.pattern = "(?:([0-9]+)x\s(.+))"
For Each v In tmp
If .test(v) Then
Set Match = .Execute(v)
qty = Match.Item(0).submatches.Item(0)
prod = Trim(Match.Item(0).submatches.Item(1))
Set ProductMatch = lookup_range.Columns(1).Find(prod)
If Not ProductMatch Is Nothing Then
GoodsCost = GoodsCost + (qty * ProductMatch.Offset(0, 3))
Else
GoodsCost = CVErr(xlErrValue)
End If
End If
Next v
End With
Exit Function
err:
GoodsCost = CVErr(xlErrValue)
End Function

Related

Converting a string with date in US format to UK format

I am trying to convert a string, which has a date in US format into UK format.
The following code seems to be hit or miss when it comes to a date that is single digits for both the day and the month:
X = 3
Do While strTimeStamp = 0
If InStr(WS2.Cells(lRow, lCol), "TIMESTAMP") <> 0 Then
strHPCStats = Split(WS2.Cells(lRow, lCol), " ")
'strHPCStats(X) = Mid(strHPCStats(X), 4, 6)
re.Pattern = "^(\d{2})(\d{2})(\d{4})$"
strHPCStats(X) = re.Replace(strHPCStats(X), "$3/$2/$1")
strHPCStats(X) = Format$(strHPCStats(X), "dd/mmm/yyyy")
strTimeStamp = strHPCStats(X)
WS2.Cells(lRow, lCol).EntireRow.Delete
lRow = lRow - 1
Else
WS2.Cells(lRow, lCol).EntireRow.Delete
lRow = lRow - 1
End If
lRow = lRow + 1
Loop
The typical string:
4:19:17 (application) TIMESTAMP 3/13/2022
The string where it is having trouble:
5:36:32 (cameo) TIMESTAMP 4/1/2022
d{2} will look for exactly 2 digits, so if your date has a month (or day) with only 1 digit, the regex doesn't match.
If you want to specify 1 or 2 digits, you can for example use d{1,2}, so the statement would be
e.Pattern = "^(\d{1,2})(\d{1,2})(\d{4})$"
Details: How to use Regular Expressions (Regex) in Microsoft Excel both in-cell and loops
There's no need to use regular expressions, given your expected data.
Just look for two slashes in a space-separated string:
Function us2ukDate(S As String) As Date
Dim v, w, x
v = Split(S, " ")
For Each w In v
If (Len(w) - Len(Replace(w, "/", ""))) = 2 Then
x = Split(w, "/")
us2ukDate = DateSerial(x(2), x(0), x(1))
Exit Function
End If
Next w
End Function
testing example
If, instead of just returning the date, you want to change the format within the string, you could do something like:
Sub convertStrings()
Const d1 = "4:19:17 (application) TIMESTAMP 3/13/2022"
Const d2 = "5:36:32 (cameo) TIMESTAMP 4/1/2022"
Dim sParts
sParts = Split(d1, " ")
sParts(UBound(sParts)) = Format(us2ukDate(sParts(UBound(sParts))), "dd-mmm-yyyy")
Debug.Print Join(sParts, " ")
sParts = Split(d2, " ")
sParts(UBound(sParts)) = Format(us2ukDate(sParts(UBound(sParts))), "dd-mmm-yyyy")
Debug.Print Join(sParts, " ")
End Sub

How to turn general data written as fractions into 3 place decimal numbers. Replace " 0." with "."

I'm trying to turn general data written as fractions like 3/4" or 13 7/32" into 3 place decimal numbers such as 0.750 or 13.219.
I have a working table replacement that handles 0 to 1" fractions. It can't handle the mixed numbers like 13 7/32". It leaves me with 13 0.219 which is why I need to replace " 0." with "." to join the 13 and 219 together with a decimal.
We do this data conversion in multiple steps and hand type because Excel tries converting some fractions like 3/4" into a date.
Original data
Resulting data
Sub FractionConvertMTO()
'this section works
For i = 6 To 70
Worksheets("BOM").Range("F6:H48").Select
Selection.Replace what:=Cells(i, 21).Value, Replacement:=Cells(i, 22).Value, _
LookAt:=xlPart, SearchOrder:=xlByRows, MatchCase:=False
Next
'this section doesn't work
For i = 6 To 70
Worksheets("BOM").Range("F6:H48").Select
str1 = " "
str1 = Trim(Replace(str1, " ", "+"))
Next
'this section changes the format.
For i = 66 To 130
Range("F6:H48").NumberFormat = "0.000"
Next
'this section is supposed to add an = sign in front of the cell contents but doesn't work.
Dim Cell As Range
For Each Cell In Range("F6:H48")
Cell.Value = "=" & Cell.Value
Next Cell
'this section works to highlight the first cell
Worksheets("BOM").Cells(1, 1).Select
End Sub
I dug up the following method from my library of useful functions. It converts numbers represented as a fractional string to the numeric equivalent. Simply loop through the cells needing conversion and call this method:
Public Function FractionToNumber(ByVal Value As String, Optional ByVal Digits As Long = 0) As Double
Dim P As Integer
Dim N As Double
Dim Num As Double
Dim Den As Double
Value = Trim$(Value)
P = InStr(Value, "/")
If P = 0 Then
N = Val(Value)
Else
Den = Val(Mid$(Value, P + 1))
Value = Trim$(Left$(Value, P - 1))
P = InStr(Value, " ")
If P = 0 Then
Num = Val(Value)
Else
Num = Val(Mid$(Value, P + 1))
N = Val(Left$(Value, P - 1))
End If
End If
If Den <> 0 Then N = N + Num / Den
FractionToNumber = Round(N, Digits)
End Function
You may also code something like the following:
Sub FractionConvertMTO()
Dim rng As Range
Dim Arr As Variant
Arr = Worksheets("MTO").Range("F6:H48")
For Row = 1 To UBound(Arr, 1)
For col = 1 To UBound(Arr, 2)
str1 = Arr(Row, col)
pos1 = InStr(str1, " ")
pos2 = InStr(str1, "/")
If pos2 = 0 Then
N = val(str1)
Num = 0: Den = 1
Else
If pos1 And pos1 < pos2 Then
N = val(Left$(str1, pos1 - 1))
Num = val(Mid$(str1, pos1 + 1))
Else
N = 0
Num = val(Left$(str1, pos2 - 1))
End If
Den = val(Mid$(str1, pos2 + 1))
End If
Arr(Row, col) = N + Num / Den
Next col
Next Row
Worksheets("MTO").Range("F6", "H48") = Arr
End Sub
If you dispose of the newer dynamic array features (vers. 2019+,MS365) you might write the results in one go to the entire original range (target range) as follows (overwriting the existing range; otherwise define a given offset to identify another target range: rng.Offset(,n)=..).
Tip: make a backup copy before testing (as it overwrites rng)!
Note that this example assumes the " character (asc value of 34).
A) First try via tabular VALUE() formula evaluation
Caveat: converting blanks by VALUE() would be written as #VALUE! results, which would need a further loop. To avoid this you can prefix a zero to the formulae myFormula = "=VALUE(SUBSTITUTE(" & """0""&" & rng.Address & ","""""""",""""))" so that results would be displayed as zero.
Sub ChangeToFractionValues()
'1) define original range to be replaced
Dim rng As Range
Set rng = ThisWorkbook.Worksheets("BOM").Range("F6:H48")
'2) define tabular formula
Dim myFormula As String
'myFormula = "=VALUE(SUBSTITUTE(" & rng.Address & ","""""""",""""))"
'Alternative to avoid #VALUE! displays for blanks:
myFormula = "=VALUE(SUBSTITUTE(" & """0""&" & rng.Address & ","""""""",""""))"
'Debug.Print myFormula
'3) overwrite original range (otherwise code an offset rng.Offset(,n).Value = ...
rng.Value2 = rng.Parent.Evaluate(myFormula)
End Sub
Conclusion due to comment:
Though fast, this approach has a big disadvantage: Excel interpretes date-like numbers as such, transforms them internally to dates by returning the numeric part here, so a cell input of 3/4" would return the corresponding date value of the current year for March 4th.
B) Reworked code based on direct cell evaluations in a loop //Edit
Similar to the above processing this approach is also based on evaluation, but collects all formulae as strings in a variant datafield array v, which allows to manipulate and evaluate each cell input individually:
Sub ChangeToFractionValues()
'1) define original range to be replaced
Dim rng As Range
Set rng = ThisWorkbook.Worksheets("BOM").Range("F6:H48")
'2) assign formula strings to variant 1-based 2-dim data field array
Dim v As Variant
v = rng.Formula2
'3) evaluate results in a loop
Dim i As Long, j As Long
For i = 1 To UBound(v)
For j = 1 To UBound(v, 2)
v(i, j) = Evaluate("0" & Replace(v(i, j), Chr(34), ""))
Next j
Next i
'4) overwrite original range (otherwise code an offset rng.Offset(,n).Value = ...
rng.Value = v
End Sub
str1 = trim(Replace(str1, "0.", "."))

Traverse thru array elements and extract delimited values when condition is satisfied

I want to loop thru an array and extract its delimited values that match every date in a range. For e.g., in the picture below:
I have a date range, say 01-01 to 01-10.
I also have a list of strings (see second pic).
In the array below (see first pic), I have three different values delimited by a semi-colon.
For all matching strings (from second pic) e.g., SISBTXTRPR-(number) and date, I want to extract the last part of the array value.
Picture 1
Picture 2
So, for all array values that match "SISBTXTRPR-4649" (the string from picture 2) and a date (in this case 12-12), I want to extract "2h" from the array. The date range for each string, in this case, "SISBTXTRPR-4649" will be 10 days. I am racking my brain on how to do this :(
This is all I could come up with so far:
While i < UBound(sTimeStamp)
If StrComp(Trim(Format(Now(), "MM-DD")), Trim(Split(sTimeStamp(9), ";")(1))) = 0 And StrComp(Trim(Worksheets("KPIs").Range("AN" & iCounter)), Trim(Split(sTimeStamp(1), ";")(0))) Then
End If
i = i + 1
Wend
Link to file
Sample File
The next code will return occurrences for each string in 'Task' range matching the date from its corresponding 'sTimeStamp Array' string with the one from the 'Date Range Array'. Each occurrence will be add to the next column of 'Task' string column:
Private Sub findOccurrences()
Dim sTask As Worksheet, sStamp As Worksheet, sDate As Worksheet
Dim arrTask As Variant, arrStamp As Variant, arrDate As Variant
Dim i As Long, j As Long, arrS As Variant, El As Variant, dtRef As Date
Set sTask = ThisWorkbook.Sheets("Task")
Set sStamp = ThisWorkbook.Sheets("sTimeStamp Array")
Set sDate = ThisWorkbook.Sheets("Date Range Array")
arrTask = sTask.Range("A2:A" & sTask.Range("A" & sTask.Rows.Count).End(xlUp).Row).Value
arrStamp = sStamp.Range("A2:A" & sStamp.Range("A" & sStamp.Rows.Count).End(xlUp).Row).Value
arrDate = sDate.Range("A2:A" & sDate.Range("A" & sDate.Rows.Count).End(xlUp).Row).Value
'____________________________________________________________________________
sTask.Range("B2:K" & sTask.Range("A" & sTask.Rows.Count).End(xlUp).Row).Clear
Do While i < UBound(arrStamp)
i = i + 1
arrS = Split(arrStamp(i, 1), ";")
For j = 1 To UBound(arrTask)
If arrS(0) = arrTask(j, 1) Then
For Each El In arrDate
dtRef = DateValue(Format(El, "MM-DD"))
If dtRef = DateValue(Format(arrS(1), "MM-DD")) Then
Debug.Print arrS(0) & " (row number " & j + 1 & "), interval """ & _
El & """ exists."
sTask.Cells(j + 1, sTask.Cells(j + 1, _
sTask.Columns.Count).End(xlToLeft).Column).Offset(0, 1).Value = El
End If
Next
End If
Next j
Loop
End Sub
And the short variant working similar to your approach, finding the occurrences for Today date (if I correctly deduced what you intended to achieve), replace the looping part with this:
'______________________________________________________________________________
sStamp.Range("B2:B" & sStamp.Range("A" & sStamp.Rows.Count).End(xlUp).Row).Clear
sTask.Range("A2:A" & sTask.Range("A" & sStamp.Rows.Count).End(xlUp).Row).Interior.ColorIndex = -4142
While i < UBound(arrStamp)
i = i + 1
If StrComp(DateValue(Format(Date, "MM-DD")), DateValue(Split(arrStamp(i, 1), ";")(1))) = 0 And _
Not isMatchErr(CStr(Split(arrStamp(i, 1), ";")(1)), arrDate) Then
Debug.Print "OK for """ & Split(arrStamp(i, 1), ";")(0) & """ of row """ & i & """."
sStamp.Range("B" & i + 1).Value = "OK"
If Not isMatchErr(CStr(Split(arrStamp(i, 1), ";")(0)), arrTask) Then
rowOK = WorksheetFunction.Match(Split(arrStamp(i, 1), ";")(0), arrTask, 0) + 1
sTask.Range("A" & rowOK).Interior.ColorIndex = 3
End If
End If
Wend
And add the next function:
Function isMatchErr(strTime As String, arrDate As Variant) As Boolean
Dim k As Long
On Error Resume Next
k = WorksheetFunction.Match(strTime, arrDate, 0)
If Err.Number <> 0 Then
Err.Clear: On Error GoTo 0: isMatchErr = True
End If
On Error GoTo 0
End Function
Besides the message in Immediate Window, an "OK" will be put on column B:B for all occurrences (in 'sTimeStamp Array' sheet) and background of the matching cell (in 'Task' sheet will be colored in red... In order to do that, I added a new record and modified an existing cell, for "Today" ("01-12"). Please do the same in order to obtain at least two results in column B:B.
Please confirm that this is what you wanted. If not, please better clarify the need...

Replace sequential numbers with a range

How can I find sequential numbers in a cell, and replace them with a range?
For example:
change:
1,3,5,15,16,17,25,28,29,31...
to:
1,3,5,15-17,25,28-29,31...
The numbers are already sorted, i.e. in increasing order.
Thanks.
An interesting question that I wanted to look at do without looping through a sequence (which would need sorting first) checking for sequential builds
This function
forces the string to a range address
uses Union to group consecutive rows together
manipulates the string to remove the column identifier
loop wasn't necessary, shorter version!
Function NumOut(strIn As String) As String
Dim rng1 As Range
Set rng1 = Range("A" & Join(Split(Application.Trim([a1]), ", "), ",A"))
'force the range into areas rather than cells
Set rng1 = Union(rng1, rng1)
NumOut = Replace(Replace(Replace(rng1.Address, "$A$", vbNullstring), ": ", "-"), ",", ", ")
End Function
Thought I'd try an all-formulae solution using Microsoft365's LET() as a way to capture variables.
The below solution only counts 3+ consecutive numbers as ranges of numbers, not two.
Formula in B1:
=LET(X,FILTERXML("<t><s>"&SUBSTITUTE(A1,",","</s><s>")&"</s></t>","//s"),Y,TRANSPOSE(FILTERXML("<t><s>"&SUBSTITUTE(A1,",","</s><s>")&"</s></t>","//s[preceding::*[1]+1=.][following::*[1]-1=.]")),SUBSTITUTE(TEXTJOIN(",",,FILTERXML("<t><s>"&TEXTJOIN("</s><s>",,IF(MMULT(--(X=Y),SEQUENCE(COUNTA(Y),,,0)),"-",X))&"</s></t>","//s[.*0=0 or (.='-' and preceding::*[1]*0=0)]")),",-,","-"))
While the given range/area based answer is interesting, it suffers from a couple of flaws:
It is limited to an input string of 255 characters
It is relatively slow
Here's a basic array loop based method. It can handle long strings. In my testing it runs in about 1/3 the time. It also has the bonus of not requiring the input to be sorted
Function NumOut2(strIn As String) As String
Dim arrIn() As String
Dim arrBuckets() As Long
Dim i As Long
Dim InRange As Boolean
Dim mn As Long, mx As Long
arrIn = Split(strIn, ", ")
mn = arrIn(0)
mx = arrIn(0)
For i = 1 To UBound(arrIn)
If arrIn(i) < mn Then
mn = arrIn(i)
ElseIf arrIn(i) > mx Then
mx = arrIn(i)
End If
Next
ReDim arrBuckets(mn To mx)
For i = 0 To UBound(arrIn)
arrBuckets(arrIn(i)) = arrIn(i)
Next
NumOut2 = LBound(arrBuckets)
InRange = False
For i = LBound(arrBuckets) + 1 To UBound(arrBuckets)
If arrBuckets(i) > 0 Then
If arrBuckets(i) = arrBuckets(i - 1) + 1 Then
If InRange Then
Else
InRange = True
NumOut2 = NumOut2 & "-"
End If
Else
If InRange Then
NumOut2 = NumOut2 & arrBuckets(i - 1) & ", " & arrBuckets(i)
Else
NumOut2 = NumOut2 & ", " & arrBuckets(i)
End If
End If
Else
If InRange Then
NumOut2 = NumOut2 & arrBuckets(i - 1)
End If
InRange = False
End If
Next
End Function

Convert a string reference to variable name

I am trying to soft code the output variables, so that I don't have to modify the VBA code each time i need to modify the outputs.
This is the code that works
Sub Working()
Dim cat(1 To 10)
Dim bat(1 To 10)
For i = 1 To 10
cat(i) = i * 10
bat(i) = i * 5
Next i
Sheet2.Range("A2:A11") = Application.Transpose(cat())
Sheet2.Range("B2:B11") = Application.Transpose(bat())
End Sub
This is the ideal way i would want to write, but doesnt work
Sub not_working()
Dim cat(1 To 10)
Dim bat(1 To 10)
For i = 1 To 10
cat(i) = i * 10
bat(i) = i * 5
Next i
a = 3
Do While Sheet1.Cells(a, 1) <> ""
OutVar = Sheet1.cells(a, 1) & "()"
Sheet3.Range( _
Cells(2, a - 2).Address, Cells(11, a - 2).Address _
) = Application.Transpose(Outvar)
a = a + 1
Loop
End Sub
' Sheet1.cells(3,1) = cat - these cells contain the variable names
' Sheet1.cells(4,1) = bat - these cells contain the variable names
Can someone please suggest if it is possible to do so?
If I understand your requirement correctly, a ragged array will meet it.
If you have a variable of type Variant, you can set that variable to, for example, an integer, a real, a string, a boolean or an array.
If you have an array of type Variant, you can set each element of that array to a different type of value.
In my code below, I have variant array Main. I set:
Main(0) to a 1D array,
Main(1) to a larger 1D array,
Main(2) to a 2D array,
Main(3) to a single integer,
Main(4) to the used range of a worksheet.
This is called a ragged array because each element is a different size.
Having loaded the array with values, I use a general routine to output each element of Main according to its nature.
Each of your 200-300 variables would become an element of Main.
Have a look at my code. This is only a brief introduction to what can be achieved with variant arrays. Come back with questions if you think I am heading in the correct direction but have not gone far enough.
Option Explicit
Sub DemoRaggedArray()
Dim InxDim As Long
Dim InxMain As Long
Dim InxWCol As Long
Dim InxWRow As Long
Dim Main() As Variant
Dim NumOfDim As Long
Dim Work() As Variant
ReDim Main(0 To 5)
Work = Array(1, "A", True)
Main(0) = Work
Main(1) = Array(2, "B", False, 1.2)
ReDim Work(1 To 2, 1 To 3)
Work(1, 1) = 1
Work(1, 2) = 2.5
Work(1, 3) = DateSerial(2012, 12, 27)
Work(2, 1) = True
Work(2, 2) = "String"
Main(2) = Work
Main(3) = 27
' Cells A1:C4 of the worksheet have been set to their addresses
Main(4) = WorksheetFunction.Transpose(Worksheets("Sheet2").UsedRange.Value)
For InxMain = LBound(Main) To UBound(Main)
Debug.Print "Type of Main(" & InxMain & ") is " & VarTypeName(Main(InxMain))
Select Case VarType(Main(InxMain))
Case vbEmpty, vbNull
' No value
Case Is >= vbArray
' Array
NumOfDim = NumDim(Main(InxMain))
Debug.Print " Main(" & InxMain & ") is dimensioned as: (";
For InxDim = 1 To NumOfDim
Debug.Print LBound(Main(InxMain), InxDim) & " To " & _
UBound(Main(InxMain), InxDim);
If InxDim < NumOfDim Then
Debug.Print ", ";
End If
Next
Debug.Print ")"
Select Case NumOfDim
Case 1
For InxWCol = LBound(Main(InxMain)) To UBound(Main(InxMain))
Debug.Print " (" & InxWCol & ")[" & _
VarTypeName(Main(InxMain)(InxWCol)) & "]";
Select Case VarType(Main(InxMain)(InxWCol))
Case vbEmpty, vbNull, vbArray
' No code to handle these types
Case Else
Debug.Print "=" & Main(InxMain)(InxWCol);
End Select
Next
Debug.Print
Case 2
For InxWRow = LBound(Main(InxMain), 2) To UBound(Main(InxMain), 2)
For InxWCol = LBound(Main(InxMain), 1) To UBound(Main(InxMain), 1)
Debug.Print " (" & InxWCol & "," & InxWRow & ")[" & _
VarTypeName(Main(InxMain)(InxWCol, InxWRow)) & "]";
Select Case VarType(Main(InxMain)(InxWCol, InxWRow))
Case vbEmpty, vbNull, vbArray
' No code to handle these types
Case Else
Debug.Print "=" & Main(InxMain)(InxWCol, InxWRow);
End Select
Next
Debug.Print
Next
Case Else
Debug.Print " There is no display code for this number of dimensions"
End Select
Case Else
' Single variable
Debug.Print " Value = " & Main(InxMain)
End Select
Next
End Sub
Public Function NumDim(ParamArray TestArray() As Variant) As Integer
' Returns the number of dimensions of TestArray.
' If there is an official way of determining the number of dimensions, I cannot find it.
' This routine tests for dimension 1, 2, 3 and so on until it get a failure.
' By trapping that failure it can determine the last test that did not fail.
' Coded June 2010. Documentation added July 2010.
' * TestArray() is a ParamArray because it allows the passing of arrays of any type.
' * The array to be tested in not TestArray but TestArray(LBound(TestArray)).
' * The routine does not validate that TestArray(LBound(TestArray)) is an array. If
' it is not an array, the routine return 0.
' * The routine does not check for more than one parameter. If the call was
' NumDim(MyArray1, MyArray2), it would ignore MyArray2.
Dim TestDim As Integer
Dim TestResult As Integer
On Error GoTo Finish
TestDim = 1
Do While True
TestResult = LBound(TestArray(LBound(TestArray)), TestDim)
TestDim = TestDim + 1
Loop
Finish:
NumDim = TestDim - 1
End Function
Function VarTypeName(Var As Variant)
Dim Name As String
Dim TypeOfVar As Long
TypeOfVar = VarType(Var)
If TypeOfVar >= vbArray Then
Name = "Array of type "
TypeOfVar = TypeOfVar - vbArray
Else
Name = ""
End If
Select Case TypeOfVar
Case vbEmpty
Name = Name & "Uninitialised"
Case vbNull
Name = Name & "Contains no valid data"
Case vbInteger
Name = Name & "Integer"
Case vbLong
Name = Name & "Long integer"
Case vbSingle
Name = Name & "Single-precision floating-point number"
Case vbDouble
Name = Name & "Double-precision floating-point number"
Case vbCurrency
Name = Name & "Currency"
Case vbDate
Name = Name & "Date"
Case vbString
Name = Name & "String"
Case vbObject
Name = Name & "Object"
Case vbError
Name = Name & "Error"
Case vbBoolean
Name = Name & "Boolean"
Case vbVariant
Name = Name & "Variant"
Case vbDataObject
Name = Name & "Data access object"
Case vbDecimal
Name = Name & "Decimal"
Case vbByte
Name = Name & "Byte"
Case vbUserDefinedType
Name = Name & "Variants that contain user-defined types"
Case Else
Name = Name & "Unknown type " & TypeOfVar
End Select
VarTypeName = Name
End Function
Output from DemoRaggedArray
Type of Main(0) is Array of type Variant
Main(0) is dimensioned as: (0 To 2)
(0)[Integer]=1 (1)[String]=A (2)[Boolean]=True
Type of Main(1) is Array of type Variant
Main(1) is dimensioned as: (0 To 3)
(0)[Integer]=2 (1)[String]=B (2)[Boolean]=False (3)[Double-precision floating-point number]=1.2
Type of Main(2) is Array of type Variant
Main(2) is dimensioned as: (1 To 2, 1 To 3)
(1,1)[Integer]=1 (2,1)[Boolean]=True
(1,2)[Double-precision floating-point number]=2.5 (2,2)[String]=String
(1,3)[Date]=27/12/2012 (2,3)[Uninitialised]
Type of Main(3) is Integer
Value = 27
Type of Main(4) is Array of type Variant
Main(4) is dimensioned as: (1 To 3, 1 To 4)
(1,1)[String]=A1 (2,1)[String]=B1 (3,1)[String]=C1
(1,2)[String]=A2 (2,2)[String]=B2 (3,2)[String]=C2
(1,3)[String]=A3 (2,3)[String]=B3 (3,3)[String]=C3
(1,4)[String]=A4 (2,4)[String]=B4 (3,4)[String]=C4
Type of Main(5) is Uninitialised
Note the date is displayed as "27/12/2012" because that is the default date format for my country. If you run this code, it will display in your country's default format.

Resources