Get "trimmed minimum", similiar to Excel TRIMMEAN Function - excel

I would like to implement a custom function into Excel which returns the minimum of a trimmed data sample.
Two inputs:
Data
Percentage, which states how many data points from the original data sample should be excluded
My first draft (seen below) misses two features right now:
When I use the function and select a whole column (e. g. =TrimMIN(A:A)) it takes takes a long time
I need to sort the input Range 'data' before trimming it but the line 'data.Cells.Sort' is not working
Looking forward to get get some ideas on those two issues.
My code:
Function TrimMIN(data As Range, percentage As Double) As Double
Dim dataNew As Range
Dim dataNewS As Variant
Dim diff, counter, upper, lower, countDataNew As Double
counter = 0
'data.Cells.Sort
diff = Round(data.Count * percentage / 2, [0])
Debug.Print "diff= " & diff
upper = data.Count - diff
lower = diff
countDataNew = data.Count - diff - diff
'Application.Min(data)
'Debug.Print "upper= " & upper
'Debug.Print "lower= " & lower
'Debug.Print "data.count= " & data.count
'Debug.Print "countDataNew= " & countDataNew
Dim cel As Range
For Each cel In data.Cells
counter = counter + 1
'Debug.Print "counter= " & counter
Debug.Print "celValue= " & cel.Value
If counter > lower And counter <= upper Then
'Debug.Print "counter in range, counter is " & counter
If Not dataNew Is Nothing Then
' Add the 2nd, 3rd, 4th etc cell to our new range, rng2
' this is the most common outcome so place it first in the IF test (faster coding)
Set dataNew = Union(dataNew, cel)
Else
' the first valid cell becomes rng2
Set dataNew = cel
End If
End If
Next cel
'Debug.Print "dataNew.count " & dataNew.count
TrimMIN = Application.Min(dataNew)
End Function

This is a working function.
Ideally it is up to you to place an appropriate range as argument to the funtion...
Public Function TrimMin(data As Range, percentage As Double) As Double
Dim usedData As Variant
'avoid calculating entire columns or rows
usedData = Intersect(data, data.Parent.UsedRange).Value
Dim x As Long, y As Long
x = UBound(usedData) - LBound(usedData) + 1
y = UBound(usedData, 2) - LBound(usedData, 2) + 1
Dim arr() As Variant
ReDim arr(1 To x * y)
Dim i As Long, j As Long, counter As Long
counter = 1
For i = 1 To x
For j = 1 To y
If Application.WorksheetFunction.IsNumber(usedData(i, j)) Then
arr(counter) = usedData(i, j)
counter = counter + 1
End If
Next j
Next i
ReDim Preserve arr(1 To counter - 1)
Dim diff As Long
diff = Round((counter - 1) * percentage / 2, 0) + 1
'use the worksheet function to obtain the appropriate small value
TrimMin = Application.WorksheetFunction.Small(usedData, diff)
End Function

Related

How do I break a range into n chunks and run a function on each chuck and append the results into a single column?

