Randomly distribute a range values to a bigger range - vba

I'm building an automatic calendar project.
Input information is a task list for the month/year and an idea is to distribute exact values from the input list randomly over the output range.
I thought using a recursive vba function
Sub distributeRandomly()
Dim InRng as Range
Dim OutRng as Range
Dim distributePercent as Single
Set InRng=Application.InputBox("Select input range",Type:=8)
Set OutRng=Application.Input("Select output range",Type:=8)
distributePercent=InRng.count/OutRng.count
distributeRandomlyRec(InRng,OutRng,distributePercent)
End Sub
Function distributeRandomlyRec(InRng As Range,OutRng as range,distributePercent as Single)
Dim i1 As Integer
Dim i2 As Integer
if OutRng.count=0 or InRng.count=0 Then
Exit Function
Else
Randomize
If distributePercent <= Rnd Then
Randomize
i1=int(OutRng.count*Rnd+1)
Randomize
i2=int(InRng.count*Rnd+1)
OutRng.Cells(i1).value=InRng.Cells(i2).value
##Here i stacked to define a new ranges without the choosen cells i1 and i2
##Maybe convert old range to array , delete a value and then
##Transpose() an array to a new range ?
##NewOutRng = ?
##NewInRng = ?
if NewOutRng.count=0 or NewInRng.count=0 Then
Exit Function
else
distributePercent=NewInRng.count/NewOutRng.count
distributeRandomlyRec(NewInRng,NewOutRnge,distributePercent)
End If
End Function

fixed the code - no recursive version , tested
Sub distributeRandomly()
Dim InRng As Range
Dim OutRng As Range
Dim inCollection As New Collection
Dim outCollection As New Collection
Dim i1 As Integer
Dim i2 As Integer
Dim distributePercent As Single
Set InRng=Application.InputBox("Select input range",Type:=8)
Set OutRng=Application.InputBox("Select output range",Type:=8)
Dim c As Range
For Each c In InRng
inCollection.Add c.Value
Next
While inCollection.Count > 0
If (OutRng.Count - outCollection.Count) > 0 Then
Randomize
distributePercent = inCollection.Count / (OutRng.Count - outCollection.Count)
If distributePercent >= Rnd Then
Randomize
i1 = Int(inCollection.Count * Rnd + 1)
outCollection.Add inCollection(i1)
inCollection.Remove (i1)
Else
outCollection.Add (vbNullString)
End If
End If
Wend
For i2 = 1 To outCollection.Count
OutRng.Cells(1, i2).Value = outCollection(i2)
Next
End Sub

Related

Summing up two arrays or matrices element by element

