You can find the problem here.
The example:
Column A, B, and C are each 3 and together equal 9:
| A | B | C |...| Total |
+----+-----+----+...+-------+
| 3 | 3 | 3 |...| 9 |
Columns A and B are edited to equal 2 each, but I still want to maintain the total of 9, so I want column C to automatically change to 5:
| A | B | C |...| Total |
+----+-----+----+...+-------+
| 2 | 2 | 3 |...| 7 |
I realized that my code was a bit of a mess, so I broke it into sheet1, module main and a class named CollectionOfGeneratedValues
.
The only variables you have to adjust in your code are the
masterRange
, Columns inrangeToFill
and a column insumTarget
to suit your data input.
Quick runthrough:
You have to set the
masterRange
, or the range that you are working with, inside VBA. Inside the spreadsheet you must set thesumtarget
for each row of themasterRange
.When a value is entered into a cell inside of your
masterRange
, we find out what row this is and generate a separate range that is just that row.If the input amount is greater than the
sumTarget
weExit Sub
and scold user.We generate an array of values whose sum, along with user input will be the
sumtarget
. We then take the sum target and subtract the user input.- Afterwards generate a random number between 0 and the new
sumtarget.value
- We then store that rand number and subtract its value from
sumtarget
. - We do this
columnsInRange
- 1 times. - When we step out of the
for loop
for the last value we set the value to whatever is leftover ofsumtarget
. - With the collection that was created by the steps above we perform a
Fisher-Yates Shuffle
, so that we don't always the values of collection / our spreadsheet come in a descending order
- Afterwards generate a random number between 0 and the new
Sheet 1:
Option Explicit
Private Sub Worksheet_Change(ByVal target As Range)
Main.Main target
End Sub
Module Main
:
Option Explicit
Sub Main(ByRef target As Range)
Dim masterRange As Range
Dim rangeToFill As Range
Dim valuesToFillRange As CollectionOfGeneratedValues
Application.EnableEvents = False
Set masterRange = Range("B1:E5")
Set valuesToFillRange = New CollectionOfGeneratedValues
If Not Intersect(masterRange, target) Is Nothing Then
If checkUserInputValid(target) Then Exit Sub
valuesToFillRange.generateValues target
valuesToFillRange.shuffleCollection
Call printValues(valuesToFillRange, target)
End If
Application.EnableEvents = True
End Sub
Function checkUserInputValid(ByRef userInput As Range) As Boolean
checkUserInputValid = False
If userInput.value > getSumTarget(userInput) Then
MsgBox ("WILL NOT CALCULATE FOR ROW " & userInput.Row & ", USER INPUT GREATER THEN SUMTARGET")
checkUserInputValid = True
Application.EnableEvents = True
End If
End Function
Function getSumTarget(ByRef userInput As Range) As Long
getSumTarget = Range("F" & userInput.Row)
End Function
Function printValues(ByRef valuesToFillRange As CollectionOfGeneratedValues, ByRef userInput As Range)
Dim rangeToFill As Range
Dim collectionCounter As Long
Dim cellInRangeToFill As Range
Set rangeToFill = Range("A" & userInput.Row & ":E" & userInput.Row)
collectionCounter = 1
For Each cellInRangeToFill In rangeToFill
If cellInRangeToFill.Address <> userInput.Address Then
cellInRangeToFill.value = valuesToFillRange(collectionCounter)
collectionCounter = collectionCounter + 1
End If
Next cellInRangeToFill
End Function
Class named CollectionOfGeneratedValues
:
Option Explicit
Private CollectionOfGeneratedValues As Collection
Private Sub Class_Initialize()
Set CollectionOfGeneratedValues = New Collection
End Sub
Private Sub Class_Terminate()
Set CollectionOfGeneratedValues = Nothing
End Sub
Private Property Get NewEnum() As IUnknown
Set NewEnum = CollectionOfGeneratedValues.[_NewEnum]
End Property
Friend Property Get Count() As Long
Count = CollectionOfGeneratedValues.Count
End Property
Friend Sub Add(num As Long)
CollectionOfGeneratedValues.Add num
End Sub
Public Property Get Item(Index As Variant) As Long
Item = CollectionOfGeneratedValues.Item(Index)
End Property
Public Sub Clear()
Set CollectionOfGeneratedValues = New Collection
End Sub
Public Sub shuffleCollection()
Dim holdValuesArray As Collection
Set holdValuesArray = generateColOfValues()
Call swap(holdValuesArray)
End Sub
Private Function generateColOfValues() As Collection
Dim counter As Long
Dim maxNum As Long
Set generateColOfValues = New Collection
maxNum = Me.Count
For counter = 1 To maxNum
generateColOfValues.Add Me.Item(counter)
Next counter
End Function
Private Sub swap(ByRef holdValuesArray As Collection)
Dim randomNum As Long
Dim maxNum As Long
Dim counter As Long
Me.Clear
maxNum = holdValuesArray.Count
For counter = 1 To maxNum
randomNum = Application.WorksheetFunction.RandBetween(1, holdValuesArray.Count)
Me.Add (holdValuesArray(randomNum))
holdValuesArray.Remove (randomNum)
Next counter
End Sub
Public Sub generateValues(ByRef userInput As Range)
Dim userSetValue As Long
Dim sumTarget As Long
Dim sumLeft As Long
Dim numbersToGenerate As Long
userSetValue = userInput.value
sumTarget = getSumTarget(userInput)
sumLeft = setInitialSumLeft(sumTarget, userSetValue)
numbersToGenerate = getNumbersToGenerate(userInput)
Call getValues(numbersToGenerate, sumLeft)
End Sub
Private Function getSumTarget(ByRef userInput As Range) As Long
getSumTarget = Range("F" & userInput.Row)
End Function
Private Function setInitialSumLeft(ByVal sumTarget As Long, ByVal userSetValue As Long) As Long
setInitialSumLeft = sumTarget - userSetValue
End Function
Private Function getNumbersToGenerate(ByRef userInput As Range) As Long
Dim rangeToFill As Range
Set rangeToFill = Range("A" & userInput.Row & ":E" & userInput.Row)
getNumbersToGenerate = rangeToFill.Columns.Count - 1
End Function
Private Sub getValues(ByVal numbersToGenerate As Long, ByVal sumLeft As Long)
Dim counter As Long
Dim value As Long
For counter = 1 To numbersToGenerate - 1
value = Application.WorksheetFunction.RandBetween(0, sumLeft / 1.25)
Me.Add value
sumLeft = sumLeft - value
Next counter
Me.Add sumLeft
End Sub