For context of the code here. I have combined several of these 96 cell ranges into one larger range composed on n smaller ranges. Reason for doing this is to make it (more) scalable.
I have a range of data I want to break them up into n ranges/chunks and run my function on each (function below, shout out to #Tim Williams for the function) and combine the outputs all into a column. One solution I don't really like but I could do is to simply run the function on each n chunks/ranges and manually combine them, however, that's not really optimal for what I need. I am still pretty new to VBA, any ideas would be much appreciated!
The function I am using is as follows. Note my comment within the function:
Sub Tester()
Dim rng As Range, arr
Dim Poolws As Worksheet
Dim Combows As Worksheet
Dim plates As Range
Set Poolws = ThisWorkbook.Sheets("Pools")
Set Combows = ThisWorkbook.Sheets("Combined Plates")
Set rng = Combows.Range("C3:N66")
Set plates = Combows.Range("A2")
ArrayToCell BlockToList(rng, plates), Poolws.Range("A2") 'read by column
ArrayToCell BlockToList(rng, plates, False), Poolws.Range("F2") 'read by column
End Sub
'convert a rectangular range into a 2-d single-column array
' Read by row(default) or by column (pass False as second argument)
Function BlockToList(rng As Range, plates As Range, Optional rowMajor As Boolean = True)
Dim m As Long, n As Long, dr, dc, arrData, arrOut, platenum, i As Long
arrData = rng.Value
platenum = plates.Value
dr = UBound(arrData, 1)
dc = UBound(arrData, 2)
ReDim arrOut(1 To (dr * dc), 1 To 1)
If rowMajor Then
For m = 1 To dr
For n = 1 To dc
i = i + 1
arrOut(i, 1) = arrData(m, n)
Next n
Next m
Else
For m = 1 To dc
' I think something in the following lines needs to change.
' divide array by plantenum into that many arrays then on each
' run the following, pasting the results sequentially in a column
For n = 1 To dr / platenum
i = i + 1
arrOut(i, 1) = arrData(n, m)
Next n
Next m
End If
BlockToList = arrOut
End Function
'Utility method for populating an array to a range
Sub ArrayToCell(arr, rngDest As Range)
rngDest.Cells(1).Resize(UBound(arr, 1), UBound(arr, 2)).Value = arr
End Sub
Here's how I'd approach that:
Sub Tester()
Const PLT_ROWS As Long = 8
Const PLT_COLS As Long = 12
Dim rng As Range, arr, rngOut As Range
Dim Poolws As Worksheet
Dim Combows As Worksheet
Dim plates As Range
Set Poolws = ThisWorkbook.Sheets("Pools")
Set Combows = ThisWorkbook.Sheets("Combined Plates")
Set rng = Combows.Range("C3").Resize(PLT_ROWS, PLT_COLS)
Set rngOut = Poolws.Range("F2")
Do While Application.CountA(rng) > 0
ArrayToCell BlockToList(rng, False), rngOut 'by column or by row?
Set rng = rng.Offset(rng.Rows.Count, 0) 'next input block
Set rngOut = rngOut.Offset(0, 1) 'output next column over?
'Set rngOut = rngOut.Offset(rng.Cells.Count, 0) '...or append to previous?
Loop
End Sub
Rest of code from previous question is unchanged - in order to keep your code as modular as possible it's best to avoid special-casing your "core" methods where you can.
If you're dealing with multi-plate output files from an instrument, ideally you want to be reading directly from those files (typically after opening them in Excel so you don't need to do the parsing) with no intermediate copy/paste/consolidate steps.
I found a solution (for anyone who cares):
I added a loop that breaks the range/array/chunk into sections (in this case I know they are always 8 "tall"). I'm sure someone could have a better solution, but this one worked for me! Code as follows:
'convert a rectangular range into a 2-d single-column array
' Read by row(default) or by column (pass False as second argument)
Function BlockToList(rng As Range, plates As Range, Optional rowMajor As Boolean = True)
Dim m As Long, n As Long, o As Long, dr, dc, arrData, arrOut, platenum, i As Long
arrData = rng.Value
platenum = plates.Value
dr = UBound(arrData, 1)
dc = UBound(arrData, 2)
ReDim arrOut(1 To (dr * dc), 1 To 1)
If rowMajor Then
For m = 1 To dr
For n = 1 To dc
i = i + 1
arrOut(i, 1) = arrData(m, n)
Next n
Next m
Else
For o = 0 To platenum * 8
If ((o * 8) + 8) <= dr Then
For m = 1 To dc
' divide array by plantenum into that many arrays then on each
' run the following, pasting the results sequentially in a column
For n = ((o * 8) + 1) To ((o * 8) + 8)
i = i + 1
arrOut(i, 1) = arrData(n, m)
Next n
Next m
End If
Next o
End If
BlockToList = arrOut
End Function

How to split a content from a cell in excel