I have two variables defined like this:
Per_Mnd = Worksheets("Sheet1").Range("G2:G8").Value
Per_Mnd2 = Worksheets("Sheet2").Range("G2:G8").Value
Obviously both Per_Mnd and Per_Mnd2 have 7 rows and 1 column. Now I want to sum them up element by element, getting another 7×1 array. How do I do it?
And what is they are defined by matrix
Per_Mnd = Worksheets("Sheet1").Range("G2:H8").Value
Per_Mnd2 = Worksheets("Sheet2").Range("G2:H8").Value
How can I quickly sum them up element by element?
thanks!
You can simply evaluate an INDEX formula to return the array:
Sub Test()
Dim oarr As Variant
Dim Per_Mnd As Variant
Dim Per_Mnd2 As Variant
Per_Mnd = Worksheets("Sheet1").Range("G2:G8").Value
Per_Mnd2 = Worksheets("Sheet2").Range("G2:G8").Value
With Application
oarr = .Transpose(.Evaluate("INDEX({" & Join(.Transpose(Per_Mnd), ",") & "}+{" & Join(.Transpose(Per_Mnd2), ",") & "},)"))
End With
Debug.Print oarr(3, 1)
End Sub
Note: this only works with single column arrays of the same size.
If you want to sum a matrix, the VBA way is WorksheetFunction.SumProduct
Take the example below, returning 60 -> 1*10+2*10+3*10
Public Sub TestMe()
Dim el1 As Variant
Dim el2 As Variant
Dim res As Variant
el1 = Application.Transpose(Range("A1:A3"))
el2 = Application.Transpose(Range("B1:B3"))
Debug.Print WorksheetFunction.SumProduct(el1, el2)
ReDim res(1 To UBound(el1))
Dim cnt As Long
For cnt = LBound(el1) To UBound(el1)
res(cnt) = el1(cnt) + el2(cnt)
Next cnt
End Sub
The idea of Application.Transpose() is to present the Range() as a one dimensional array. Once we do so, we introduce res(1 to UBound(el1) where we write the product per element. Or you can even do the SumArray as a function, returning the new array:
Public Function SumArray(arr1 As Variant, arr2 As Variant) As Variant
ReDim res(1 To UBound(arr1))
Dim cnt As Long
For cnt = LBound(arr1) To UBound(arr1)
res(cnt) = arr1(cnt) + arr2(cnt)
Next cnt
SumArray = res
End Function

Pass array function into user defined function

I have a standard user defined function that concationates all the unique values. What I am trying to do is to perform this function on a range that satisfies a condition.
Function ConcatUniq(xRg As Range, xChar As String) As String
'updateby Extendoffice 20151228
Dim xCell As Range
Dim xDic As Object
Set xDic = CreateObject("Scripting.Dictionary")
For Each xCell In xRg
xDic(xCell.Value) = Empty
Next
ConcatUniq = Join$(xDic.Keys, xChar)
Set xDic = Nothing
End Function
Lets make an example:
If we have the following data:
A1:A5 = {1,2,2,4,1}
B1:B5 = {"group1", "group1","group1", "group2", "group2"}
C1 = "group1"
Now I want to find the unique values using the ConcatUniq function for all numbers that are in group1. Usually, if I want to perform another function for example the median I would do the following:
=MEDIAN(IF(B1:B5=C1,A1:A5))
Activate it using cntrl shift enter which gives 2 (create an array function from it).
For some reasons this does not work in combination with a user defined function.
=ConcatUniq(IF(B1:B5=C1,A1:A5)," ")
Desired result:
1 2
Does someone know how I could fix this problem?
You need to use ParamArray to accommodate array returned from Excel's array formula. As ParamArray should always be the last one, so your method signature will change.
This will work with =ConcatUniq(" ",IF(B1:B5=C1,A1:A5)) on CTRL + SHIFT + ENTER
Public Function ConcatUniq(xChar As String, ParamArray args())
Dim xDic As Object
Dim xVal
Set xDic = CreateObject("Scripting.Dictionary")
For Each xVal In args(0)
If Not Not xVal Then
xDic(xVal) = Empty
End If
Next
ConcatUniq = Join$(xDic.Keys, xChar)
End Function
Perhaps something like this:
Public Function ConcatUniq(ByVal rangeOrArray As Variant, ByVal xChar As String) As String
Dim generalArray As Variant
If IsArray(rangeOrArray) Then
'operate on it as if was an array
generalArray = rangeOrArray
Else
If TypeName(rangeOrArray) = "Range" Then
'operate on it as if was a Range
If rangeOrArray.Cells.Count > 1 Then
generalArray = rangeOrArray.Value
Else
generalArray = Array(rangeOrArray.Value)
End If
Else
'Try to process as if it was a derivative of a value of a single cell range.....
generalArray = Array(rangeOrArray)
End If
End If
Dim xDic As Object
Set xDic = CreateObject("Scripting.Dictionary")
Dim xCell As Variant
For Each xCell In generalArray
If xCell <> False Then xDic(xCell) = Empty ' EDIT - HACKY....
Next
ConcatUniq = Join$(xDic.Keys, xChar)
End Function
You can see that that whole block of if-elses can be factored out to be a separate function to transform worksheet input to a unified form for operating on values of a worksheet.
The easiest solution would probably be to introduce an additional function. This function would take care of the condition and would generate an array consisting only of data fulfilling the condition.
Try something like this:
function condition_check(data1() as integer, data2() as string, condition_value as string) as integer
number_of_elements = Ubound(data1)
j = 0
for i = 0 to number_of_elements
if data2(i) = condition_value then
condition_check(j) = data1(i)
j = j+1
end if
next i
end function

Using Worksheet Functions In A Macro

I have an array of integers in VBA from which I would like to get the upper and lower quartiles.
I would like to use this method to get them: https://msdn.microsoft.com/en-us/library/office/ff836118.aspx
The documentation suggests you can use an array to do this, but when I try to run my code (below) I get an error saying Unable to get the Quartile property of the WorksheetFunction class
Please assist.
Dim totalsalesthatday() As String
Dim doINeedTo As Boolean
Dim totalsalesthatdayAverage As Integer
Dim randomnumberthingy As Integer
Dim quartile1 As Integer
Dim quartile3 As Integer
Dim iqr As Integer
Dim upper As Integer
Dim lower As Integer
quantity = 0
For Each queryaddress In worksheetname.Range("A2:A21")
query = queryaddress.Value
offsetnum = 0
If offsetnum = 0 Then
doINeedTo = True
End If
For Each daysoftheweek In Sheets
quantity = 0
If InStr(1, daysoftheweek.Name, worksheetnamename, vbTextCompare) > 0 And daysoftheweek.ListObjects.Count > 0 Then
Set itemaddress = daysoftheweek.Columns(5).Find(query, , xlValues, xlWhole)
If Not itemaddress Is Nothing Then
firstAddress = itemaddress.Address
Do
Set itemrow = itemaddress.EntireRow
quantity = quantity + itemrow.Columns(6).Value
Set itemaddress = daysoftheweek.Columns(5).FindNext(itemaddress)
Loop While Not itemaddress Is Nothing And itemaddress.Address <> firstAddress
End If
offsetnum = offsetnum + 1
ReDim Preserve totalsalesthatday(offsetnum)
totalsalesthatday(offsetnum) = daysoftheweek.ListObjects.Item(1).ListRows.Count
queryaddress.Offset(0, offsetnum).Value = quantity
worksheetname.Range("A1").Offset(0, offsetnum).Value = daysoftheweek.Name
End If
Next
If doINeedTo Then
quartile1 = WorksheetFunction.Quartile(totalsalesthatday, 1)
quartile3 = WorksheetFunction.Quartile_Inc(totalsalesthatday, 3)
iqr = quartile3 - quartile1
upper = quartile3 + (iqr * 1.5)
lower = quartile1 - (iqr * 1.5)
The error in question is at this line: quartile1 = WorksheetFunction.Quartile(totalsalesthatday, 1)
The .Quartile function parameters are an array and a double. Try changing your data types.

Non-repeating random number generator?

I created a trivia game using visual basic for applications (Excel) that chooses questions by going through a case statement where the cases are numbers. I have the program randomly select a number from 1 to the max amount of questions there are. Using this method, the game repeats questions.
Is there a way to make something that generates numbers randomly (different results every time) and doesn't repeat a number more than once? And after it's gone through all the numbers it needs to execute a certain code. (I'll put in code that ends the game and displays the number of questions they got right and got wrong)
I thought of a few different ways to do this, however I couldn't even begin to think of what the syntax might be.
Sounds like you need an Array Shuffler!
Check out the below link -
http://www.cpearson.com/excel/ShuffleArray.aspx
Function ShuffleArray(InArray() As Variant) As Variant()
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' ShuffleArray
' This function returns the values of InArray in random order. The original
' InArray is not modified.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Dim N As Long
Dim Temp As Variant
Dim J As Long
Dim Arr() As Variant
Randomize
L = UBound(InArray) - LBound(InArray) + 1
ReDim Arr(LBound(InArray) To UBound(InArray))
For N = LBound(InArray) To UBound(InArray)
Arr(N) = InArray(N)
Next N
For N = LBound(InArray) To UBound(InArray)
J = CLng(((UBound(InArray) - N) * Rnd) + N)
Temp = InArray(N)
InArray(N) = InArray(J)
InArray(J) = Temp
Next N
ShuffleArray = Arr
End Function
Sub ShuffleArrayInPlace(InArray() As Variant)
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' ShuffleArrayInPlace
' This shuffles InArray to random order, randomized in place.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Dim N As Long
Dim Temp As Variant
Dim J As Long
Randomize
For N = LBound(InArray) To UBound(InArray)
J = CLng(((UBound(InArray) - N) * Rnd) + N)
If N <> J Then
Temp = InArray(N)
InArray(N) = InArray(J)
InArray(J) = Temp
End If
Next N
End Sub
Here's yet another take. It generates an array of unique, random longs.
In this example, I use 1 to 100. It does this by using the collection object. Then you can just do a normal loop through each array element in qArray without the need to randomize more than once.
Sub test()
Dim qArray() As Long
ReDim qArray(1 To 100)
qArray() = RandomQuestionArray
'loop through your questions
End Sub
Function RandomQuestionArray()
Dim i As Long, n As Long
Dim numArray(1 To 100) As Long
Dim numCollection As New Collection
With numCollection
For i = 1 To 100
.Add i
Next
For i = 1 To 100
n = Rnd * (.Count - 1) + 1
numArray(i) = numCollection(n)
.Remove n
Next
End With
RandomQuestionArray = numArray()
End Function
I see you have an answer, I was working on this but lost my internet connection. Anyway here is another method.
'// Builds a question bank (make it a hidden sheet)
Sub ResetQuestions()
Const lTotalQuestions As Long = 300 '// Total number of questions.
With Range("A1")
.Value = 1
.AutoFill Destination:=Range("A1").Resize(lTotalQuestions), Type:=xlFillSeries
End With
End Sub
'// Gets a random question number and removes it from the bank
Function GetQuestionNumber()
Dim lCount As Long
lCount = Cells(Rows.Count, 1).End(xlUp).Row
GetQuestionNumber = Cells(Int(lCount * Rnd + 1), 1).Value
Cells(lRandom, 1).Delete
End Function
Sub Test()
Msgbox (GetQuestionNumber)
End Sub
For whatever it's worth here is my stab at this question. This one uses a boolean function instead of numerical arrays. It's very simple yet very fast. The advantage of it, which I'm not saying is perfect, is an effective solution for numbers in a long range because you only ever check the numbers you have already picked and saved and don't need a potentially large array to hold the values you have rejected so it won't cause memory problems because of the size of the array.
Sub UniqueRandomGenerator()
Dim N As Long, MaxNum As Long, MinNum As Long, Rand As Long, i As Long
MinNum = 1 'Put the input of minimum number here
MaxNum = 100 'Put the input of maximum number here
N = MaxNum - MinNum + 1
ReDim Unique(1 To N, 1 To 1)
For i = 1 To N
Randomize 'I put this inside the loop to make sure of generating "good" random numbers
Do
Rand = Int(MinNum + N * Rnd)
If IsUnique(Rand, Unique) Then Unique(i, 1) = Rand: Exit Do
Loop
Next
Sheet1.[A1].Resize(N) = Unique
End Sub
Function IsUnique(Num As Long, Data As Variant) As Boolean
Dim iFind As Long
On Error GoTo Unique
iFind = Application.WorksheetFunction.Match(Num, Data, 0)
If iFind > 0 Then IsUnique = False: Exit Function
Unique:
IsUnique = True
End Function

How to export to excel with specified month

Sub Initialize
'Copyright Botstation (www.botstation.com)
Dim session As New NotesSession
Dim wks As New NotesUIWorkspace
Dim db As NotesDatabase
Dim view As NotesView
Dim uiView As NotesUIView
Dim doc As NotesDocument
Dim column As NotesViewColumn
Dim row As Long,colcounter As Long,arrcnt As Long,arrcounter As Long, x As Long
Dim filename As String, currentvalue As String
Dim rowsatonce As Integer,cn As Integer
Dim xlApp As Variant, xlsheet As Variant,xlwb As Variant, xlrange As Variant, tempval As Variant
Dim DataArray
Dim VColumns List As String
ReDim DataArray(0, 80) As String
'80 columns is our expected max number of columns in the view. It's dynamically recomputed below to actual (lower) number. Change if the number of columns is larger.
Set db=session.CurrentDatabase
Set xlApp = CreateObject("Excel.Application")
xlApp.Visible = True 'Excel program is visible (to avoid errors and see what is happening)
Set xlwb=xlApp.Workbooks.Add
Set xlsheet =xlwb.Worksheets(1)
Set uiView = wks.CurrentView
Set view = db.GetView( uiView.ViewName ) ' get the view currently open in UI
arrcnt=0
row=1
colcounter=0
rowsatonce=20
ForAll c In view.Columns
If c.isIcon<>True Then ' do not include icon columns
If c.Formula<>"""1""" And c.Formula<>"1" Then 'do not include columns which are used for counting docs (Total)
colcounter=colcounter+1
DataArray(row-1, colcounter-1) =c.Title
VColumns(CStr(cn))=CStr(cn)
End If
End If
cn=cn+1
End ForAll
ReDim Preserve DataArray(0, colcounter-1) As String
xlsheet.Range("A1").Resize(1, colcounter).Value = DataArray ' set column names
ReDim DataArray(rowsatonce-1, colcounter-1) As String
row=2
x=0
Set doc = view.GetFirstDocument
While Not ( doc Is Nothing )
ForAll col In VColumns
currentvalue=""
tempval= doc.ColumnValues(Val(col))
If IsArray(tempval) Then
ForAll v In tempval
If currentvalue="" Then
currentvalue=v
Else
currentvalue=currentvalue+","+v
End If
End ForAll
Else
currentvalue=tempval
End If
x=x+1
DataArray(arrcounter, x-1) =currentvalue
End ForAll
x=0
row=row+1
arrcounter=arrcounter+1
If arrcounter/rowsatonce=arrcounter\rowsatonce And arrcounter<>0 Then
xlsheet.Range("A"+Cstr(arrcnt*rowsatonce+2)).Resize(rowsatonce, colcounter).Value = DataArray
arrcnt=arrcnt+1
arrcounter=0
ReDim DataArray(rowsatonce-1, colcounter-1) As String
End If
Set doc = view.GetNextDocument (doc)
Wend
If arrcounter/rowsatonce<>arrcounter\rowsatonce And arrcounter>0 Then
' Redim Preserve DataArray(arrcounter, colcounter-1) As String
xlsheet.Range("A"+Cstr(arrcnt*rowsatonce+2)).Resize(arrcounter, colcounter).Value = DataArray
End If
MsgBox "Done"
End Sub
Once you have got the month that you want to include you can add a condition after this line:
While Not ( doc Is Nothing )
Compare the month (and probably year) with the (date) item on the document. You might need the NotesDateTime class to do this.
To filter the right month you can do this: (assuming you also need the year)
If year(date1) * 100 + month(date1) = year(date2) * 100 + month(date3)