After some help on this website I am now looking for more. This was my previous post: stacking and layering boxes in excel
I am now able to make all possible combinations. However my next step would be to set some parameters. By this I mean the height and weight of the boxes. If I were to place on "Sheet2" in Column A by box names (A,B,....) Column B by weight (kg) and Column C by height (millimeters). Then on "Sheet3" I place my maximum height and maximum weight. B2 maximum weight of 30 kg and C3 maximum height of 500 mm.
How can I get my macro to check against these parameters and if they do fit them they are placed in the column as in my previous question and if it goes over my weight or height it does not bother with placing it.
Hope to hear soon :) Starting to enjoy excel!
Edit:
Box name Weight height
A 1 0.12
B 5 0.92
C 3 0.5
D 2 0.34
........etc
This is how I would place my input information. I would like this for many boxes, maybe even up to 100
as a enhancement to the previous solution
input format
(Please implement your own input/output farmat after studying my code)
<num of box> <box name 1> <box name 2> ... <box name N>
<max height> <height 1> <height 2>...
<max weight> <weight 1> <weight 2> ...
<output result 1>
<output result 2>
.
.
.
sample Input & output
3 A B C D E
7.7 3 1 1 1 2
5.5 2 1 2 3 3
A
B
AB
C
AC
BC
ABC
D
AD
BD
CD
E
AE
BE
CE
Not limited to integer, you can use floating numbers
Code:
Function stackBox()
Dim ws As Worksheet
Dim width As Long
Dim height As Long
Dim numOfBox As Long
Dim optionsA() As Variant
Dim results() As Variant
Dim str As String
Dim outputArray As Variant
Dim i As Long, j As Long
Dim currentSymbol As String
'------------------------------------new part----------------------------------------------
Dim maxHeight As Double
Dim maxWeight As Double
Dim heightarray As Variant
Dim weightarray As Variant
Dim totalHeight As Double
Dim totalWeight As Double
'------------------------------------new part----------------------------------------------
Set ws = Worksheets("Sheet1")
With ws
'clear last time's output
height = .Cells(.Rows.Count, 1).End(xlUp).row
If height > 3 Then
.Range(.Cells(4, 1), .Cells(height, 1)).ClearContents
End If
numOfBox = .Cells(1, 1).Value
width = .Cells(1, .Columns.Count).End(xlToLeft).Column
If width < 2 Then
MsgBox "Error: There's no item, please fill your item in Cell B1,C1,..."
Exit Function
End If
'------------------------------------new part----------------------------------------------
maxHeight = .Cells(2, 1).Value
maxWeight = .Cells(3, 1).Value
ReDim heightarray(1 To 1, 1 To width - 1)
ReDim weightarray(1 To 1, 1 To width - 1)
heightarray = .Range(.Cells(2, 2), .Cells(2, width)).Value
weightarray = .Range(.Cells(3, 2), .Cells(3, width)).Value
'------------------------------------new part----------------------------------------------
ReDim optionsA(0 To width - 2)
For i = 0 To width - 2
optionsA(i) = .Cells(1, i + 2).Value
Next i
GenerateCombinations optionsA, results, numOfBox
' copy the result to sheet only once
ReDim outputArray(1 To UBound(results, 1) - LBound(results, 1) + 1, 1 To 1)
Count = 0
For i = LBound(results, 1) To UBound(results, 1)
If Not IsEmpty(results(i)) Then
'rowNum = rowNum + 1
str = ""
totalHeight = 0#
totalWeight = 0#
For j = LBound(results(i), 1) To UBound(results(i), 1)
currentSymbol = results(i)(j)
str = str & currentSymbol 'results(i)(j) is the SYMBOL e.g. A, B, C
'look up box's height and weight , increment the totalHeight/totalWeight
updateParam currentSymbol, optionsA, heightarray, weightarray, totalHeight, totalWeight
Next j
If totalHeight < maxHeight And totalWeight < maxWeight Then
Count = Count + 1
outputArray(Count, 1) = str
End If
'.Cells(rowNum, 1).Value = str
End If
Next i
.Range(.Cells(4, 1), .Cells(UBound(outputArray, 1) + 3, 1)).Value = outputArray
End With
End Function
Sub updateParam(ByRef targetSymbol As String, ByRef symbolArray As Variant, ByRef heightarray As Variant, ByRef weightarray As Variant, ByRef totalHeight As Double, ByRef totalWeight As Double)
Dim i As Long
Dim index As Long
index = -1
For i = LBound(symbolArray, 1) To UBound(symbolArray, 1)
If targetSymbol = symbolArray(i) Then
index = i
Exit For
End If
Next i
If index <> -1 Then
totalHeight = totalHeight + heightarray(1, index + 1)
totalWeight = totalWeight + weightarray(1, index + 1)
End If
End Sub
Sub GenerateCombinations(ByRef AllFields() As Variant, _
ByRef Result() As Variant, ByVal numOfBox As Long)
Dim InxResultCrnt As Integer
Dim InxField As Integer
Dim InxResult As Integer
Dim i As Integer
Dim NumFields As Integer
Dim Powers() As Integer
Dim ResultCrnt() As String
NumFields = UBound(AllFields) - LBound(AllFields) + 1
ReDim Result(0 To 2 ^ NumFields - 2) ' one entry per combination
ReDim Powers(0 To NumFields - 1) ' one entry per field name
' Generate powers used for extracting bits from InxResult
For InxField = 0 To NumFields - 1
Powers(InxField) = 2 ^ InxField
Next
For InxResult = 0 To 2 ^ NumFields - 2
' Size ResultCrnt to the max number of fields per combination
' Build this loop's combination in ResultCrnt
ReDim ResultCrnt(0 To NumFields - 1)
InxResultCrnt = -1
For InxField = 0 To NumFields - 1
If ((InxResult + 1) And Powers(InxField)) <> 0 Then
' This field required in this combination
InxResultCrnt = InxResultCrnt + 1
ResultCrnt(InxResultCrnt) = AllFields(InxField)
End If
Next
If InxResultCrnt = 0 Then
Debug.Print "testing"
End If
'additional logic here
If InxResultCrnt >= numOfBox Then
Result(InxResult) = Empty
Else
' Discard unused trailing entries
ReDim Preserve ResultCrnt(0 To InxResultCrnt)
' Store this loop's combination in return array
Result(InxResult) = ResultCrnt
End If
Next
End Sub
Related
I have a table with two Columns Product and Price($).
Product
Price($)
A
100
B
400
C
350
D
50
E
515
F
140
I am trying to use vba to get combination of value of all products that will not exceed $500. I have been trying with this code and I am not sure how to proceed from this point on.
Sub getCombination()
Dim price As Long
Dim limit As Long
Dim i As Integer
Dim j As Integer
Dim combination As String
limit = 500
combination = ""
Range("B2").Activate
price = Range("B2").Value
For i = 1 To 6
For j = 1 To 6
If price <= limit Then
price = price + ActiveCell.Offset(j, 0).Value
combination = combination & ActiveCell.Offset(0, -1).Value & "," & ActiveCell.Offset(1, -1).Value
End If
Next j
Next i
ActiveCell.Offset(1, 0).Activate
MsgBox combination
End Sub
My Expected output is something like
A,B
A,C
A,C,D
B,D
C,F
A,D
C,D
(Please note: Not All output combinations are specified here!)
How should I proceed with the existing code? Or do I really have a better way for me to implement this?
Since the item can be used or not, that is a binary response. Using a binary number with the same number of digits as the number of items we can do all the combinations and do the testing:
Sub getCombination()
Dim rngArr As Variant
rngArr = ActiveSheet.Range("A2:B7")
Dim cnt As Long
cnt = 2 ^ UBound(rngArr, 1) - 1
Dim OutArray As Variant
ReDim OutArray(1 To cnt, 1 To 2)
Dim k As Long
k = 1
Dim i As Long
For i = 1 To cnt
Dim bin As String
bin = Application.Dec2Bin(i, UBound(rngArr, 1))
Dim delim As String
delim = ""
Dim j As Long
For j = 1 To UBound(rngArr, 1)
If Mid(bin, j, 1) = "1" Then
OutArray(k, 1) = OutArray(k, 1) & delim & rngArr(j, 1)
delim = ", "
OutArray(k, 2) = OutArray(k, 2) + rngArr(j, 2)
End If
Next j
If OutArray(k, 2) <= 500 Then
k = k + 1
Else
OutArray(k, 1) = ""
OutArray(k, 2) = 0
End If
Next i
Dim fnlarr As Variant
ReDim fnlarr(1 To k - 1)
For i = 1 To k - 1
fnlarr(i) = OutArray(i, 1)
Next i
Debug.Print Join(fnlarr, " | ")
End Sub
I have been trying to sort the Column values from A to Z which are populated in the List Box.
I have tried with the following but it does not adjust it. Any help will be appreciated.
Dim ws As Worksheet
Dim rng As Range
Dim myArray
Set ws = Sheets("Sheet2")
Set rng = ws.Range("A2:A" & ws.Range("A" & ws.Rows.Count).End(xlUp).Row), Order1:=xlAscending
With Me.ListBox1
.Clear
.ColumnHeads = False
.ColumnCount = rng.Columns.Count
MyArray = rng
.List = SortArray(myArray)
End With
I want to use the Arrays for Sorting Function which will be populated to Listbox.
Sub SortArray(myListBox As MSForms.ListBox, Optional resetMacro As String)
Dim j As Long
Dim i As Long
Dim temp As Variant
If resetMacro <> "" Then
Run resetMacro, myListBox
End If
With myListBox
For j = 0 To .ListCount - 2
For i = 0 To .ListCount - 2
If LCase(.List(i)) > LCase(.List(i + 1)) Then
temp = .List(i)
.List(i) = .List(i + 1)
.List(i + 1) = temp
End If
Next i
Next j
End With
End Sub
Method 1: Sort Data in Cells
You need to sort the range using the Range.Sort method
Set rng = ws.Range("A2:A" & ws.Range("A" & ws.Rows.Count).End(xlUp).Row)
rng.Sort key1:=ws.Range("A2"), order1:=xlAscending, Header:=xlNo
Also see VBA Excel sort range by specific column.
Method 2: Sort Data in Array
Or load the data into an array and sort the array. See VBA array sort function?
Note: The QuickSort algorithm was retrieved from the link above.
Option Explicit
Private Sub LoadButton_Click()
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Sheet2")
Dim DataRange As Range
Set DataRange = ws.Range("A2:A" & ws.Range("A" & ws.Rows.Count).End(xlUp).Row)
' 2-dimensional array of the data
Dim DataArray() As Variant
DataArray = DataRange.Value
' Sort data in 2-dimensional array DataArray
QuickSortArray SortArray:=DataArray, SortColumn:=1
' Load sorted data into ListBox
SortedListForm.SortedListBox.List = DataArray
End Sub
' QickSort algorithm that takes a 2-dimensional array
Public Sub QuickSortArray(ByRef SortArray As Variant, Optional ByVal Min As Long = -1, Optional ByVal Max As Long = -1, Optional ByVal SortColumn As Long = 0)
On Error Resume Next
'Sort a 2-Dimensional array
' SampleUsage: sort arrData by the contents of column 3
'
' QuickSortArray arrData, , , 3
'
'Posted by Jim Rech 10/20/98 Excel.Programming
'Modifications, Nigel Heffernan:
' ' Escape failed comparison with empty variant
' ' Defensive coding: check inputs
Dim i As Long
Dim j As Long
Dim RowTemp As Variant
Dim ColTempIdx As Long
If IsEmpty(SortArray) Then
Exit Sub
End If
If InStr(TypeName(SortArray), "()") < 1 Then 'IsArray() is somewhat broken: Look for brackets in the type name
Exit Sub
End If
If Min = -1 Then
Min = LBound(SortArray, 1)
End If
If Max = -1 Then
Max = UBound(SortArray, 1)
End If
If Min >= Max Then ' no sorting required
Exit Sub
End If
i = Min
j = Max
Dim SortItem As Variant
SortItem = Empty
SortItem = SortArray((Min + Max) \ 2, SortColumn)
' We send 'Empty' and invalid data items to the end of the list:
If IsObject(SortItem) Then ' note that we don't check isObject(SortArray(n)) - SortItem *might* pick up a valid default member or property
i = Max
j = Min
ElseIf IsEmpty(SortItem) Then
i = Max
j = Min
ElseIf IsNull(SortItem) Then
i = Max
j = Min
ElseIf SortItem = "" Then
i = Max
j = Min
ElseIf VarType(SortItem) = vbError Then
i = Max
j = Min
ElseIf VarType(SortItem) > 17 Then
i = Max
j = Min
End If
Do While i <= j
Do While SortArray(i, SortColumn) < SortItem And i < Max
i = i + 1
Loop
Do While SortItem < SortArray(j, SortColumn) And j > Min
j = j - 1
Loop
If i <= j Then
' Swap the rows
ReDim RowTemp(LBound(SortArray, 2) To UBound(SortArray, 2))
For ColTempIdx = LBound(SortArray, 2) To UBound(SortArray, 2)
RowTemp(ColTempIdx) = SortArray(i, ColTempIdx)
SortArray(i, ColTempIdx) = SortArray(j, ColTempIdx)
SortArray(j, ColTempIdx) = RowTemp(ColTempIdx)
Next ColTempIdx
Erase RowTemp
i = i + 1
j = j - 1
End If
Loop
If (Min < j) Then
QuickSortArray SortArray, Min, j, SortColumn
End If
If (i < Max) Then
QuickSortArray SortArray, i, Max, SortColumn
End If
End Sub
I am working on a project in which I have to generate a table of output from a range of data. I am able to generate Unique values from the list of data and I am able to calculate Count, Mean and Standard Deviation for the individual values. But, I am not able to connect these two functions. Can anyone tell a solution to how I can execute everything from one function?
I want to call the Uniques function in the code from the array output and then execute a mathematical function on these values.
Code
Option Explicit
Public Function uniques(myrange As Range)
Dim list As New Collection
Dim Ulist() As String
Dim value As Variant
Dim i As Integer
'Adding each value of myrang into the collection.
On Error Resume Next
For Each value In myrange
'here value and key are the same. The collection does not allow duplicate keys hence only unique values will remain.
list.Add CStr(value), CStr(value)
Next
On Error GoTo 0
'Defining the length of the array to the number of unique values. Since the array starts from 0, we subtract 1.
ReDim Ulist(list.Count - 1, 0)
'Adding unique value to the array.
For i = 0 To list.Count - 1
Ulist(i, 0) = list(i + 1)
Next
'Printing the array
uniques = Ulist
End Function
Public Function findtext(tofind As String, myrange As Range) As Integer
' Removed RA from dim
Dim i As Integer
Dim rcount As Integer
rcount = 0
For i = 1 To myrange.Rows.Count
'tofind = uniques(myrange.Cells.value)
If myrange(i, 1) = tofind Then
rcount = rcount + 1
End If
Next i
findtext = rcount
End Function
Public Function findavg(tofind As String, myrange As Range)
Dim avg As Double, rcount As Integer
Dim SUM As Double, findtext As Double
Dim i As Integer
SUM = 0
rcount = 0
For i = 1 To myrange.Rows.Count
If myrange(i, 1) = tofind Then
SUM = SUM + myrange(i, 2)
rcount = rcount + 1
avg = SUM / rcount
End If
Next i
findavg = avg
End Function
Public Function findstd(tofind As String, myrange As Range)
Dim std As Double, rcount As Integer
Dim SUM As Double, avg As Double, totalstd As Double
Dim i As Integer
SUM = 0
std = 0
rcount = 0
For i = 1 To myrange.Rows.Count 'i to active selection
If myrange(i, 1) = tofind Then 'criteria = "A"....etc
SUM = SUM + myrange(i, 2) 'sum in loop
rcount = rcount + 1 'add & count in loop
avg = SUM / rcount
End If
Next i
For i = 1 To myrange.Rows.Count
If myrange(i, 1) = tofind Then
std = std + (myrange(i, 2) - avg) ^ 2
End If
Next i
findstd = Sqr(std / (rcount - 1))
End Function
Function arrayutput(tofind As String, myrange As Range) As Variant
'we take it as zero because we haven't taken option base1
Dim Output(0, 2) As Variant
Output(0, 0) = findtext(tofind, myrange) 'first column
Output(0, 1) = findavg(tofind, myrange) 'second column
Output(0, 2) = findstd(tofind, myrange)
arrayutput = Output
End Function
Please, try the next code. It uses a dictionary to solve the unique part, the count and sum, then process its data and populate an array. Its content is dropped at once in the range. The code assumes that the range to be processed is in columns A:C and the processing result is placed in a range starting from "G2":
Sub testExtractDataAtOnce()
'the code needs a reference to 'Microsoft Scripting Runtime'
Dim sh As Worksheet, lastRow As Long, arr, arrIt, arrFin
Dim i As Long, dict As New Scripting.Dictionary
Set sh = ActiveSheet 'use here your necessary sheet
lastRow = sh.Range("B" & sh.Rows.count).End(xlUp).row
arr = sh.Range("B2:C" & lastRow).Value 'put the range to be processed in an array
For i = 1 To UBound(arr) 'process the array and fill the dictionary
If Not dict.Exists(arr(i, 1)) Then
dict.Add arr(i, 1), 1 & "|" & arr(i, 2) 'create the unique key and corresponding count | value
Else
arrIt = Split(dict(arr(i, 1)), "|") 'extract the count and previous value and use it in the next line
'add the count and the new value to the existing key data:
dict(arr(i, 1)) = CLng(arrIt(0)) + 1 & "|" & CDbl(arrIt(1)) + arr(i, 2)
End If
Next i
ReDim arrFin(1 To dict.count, 1 To 4) 'redim the final array to accept all the necessary fields
Dim avg As Double, std As Double
For i = 0 To dict.count - 1 'iterate between the dictionary data
arrIt = Split(dict.Items(i), "|") 'extract the count and the value (sum)
arrFin(i + 1, 1) = dict.Keys(i): arrFin(i + 1, 2) = arrIt(0) 'write the key and count
avg = arrIt(1) / arrIt(0) 'calculate the average (neccessary for the next steps, too)
arrFin(i + 1, 3) = avg 'put the average in the array
'call the adapted function (able to extract the stdDev from the array):
arrFin(i + 1, 4) = findstd(CStr(dict.Keys(i)), avg, CDbl(arrIt(0)), arr)
Next i
'Drop the processed result in the sheet, at once. You can use any range instead of "G2" and any sheet
sh.Range("G2").Resize(UBound(arrFin), 4).Value = arrFin
End Sub
Public Function findstd(tofind As String, avg As Double, rcount As Long, arr)
Dim std As Double, i As Long
For i = 1 To UBound(arr)
If arr(i, 1) = tofind Then
std = std + (arr(i, 2) - avg) ^ 2
End If
Next i
findstd = Sqr(std / (rcount - 1))
End Function
Please test it, send some feedback.
If you do not know how to add a reference, please run the next code before running the above one. It will automatically add the necessary reference:
Sub addScrRunTimeRef()
'Add a reference to 'Microsoft Scripting Runtime':
'In case of error ('Programmatic access to Visual Basic Project not trusted'):
'Options->Trust Center->Trust Center Settings->Macro Settings->Developer Macro Settings->
' check "Trust access to the VBA project object model"
Application.VBE.ActiveVBProject.References.AddFromFile "C:\Windows\SysWOW64\scrrun.dll"
End Sub
I am trying to write a simple randomizing program that reads from a column of names and randomly writes them to three columns of four. I have something that kind of works, but it is duplicating my names and I can figure out how to fix it with arrays or collections as those wont let me compare values. Thank you in advance.
Goal: randomization without doubling up two names
Problem: comparing and writing (to worksheet) collection and/or arrays
Option Explicit
Private Sub Randomize_Click()
Dim HowMany As Integer
Dim NoOfNames As Long
Dim RandomNumber As Integer
Dim Names(), dub() As String 'Array to store randomly selected names
Dim i, j, r, a, p As Byte
Dim CellsOut As Long 'Variable to be used when entering names onto worksheet
Dim ArI As Byte 'Variable to increment through array indexes
Application.ScreenUpdating = False
HowMany = 4 ' use with a third loops?
CellsOut = 4
For a = 1 To 6
For r = 1 To 3
For j = 2 To 5
'CellsOut = i 'turn this into loops
ReDim Names(1 To 4) 'Set the array size to how many names required
NoOfNames = Application.CountA(Worksheets("Employees").Range("A:A")) - 1 ' Find how many
names in the list
i = 1
Do While i <= HowMany
RandomNo:
RandomNumber = Application.RandBetween(2, NoOfNames + 1)
dub = RandomNumber
'dub.Add Unit.Value
If Names(i) = Cells(RandomNumber, 1).Value Then
'If Names(i) = dub(Unit) Then
GoTo RandomNo
End If
Names(i) = Worksheets("Employees").Cells(RandomNumber, 1).Value ' Assign random
name to the array
i = i + 1 '
Loop
'Loop through the array and enter names onto the worksheet
For ArI = LBound(Names) To UBound(Names)
Cells(CellsOut, j) = Names(ArI)
CellsOut = CellsOut + 1
Next ArI
Application.ScreenUpdating = True
CellsOut = 4
Next j
Next r
Next a
End Sub
Display
Names
Random Names
Current Setup
This solution uses the dictionary to randomize numbers which I was exploring earlier today.
The complete code goes into a standard module.
Adjust the three constants at the beginning of randomizeNames.
You only run randomizeNames, e.g. via a command button:
Private Sub Randomize_Click()
randomizeNames
End Sub
The Code
Option Explicit
Sub randomizeNames()
' Constants
Const srcFirst As String = "A2"
Const NoC As Long = 3
Const tgtFirst As String = "C2"
' Define Source First Cell Range ('cel').
Dim cel As Range
Set cel = Range(srcFirst)
' Define Source Last Cell Range ('rng').
Dim rng As Range
Set rng = Cells(Rows.Count, cel.Column).End(xlUp)
' Define Source Column Range ('rng').
Set rng = Range(cel, rng)
' Define Number of Elements (names) ('NoE').
Dim NoE As Long
NoE = rng.Rows.Count
' Write values from Source Column Range to Source Array ('Source').
Dim Source As Variant
If NoE > 1 Then
Source = rng.Value
Else
ReDim Source(1 To 1, 1 To 1)
Source(1, 1) = rng.Value
End If
' Define Random Numbers Array ('RNA').
Dim RNA As Variant
' This line uses both functions.
RNA = getDictionary(Dictionary:=getRandomDictionary(1, NoE), _
FirstOnly:=True)
' Instead of numbers, write elements from Source Array
' to Random Number Array (Random Names Array).
Dim i As Long
For i = 1 To NoE
RNA(i, 1) = Source(RNA(i, 1), 1)
Next i
' Define Number of Rows in Target Array ('NoR') and the Remainder
' of elements ('Remainder').
Dim NoR As Long
NoR = Int(NoE / NoC)
Dim Remainder As Long
Remainder = NoE Mod NoC
If Remainder > 0 Then
NoR = NoR + 1
Else
Remainder = NoC
End If
' Define Target Array ('Target').
Dim Target As Variant
ReDim Target(1 To NoR, 1 To NoC)
' Declare additional variables.
Dim j As Long ' Target Array Columns Counter
Dim k As Long ' Random Names Array Rows Counter
' Write values from Random Names Array to Target Array.
For i = 1 To NoR - 1
For j = 1 To NoC
k = k + 1
Target(i, j) = RNA(k, 1)
Next j
Next i
For j = 1 To Remainder
k = k + 1
Target(i, j) = RNA(k, 1)
Next j
' Define Target First Cell Range ('cel').
Set cel = Range(tgtFirst)
' Clear contents from Target First Cell Range to bottom-most cell
' of last column of Target Range.
cel.Resize(Rows.Count - cel.Row + 1, NoC).ClearContents
' Write values from Target Array to Target Range.
Range(tgtFirst).Resize(NoR, NoC).Value = Target
End Sub
Function getRandomDictionary(ByVal LowOrHigh As Long, _
ByVal HighOrLow As Long) _
As Object
' Define Numbers Dictionary ('dict').
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
' Define the lower number ('Minimum') and the Number of Elements ('NoE').
Dim NoE As Long
Dim Minimum As Long
If LowOrHigh < HighOrLow Then
Minimum = LowOrHigh
NoE = HighOrLow - LowOrHigh + 1
Else
Minimum = HighOrLow
NoE = LowOrHigh - HighOrLow + 1
End If
' Write random list of numbers to Numbers Dictionary.
Dim Current As Long
Do
' Randomize ' Takes considerably longer.
Current = Int(Minimum + NoE * Rnd)
dict(Current) = Empty
Loop Until dict.Count = NoE
' Write result.
Set getRandomDictionary = dict
End Function
Function getDictionary(Dictionary As Object, _
Optional ByVal Horizontal As Boolean = False, _
Optional ByVal FirstOnly As Boolean = False, _
Optional ByVal Flip As Boolean = False) _
As Variant
' Validate Dictionary.
If Dictionary Is Nothing Then
GoTo ProcExit
End If
Dim NoE As Long
NoE = Dictionary.Count
If NoE = 0 Then
GoTo ProcExit
End If
' Write values from Dictionary to Data Array ('Data').
Dim Data As Variant
Dim Key As Variant
Dim i As Long
If Not Horizontal Then
If Not FirstOnly Then
ReDim Data(1 To NoE, 1 To 2)
If Not Flip Then
For Each Key In Dictionary.Keys
i = i + 1
Data(i, 1) = Key
Data(i, 2) = Dictionary(Key)
Next Key
Else
For Each Key In Dictionary.Keys
i = i + 1
Data(i, 1) = Dictionary(Key)
Data(i, 2) = Key
Next Key
End If
Else
ReDim Data(1 To NoE, 1 To 1)
If Not Flip Then
For Each Key In Dictionary.Keys
i = i + 1
Data(i, 1) = Key
Next Key
Else
For Each Key In Dictionary.Keys
i = i + 1
Data(i, 1) = Dictionary(Key)
Next Key
End If
End If
Else
If Not FirstOnly Then
ReDim Data(1 To 2, 1 To NoE)
If Not Flip Then
For Each Key In Dictionary.Keys
i = i + 1
Data(1, i) = Key
Data(2, i) = Dictionary(Key)
Next Key
Else
For Each Key In Dictionary.Keys
i = i + 1
Data(1, i) = Dictionary(Key)
Data(2, i) = Key
Next Key
End If
Else
ReDim Data(1 To 1, 1 To NoE)
If Not Flip Then
For Each Key In Dictionary.Keys
i = i + 1
Data(1, i) = Key
Next Key
Else
For Each Key In Dictionary.Keys
i = i + 1
Data(1, i) = Dictionary(Key)
Next Key
End If
End If
End If
' Write result.
getDictionary = Data
ProcExit:
End Function
List of US Top 30 Names
James
John
Robert
Michael
William
Mary
David
Joseph
Richard
Charles
Thomas
Christopher
Daniel
Elizabeth
Matthew
Patricia
George
Jennifer
Linda
Anthony
Barbara
Donald
Paul
Mark
Andrew
Edward
Steven
Kenneth
Margaret
Joshua
I'm currently programming an Excel Function which should return the average of the last 5 non-empty positions of an array. To do that I want to go through the array while inside the function as follows:
Function AVERAGE_LAST_5(rng As Range) As Long
Dim x As Integer, i As Integer, j As Integer, sum As Integer
Dim myArr() As Variant
myArr() = Application.Transpose(Application.Transpose(rng))
x = rng.Count
i = 0:: j = 0:: sum = 0
For i = x To 1 Step -1
If myArr(x).Value <> 0 Then
sum = sum + myArr(x)
j = j + 1
Else
End If
If j = 5 Then Stop
x = x - 1
Next
AVERAGE_LAST_5 = sum / 5
End Function
Problem: the for loop doesn't work, when reaching the first if the program aborts.
Does anyone has had the same problem?
Can anyone help me with it?
myarr will be a two-dimensional array, and not a range. You will need to provide both dimensions:
If isarray(myarr) then
for i = ubound(myarr,1) to lbound(myarr,1) step -1
for j = ubound(myarr,2) to lbound (myarr,2) step -1
if myarr(i,j) <> 0 then
K=k+1
Mysum = mysum + myarr(I,j)
Endif
Next j
Next i
Else ‘ single value
mysum =myarr(I,j)
Endif
Arrays Are Faster
Final Version (Hopefully)
This version additionally has the NumberOfLastValues argument (Required) so you can choose how many values will be summed up and it is shortened with the GoSub...Return statement since the If statement is the same for by rows and by columns.
For some other details look in the First Version below.
Usage
In VBA:
Sub LastAverage()
Debug.Print AvgLast(Range("B4:G14"), 5)
End Sub
In Excel:
=AvgLast(B4:G14,5)
Function AvgLast(SearchRange As Range, ByVal NumberOfLastValues As Long, _
Optional ByVal Row_0_Column_1 As Integer = 0) As Double
Dim vntRange As Variant ' Range Array
Dim i As Long ' Range Array Rows Counter
Dim j As Integer ' Range Array Columns Counter
Dim k As Long ' Values Counter
Dim dblSum As Double ' Values Accumulator
If SearchRange Is Nothing Then Exit Function
vntRange = SearchRange.Value
If Row_0_Column_1 = 0 Then
' By Row
For i = UBound(vntRange) To 1 Step -1
For j = UBound(vntRange, 2) To 1 Step -1
GoSub Calc
Next
Next
Else
' By Column
For j = UBound(vntRange, 2) To 1 Step -1
For i = UBound(vntRange) To 1 Step -1
GoSub Calc
Next
Next
End If
TiDa:
If k > 0 Then
AvgLast = dblSum / k
End If
Exit Function
Calc:
If vntRange(i, j) <> "" And IsNumeric(vntRange(i, j)) Then
k = k + 1
dblSum = dblSum + vntRange(i, j)
If k = NumberOfLastValues Then GoTo TiDa
End If
Return
End Function
First Version
It will return the average if there is at least 1 value and at most 5 values, otherwise it will return 0.
The Row_0_Column_1 arguments parameter is by default 0 and means that the search is done by row (first loop). If it is 1, then the search is done by column (second loop).
The basics are that the range is pasted (depsited) into an array and then the array is searched for existing 'numeric' values and not "" values that are summed up and when reaching the fifth value it 'jumps' out of the loop and divides the sum by 5.
Function AvgLast5(SearchRange As Range, Optional Row_0_Column_1 As Integer = 0) _
As Double
Dim vntRange As Variant ' Range Array
Dim i As Long ' Range Array Rows Counter
Dim j As Integer ' Range Array Columns Counter
Dim k As Long ' Values Counter
Dim dblSum As Double ' Values Accumulator
If SearchRange Is Nothing Then Exit Function
vntRange = SearchRange.Value
If Row_0_Column_1 = 0 Then
' By Row
For i = UBound(vntRange) To 1 Step -1
For j = UBound(vntRange, 2) To 1 Step -1
If vntRange(i, j) <> "" And IsNumeric(vntRange(i, j)) Then
k = k + 1
dblSum = dblSum + vntRange(i, j)
If k = 5 Then GoTo TiDa
End If
Next
Next
Else
' By Column
For j = UBound(vntRange, 2) To 1 Step -1
For i = UBound(vntRange) To 1 Step -1
If vntRange(i, j) <> "" And IsNumeric(vntRange(i, j)) Then
k = k + 1
dblSum = dblSum + vntRange(i, j)
If k = 5 Then GoTo TiDa
End If
Next
Next
End If
TiDa:
If k > 0 Then
AvgLast5 = dblSum / k
End If
End Function
after a couple of tough days at work I've finally got some time to improve my function taking your tips on board.
I've made some changes to enable the function to deal either with 1-Row or 1-Column Ranges. Basic Error handling was also added and a function discripton is as well available (under the FX Excel button).
Feel free to comment and/or use the code.
Here the result:
Function AVERAGE_LAST_N(rng As Range, N As Integer)
Dim NrN As Integer, NrR As Integer, NrC As Integer
Dim i As Integer, j As Integer
Dim sum As Double
Dim myArr As Variant
NrN = rng.Count 'Number of array positions
NrR = rng.Rows.Count 'Number of Rows in the array
NrC = rng.Columns.Count 'Number of Rows in the array
i = 0:: j = 0:: sum = 0 'Counters
'####################################################'
'## Transpose Range into array if row or if column ##'
'####################################################'
If rng.Rows.Count > 1 And rng.Columns.Count = 1 Then 'Transpose a Column Range into an Array
myArr = Application.Transpose(rng)
ElseIf rng.Rows.Count = 1 And rng.Columns.Count > 1 Then 'Transpose a Row Range into an Array
myArr = Application.Transpose(Application.Transpose(rng))
ElseIf rng.Rows.Count > 1 And rng.Columns.Count > 1 Then 'Retunrs an Error if Range is a Matrix *ERR_002*
AVERAGE_LAST_N = "ERR_002"
Exit Function
End If
'####################################################'
'## Transpose Range into array if row or if column ##'
'####################################################'
'################'
'## Start Main ##'
'################'
For i = NrN To 1 Step -1
If IsNumeric(myArr(NrN)) Then
sum = sum + myArr(NrN)
j = j + 1
End If
If j = N Then Exit For
NrN = NrN - 1
Next
AVERAGE_LAST_N = sum / N
'##############'
'## End Main ##'
'##############'
'####################'
'## Error Debuging ##'
'####################'
If j < N Then
AVERAGE_LAST_N = "ERR_001"
Exit Function
End If
'####################'
'## Error Debuging ##'
'####################'
End Function
Sub DescribeFunction()
Dim FuncName As String
Dim FuncDesc As String
Dim Category As String
Dim ArgDesc(1) As String
FuncName = "AVERAGE_LAST_N"
FuncDesc = "Returns the average of the last N non-empty values in the selected Range"
Category = 14 'Text category
ArgDesc(0) = "Range that contains the values" & Chr(10) & _
"ERR_001 - There are not enought non-empty or null values in the range" & Chr(10) & _
"ERR_002 - Selected range is a matrix and not a row or column range"
ArgDesc(1) = "Dimention of the sample" & Chr(10) & _
"ERR_001 - There are not enought non-empty or null values in the range" & Chr(10) & _
"ERR_002 - Selected range is a matrix and not a row or column range"
Application.MacroOptions _
Macro:=FuncName, _
Description:=FuncDesc, _
Category:=Category, _
ArgumentDescriptions:=ArgDesc
End Sub
'#######################################################################################
' ###############################################
' ############# Error DB ##############
' ###############################################
'
'
' ERR_001 - There are not enought non-empty values in the range
' ERR_002 - Selected range is a matrix and not a row or column range
'
Rafa