I am wondering how to take out the date part from the content and split both the code and date to separate columns. I will show you guys an example
Column A
Orient / 21/Dec / 30-12-2020
TechSol/8 / 1-1-2021
Orient / 12/Jan / 1-10-2021
AE-003 / 13-1-2021
I want to get the results like this:
B column
C column
Orient / 21/Dec
30-12-2020
TechSol/8
1-1-2021
Orient / 12/OCT
1-10-2021
AE-003
13-1-2021
the format of the combined cell is always like Code / Date, that is code is always separated from a date with <space> dash <space>. I am unable to figure out a way to separate them. When I use text to the column with character as / such dash are also present in the code. But I use fixed-width option it still doesn't work for me, as these are all different widths. using the formula =right is not working for me because the date format is not always in a fixed format, for example, 10 October will be in dd-mm-yyyy but single-digit month or day will be in the format d-m-yyyy so the character length is not also fixed.
I hope you all understood my issue. I need a formula to split these into different columns.
Please, try the next function:
Function SplitTEXT(x As String) As Variant
Dim arr, sec As String
arr = Split(x, "/ "): sec = arr(UBound(arr)) 'split and memorize he last array element (date)
arr(UBound(arr)) = "###$" & arr(UBound(arr)) 'add a unusual string to the last array element
'in order to easily and faster replace it in the next line
'Create an array from joined array elements after replacing the last one and the last (memorized) element (date):
SplitTEXT = Array(Join(Filter(arr, arr(UBound(arr)), False), "/ "), sec)
End Function
It can be tested for all your example strings in the next way:
Sub testSplitTEXT()
Dim x As String, arr
x = "Orient / 21/Dec / 30-12-2020"
'x = "TechSol/8 / 1-1-2021"
'x = "Orient / 12/Jan / 1-10-2021"
'x = "AE-003 / 13-1-2021"
arr = SplitTEXT(x)
Debug.Print arr(0), arr(1)
Range("B1:C1").value = arr
End Sub
You must only uncomment the x = ... lines...
Or, use the next way to iterate between each A:A column values and split as you requested (on B:C columns):
Sub testSplitTIteration()
Dim i As Long, sh As Worksheet, lastR As Long
Set sh = ActiveSheet
lastR = sh.Range("A" & sh.rows.count).End(xlUp).row
For i = 2 To lastR
sh.Range("B" & i & ":C" & i).value = SplitTEXT(sh.Range("A" & i).value)
Next
End Sub
Given the examples you show:
Col B: Return up to the last / in the string
Col C: Return all after the last <space> in the string
B1: =LEFT(A1,FIND(CHAR(1),SUBSTITUTE(A1,"/",CHAR(1),LEN(A1)-LEN(SUBSTITUTE(A1,"/",""))))-1)
C1: =TRIM(RIGHT(SUBSTITUTE(A1," ",REPT(" ",99)),99))
Split by the Last Occurrence
Option Explicit
Sub splitByLastOccurrence()
Const sName As String = "Sheet1"
Const sFirst As String = "A1"
Const dName As String = "Sheet1"
Const dFirst As String = "B1"
Const Delimiter As String = " / "
Dim wb As Workbook: Set wb = ThisWorkbook ' workbook containing this code
' Attempt to define (one-column) Source Range.
Dim rg As Range
Dim isRangeDefined As Boolean
With wb.Worksheets(sName).Range(sFirst)
Set rg = .Resize(.Worksheet.Rows.Count - .Row + 1) _
.Find("*", , xlFormulas, , , xlPrevious)
If Not rg Is Nothing Then
Set rg = .Resize(rg.Row - .Row + 1)
isRangeDefined = True
End If
End With
If isRangeDefined Then
' Write (one-column) Source Range to (one-column) Data Array.
Dim rCount As Long: rCount = rg.Rows.Count
Dim Data As Variant
If rCount = 1 Then
ReDim Data(1 To 1, 1 To 1): Data(1, 1) = rg.Value
Else
Data = rg.Value
End If
ReDim Preserve Data(1 To rCount, 1 To 2) ' increase by one column
Dim i As Long
Dim Pos As Long
Dim cString As String
' Write result to (two-column) Data Array.
For i = 1 To rCount
If Not IsError(Data(i, 1)) Then
cString = Data(i, 1)
Pos = InStrRev(cString, Delimiter)
If Pos > 0 Then
Data(i, 1) = Left(cString, Pos - 1)
Data(i, 2) = Right(cString, _
Len(cString) - Pos - Len(Delimiter) + 1)
End If
End If
Next i
' Write values from (two-column) Data Array
' to (two-column) Destination Range.
With wb.Worksheets(dName).Range(dFirst).Resize(, 2)
.Resize(rCount).Value = Data
.Resize(.Worksheet.Rows.Count - .Row - rCount + 1) _
.Offset(i - 1).ClearContents
End With
'Else
' No range.
End If
End Sub
Tiny variant using ReDim
For the sake of the art, I demonstrate a tiny variant to #FaneDuru 's valid answer (and can be called the same way).
This approach needs the following steps:
split the string passed as argument thus resulting in an array a with up to three elements,
remember the last element (identified via the Ubound() function) and assign it to b,
redimension array a via ReDim Preserve thus removing the last element (instead of a negative filtering),
return a function result as array comprising the joined elements of array a as well as the remembered element b.
Function SplitText(s As String) As Variant
'[0]split string
Dim a, b, ubnd As Long
a = Split(s, "/ "): ubnd = UBound(a)
b = a(ubnd)
'[1]redimension array a
ReDim Preserve a(IIf(ubnd = 1, 0, 1))
'[2]return results
SplitText = Array(Join(a, "/"), b)
End Function
I have found the answer to my problem. All I wanted to do what a reverse search to find the last / to extract the date which was variable and substitute the date to the first cell to delete that.
=IF(ISERROR(FIND(" / ",A1)),A1,RIGHT(A1,LEN(A1)-FIND("~",SUBSTITUTE(A1," ","~",LEN(A1)-LEN(SUBSTITUTE(A1," ",""))))))

Macro generated by maximum of absolute value of cells

Numeric data is streamed into cells B8, B10, B12, B14, B16 and B18 (see below).
Cell B23 is the maximum of the absolute value of the above cells at any time, so the formula in B23 is :
=MAX(ABS($B$8),ABS($B$10),ABS($B$12),ABS($B$14),ABS($B$16),ABS($B$18))
Cell B5 is a user-defined constant, in our case 13.00, and is the threshold value that will trigger one of the macros.
So, in the case below, B23 = 8.00, and because 8.00 < 13.00 no macro is called.
If, however, B5 was 7.50, then since B23 (8.00) >= 7.50, and B14 is a positive value, Macro_7 is to be called. Had B14 been -8.00, then Macro_8 is to be called.
This process is to be started when the user presses the START button, which has macro START assigned to it. Once a macro is called, the process ends till the user restarts it.
I am having trouble coding this in VBA and would appreciate any assistance.
Please try this function.
Function AbsoluteMaximum(RowNum As Long, _
Sign As Long) As Double
Dim AbsMax As Double ' variables range
Dim Tmp As Double ' temporary value
Dim R As Long ' row number
Dim i As Integer ' loop counter: iterations
R = RowNum
RowNum = 0 ' return 0 in case of failure
For i = 1 To 6 ' number of cells
Tmp = Cells(R, "B").Value
If Abs(Tmp) > AbsMax Then
AbsMax = Abs(Tmp)
Sign = Sgn(Tmp)
RowNum = R
End If
R = R + 2
Next i
AbsoluteMaximum = AbsMax
End Function
It returns 3 values: the absolute maximum, the row number where it was found and its Sign. The Sgn() function returns 1 for a positive number, -1 for a negative number and 0 for zero.
This is how you can call the function from VBA.
Sub Test_AbsMax()
Dim RowNum As Long
Dim Sign As Long
Dim AbsMax As Double
RowNum = 8 ' start row: change to suit
AbsMax = AbsoluteMaximum(RowNum, Sign)
MsgBox "Absolute Max = " & AbsMax & vbCr & _
"Sign = " & Sign & vbCr & _
"in row number " & RowNum
End Sub
You can use the Sign variable with code like
Clm = Iif(Sign < 0, 3, 1), specifying columns A or C to link to a button.
Observe that RowNum is the first row number for your variables when the function is called but changed by the function to become the row number where the maximum was found. Therefore its value is different before and after the function call.
If this number is below the threshold you would call no further macro. Else you would call a macro determined by RowNum and Sign.
Try this
Sub RunMacro()
Dim rng As Range
Dim dThreshold As Double
Dim i As Long
Dim dValue As Double
Dim dRunningMin As Double: dRunningMin = 1E+20
Dim lIndex As Long
' Change the sheet name
With ThisWorkbook.Sheets("Sheet2")
Set rng = .Range("B8:B18")
dThreshold = .Range("B5")
lIndex = 0
For i = 1 To rng.Rows.Count Step 2
dValue = rng.Cells(i, 1).Value
If Abs(dValue) >= dThreshold Then
If Abs(dValue) - dThreshold < dRunningMin Then
dRunningMin = Abs(dValue) - dThreshold
lIndex = i + IIf(dValue < 0, 1, 0)
End If
End If
Next i
If lIndex > 0 Then
Application.Run "Macro_" & lIndex
End If
End With
End Sub
The code above will work out the number whose absolute value is greater than the threshold and is nearest to it.
e.g.
Threshold Macro
13 None
7.5 Macro_7
4 Macro_3 (but not Macro_10)
3.1 Macro_6
3 Macro_11
2 Macro_1
If, however, you want to run all macros for numbers whose absolute values are greater than the threshold then you need something like this:
Sub RunMacros()
Dim rng As Range
Dim dThreshold As Double
Dim i As Long
Dim dValue As Double
' Change the sheet name
With ThisWorkbook.Sheets("Sheet2")
Set rng = .Range("B8:B18")
dThreshold = .Range("B5")
For i = 1 To rng.Rows.Count Step 2
dValue = rng.Cells(i, 1).Value
If Abs(dValue) >= dThreshold Then
Application.Run "Macro_" & i + IIf(dValue < 0, 1, 0)
End If
Next i
End With
End Sub
e.g.
Threshold Macro
13 None
7.5 Macro_7
4 Macro_3, Macro_7 and Macro_10
3.1 Macro_3, Macro_6, Macro_7, Macro_10
3 Macro_3, Macro_6, Macro_7, Macro_10, Macro_11
2 Macro_1, Macro_3, Macro_6, Macro_7, Macro_10, Macro_11

How to find the max. absolute sequential difference of two values in a given range in VBA

I have got a specific Range e.g B2-I2 (which can vary) that contains values e.g 1,2,4,5,34,4,23,12. The aim is to have a macro which finds the largest absolute difference in that given range when the function is executed. In the above example the largest abs. difference would be 30 (as 34-4).
It looks like you're wanting to find the largest sequential difference, if so, try this ...
Public Function GetLargestDifference(ByVal objCells As Range) As Double
Dim objCell As Range, i As Long, dblThisDiff As Double, arrValues()
' Put the (potentially) non sequential set of cells into a one dimensional array.
For Each objCell In objCells
ReDim Preserve arrValues(i)
arrValues(i) = objCell.Value
i = i + 1
Next
' Now process that array and check for the max difference.
For i = 0 To UBound(arrValues) - 1
dblThisDiff = arrValues(i) - arrValues(i + 1)
If dblThisDiff > GetLargestDifference Then GetLargestDifference = dblThisDiff
Next
End Function
... there's no error checking for non numeric values but you can add that as required.
If you need to do an absolute check then replace this line ...
dblThisDiff = arrValues(i) - arrValues(i + 1)
... with this ...
dblThisDiff = Abs(arrValues(i) - arrValues(i + 1))
try:
Option Explicit
Sub test()
Dim i As Long, y As Long, ValueArr As Long, ValueY As Long, MaxDiff As Long
Dim arr As Variant
With ThisWorkbook.Worksheets("Sheet1")
arr = Application.Transpose(.Range("B2:I2").Value)
For i = LBound(arr) To UBound(arr)
ValueArr = Abs(arr(i, 1))
For y = 2 To 9
ValueY = Abs(.Cells(2, y).Value)
If ValueArr - ValueY > MaxDiff Then
MaxDiff = ValueArr - ValueY
End If
Next y
Next i
MsgBox MaxDiff
End With
End Sub

Excel VBA to get Random Integer Values without repetitions

Write a subroutine in VBA to generate a winning lotto ticket consisting of 6 integer numbers randomly drawn from 1 to 40.
In order to have a small simulation animation, range("A1:E8") should contain the numbers 1 to 40 and the subroutine should then cycle through these numbers using a colored cell and then momentarily pause 2 seconds on a selected winning number. The list of winning numbers drawn should then be printed in the range("G2:G7"). In case a number drawn has already been drawn previously in the list, then a new number should be redrawn.
I have only been able to do as follows.
Option Explicit
Sub test1()
Sheet1.Cells.Clear
Dim i As Integer
For i = 1 To 40
Cells(i, 1) = i
Next
End Sub
'-----------------------------
Option Explicit
Option Base 1
Function arraydemo(r As Range)
Dim cell As Range, i As Integer, x(40, 1) As Double
i = 1
For Each cell In r
x(i, 1) = cell.Value
i = i + 1
Next cell
arraydemo = x
End Function
Sub test3()
Dim x() As String
chose = Int(Rnd * UBound(x))
End Sub
I got stuck elsewhere, the sub test3(), does not seem appropriate here. I need some suggestions. Also, I appologise for my poor formatting, I am new to this.
Populating your range like this:
range("A1:E8") should contain the numbers 1 to 40
Sheet1.Cells.Clear
Dim i As Integer
Dim rng as Range
Set rng = Range("A1:E8")
For i = 1 To 40
rng
Next
generate a winning lotto ticket consisting of 6 integer numbers randomly drawn from 1 to 40
Using a dictionary object to keep track of which items have been picked (and prevent duplicate) in a While loop (until there are 6 numbers chosen):
Dim picked as Object
Set picked = CreateObject("Scripting.Dictionary")
'Select six random numbers:
i = 1
While picked.Count < 6
num = Application.WorksheetFunction.RandBetween(1, 40)
If Not picked.Exists(num) Then
picked.Add num, i
i = i + 1
End If
Wend
Using the Application.Wait method to do the "pause", you can set up a procedure like so:
'Now, show those numbers on the sheet, highlighting each cell for 2 seconds
For Each val In picked.Keys()
rng.Cells(picked(val)).Interior.ColorIndex = 39 'Modify as needed
Application.Wait Now + TimeValue("00:00:02")
rng.Cells(picked(val)).Interior.ColorIndex = xlNone
Next
The list of winning numbers drawn should then be printed in the range("G2:G7").
Print the keys from the picked dictionary:
Range("G2:G7").Value = Application.Transpose(picked.Keys())
Putting it all together:
Sub Lotto()
Dim i As Integer, num As Integer
Dim rng As Range
Dim picked As Object 'Scripting.Dictionary
Dim val As Variant
'Populate the sheet with values 1:40 in range A1:E8
Set rng = Range("A1:E8")
For i = 1 To 40
rng.Cells(i) = i
Next
'Store which numbers have been already chosen
Set picked = CreateObject("Scripting.Dictionary")
'Select six random numbers:
i = 1
While picked.Count < 6
num = Application.WorksheetFunction.RandBetween(1, 40)
If Not picked.Exists(num) Then
picked.Add num, i
i = i + 1
End If
Wend
'Now, show those numbers on the sheet, highlighting each cell for 2 seconds
For Each val In picked.Keys()
rng.Cells(val).Interior.ColorIndex = 39 'Modify as needed
Application.Wait Now + TimeValue("00:00:02")
rng.Cells(val).Interior.ColorIndex = xlNone
Next
'Display the winning series of numbers in G2:G7
Range("G2:G7").Value = Application.Transpose(picked.Keys())
End Sub
NOTE This absolutely will not work on Excel for Mac, you would need to use a Collection instead of a Dictionary, as the Scripting.Runtime library is not available on Mac OS.
In addition to the excellent answer given by member David Zemens, following is the universal function written in "pure" Excel VBA, which does not contain any Excel Worksheet Functions, neither Dictionary Object (re: CreateObject("Scripting.Dictionary").
Option Explicit
'get N random integer numbers in the range from LB to UB, NO repetition
'general formula: Int ((UpperBound - LowerBound + 1) * Rnd + LowerBound)
Function RandomNumbers(LB As Integer, UB As Integer, N As Integer) As Variant
Dim I As Integer
Dim arrRandom() As Integer
Dim colRandom As New Collection
Dim colItem As Variant
Dim tempInt As Integer
Dim tempExists As Boolean
'check that ArraySize is less that the range of the integers
If (UB - LB + 1 >= N) Then
While colRandom.Count < N
Randomize
' get random number in interval
tempInt = Int((UB - LB + 1) * Rnd + LB)
'check if number exists in collection
tempExists = False
For Each colItem In colRandom
If (tempInt = colItem) Then
tempExists = True
Exit For
End If
Next colItem
' add to collection if not exists
If Not tempExists Then
colRandom.Add tempInt
End If
Wend
'convert collection to array
ReDim arrRandom(N - 1)
For I = 0 To N - 1
arrRandom(I) = colRandom(I + 1)
Next I
'return array of random numbers
RandomNumbers = arrRandom
Else
RandomNumbers = Nothing
End If
End Function
'get 5 Random numbers in the ranger 1...10 and populate Worksheet
Sub GetRandomArray()
Dim arr() As Integer
'get array of 5 Random numbers in the ranger 1...10
arr = RandomNumbers(1, 10, 5)
'populate Worksheet Range with 5 random numbers from array
If (IsArray(arr)) Then
Range("A1:A5").Value = Application.Transpose(arr)
End If
End Sub
The function
Function RandomNumbers(LB As Integer, UB As Integer, N As Integer)
returns array of N random numbers in the range LB...UB inclusively without repetition.
Sample Sub GetRandomArray() demonstrates how to get 5 random numbers in the range 1...10 and populate the Worksheet Range: it could be customized for any particular requirements (e.g. 8 from 1...40 in PO requirements).
APPENDIX A (Courtesy of David Ziemens)
Alternatively, you can do similar without relying on Collection object at all. Build a delimited string, and then use the Split function to cast the string to an array, and return that to the calling procedure.
This actually returns the numbers as String, but that shouldn't matter for this particular use-case, and if it does, can easily be modified.
Option Explicit
Sub foo()
Dim arr As Variant
arr = RandomNumbersNoCollection(1, 40, 6)
End Sub
'get N random integer numbers in the range from LB to UB, NO repetition
'general formula: Int ((UpperBound - LowerBound + 1) * Rnd + LowerBound)
Function RandomNumbersNoCollection(LB As Integer, UB As Integer, N As Integer)
Dim I As Integer
Dim numbers As String ' delimited string
Dim tempInt As Integer
Const dlmt As String = "|"
'check that ArraySize is less that the range of the integers
If (UB - LB + 1 >= N) Then
' get random number in interval
Do
Randomize
tempInt = Int((UB - LB + 1) * Rnd + LB)
If Len(numbers) = 0 Then
numbers = tempInt & dlmt
ElseIf InStr(1, numbers, tempInt & dlmt) = 0 Then
numbers = numbers & tempInt & dlmt
End If
Loop Until UBound(Split(numbers, dlmt)) = 6
numbers = Left(numbers, Len(numbers) - 1)
End If
RandomNumbersNoCollection = Split(numbers, dlmt)
End Function

Resources